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
+9 -1
View File
@@ -1,9 +1,17 @@
import { PrismaClient } from "../generated/client.js";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
export function createPrismaClient(): InstanceType<typeof PrismaClient> {
export interface CreatePrismaClientOptions {
// T33:供測試量測查詢次數用(驗證批次查詢不隨資料量線性成長),一般呼叫端不需要帶這個選項。
logQueries?: boolean;
}
export function createPrismaClient(options: CreatePrismaClientOptions = {}): InstanceType<typeof PrismaClient> {
const url = process.env.DATABASE_URL ?? "file:./prisma/dev.db";
const adapter = new PrismaBetterSqlite3({ url });
if (options.logQueries) {
return new PrismaClient({ adapter, log: [{ emit: "event", level: "query" }] });
}
return new PrismaClient({ adapter });
}
@@ -0,0 +1,56 @@
import type { CharacterBuildStatus } from "./character.js";
// 大廳呈現狀態:純推導型別,不落地為獨立欄位,避免與來源欄位不同步。
export type LobbyPresenceState =
| "BUILDING" // 建置中:CharacterBuildStatus = CANDIDATE
| "SLEEPING" // 睡眠:ScheduleSlotStatus = SLEEP
| "BUSY" // 忙碌:ScheduleSlotStatus = BUSY
| "SEMI_BUSY" // 半忙碌:ScheduleSlotStatus = SEMI_BUSY
| "IDLE" // 空閒:ScheduleSlotStatus = FREE 且未在任何非大廳房間
| "IN_SOLO_ROOM" // 在 SOLO 房:所在房間 Room.mode = SOLO
| "IN_GROUP_ROOM"; // 在 GROUP 房:所在房間 Room.mode = GROUP
// 角色目前所在「非大廳」房間的房型;恆在場的大廳(Room.mode = LOBBY)不計入本推導。
export type LobbyPresenceRoomMode = "SOLO" | "GROUP" | null;
export type LobbyPresenceScheduleSlotStatus = "SLEEP" | "BUSY" | "SEMI_BUSY" | "FREE";
export interface LobbyPresenceInput {
buildStatus: CharacterBuildStatus;
scheduleSlotStatus: LobbyPresenceScheduleSlotStatus;
roomMode: LobbyPresenceRoomMode;
}
/**
* 狀態機(優先序由上到下,命中即回傳;轉移條件即為對應來源欄位變化,見 prisma/schema.prisma:18 的
* CharacterBuildStatus 與 prisma/schema.prisma:87 的 ScheduleSlotStatus):
*
* | 狀態 | 條件(資料來源) |
* | --- | --- |
* | BUILDING | buildStatus === "CANDIDATE" |
* | IN_SOLO_ROOM | roomMode === "SOLO" |
* | IN_GROUP_ROOM | roomMode === "GROUP" |
* | SLEEPING | scheduleSlotStatus === "SLEEP" |
* | BUSY | scheduleSlotStatus === "BUSY" |
* | SEMI_BUSY | scheduleSlotStatus === "SEMI_BUSY" |
* | IDLE | scheduleSlotStatus === "FREE" |
*
* 建置中與在房狀態優先於行程狀態:候補角色不顯示行程狀態;入房角色以房間狀態覆蓋行程呈現
* (行程仍照常流動供自走判斷使用,僅呈現狀態以房間為準)。日後談(Work.inEpilogue)僅為
* 附加標記、不限制呼叫,故不在此列舉。四個輸入欄位的笛卡兒積皆有對應狀態,不存在「無法判定」的組合。
*/
export function deriveLobbyPresenceState(input: LobbyPresenceInput): LobbyPresenceState {
if (input.buildStatus === "CANDIDATE") return "BUILDING";
if (input.roomMode === "SOLO") return "IN_SOLO_ROOM";
if (input.roomMode === "GROUP") return "IN_GROUP_ROOM";
switch (input.scheduleSlotStatus) {
case "SLEEP":
return "SLEEPING";
case "BUSY":
return "BUSY";
case "SEMI_BUSY":
return "SEMI_BUSY";
case "FREE":
return "IDLE";
}
}
+1
View File
@@ -4,3 +4,4 @@ export * from "./domain/character.js";
export * from "./domain/memory.js";
export * from "./domain/emotion.js";
export * from "./domain/relationship.js";
export * from "./domain/lobby-presence.js";