feat(遊戲化): 新增大廳、附身、時間流、後日談進度等遊戲化系統功能

新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、
時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙
測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 09:10:26 +08:00
co-authored by Claude Sonnet 5
parent 127a59dafd
commit 1bea06e3cd
195 changed files with 17219 additions and 738 deletions
+12 -10
View File
@@ -72,16 +72,18 @@ export default async function smokeI() {
}
// I-4 破例規則:睡眠時段一般不回應,且不應無條件破例
const sleepSessionA = "smoke-i-sleep-a-" + Date.now();
const noExceptionResult = await post(`/chat/${CHARACTER_ID}`, {
// T38:每個獨立情境各自用一間 SOLO 房並在檢查完後結束,對應舊版「各自獨立 sessionId」的語意
// (避免工作記憶互相污染;房間結束後下一個情境會取得全新的房間)。
const sleepRoomA = await post("/room/solo", { userId: USER_ID, characterId: CHARACTER_ID });
const noExceptionResult = await post(`/room/${sleepRoomA.id}/message`, {
userId: USER_ID,
sessionId: sleepSessionA,
text: "在嗎",
now: WEEKDAY_SLEEP_TIME,
});
if (noExceptionResult.text !== "" || noExceptionResult.availability !== "NO_REPLY") {
throw new Error("條件不足時睡眠時段不應破例回應");
}
await post(`/room/${sleepRoomA.id}/end`, {});
// 條件滿足(親密度高+使用者情緒低落)時應觸發「深夜還陪你聊」破例
await prisma.relationship.update({
@@ -92,22 +94,21 @@ export default async function smokeI() {
where: { characterId: CHARACTER_ID },
data: { calm: 100, joy: 0, sad: 0, alert: 0, shy: 0, grumpy: 0, updatedAt: new Date(WEEKDAY_SLEEP_TIME) },
});
const sleepSessionB = "smoke-i-sleep-b-" + Date.now();
const exceptionResult = await post(`/chat/${CHARACTER_ID}`, {
const sleepRoomB = await post("/room/solo", { userId: USER_ID, characterId: CHARACTER_ID });
const exceptionResult = await post(`/room/${sleepRoomB.id}/message`, {
userId: USER_ID,
sessionId: sleepSessionB,
text: "我今天過得好難過,很失望",
now: WEEKDAY_SLEEP_TIME,
});
if (exceptionResult.exceptionType !== "LATE_NIGHT_CHAT" || !exceptionResult.text) {
throw new Error("條件滿足時應觸發「深夜還陪你聊」破例並正常生成回應");
}
await post(`/room/${sleepRoomB.id}/end`, {});
// 冷卻期內同類破例不應再次觸發
const sleepSessionC = "smoke-i-sleep-c-" + Date.now();
const cooldownResult = await post(`/chat/${CHARACTER_ID}`, {
const sleepRoomC = await post("/room/solo", { userId: USER_ID, characterId: CHARACTER_ID });
const cooldownResult = await post(`/room/${sleepRoomC.id}/message`, {
userId: USER_ID,
sessionId: sleepSessionC,
text: "我今天過得好難過,很失望",
now: WEEKDAY_SLEEP_TIME,
});
@@ -157,6 +158,7 @@ export default async function smokeI() {
throw new Error("同一天不應重複生成離線事件");
}
// 清理本次測試建立的角色(cascade 會一併清掉作息表、破例紀錄、記憶等)
// 清理本次測試建立的角色(cascade 會一併清掉作息表、破例紀錄、記憶等)與本次建立的三間 SOLO 房。
await prisma.room.deleteMany({ where: { id: { in: [sleepRoomA.id, sleepRoomB.id, sleepRoomC.id] } } });
await prisma.character.deleteMany({ where: { id: CHARACTER_ID } });
}