FloorDemoScene (F from Parade): an 80x45-tile venue you patrol in the dark with a flashlight cone that reveals detail, plus all three v0.2 infractions and the UV stamp check. Self-populates from the patron generator; consumes Patron[] only, so Phase 2 can hand it the real admitted list. Pure, tested logic (venueMap, cone, player, crowdSim, bangJudge, escalation, patDown); Phaser layer stays thin (FloorView + three overlays). 120 new tests, 169 total. Two bugs found by running it rather than by the suite, both fixed: - bump penalties had a per-agent cooldown but no sim-wide bound, so the leak rate scaled with crowd size and an unattended venue pinned vibe to 0 / aggro to 100 by ~1AM. Sim-wide gate added, with a regression test. - beat.update() sat below the overlay early-return, freezing the beat clock behind modals and making the stall rhythm game unwinnable. Clock and beat now always run. Also retuned dwell times and the tile/darkness palette for readability, and dropped Light2D in favour of the cone mask it was fighting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
// The Cut-Off judgement matrix. Pure — no Phaser, no bus. The scene decides
|
|
// what to do with the result; this only decides what the room thinks of you.
|
|
|
|
import type { DrunkStage } from '../../data/types';
|
|
import {
|
|
CUTOFF_REPLY_CLEAN,
|
|
CUTOFF_REPLY_SCENE,
|
|
CUTOFF_REPLY_SOFT,
|
|
ESCALATION_LINES,
|
|
type EscalationLine,
|
|
} from './strings';
|
|
|
|
export type EscalationOutcome = 'clean' | 'scene' | 'staggerFree';
|
|
|
|
export interface EscalationResult {
|
|
outcome: EscalationOutcome;
|
|
vibeDelta: number;
|
|
aggroDelta: number;
|
|
/** What you said (from ESCALATION_LINES). */
|
|
line: string;
|
|
/** What they said back (from CUTOFF_REPLY_*). */
|
|
reply: string;
|
|
/** Eject: attach them to the escort-out walk. */
|
|
eject: boolean;
|
|
/** Resolved: stop treating them as an open infraction. */
|
|
resolved: boolean;
|
|
}
|
|
|
|
/** Softest first — comparing rungs is the whole judgement. */
|
|
const RUNG: Readonly<Record<EscalationLine, number>> = {
|
|
water: 0,
|
|
done: 1,
|
|
escort: 2,
|
|
};
|
|
|
|
/** The correct rung for each drunk stage. */
|
|
export const CORRECT_LINE: Readonly<Record<DrunkStage, EscalationLine>> = {
|
|
sober: 'water',
|
|
tipsy: 'water',
|
|
loose: 'water',
|
|
messy: 'done',
|
|
maggot: 'escort',
|
|
};
|
|
|
|
// Deliberately small. Being right about a drunk shouldn't feel like a jackpot,
|
|
// and overreach should sting more than restraint pays (design doc §4.3).
|
|
const CLEAN_VIBE = 4;
|
|
const SCENE_VIBE_PER_RUNG = -3;
|
|
const SCENE_AGGRO_PER_RUNG = 5;
|
|
const SOFT_VIBE = -1;
|
|
|
|
export function judgeEscalation(stage: DrunkStage, line: EscalationLine): EscalationResult {
|
|
const said = ESCALATION_LINES[line];
|
|
const overreach = RUNG[line] - RUNG[CORRECT_LINE[stage]];
|
|
|
|
if (overreach === 0) {
|
|
return {
|
|
outcome: 'clean',
|
|
vibeDelta: CLEAN_VIBE,
|
|
aggroDelta: 0,
|
|
line: said,
|
|
reply: CUTOFF_REPLY_CLEAN[stage],
|
|
// Water is a kindness, not an exit — only the top two stages walk.
|
|
eject: stage === 'messy' || stage === 'maggot',
|
|
resolved: true,
|
|
};
|
|
}
|
|
|
|
if (overreach > 0) {
|
|
return {
|
|
outcome: 'scene',
|
|
vibeDelta: SCENE_VIBE_PER_RUNG * overreach,
|
|
aggroDelta: SCENE_AGGRO_PER_RUNG * overreach,
|
|
line: said,
|
|
reply: CUTOFF_REPLY_SCENE[stage],
|
|
// They're not going anywhere with you now, but the moment is spent.
|
|
eject: false,
|
|
resolved: true,
|
|
};
|
|
}
|
|
|
|
return {
|
|
outcome: 'staggerFree',
|
|
vibeDelta: SOFT_VIBE,
|
|
aggroDelta: 0,
|
|
line: said,
|
|
reply: CUTOFF_REPLY_SOFT[stage],
|
|
eject: false,
|
|
resolved: false,
|
|
};
|
|
}
|
|
|
|
/** A maggot left alone too long becomes a deferred heat risk. */
|
|
export const MAGGOT_GRACE_MIN = 25;
|
|
|
|
export function maggotOverdue(spottedAtClockMin: number, nowClockMin: number): boolean {
|
|
return nowClockMin - spottedAtClockMin > MAGGOT_GRACE_MIN;
|
|
}
|