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
View File
@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { CLOCK } from "./clock.js";
import { SystemClockService } from "./system-clock.service.js";
@Module({
providers: [SystemClockService, { provide: CLOCK, useExisting: SystemClockService }],
exports: [CLOCK, SystemClockService],
})
export class ClockModule {}
+7
View File
@@ -0,0 +1,7 @@
// T36:可注入時鐘介面。本計畫所有新程式一律從時鐘取時間、不直接 `new Date()`,
// 以便測試以 ControllableClock(見 controllable-clock.ts)快轉時間而不需真的等待。
export interface Clock {
now(): Date;
}
export const CLOCK = Symbol("CLOCK");
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { ControllableClock } from "./controllable-clock.js";
describe("ControllableClock", () => {
it("回報設定時刻,不隨真實時間流動", () => {
const clock = new ControllableClock("2026-08-18T00:00:00.000Z");
expect(clock.now().toISOString()).toBe("2026-08-18T00:00:00.000Z");
expect(clock.now().toISOString()).toBe("2026-08-18T00:00:00.000Z");
});
it("可快轉跨夜情境,秒級內完成", () => {
const clock = new ControllableClock("2026-08-18T23:50:00.000Z");
clock.advance(20 * 60 * 1000); // 快轉 20 分鐘跨過午夜
expect(clock.now().toISOString()).toBe("2026-08-19T00:10:00.000Z");
});
it("set() 可直接指定新時刻", () => {
const clock = new ControllableClock();
clock.set("2026-01-01T00:00:00.000Z");
expect(clock.now().toISOString()).toBe("2026-01-01T00:00:00.000Z");
});
});
+23
View File
@@ -0,0 +1,23 @@
import type { Clock } from "./clock.js";
// T36 測試專用時鐘:可任意設定與快轉當前時間,讓「跨夜→角色去睡→房間關閉」這類情境
// 在秒級內於測試中重現,不需真的等待。正式環境一律使用 SystemClockService。
export class ControllableClock implements Clock {
private current: Date;
constructor(initial: Date | string | number = new Date(0)) {
this.current = new Date(initial);
}
now(): Date {
return this.current;
}
set(date: Date | string | number): void {
this.current = new Date(date);
}
advance(ms: number): void {
this.current = new Date(this.current.getTime() + ms);
}
}
@@ -0,0 +1,9 @@
import { Injectable } from "@nestjs/common";
import type { Clock } from "./clock.js";
@Injectable()
export class SystemClockService implements Clock {
now(): Date {
return new Date();
}
}