HardYards/web/world/js/garden.js
type-two 7acee5437b Sprint 13: garden model exported as garden.js (B's gate-1.2 blocker) + applyMute(sky,on) as a value
- createGarden / GARDEN_DRAIN / HAIL_WEIGHT / RAIN_WEIGHT move from main.js
  module-locals to garden.js, unchanged — the audit must predict the sim off
  the sim's OWN code, not a drifting copy. GARDEN_DRAIN's comment now carries
  the gate-1.4 ruling: the constant does not move; it owns the bare-bed arc.
- M-mute application extracted to exported applyMute(sky, on): calls C's
  sky.setMute when the tap exists, reports honestly when it does not, so the
  selftest can fail the wiring instead of trusting a ?.() no-op.
2026-07-18 12:44:37 +10:00

104 lines
4.5 KiB
JavaScript

/**
* garden.js — the garden model, one shared copy. [Lane A]
*
* SPRINT13 gate 1.2 — B's export ask, granted. These were module-locals in
* main.js, which made the audit's garden prediction impossible without copying
* three constants "C is about to change this very sprint" (B's words) — the
* exact drift sweep.js exists to prevent on the winnability side. Now main.js
* and tools/site_audit read the SAME model, so "same number, two harnesses"
* is structural rather than aspirational. B: the TEMPORARY COPY in
* garden_probe.html can die now.
*
* Nothing here changed in the move — this is a mechanical lift. The one thing
* that DID change this sprint is the comment on GARDEN_DRAIN, because the
* gate-1 rulings resolved what it is for.
*/
/**
* How fast an unprotected garden dies, in HP per second at full exposure.
*
* SPRINT13 gate 1.4 RULING: **this constant does not move.** My old comment
* here said "the lever that actually needs moving is the shadow geometry, not
* this" — C measured the geometry and it was never wrong (probe2: raycast and
* rasteriser agree at 1.0000). What was wrong was the LINE the yard offered:
* the canonical p1..p4 covers 25% of what hail sees, and no drain value can
* separate a 25% line from a bare bed because the drain scales BOTH sides.
* The separation now comes from anchor placement (backyard_01's p5, its
* `separation` block, and the a.test assert that flies it), which is where it
* always had to come from.
*
* What 0.9 actually owns is the week's bare-bed arc — the difficulty curve of
* neglect, measured: gentle 97.6 · southerly 83.7 · buster 82.6 · wildnight
* 35.7 · icenight 0.0. Move this and all five nights retune at once; the
* wildnight bare bed must stay under the win line (< 50) for the pinned
* separation target to mean anything, and the early nights must stay
* survivable-by-default or night 1 stops being a tutorial. Retunes go through
* THREADS with that table, not through this number quietly.
*/
export const GARDEN_DRAIN = 0.9;
/**
* Decision 13's weights. Hail is what actually kills a garden and cloth honestly
* stops it (stones fall ≤20° off vertical; rain leans 73° in a gale and walks
* straight under the sail — Lane C's numbers). Rain is demoted to a drizzle of
* damage so the night still costs you something when it isn't hailing.
*
* SPRINT6 gate 1 lists these first among the balance levers.
*/
export const HAIL_WEIGHT = 5.0;
export const RAIN_WEIGHT = 0.25;
/**
* The garden: the thing you are actually protecting, and the only score that
* matters. Deliberately not inside hud.js — the HUD reads, it doesn't decide.
*
* It keeps its damage split by CAUSE, and that isn't bookkeeping for its own
* sake: the aftermath verdict is the game's entire feedback channel, and a
* verdict that guesses teaches the wrong lesson. A garden that only knows it
* went from 100 to 39 cannot tell a player whether their hardware let go or
* their perfectly-held sail simply wasn't over the bed — and those are opposite
* mistakes with opposite fixes.
*
* @param {{ setPlants(state: string): void }} world
*/
export function createGarden(world) {
let hp = 100;
let state = 'full';
let byHail = 0;
let byRain = 0;
return {
get hp() { return hp; },
get state() { return state; },
/** HP lost to each cause this run. The verdict reads these. */
get damage() { return { hail: byHail, rain: byRain }; },
reset() { hp = 100; state = 'full'; byHail = 0; byRain = 0; world.setPlants('full'); },
/**
* Decision 13, and the two terms stay SEPARATE on the way in rather than
* being pre-summed by the caller — that sum is exactly the information the
* verdict needs and can never recover afterwards.
*
* @param {number} dt
* @param {number} hail 0..1 sky.gardenHailExposure(bed, t)
* @param {number} rain 0..1 sky.gardenExposure(bed, t)
*/
step(dt, hail, rain) {
const dHail = HAIL_WEIGHT * hail * GARDEN_DRAIN * dt;
const dRain = RAIN_WEIGHT * rain * GARDEN_DRAIN * dt;
const total = Math.min(hp, dHail + dRain);
if (total > 0) {
// Attribute proportionally, so the split still adds up on the last tick
// when the garden bottoms out at 0 mid-step.
const scale = total / (dHail + dRain);
byHail += dHail * scale;
byRain += dRain * scale;
hp -= total;
}
const next = hp > 66 ? 'full' : hp > 33 ? 'tattered' : 'dead';
if (next !== state) { state = next; world.setPlants(next); }
},
};
}