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
@@ -1,6 +1,7 @@
import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service.js";
import { AgeDeterminationService } from "./age-determination.service.js";
import { isOutsideSoloRoom } from "./room-context-guard.js";
export interface RomanceEligibility {
romanceAxisEligible: boolean; // 戀愛軸是否可啟動(含後日談自然成年者)
@@ -28,15 +29,20 @@ export class RomanceEligibilityService {
return { romanceAxisEligible: currentAdult, adultModeEligible: canonAdult, isEpilogueAdult };
}
async checkAdultContentAllowed(characterId: string, userId: string): Promise<AdultContentCheckResult> {
const result = await this.evaluate(characterId, userId);
async checkAdultContentAllowed(characterId: string, userId: string, roomId?: string): Promise<AdultContentCheckResult> {
const result = await this.evaluate(characterId, userId, roomId);
await this.prisma.client.adultContentAttemptLog.create({
data: { characterId, userId, allowed: result.allowed, reason: result.reason },
});
return result;
}
private async evaluate(characterId: string, userId: string): Promise<AdultContentCheckResult> {
private async evaluate(characterId: string, userId: string, roomId?: string): Promise<AdultContentCheckResult> {
// T42:房型已轉為 GROUP/LOBBY/SELF_CHAT 時,成人內容一律禁止——房型本身是硬邊界,
// 不受角色資格或使用者 opt-in 狀態影響,優先於下方其他判斷。
if (await isOutsideSoloRoom(this.prisma, roomId)) {
return { allowed: false, reason: "目前在多人房(GROUP/LOBBY/SELF_CHAT),成人內容僅限 1:1 房間" };
}
const eligibility = await this.getEligibility(characterId);
if (!eligibility.adultModeEligible) {
return {