feat(遊戲化): 新增大廳、附身、時間流、後日談進度等遊戲化系統功能
新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createRoom, listCharacters, type CharacterSummary, type RoomMode } from "../../lib/api";
|
||||
|
||||
const USER_ID = "seed-user-primary";
|
||||
|
||||
// K-7 建房介面:建立後導向 /room/[roomId](T19-P2 起的通用房間畫面,含 SSE/立繪舞台/語音/靜音,
|
||||
// 見 T19-P3),不再自己維護一份較舊、功能較少的房間顯示邏輯。
|
||||
export function RoomView() {
|
||||
const router = useRouter();
|
||||
const [characters, setCharacters] = useState<CharacterSummary[]>([]);
|
||||
const [selectedCharacterIds, setSelectedCharacterIds] = useState<string[]>([]);
|
||||
const [mode, setMode] = useState<RoomMode>("GROUP");
|
||||
const [topicSeed, setTopicSeed] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void listCharacters().then(setCharacters);
|
||||
}, []);
|
||||
|
||||
async function handleCreateRoom() {
|
||||
if (selectedCharacterIds.length === 0 || pending) return;
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const created = await createRoom(USER_ID, selectedCharacterIds, mode, topicSeed || undefined);
|
||||
router.push(`/room/${created.id}`);
|
||||
} catch {
|
||||
setError("房間建立失敗,請確認 api 服務是否已啟動");
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div 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>
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
{characters.map((character) => {
|
||||
const checked = selectedCharacterIds.includes(character.id);
|
||||
return (
|
||||
<label
|
||||
key={character.id}
|
||||
className="flex items-center gap-1 rounded-full border px-2 py-1"
|
||||
style={{ borderColor: "var(--line)", color: "var(--ink)" }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() =>
|
||||
setSelectedCharacterIds((prev) => (checked ? prev.filter((id) => id !== character.id) : [...prev, character.id]))
|
||||
}
|
||||
/>
|
||||
{character.aliases[0]?.formalName ?? character.id}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<label className="flex items-center gap-1" style={{ color: "var(--ink)" }}>
|
||||
<input type="radio" checked={mode === "GROUP"} onChange={() => setMode("GROUP")} />
|
||||
群聊
|
||||
</label>
|
||||
<label className="flex items-center gap-1" style={{ color: "var(--ink)" }}>
|
||||
<input type="radio" checked={mode === "SELF_CHAT"} onChange={() => setMode("SELF_CHAT")} />
|
||||
角色自聊(旁觀)
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
value={topicSeed}
|
||||
onChange={(event) => setTopicSeed(event.target.value)}
|
||||
placeholder="話題種子(選填)"
|
||||
className="rounded-full border bg-transparent px-3 py-1.5 text-xs"
|
||||
style={{ borderColor: "var(--line)", color: "var(--ink)" }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleCreateRoom()}
|
||||
disabled={pending || selectedCharacterIds.length === 0}
|
||||
className="self-start rounded-full px-4 py-1.5 text-xs font-bold text-white disabled:opacity-50"
|
||||
style={{ background: "var(--accent-bright)" }}
|
||||
>
|
||||
建立房間
|
||||
</button>
|
||||
{error && (
|
||||
<p className="text-xs" style={{ color: "var(--accent)" }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user