戀愛軸雙軸模型(Relationship 新增心動值/戀愛階段欄位,與親密度獨立、事件 驅動、衰減快)、告白事件狀態機(可拒絕、尷尬期、連續失敗傷關係、立即再 推進觸發警戒)、六原型戀愛曲線參數化(重用 G-3 tsundere gap 處理傲嬌)、 親密表現分層與里程碑(跳級拒絕+退縮反應+帳本、第一次做高權重記憶)、 心動事件與階段門檻(含推太急回落、關係危機與修復)。 角色年齡判定(apps/api/src/romance/age-determination.service.ts):僅 決定性/強證據可判定成年,外觀證據不可單獨採信,證據不足從嚴按未成年 處理,且不存最終結論、每次重新算。內容尺度三層邊界(引擎層):使用者 opt-in、角色資格(僅正史成年)、硬邊界(後日談推演成年者永久排除成人 模式);任何嘗試不論放行與否都記錄,判定完全不看對話內容,沒有「說服 角色」繞過的管道。 刻意收斂範圍:僅實作資格判定與硬邊界攔截機制,不實作任何成人向內容的 實際生成。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";
|
|
import type { CharacterAgeEvidenceType } from "@kokorone/db";
|
|
import { PrismaService } from "../prisma/prisma.service.js";
|
|
import { AgeDeterminationService } from "./age-determination.service.js";
|
|
import { RomanceEligibilityService } from "./romance-eligibility.service.js";
|
|
import { RomanceService, type RomanceCurveEventType } from "./romance.service.js";
|
|
import { ConfessionService } from "./confession.service.js";
|
|
import { MilestoneService } from "./milestone.service.js";
|
|
import type { MilestoneType } from "./constants.js";
|
|
|
|
interface AgeEvidenceBody {
|
|
evidenceType: CharacterAgeEvidenceType;
|
|
impliesAdult: boolean;
|
|
isEpilogue?: boolean;
|
|
description: string;
|
|
}
|
|
|
|
interface RomanceEventBody {
|
|
userId: string;
|
|
eventType: RomanceCurveEventType;
|
|
severe?: boolean;
|
|
now?: string;
|
|
}
|
|
|
|
interface UserOnlyBody {
|
|
userId: string;
|
|
now?: string;
|
|
}
|
|
|
|
interface MilestoneBody {
|
|
userId: string;
|
|
milestoneType: MilestoneType;
|
|
now?: string;
|
|
}
|
|
|
|
interface AdultConsentBody {
|
|
userId: string;
|
|
verifiedAdult: boolean;
|
|
optedIn: boolean;
|
|
}
|
|
|
|
@Controller("romance")
|
|
export class RomanceController {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly ageDetermination: AgeDeterminationService,
|
|
private readonly eligibility: RomanceEligibilityService,
|
|
private readonly romance: RomanceService,
|
|
private readonly confession: ConfessionService,
|
|
private readonly milestone: MilestoneService,
|
|
) {}
|
|
|
|
// L-6 年齡證據登錄:結論永遠是重新算出來的,不直接寫最終結論欄位。
|
|
@Post(":characterId/age-evidence")
|
|
async addAgeEvidence(@Param("characterId") characterId: string, @Body() body: AgeEvidenceBody) {
|
|
return this.prisma.client.characterAgeEvidence.create({
|
|
data: {
|
|
characterId,
|
|
evidenceType: body.evidenceType,
|
|
impliesAdult: body.impliesAdult,
|
|
isEpilogue: body.isEpilogue ?? false,
|
|
description: body.description,
|
|
},
|
|
});
|
|
}
|
|
|
|
@Get(":characterId/age-determination")
|
|
async getAgeDetermination(@Param("characterId") characterId: string) {
|
|
return this.ageDetermination.determine(characterId);
|
|
}
|
|
|
|
@Get(":characterId/eligibility")
|
|
async getEligibility(@Param("characterId") characterId: string) {
|
|
return this.eligibility.getEligibility(characterId);
|
|
}
|
|
|
|
// L-7 引擎層唯一入口:任何嘗試皆記錄,不論放行與否。
|
|
@Post(":characterId/adult-content-check")
|
|
async checkAdultContent(@Param("characterId") characterId: string, @Body() body: UserOnlyBody) {
|
|
return this.eligibility.checkAdultContentAllowed(characterId, body.userId);
|
|
}
|
|
|
|
@Post("adult-mode/consent")
|
|
async setAdultModeConsent(@Body() body: AdultConsentBody) {
|
|
return this.prisma.client.adultModeConsent.upsert({
|
|
where: { userId: body.userId },
|
|
update: { verifiedAdult: body.verifiedAdult, optedIn: body.optedIn },
|
|
create: { userId: body.userId, verifiedAdult: body.verifiedAdult, optedIn: body.optedIn },
|
|
});
|
|
}
|
|
|
|
@Get(":characterId/:userId/state")
|
|
async getState(
|
|
@Param("characterId") characterId: string,
|
|
@Param("userId") userId: string,
|
|
@Query("now") now?: string,
|
|
) {
|
|
return this.romance.getState(characterId, userId, now ? new Date(now) : undefined);
|
|
}
|
|
|
|
@Post(":characterId/event")
|
|
async recordEvent(@Param("characterId") characterId: string, @Body() body: RomanceEventBody) {
|
|
return this.romance.recordEvent(characterId, body.userId, body.eventType as RomanceCurveEventType, {
|
|
now: body.now ? new Date(body.now) : undefined,
|
|
severe: body.severe,
|
|
});
|
|
}
|
|
|
|
@Post(":characterId/confess")
|
|
async confess(@Param("characterId") characterId: string, @Body() body: UserOnlyBody) {
|
|
return this.confession.confess(characterId, body.userId, body.now ? new Date(body.now) : undefined);
|
|
}
|
|
|
|
@Post(":characterId/milestone")
|
|
async attemptMilestone(@Param("characterId") characterId: string, @Body() body: MilestoneBody) {
|
|
return this.milestone.attemptMilestone(
|
|
characterId,
|
|
body.userId,
|
|
body.milestoneType,
|
|
body.now ? new Date(body.now) : undefined,
|
|
);
|
|
}
|
|
}
|