Paste E's letterhead/invoice kit into hud.js; rule HARD YARDS + sequential ABN
Gate 3.1: the CUT block from tools/jobsheet verbatim — letterhead, billto, brief, sched, .void, AMOUNT DUE, terms. Docket numbers shared across the pair (JOB/INV 1040+night); week.js nights carry an addr. Cards scroll when taller than the window (safe center + overflow). Both rulings adopted and pinned by DOM asserts; selftest 317/0/0.
This commit is contained in:
parent
8decac0626
commit
3d89ce8fe8
43
THREADS.md
43
THREADS.md
@ -3966,3 +3966,46 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
· **gate 3 — John plays the week. FIVE sprints standing, and for the first time the excuse is
|
||||
gone: the week is playable end to end.** §BROKE_BELOW watch: if the bank runs away, A's
|
||||
declared first cut is the $20 clean bonus, not feeFor/gardenBonusMax.
|
||||
|
||||
[A] 2026-07-17 — 📄 **E's letterhead/invoice kit is PASTED (SPRINT12 gate 3.1), and both rulings are made.**
|
||||
The CUT block went into hud.js verbatim — CSS and markup shape both — replacing my Sprint 11
|
||||
placeholder rules that were holding the seat for exactly this. `.void` survives, exactly as E built
|
||||
it: the forfeited clean bonus renders struck-through at the QUOTED amount, not zeroed and not
|
||||
hidden. Both documents now carry the same letterhead and share a docket number (JOB 1043's morning
|
||||
is INV 1043; `docketNo = 1040 + night`), the client moved into E's bill-to block, and week.js grew
|
||||
an `addr` per night ("14 Kurrajong St — the backyard" / "2 Bight Rd — the corner block") because a
|
||||
letterhead bills somebody somewhere. AMOUNT DUE landed with it: pay − spent, 20px, red when
|
||||
negative — the invoice finally has a conclusion, and the bank line under it stays because "what the
|
||||
night was worth" and "where that leaves you" are different facts. Both terms lines shipped; the
|
||||
invoice's collateral variant amended from E's "back up by Thursday" to "made right by Friday" so it
|
||||
reads against any broken thing and lands on the Hendersons' return day.
|
||||
|
||||
**RULING — the masthead is HARD YARDS, adopted as proposed.** E's reasoning holds on all three
|
||||
legs: it's the game's own joke ("nobody thanks you for the storm that did nothing" IS the hard
|
||||
yards), it's literally about yards, and it's one word at letterhead size. Their caveat is adopted
|
||||
with it: if a future sprint names the player, a surname mark is the better answer and one constant
|
||||
changes. Not renaming the outfit in a juice pass either.
|
||||
|
||||
**RULING — the ABN stays sequential, and this is now pinned by an assert.** Any plausible 11-digit
|
||||
number is potentially a real business's tax identity; sequential digits read as "example" the way
|
||||
555 numbers do. Deliberate, not lazy — a.test now FAILS if someone "fixes" it realistic.
|
||||
|
||||
Two things the paste taught, both logged as warnings elsewhere and both ignored by me anyway:
|
||||
· **The card CSS is a template literal and I put backticks in a comment** ("engines that don't
|
||||
know `safe`"). node --check PASSED — the stray pair re-paired with a later literal into
|
||||
accidentally-valid JS — and the browser's import of hud.js died on "Unexpected identifier
|
||||
'safe'", which took boot down with it. The dawn block's own comment has warned about exactly
|
||||
this since E wrote it. Caught by the browser, not the linter; `node -e "import(...)"` is the
|
||||
honest check and --check is not.
|
||||
· **E's kit made the tall cards taller than a short window** — at 800×450 the RIG IT button was
|
||||
below the fold of an unscrollable overlay, a soft-lock with good typography. #hud-card is now
|
||||
`place-items:safe center` + `overflow-y:auto` (plain `center` clips BOTH ends of overflow; the
|
||||
un-safe value stays above it as fallback). Seen in the pane, invisible to every assert.
|
||||
|
||||
Verified by LOOKING, per the standing rule: job sheet night 1 and the carport invoice both rendered
|
||||
in the browser against E's four preview PNGs — letterhead, bill-to, struck $20, red −$130 due,
|
||||
terms lines, all there. Plus two new a.test DOM renders asserting the kit's load-bearing pieces
|
||||
(mark, docket, sequential ABN, .void at quoted amount WITH line-through actually drawing, due
|
||||
arithmetic, neg class) — because both of last sprint's card bugs were invisible to a suite that
|
||||
never rendered a card. hud.dispose() also stops leaking the dawn div. selftest **317/0/0**
|
||||
(315 + the two card renders).
|
||||
|
||||
@ -20,6 +20,33 @@ import { forecastLines, leadFor } from './weather.js';
|
||||
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
||||
const kmh = (ms) => ms * 3.6;
|
||||
|
||||
/**
|
||||
* SPRINT12 gate 3 — the letterhead, and the two rulings E left with it
|
||||
* (tools/jobsheet/index.html; full reasoning in THREADS [A]).
|
||||
*
|
||||
* RULING 1 — the masthead is HARD YARDS, adopted as proposed. It's the joke the
|
||||
* game already tells ("nobody thanks you for the storm that did nothing") said
|
||||
* as a business name, it's literally about yards, and it's one mark at
|
||||
* letterhead size. E's own caveat stands with it: the day the player gets a
|
||||
* name, a surname mark is the better answer, and this constant is the only
|
||||
* thing that changes.
|
||||
*
|
||||
* RULING 2 — the ABN stays sequential, exactly as E shipped it. Any plausible
|
||||
* 11-digit number is potentially a REAL business's tax identity, and we are not
|
||||
* printing a stranger's ABN on a game card. Sequential digits read as "example"
|
||||
* the way 555 phone numbers do — deliberate, not lazy. DO NOT "fix" this into
|
||||
* something realistic; that is the bug, not the polish.
|
||||
*/
|
||||
const BUSINESS = { mark: 'HARD YARDS', trade: 'landscaping · shade · drainage' };
|
||||
const ABN = 'ABN 12 345 678 901';
|
||||
|
||||
/**
|
||||
* Docket numbers. 1040 + night: a running count off a small outfit's pad, and
|
||||
* the job sheet and its invoice share the number — JOB 1043's morning is
|
||||
* INV 1043, which is how the two documents answer each other line for line.
|
||||
*/
|
||||
const docketNo = (night) => 1040 + night;
|
||||
|
||||
// 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.
|
||||
@ -49,11 +76,19 @@ const CSS = `
|
||||
color:#93a6b2; opacity:.85; white-space:nowrap; }
|
||||
|
||||
#hud-card { position:fixed; inset:0; z-index:30; display:none; place-items:center;
|
||||
/* SPRINT12: E's letterhead + brief + conditions made the tall cards taller
|
||||
than a short window, and a RIG IT button below the fold with no scroll is a
|
||||
soft-lock with good typography. 'safe center' keeps the top reachable when
|
||||
the card overflows (plain 'center' clips BOTH ends); the value above is the
|
||||
fallback for engines that don't know 'safe'. Seen, not reasoned: the button
|
||||
was gone at 800x450. (And no backticks in this comment — this whole CSS
|
||||
block is a JS template literal, as the dawn block already warns.) */
|
||||
place-items:safe center; overflow-y:auto;
|
||||
background:#060a0dc4; backdrop-filter:blur(3px);
|
||||
font:13px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace; color:#dde5ea; }
|
||||
#hud-card.on { display:grid; }
|
||||
#hud-card .card { background:#0d1418; border:1px solid #2f3f4a; border-radius:9px;
|
||||
padding:22px 26px; min-width:440px; max-width:620px; pointer-events:auto; }
|
||||
padding:22px 26px; margin:20px 0; min-width:440px; max-width:620px; pointer-events:auto; }
|
||||
#hud-card h1 { margin:0 0 2px; font-size:19px; letter-spacing:.16em; color:#fff; }
|
||||
#hud-card h2 { margin:0 0 16px; font-size:12px; font-weight:400; color:#8ba0ad; letter-spacing:.04em; }
|
||||
#hud-card .row { display:flex; justify-content:space-between; gap:18px; padding:3px 0;
|
||||
@ -84,22 +119,126 @@ const CSS = `
|
||||
#hud-card .ledger .row.total { margin-top:6px; padding-top:8px; border-top:1px solid #24343d; }
|
||||
#hud-card .ledger .row.total b { color:#fff; }
|
||||
|
||||
/* SPRINT11 — the job sheet and the invoice. STRUCTURE ONLY, deliberately: the
|
||||
letterhead and the invoice styling are Lane E's this sprint (§gate 2), and
|
||||
everything below is the plainest thing that reads correctly so E can restyle
|
||||
it without unpicking layout. The class names are the contract — .letterhead,
|
||||
.brief, .jobsheet, .tomorrow — and E owns what they look like. */
|
||||
#hud-card .letterhead { display:flex; justify-content:space-between; align-items:baseline;
|
||||
margin:-4px 0 10px; padding-bottom:8px; border-bottom:1px solid #24343d;
|
||||
font-size:12px; letter-spacing:.18em; text-transform:uppercase; color:#7ee0ff; }
|
||||
#hud-card .letterhead .inv { color:#8ba0ad; letter-spacing:.1em; }
|
||||
#hud-card .brief { margin:10px 0 12px; padding:9px 11px; border-left:2px solid #2f3f4a;
|
||||
background:#101a20; color:#c3d2da; font-size:13px; line-height:1.55; font-style:italic; }
|
||||
#hud-card .jobsheet { margin-top:12px; padding-top:10px; border-top:1px solid #24343d; }
|
||||
#hud-card .jobsheet .row b { color:#a8f0b8; }
|
||||
#hud-card .jobsheet .row.total { margin-top:6px; padding-top:8px; border-top:1px solid #24343d; }
|
||||
#hud-card .jobsheet .row.total b { color:#fff; }
|
||||
/* Tomorrow is a rumour, and it should look like one next to tonight's numbers. */
|
||||
/* SPRINT12 — E's letterhead/invoice kit, pasted from tools/jobsheet/index.html
|
||||
(between the CUT markers, verbatim). Sprint 11's placeholder rules for
|
||||
.letterhead/.brief are gone — E designed against those class names and this
|
||||
is the styling they were holding the seat for. E's design commentary kept:
|
||||
the reasoning is why the rules look like this. */
|
||||
|
||||
/* --- the letterhead ----------------------------------------------------
|
||||
One business, two documents. This is the whole trick of the sprint: the
|
||||
job sheet and the invoice carry the SAME letterhead, so they read as a
|
||||
pair of papers off the same outfit's printer rather than as two game
|
||||
screens. The player's business is the constant; the client changes every
|
||||
night. That's the framing DESIGN.md opens with — "you run a small
|
||||
landscaping outfit" — finally showing up on the glass.
|
||||
|
||||
Sits above the existing .card content, inside the same .card box. */
|
||||
#hud-card .letterhead {
|
||||
display: flex; align-items: baseline; justify-content: space-between;
|
||||
gap: 16px; margin: -4px 0 14px; padding-bottom: 10px;
|
||||
border-bottom: 2px solid #2f3f4a;
|
||||
}
|
||||
#hud-card .letterhead .mark {
|
||||
font-weight: 800; font-size: 15px; letter-spacing: .22em; color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* The trade line does the work a logo would. Lowercase and spaced out so it
|
||||
reads as printed rule, not as a heading competing with the mark. */
|
||||
#hud-card .letterhead .trade {
|
||||
font-size: 10px; letter-spacing: .13em; color: #6d818e; margin-top: 3px;
|
||||
}
|
||||
#hud-card .letterhead .docket { text-align: right; white-space: nowrap; }
|
||||
#hud-card .letterhead .docket .kind {
|
||||
font-weight: 700; font-size: 11px; letter-spacing: .18em; color: #8ba0ad;
|
||||
}
|
||||
#hud-card .letterhead .docket .no { font-size: 10px; color: #55677a; letter-spacing: .06em; }
|
||||
|
||||
/* --- bill-to / job-for block -------------------------------------------
|
||||
Deliberately the same component on both documents. On the job sheet it's
|
||||
who you're working for; on the invoice it's who's paying. Same box, and
|
||||
the fact that it doesn't move between the two is the joke landing. */
|
||||
#hud-card .billto { margin: 0 0 12px; }
|
||||
#hud-card .billto .who { font-weight: 700; color: #fff; letter-spacing: .04em; }
|
||||
#hud-card .billto .addr { font-size: 11px; color: #6d818e; }
|
||||
|
||||
/* --- the brief ---------------------------------------------------------
|
||||
The client's own words. Italic and rule-marked because it is the one bit
|
||||
of text on the card that isn't the game talking to the player — it's a
|
||||
person who wants their seedlings alive. It should feel quoted, and it
|
||||
should be the thing the player's eye lands on after the letterhead,
|
||||
because it is the only place the stakes are ever said out loud. */
|
||||
#hud-card .brief {
|
||||
margin: 0 0 14px; padding: 9px 0 9px 12px;
|
||||
border-left: 2px solid #3f5561;
|
||||
color: #c3d0d9; font-style: italic; line-height: 1.6;
|
||||
}
|
||||
#hud-card .brief .attrib {
|
||||
display: block; margin-top: 5px; font-style: normal;
|
||||
font-size: 10px; letter-spacing: .1em; color: #55677a;
|
||||
}
|
||||
|
||||
/* --- section rules ------------------------------------------------------ */
|
||||
#hud-card .sect {
|
||||
margin: 16px 0 7px; font-size: 10px; font-weight: 700; letter-spacing: .18em;
|
||||
color: #55677a; border-bottom: 1px solid #1c262d; padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* --- the pay schedule (job sheet) --------------------------------------
|
||||
This is a QUOTE, not a receipt: it's what the night is worth if you do it
|
||||
right. It reuses your .row shape so it lines up with everything else, but
|
||||
the money is dimmed — these are amounts you have NOT earned yet, and they
|
||||
should not read with the same confidence as the bank line. The conditions
|
||||
are the point; the numbers are just what the conditions are worth. */
|
||||
#hud-card .sched .row b { color: #9fb3bf; font-weight: 700; }
|
||||
#hud-card .sched .cond {
|
||||
font-size: 10px; color: #55677a; letter-spacing: .02em;
|
||||
padding: 0 0 5px; border-bottom: 1px solid #1c262d; margin-top: -2px;
|
||||
}
|
||||
#hud-card .sched .row.headline b { color: #fff; }
|
||||
#hud-card .sched .row.max { border-bottom: 0; margin-top: 6px; padding-top: 7px;
|
||||
border-top: 1px solid #24343d; }
|
||||
#hud-card .sched .row.max span { color: #8ba0ad; }
|
||||
#hud-card .sched .row.max b { color: #ffd9a3; }
|
||||
|
||||
/* --- the invoice -------------------------------------------------------
|
||||
Your .ledger already IS an invoice; it just never said so. These rules sit
|
||||
ON TOP of it — paste them and .ledger keeps every behaviour it has, it
|
||||
just gains a rule above the total and a due line under it.
|
||||
|
||||
.earned/.bad you already colour. The one thing I've added is .void: a
|
||||
bonus that was on the job sheet and did NOT survive the night. Struck
|
||||
through, not hidden. The player must see the money they didn't get sitting
|
||||
in the same column as the money they did — a bonus you never see fail is a
|
||||
bonus you never learn to protect. This is the single highest-value line in
|
||||
the whole handover, and it's four CSS declarations. */
|
||||
#hud-card .ledger .row.void span { color: #55677a; }
|
||||
#hud-card .ledger .row.void b {
|
||||
color: #55677a; font-weight: 400; text-decoration: line-through;
|
||||
text-decoration-color: #7d2b2b; text-decoration-thickness: 1px;
|
||||
}
|
||||
/* The amount due. An invoice's whole rhetorical shape is that everything
|
||||
above is argument and this is the conclusion. Give it the weight. */
|
||||
#hud-card .ledger .row.due {
|
||||
margin-top: 8px; padding-top: 9px; border-top: 2px solid #2f3f4a; border-bottom: 0;
|
||||
align-items: baseline;
|
||||
}
|
||||
#hud-card .ledger .row.due span { color: #8ba0ad; letter-spacing: .12em; font-weight: 700;
|
||||
font-size: 11px; }
|
||||
#hud-card .ledger .row.due b { color: #fff; font-size: 20px; letter-spacing: .02em; }
|
||||
#hud-card .ledger .row.due.neg b { color: #ff8f86; }
|
||||
|
||||
/* The terms line. Pure flavour, and the cheapest joke on the card: a real
|
||||
invoice's most boring sentence, printed under a night that nearly killed
|
||||
you. Left in as its own class so you can delete it in one line if it
|
||||
doesn't land. */
|
||||
#hud-card .terms {
|
||||
margin-top: 10px; font-size: 10px; color: #45566a; letter-spacing: .04em;
|
||||
}
|
||||
/* ---- end of E's kit ----------------------------------------------------- */
|
||||
|
||||
/* Tomorrow is a rumour, and it should look like one next to tonight's numbers.
|
||||
(Mine, not E's — the kit predates the tomorrow line and doesn't restyle it.) */
|
||||
#hud-card .tomorrow { display:flex; gap:10px; align-items:baseline; margin-top:9px;
|
||||
font-size:12px; color:#6d8494; }
|
||||
#hud-card .tomorrow .tlabel { letter-spacing:.14em; text-transform:uppercase; color:#55707f; }
|
||||
@ -406,11 +545,28 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
// else. The client and the brief are data (week.js NIGHTS), so an
|
||||
// unbriefed night degrades to the old storm card rather than an empty
|
||||
// letterhead — which is also what keeps every existing test honest.
|
||||
//
|
||||
// SPRINT12 — the letterhead is E's kit now: the outfit's mark on the
|
||||
// left, the docket on the right, the client demoted to a bill-to block —
|
||||
// because the business is the constant and the client changes nightly.
|
||||
const job = wk.job ?? {};
|
||||
const letterhead = job.client
|
||||
? `<div class="letterhead">${job.client}</div>` : '';
|
||||
const letterhead = job.client ? `
|
||||
<div class="letterhead">
|
||||
<div>
|
||||
<div class="mark">${BUSINESS.mark}</div>
|
||||
<div class="trade">${BUSINESS.trade}</div>
|
||||
</div>
|
||||
<div class="docket">
|
||||
<div class="kind">JOB SHEET</div>
|
||||
<div class="no">JOB ${docketNo(wk.night)} · ${ABN}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="billto">
|
||||
<div class="who">${job.client}</div>
|
||||
${job.addr ? `<div class="addr">${job.addr}</div>` : ''}
|
||||
</div>` : '';
|
||||
const brief = job.brief
|
||||
? `<p class="brief">${job.brief}</p>` : '';
|
||||
? `<div class="brief">${job.brief}</div>` : '';
|
||||
|
||||
// BUDGET Y — and this is the actual feature, not the flavour. The fee has
|
||||
// existed since Sprint 8 and the player has never seen it until the money
|
||||
@ -418,13 +574,22 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
// was worth, which is a reveal, not a decision. Quoting the maxima up front
|
||||
// makes the shop a judgement ("is $45 of garden bonus worth a $30
|
||||
// shackle?") instead of a guess.
|
||||
// E's schedule shape: each amount carries its CONDITION underneath,
|
||||
// because the conditions are the point — the numbers are just what the
|
||||
// conditions are worth. The garden's condition says "scales", honestly:
|
||||
// E's preview copy read "bed above 80% at dawn" and the mechanic pays
|
||||
// linearly, and a quote that misstates its own terms is a lie on paper.
|
||||
const q = wk.quote;
|
||||
const schedule = q ? `
|
||||
<div class="jobsheet">
|
||||
<div class="row"><span>base — the job</span><b>$${q.base}</b></div>
|
||||
<div class="row"><span>garden bonus — if the bed lives</span><b>up to $${q.garden}</b></div>
|
||||
<div class="row"><span>clean bonus — nothing of theirs broken</span><b>$${q.clean}</b></div>
|
||||
<div class="row total"><span>the job's worth</span><b>up to $${q.total}</b></div>
|
||||
<div class="sect">WHAT IT PAYS</div>
|
||||
<div class="sched">
|
||||
<div class="row headline"><span>callout fee</span><b>$${q.base}</b></div>
|
||||
<div class="cond">the night, rigged and stood up</div>
|
||||
<div class="row"><span>garden bonus</span><b>up to $${q.garden}</b></div>
|
||||
<div class="cond">paid on what's left of the bed at dawn</div>
|
||||
<div class="row"><span>clean site</span><b>$${q.clean}</b></div>
|
||||
<div class="cond">nothing of theirs broken</div>
|
||||
<div class="row max"><span>the night, done right</span><b>$${q.total}</b></div>
|
||||
</div>` : '';
|
||||
|
||||
// Lane C's forecast lead (SPRINT11 gate 2): tomorrow, hedged. Their
|
||||
@ -447,13 +612,18 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
<span class="tline">${tf.wind}${tf.confidence ? ` · ${tf.confidence}` : ''}</span>
|
||||
</div>` : '';
|
||||
|
||||
// E's document order: letterhead, who it's for, their words, THEN the
|
||||
// night — the paper establishes whose job this is before it says what the
|
||||
// weather will do about it. The terms line under the button is E's
|
||||
// cheapest joke and their best: quoted on the forecast, weather is not a
|
||||
// variation, printed above a night that may cost you a carport.
|
||||
card.innerHTML = `<div class="card jobcard">
|
||||
${letterhead}
|
||||
${brief}
|
||||
<h1>NIGHT ${wk.night} OF ${wk.nights}</h1>
|
||||
<div class="pips">${pips}</div>
|
||||
<h2>${(f.name ?? key).replace(/_/g, ' ').toUpperCase()}${night ? ' · NIGHT' : ''}</h2>
|
||||
${siteLine}
|
||||
${brief}
|
||||
<div class="stat">${f.wind}</div>
|
||||
<div class="stat">${f.rain}${hail ? ' · hail forecast' : ''}</div>
|
||||
${f.confidence ? `<div class="stat" style="color:#8ba0ad">${f.confidence}</div>` : ''}
|
||||
@ -469,6 +639,7 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
A change means the corners that were slack all storm are the loaded ones after it.
|
||||
</div>` : ''}
|
||||
<button class="go">RIG IT</button>
|
||||
${job.client ? '<div class="terms">Quoted on the forecast at time of callout. Weather is not a variation.</div>' : ''}
|
||||
</div>`;
|
||||
card.classList.add('on');
|
||||
hud.setVisible(false);
|
||||
@ -557,18 +728,30 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
? (r.collateral ?? []).map((c) => `
|
||||
<div class="row bad"><span>${c.what}</span><b>−$${c.cost}</b></div>`).join('')
|
||||
: '';
|
||||
// SPRINT12 — E's .void: the forfeited bonus is STRUCK THROUGH, not zeroed.
|
||||
// The quoted $${cleanMax} stays on the paper with a line through it — the
|
||||
// player must see the money they didn't get sitting in the same column as
|
||||
// the money they did. A bonus you never see fail is a bonus you never
|
||||
// learn to protect. (E's words; the single highest-value line in the kit.)
|
||||
const cleanRow = w ? (w.clean
|
||||
? `<div class="row"><span>clean bonus — nothing of theirs broken</span><b>+$${w.clean}</b></div>`
|
||||
: `<div class="row bad"><span>clean bonus — forfeited</span><b>$0</b></div>`) : '';
|
||||
? `<div class="row"><span>clean site</span><b>+$${w.clean}</b></div>`
|
||||
: `<div class="row void"><span>clean site</span><b>$${w.cleanMax ?? 0}</b></div>`) : '';
|
||||
|
||||
// AMOUNT DUE is what tonight's paper actually says: everything above it is
|
||||
// argument, this is the conclusion. pay − spent, so it goes negative the
|
||||
// night you take the carport with you — and the bank line under it stays,
|
||||
// because "what this night was worth" and "where that leaves you" are two
|
||||
// different facts.
|
||||
const due = w ? w.pay - w.spent : 0;
|
||||
const money = w ? `
|
||||
<div class="ledger">
|
||||
<div class="row"><span>${w.won ? 'base — the job' : `base — night lost (${Math.round(100 * w.fee / Math.max(1, w.fullFee))}%)`}</span><b>+$${w.fee}</b></div>
|
||||
<div class="row"><span>${w.won ? 'callout fee' : `callout fee — night lost (${Math.round(100 * w.fee / Math.max(1, w.fullFee))}%)`}</span><b>+$${w.fee}</b></div>
|
||||
<div class="row"><span>garden bonus — ${r.hp.toFixed(0)}% of the bed</span><b>+$${w.bonus}</b></div>
|
||||
${cleanRow}
|
||||
<div class="row"><span>gear recovered</span><b>+$${w.refund}</b></div>
|
||||
${collateralRows}
|
||||
<div class="row"><span>spent on the rig</span><b>−$${w.spent}</b></div>
|
||||
<div class="row bad"><span>spent on the rig</span><b>−$${w.spent}</b></div>
|
||||
<div class="row due ${due < 0 ? 'neg' : ''}"><span>AMOUNT DUE</span><b>${due < 0 ? '−' : '+'}$${Math.abs(due)}</b></div>
|
||||
<div class="row total"><span>in the bank</span><b>$${w.bankBefore} → $${w.bankAfter}</b></div>
|
||||
</div>` : '';
|
||||
|
||||
@ -577,14 +760,35 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
: w.outcome === 'win' ? 'SEE THE WEEK'
|
||||
: 'SEE THE DAMAGE';
|
||||
|
||||
// The same letterhead as the job sheet, same docket number, and the kind
|
||||
// flips to TAX INVOICE — one outfit, two papers, one night apart. The
|
||||
// terms line: E's flavour when your failure broke something of theirs,
|
||||
// the world's most boring sentence otherwise.
|
||||
const letterhead = w?.client ? `
|
||||
<div class="letterhead">
|
||||
<div>
|
||||
<div class="mark">${BUSINESS.mark}</div>
|
||||
<div class="trade">${BUSINESS.trade}</div>
|
||||
</div>
|
||||
<div class="docket">
|
||||
<div class="kind">TAX INVOICE</div>
|
||||
<div class="no">INV ${docketNo(w.night)} · ${ABN}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="billto">
|
||||
<div class="who">${w.client}</div>
|
||||
${w.addr ? `<div class="addr">${w.addr}</div>` : ''}
|
||||
</div>` : '';
|
||||
|
||||
card.innerHTML = `<div class="card jobcard">
|
||||
${w?.client ? `<div class="letterhead">${w.client}<span class="inv">invoice</span></div>` : ''}
|
||||
${letterhead}
|
||||
<h1>MORNING${w ? ` · NIGHT ${w.night} OF 5` : ''}</h1>
|
||||
<h2>${r.subtitle}</h2>
|
||||
${rows}
|
||||
${money}
|
||||
<div class="verdict ${r.win ? 'win' : 'lose'}">${r.verdict}</div>
|
||||
<button class="go">${next}</button>
|
||||
${w?.client ? `<div class="terms">Payable 14 days. ${w.collateral ? 'They’ll want that made right by Friday.' : 'Thank you for your business.'}</div>` : ''}
|
||||
</div>`;
|
||||
card.classList.add('on');
|
||||
hud.setVisible(false);
|
||||
@ -600,7 +804,7 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
get cardOpen() { return card.classList.contains('on'); },
|
||||
|
||||
dispose() {
|
||||
root.remove(); card.remove(); style.remove();
|
||||
root.remove(); card.remove(); style.remove(); dawn.remove();
|
||||
d.scene.remove(barGroup);
|
||||
quad.dispose();
|
||||
for (const b of bars) {
|
||||
|
||||
@ -12,6 +12,7 @@ import { CALM_STORM, createGame, createWindRouter, stormsToPreload, verdictFor }
|
||||
import { orderRing } from '../sail.js';
|
||||
import { loadStorm, createWind } from '../weather.js';
|
||||
import { createWeek, NIGHTS, nightAt, gradeFor, BROKE_BELOW, PAY } from '../week.js';
|
||||
import { createHud } from '../hud.js';
|
||||
import { assert, assertEq, assertLess, fixedLoop } from '../testkit.js';
|
||||
|
||||
/** @param {import('../testkit.js').Suite} t */
|
||||
@ -124,6 +125,7 @@ export default async function run(t) {
|
||||
for (let i = 0; i < NIGHTS.length; i++) {
|
||||
const j = nightAt(i);
|
||||
assert(j.client, `night ${i + 1} has a client`);
|
||||
assert(j.addr, `night ${i + 1} has an address — a letterhead bills somebody somewhere`);
|
||||
assert(j.brief && j.brief.length > 20, `night ${i + 1} has a brief worth reading`);
|
||||
assert(j.site, `night ${i + 1} has a yard`);
|
||||
}
|
||||
@ -143,6 +145,7 @@ export default async function run(t) {
|
||||
// or printing "undefined" at a client.
|
||||
const j = nightAt(99); // off the end entirely
|
||||
assertEq(j.client, null, 'no client is null, not undefined');
|
||||
assertEq(j.addr, null, 'no address is null');
|
||||
assertEq(j.brief, null, 'no brief is null');
|
||||
assertEq(j.site, 'backyard_01', 'and it still names a yard');
|
||||
});
|
||||
@ -264,6 +267,81 @@ export default async function run(t) {
|
||||
assertEq(cb.work, 'bracket', 'the carport beam is a bracket job — needs the ladder');
|
||||
});
|
||||
|
||||
// --- SPRINT12 gate 3: E's letterhead/invoice kit, rendered ----------------
|
||||
// Both of Sprint 11's card bugs were invisible to the suite because nothing
|
||||
// ever RENDERED a card and looked. These do the minimum of that in DOM: the
|
||||
// kit's contract classes must exist on a real render AND their load-bearing
|
||||
// CSS must actually draw. Eyes on the browser are still required — a DOM
|
||||
// assert can't see a letterhead with buttons through it.
|
||||
|
||||
t.test('SPRINT12: the invoice wears the letterhead, and the dead bonus is struck through', () => {
|
||||
if (typeof document === 'undefined') return 'SKIPPED — no DOM';
|
||||
const def = { baseCurve: [[0, 10]], gusts: { powBase: 5, powRamp: 5 } };
|
||||
// Night 3, the carport night: collateral forfeits the clean bonus, so this
|
||||
// one render exercises the void, the neg AMOUNT DUE, and the docket number.
|
||||
const wk = createWeek();
|
||||
wk.advance(); wk.advance();
|
||||
const w = wk.settle({ hp: 86, win: true,
|
||||
collateral: [{ what: 'the carport', cost: 180 }], intactHardwareValue: 12 }, def, 71);
|
||||
|
||||
const hud = createHud({ scene: new THREE.Scene() });
|
||||
try {
|
||||
hud.showAftermath({ hp: 86, cornersLost: 1, cornersTotal: 4, hailBlocked: 'some hail',
|
||||
bill: 34, collateral: [{ what: 'the carport', cost: 180 }],
|
||||
subtitle: 'test', verdict: 'test', win: true, week: w }, () => {});
|
||||
const q = (sel) => document.querySelector(`#hud-card ${sel}`);
|
||||
|
||||
assertEq(q('.letterhead .mark')?.textContent, 'HARD YARDS', "the outfit's mark (ruling: adopted)");
|
||||
const no = q('.letterhead .docket .no')?.textContent ?? '';
|
||||
assert(no.includes('INV 1043'), `docket says INV 1043, got "${no}"`);
|
||||
// The ruling is that the ABN STAYS sequential — it reads as "example" the
|
||||
// way 555 numbers do, and a realistic one is a stranger's tax identity.
|
||||
assert(no.includes('ABN 12 345 678 901'), `the sequential ABN survives, got "${no}"`);
|
||||
assertEq(q('.billto .who')?.textContent, 'the Vasilaros place', 'the bill-to block names the client');
|
||||
|
||||
// E's .void — quoted, not paid, and still on the paper.
|
||||
const voidB = q('.ledger .row.void b');
|
||||
assert(voidB, 'the forfeited clean bonus renders as .void, not as $0 or missing');
|
||||
assertEq(voidB.textContent, `$${w.cleanMax}`, 'struck at the QUOTED amount');
|
||||
assert(getComputedStyle(voidB).textDecorationLine.includes('line-through'),
|
||||
'and the strike actually draws — the class without its CSS is decoration');
|
||||
|
||||
// AMOUNT DUE is the conclusion: pay − spent, negative the night you take
|
||||
// the carport with you, and it must SAY so.
|
||||
const due = w.pay - 71;
|
||||
const dueB = q('.ledger .row.due b');
|
||||
assert(dueB, 'the invoice concludes with AMOUNT DUE');
|
||||
assertEq(dueB.textContent, `${due < 0 ? '−' : '+'}$${Math.abs(due)}`, 'due = pay − spent');
|
||||
assertEq(q('.ledger .row.due')?.classList.contains('neg'), due < 0, 'a negative due reads as one');
|
||||
} finally { hud.dispose(); }
|
||||
});
|
||||
|
||||
t.test('SPRINT12: the job sheet carries the same letterhead, a quote, and the terms line', () => {
|
||||
if (typeof document === 'undefined') return 'SKIPPED — no DOM';
|
||||
// The same outfit, the same night, the other document: JOB 1043 in the
|
||||
// morning becomes INV 1043 — the pair answering each other is the design.
|
||||
const wk = createWeek();
|
||||
wk.advance(); wk.advance();
|
||||
const hud = createHud({ scene: new THREE.Scene() });
|
||||
try {
|
||||
hud.showForecast(
|
||||
{ key: 'storm_03b_earlybuster', def: earlyBusterDef, site: null, tomorrowDef: null },
|
||||
{ night: wk.night, nights: wk.nights, log: [], job: wk.job,
|
||||
quote: wk.quote(earlyBusterDef), bank: wk.bank }, () => {});
|
||||
const q = (sel) => document.querySelector(`#hud-card ${sel}`);
|
||||
|
||||
assertEq(q('.letterhead .mark')?.textContent, 'HARD YARDS', 'same mark as the invoice');
|
||||
assertEq(q('.letterhead .docket .kind')?.textContent, 'JOB SHEET', 'but this paper is the quote');
|
||||
assert((q('.letterhead .docket .no')?.textContent ?? '').includes('JOB 1043'),
|
||||
'and it shares the docket number with its invoice');
|
||||
assert(q('.billto .addr'), 'the bill-to block says where the job is');
|
||||
assertEq(document.querySelectorAll('#hud-card .sched .cond').length, 3,
|
||||
'three pay lines, each carrying its condition');
|
||||
assert((q('.terms')?.textContent ?? '').includes('not a variation'),
|
||||
"the terms line: quoted on the forecast, weather is not a variation");
|
||||
} finally { hud.dispose(); }
|
||||
});
|
||||
|
||||
// --- SPRINT11 gate 1: the three rulings, each pinned ----------------------
|
||||
|
||||
t.test('ruling: the carport is typed as a carport, not smuggled in as a post', () => {
|
||||
|
||||
@ -48,34 +48,38 @@ import { START_BUDGET, HARDWARE } from './contracts.js';
|
||||
* Copy lives here as data, per SPRINT11's rule: client flavour is data, not code.
|
||||
*/
|
||||
export const NIGHTS = [
|
||||
// SPRINT12: `addr` is the second line of E's bill-to block — a letterhead
|
||||
// bills somebody somewhere. Street names are the site's fiction (E's kit
|
||||
// drafted both), and the addr carries the yard so the invoice, which has no
|
||||
// site line of its own, still says where the night happened.
|
||||
{
|
||||
storm: 'storm_01_gentle', site: 'backyard_01',
|
||||
client: 'the Hendersons',
|
||||
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
|
||||
brief: 'They fly out Friday and the seedlings are the whole point — six weeks in, first season that '
|
||||
+ 'took. Get the sail up while it\'s calm. Nothing tonight will test it.',
|
||||
},
|
||||
{
|
||||
storm: 'storm_03_southerly', site: 'backyard_01',
|
||||
client: 'the Hendersons',
|
||||
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
|
||||
brief: 'A southerly, and hail with it. This is the one the seedlings can\'t take — cloth over the bed '
|
||||
+ 'or there\'s no bed on Monday.',
|
||||
},
|
||||
{
|
||||
storm: 'storm_03b_earlybuster', site: 'site_02_corner_block',
|
||||
client: 'the Vasilaros place',
|
||||
client: 'the Vasilaros place', addr: '2 Bight Rd — the corner block',
|
||||
brief: 'Short notice — corner block two streets over, never rigged it. Same southerly as Tuesday, '
|
||||
+ 'except the change is through before you\'ve finished your coffee. They said there\'s plenty '
|
||||
+ 'to tie off to.',
|
||||
},
|
||||
{
|
||||
storm: 'storm_02_wildnight', site: 'backyard_01',
|
||||
client: 'the Hendersons',
|
||||
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
|
||||
brief: 'Back at the Hendersons\', and this is the night the whole week was pointing at. Rig it like '
|
||||
+ 'they\'re watching, because Friday they will be.',
|
||||
},
|
||||
{
|
||||
storm: 'storm_02b_icenight', site: 'backyard_01',
|
||||
client: 'the Hendersons',
|
||||
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
|
||||
brief: 'Less wind than last night and more ice, which is worse for the bed and easier on the rig. '
|
||||
+ 'Last night before they land.',
|
||||
},
|
||||
@ -99,6 +103,7 @@ export function nightAt(i) {
|
||||
storm: base.storm,
|
||||
site: base.site ?? 'backyard_01',
|
||||
client: base.client ?? null,
|
||||
addr: base.addr ?? null,
|
||||
brief: base.brief ?? null,
|
||||
pay: base.pay ?? {},
|
||||
};
|
||||
@ -347,7 +352,7 @@ export function createWeek(opts = {}) {
|
||||
const s = { fee, fullFee, bonus, refund, clean, cleanMax, collateral, pay, spent,
|
||||
bankBefore, bankAfter: bank, outcome, night: week.night, held,
|
||||
grade: gradeFor(held), stormKey: week.stormKey, won: !!run.win,
|
||||
client: job.client, site: job.site };
|
||||
client: job.client, addr: job.addr, site: job.site };
|
||||
log.push(s);
|
||||
return s;
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user