Honey Village: six model builds + hub frontend, Dockerized for Dokploy
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
// 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();
|
||||
Reference in New Issue
Block a user