Skip to content

30-core-flow: games start on Start — Test play, free-cursor games, api.storage, a quiet console - #241

Open
AlexZ005 wants to merge 5 commits into
release/nextfrom
feat/30-game-flow
Open

AlexZ005 wants to merge 5 commits into
release/nextfrom
feat/30-game-flow

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Lane 30-core-flow of roadmap 30 ("Games that look finished"). Five commits, one per phase.

What landed

  • P0 console hygiene (688dc40) — <Canvas shadows={PCFShadowMap}> (identical picture, no deprecation warning); the play back-marker is a SvelteKit shallow entry (pushState from $app/navigation); rapier-compat's own init warning is filtered for that one call (it is inside the frozen dependency — no single-object form is reachable). helpers.cjs collects page.__console from before the first navigation.
  • P1 the editor stops masquerading (0da7522) — a scene is a game when a HUD screen is bound to a game state (showWhile); outside Play its screens are not drawn, the HUD editor's eye previews them inert and click-through, and GameChip ("Game · ") stands in for them.
  • P2 Test play + reset on last exit (7745a0c) — ▶ Test play (chip + the play button's right-click row) resets to the menu (respecting canResetGame), enters Play and shows the Start screen; the abandon watch now resets any non-menu round when the last player leaves (solo: at once; with peers: the 10 s window; host-written).
  • P3 free-cursor games + no grid in play (7484009) — scenePhysics.play.cursor: 'free' (omitted at default) plays without a pointer lock: the cursor aims, clicks fire On Click, press-drag carries bodies. New leaf playCursor.js (playAimNdc() is the seam 30-core-modes' pointerRay reads). The editor grid is hidden in Play.
  • P4 storage (dd59c8b) — api.storage.{get,set,remove,keys,clear,bytes} (JSON, tp:mod:<id>:<key>, 256 KB/module, local) and the Store Value / Stored Value flow nodes (tp:scene:<scene>:<key>, stamp-edge, send nothing), plus the HUD action "Save best score".

Suites (count vs base)

suite base after
svelte-check 336/47 336/47 (no new message)
vitest 196 204
game-state / game-presence / hud-actions / game-towers / play-interact (held) 45 / 61 / 65 / 20 / 46 45 / 61 / 65 / 20 / 46
console-hygiene · game-editor-flow · play-free-cursor · module-storage (new) 13 · 41 · 27 · 36

Every guard has a counterfactual in its commit body. Suites updated because they asserted the old behaviour: game-towers, game-loop, game-waves, game-untangle, game-dungeon-realms, touch-play. Pre-existing reds, A/B'd on base: game-waves' 6 ledger/run-log checks (module-side) and palette-groups' "Music has no accent". flow-node-docs was red on base (onhit undocumented) and is fixed here.

Owed (headless cannot judge)

  • Storage across a real Oculus Browser restart; Test play with two real people; free-cursor drag feel on a real mouse; the chip in non-dark themes and on a phone.

For the integrator

  • CHANGELOG: Test play; the round resets when the last player leaves; free-cursor games (+ no grid in play); api.storage + Store/Stored Value (+ "Save best score"); the three console warnings gone.
  • CLAUDE.md: the isGameHud rule beside THE HUD; the abandon-watch changes (21-F3 entry); playCursor.js and gameStorage.js (+ the key contract) in the map; page.__console in Verification.
  • Docs owed: nodes/storevalue.md, nodes/storedvalue.md, "Test play" in build-a-game.md, play.cursor, api.storage in module-sdk.md.
  • A Games-tab load leaves the scene unnamed, so template Store Values land in tp:scene:untitled:* — templates should use game-specific keys.
  • Debug hook tails +2 (playCursor, gameStorage): re-count after the union.

🤖 Generated with Claude Code

AlexZ005 and others added 5 commits September 23, 2026 00:38
…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>
- 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>
…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>
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