M5: days + escalation — one new pressure per day
- DAYS table (game/game.ts): 5 days, each introduces exactly one pressure — basics / +1 ingredient / wetter (2x tomato) / a real cut (katsu) / patience ramp (maxSeconds 45->22, angerRamp 1.8). Game.setDay loads order + ramp. - samurai.ramp scales every sin (sheathing unaffected) — day 5 the samurai runs out of patience faster. - Exit-bar: __s.playAllDays() — every day beatable by a scripted-honest run (all S, standing). Ramp bites: day 1 = 7 crime-cuts to strike, day 5 = 4. Verified in-browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
58285712ef
commit
cdde03bef4
@ -58,10 +58,18 @@
|
|||||||
`reset()` restrings in place — Matter's constraint solve-order makes the
|
`reset()` restrings in place — Matter's constraint solve-order makes the
|
||||||
restrung puppet marginally different but still walks fine (head stays ~400,
|
restrung puppet marginally different but still walks fine (head stays ~400,
|
||||||
floor 640).
|
floor 640).
|
||||||
- **Next: M5** orders/days escalation (`game/orders.ts`-ish) — one new pressure
|
- **M5 ✅** days + escalation. `DAYS` in `game/game.ts` — 5 days, each ONE new
|
||||||
per day. Then **M6** juice (title, audio, string unlockables).
|
pressure: (1) basics, (2) +1 ingredient (4 layers), (3) wetter (2× tomato),
|
||||||
- Run: `npm run dev` (:5173). Harness: `__s.playCleanRun() __s.playDisasterRun()
|
(4) a real cut (katsu), (5) patience ramp (maxSeconds 45→22, angerRamp 1.8).
|
||||||
__s.serve() __s.anger(ev)` + all M1–M3 verbs. Headless: `npm run check`.
|
`Game.setDay(n)` loads order + ramp; sins scale by `samurai.ramp` (sheathing
|
||||||
|
doesn't). Exit-bar: `__s.playAllDays()` — every day beatable by a scripted-
|
||||||
|
honest run (all S, standing). Ramp bites: day 1 needs 7 crime-cuts to strike,
|
||||||
|
day 5 only 4. Verified in-browser.
|
||||||
|
- **Next: M6** juice — title screen (「サンドニエット」brush art + SHUBATTO crawl +
|
||||||
|
attract ragdoll), Web Audio (shamisen sting / slash whoosh / wet splat), food
|
||||||
|
faces on pieces, string unlockables (rubber/chain/silk — pre-authorized defer).
|
||||||
|
- Run: `npm run dev` (:5173). Harness: `__s.playDay(n) __s.playAllDays()
|
||||||
|
__s.playCleanRun() __s.playDisasterRun()` + all M1–M3 verbs. Headless: `npm run check`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
29
src/dev.ts
29
src/dev.ts
@ -1,6 +1,7 @@
|
|||||||
import type { Stage } from './scenes/stage';
|
import type { Stage } from './scenes/stage';
|
||||||
import { FLOOR_Y } from './scenes/stage';
|
import { FLOOR_Y } from './scenes/stage';
|
||||||
import { spawnFood } from './sim/food';
|
import { spawnFood, FOODS } from './sim/food';
|
||||||
|
import { DAYS } from './game/game';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dev harness — toastsim's single most valuable organ, transplanted.
|
* Dev harness — toastsim's single most valuable organ, transplanted.
|
||||||
@ -306,6 +307,32 @@ export function installHarness(stage: Stage): void {
|
|||||||
return { ...v, standing: stage.sando.stats().standing, order: stage.session.order.name };
|
return { ...v, standing: stage.sando.stats().standing, order: stage.session.order.name };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---- M5: days + escalation ---------------------------------------------
|
||||||
|
/** Play day `n` with a scripted-but-honest run: cut the filling, stack the
|
||||||
|
* ordered layers, serve in time. Each day must stay beatable (B+). */
|
||||||
|
playDay(n = 0) {
|
||||||
|
this.freshRun();
|
||||||
|
const d = stage.session.setDay(n);
|
||||||
|
const order = stage.session.order;
|
||||||
|
const fill = order.layers.find((k) => k !== 'bread' && k in FOODS) ?? 'tomato';
|
||||||
|
this.chop(fill); // a clean cut feeds THE CUT
|
||||||
|
this.buildSando(order.layers.map((k) => [k, 0] as [string, number]));
|
||||||
|
const v = this.serve(true, Math.max(6, order.maxSeconds - 6));
|
||||||
|
return {
|
||||||
|
day: n + 1,
|
||||||
|
name: order.name,
|
||||||
|
pressure: d.pressure,
|
||||||
|
grade: v.grade,
|
||||||
|
total: v.total,
|
||||||
|
standing: stage.sando.stats().standing,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/** M5 exit-bar: every day introduces its pressure and is beatable (B+). */
|
||||||
|
playAllDays() {
|
||||||
|
return DAYS.map((_, i) => this.playDay(i));
|
||||||
|
},
|
||||||
|
|
||||||
/** M4 exit-bar B: anger the samurai to the top → the string cut → ragdoll. */
|
/** M4 exit-bar B: anger the samurai to the top → the string cut → ragdoll. */
|
||||||
playDisasterRun() {
|
playDisasterRun() {
|
||||||
this.freshRun();
|
this.freshRun();
|
||||||
|
|||||||
@ -2,11 +2,25 @@ import type { Stage } from '../scenes/stage';
|
|||||||
import { Samurai } from '../sim/samurai';
|
import { Samurai } from '../sim/samurai';
|
||||||
import { judge, type Order, type RunResult, type Verdict } from './judging';
|
import { judge, type Order, type RunResult, type Verdict } from './judging';
|
||||||
|
|
||||||
/** Day 1: one open tomato sando. Escalation is M5. */
|
/** A day introduces exactly ONE new pressure (never two) and stays beatable by
|
||||||
export const ORDERS: Order[] = [
|
* a scripted-but-honest run. angerRamp scales every sin — the samurai's
|
||||||
{ name: 'Open Tomato Sando', layers: ['bread', 'tomato', 'bread'], needsCut: true, maxSeconds: 40 },
|
* patience shortens on the rush-hour day. */
|
||||||
|
export interface Day {
|
||||||
|
order: Order;
|
||||||
|
pressure: string;
|
||||||
|
angerRamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DAYS: Day[] = [
|
||||||
|
{ order: { name: 'Open Tomato Sando', layers: ['bread', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'the basics', angerRamp: 1 },
|
||||||
|
{ order: { name: 'Cheese & Tomato', layers: ['bread', 'cheese', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: '+1 ingredient', angerRamp: 1 },
|
||||||
|
{ order: { name: 'Double Tomato', layers: ['bread', 'tomato', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'wetter — twice the tomato', angerRamp: 1 },
|
||||||
|
{ order: { name: 'Katsu Sando', layers: ['bread', 'katsu', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'a real cut — the katsu', angerRamp: 1 },
|
||||||
|
{ order: { name: 'Rush Hour', layers: ['bread', 'tomato', 'bread'], needsCut: true, maxSeconds: 22 }, pressure: 'the patience ramp', angerRamp: 1.8 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ORDERS: Order[] = DAYS.map((d) => d.order);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The run controller. Holds the samurai + the current ticket, tallies the floor,
|
* The run controller. Holds the samurai + the current ticket, tallies the floor,
|
||||||
* and turns a finished attempt into a Verdict. Kitchen sins feed the anger
|
* and turns a finished attempt into a Verdict. Kitchen sins feed the anger
|
||||||
@ -14,7 +28,8 @@ export const ORDERS: Order[] = [
|
|||||||
*/
|
*/
|
||||||
export class Game {
|
export class Game {
|
||||||
samurai = new Samurai();
|
samurai = new Samurai();
|
||||||
order: Order = ORDERS[0];
|
day = 0;
|
||||||
|
order: Order = DAYS[0].order;
|
||||||
drops = 0;
|
drops = 0;
|
||||||
seconds = 0; // ticket timer (seconds)
|
seconds = 0; // ticket timer (seconds)
|
||||||
|
|
||||||
@ -22,7 +37,18 @@ export class Game {
|
|||||||
this.samurai.onStrike = () => this.stage.stringCut();
|
this.samurai.onStrike = () => this.stage.stringCut();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(order: Order = ORDERS[0]): void {
|
/** Load day `n` (clamped): its order + its samurai patience ramp. */
|
||||||
|
setDay(n: number): Day {
|
||||||
|
this.day = Math.max(0, Math.min(DAYS.length - 1, n));
|
||||||
|
const d = DAYS[this.day];
|
||||||
|
this.order = d.order;
|
||||||
|
this.samurai.ramp = d.angerRamp;
|
||||||
|
this.drops = 0;
|
||||||
|
this.seconds = 0;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(order: Order = this.order): void {
|
||||||
this.samurai.reset();
|
this.samurai.reset();
|
||||||
this.order = order;
|
this.order = order;
|
||||||
this.drops = 0;
|
this.drops = 0;
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export const PORTRAITS = ['samurai_calm', 'samurai_watch', 'samurai_annoyed', 's
|
|||||||
|
|
||||||
export class Samurai {
|
export class Samurai {
|
||||||
anger = 0; // 0..1
|
anger = 0; // 0..1
|
||||||
|
ramp = 1; // day patience multiplier — sins hurt more on the rush-hour day
|
||||||
struck = false;
|
struck = false;
|
||||||
/** Fired once when the meter tops out — the stage wires the string cut here. */
|
/** Fired once when the meter tops out — the stage wires the string cut here. */
|
||||||
onStrike?: () => void;
|
onStrike?: () => void;
|
||||||
@ -32,10 +33,12 @@ export class Samurai {
|
|||||||
/** The last event, for barks. */
|
/** The last event, for barks. */
|
||||||
last: AngerEvent | null = null;
|
last: AngerEvent | null = null;
|
||||||
|
|
||||||
/** Apply an anger event (optionally overriding the amount, e.g. ramped late). */
|
/** Apply an anger event (optionally overriding the amount, e.g. ramped late).
|
||||||
|
* Sins scale by the day ramp; sheathing (a clean serve) does not. */
|
||||||
bump(ev: AngerEvent, amount = ANGER[ev]): void {
|
bump(ev: AngerEvent, amount = ANGER[ev]): void {
|
||||||
this.last = ev;
|
this.last = ev;
|
||||||
this.anger = Math.max(0, Math.min(1, this.anger + amount));
|
const scaled = amount > 0 ? amount * this.ramp : amount;
|
||||||
|
this.anger = Math.max(0, Math.min(1, this.anger + scaled));
|
||||||
this.onChange?.(this.anger, this.stage);
|
this.onChange?.(this.anger, this.stage);
|
||||||
if (this.anger >= 1 && !this.struck) this.strike();
|
if (this.anger >= 1 && !this.struck) this.strike();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user