Commit Graph

5 Commits

Author SHA1 Message Date
m3ultra
2b66ce4b9a Give the game a face: HUD, mouse prep, forecast and aftermath (gate 1)
SPRINT4 §Lane A 1-5. Everything the game already simulated was invisible to a
stranger; you can now play a whole round with eyes and a mouse and never open the
console.

hud.js only ever READS — it takes no decisions and owns no state worth keeping,
so it can be this chatty without anything drifting. Corner load bars are
world-anchored rather than screen-anchored because a number in the corner of the
screen can't tell you WHICH shackle is about to go, and that is the entire
"...the shackle, I knew about the shackle" moment. They hold constant screen size
with distance: a house corner is 18 m away from the posts, where a world-sized
bar is a few unreadable pixels.

Prep is Lane B's createRiggingUI wired to the phase machine — their picking was
already complete, this is the hands. Forecast reads the three authored storms, so
the difficulty select came free: Sea Breeze / Southerly Buster / Wild Night with
real peak wind, gust character and change time.

Two bugs the wiring surfaced, both found by playing rather than reading:
- The rigging session survived "play again": a second round started at $35 with
  three corners already hung, and the four anchors you clicked were silently
  ignored because the session was full. resetRig() walks it back through the
  session's own public moves, so the economy stays the one source of truth.
- The forecast card's lifetime was owned by its own button, so any other route
  into prep left a card floating over a live game eating input. The phase owns it
  now.

Verified by playing it: good rig on the wild night wins at 53%, the same yard's
cheap drum-tight span loses 4 corners, the gentle night with a good rig ends at
93% "not a leaf out of place". Selftest 184/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:32:02 +10:00
m3ultra
0cabb19dc7 Anchor rework + house/tree GLBs: give the yard a real choice (decisions 2 & 6)
Posts pulled in to (-4.5,5.5)/(4.0,6.0) with p3 at (0,7), and dress() now swaps
Lane E's house_yardside and both gum trees over the graybox, adopting their baked
anchors rather than my constants (decision 6). E's fascia sits at x=-3..3, not my
guessed -5..5 — narrowing the house span by 4 m is a real part of why small quads
exist at all.

Every anchor now carries E's rating_hint: fascia 0.35 with collateral "gutter"
(they encoded DESIGN.md's "the fascia board is a lie" straight into the asset),
tree branches descending 1.0/0.88/0.76 from fork to thin limb. branch_anchor_01
keeps the t1/t2 ids so nothing referencing them breaks; the rest are added.

The yard went from 7 anchors offering nothing under 110 m² to 11 offering 34
quads in the 18-45 m² band, 8 of which shade a quarter of the bed or more.
Measured through the same storm_02: the big house-to-post span loses its
carabiner at t=3.7 s and cascades to 2/4, while a 37.7 m² tree-to-post rig at
0.85 tension survives all 90 s intact and shades 58% of the bed. That is
DESIGN.md's thesis finally standing up in the yard rather than in a doc.

Lane B's "cascade at t=0.4 s from pre-tension alone" is gone: calm peaks are now
634 N (big) and 200 N (small) against a 1200 N carabiner, and no rig breaks
before the storm starts.

Two asserts pin it, because both halves are easy to lose by accident: at least 3
riggable quads in 18-45 m² must shade the bed, AND full bed coverage must stay
above 45 m² — if a small quad ever covers the whole bed, the rigging puzzle has
no wrong answers left. Selftest 172/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:30:17 +10:00
m3ultra
a5ed4fcdb6 Place the shed and spare table; expose world.shedTable (unblocks Lane D)
SPRINT3 decision 9 / §Lane A.1. Lane D's §7 loop — rig, carry a spare, repair
mid-storm — had nowhere to pick a spare up, so wireYardActions self-skipped the
pickup and nothing in the game could put one in your hands.

world.shedTable.pos is published synchronously from constants even though the
meshes arrive later: createWorld() has to stay sync (a.test.js and the selftest
build a yard with no server), and wireYardActions reads the position at wiring
time. dress() then refines it onto Lane E's baked pickup_anchor — which moved it
5 cm off my guess at where a table top is, so it was worth reading.

dress() is async and separate from createWorld for the same reason: a fetch in
the constructor would make the selftest slow and flaky, or break it. Each load is
guarded individually, so a missing GLB leaves its graybox standing instead of
taking the boot down.

Verified by hand rather than by registration check: walk up, hold E, spare in
hand; a second hold says "hands full"; leaning on the table with E held for 6 s
deals exactly one spare (D's latch works); nothing fires from across the yard.
Selftest 169/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:20:35 +10:00
m3ultra
7f3ef69685 Expose the yard's hemisphere light for skyfx
Lane C's skyfx modulates sun and hemi as the storm builds and hands them back on
dispose() — it doesn't own them. It already had `sun`; `hemi` was private, so the
sky could darken but the sky-bounce fill couldn't follow it down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:38:03 +10:00
m3ultra
a41328a219 Add graybox yard, third-person camera and game shell
The yard's meshes are placeholders; its layout is not. Anchor positions, ground
heights, the garden rect and the sun direction are what the other lanes build
against, so Lane E's GLBs drop into named slots without anyone re-deriving them.

Sun sits in the north (Australian yard), which puts the house's yard-facing wall
in permanent backlight. That's correct, but the fascia line carries three of the
seven anchors, so sky fill is turned up to keep it readable rather than a void.

Camera collision resolves toward the geometry, not away from it: clamping up to
a comfort distance after the raycast is what put the camera 17cm inside the
north wall. Ground is handled by heightAt() instead of a raycast — exact, can't
be tunnelled, and free.

main.js runs a fixed-dt accumulator so sim modules only ever see FIXED_DT. That
is what lets selftest fast-forward a 90s storm and get the numbers the player
got. Nothing auto-runs on import; index.html calls boot().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:33:42 +10:00