Lane D S18 gate 1.3: a corner you CUT is not a corner that failed — and gate 1's four unwired seams
THE MONEY. B ruled on the rig that a cut is a decision and emitted no `break`
— but _releaseCorner sets c.broken for a cut exactly as for an overload, and
every ledger surface reads broken. So taking the signature decision billed you
for it five ways: the hardware, the $180 carport you cut the sail TO SAVE
(billed AND wrecked), the gnome's two-corner rule, the clean bonus it voided,
and a free warranty callout tomorrow plus half a star. The invoice would have
printed "You spent the sail to keep the steel" directly above a $180 charge for
the thing you kept. Nobody wrote a wrong line; the join is a flag no verb set
until now. A cut now bills nothing, wrecks nothing, warranties nothing and
costs no standing — and the refund counts it, because "save the anchors" is a
ledger line and not a slogan. `lost` still counts it: the sail IS on the lawn.
THE FOUR SEAMS, all found by playing night 5 and reading the cards, all data or
a name in A's files, all pre-authorised in THREADS, none of them visible to 574
green asserts:
· B's authored cowboy rig had not landed, so the carport corner ARRIVED DOWN
and the knife had no object.
· pay.garden = 0 had not landed; the first emergency invoice paid $38 for a
bed nobody had a prep phase to protect.
· rig.breakCorner?.() — the door is called failCorner. An optional call to a
renamed method is a silent no-op that reads exactly like a call.
· attach({at, wind}) was never passed: clockSkew read 30.02 on the one night
that starts mid-storm, and the knife's own invoice line said "9s in" on a
night you arrive at 30.
And the paper half of John's ruling: at ★0.0 the clamp eats the stand-up
charge, so the card promised a price the game would not take and the invoice
carried "−½★ standing" four rows above "★ 0.0 → ★ 0.0". The documents now print
what will actually be charged. Whether the ruling should have teeth at the
floor is John's, and it is filed with the measurement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
008de38766
commit
db1977f55d
@ -444,13 +444,47 @@ export const EMERGENCIES = [
|
||||
* over the one piece of collateral in this game that a player has already
|
||||
* been taught to fear.
|
||||
*/
|
||||
/**
|
||||
* ⚖️ **B's AUTHORED SET, ADOPTED VERBATIM — landed by D, and it should have landed at
|
||||
* integration.** B published it in THREADS with the measurements ([B] gate 1 §5) and A
|
||||
* pre-authorised the swap in the note above ("revert-and-tell-me applies… yours is canon the
|
||||
* moment it lands"). It never landed, because it is DATA IN A's FILE while B rules the
|
||||
* semantics. **I found it by playing the night:** the dispatch card read *"CB1 carabiner — ON
|
||||
* THE GROUND"*, which is a different night from the one this sprint is about.
|
||||
*
|
||||
* Why the difference IS the gate and not a detail: in the placeholder the CARPORT CORNER
|
||||
* ARRIVES ALREADY DOWN, so the $180 carport is `theirs` — unbillable, unwreckable — and there
|
||||
* is nothing left for the knife to save. DESIGN.md line 165's signature decision had no
|
||||
* object. In B's set `cb1` is a SHACKLE on the 0.22 beam (704 N effective, holding their roof)
|
||||
* which the southerly puts over its ceiling from t=35 to t=42, and `tr1b` — the 0.88 branch on
|
||||
* the cheapest steel — is the corner that arrived gone. That is B's measured triage window:
|
||||
* ride it out → cb1 blows @35.9 s and takes q1 → the carport WRECKED, $180
|
||||
* cut at t=33 → nothing breaks, the carport saved
|
||||
* 5.9 seconds. Measured by B on the rig; re-flown by D through the shipped game.
|
||||
*/
|
||||
rig: [
|
||||
{ anchorId: 'cb1', hw: 'carabiner', broken: true },
|
||||
{ anchorId: 'tr1', hw: 'carabiner' },
|
||||
{ anchorId: 'tr1b', hw: 'carabiner' },
|
||||
{ anchorId: 'q3', hw: 'carabiner' },
|
||||
{ anchorId: 'q3', hw: 'shackle' }, // sound post, still holding
|
||||
{ anchorId: 'q1', hw: 'carabiner' }, // under-specced
|
||||
{ anchorId: 'tr1b', hw: 'carabiner', broken: true }, // 0.88 branch + cheapest steel: already gone
|
||||
{ anchorId: 'cb1', hw: 'shackle' }, // ⚠️ the CARPORT BEAM, hint 0.22
|
||||
],
|
||||
},
|
||||
/**
|
||||
* ⚖️ **B's RULING ON THE ECONOMY OF TRIAGE, landed by D — same story as the rig above: a ruling
|
||||
* B made and could not apply, because it is per-job DATA in A's file.** ([B] gate 1 §2.)
|
||||
*
|
||||
* **The garden BONUS does not apply on a callout.** It is a per-HP payment for a bed you chose
|
||||
* hardware to protect; you arrive at t=30 with thirty seconds of somebody else's storm already
|
||||
* in that bed, under steel you did not buy. Paying per-HP prices the PREVIOUS contractor's
|
||||
* geometry and docks you for their mistakes. Data, not a branch — so A's acceptance criterion
|
||||
* (§10: `settle()` runs unchanged on an emergency) holds exactly.
|
||||
*
|
||||
* **The garden's WIN still applies, and that is the knife's price.** `win = hp >= 50` gates the
|
||||
* FEE (`PAY.lostNightShare`), so cutting spends garden HP — and can spend 65% of the callout —
|
||||
* to buy certainty on their property. Ride it out for the full double rate and gamble their
|
||||
* carport; cut and take the smaller cheque. The garden is the STAKE, not the wage.
|
||||
*/
|
||||
pay: { garden: 0 },
|
||||
_why: 'THE PAIRING IS THE POOL\'S OWN AUDITED corner_block_southerly (A, S17 gate 1, '
|
||||
+ 'audit.html): 19 holding lines / 7 marginal / 38 unholdable, cheapest hold $40 '
|
||||
+ '(tr1,tr1b,q1,q3 → garden 87.6 FULL). ⚠️ THAT AUDIT ANSWERS A DIFFERENT QUESTION and '
|
||||
|
||||
@ -81,6 +81,28 @@ const starDelta = (d) => {
|
||||
return `${d < 0 ? '−' : '+'}${whole || !half ? whole : ''}${half ? '½' : ''}★`;
|
||||
};
|
||||
|
||||
/**
|
||||
* ⚠️ **SPRINT18 gate 1.3 [Lane D] — WHAT STANDING A CLIENT UP WILL ACTUALLY COST, WHICH IS NOT
|
||||
* ALWAYS `REP.STOOD_UP`.** Found by playing the week to ★0.0 and taking the emergency anyway.
|
||||
*
|
||||
* `settle()` clamps rep at `REP.MIN`, so at ★0.0 a −½★ debit moves NOTHING — and both papers went on
|
||||
* promising it. On the offer card that is a price the game will not charge; on the invoice it is
|
||||
* worse, because the same card carries the row *"stood up the Hendersons · −½★ standing"* four lines
|
||||
* above *"reputation ★ 0.0 → ★ 0.0"*. **The invoice arguing with itself**, which is the defect class
|
||||
* this repo has spent five sprints hunting, on John's own ruling.
|
||||
*
|
||||
* This function is the PAPER half only, and the split is deliberate: whether the ruling should have
|
||||
* teeth at the floor is a DESIGN question and it is John's (filed in THREADS with the measurement —
|
||||
* at ★0.0 a fee-maximiser can dodge every booked night for free again, which is precisely the player
|
||||
* the ruling was written for). Until he rules, the documents tell the truth about the number.
|
||||
*
|
||||
* @param {number} n how many clients would be stood up
|
||||
* @param {number|null} rep the standing the charge lands against (`wk.rep` / `w.repBefore`)
|
||||
*/
|
||||
const standUpCost = (n, rep) => (rep == null
|
||||
? REP.STOOD_UP * n
|
||||
: Math.max(REP.MIN, rep + REP.STOOD_UP * n) - rep);
|
||||
|
||||
// Load bar colours: green → amber → red. The amber band is deliberately wide;
|
||||
// a corner at 60% in a lull is one gust from 100% and the player should feel
|
||||
// that before the bar turns red on them.
|
||||
@ -943,10 +965,15 @@ export function createHud(d) {
|
||||
as plural — night 7's brief says "The Vasilaros place again — THEY
|
||||
asked for you by name". The spec's own example wording is "the
|
||||
Hendersons are expecting you". So: no agreement logic at all. */''}
|
||||
${/* SPRINT18 gate 1.3 [D] — the cost AS IT WILL BE CHARGED (standUpCost): at ★0.0 the
|
||||
clamp eats it, and a card promising a price the invoice will not take is the same lie
|
||||
as a card hiding one. The words change with it, because "−0★" would be gibberish. */''}
|
||||
${o.standsUp?.length ? `<div class="line cost"><span>${
|
||||
o.standsUp.map((s) => s.client ?? 'a client').join(' and ')
|
||||
} are expecting you</span><b>${
|
||||
starDelta(REP.STOOD_UP * o.standsUp.length)} standing</b></div>` : ''}
|
||||
standUpCost(o.standsUp.length, wk.rep) === 0
|
||||
? 'nothing left to dock'
|
||||
: `${starDelta(standUpCost(o.standsUp.length, wk.rep))} standing`}</b></div>` : ''}
|
||||
${cons.length ? `<div class="cons">⚖ ${cons.map((c) => c.label).join(' · ')}</div>` : ''}
|
||||
${cons.map((c) => `<div class="says">“${c.says}”</div>`).join('')}
|
||||
${/* gate 0, clause 3 — and if you have done this to THEM before, it is
|
||||
@ -1496,9 +1523,14 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
<div class="row"><span>you took this callout</span><b>${w.client ?? 'no client'}</b></div>
|
||||
${w.stoodUp?.length
|
||||
? w.stoodUp.map((s) => `
|
||||
<div class="row bad"><span>stood up ${s.client ?? 'a client'} — they were expecting you</span><b>${
|
||||
starDelta(REP.STOOD_UP)} standing</b></div>
|
||||
<div class="cond">No invoice for that one. They will remember the phone call.</div>`).join('')
|
||||
<div class="row ${standUpCost(1, w.repBefore) === 0 ? '' : 'bad'}"><span>stood up ${
|
||||
s.client ?? 'a client'} — they were expecting you</span><b>${
|
||||
standUpCost(1, w.repBefore) === 0
|
||||
? 'nothing left to dock'
|
||||
: `${starDelta(standUpCost(1, w.repBefore))} standing`}</b></div>
|
||||
<div class="cond">${standUpCost(1, w.repBefore) === 0
|
||||
? 'Your standing is already on the floor, so this one was free. They will remember the phone call anyway.'
|
||||
: 'No invoice for that one. They will remember the phone call.'}</div>`).join('')
|
||||
: '<div class="row"><span>nobody was expecting you — nothing to stand up</span><b>—</b></div>'}
|
||||
</div>` : ''}
|
||||
${/* 🚨 SPRINT18 gate 1 — THE EMERGENCY'S PAPERWORK. Three things the invoice
|
||||
@ -1521,9 +1553,20 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
${(w.inherited ?? []).map((c) => `
|
||||
<div class="row"><span>${c.anchorId.toUpperCase()} — ${c.hw}, already on the ground when you arrived</span><b>not billed</b></div>
|
||||
<div class="cond">That corner is what the callout was for. It is not on you.</div>`).join('')}
|
||||
${(w.cutAways ?? []).map((c) => `
|
||||
</div>` : ''}
|
||||
${/* 🚨 SPRINT18 gate 1.3 [Lane D] — **THE KNIFE'S LINE, ON ANY NIGHT.** It shipped inside the
|
||||
EMERGENCY ATTENDANCE block, so a sail cut loose on an ordinary Tuesday printed NOTHING —
|
||||
the verb is available every night the sail is up, and principle 2 does not have a "only
|
||||
on callouts" clause. Its own section for the same reason the emergency has one: this is
|
||||
not a charge and not a bonus, it is a decision, and the ledger below has no row shape for
|
||||
that. `reason` is printed because B put it in the payload for the paperwork and D fills
|
||||
it with what the cut was FOR — the sentence a player would say out loud. */''}
|
||||
${w?.cutAways?.length ? `
|
||||
<div class="sect">THE KNIFE</div>
|
||||
<div class="sched">
|
||||
${w.cutAways.map((c) => `
|
||||
<div class="row"><span>sail cut loose${c.corners?.length ? ` at ${c.corners.map((x) => String(x).toUpperCase()).join(', ')}` : ''} — ${
|
||||
Math.round(c.t)}s in, deliberate</span><b>your call</b></div>
|
||||
Math.round(c.t)}s in, deliberate${c.reason ? ` · for ${c.reason}` : ''}</span><b>your call</b></div>
|
||||
<div class="cond">You spent the sail to keep the steel. That is the trade the knife is for.</div>`).join('')}
|
||||
</div>` : ''}
|
||||
${w?.constraints?.length ? `
|
||||
|
||||
@ -223,7 +223,7 @@ export function applyMute(sky, on) {
|
||||
*
|
||||
* @returns {{verdict: string, mode: string}}
|
||||
*/
|
||||
export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped, beyondSaving = false }) {
|
||||
export function verdictFor({ hp, lost, cut = [], win, dmg, pondPeak, pondDumped, beyondSaving = false }) {
|
||||
// SPRINT12 — B's flag, actioned: "weakest link that went first" reduces on
|
||||
// the EFFECTIVE rating (rating × ratingHint), matching sail.js's failure
|
||||
// line and B's summary.weakest tie-break fix. On the bare rating a rated
|
||||
@ -237,6 +237,33 @@ export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped, beyondSav
|
||||
const named = worst ? `${worst.hw.name} at ${worst.anchorId.toUpperCase()}` : '';
|
||||
const hailKilled = dmg.hail > dmg.rain;
|
||||
|
||||
/**
|
||||
* 🚨 SPRINT18 gate 1.3 [Lane D] — **THE KNIFE'S OWN VERDICT, and without it this function calls a
|
||||
* deliberate cut "the hail fell where your sail wasn't".**
|
||||
*
|
||||
* That is mode `uncovered`, and it is the single worst thing the card could say to a player who
|
||||
* just made the game's signature decision: it blames placement on a night where the sail came down
|
||||
* because they cut it down. The verdict's whole charter is that it reads what ACTUALLY happened.
|
||||
*
|
||||
* It fires only when nothing of YOURS failed, and that composition rule is deliberate rather than
|
||||
* tidy: if a corner blew as well, that failure is the louder fact and the modes below say it
|
||||
* truthfully — the cut is still named on the invoice's own line. So the knife speaks when the knife
|
||||
* is the story, and shuts up when it isn't.
|
||||
*
|
||||
* "Nothing of theirs" is true BY CONSTRUCTION here, not by hope: `collateralWalk` prices `yours`,
|
||||
* and with `yours` empty the gnome's two-corner rule and every structure key are empty too.
|
||||
*/
|
||||
if (cut.length && !lost.length) {
|
||||
const where = cut.map((c) => c.anchorId.toUpperCase()).join(', ');
|
||||
return win
|
||||
? { mode: 'cut-held',
|
||||
verdict: `YOU CUT IT LOOSE at ${where} — and the bed came through anyway. `
|
||||
+ 'Nothing of theirs is on the ground, and nothing of yours broke.' }
|
||||
: { mode: 'cut',
|
||||
verdict: `YOU CUT IT LOOSE at ${where}. The bed went with it — and their property didn't. `
|
||||
+ 'That was the trade.' };
|
||||
}
|
||||
|
||||
// SPRINT9 decision 1. This case had to come FIRST because it did not used to
|
||||
// exist: `lost >= 2` was unconditionally a cascade-and-a-loss, and under the
|
||||
// new rule it can be the best night the game has. A sail that tore itself
|
||||
@ -353,6 +380,17 @@ export function rigRecordFor(corners, tally = new Map(), theirs = new Set()) {
|
||||
repaired: broke && !c.broken,
|
||||
brokenAtDawn: !!c.broken,
|
||||
theirs: theirs.has(c.anchorId),
|
||||
/**
|
||||
* SPRINT18 gate 1.3 [D] — you let this one go on purpose. week.js reads it exactly where it
|
||||
* already reads `theirs`, and for the same reason: warranty and standing are about work that
|
||||
* FAILED. A corner still down at dawn because you cut it booked a free callout to go and
|
||||
* re-hang your own decision, and docked half a star for taking it.
|
||||
*
|
||||
* NOTE it does not suppress `broke`/`brokenAtDawn`. Those two are the RECORD — the sail really
|
||||
* is on the ground and the client can see it — and a record that edits itself to be flattering
|
||||
* is the one thing this ledger has never done.
|
||||
*/
|
||||
cut: !!c.cut,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -436,15 +474,83 @@ export function priceOffer(night, { def = null, site = null, rep = 3, remembers
|
||||
export function answerableFor(corners, emergency = null) {
|
||||
const theirs = new Set((emergency?.rig ?? []).filter((c) => c.broken).map((c) => c.anchorId));
|
||||
const lost = (corners ?? []).filter((c) => c.broken);
|
||||
/**
|
||||
* 🚨 **SPRINT18 gate 1.3 [Lane D] — A CORNER YOU CUT IS NOT A CORNER THAT FAILED, AND WITHOUT
|
||||
* THIS LINE THE KNIFE IS A TRAP.**
|
||||
*
|
||||
* B's `cutAway` deliberately emits no `break` ("warranty must never chase a corner the player
|
||||
* chose to let go") — but every ledger surface downstream reads `c.broken`, and `_releaseCorner`
|
||||
* sets it for a cut exactly as it does for an overload. So on the tree as merged, taking DESIGN.md
|
||||
* line 165's signature decision billed you for it on five surfaces at once: the cut corner's
|
||||
* hardware went on `bill`, the collateral walk BILLED AND WRECKED the $180 carport you cut the
|
||||
* sail to save, a four-corner cut tripped the gnome's two-corner rule, `clean` was voided by the
|
||||
* collateral it caused, and week.js booked warranty and docked standing for every corner you let
|
||||
* go. The invoice would have printed *"You spent the sail to keep the steel. That is the trade the
|
||||
* knife is for"* directly above a $180 charge for the thing you kept.
|
||||
*
|
||||
* Found by playing it, not by a test — the same way every card bug in this repo has been found.
|
||||
* A is the author of both halves and neither half is wrong; nobody joined them, because the join
|
||||
* is a flag on an object and there was no verb setting the flag until now.
|
||||
*
|
||||
* The ruling, and it is B's extended to every surface that reads a corner: **a cut is a DECISION.
|
||||
* It bills nothing, wrecks nothing, books no warranty and costs no standing. Its price is the
|
||||
* garden — and through the garden, the night's fee — and that is enough.** `lost` still counts it
|
||||
* (the HUD's corners-intact read must stay honest: the sail IS on the ground), and `cut` is named
|
||||
* so the paper can say who put it there.
|
||||
*/
|
||||
const cut = lost.filter((c) => c.cut);
|
||||
return {
|
||||
theirs,
|
||||
lost,
|
||||
yours: lost.filter((c) => !theirs.has(c.anchorId)),
|
||||
cut,
|
||||
yours: lost.filter((c) => !theirs.has(c.anchorId) && !c.cut),
|
||||
inherited: lost.filter((c) => theirs.has(c.anchorId))
|
||||
.map((c) => ({ anchorId: c.anchorId, hw: c.hw?.name ?? '?' })),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚨 **SPRINT18 gate 1.3 [Lane D] — THE COLLATERAL WALK, EXTRACTED.** What a set of failed corners
|
||||
* costs you in the client's property. Lifted OUT of scoreRun unchanged, for the two reasons
|
||||
* `answerableFor` and `rigRecordFor` were extracted before it, and one new one:
|
||||
*
|
||||
* · scoreRun lives inside `boot()` behind a WebGL context where no assert can reach it.
|
||||
* · **the knife's HUD read has to promise exactly what the invoice will honour.** The panel says
|
||||
* "saves the carport $180" before the cut and the invoice charges $0 after it; if those two ever
|
||||
* came off different arithmetic, the game would be advertising a saving. One walk, two readers —
|
||||
* the same discipline that made `calloutFee` one function for the board, the dispatch and the
|
||||
* invoice.
|
||||
*
|
||||
* PURE, and that is load-bearing: `wreckStructure()` stays at the call site. A HUD asking what a cut
|
||||
* would save must not swap a structure for its wreck as a side effect of the question.
|
||||
*
|
||||
* @param {Array} corners the corners to price — `yours` at dawn, or the LIVE ones for the knife
|
||||
* @param {object} world contracts World (anchor(), collateralFor(), gnome)
|
||||
* @returns {{items: {what:string,cost:number}[], keys: string[], total: number}}
|
||||
*/
|
||||
export function collateralWalk(corners, world) {
|
||||
const items = [];
|
||||
const keys = [];
|
||||
// The gnome is collateral if the sail came down over it (DESIGN.md: the worst debris in any storm
|
||||
// is your own failed work). Two corners, per the original rule.
|
||||
if ((corners ?? []).length >= 2 && world?.gnome) {
|
||||
items.push({ what: 'garden gnome', cost: world.gnome.collateralValue });
|
||||
}
|
||||
/**
|
||||
* Priced per STRUCTURE, not per corner: two beams letting go is one carport gone, not $360.
|
||||
*/
|
||||
const taken = new Set();
|
||||
for (const c of corners ?? []) {
|
||||
const key = world?.anchor ? world.anchor(c.anchorId)?.collateral : null;
|
||||
const priced = key && world.collateralFor ? world.collateralFor(key) : null;
|
||||
if (!priced || taken.has(key)) continue;
|
||||
taken.add(key);
|
||||
keys.push(key);
|
||||
items.push({ what: priced.label, cost: priced.cost });
|
||||
}
|
||||
return { items, keys, total: items.reduce((s, i) => s + i.cost, 0) };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase machine
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -723,7 +829,18 @@ export async function boot(opts = {}) {
|
||||
// world into its ground clamp, solid collider, ladder and broom, none of
|
||||
// which have a setter (Lane D's modules). A site change only happens at a
|
||||
// forecast, never mid-storm, so a clean rebuild is honest and cheap.
|
||||
player = await createPlayer(scene, world, cameraRig, { wind, interact });
|
||||
// SPRINT18 gate 1.3 — D's knife rides in on the opts it already had. Every one of these is a
|
||||
// GETTER because `game` and `garden` are declared below the first call to this function (the
|
||||
// TDZ trap documented twice above); the knife reads them at frame time, when they exist.
|
||||
player = await createPlayer(scene, world, cameraRig, {
|
||||
wind,
|
||||
interact,
|
||||
getPhase: () => game.phase,
|
||||
getGarden: () => garden,
|
||||
/** One walk, two readers: what the panel promises is what the invoice honours. */
|
||||
collateralAtRisk: collateralWalk,
|
||||
secsLeft: () => STORM_LEN - game.phaseT,
|
||||
});
|
||||
|
||||
// SPRINT13 gate 2.5 — the opening frame, per YARD rather than per game.
|
||||
// Done here because the spawn is here: the player and the yard are rebuilt
|
||||
@ -882,10 +999,15 @@ export async function boot(opts = {}) {
|
||||
* see (this wire lives inside `boot()` behind a WebGL context). The fallback is
|
||||
* one `??` and it agrees with the argument on every wired path.
|
||||
*/
|
||||
async function rigSail(anchorIds, hwChoices, tension = 1.0, fabric) {
|
||||
/**
|
||||
* @param {object} [opts] `{at, wind}` — B's `attach` door (sail.js). Absent/`at:0` is
|
||||
* byte-for-byte the old behaviour, so every ordinary night and every bench is untouched.
|
||||
* See the emergency's note on `onCommit` for the one caller that passes it.
|
||||
*/
|
||||
async function rigSail(anchorIds, hwChoices, tension = 1.0, fabric, opts) {
|
||||
const cloth = fabric ?? rigging?.session?.fabric;
|
||||
if (cloth) rig.setFabric(cloth);
|
||||
rig.attach(anchorIds, hwChoices, tension);
|
||||
rig.attach(anchorIds, hwChoices, tension, opts);
|
||||
disposeSailView();
|
||||
sailView = await createSailView(rig);
|
||||
scene.add(sailView);
|
||||
@ -996,7 +1118,27 @@ export async function boot(opts = {}) {
|
||||
// verbatim; `fabric` is undefined on bare lane/a (their commit() passes it,
|
||||
// and that lands at integration) and rigSail falls back to session.fabric so
|
||||
// the sim flies the right cloth on both branches. See rigSail's note.
|
||||
onCommit: (ids, hw, tension, fabric) => { void rigSail(ids, hw, tension, fabric); },
|
||||
/**
|
||||
* 🚨 SPRINT18 gate 1.3 [D] — **`{at, wind}`: THE CLOTH FINALLY ARRIVES IN THE STORM IT IS
|
||||
* STANDING IN.** C measured it and B fixed it and nobody wired it, so on the merged tree the one
|
||||
* night that starts mid-storm attached its sail at t=0: measured live by D on the shipped
|
||||
* emergency, **`rig.clockSkew` read 30.02** while the sky, garden, debris and player were all
|
||||
* thirty seconds in. B's own number for what that hides: the same rig entering the same storm
|
||||
* pulls **90 N at t=0 and 655 N at t=40**. The triage that this whole gate is about was being
|
||||
* judged against a sail feeling a seventh of the load, and the knife's own invoice line said
|
||||
* *"cut loose 9s in"* on a night you arrived at 30 — because the log stamps the SAIL's clock.
|
||||
*
|
||||
* The night is asked, not a flag: `week.job.emergency` is a fact the night already owns, so this
|
||||
* needs no new state and no ordering change (`adoptEmergencyRig` runs before `setPhase`, so
|
||||
* reading `game.phaseT` here would read the forecast's clock — which is how it would go wrong).
|
||||
* With a `wind` B's `attach` genuinely PRE-FLIES the thirty seconds rather than teleporting the
|
||||
* cloth into a gale, so the drape and the belly's water are real history.
|
||||
*/
|
||||
onCommit: (ids, hw, tension, fabric) => {
|
||||
const em = week.job?.emergency;
|
||||
void rigSail(ids, hw, tension, fabric,
|
||||
em ? { at: em.arriveAt, wind: winds[week.stormKey] } : undefined);
|
||||
},
|
||||
onMessage: pushEvent,
|
||||
panel: true,
|
||||
});
|
||||
@ -1047,13 +1189,8 @@ export async function boot(opts = {}) {
|
||||
* subtitle read this.
|
||||
*/
|
||||
const em = week.job.emergency;
|
||||
const { theirs, lost, yours, inherited } = answerableFor(rig.corners, em);
|
||||
const { theirs, lost, yours, cut, inherited } = answerableFor(rig.corners, em);
|
||||
const bill = yours.reduce((s, c) => s + c.hw.cost, 0);
|
||||
const collateral = [];
|
||||
// The gnome is collateral if the sail came down over it. DESIGN.md: the
|
||||
// worst debris in any storm is your own failed work. `yours`, not `lost`:
|
||||
// two corners on the ground when you ARRIVED is not the sail you dropped.
|
||||
if (yours.length >= 2) collateral.push({ what: 'garden gnome', cost: world.gnome.collateralValue });
|
||||
|
||||
/**
|
||||
* SPRINT11 — the carport trap finally BILLS (§gate 1).
|
||||
@ -1072,16 +1209,13 @@ export async function boot(opts = {}) {
|
||||
*
|
||||
* Priced per STRUCTURE, not per corner: two beams letting go is one carport
|
||||
* gone, not $360. It bills once and the wreck swaps once.
|
||||
*
|
||||
* SPRINT18 gate 1.3 [D] — the ARITHMETIC moved out to `collateralWalk` (pure, exported, above)
|
||||
* so the knife's HUD read prices what it saves through this exact walk. The WRECK stays here:
|
||||
* a HUD asking "what would a cut save" must not swap a structure for its wreck.
|
||||
*/
|
||||
const taken = new Set();
|
||||
for (const c of yours) {
|
||||
const key = world.anchor(c.anchorId)?.collateral;
|
||||
const priced = key ? world.collateralFor(key) : null;
|
||||
if (!priced || taken.has(key)) continue;
|
||||
taken.add(key);
|
||||
collateral.push({ what: priced.label, cost: priced.cost });
|
||||
world.wreckStructure(key); // false offline/graybox — the bill still lands
|
||||
}
|
||||
const { items: collateral, keys: wrecked } = collateralWalk(yours, world);
|
||||
for (const key of wrecked) world.wreckStructure(key); // false offline/graybox — the bill still lands
|
||||
const s = rigging.summary;
|
||||
const hp = garden.hp;
|
||||
/**
|
||||
@ -1116,7 +1250,7 @@ export async function boot(opts = {}) {
|
||||
* answerable for; the corner count above it still reports every corner down,
|
||||
* because those are two different facts and both papers say so honestly.
|
||||
*/
|
||||
const { verdict, mode } = verdictFor({ hp, lost: yours, win, dmg, pondPeak, pondDumped,
|
||||
const { verdict, mode } = verdictFor({ hp, lost: yours, cut, win, dmg, pondPeak, pondDumped,
|
||||
beyondSaving: week.job.gardenBeyondSaving });
|
||||
|
||||
// SPRINT16 gate 2.2 [Lane B] — the fabric bet on the record. The F key
|
||||
@ -1144,7 +1278,13 @@ export async function boot(opts = {}) {
|
||||
win,
|
||||
subtitle: yours.length
|
||||
? `${yours.map((c) => `${c.hw.name} at ${c.anchorId.toUpperCase()}`).join(', ')} let go.`
|
||||
: inherited.length
|
||||
// SPRINT18 gate 1.3 [D] — a cut sail must not be headlined "Every corner held." The card
|
||||
// would then carry that line, a CUT LOOSE row in the ledger and a cut verdict underneath:
|
||||
// three accounts of one night, one of them false. `cut` before `inherited` because on the
|
||||
// shipped emergency both are true and the cut is the thing the PLAYER did.
|
||||
: cut.length
|
||||
? `You cut it loose at ${cut.map((c) => c.anchorId.toUpperCase()).join(', ')}.`
|
||||
: inherited.length
|
||||
// An emergency you held: nothing of yours went, and the corner that was
|
||||
// already down stays named rather than quietly counted as a hold.
|
||||
? `Nothing of yours let go. ${inherited.map((c) => c.anchorId.toUpperCase()).join(', ')} was on the ground when you got there.`
|
||||
@ -1169,9 +1309,18 @@ export async function boot(opts = {}) {
|
||||
* somebody else's four rated shackles at dawn, and the biggest earner in the
|
||||
* game would be turning up to other people's disasters and salvaging them.
|
||||
*/
|
||||
/**
|
||||
* SPRINT18 gate 1.3 [D] — **"LOSE THE SAIL, SAVE THE ANCHORS" IS A LEDGER LINE, NOT A
|
||||
* SLOGAN.** `!c.broken` alone excluded a CUT corner from the refund, so the knife also quietly
|
||||
* took your steel: you cut the cloth, and the shackle still bolted to the post — undamaged,
|
||||
* unclippable in thirty seconds at dawn — paid you nothing. DESIGN.md line 165 promises the
|
||||
* opposite in the same breath as the verb, and `bill` already agrees (a cut corner is not
|
||||
* `yours`, so its hardware is not written off). This is the other half of that sentence.
|
||||
* On an EMERGENCY it changes nothing — none of that steel is yours to unclip either way.
|
||||
*/
|
||||
intactHardwareValue: em
|
||||
? (rigging.session.spares ?? 0) * SPARE_COST
|
||||
: rig.corners.filter((c) => !c.broken).reduce((sum, c) => sum + c.hw.cost, 0)
|
||||
: rig.corners.filter((c) => !c.broken || c.cut).reduce((sum, c) => sum + c.hw.cost, 0)
|
||||
+ (rigging.session.spares ?? 0) * SPARE_COST,
|
||||
pondPeak,
|
||||
pondDumped,
|
||||
@ -1358,9 +1507,26 @@ export async function boot(opts = {}) {
|
||||
throw new Error(`main: the emergency rig would not commit — ${em.rig?.length ?? 0} corners `
|
||||
+ `on ${week.site}. Check every anchorId exists in that yard's site JSON.`);
|
||||
}
|
||||
// Corners that ARRIVED gone. Feature-detected; paper-only until B's door lands.
|
||||
/**
|
||||
* Corners that ARRIVED gone.
|
||||
*
|
||||
* ⚠️ **SPRINT18 gate 1.3 [D] — THE DOOR IS CALLED `failCorner`, AND FOR THIS WHOLE SPRINT THIS
|
||||
* LINE CALLED A METHOD THAT DOES NOT EXIST.** B opened it ([B] gate 1 §4) under the name
|
||||
* `failCorner`; this asked for `breakCorner?.()`, and an optional call to a renamed method is a
|
||||
* silent no-op that reads exactly like a call — **the `rigging.setWorld` lesson this file quotes
|
||||
* elsewhere, arriving through the door built to prevent it.** Measured live:
|
||||
* `typeof rig.failCorner === 'function'`, `typeof rig.breakCorner === 'undefined'`, and the
|
||||
* corner every card called "ON THE GROUND" was flying.
|
||||
*
|
||||
* A's fallback reasoning ("the authored corner is a $5 carabiner on the carport beam, so it fails
|
||||
* within seconds through the real overload path") was doing the work and hid the break — but it
|
||||
* only holds for A's placeholder rig. Under B's canon set the arrived-broken corner is `tr1b`, a
|
||||
* carabiner on a 0.88 branch that does NOT self-destruct, so the dispatch would have promised a
|
||||
* corner on the ground and the sim would have flown it all night. Still feature-detected, because
|
||||
* the pattern is right; only the name was wrong.
|
||||
*/
|
||||
for (const c of em.rig ?? []) {
|
||||
if (c.broken) rig.breakCorner?.(c.anchorId, { reason: 'arrived' });
|
||||
if (c.broken) rig.failCorner?.(c.anchorId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1577,7 +1743,11 @@ export async function boot(opts = {}) {
|
||||
// that doesn't wait), and M only once C's bus has a tap — hud owns that
|
||||
// question, because a help line that lists a dead key is a help line that
|
||||
// teaches a stranger the game is broken.
|
||||
hud.setHelp(`WASD move · shift run · E repair/pickup · C brace · RMB orbit · ${hud.comfortKeysHint()}`);
|
||||
// SPRINT18 gate 1.3 — X is on the help line because a key nobody advertises is a key nobody
|
||||
// presses, and the knife is the one verb the game has to offer BEFORE the moment it is needed.
|
||||
// "hold X cut sail" rather than "X cut sail": it is a hold, and the difference matters when the
|
||||
// window is six seconds long.
|
||||
hud.setHelp(`WASD move · shift run · E repair/pickup · hold X cut sail · C brace · RMB orbit · ${hud.comfortKeysHint()}`);
|
||||
}
|
||||
if (to === 'aftermath') {
|
||||
// The dawn comes up BEFORE the scoreboard, on E's 2.2 s ease. Their note
|
||||
|
||||
@ -646,7 +646,13 @@ export function createWeek(opts = {}) {
|
||||
// for a failure that predates you, which is the exact opposite of what the
|
||||
// warranty ledger was built to say. main.js's rigRecordFor sets the flag; the
|
||||
// rep block below skips it for the same reason.
|
||||
return (prev.rig ?? []).filter((c) => c.brokenAtDawn && !c.theirs).map((c) => ({
|
||||
// 🚨 SPRINT18 gate 1.3 [Lane D] — `!c.cut`: THE KNIFE. B's ruling on `cutAway` in sail.js is
|
||||
// that a cut is a DECISION and emits no `break`, precisely so "warranty must never chase a
|
||||
// corner the player chose to let go" — but warranty is booked off `brokenAtDawn`, which a cut
|
||||
// sets, so without this line the ledger sent you back unpaid tomorrow to re-hang a sail you
|
||||
// deliberately spent to save the client's carport. Same shape as the `!c.theirs` line above it,
|
||||
// one verb along.
|
||||
return (prev.rig ?? []).filter((c) => c.brokenAtDawn && !c.theirs && !c.cut).map((c) => ({
|
||||
anchorId: c.anchorId, hw: c.hw, cost: c.hwCost,
|
||||
night: prev.night, site: prev.site, addr: prev.addr, client: prev.client,
|
||||
}));
|
||||
@ -940,8 +946,12 @@ export function createWeek(opts = {}) {
|
||||
// SPRINT18 gate 1 — their corners are not your record. A rig you inherited
|
||||
// mid-storm cannot cost you a clean night for breaking before you got there,
|
||||
// and cannot cost you standing for still being down at dawn.
|
||||
const brokeCount = rigRec.filter((c) => c.broke && !c.theirs).length;
|
||||
const dawnBroken = rigRec.filter((c) => c.brokenAtDawn && !c.theirs).length;
|
||||
// SPRINT18 gate 1.3 — and a corner you CUT is not your record either. Standing is what the
|
||||
// client thinks of your work; the knife is the work. Its price is the garden (and the fee the
|
||||
// garden carries), ruled by B and measured by D — not a second, silent bite at the star the
|
||||
// same night. `!c.cut` twice, for the same reason `!c.theirs` is there twice.
|
||||
const brokeCount = rigRec.filter((c) => c.broke && !c.theirs && !c.cut).length;
|
||||
const dawnBroken = rigRec.filter((c) => c.brokenAtDawn && !c.theirs && !c.cut).length;
|
||||
const collateralEvents = (run.collateral ?? []).length;
|
||||
const repBefore = rep;
|
||||
let repDelta = 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user