Skip to content

[feat] 30 visuals-core: towers, stars room and jam room, finished - #242

Open
AlexZ005 wants to merge 14 commits into
release/nextfrom
feat/30-core-games
Open

AlexZ005 wants to merge 14 commits into
release/nextfrom
feat/30-core-games

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Roadmap 30, lane 30-visuals-core: the three core-authored Games-tab games brought to the standard shell and a finished look. Built on feat/30-author-kit (#240) and feat/30-game-flow (#241), merged in clean.

What landed

  • P0 Towers (39f6671): a sky gradient with fog and a ground; a 2 m floor tile from a shader graph; chamfered walls with a glowing trim; wooden crates; thin glowing ring sensors beside a marked pole (they replace the stacked translucent squares); turning stars. Best height saved with Store Value (max) and shown on the menu. Round over reads this round's height from storage, because a perRound latch reads 0 as soon as the round ends, which is why it used to say 0 m. Also a core HUD fix: a centred or right-aligned single-line HUD text rendered flush left in every game (HudElement.svelte, justify-content now follows align).
  • P1 Stars Room (7536ef9): a space room with a gradient sky, a starfield from one emitter, glass walls (select-through) in a glowing frame, and crystal stars. New start screen with Start round / Free play (free play is its own game state), a 2-minute round, and Round over read from storage with a leaderboard; the best round is saved. The load-time "Particle emitter cap (8)" toast is fixed without raising the cap: one pooled burst emitter that a Script node moves onto the star just hit (2 emitters on load, was 24).
  • P2 Jam Room is a game (ecb1429): a small studio around the same 12 cabled devices, with play.cursor: 'free'. Start plays a 3-second count-in, then the goal is to keep the band playing for 8 bars. The goal uses core's Transport node, and the start beat is held with Set Variable. The music modules publish no flow values, so the player presses ▶ on the Transport device. The best tempo is saved. New suite game-jam-room.

Suites (staged scenes, restarted server)

suite base now
game-towers 19/20 40/40
game-stars-room 36/36 47/47
game-jam-room (new) — 30/30
music-template 11/11 11/11
game-editor-flow 41/41 41/41 (one flaky 39/41 run on the solo-reset timing, 41/41 on re-run)
module-storage 36/36 36/36

svelte-check 336/47 with an identical list; vitest 204; build green. Each commit body has its counterfactuals (16 reds across the three phases).

Centre luminance in play, before → after: towers 0.593 → 0.563, stars 0.218 → 0.271, jam 0.233 → 0.426 (edit views 0.20/0.24/0.22 → 0.44/0.31/0.30). Staged content: cloud-lane-30-staging/{towers,stars-room,jam-room}.

Owed

  • The look on a real display and in the Quest browser.
  • A jam round with two real people.
  • The Towers best height surviving an Oculus Browser restart.

For the integrator

CHANGELOG lines and CLAUDE.md gotchas are in the lane handover (STATUS-30-visuals-core.md). Re-author the three games on the union (--only towers,stars-room,jam-room).

🤖 Generated with Claude Code

AlexZ005 and others added 14 commits September 23, 2026 00:35
…s, object flags

- build() gains capsule / plane / ring / icosahedron / dodecahedron (core three geometries,
  rebuilt from their parameters by ObjectLoader) and box `bevel` (+ bevelSegments): a port of
  three/examples RoundedBoxGeometry baked into a PLAIN BufferGeometry, because toJSON writes
  a subclass type ObjectLoader cannot rebuild (core Box params carry no bevel)
- light `kind`: spot (angle, penumbra, target) / directional (castShadow, shadowMapSize,
  frustum FITTED to the built meshes in the light's own frame; `fit: false` opts out) /
  hemisphere (groundColor); spot + directional follow createLight (cast by default, the V-1
  bias pair) and aim by ROTATION at `target` (24-E1). point (no kind) is untouched
- materials: `physical: true` or any physical-only field (clearcoat, clearcoatRoughness,
  transmission, thickness, ior, sheen, sheenColor, sheenRoughness, iridescence,
  specularIntensity) -> MeshPhysicalMaterial; `toon: true` -> MeshToonMaterial;
  `flatShading`, `side: 'double'|'back'`; emissiveIntensity as before
- flags: `pick: 'through'` -> userData.pick; `origin: [x,y,z]` -> userData.origin;
  `particles: '<preset>' | {preset, ...patch}` -> userData.particles from particlePresets;
  `anim: '<preset>' | [..]` -> animationPreview.applyPreset (by key or name), an ordinary
  authored clip (not playing — a Play Animation node runs it)
- loader: `--def <file.json[,..]>` adds defs from JSON (one or an array); a same-slug def
  REPLACES the DEFS entry. Unknown object types now throw instead of silently becoming cones
- suite author-kit (new): authors a def-under-test via --def/--only into a temp dir, loads it
  back through sessions.readSessionZip + applySession, one check per feature: 40 PASS
- byte-identity: level-blockout/towers/stars-room/untangle/waves authored before and after,
  scripts/compare-authored.cjs reports SAME for all five (thumbs identical in size)
- counterfactual: 'clearcoat' dropped from PHYSICAL_KEYS + the pick line removed -> exactly
  "physical: clearcoat 0.8 survives (0)" and "pick: 'through' lands on userData.pick" FAIL
- held: templates-modal 47 / template-modules 29 PASS lines at base (no src change in this
  commit); svelte-check 336/47 identical list; vitest 196; build green

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hState warning, rapier's init warning

- App.svelte: <Canvas shadows={PCFShadowMap}>. threlte defaults to PCFSoftShadowMap, which
  three 0.185 deprecates and downgrades to PCF on the first shadow render - so the picture is
  identical and the warning is gone.
- playMode.js: the back-button marker is a SvelteKit SHALLOW entry (pushState from
  $app/navigation) instead of a raw history.pushState, which the router answers in dev with
  "will conflict with SvelteKit's router". Same url, same navigation index, so the Back that
  spends it never navigates. New playMarkerState() reads the state under sveltekit:states.
- physics.js: the wasm "deprecated parameters for the initialization function" warning is
  rapier-compat's OWN init() handing wasm-bindgen a bare byte array; the call is inside the
  frozen dependency, so there is no single-object form to pass. It is filtered for the
  length of that one call (exact prefix, restored in finally) - every other warn passes.
- helpers.cjs: page.__console collects every console message from BEFORE the first
  navigation (the PCF warning fires on frame one; a listener attached after boot misses it).
- touch-play: reads the marker through playMarkerState() (the state moved under the
  router's key).

Suite console-hygiene (new) 13/13. Counterfactual: all three source fixes reverted at once
-> 4 red (the PCF warning, the marker not being a shallow entry, the router warning, the
wasm warning), restored -> green. touch-play 39/39.
Held at base: game-state 45, game-presence 61, hud-actions 65, game-towers 20,
play-interact 46. svelte-check 336/47 (no new message), vitest 196, build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, sun, hemi

- a def `env` may be {preset:'custom' | <preset>, base?, exposure, background:'#hex' |
  {top, bottom}, fog:{color, near, far} | null, ground:{color, roughness?}, sun:{color,
  intensity, dir | position} | null, hemi:{sky, ground, intensity} | null}; colours may be
  numbers. It builds a payload from a base preset and commits it through the environment
  module's own custom path (applyCustomPreset), so the scene saves it as customPreset and a
  peer / late joiner gets it on the existing environment singleton. The payload's own
  exposure stays 1 and the def's exposure is the STATE multiplier (applyCustomPreset would
  otherwise square it). A plain preset env takes the old line verbatim
- environment.js (ADDITIVE, absent on every stock preset and every older save):
  `gradient: {top, bottom}` -> a cached 2x256 CanvasTexture background (`background` keeps
  the flat horizon colour for the backgroundColor store and older peers); `ground: {color,
  roughness?}` -> an `env-ground` disc in ENV_ROOT (scene root, never synced or saved as an
  object) that receives the sun's shadows, so the ShadowMaterial catcher stands down while
  it shows; hidden in wireframe and in passthrough (it would cover the real room).
  Read only through skyGradientOf / skyGroundOf (a malformed value reads as absent)
- the per-def reset uses environmentRestore(null) — setEnvironment('studio') kept a custom
  payload in the state, which environmentSnapshot would have saved into the NEXT def
- suite author-kit: + a sky section (state, texture background, fog, ground visible,
  catcher down, sun dir, hemi, exposure not squared) and a PIXEL check on the loaded scene:
  sky bands R 206 vs 81 (upper redder), B 206 vs 81 (lower bluer) — 53 PASS
- counterfactuals: the builder ignoring the gradient -> 4 FAIL (saved gradient, texture
  background, both pixel bands 34/34, 255/255); environment.js skyGradientOf -> null ->
  3 FAIL (texture background + both pixel bands)
- byte-identity: level-blockout/towers/stars-room/untangle/waves SAME vs the pre-lane build
- held: templates-modal 47/0, template-modules 29/0 (= base); svelte-check 336/47 with an
  identical list; vitest 196; build green on this environment.js

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- hudDocs: `isGameHud(docs)` + the `hudIsGame` store. THE RULE: a scene is a game when any
  HUD document holds a screen bound to a game state (`showWhile` set) - that binding is the
  one thing that makes the editor masquerade (the shared state picks the screen, so the
  menu paints itself over the editor with nobody in Play). Every Games-tab template has
  one. A hudbutton->setgamestate wiring alone was considered and NOT taken: with no
  state-bound screen nothing masquerades, and reading the graph would put a flow edge
  into a leaf that keeps none. A HUD with no state-bound screen keeps its old behaviour.
- HudLayer: outside Play a game's screens (scene and camera docs) are not drawn. The HUD
  editor's preview eye (`hudPreviewInViewport`) still draws them, with every element INERT:
  HudElement `editor` (the artboard's swallow, out of the tab order) AND the whole preview
  click-through (`.hud-inert`), so a previewed menu can never fire the game nor eat a
  viewport click (the roadmap's selection finding 4). An inert preview claims no keys.
- GameChip.svelte (new, mounted in App.svelte beside HudLayer): "Game · <state>" + an eye
  that toggles the same preview store. Editor-only: hidden in Play, VR and embed mode.
  Top-left under the logo (tucks under a docked Connect bar via --connect-bottom), z 38 -
  the authoring HUD band, under every window. Test play joins it in P2.
- Suites broken by the change, updated in place: game-towers ("the menu renders" moved to
  after the play press), game-loop (the 21-D scenario presses Start IN PLAY now - both
  peers play, the menu screen frees the pointer, A approves the late joiner from the
  editor while B keeps the round alive), game-waves/game-untangle/game-dungeon-realms
  (the editor-side menu check becomes "the chip stands in for it", the menu check moves
  into play).

Suite game-editor-flow (new) 17/17 on the real Towers .tpscene + collectible zip.
Counterfactual: HudLayer restored to base -> 8 red, incl. "clicking where Start used to
be leaves the game on its menu (playing)" - the reported bug, reproduced.
game-loop 31/31 · game-untangle 47/47 · game-dungeon-realms 65/65 · game-waves 85/91
with the SAME six ledger/run-log reds on base source (A/B'd: pre-existing, module-side:
a run logged twice 1 ms apart, an overkill pulse count).
Held: game-towers 20 · hud-actions 65 (base 20/65). svelte-check 336/47, no new message.
Build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n the real GPU

- the card renders with the SCENE's look: the live background (flat or the p1 gradient
  texture), its fog, the environment rig cloned from the scene root (hemi + sun + catcher /
  ground + extra env lights, at the intensities the viewport uses) and the authored lights
  (spot/directional targets seated on their -Z forward, the lightHelpers rule); shadows on
  (PCF). The private hemi + directional pair is the fallback ONLY for a scene with no light
- tone mapping = the post stack's Tone mapping curve when it has one, else NONE — what a
  composed desktop frame shows (the default Shaded+AO view keeps the composer running, and
  the renderer's own ACES never reaches a composed frame — the documented gotcha). MEASURED:
  ACES darkened the Waves card to 0.040 centre luminance against the live frame's 0.11-0.13;
  none reads 0.086. `thumb.toneMapping: 'agx'|'aces'|'neutral'|'reinhard'|'cineon'|'linear'|
  'none'` overrides per def
- camera: `thumb.camera`, else the def's `view` (NEW — the view the file opens on), else a
  3/4 PROJECTED fit of the content box (camera markers and floor-like slabs excluded — a thin
  mesh covering over a quarter of the footprint), so a 30x40 m ground no longer shrinks every
  game piece to a speck. Camera markers are hidden in every card now (chrome, not scenery)
- the authoring browser uses the per-platform ANGLE backend (vulkan on Linux, the helpers.cjs
  GPU_ARGS): `--use-angle=gl` fell back to SwiftShader, so every card was a software render.
  The run now prints its GPU line and WARNs on SwiftShader
- suite author-kit: + card checks — the sky def's card shows the authored gradient (top R 242
  / bottom B 242), the studio kit card reads (0.416 > 0.2), and the Waves def from the sibling
  modules checkout shows its OWN sunset sky (51,32,48 vs the scene's 51,32,47) and grew past
  the old card (2110 B vs 1690). 58 PASS
- DEVIATION (QUESTIONS-30-author-kit fork 1, recommendation taken): the brief's "Waves card
  > 6 KB with centre luminance > 0.2" is not reachable truthfully — the Waves scene is dark
  (its live frames read 0.11-0.13 on the same metric) and the OLD grey-lit card read 0.198, so
  a > 0.2 check would pass the wrong card. The 0.2 bar is asserted on a lit controlled scene
- counterfactual: HEAD's old thumbnail block restored (private lights, grey, whole-bounds) ->
  3 FAIL: the gradient card (top R 80 / bottom B 80), the Waves size (1696 B) and the Waves
  sky (34,42,51 = the private grey)
- scene content: level-blockout/towers/stars-room/untangle/waves canonical session.json SAME
  as the pre-lane build (only the card and the inline thumbnail sizes differ — new renderer,
  and the inline one now comes from a real GPU)
- no src change in this commit: svelte-check 336/47 and build as at p1; held suites as at p1

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- a "THE DEF SCHEMA" block at the top of scripts/author-templates.cjs: def-level fields (kind,
  identity, modules/installModules, generate/layout, objects, env, gravity/physics, post,
  graphs/hud/shaders/animations, music/sounds, view, thumb {camera, sceneGroups, toneMapping},
  contest), every object type with its params and defaults, the material fields (standard,
  physical, toon), the object flags (physics, shadow, pick, origin, anim, particles), the custom
  sky, the loader flags (--out, --only, --def, APP_URL, MODULES_REPO) and the card rules
- comment-only: node --check clean; the modules repo AUTHORING.md table is handed to the
  integrator (this lane may not edit the modules repo)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aves

- gamePresence.testPlay(): back to the menu (through requestResetGame, so it obeys
  canResetGame: the host, anyone alone, an admin under a roles plugin; anybody else is
  told and joins the game as it is), this peer's screen overrides cleared (the state-bound
  screen decides), then requestPlay() synchronously inside the click - the Start screen.
- Entry points: the game chip's ▶ Test play, and a "Test play (start from the menu)" row
  in the play button's right-click menu (Controls playModeItems, games only).
- THE ABANDON WATCH, extended rather than a second writer: every state but `menu` is a
  round a player can leave (paused and over used to linger forever); ALONE the reset is
  immediate (nobody's reload or connect dance to protect), with peers the 10 s window
  stands; the watch also runs on the edges (isLocked, gameState, peerPlayModes - deferred a
  macrotask, never writing a store inside its own subscriber), so a round entered and left
  inside the first second is still armed. Still host-only (sessionHost === null).
- Leaving play clears THIS peer's screen overrides on a game (a pause menu opened with P no
  longer greets the next Play over a round that has returned to its menu).

game-editor-flow 41/41 (sections 3 Test play solo: stale pause override undone, Start
screen, Start -> playing, Esc -> editor with `menu` and no HUD, the right-click row; 4 two
peers: A leaves while B plays -> still playing after the 10 s window, B leaves -> `menu` on
both, written by the host alone).
Counterfactual: the watch's resetGame() dropped -> 3 red (solo reset, the chip's state, the
two-peer reset), restored -> green.
Held: game-presence 61 · game-state 45 (base 61/45). Also green: play-menu-mode 32,
hud-editor-wysiwyg 53, contest-starters 27, game-stars-room 36 (suites that leave play
around game state). svelte-check 336/47, no new message. Build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- scenePhysics.play.cursor: 'free' | 'locked'. Normalized at the one boundary; 'locked' is
  the default and is NEVER written (the play block carries `cursor` only when free), so a
  scene that never uses it saves byte-identically. resolvePlaySettings resolves it too, so a
  module can ask for it through the userData.play contract, field by field.
- playCursor.js (new leaf): THE one answer to "where does play mode aim" - playAimNdc() is
  the cursor NDC in free-cursor play and the crosshair (0,0) otherwise; playCursorFree();
  cursorClient(). Recorded in CLIENT px, converted against the canvas at read time (the W9
  rule). This is the seam 30-core-modes' pointerRay reads in play (it also answers the stale
  mouse ray under a lock) and 30-untangle feature-detects.
- PointerLockControls: no lock is requested in a free-cursor game (entry, the menu-substate
  re-lock, the canvas-press recapture); movement keys still walk/fly, Escape still leaves.
- playInteract: tap and grab aim through playAimNdc, and the carry target follows the aim
  ray; in free mode only a press on the VIEWPORT canvas is a world gesture (a HUD button or
  any page chrome is not). PlayReticle draws nothing when the cursor itself aims.
- Grid.svelte: the editor grid is not drawn while playing, on every scene and either cursor
  (the existing "Show helpers in Play (debug)" toggle is the one way to keep it); the mesh
  is named `editor-grid`.
- debug hook: playCursor added to all three tails (213/213/213).

Suite play-free-cursor (new) 27/27: the field omitted at default, 0 pointer-lock requests
(spy on Element.prototype.requestPointerLock), no reticle, a REAL click at an OFF-centre
object's pixel fires its On Click, a REAL press-drag carries a dynamic body under the
cursor (792px vs cursor 792px), page chrome is not a world tap, the grid hidden in play in
both cursor modes, a locked game still asks for the lock (the spy works).
Counterfactual: lock requested anyway + grid always drawn + aim pinned to the crosshair ->
7 red (5 lock requests; both grid checks; click, grab, carry position), restored -> green.
Held: play-interact 46 · game-towers 20 (base 46/20). Also green: play-menu-mode 32,
touch-play 39, scene-physics-state 37. svelte-check 336/47, no new message. Build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- gameStorage.js (new leaf, safeStorage only): THE KEY CONTRACT, byte for byte -
  `tp:mod:<moduleId>:<key>` for a module, `tp:scene:<scene name or 'untitled'>:<key>` for
  the flow nodes; values are JSON text; a missing or unparseable entry hands back the
  fallback; a parsed-value cache kept current by every write through the leaf.
- moduleSDK makeApi: `api.storage.{get(key, fallback), set(key, value) -> bool, remove,
  keys, clear, bytes}` - 256 KB per module measured as the JSON text + key of every entry
  (a set over it returns false and ONE toast per module per session), LOCAL (never
  replicated, never in a scene file, never undone), deliberately NOT journalled: storage
  outlives the module (a reinstall keeps progress; clear() is the module's own reset).
  A throwing localStorage falls back to memory for the session through safeStorage.
- Flow nodes, palette group Game, in BOTH registries (nodeCatalog + CORE_NODE_TYPES) with
  manual lines: Store Value (trigger + value; key, mode set|max|min|add) acts on the stamp
  EDGE inside the actionSeenAt family and sends NOTHING - its card says "Saved on this
  device only"; Stored Value (key, output number|text). flowSockets types both.
- hudActions: "Save best score" on a button - Store Value (best, max) fed by the `score`
  variable; the press role gains an additive `via` (absent on every other press action).
- nodeDocs: a manual line for `onhit`, which 24-A shipped without one (flow-node-docs had
  been red on base for it) - one line outside this lane's nodes.
- debug hook: gameStorage added to all three tails.

tests/unit/gameStorage.test.js (8, no browser) · module-storage (new) 36/36: the api
round-trips, the raw key, two modules apart, the cap, a throwing localStorage (no error,
memory fallback, nothing reached storage), Counter -> Store Value (max) keeping the best
under tp:scene:Arcade:best with nothing on the wire and nothing on the second peer, a
reload keeping both halves, another scene's namespace not seeing it, and "Save best score"
built and pressed.
Counterfactual: the module namespace dropped from the key -> 5 red (the key contract, two
modules colliding 7/7, keys and clear crossing, the reload), restored -> green.
Held: hud-actions 65 (base 65). flow-node-docs green (was red on base: onhit),
flow-unknown-node 24. palette-groups 10/11 with its one red ("Music" has no card accent)
identical on base - not this lane. vitest 204 (+8). svelte-check 336/47, no new message.
Build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t height

- Towers def (scripts/author-templates.cjs, the DEFS region + towersGraph): a sky gradient
  with a far fog and a solid ground disc (daylight, exposure 1.05); a 2 m floor tile from a
  shader graph (uv -> split -> GLSL expression -> Surface albedo, multiplying the colour);
  chamfered walls with a glowing trim; wooden crates (physical + sheen + thin clearcoat,
  bevelSegments 1 to keep the file small); the pad under a clearcoat with a bright rim and a
  blue light; the height markers are thin glowing TORUS ring sensors (select-through) beside
  a marked pole instead of stacked translucent squares; faceted stars that turn and breathe
  (the Turntable + Pulse presets folded into ONE `Star glow` clip, played on round start);
  `view` + `thumb.camera`; collectible module version 1.1.1; Play again beside Menu on
  Round over.
- BEST HEIGHT (Store Value / Stored Value, 30-core-flow's nodes): `towers-best` (max) is
  written on every ring crossing and shown on the menu ("Your best tower: N m");
  `towers-last` holds THIS round, zeroed by its own `set` node on round start. Never on
  `over`: a perRound latch reads un-set the instant the round ends, which is also why the
  old Round-over line always read "0 m". `max`, not `set`, for the round's value too: a
  crate jittering in a ring re-fires the crossing after the round ended, when the height
  reads 0. Game-specific keys (a Games-tab load leaves the scene unnamed).
- core, outside the defs (minimal, named in the handover): HudElement.svelte — a centred or
  right-aligned SINGLE-LINE HUD text rendered flush left, in every game. The text box is a
  flex row, so its line is an anonymous flex item shrunk to the words and text-align had
  nothing to align; the row now takes justify-content from the same `align`. Scoped to
  text/timer (the list is a flex column).
- game-towers: TOWERS_TPSCENE env (a lane's staged build); 24 -> 36 objects (the trims, the
  pad rim + its light, the pole + 4 marks, the card camera); new checks for the look as
  data, no warning toast on load, the menu's best, centred texts, the star clips playing,
  centre luminance >= 0.25 in play, the saved best/last, Round over from storage, a
  re-fired crossing on the over screen, Play again zeroing the round, Quit showing the best.
  The pad is cleared before the round ends: a crate standing through a ring re-crosses it
  in the next round — measured, that was the base's red on "Restart cleared the height
  latches" (19/20 at base).

Counterfactuals (each broke, went red, restored):
- storelast mode 'set' -> "a crossing re-fired on the Round-over screen leaves this round's
  height alone" red ({"best":3,"last":0})
- HudElement back to boxStyle -> "centred HUD texts are centred on their box" red
  (offsets TOWERS -124.5, Your best -133 px)
- no `shaders` in the def -> "the arena floor carries its tile shader graph" red
- no `animations` -> "every star carries the Turntable + Pulse clip" and "Start sets all
  three stars turning" red

Suites (staged scene): game-towers 40/40 (base 19/20), game-editor-flow 41/41 (base 41).
svelte-check 336 errors / 47 warnings, list identical to base; vitest 204/204; build green.
Staged: cloud-lane-30-staging/towers (scene 48.8 KB, was 13.2 KB; thumb 6.5 KB). Centre
luminance in play 0.547 (before 0.593, both at the probe viewport).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…reen

- Stars Room def (the DEFS region + starObjects/starsGraph): a custom sky (a deep-blue
  gradient, a cool hemisphere, a soft key light; exposure 0.55 -> 1.1), a starfield from ONE
  continuous emitter flung to a ~20 m shell (speed / drag) that fades in only once it is past
  the glass, glass walls (clearcoat, 10% opacity, select-through) hung in a glowing frame, a
  glossy deck with a light ring, crystal stars (physical icosahedra, flat-shaded, emissive,
  SPHERE colliders), sheen planets, `view` + `thumb.camera`.
- THE EMITTER-CAP TOAST ON LOAD, fixed without touching the cap: a burst node per star was
  24 of the runtime's 8 emitters. Now ONE pooled burst on a `Burst anchor` (parked under the
  deck, since a burst emitter auto-fires once when it is built) that a Script node moves onto
  the star just hit — the star's number is sum(i * hit_i) (each On Hit reads 1 for its 0.3 s
  pulse), and the same sum is the burst's trigger. 2 emitters on load (was 24).
- A START SCREEN: Start round or Free play. Free play is a game STATE of its own ('free' —
  setGameState keeps an unknown state), so its screen is state-bound and the P menu's Resume
  (a `hide`) falls back to it; an override would have fallen back to the start screen.
  The round is two minutes (Game Time remaining; time up -> over "Time's up!").
- ROUND OVER FROM STORAGE: every perRound latch reads un-lit the instant the round ends, so
  the over screen reads stored values — `stars-best` and `stars-last` (max, written on every
  hit; max so a star knocked on the over screen cannot wipe them) and `stars-time` (max of a
  value that is 0 until the 24th star lights), each round zeroing its own two by `set` nodes.
  The title and the line pick their FORMAT through a Select (the Select-passes-a-string
  trick the lit colour already used): EVERY STAR LIT / "Every star lit in Ns" on a win,
  TIME'S UP / "Lit N / 24 in time" otherwise; the best round shows on the start screen and
  Round over, which also gets the touch leaderboard, Play again and Menu.
- game-stars-room: 36 -> 50 objects; starts on the start screen, Free play enters `free`;
  new checks for the glass, the crystals, the starfield/exposure, the emitter budget and the
  load toast, the pooled burst standing on the knocked star, centre luminance >= 0.25 in
  play, the stored round, and the best on the start screen. "Back to free play" -> "Menu".

Counterfactuals (one broken build, 4 red, restored):
- the Script edge removed -> "the pooled burst moved onto the knocked star" red (anchor
  still parked at 0,-3,0)
- a burst node per star again -> "the emitters fit the runtime's cap of 8" (25 + 1) and
  "no emitter-cap toast on load" red (the toast is back)
- the over title's Select unwired -> "the over screen names the time and the best, from
  storage" red

Suites (staged scene): game-stars-room 47/47 (base 36/36). No src change in this phase
(svelte-check/vitest/build as at p0: 336/47 identical, 204, green).
Staged: cloud-lane-30-staging/stars-room (scene 49.7 KB, was 34.7 KB; thumb 9.4 KB).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Jam Room def (JAM_DEF + jamGraph, replacing the inline DEFS entry; same place in DEFS, so
  the index order is unchanged): a small studio around the same twelve cabled devices and
  layout — a warm wooden floor, two rugs, plastered walls and a ceiling (select-through;
  the ceiling casts no shadow and glows faintly, with two light discs), five fabric
  acoustic panels, two floor lamps with their lights, a spot on the beat lab, an amp, a
  plant, a stool; a warm custom sky, the post floor, `view` inside the room (Play starts
  from the editor camera's spot) + `thumb.camera`.
- `play.cursor: 'free'` (30-core-flow's P3): you play the instruments by pointing at them —
  no pointer lock, the real cursor clicks keys, pads and the drum grid. No simulation.
- THE SHELL: a start screen (Start jam, this device's best tempo), a three-second COUNT-IN
  (its own screen, the number derived from Game Time), then the goal: keep the band
  playing for EIGHT BARS. Round over is "Session complete — 8 bars at N BPM", with Play
  again and Menu; the P menu has Resume / Restart / Quit.
- WHAT THE GOAL IS BUILT ON: core's Transport node (musicClock's shared transport as a flow
  value — beat, bpm, playing, identical on every peer). The music modules publish nothing to
  the flow (no notes-played count, no transport control), so the round cannot START the
  transport: the HUD says "Press ▶ on the Transport" until it runs, then "Keep the band
  going · N BPM". The beat the count-in ended on is kept with Set Variable (a replicated
  sample-and-hold), so bars count from THIS round; a transport restarted mid-round counts
  from its own zero (Select on beat < start). The goal gates on bars >= 8 AND the
  transport playing AND the round on and counted in, through All Players.
- THE SCORE is the tempo you finished at (the Transport's BPM −/+), `jam-best-bpm` (Store
  Value max) shown on the start screen and Round over; `jam-last-bpm` for the line.
- NEW suite game-jam-room (30): the twelve devices + the studio as data, the start screen,
  the free cursor in Play, the count-in, the goal waiting for the transport, the bars
  counting at 240 BPM, Session complete + the saved best, Play again counting from its own
  start, Quit showing the best, and centre luminance >= 0.25 on the goal screen (measured
  AFTER Start: the start screen's panel covers the centre, so it would measure the menu).
  Loads JAM_ROOM_TPSCENE or the sibling scenes checkout; the real music-lab/music-fx zips.

Counterfactuals (one broken build, 7 red, restored):
- the count-in's Set Variable trigger removed -> "the count-in kept the beat it ended on"
  and "the new round counts from its own start" red (bars 18.9 on Play again)
- no `play.cursor` -> "a free-cursor game" and "in Play the cursor stays free" red
- the best-tempo Store Value untriggered -> "Session complete names the tempo and the best",
  "the best tempo is saved", "the start screen shows the saved best" red

Suites (staged scene): game-jam-room 30/30 (new; luminance 0.425). No src change in this
phase (as at p0: svelte-check 336/47 identical, vitest 204, build green).
Staged: cloud-lane-30-staging/jam-room (scene 67.4 KB, was 35.6 KB; thumb 8.4 KB).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant