Files
ModelTest20260714/GLM5.2/src/systems/CorruptionDirector.ts
T

237 lines
7.4 KiB
TypeScript

// CorruptionDirector.ts: single source of truth for all state + A/V params.
// Holds happiness/awareness/day/task flags/clue set/erosion stage.
// Emits events for scenes to react.
import { audio } from './AudioEngine';
import {
HAPPY_START, AWARE_START, HAPPY_MAX, AWARE_MAX, OVERFLOW_THRESHOLD,
EROSION_MATRIX, AWARE_CLUE_GAIN, AWARE_SAD_GAIN, HAPPY_SAD_LOSS,
HAPPY_SMALL_DAILY_CAP, DEFAULT_NAME, LOW_HAPPY_THRESHOLD, LOW_HAPPY_DURATION_MS,
FORCE_FESTIVAL_HAPPY,
} from '../data/gamedata';
export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4';
type Handler = (payload?: any) => void;
class CorruptionDirector {
happiness = HAPPY_START;
awareness = AWARE_START;
day = 1;
stage: Stage = 'S1';
playerName = DEFAULT_NAME;
startTime = Date.now();
// clue set
clues = new Set<string>();
// task completion
tasksDone = new Set<string>();
// sadness: small-interaction daily counters per kind
smallInteractCounts: Record<string, number> = {}; // key: kind@day
// npc talk counts per day (for 纠缠 detection)
npcTalkCounts: Record<string, number> = {};
// rejection records (Day 5)
rejectedTasks = new Set<string>();
// escaped forced festivals count (E4)
escapedFestivals = 0;
// happiness-low tracker
private lowHappySince = 0;
// overflow pool
private overflowPool = 0;
// last negative flash time
private lastFlashAt = 0;
// gaze tracking (well / fog) once per day
gazedWell = false;
gazedFog = false;
// whether forced festival is active
forcedFestivalActive = false;
private listeners: Record<string, Handler[]> = {};
on(ev: string, h: Handler) {
(this.listeners[ev] ||= []).push(h);
return () => this.off(ev, h);
}
off(ev: string, h: Handler) {
const arr = this.listeners[ev]; if (!arr) return;
const i = arr.indexOf(h); if (i >= 0) arr.splice(i, 1);
}
emit(ev: string, payload?: any) {
(this.listeners[ev] || []).slice().forEach(h => { try { h(payload); } catch (e) { console.error(e); } });
}
reset() {
this.happiness = HAPPY_START;
this.awareness = AWARE_START;
this.day = 1;
this.stage = 'S1';
this.playerName = DEFAULT_NAME;
this.startTime = Date.now();
this.clues.clear();
this.tasksDone.clear();
this.smallInteractCounts = {};
this.npcTalkCounts = {};
this.rejectedTasks.clear();
this.escapedFestivals = 0;
this.lowHappySince = 0;
this.overflowPool = 0;
this.gazedWell = false;
this.gazedFog = false;
this.forcedFestivalActive = false;
this.recomputeStage();
this.emit('change');
}
recomputeStage() {
const h = this.happiness;
let s: Stage = 'S0';
for (const row of EROSION_MATRIX) {
if (h >= row.happinessRange[0] && h <= row.happinessRange[1]) { s = row.stage; break; }
}
if (s !== this.stage) {
this.stage = s;
audio.setErosion(s);
this.emit('stage', s);
}
this.emit('change');
}
setHappiness(v: number, opts: { silent?: boolean } = {}) {
const prev = this.happiness;
let nv = Math.max(0, Math.min(HAPPY_MAX, Math.round(v)));
// forced maintenance at 100: small deductions accumulate in overflow pool
if (prev >= HAPPY_MAX && nv < prev) {
const diff = prev - nv;
this.overflowPool += diff;
nv = HAPPY_MAX; // stays at 100
if (this.overflowPool >= OVERFLOW_THRESHOLD) {
this.overflowPool = 0;
this.emit('negativeFlash');
this.stage = 'S4';
audio.setErosion('S4');
this.emit('stage', 'S4');
}
}
this.happiness = nv;
if (!opts.silent) this.recomputeStage();
this.checkLowHappy();
}
addHappiness(delta: number) {
if (delta > 0 && this.happiness >= HAPPY_MAX) {
// overflow pool accumulation (PRD: 小额扣分不再生效而是累积进隐藏溢出池) — for increases beyond 100, ignore
return;
}
this.setHappiness(this.happiness + delta);
}
addAwareness(delta: number) {
this.awareness = Math.max(0, Math.min(AWARE_MAX, this.awareness + delta));
this.emit('change');
}
// §6.2 clue discovery
discoverClue(id: string) {
if (this.clues.has(id)) return false;
this.clues.add(id);
this.addAwareness(AWARE_CLUE_GAIN);
this.emit('clue', id);
this.emit('change');
return true;
}
// §6.1 small interaction (water/feed/pet) +1, daily cap 3 per kind
smallInteraction(kind: string) {
const key = `${kind}@${this.day}`;
const count = this.smallInteractCounts[key] || 0;
if (count >= HAPPY_SMALL_DAILY_CAP) return false;
this.smallInteractCounts[key] = count + 1;
this.addHappiness(1);
return true;
}
// §6.3 sadness behavior: -5 happy, +3 aware, nearest NPC turns
sadness(behavior: string, nearestNpc?: string) {
this.addHappiness(-HAPPY_SAD_LOSS);
this.addAwareness(AWARE_SAD_GAIN);
this.emit('sadness', { behavior, nearestNpc });
this.emit('change');
}
// gaze well/fog (§6.2)
gaze(target: 'well' | 'fog') {
if (target === 'well' && !this.gazedWell) { this.gazedWell = true; this.addAwareness(3); }
if (target === 'fog' && !this.gazedFog) { this.gazedFog = true; this.addAwareness(3); }
}
// NPC talk counter for 纠缠 (>=5/day)
noteNpcTalk(npcKey: string): boolean {
const key = `${npcKey}@${this.day}`;
this.npcTalkCounts[key] = (this.npcTalkCounts[key] || 0) + 1;
return this.npcTalkCounts[key] >= 5;
}
private checkLowHappy() {
if (this.happiness < LOW_HAPPY_THRESHOLD) {
if (this.lowHappySince === 0) this.lowHappySince = Date.now();
else if (Date.now() - this.lowHappySince >= LOW_HAPPY_DURATION_MS && !this.forcedFestivalActive) {
this.triggerForcedFestival();
}
} else {
this.lowHappySince = 0;
}
}
// §6.1 forced festival
triggerForcedFestival() {
this.forcedFestivalActive = true;
this.emit('forcedFestivalStart');
// raise happiness to 50
this.setHappiness(FORCE_FESTIVAL_HAPPY);
}
escapeFestival() {
if (!this.forcedFestivalActive) return;
this.forcedFestivalActive = false;
this.escapedFestivals++;
this.emit('forcedFestivalEnd');
this.emit('change');
}
advanceDay() {
this.day = Math.min(7, this.day + 1);
this.gazedWell = false; this.gazedFog = false;
this.emit('day', this.day);
this.emit('change');
}
// ending determination per §9 priority
determineEnding(): 'E4'|'E3'|'E2'|'E1' {
if (this.day >= 5 && this.happiness <= 10 && this.escapedFestivals >= 3) return 'E4';
if (this.awareness >= 70 && this.clues.size >= 6) return 'E3';
if (this.awareness >= 30) return 'E2';
if (this.awareness <= 29 && this.happiness >= 90) return 'E1';
// fallback
if (this.awareness <= 29) return 'E1';
return 'E2';
}
playTimeSeconds(): number {
return Math.floor((Date.now() - this.startTime) / 1000);
}
formatPlayTime(): string {
const s = this.playTimeSeconds();
const m = Math.floor(s / 60);
const sec = s % 60;
return `${m}${sec}秒`;
}
// debug setters
debugSetHappiness(v: number) { this.happiness = Math.max(0,Math.min(HAPPY_MAX,v)); this.recomputeStage(); }
debugSetAwareness(v: number) { this.awareness = Math.max(0,Math.min(AWARE_MAX,v)); this.emit('change'); }
debugSetDay(d: number) { this.day = Math.max(1,Math.min(7,d)); this.emit('day',this.day); this.emit('change'); }
debugAllClues() { ['C1','C2','C3','C4','C5','C6','C7','C8','C9'].forEach(c=>this.clues.add(c)); this.addAwareness(0); this.emit('change'); }
}
export const director = new CorruptionDirector();