新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
checkAdultContent,
|
|
fetchRomanceEligibility,
|
|
setAdultModeConsent,
|
|
type AdultContentCheckResult,
|
|
type RomanceEligibility,
|
|
} from "../../lib/api";
|
|
|
|
const CHARACTER_ID = "seed-character-genki";
|
|
const USER_ID = "seed-user-primary";
|
|
|
|
// L-8 成人模式 opt-in 介面:只做開關與既有 API 串接。
|
|
// 「成年使用者驗證」的實際機制計畫未指定(需人工確認),這裡先讓 verifiedAdult 等同使用者自我宣告,
|
|
// 待正式驗證流程拍板後再另行補項——不在這裡臆測或假造一套驗證邏輯。
|
|
export function AdultModeSection() {
|
|
const [optedIn, setOptedIn] = useState(false);
|
|
const [eligibility, setEligibility] = useState<RomanceEligibility | null>(null);
|
|
const [checkResult, setCheckResult] = useState<AdultContentCheckResult | null>(null);
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function refresh() {
|
|
setError(null);
|
|
try {
|
|
const [eligibilityResult, checkResultValue] = await Promise.all([
|
|
fetchRomanceEligibility(CHARACTER_ID),
|
|
checkAdultContent(CHARACTER_ID, USER_ID),
|
|
]);
|
|
setEligibility(eligibilityResult);
|
|
setCheckResult(checkResultValue);
|
|
} catch {
|
|
setError("無法連線到 api 服務,成人模式狀態暫時無法顯示");
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void refresh();
|
|
}, []);
|
|
|
|
async function handleToggle(next: boolean) {
|
|
setPending(true);
|
|
setError(null);
|
|
try {
|
|
await setAdultModeConsent(USER_ID, next, next);
|
|
setOptedIn(next);
|
|
await refresh();
|
|
} catch {
|
|
setError("成人模式設定失敗,請確認 api 服務是否已啟動");
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section className="flex flex-col gap-3 rounded-2xl border p-4" style={{ borderColor: "var(--line)" }}>
|
|
<h2 className="text-sm font-bold" style={{ color: "var(--ink)" }}>
|
|
成人模式
|
|
</h2>
|
|
<p className="text-xs" style={{ color: "var(--ink-soft)" }}>
|
|
系統預設為全年齡純愛尺度;開啟後也只有正史成年角色能解鎖,正史未成年、後日談推演才成年的角色永久限純愛尺度。
|
|
</p>
|
|
|
|
<label className="flex items-center gap-2 text-sm" style={{ color: "var(--ink)" }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={optedIn}
|
|
disabled={pending}
|
|
onChange={(event) => void handleToggle(event.target.checked)}
|
|
/>
|
|
主動開啟成人模式(正式的成年使用者驗證機制尚待人工確認,目前僅為自我宣告已成年)
|
|
</label>
|
|
|
|
{error && (
|
|
<p className="text-xs" style={{ color: "var(--accent)" }}>
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
{eligibility && (
|
|
<div className="flex flex-col gap-1 text-xs" style={{ color: "var(--ink-soft)" }}>
|
|
<span>戀愛軸資格:{eligibility.romanceAxisEligible ? "可用" : "尚未解鎖"}</span>
|
|
<span>成人模式角色資格:{eligibility.adultModeEligible ? "符合" : "不符合"}</span>
|
|
{eligibility.isEpilogueAdult && <span>此角色為後日談推演成年,永久限純愛尺度。</span>}
|
|
</div>
|
|
)}
|
|
|
|
{checkResult && (
|
|
<p
|
|
className="rounded-lg px-3 py-2 text-xs"
|
|
style={{
|
|
background: checkResult.allowed ? "var(--chip-bg)" : "var(--bg-soft)",
|
|
color: checkResult.allowed ? "var(--accent)" : "var(--ink-soft)",
|
|
}}
|
|
>
|
|
{checkResult.allowed ? "目前可使用成人內容。" : `目前無法使用成人內容:${checkResult.reason}`}
|
|
</p>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|