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