新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
62 lines
3.2 KiB
TypeScript
62 lines
3.2 KiB
TypeScript
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; // 戀愛軸是否可啟動(含後日談自然成年者)
|
||
adultModeEligible: boolean; // 成人模式角色資格:僅正史成年,永久不含後日談推演成年
|
||
isEpilogueAdult: boolean; // 正史未成年、後日談推演後才成年——永久限純愛尺度
|
||
}
|
||
|
||
export interface AdultContentCheckResult {
|
||
allowed: boolean;
|
||
reason: string;
|
||
}
|
||
|
||
// L-7 內容尺度三層邊界:使用者分級(opt-in)、角色資格(僅正史成年)、硬邊界(永不可解除)。
|
||
// 這是唯一的判定入口——任何想要放行成人內容的呼叫都必須經過 checkAdultContentAllowed,且無論結果一律記錄,
|
||
// 不存在「在角色演出層說服她」繞過的可能:判定完全不看對話內容,只看角色資格與使用者驗證狀態。
|
||
@Injectable()
|
||
export class RomanceEligibilityService {
|
||
constructor(
|
||
private readonly prisma: PrismaService,
|
||
private readonly ageDetermination: AgeDeterminationService,
|
||
) {}
|
||
|
||
async getEligibility(characterId: string): Promise<RomanceEligibility> {
|
||
const { canonAdult, currentAdult, isEpilogueAdult } = await this.ageDetermination.determine(characterId);
|
||
return { romanceAxisEligible: currentAdult, adultModeEligible: canonAdult, isEpilogueAdult };
|
||
}
|
||
|
||
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, 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 {
|
||
allowed: false,
|
||
reason: eligibility.isEpilogueAdult
|
||
? "角色為正史未成年、後日談推演後才成年——成人模式資格永久排除,不隨任何開關或關係狀態改變"
|
||
: "角色未通過正史成年判定,不具成人模式資格",
|
||
};
|
||
}
|
||
const consent = await this.prisma.client.adultModeConsent.findUnique({ where: { userId } });
|
||
if (!consent || !consent.verifiedAdult || !consent.optedIn) {
|
||
return { allowed: false, reason: "使用者尚未完成成年驗證與明確 opt-in,預設全關" };
|
||
}
|
||
return { allowed: true, reason: "角色具正史成年資格,使用者已完成成年驗證並主動開啟成人模式" };
|
||
}
|
||
}
|