30b core-games: Towers, Stars Room and Jam Room become fun (game feel nodes, milestones, a VR jam cockpit) - #246
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 / pedalfootswitches — 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)
toggleVRMode's tick
force=true, grab = hapticPattern('hit') + select only in Edit).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).
ringchime, success buzz; the gold ring = "Top of the tower!" + confetti +
levelup), crate On Grabpop+On Impact
hit, whistle/banner at round start, clicks,arcademusic, 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 =
coinat the star + atapbuzz once per star per round, round banners, confetti +cheer at the end,
spacemusic, HOW TO PLAY, play.spawn [0,0,5.2].(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:
normalizeSpawnkeepsvrOnly,playSpawn.desktopSpawn(PLC default,objectActions Interact fly-to, moduleSDK respawn).
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
npm run buildgreen (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
towers 0.46/0.48, stars 0.31/0.30, jam 0.26/0.33).
Outside my lane (minimal, named)
fireObjectGrab(one call each).moduleSDK read it.
For the integrator
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.
gameFeelActions.js(the five nodes' runtime half; LOCAL from thereplicated stamp; music is a declaration; debug per sound name);
play.spawn.vrOnlybeside 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 checkout is absent here, so flow-node-docs SKIPped its docs half — those six rows are owed in
nodes.md).
OWED on device (Quest 3) — never claimed
real keys/pads, the speakers on stands; desktop Jam Room still starts from Scene's fixed Player.
🤖 Generated with Claude Code