Skip to content

30b core-games: Towers, Stars Room and Jam Room become fun (game feel nodes, milestones, a VR jam cockpit) - #246

Merged
AlexZ005 merged 19 commits into
feat/1.17from
feat/30b-core-games
Sep 23, 2026
Merged

AlexZ005 merged 19 commits into
feat/1.17from
feat/30b-core-games

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Lane 30b-core-games — Towers, Stars Room and Jam Room become fun

Merges feat/30b-vr-play + feat/30b-vr-modes (the C1-C6 contract) and builds on them. Needs #244 and #245 to land first (this branch contains both).

Deviations (also in QUESTIONS-30b-core-games.md): (1) the Jam Room spawn is VR-ONLY — an additive
play.spawn.vrOnly (desktop keeps its camera); (2) the sweep cannot flip the mixer mute / pedal
footswitches — music-fx has no click handler on them (a modules-repo fix); (3) no banner at a round's END
(it doubled the results panel); (4) Meshy credits unused (the author kit has no model-asset field).

Done (one commit per phase)

  • da87d8a merge of both VR lanes (the union their handovers spelled out: App.svelte tails 217/217/217,
    toggleVRMode's tick force=true, grab = hapticPattern('hit') + select only in Edit).
  • 8b604de P0 game feel as flow nodes (core): Announce ({v}), Game Sound (20 procedural sounds, placed
    at a wired object), Effect Burst (pooled, at an object or in front of the player), Controller Buzz
    (a haptic pattern), Game Music (a DECLARATION: Interact/Play, 'always' | 'round', on), On Grab
    (desktop carry + VR Interact grip). LOCAL on every peer from the replicated stamp, inside the
    actionSeenAt family; gameFeelActions.js (effectsBurst + vrControls primed). One outside-lane line:
    NodeWrapper's Music accent (palette-groups was red on feat/1.17).
  • accd772 P1 Towers + Stars Room: ring milestones (perRound Once -> banner, burst AT the ring, ring
    chime, success buzz; the gold ring = "Top of the tower!" + confetti + levelup), crate On Grab pop +
    On Impact hit, whistle/banner at round start, clicks, arcade music, HOW TO PLAY + a VR line,
    play.spawn [0,0,4.5]. Stars: a sparkle AT every knocked star (the pooled-anchor trick retired, 49
    objects), LIGHTING = coin at the star + a tap buzz once per star per round, round banners, confetti +
    cheer at the end, space music, HOW TO PLAY, play.spawn [0,0,5.2].
  • 4602922 P2 Jam Room cockpit: every instrument on a stand in tip reach of a VR-only spawn [1,0,-1]
    (measured: piano/drums/sampler/transport/mixer 0.54-0.69 m from the feet axis at 0.82-0.92 m; all 12
    devices on a clear laser line <= 3.5 m); NO music node (said in the def); clicks, "Go!", end = confetti +
    fanfare + buzz. Core: normalizeSpawn keeps vrOnly, playSpawn.desktopSpawn (PLC default,
    objectActions Interact fly-to, moduleSDK respawn).
  • 9fb5c7c P3 what the shots showed: no end banner over a results panel, a confetti puff with each ring
    sparkle (an additive sparkle vanishes against a daylight sky), the VR wrist/strip lines asserted in the
    stars and jam suites.

Every commit body lists its counterfactuals (all red, then restored).

Final battery (fresh server, final head 9fb5c7c, staged scenes) — all green

suite base now
game-feel-nodes (NEW) — 25/25 (two peers; the P0 commit body says 29 — 25 is exact)
game-towers (held) 40/40 52/52
game-stars-room (held) 47/47 57/57
game-jam-room (held) 30/30 39/39
music-template (held) 11/11 11/11
author-kit (held) green 59/59
templates-modal (held) green 45/45
vr-sweep 23/23 23/23 (on the new jam layout)
vr-game-panel 35/35 35/35 (staged Towers)
vr-mode-spawn 24/24 24/24
editor-modes · play-interact · game-editor-flow green 45 · 46 · 41
flow-unknown-node · palette-groups · flow-node-docs 24 · 9/1 red · green 24 · 11 · 8 (docs half SKIP: no docs checkout)
vitest 263 green (base 255 after the merges). svelte-check 333/47 — full message list IDENTICAL to the merge
commit da87d8a (diffed without line numbers). npm run build green (server stopped).

Staged content

/home/deck/.code/theprototype-app/cloud-lane-30-staging/30b-core-games/ — games/{towers,stars-room,jam-room}/
{scene.tpscene,thumb.webp} + index.json + README.md. towers 56282 B, stars-room 51354 B, jam-room 85884 B.
Screenshots: /home/deck/.code/lanes-30/after-30b/30b-core-games/ ({game}-{1-edit,2-play,3-started,4-later}.png

  • report.json: no editor helper line visible in Play in any of the three; luminance edit/started
    towers 0.46/0.48, stars 0.31/0.30, jam 0.26/0.33).

Outside my lane (minimal, named)

  • NodeWrapper.svelte: the Music group's accent (one line).
  • playInteract.beginGrab + vrControls grab: fireObjectGrab (one call each).
  • vr-modes' spawn files: locomotionPolicy.normalizeSpawn (vrOnly), playSpawn.desktopSpawn, objectActions +
    moduleSDK read it.
  • gameKit.js (vr-play's hub): exports gameFeelActions for the suites.

For the integrator

  • CHANGELOG: "Towers tells you when your tower reaches each ring — a banner, sparkles at the ring and a
    chime — and the gold ring at the top is a fanfare with confetti; crates pop when lifted and knock when
    they land; the game has music." · "Stars Room sparkles where you hit a star and pays a coin when it
    lights; a round ends in confetti and a cheer; space music." · "The Jam Room is a VR cockpit: stand in
    the middle and every instrument is in reach of your hands." · "New Game nodes: Announce, Game Sound,
    Effect Burst, Controller Buzz, Game Music, and the On Grab trigger." · every game menu says how to play.
  • CLAUDE.md: architecture entry for gameFeelActions.js (the five nodes' runtime half; LOCAL from the
    replicated stamp; music is a declaration; debug per sound name); play.spawn.vrOnly beside vr-modes'
    play.spawn line; gotcha: "an ADDITIVE burst vanishes against a bright sky — pair it with a normal-
    blended one (Towers' ring puff)"; gotcha: "a banner at a round's end doubles the results panel"; the
    flow-node-docs catalog scan breaks on a {v} nested inside a node's defaults/labels object (V_MARK).
  • Docs site: node pages for announce / gamesound / effectburst / hapticpulse / gamemusic / ongrab (the
    docs checkout is absent here, so flow-node-docs SKIPped its docs half — those six rows are owed in
    nodes.md).
  • Modules repo: music-fx click handlers on the footswitch / mixer mute (QUESTIONS 2).

OWED on device (Quest 3) — never claimed

  • Every sound's level against the music; the ring/top/coin moments' feel; the buzz patterns.
  • Towers: the spawn in front of the pad; the milestone banner readable in the headset; crate pop/knock.
  • Stars Room: the spawn inside the south glass; a coin + tap per star lit by hand.
  • Jam Room: the cockpit's reach and heights with real arms (stand 0.74 m, desks 0.78 m), the sweep over
    real keys/pads, the speakers on stands; desktop Jam Room still starts from Scene's fixed Player.
  • In VR the in-play hint lines still say "Press P to pause" / "P: menu" (no VR pause button exists).

🤖 Generated with Claude Code

AlexZ005 and others added 19 commits September 23, 2026 14:03
…seven music presets, two local volumes

- gameSfx.js (leaf): twenty procedural WebAudio effects with no asset files - click, pop,
  whoosh, success, fail, hit, kick, shoot, laser, explosion, coin, levelup, goal, whistle,
  cheer, step, ring, sparkle, hurt, portal - on the app's one AudioContext, through ONE
  "Game sounds" gain into the engine's sfx bus; a position spatialises it; a 24-voice cap
  so a sweep or a barrage cannot stall the audio thread. renderGameSound measures a sound
  offline (the suite's proof that every name makes one).
- gameMusicPresets.js (pure, vitest) + gameMusic.js: arcade / ambient / dungeon / stadium /
  space / puzzle / studio as four-bar loops of data, scheduled with a 25 ms lookahead
  timer (not rAF - an immersive session stops window rAF) and numbered off the SESSION
  clock, so two peers on one preset hear the same bar. Quiet under the effects (MIX 0.55).
  Refused in Edit (a module starting music from register() must not score the editor)
  and stopped on the falling edge of Interact/Play.
- gameFeel.js (leaf): the ONE "is the player playing" predicate (isLocked === true or
  editorMode === 'interact') every piece of game feel reads.
- moduleSDK: api.playSound routes a game name to the new set, keeps the ping chimes
  (ding/chime/pluck/bell), and an UNKNOWN name is now a quiet no-op returning false (it
  used to fall through to the ding) - the C5 contract; api.music.play/stop/current/presets.
- Settings > Interface > Sound: "Game sounds" and "Music" sliders, LOCAL, persisted via
  safeStorage (game:soundVolume / game:musicVolume).
- gameKit.js: the new leaves under ONE debug-hook name (App.svelte tails 215/215/215).

Suite game-audio (new): 25/25. vitest gameMusicPresets 6/6 (all vitest 233 green).
Counterfactuals (all four broken in one run, each red on its own checks):
- the stop-on-leave subscription removed -> 7.1, 7.2, 8.2 red (music kept playing, peak 0.0597)
- the Edit refusal removed -> 5.1, 5.2 red (music in the editor, peak 0.0751)
- the music volume no longer reaching the live gain -> 9.2 red
- the coin built as silence -> 1.2 red (quiet: ["coin"])
Held: hud-actions 65/0, game-editor-flow 41/0, sdk-game-seams 78/0 (module-toolbox and
editor-modes waited on the machine lock; run at P1). svelte-check 335/47, identical list to base. Build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bump/hit/knock defaults in Interact/Play

- vrControls.hapticPulse is now a NO-OP in Edit (gameFeelActive), the one funnel every
  core pulse and api.haptic go through - "the vibration should be only interactive mode,
  not in edit mode" (the user, Quest 3). A 64-pulse debug ring + a suppressed count
  (hapticDebug) is the suites' view, since no headset is attached headless.
- hapticPatterns.js (pure, vitest): tap, bump, hit, success, fail, rumble, heartbeat as
  timed, never-overlapping pulses (a new actuator pulse REPLACES the running one), a
  scale, and knockHapticScale. vrControls.hapticPattern plays one through setTimeout,
  each pulse re-gated, so leaving the game mid-rumble stops it.
- api.hapticPattern(name, hand?) -> boolean (false in Edit / unknown name).
- defaults: a VR grip GRAB lands with 'hit'; a KNOCK plays 'hit' sized by its closing
  speed (hapticKnock, the knock seam Scene hands knock.js); vrGameInput.js (new, plugs in
  through vrControls' frame + trigger hook registries, started from Scene) taps a hand
  when its laser ENTERS something clickable and bumps it when the trigger lands there.
  "Clickable" is one resolver for tap, bump and the P4 sweep: module interactive-group
  content, a part of a module device (userData.device - the piano key, not the piano),
  an On Click target, or userData.clickable === true.
- flowRuntime: objectHasOnClick(uuid) (fireObjectClick's own two rules), and
  fireObjectClick returns how many nodes it reached (additive).

Suite game-haptics (new, a stubbed XR session with recording actuators): 18/18.
vitest hapticPatterns 5/5 (all vitest 238 green).
Counterfactuals (four guards broken in one run):
- the Edit gate removed -> 1.1, 1.2, 3.4 red (3 pulses in Edit; rumble played 4 of 4 after leaving)
- the hover edge removed (tap every frame) -> 4.2 red (2 extra taps on one target)
- every object treated as clickable -> 4.3, 5.2 red
- the knock scale ignored -> 6.2 red (0.850 -> 0.850)
Held: hud-actions 65/0, game-editor-flow 41/0, sdk-game-seams 78/0, module-toolbox 46/0,
editor-modes 45/0.
svelte-check 334/47 (base 335: the JSDoc on hapticPulse's forEach removed a pre-existing
implicit-any; no new message). Build green.

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

- effectsBurst.js: api.effects.burst([x,y,z], {kind, color, count}) - sparkle / confetti /
  smoke / sparks as a POOL of 12 THREE.Points systems (96 particles each) at the scene root
  (never objectsGroup - golden rule 5), recycled oldest-first, LOCAL (nothing on the wire).
  Deterministic spread (a golden-angle spiral), per-kind motion (gravity, drag, rise,
  swell), additive kinds fade by darkening. Ticked from the flow runtime's frame loop (it
  runs in a headset, where window rAF stops) and leaving it when idle; the AGE is real
  time, so a burst lives its life whatever the frame rate (a throttled tab ticks ~5/s).
- gameAnnounce.js (leaf) + api.announce(text, {sub, ms, color}): a big centred banner,
  ONE at a time (a new one replaces), gone by itself; HudLayer draws it on the desktop
  (#game-announce, pointer-events none, stands down in VR - P3 draws it head-locked).
- a scene clear (runSceneClearHandlers) takes the live bursts and the banner with it.
- vrControls.hapticPulse gains an additive `force` (4th arg) for 30b-vr-modes' Edit/Interact
  switch tick, the one pulse that must be felt IN Edit (asked for by that lane).

Suite game-effects (new): 22/22.
Counterfactuals (four broken in one run):
- the burst never joins the frame loop -> 3.1, 3.2 red, and 6.1 (the frame changed 60 px, not 3709)
- the pool grows without bound -> 4.1, 4.2 red (21 systems for 20 bursts)
- the banner timer never clears -> 7.4 red
- the scene-clear hooks removed -> 8.2 red
Held: hud-actions 65/0, game-editor-flow 41/0, sdk-game-seams 78/0, module-toolbox 46/0,
editor-modes 45/0. svelte-check 334/47, no new message. Build green.

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

- Cause: helpers lived on render layer 1, and three's WebXRManager renders each eye
  through a sub-camera whose mask it rewrites every frame as
  (camera.mask | 0b110) & ~0b100 (left) and & ~0b010 (right) — layer 1 IS the left eye.
  So a light helper was drawn by the left eye whatever the editor camera enabled, and
  never by the right: the "light source helper with one eye" in Towers, Stars Room and
  Football on the Quest.
- helperLayer: HELPER_LAYER 1 -> 30 (3..30 are only ever inherited by the eye cameras;
  31 is overloadGuard's REDUCED_LAYER, postprocessing Selection ids count up from 2).
  helpersHiddenFor(locked, mode, debug) is the one predicate: Play OR Interact hide
  helpers (unless the debug toggle); editorHelpersShown is the same answer as a store.
  The editor camera and the camera-marker hop now follow editorMode as well as isLocked.
- helpers-in-play reads the layer from the app instead of assuming 1.
- New suite vr-helper-eyes (13): asserts three's installed WebXRManager still reserves
  1/2 for the eyes, then computes both eye masks with that formula — Edit: both eyes
  draw the helper and its proxy; VR Interact: neither; desktop Interact and Play: none.
- Counterfactual: HELPER_LAYER back to 1 -> vr-helper-eyes red ("the helper layer is not
  an XR eye layer", "Edit: BOTH eyes draw the light helper" L true R false).
- svelte-check 335/47 (unchanged list); vitest 222 green; build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The Quest report: "In interact mode I do not need helper boxes or shapes, outlines
  around objects." Play already hid the helper layer and the grid; Interact hid nothing.
- Every editor helper now answers the ONE predicate from P0 (helperLayer.helpersHidden /
  editorHelpersShown — Play or Interact, unless "Show helpers in Play (debug)"):
  the grid (Grid.svelte), collider/trigger wireframes (colliderHelpers), the tiny-object
  dots (tinyMarkers), a peer's lock box (LockHighlights), the selection + lock outlines
  (Outline: Interact joins Play), the VR selection shell (VRSelectionShell) and the VR
  hover box + emissive hover tint (vrControls updateHoverBox / setHovered). Light
  helpers, frustums and the module-content proxy ride the helper layer (P0). The
  selection itself survives Interact — only its glare goes.
- New suite interact-clean-view (16): a scene holding one of each helper, read in Edit,
  Interact, back in Edit, VR Interact/Edit, Play, and Play with the debug toggle.
- Counterfactuals: colliderHelpers without the helpersHidden gate -> "Interact: no
  collider wireframes" red (and Play); Outline without the Interact clause -> "Interact:
  no selection or lock outline (1/1)" red.
- Held: editor-modes, play-interact, pointer-capture-play green (= base).
- svelte-check 335/47 unchanged; build green.

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

- The Quest report: "When in game and in edit mode I should be able to move around the
  world and scale it with grips, now it disables this for some reason". Nothing disabled
  it: the world gestures fire only on a grip that closes on EMPTY AIR, and a game scene
  has none — Stars Room is a closed room, Football a glass box, Towers a 26 m floor ringed
  by walls — so every grip grabbed the room.
- vrGrip.js (pure, imports nothing): SCENERY (world bounds >= 4 m on any axis, or bounds
  that contain the head) is never held by a grip; pickGripTarget walks the ray's top-level
  hits: Edit takes the first non-scenery object, Interact only a grabbable one (a dynamic
  body under a play block whose interaction is 'grab'; a static object in front blocks);
  gripMovesWorld: only Edit's empty-air grip moves the world.
- vrControls: gripTargetOf (ray hits + the hand-inside test through that rule) feeds
  onSqueezeStart. Interact: no world grab/pan, no two-hand object scale, a RIGID grab with
  no snapping and no stick reel/scale, no selection (no editor lock), and no undo entry on
  release (a player's throw is not an edit). vrGripDebug for suites.
- knock.js: in VR only Interact's hands knock (an Edit hand is placing things).
- tests/e2e/fakeXR.cjs: a fake XR session whose gamepads a suite presses, so the REAL
  per-frame path (Scene's useTask -> updateVRControls) runs headless; controller poses are
  written into the controller matrices.
- New: vitest vrGrip (10), e2e vr-grips-by-mode (18): Edit two grips on a wall = world
  grab that scales x2 and leaves the wall; right grip on the floor = pan; a cube is still
  held, selected and one undo step. Interact: walls/air hold nothing and never move the
  world; a static podium is not holdable; the dynamic cube is, unselected, with no undo.
- Counterfactuals: scenery not skipped in pickGripTarget -> 8 e2e reds (Edit grabs the
  wall/floor instead of the world) + 3 vitest reds; gripMovesWorld always true -> "Interact:
  ... start no world gesture" and "Interact: the world stays put (x2.67)" red.
- Held: vr-world-grab 9/2 = base (the 2 are its pre-existing two-peer sync checks, red on
  pristine base with PEER_CONFIG too); vr-locomotion green.
- svelte-check 335/47 unchanged; vitest green; build green.

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

- The Quest report (the dungeon): "I can go through walls, teleport, I want to be able to
  do this only in edit mode and fly only in edit mode. In interactive mode I should be
  able to move like in a game."
- locomotionPolicy.js (pure): Edit = fly + teleport + world gestures, no walls (exactly
  the old movement); Interact = walk, collide, gravity, snap turn, NO fly or teleport
  unless `play.locomotion: {teleport?, fly?}` allows (absent = false; a flier drops
  gravity but keeps walls). normalizeLocomotion/normalizeSpawn + yaw helpers + the VR
  spawn as two WebXR offsets (used from P4).
- scenePhysics: play.locomotion and play.spawn normalised at the boundary and OMITTED
  when absent/empty, so saved scenes stay byte-identical. playSettings.resolvePlaySettings
  resolves `locomotion` field by field from the scene and publishers (the dungeon Kit
  publishes {teleport:false, fly:false}) and `spawn`.
- charController: tickWalker's body became resolveWalk(feet, height, dt, desired) so the VR
  walker resolves through the SAME tiers (rapier capsule with a sim / dungeon raster /
  ground plane). Asked by the dungeon lane: the raster now also clamps the rapier tier (Kit
  walls are module meshes, never rapier colliders). Desktop parity: tickWalker unchanged
  in behaviour (char-controller 55/55, twice).
- vrControls: vrWalkStep (stick -> head-yaw step at 2.2 m/s -> resolveWalk) and
  tickVRInteractLocomotion (feet = head world y minus the head's height in the BASE
  reference space captured at session start; moves by offsetting the XR reference space;
  falls out of the world -> back to the spawn/origin). updateTeleport honours the policy.
  VRControls.svelte: Interact runs the walker (a held object no longer stops your feet);
  Edit keeps its stick and now FLIES THROUGH walls (the raster clamp moved to Interact).
- Desktop Play keeps its built-in flight (the 21-E6 parity contract) but a 1 m head
  capsule now stops it at colliders while a simulation runs (collideRigStep; no world =
  byte-for-byte the old step).
- fakeXR.installSpace: a reference space + frame that compose offsets exactly as WebXR
  specifies, so the real per-frame walker runs headless.
- New: vitest locomotionPolicy (12, incl. the spawn offsets against WebXR's composition
  rule), e2e vr-walk-interact (20): a real rapier world — wall stops at -2.5, gravity lands
  the feet, a 0.25 m step is climbed, a 0.6 m block is a wall, a flier climbs; the real
  per-frame path walks forward and stops at the wall; teleport off in Interact, on with
  play.locomotion.teleport and in Edit; Edit flies through the wall; desktop Play stops.
- Counterfactuals: tickVRInteractLocomotion disabled -> "Interact: the wall stops you (z
  -7.55)" red; the teleport gate removed -> "Interact: the right stick does NOT arm a
  teleport" red; collideRigStep disabled -> "desktop Play: the wall stops you (z -15.10)" red.
- svelte-check 334/47 (one base error gone with the Edit raster clamp); build green.

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

- The Quest report: "By default for games there should be interact mode in VR, but I
  should be able to switch to edit mode", and "if the game should have a specific place
  where the character should stand, it should move."
- vrControls.onVRSessionStart (Scene's XR onsessionstart): remembers the base reference
  space, and a GAME (a HUD screen bound to a game state, a spawn, or a userData.play
  publisher) lands in Interact. Entering Interact in a session (editorMode subscription,
  the LAST statement of the file for the TDZ rule) drops any editor gesture, resets the
  world to 1:1 and pays the spawn on the first frame with a viewer pose.
- The mode button: LEFT Y (B on the right when the radial menu lives on the left, so
  they never share a button) toggles Edit <-> Interact with a haptic tick and a wrist
  label on that controller ('vr-mode-label': INTERACT teal / EDIT amber + the button).
- Spawn = `play.spawn: {position:[x,y,z], yaw}` (y = FEET, three rotation.y) or a
  module's `api.setSpawn(position, yaw, {teleport?})` (LOCAL, overrides the scene's,
  cleared when the module is disabled; without teleport a new spawn is a checkpoint) +
  `api.respawnPlayer()`. VR: vrControls.spawnPlayer turns the rig in place then moves it
  so the feet land on the point (locomotionPolicy.vrSpawnOffsets). Desktop Play: the
  play camera (playSpawn.spawnDesktopPlayer, ahead of the dungeon's per-peer rooms).
  Desktop Interact: the editor view flies there (objectActions.setEditorMode).
- New: e2e vr-mode-spawn (22): a plain scene stays in Edit, a game lands in Interact ON
  the spawn facing its yaw with the world back at 1:1; left Y -> Edit with one haptic tick
  and the label; walking off and pressing Y again re-spawns; with the menu on the left the
  right B switches; api.setSpawn checkpoint vs {teleport:true}, a malformed spawn refused,
  the module's spawn dropped on disable; desktop Play and desktop Interact spawn.
- Counterfactuals: no spawn on entering Interact -> "the player stands ON the spawn" and
  "facing the spawn's yaw" red; the mode button unwired -> "left Y: Interact -> Edit",
  "a haptic tick", "the label follows", "the RIGHT B toggles" red.
- svelte-check 334/47; build green.

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

- The Quest report: "When I spin the world around, the untangled dots do not spin
  around." The world gestures transform `world-grab-rig`; module viewport content lives
  at the SCENE ROOT (golden rule 5 keeps it out of objectsGroup), outside the rig.
- moduleWorld.js (a leaf): a `module-world-root` group INSIDE the rig (Scene.svelte, a
  sibling of objectsGroup — never serialised or sent), and every REGISTERED module group
  (registerInteractiveGroup / registerSystemGroup / registerListedGroup) is re-homed under
  it as it reaches the scene root (childadded + registry revision), local transform kept —
  desktop (rig = identity) moves nothing, VR carries it. For every module at once, no
  module change: `getObjectByName` still finds it, the scene instance's remove() also takes
  a re-homed group out, and core's scene-root scans (playSettings.playPublishers,
  moduleContent's rows) read the module root too. Unregistered content stays put.
- New e2e module-world-root (11): an inline module's groups land under the root (also a
  group added before its name was registered), an unregistered one does not, nothing
  enters objectsGroup, the dot follows a 90-degree spin + x2 scale of the rig, the Module
  content rows and the play publishers still find them, api.scene().remove works, disable
  removes them.
- Counterfactual: adoptModuleGroups a no-op -> "a registered group lands under the world
  rig's module root", "...also when added BEFORE its name was registered" and "the dot
  followed (1.000,1.000,0.000)" red.
- Held: object-list-modules 25/25 (= base, UNTANGLE_ZIP/TPSCENE from the sibling
  checkouts); svelte-check 334/47; vitest 244; build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, the wrist card, the top strip, the banner

- vrGamePanel.js: DOM is invisible in a headset, so the game's HUD documents are drawn
  into CANVAS TEXTURES on four scene-root meshes (default layer = both eyes, never the
  helper layer; LOCAL; only while presenting AND in Interact/Play):
  - the BOARD: a menu-like screen (input 'menu', a control on it, or bound to menu /
    paused / over) laid out with the desktop's own 9-grid (rectInFrame over the 1280x720
    stage), CROPPED to the part of the stage the screen uses and sized from it (1.6 mm per
    stage px, 1.15 m for the Towers menu) - drawn whole it was a wall of empty backdrop
    with the menu small in the middle. 1.2 m ahead at chest height, following the head
    LAZILY with hysteresis: a glance inside 35 degrees leaves it, a real turn brings it all
    the way back in front (without the latch it parked 28 degrees off to the side). A
    footer of ours: "Edit mode" (objectActions.setEditorMode('edit')) and "Top strip".
  - the WRIST card: the other screens (score, timer, level) as short lines on the LEFT
    wrist, shown when the wrist turns toward the face, with the same two buttons - a way
    back to editing mid-round. Placed clear of 30b-vr-modes' mode label.
  - the TOP STRIP: the glanceable readouts in one head-locked row (a sentence-long hint
    stays on the wrist), LOCAL on/off (vr:gameStrip), default on.
  - the ANNOUNCE banner, head-locked (api.announce in a headset).
  A press is the desktop press verbatim (fireHudButton; a toggle writes its value
  first; sliders/dropdowns/tabs step), with a 'click' sound and a 'bump'.
- vrGameInput wires it: the frame hook draws, hovers (the laser on the board wins over the
  world behind it) and POKES (the tip through a button presses once, re-arms on retreat);
  the trigger hook presses a board button and CONSUMES the press, and the trailing
  'select' is swallowed once; two beam providers end the laser on the board/wrist.

Suite vr-game-panel (new): 36/36, including section 13 on the REAL Towers template: its
menu on the board, "Start round" pressed by laser starts the round, its HUD on the
wrist/strip. Evidence: /home/deck/.code/lanes-30/after-30b/30b-vr-play/.
Counterfactuals (five broken in one run):
- the Interact/Play gate removed -> 8.2 red (the wrist and strip stayed up in Edit)
- the board press not consuming -> 3.2, 8.1 red
- the poke never re-arming -> 6.3 red
- the crop removed -> 1.6 red (a 2.05 m board)
- the follow latch releasing at the band edge -> 9.3 red (parked at 52.6 of 80 degrees)
svelte-check 334/47, no new message. Build green. Held suites run once for P3+P4
(machine-wide e2e queue of 11 lanes); see the P4 commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in Interact/Play, ctx.source, {sweep: false}

- vrGameInput: while the trigger is held in Interact/Play, the controller TIP (a 6 cm
  sphere just ahead of the controller) clicks the clickable it becomes NEAREST to, once,
  spent while still touched and re-armed on leaving; with the tip touching nothing, the
  LASER does the same for targets out of reach (> 25 cm) and for the game board's buttons.
  The first entry is the press itself (source 'trigger', fired on the PRESS so an
  instrument sounds when hit), later entries are 'sweep'. The sweep's hook does not
  consume; on its first frame it reads vrControls.triggerClaimed(index) and stands down
  under a gesture another hook claimed (knob drag, cable, sleeve). A press that clicked
  anything swallows its trailing 'select'.
- nearest-only is a deliberate reading of "every clickable the tip ENTERS": a 6 cm sphere
  is wider than a 2.2 cm key and the literal reading played clusters (QUESTIONS fork 2).
- moduleSDK: every click handler gets ctx = {source, mode} (additive); a handler
  registered with {sweep: false} still hears the press and never a sweep.
- vrControls: triggerClaims per slot + triggerClaimed(index) (additive).
- Scene: in Interact/Play the VR trigger's final fallthrough is interactClick (handlers,
  On Click, the miss) instead of raycastSelect - an Interact press selects nothing
  (30b-vr-modes handed this path to C3).

Suite vr-sweep (new): 24/24, including the REAL Jam Room (a held sweep over three piano
keys plays key-64, key-65, key-67 once each; over three drum steps flips each).
Counterfactuals:
- re-arm removed -> 2.1 red; {sweep:false} ignored -> 3.1 red; the swallow never set ->
  4.1, 5.2 red; the claim check removed -> 6.1 red; the Interact fallthrough removed ->
  9.1 red (the release selected the object) [one run, five broken]
- every touched target fired (no nearest rule) -> 11.1 red (["n1","n0","n2","n3","n4","n5","n6"])
svelte-check 334/47, no new message. Build green. Held suites for P3+P4 NOT yet re-run
(budget checkpoint; P2's held run was green) - owed before the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, banners), the VR board, hold-and-sweep

- a "Game feel" section with a ten-line example: api.playSound's game set (and the
  unknown-name no-op), api.music, api.hapticPattern (and Edit silence + the core
  defaults), api.effects.burst, api.announce; how a game's HUD reaches the VR board,
  wrist card and strip with nothing to do on the module side; the hold-and-sweep, what
  counts as clickable, ctx = {source, mode} and {sweep: false}.
- the Interaction section: the VR trigger now follows the editor modes.
- Misc: api.hapticPattern beside api.haptic.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ecks wait on the thing

- effectsBurst: the Points program was compiled on the FIRST draw that needed it, and
  on a loaded device that stall ate the first burst of a session whole (measured: the
  first burst on a fresh page changed 0 pixels, every later one ~1500) - the first
  "ring reached" sparkle is exactly the one a player would miss. warmUp() compiles the
  burst material when the pool is built (renderer.compile on one system; compile()
  walks visible objects only, so it is shown for the call).
- game-effects: 1.5 asserts the program exists before any frame is drawn; the pixel
  check uses a confetti burst (2.2 s of life - a slow frame cannot outlive it) and
  samples until a frame differs instead of one fixed 120 ms sleep, with a state dump in
  its message. (While finding this, zero-pixel frames on a long-lived dev server turned
  out to be the edited-under-a-running-server dual-instance trap; a fresh server reads
  3361 px.)
- game-haptics: the pattern checks wait for the PULSES (headless timers ran ~2x late on
  a machine running two e2e slots), not a clock.
Counterfactual: warmUp removed -> 1.5 red (16 -> 16 programs).
game-effects 24/24, game-haptics 18/18 on a fresh server; svelte-check 334/47; build green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…at/30b-vr-play)

- the union the two lanes' handovers spelled out: App.svelte debug tails gameKit + moduleWorld +
  playSpawn (counts equal), Scene imports both hapticKnock and onVRSessionStart, moduleSDK takes
  both import blocks, vrControls keeps the haptic ring AND the Interact walker, the grab lands
  with hapticPattern('hit') and selects only in Edit, and the Edit/Interact switch tick passes
  force=true through the Edit haptic gate
- svelte-check 333/47 (feat/1.17 335/47; each lane removed one), vitest 255 green

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…und, effect burst, controller buzz, game music, on grab

The three core games are FLOW graphs, and 30b-vr-play's game-feel kit (api.announce,
the procedural sound set, api.effects.burst, api.hapticPattern, api.music) was reachable
from a MODULE only - so a Towers ring could not say it was reached, sparkle, chime or buzz.

- five Game nodes (nodeCatalog 'Game' group, AnimationNode cards, flowSockets, nodeDocs):
  Announce ({v} filled from a wired value), Game Sound (the 20 procedural sounds, placed at
  a wired object), Effect Burst (pooled sparkle/confetti/smoke/sparks at a wired object, or
  in front of the player), Controller Buzz (a haptic pattern), Game Music (a DECLARATION:
  plays while in Interact/Play, 'always' or only during a 'round', `on` switches it)
- the four actions run on the trigger's STAMP EDGE inside flowRuntime's actionSeenAt family
  (updateGameNodes), LOCAL on every peer from the replicated stamp - no message of their own;
  haptics and music keep the core Interact/Play gate (silent in Edit)
- gameFeelActions.js holds the runtime half (effectsBurst and vrControls PRIMED, the
  flowRuntime rule); musicWanted + fillValue are pure and vitest-covered
- On Grab (Triggers): fireObjectGrab from the desktop play/Interact carry (playInteract
  beginGrab) and a VR Interact grip (vrControls, never an Edit move) - Towers' crates lift
  with a sound
- option lists come from the leaves that own them (GAME_SOUNDS, HAPTIC_PATTERN_NAMES,
  MUSIC_PRESET_IDS); gameKit exports gameFeelActions for the suites
- outside the lane, one line: NodeWrapper gains the Music group's accent - palette-groups
  was red on feat/1.17 ("Music missing of 12")

Suites: NEW game-feel-nodes 29/29 (two peers); vitest gameFeelActions 7 (262 total);
flow-unknown-node 24/0, palette-groups 10/0 (base 9/1), flow-node-docs green.
svelte-check 333/47 (unchanged from the merged base).

Counterfactuals (each red, then restored):
- action dispatch moved ahead of staleTrigger -> 3.1/3.2 red (fresh nodes announce an old stamp)
- musicWanted ignoring `while` -> 5.4 red (music in menu and over)
- worldPositionOf returning null -> 1.4/1.7 red (sound unplaced, burst in front of the camera)
- the family dropped from updateGameNodes' filter -> 14 red (1.x, 3.2, 4.x, 6.x)
- beginGrab not calling fireObjectGrab -> 7.2 red

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hts with a coin, both sound like games

The user on Towers: "When I reach some of the rings and go to the top, it should dynamically
tell me and give effects such as sparkles ... there should be some sounds." Built on P0's
Game Feel nodes, so the defs stay data.

Towers (scripts/author-templates.cjs):
- each ring is a MILESTONE: its first crossing in a round (a perRound Once - a crate
  jittering in the sensor must not re-announce) says "Ring N reached", bursts sparkle AT the
  ring, chimes `ring` from it, and asks the `success` buzz; the gold ring at 4 m says "Top of
  the tower!" with confetti + a sparkle at the ring and the `levelup` fanfare (it replaces 30's
  particle emitter on the pad)
- crates: an On Grab `pop` and an On Impact (0.8) `hit` knock, both AT the crate
- round start whistle + "Build!", all-stars cheer + confetti + banner, time-up whistle + banner,
  every menu button clicks, the `arcade` music while in the game
- HOW TO PLAY on the menu (the goal, the milestones, the clock) with a desktop and a VR line
- play.spawn [0, 0, 4.5] facing the pad
Stars Room:
- every knock sparkles AT the star (the core's pooled Effect Burst - which retires 30's
  Burst anchor + Script + hit-index sum, 49 objects); LIGHTING a star (a perRound Once) pays a
  `coin` at it and a `tap` buzz, once per star per round
- round start banner + whistle, free-play banner, win banner + `levelup`, time-up banner +
  whistle, confetti + a cheer when the round ends either way, button clicks, `space` music
- HOW TO PLAY on the start screen; play.spawn [0, 0, 5.2] inside the south glass
- gameFeelActions debug counts per sound name (a suite counts coins among the clicks)

Suites (staged scenes, cloud-lane-30-staging/30b-core-games): game-towers 52/52 (base 40,
+12 30b checks), game-stars-room 55/55 (base 47; the anchor check became the sparkle-at-the-
star check, 50 -> 49 objects, +8 30b checks).

Counterfactuals (re-authored into a scratch tree, each red, then restored):
- towers milestone wired from the ring sensor instead of the Once -> "a second crossing of
  ring 1 ... says nothing" red
- towers spawn removed -> "Play put the eye on the spawn" red (0, 2.9, 3: Scene's fixed Player)
- stars coin wired from every hit -> "no second coin" red (2) and "exactly one coin per star"
  red (26 / 24)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ment in reach of the spawn, a session that ends in a fanfare

The brief: "make sure every instrument is reachable standing at the spawn (play.spawn);
music preset studio stays OFF (the room IS the music)". 30's layout was a semicircle for a
desktop view: in VR the piano sat on the FLOOR (keys at 0.08 m) and the drums were two metres
from anyone.

- THE COCKPIT (scripts/author-templates.cjs): a U of stands around the spawn at [1, 0, -1]
  facing -Z - the piano straight ahead on a keyboard stand (keys at 0.88 m), the drum machine
  on the left desk, the sampler and the mixer on the right desk, the transport on its stand
  behind-left, each turned to face the player; the pedal chain on a board beyond the right
  desk and the two speakers on stands behind the piano (above the keys, so the laser and the
  sound clear it). The furniture is select-through. The stage spot, the view and the card
  camera re-aim at the cockpit.
- a VR-ONLY spawn: `play.spawn.vrOnly` (additive, kept by normalizeSpawn only when exactly
  true) - the headset stands in the cockpit; desktop Play and desktop Interact keep their
  own camera (playSpawn.desktopSpawn, used by spawnDesktopPlayer's default, objectActions'
  Interact fly-to and moduleSDK's respawn). A level desktop eye at the cockpit sees only the
  piano - measured: centre luminance 0.236 against the 0.25 floor with the flag ignored.
- NO Game Music node, said in the def: the band is the music
- how to play on the start screen, a VR line (hold the trigger and sweep), every button
  clicks, "Go!" when the count-in ends, and Session complete = a banner "8 bars at N BPM",
  confetti, the level-up fanfare and a success buzz

Outside the lane (vr-modes' files, minimal): locomotionPolicy.normalizeSpawn keeps vrOnly;
playSpawn.desktopSpawn; objectActions + moduleSDK read it. vitest locomotionPolicy +1.

Suites: game-jam-room 37/37 (base 30; the cockpit's reach MEASURED - the nearest control of
piano/drums/sampler/transport/mixer is 0.54-0.69 m from the feet axis at 0.82-0.92 m high, and
all twelve devices have an unblocked laser line under 3.5 m); vr-sweep 10.x green on the new
layout (keys and drum steps swept); music-template green.

Counterfactuals (each red, then restored):
- normalizeSpawn dropping vrOnly -> the VR-only spawn check and "desktop Play does not stand
  on the VR-only spawn" red (and the play frame's luminance fell under its floor)
- the 30 scene (round-1 layout) -> the spawn, tip-reach and laser checks red
- speakers on the floor behind the piano (the first cut) -> the laser check red: the keys
  block the line (key-62, key-69)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ults panel, a ring burst that reads in daylight, the score read in VR

Reading the after-30b screenshots (1540x774, GPU ANGLE Vulkan) before calling it done:
- a round's END banner ("Every star lit!", "Time's up!", "All stars collected!", "Session
  complete!") sat on top of the Round over / Session complete panel that says the same thing,
  and read twice (jam-room-4-later). The end is confetti + a cheer/fanfare/whistle + a buzz
  now; its words are the panel's. Start/free-play/milestone banners stay (no panel under them).
- the ring sparkle is ADDITIVE and near-invisible against Towers' daylight sky: rings 1-3 add
  a small confetti puff at the ring (normal blending; the gold ring's burst was confetti).
- the stars/jam suites now read the VR wrist card / top strip lines (vr-play's overlay split):
  "Lit: 1 / 24" + the clock, and "Bar N / 8" + the beat, with no board screen while playing.

Suites (restaged scenes): game-towers 52/52, game-stars-room 57/57, game-jam-room 39/39,
vr-game-panel 35/35 on the staged Towers (13.x: Start pressed by laser on the VR board, the HUD
on the wrist/strip).

Counterfactual: the jam session re-authored WITH its end banner -> "no banner over the Session
complete panel" red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AlexZ005
AlexZ005 merged commit c94a6f1 into feat/1.17 Sep 23, 2026
4 checks passed
@AlexZ005
AlexZ005 deleted the feat/30b-core-games branch September 23, 2026 22:38
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