diff --git a/THREADS.md b/THREADS.md index a17fec3..7619718 100644 --- a/THREADS.md +++ b/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). diff --git a/web/world/js/hud.js b/web/world/js/hud.js index 2bbb6e7..4e1f2de 100644 --- a/web/world/js/hud.js +++ b/web/world/js/hud.js @@ -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 - ? `
${job.client}
` : ''; + const letterhead = job.client ? ` +
+
+
${BUSINESS.mark}
+
${BUSINESS.trade}
+
+
+
JOB SHEET
+
JOB ${docketNo(wk.night)} Β· ${ABN}
+
+
+
+
${job.client}
+ ${job.addr ? `
${job.addr}
` : ''} +
` : ''; const brief = job.brief - ? `

${job.brief}

` : ''; + ? `
${job.brief}
` : ''; // 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 ? ` -
-
base β€” the job$${q.base}
-
garden bonus β€” if the bed livesup to $${q.garden}
-
clean bonus β€” nothing of theirs broken$${q.clean}
-
the job's worthup to $${q.total}
+
WHAT IT PAYS
+
+
callout fee$${q.base}
+
the night, rigged and stood up
+
garden bonusup to $${q.garden}
+
paid on what's left of the bed at dawn
+
clean site$${q.clean}
+
nothing of theirs broken
+
the night, done right$${q.total}
` : ''; // Lane C's forecast lead (SPRINT11 gate 2): tomorrow, hedged. Their @@ -447,13 +612,18 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) { ${tf.wind}${tf.confidence ? ` Β· ${tf.confidence}` : ''}
` : ''; + // 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 = `
${letterhead} + ${brief}

NIGHT ${wk.night} OF ${wk.nights}

${pips}

${(f.name ?? key).replace(/_/g, ' ').toUpperCase()}${night ? ' Β· NIGHT' : ''}

${siteLine} - ${brief}
${f.wind}
${f.rain}${hail ? ' Β· hail forecast' : ''}
${f.confidence ? `
${f.confidence}
` : ''} @@ -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.
` : ''} + ${job.client ? '
Quoted on the forecast at time of callout. Weather is not a variation.
' : ''} `; card.classList.add('on'); hud.setVisible(false); @@ -557,18 +728,30 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) { ? (r.collateral ?? []).map((c) => `
${c.what}βˆ’$${c.cost}
`).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 - ? `
clean bonus β€” nothing of theirs broken+$${w.clean}
` - : `
clean bonus β€” forfeited$0
`) : ''; + ? `
clean site+$${w.clean}
` + : `
clean site$${w.cleanMax ?? 0}
`) : ''; + // 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 ? `
-
${w.won ? 'base β€” the job' : `base β€” night lost (${Math.round(100 * w.fee / Math.max(1, w.fullFee))}%)`}+$${w.fee}
+
${w.won ? 'callout fee' : `callout fee β€” night lost (${Math.round(100 * w.fee / Math.max(1, w.fullFee))}%)`}+$${w.fee}
garden bonus β€” ${r.hp.toFixed(0)}% of the bed+$${w.bonus}
${cleanRow}
gear recovered+$${w.refund}
${collateralRows} -
spent on the rigβˆ’$${w.spent}
+
spent on the rigβˆ’$${w.spent}
+
AMOUNT DUE${due < 0 ? 'βˆ’' : '+'}$${Math.abs(due)}
in the bank$${w.bankBefore} β†’ $${w.bankAfter}
` : ''; @@ -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 ? ` +
+
+
${BUSINESS.mark}
+
${BUSINESS.trade}
+
+
+
TAX INVOICE
+
INV ${docketNo(w.night)} Β· ${ABN}
+
+
+
+
${w.client}
+ ${w.addr ? `
${w.addr}
` : ''} +
` : ''; + card.innerHTML = `
- ${w?.client ? `
${w.client}invoice
` : ''} + ${letterhead}

MORNING${w ? ` Β· NIGHT ${w.night} OF 5` : ''}

${r.subtitle}

${rows} ${money}
${r.verdict}
+ ${w?.client ? `
Payable 14 days. ${w.collateral ? 'They’ll want that made right by Friday.' : 'Thank you for your business.'}
` : ''}
`; 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) { diff --git a/web/world/js/tests/a.test.js b/web/world/js/tests/a.test.js index 2f5de5a..90dd8ab 100644 --- a/web/world/js/tests/a.test.js +++ b/web/world/js/tests/a.test.js @@ -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', () => { diff --git a/web/world/js/week.js b/web/world/js/week.js index c81a71f..4d3d9d0 100644 --- a/web/world/js/week.js +++ b/web/world/js/week.js @@ -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; },