戀愛軸雙軸模型(Relationship 新增心動值/戀愛階段欄位,與親密度獨立、事件 驅動、衰減快)、告白事件狀態機(可拒絕、尷尬期、連續失敗傷關係、立即再 推進觸發警戒)、六原型戀愛曲線參數化(重用 G-3 tsundere gap 處理傲嬌)、 親密表現分層與里程碑(跳級拒絕+退縮反應+帳本、第一次做高權重記憶)、 心動事件與階段門檻(含推太急回落、關係危機與修復)。 角色年齡判定(apps/api/src/romance/age-determination.service.ts):僅 決定性/強證據可判定成年,外觀證據不可單獨採信,證據不足從嚴按未成年 處理,且不存最終結論、每次重新算。內容尺度三層邊界(引擎層):使用者 opt-in、角色資格(僅正史成年)、硬邊界(後日談推演成年者永久排除成人 模式);任何嘗試不論放行與否都記錄,判定完全不看對話內容,沒有「說服 角色」繞過的管道。 刻意收斂範圍:僅實作資格判定與硬邊界攔截機制,不實作任何成人向內容的 實際生成。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
2.7 KiB
TypeScript
56 lines
2.7 KiB
TypeScript
import { Injectable } from "@nestjs/common";
|
|
import { PrismaService } from "../prisma/prisma.service.js";
|
|
import { AgeDeterminationService } from "./age-determination.service.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): Promise<AdultContentCheckResult> {
|
|
const result = await this.evaluate(characterId, userId);
|
|
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> {
|
|
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: "角色具正史成年資格,使用者已完成成年驗證並主動開啟成人模式" };
|
|
}
|
|
}
|