Conversation
…t = one per press) - three 0.185's OrbitControls.onPointerDown calls domElement.setPointerCapture with no guard, and the editor <OrbitControls> stays MOUNTED in play. Chromium refuses a capture with InvalidStateError while the page holds a pointer lock, and the throw lands before _addPointer, so EVERY press in play threw once and later surfaced as "Something went wrong ... Copy diagnostics" (the Quest diagnostics log, nodes/2.*.js:372:15736). - Half 1: Scene's frame task stands the active OrbitControls down while isLocked === true and restores exactly what it stood down when play ends. Per FRAME, not on the isLocked edge: TransformControls unmounts in play and its auto-pause cleanup writes enabled = true unconditionally a tick later, which would stomp an edge write. - Half 2: setPointerCapture is guarded on the element the controls listen on (skipped while document.pointerLockElement is set). Smaller than subclassing, which would mean replacing threlte's <OrbitControls> with a hand-built <T is>; it also covers the camera preview's OrbitControls, which listens on the same element. It calls the prototype method at call time, and is removed on destroy. - New suite pointer-capture-play (12): Chromium's rule modelled in-page (a pointerLockElement getter + a prototype setPointerCapture that throws under a lock, checked as a premise), 20 real CDP presses in play, a hand re-enable to prove the guard alone, no diagnostics toast after leaving play, and the editor still orbits. - Counterfactuals: both halves removed -> 20 errors for 20 presses, the toast, 6 red (the reported bug, reproduced); capture guard alone removed -> the re-enabled-controls check red (2 errors) + the toast. - svelte-check 336/47 = base (line shifts only); vitest 196/196; build green; selection-extras 19/19 at base. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, object flags
- build() gains capsule / plane / ring / icosahedron / dodecahedron (core three geometries,
rebuilt from their parameters by ObjectLoader) and box `bevel` (+ bevelSegments): a port of
three/examples RoundedBoxGeometry baked into a PLAIN BufferGeometry, because toJSON writes
a subclass type ObjectLoader cannot rebuild (core Box params carry no bevel)
- light `kind`: spot (angle, penumbra, target) / directional (castShadow, shadowMapSize,
frustum FITTED to the built meshes in the light's own frame; `fit: false` opts out) /
hemisphere (groundColor); spot + directional follow createLight (cast by default, the V-1
bias pair) and aim by ROTATION at `target` (24-E1). point (no kind) is untouched
- materials: `physical: true` or any physical-only field (clearcoat, clearcoatRoughness,
transmission, thickness, ior, sheen, sheenColor, sheenRoughness, iridescence,
specularIntensity) -> MeshPhysicalMaterial; `toon: true` -> MeshToonMaterial;
`flatShading`, `side: 'double'|'back'`; emissiveIntensity as before
- flags: `pick: 'through'` -> userData.pick; `origin: [x,y,z]` -> userData.origin;
`particles: '<preset>' | {preset, ...patch}` -> userData.particles from particlePresets;
`anim: '<preset>' | [..]` -> animationPreview.applyPreset (by key or name), an ordinary
authored clip (not playing — a Play Animation node runs it)
- loader: `--def <file.json[,..]>` adds defs from JSON (one or an array); a same-slug def
REPLACES the DEFS entry. Unknown object types now throw instead of silently becoming cones
- suite author-kit (new): authors a def-under-test via --def/--only into a temp dir, loads it
back through sessions.readSessionZip + applySession, one check per feature: 40 PASS
- byte-identity: level-blockout/towers/stars-room/untangle/waves authored before and after,
scripts/compare-authored.cjs reports SAME for all five (thumbs identical in size)
- counterfactual: 'clearcoat' dropped from PHYSICAL_KEYS + the pick line removed -> exactly
"physical: clearcoat 0.8 survives (0)" and "pick: 'through' lands on userData.pick" FAIL
- held: templates-modal 47 / template-modules 29 PASS lines at base (no src change in this
commit); svelte-check 336/47 identical list; vitest 196; build green
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hState warning, rapier's init warning
- App.svelte: <Canvas shadows={PCFShadowMap}>. threlte defaults to PCFSoftShadowMap, which
three 0.185 deprecates and downgrades to PCF on the first shadow render - so the picture is
identical and the warning is gone.
- playMode.js: the back-button marker is a SvelteKit SHALLOW entry (pushState from
$app/navigation) instead of a raw history.pushState, which the router answers in dev with
"will conflict with SvelteKit's router". Same url, same navigation index, so the Back that
spends it never navigates. New playMarkerState() reads the state under sveltekit:states.
- physics.js: the wasm "deprecated parameters for the initialization function" warning is
rapier-compat's OWN init() handing wasm-bindgen a bare byte array; the call is inside the
frozen dependency, so there is no single-object form to pass. It is filtered for the
length of that one call (exact prefix, restored in finally) - every other warn passes.
- helpers.cjs: page.__console collects every console message from BEFORE the first
navigation (the PCF warning fires on frame one; a listener attached after boot misses it).
- touch-play: reads the marker through playMarkerState() (the state moved under the
router's key).
Suite console-hygiene (new) 13/13. Counterfactual: all three source fixes reverted at once
-> 4 red (the PCF warning, the marker not being a shallow entry, the router warning, the
wasm warning), restored -> green. touch-play 39/39.
Held at base: game-state 45, game-presence 61, hud-actions 65, game-towers 20,
play-interact 46. svelte-check 336/47 (no new message), vitest 196, build green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ers routed by mode
- New LOCAL store `editorMode` ('edit' | 'interact', sceneStore; never sent, never
saved). EDIT (default): a viewport click SELECTS first; a module click handler runs
only if it registered for Edit; On Click nodes do NOT fire from the editor pick.
INTERACT: a click is play's tap aimed by the cursor (module groups, handlers that run
in 'interact', On Click nodes, the miss), nothing is selected and the gizmo is put
away; a press on a dynamic body while a sim runs CARRIES it along the cursor ray with
playInteract's own hold (one spring, one throw, one cancel discipline — only the aim
differs) and the camera controls stand down for that gesture. Play is unchanged.
- SDK: `api.registerClickHandler(fn, {modes})`, modes any of 'edit'|'interact'|'play';
ABSENT = ['interact', 'play'], so a module that never passes it stops eating editor
clicks (the reported Jam Room case: Piano / Drum machine / Sampler never selectable).
One dispatch `runClickHandlers(object, mode)` for the editor pick, Interact and the
play tap. A core handler pushed straight into the array (vrPatch's plug click —
patching is authoring) runs in every mode. VR's trigger passes no mode and keeps
offering every handler first (VR has no mode toggle yet — recorded).
- UI: an Edit/Interact toggle cell ('Interact mode (I)', a real <button> with
aria-pressed, `#editor-mode-toggle`) joins the default Controls bar at the right end
(the place loadLayout already appends a default id an older record lacks, so the well
keeps its slot); key I through the shortcut registry, standing down in text fields,
mesh sessions (MESH_EDIT_KEYS already) and — through a new `registerKeySessionProbe`
seam Scene feeds — sculpt, spline edit and draw. A transform tool (1/2/3, the bar's
Move/Rotate/Scale) picked in Interact goes back to Edit in that tool.
- objectActions: setEditorMode / toggleEditorMode (Interact detaches the gizmo, Edit
re-seats it on the selection), and applySelectionSet attaches no gizmo in Interact.
- New suite editor-modes (41, two peers): toggle + key + registry row + text field;
an edit-only handler runs in Edit and the default one does not (and vice versa in
Interact); an On Click node fires only in Interact and its pulse reaches the peer;
the gizmo leaves and comes back; the REAL music-lab piano (installed zip): an Edit
click on a key selects the piano and sends no devicenote, an Interact click sends one
and selects nothing; an Interact press-drag carries a dynamic crate with the cursor
(up the screen = up in the world), without orbiting, and lets go on release.
- controls-roster updated in-commit for the new default cell (108/108): the default
row, the counts and the rightmost cell; its cell locator addresses the CELL rather
than a <p>.
- Counterfactual: every handler hears every click again (the pre-P1 routing) ->
5 red: the no-{modes} handler runs in Edit (2.3), both counts
double (2.5/2.6), and the Edit click on a piano key plays a note instead of selecting
the piano (5.3/5.4) — the reported bug, reproduced.
- svelte-check 335/47 (base 336/47: one pre-existing implicit-any in Scene removed,
nothing new); vitest 196/196; build green. Held: play-interact 46/46, selection-extras 19/19,
sdk-game-seams 78/78, module-toolbox 46/46, game-untangle 46/46.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, sun, hemi
- a def `env` may be {preset:'custom' | <preset>, base?, exposure, background:'#hex' |
{top, bottom}, fog:{color, near, far} | null, ground:{color, roughness?}, sun:{color,
intensity, dir | position} | null, hemi:{sky, ground, intensity} | null}; colours may be
numbers. It builds a payload from a base preset and commits it through the environment
module's own custom path (applyCustomPreset), so the scene saves it as customPreset and a
peer / late joiner gets it on the existing environment singleton. The payload's own
exposure stays 1 and the def's exposure is the STATE multiplier (applyCustomPreset would
otherwise square it). A plain preset env takes the old line verbatim
- environment.js (ADDITIVE, absent on every stock preset and every older save):
`gradient: {top, bottom}` -> a cached 2x256 CanvasTexture background (`background` keeps
the flat horizon colour for the backgroundColor store and older peers); `ground: {color,
roughness?}` -> an `env-ground` disc in ENV_ROOT (scene root, never synced or saved as an
object) that receives the sun's shadows, so the ShadowMaterial catcher stands down while
it shows; hidden in wireframe and in passthrough (it would cover the real room).
Read only through skyGradientOf / skyGroundOf (a malformed value reads as absent)
- the per-def reset uses environmentRestore(null) — setEnvironment('studio') kept a custom
payload in the state, which environmentSnapshot would have saved into the NEXT def
- suite author-kit: + a sky section (state, texture background, fog, ground visible,
catcher down, sun dir, hemi, exposure not squared) and a PIXEL check on the loaded scene:
sky bands R 206 vs 81 (upper redder), B 206 vs 81 (lower bluer) — 53 PASS
- counterfactuals: the builder ignoring the gradient -> 4 FAIL (saved gradient, texture
background, both pixel bands 34/34, 255/255); environment.js skyGradientOf -> null ->
3 FAIL (texture background + both pixel bands)
- byte-identity: level-blockout/towers/stars-room/untangle/waves SAME vs the pre-lane build
- held: templates-modal 47/0, template-modules 29/0 (= base); svelte-check 336/47 with an
identical list; vitest 196; build green on this environment.js
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The pick returned the NEAREST hit whatever its opacity, so a near-invisible shell won every click (measured: Stars Room's 0.12-opacity walls took 16 of 16 clicks on stars, pads and planets; Football's 0.06 ceiling ate the lamps and bars). New pure leaf `selectThrough.js` (imports nothing): `pickStack` collapses raw hits into the stack of top-level targets (a target is opaque if ANY of its hits is), `isSeeThrough` = the top-level `userData.pick === 'through'` flag, a surface under 0.25 effective opacity (the hit face's own material on a multi-material mesh), or a hidden node on the way up; `primaryIndex` = the first opaque target, else the nearest. - CLICK-CYCLE: a plain repeat within 4 px walks DOWN the stack and wraps (so the shell in front is reachable too), with a one-time "Click again to select behind" hint. TIMING DEVIATES FROM THE PLAN (< 600 ms): a repeat cycles only after the 400 ms double-click window and within 1500 ms — inside 400 ms is the configurable double-click action, and a cycle there would kill double-click-to-open-properties wherever anything sits behind the cursor. Recorded as a fork in the lane's questions file. - Wired into Scene's raycastSelect (Edit and VR; the cycle needs click coordinates, so desktop only) and into Interact's click, so a glass wall does not take a star's click there either. An Edit-mode module handler is offered the CHOSEN hit. - Inspector ▸ Object: "Click-through in the viewport" (#inspector-pick-through), fanned over the selection. objectActions.setPickThrough writes userData.pick through ONE `props` undo entry and the existing `objectParameters` message (parameter 'pick', null = cleared) — the userData.physics shape; commandsHandler gains the receive branch. Absent is the default, so a never-flagged object serialises byte-identically. - New suites: select-through (17, two peers: a box inside a 0.1 shell selects the box, the repeat reaches the shell and comes back, a double-click does not cycle, the real checkbox makes an opaque wall stand aside, the flag reaches the peer, one undo clears it on both, no key on an unflagged object); vitest selectThrough (18). - Counterfactual: nearest hit only -> 5 red (the box inside the shell selects the shell, the cycle checks, and the flagged wall still takes the click). - Probe (SELECT_ONLY, stars-room + football, real Templates modal + feed): Stars Room objects that select THEMSELVES 1/15 -> 10/15 (the rest: the see-through walls themselves, reachable by the repeat, and a star genuinely behind a planet); Football 3/16 -> 8/16 (the remaining misses are the HUD menu buttons over the viewport — roadmap cause 4, 30-core-flow). - svelte-check 335/47 (= after P1, nothing new); vitest 214/214; build green. Held on this commit: editor-modes 41/41, selection-extras 19/19. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…content section
- Scene-root module content (the untangle board, the dungeon, the sabers, ...) lives
outside objectsGroup by golden rule 5, so it was never picked and never LISTED (only
the advanced System filter polled it, naming no module). New LEAF `moduleContent.js`
(THREE + stores + helperLayer): the registry moduleSDK writes on every
registerInteractiveGroup / registerSystemGroup (and the new registerListedGroup), the
rows (named children to depth 2, capped at 200 with a "+N more" line), and the
selection PROXY — a scene-root Box3Helper on the helper layer sized to the group's
bounds, following it at 4 Hz. Never in objectsGroup: it never replicates, never
saves and never enters the selection SET, so the outline, the gizmo and every
act-on-selection command are untouched. Any object selection or deselect clears it.
- `ModuleContent.svelte` under the scene tree: one READ-ONLY row per group present in
the scene, the module's name as a badge, expandable, a local hide-in-viewport eye; a
click selects the proxy, frames it and opens the Inspector; its context menu is
Frame it / Open <the module's toolbox> / Hide in viewport (this device) — no rename,
delete or drag. Re-derives on pokeScene AND every second (module content changes
without a poke — the System view's precedent, which stays).
- Inspector: a card instead of transform rows ("Made by the <module> module — edit it
with its toolbox or nodes", plus the toolbox's button when it has one).
- Viewport: an EDIT click on module content that no edit handler took selects its
proxy when it is nearer than the object hit; in INTERACT the click stays the module's.
- SDK: `api.registerListedGroup(name, {label, icon})` (journalled) names a group for a
person; without it the row reads the group name humanised ('untangle-module' ->
'Untangle module'). moduleSDK re-exports `moduleContentDebug` for suites (no new
debug-hook tail).
- New suite object-list-modules (24, two peers, the REAL untangle zip + template from
the feed / sibling checkout, skip-never-fail): the row, its label and badge, the
dots listed, not in the scene tree, a read-only menu; a click selects a proxy at the
scene root around the board, frames it, the Inspector card, an empty object set;
object counts unchanged on both peers, no proxy on the peer or in a saved payload;
deselect removes it; an Edit viewport click selects the proxy, an Interact one does not.
- Counterfactual: the section filtered out of the list -> 1.1 red (the row is
gone and the suite stops there); the Edit viewport branch disabled -> 5.2 red (an Edit
click on the board selects nothing).
- svelte-check 335/47 (nothing new); vitest 214/214; build green. Held: editor-modes 41/41, selection-extras 19/19,
module-toolbox 46/46, game-untangle 46/46.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- hudDocs: `isGameHud(docs)` + the `hudIsGame` store. THE RULE: a scene is a game when any
HUD document holds a screen bound to a game state (`showWhile` set) - that binding is the
one thing that makes the editor masquerade (the shared state picks the screen, so the
menu paints itself over the editor with nobody in Play). Every Games-tab template has
one. A hudbutton->setgamestate wiring alone was considered and NOT taken: with no
state-bound screen nothing masquerades, and reading the graph would put a flow edge
into a leaf that keeps none. A HUD with no state-bound screen keeps its old behaviour.
- HudLayer: outside Play a game's screens (scene and camera docs) are not drawn. The HUD
editor's preview eye (`hudPreviewInViewport`) still draws them, with every element INERT:
HudElement `editor` (the artboard's swallow, out of the tab order) AND the whole preview
click-through (`.hud-inert`), so a previewed menu can never fire the game nor eat a
viewport click (the roadmap's selection finding 4). An inert preview claims no keys.
- GameChip.svelte (new, mounted in App.svelte beside HudLayer): "Game · <state>" + an eye
that toggles the same preview store. Editor-only: hidden in Play, VR and embed mode.
Top-left under the logo (tucks under a docked Connect bar via --connect-bottom), z 38 -
the authoring HUD band, under every window. Test play joins it in P2.
- Suites broken by the change, updated in place: game-towers ("the menu renders" moved to
after the play press), game-loop (the 21-D scenario presses Start IN PLAY now - both
peers play, the menu screen frees the pointer, A approves the late joiner from the
editor while B keeps the round alive), game-waves/game-untangle/game-dungeon-realms
(the editor-side menu check becomes "the chip stands in for it", the menu check moves
into play).
Suite game-editor-flow (new) 17/17 on the real Towers .tpscene + collectible zip.
Counterfactual: HudLayer restored to base -> 8 red, incl. "clicking where Start used to
be leaves the game on its menu (playing)" - the reported bug, reproduced.
game-loop 31/31 · game-untangle 47/47 · game-dungeon-realms 65/65 · game-waves 85/91
with the SAME six ledger/run-log reds on base source (A/B'd: pre-existing, module-side:
a run logged twice 1 ms apart, an overkill pulse count).
Held: game-towers 20 · hud-actions 65 (base 20/65). svelte-check 336/47, no new message.
Build green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n the real GPU - the card renders with the SCENE's look: the live background (flat or the p1 gradient texture), its fog, the environment rig cloned from the scene root (hemi + sun + catcher / ground + extra env lights, at the intensities the viewport uses) and the authored lights (spot/directional targets seated on their -Z forward, the lightHelpers rule); shadows on (PCF). The private hemi + directional pair is the fallback ONLY for a scene with no light - tone mapping = the post stack's Tone mapping curve when it has one, else NONE — what a composed desktop frame shows (the default Shaded+AO view keeps the composer running, and the renderer's own ACES never reaches a composed frame — the documented gotcha). MEASURED: ACES darkened the Waves card to 0.040 centre luminance against the live frame's 0.11-0.13; none reads 0.086. `thumb.toneMapping: 'agx'|'aces'|'neutral'|'reinhard'|'cineon'|'linear'| 'none'` overrides per def - camera: `thumb.camera`, else the def's `view` (NEW — the view the file opens on), else a 3/4 PROJECTED fit of the content box (camera markers and floor-like slabs excluded — a thin mesh covering over a quarter of the footprint), so a 30x40 m ground no longer shrinks every game piece to a speck. Camera markers are hidden in every card now (chrome, not scenery) - the authoring browser uses the per-platform ANGLE backend (vulkan on Linux, the helpers.cjs GPU_ARGS): `--use-angle=gl` fell back to SwiftShader, so every card was a software render. The run now prints its GPU line and WARNs on SwiftShader - suite author-kit: + card checks — the sky def's card shows the authored gradient (top R 242 / bottom B 242), the studio kit card reads (0.416 > 0.2), and the Waves def from the sibling modules checkout shows its OWN sunset sky (51,32,48 vs the scene's 51,32,47) and grew past the old card (2110 B vs 1690). 58 PASS - DEVIATION (QUESTIONS-30-author-kit fork 1, recommendation taken): the brief's "Waves card > 6 KB with centre luminance > 0.2" is not reachable truthfully — the Waves scene is dark (its live frames read 0.11-0.13 on the same metric) and the OLD grey-lit card read 0.198, so a > 0.2 check would pass the wrong card. The 0.2 bar is asserted on a lit controlled scene - counterfactual: HEAD's old thumbnail block restored (private lights, grey, whole-bounds) -> 3 FAIL: the gradient card (top R 80 / bottom B 80), the Waves size (1696 B) and the Waves sky (34,42,51 = the private grey) - scene content: level-blockout/towers/stars-room/untangle/waves canonical session.json SAME as the pre-lane build (only the card and the inline thumbnail sizes differ — new renderer, and the inline one now comes from a real GPU) - no src change in this commit: svelte-check 336/47 and build as at p1; held suites as at p1 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Under a pointer lock the cursor is pinned, so pointerRay() kept answering with the mouse position from the moment the lock began — a STALE ray that never moved again. Untangle's carried dot followed it, which is part of why a drag in play went nowhere. While isLocked === true, the pointer is locked and play is not in its menu substate (playPointerFree), pointerRay() now returns the CROSSHAIR ray: the view's centre, NDC (0,0) through the play camera — the ray playInteract already aims with. - Hook for 30-core-flow: a scene whose play block says `cursor: 'free'` plays with the real cursor and no lock, so its ray stays the mouse's. Read defensively through resolvePlaySettings (absent = 'locked', today's play) — nothing to merge when that field lands. - editor-modes gains section 7 (pointer lock stubbed): the mouse ray is off-centre, the locked ray matches the crosshair's direction (dot > 0.99999) and origin, and the mouse ray is back once unlocked. 45/45. - Counterfactual: the mouse ray always -> 7.2 red (dot 0.853839: the locked ray is the stale mouse ray). - svelte-check 335/47 (nothing new); vitest 214/214; build green. Held: sdk-game-seams 78/78, play-interact 46/46. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- a "THE DEF SCHEMA" block at the top of scripts/author-templates.cjs: def-level fields (kind,
identity, modules/installModules, generate/layout, objects, env, gravity/physics, post,
graphs/hud/shaders/animations, music/sounds, view, thumb {camera, sceneGroups, toneMapping},
contest), every object type with its params and defaults, the material fields (standard,
physical, toon), the object flags (physics, shadow, pick, origin, anim, particles), the custom
sky, the loader flags (--out, --only, --def, APP_URL, MODULES_REPO) and the card rules
- comment-only: node --check clean; the modules repo AUTHORING.md table is handed to the
integrator (this lane may not edit the modules repo)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aves - gamePresence.testPlay(): back to the menu (through requestResetGame, so it obeys canResetGame: the host, anyone alone, an admin under a roles plugin; anybody else is told and joins the game as it is), this peer's screen overrides cleared (the state-bound screen decides), then requestPlay() synchronously inside the click - the Start screen. - Entry points: the game chip's ▶ Test play, and a "Test play (start from the menu)" row in the play button's right-click menu (Controls playModeItems, games only). - THE ABANDON WATCH, extended rather than a second writer: every state but `menu` is a round a player can leave (paused and over used to linger forever); ALONE the reset is immediate (nobody's reload or connect dance to protect), with peers the 10 s window stands; the watch also runs on the edges (isLocked, gameState, peerPlayModes - deferred a macrotask, never writing a store inside its own subscriber), so a round entered and left inside the first second is still armed. Still host-only (sessionHost === null). - Leaving play clears THIS peer's screen overrides on a game (a pause menu opened with P no longer greets the next Play over a round that has returned to its menu). game-editor-flow 41/41 (sections 3 Test play solo: stale pause override undone, Start screen, Start -> playing, Esc -> editor with `menu` and no HUD, the right-click row; 4 two peers: A leaves while B plays -> still playing after the 10 s window, B leaves -> `menu` on both, written by the host alone). Counterfactual: the watch's resetGame() dropped -> 3 red (solo reset, the chip's state, the two-peer reset), restored -> green. Held: game-presence 61 · game-state 45 (base 61/45). Also green: play-menu-mode 32, hud-editor-wysiwyg 53, contest-starters 27, game-stars-room 36 (suites that leave play around game state). svelte-check 336/47, no new message. Build green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- scenePhysics.play.cursor: 'free' | 'locked'. Normalized at the one boundary; 'locked' is the default and is NEVER written (the play block carries `cursor` only when free), so a scene that never uses it saves byte-identically. resolvePlaySettings resolves it too, so a module can ask for it through the userData.play contract, field by field. - playCursor.js (new leaf): THE one answer to "where does play mode aim" - playAimNdc() is the cursor NDC in free-cursor play and the crosshair (0,0) otherwise; playCursorFree(); cursorClient(). Recorded in CLIENT px, converted against the canvas at read time (the W9 rule). This is the seam 30-core-modes' pointerRay reads in play (it also answers the stale mouse ray under a lock) and 30-untangle feature-detects. - PointerLockControls: no lock is requested in a free-cursor game (entry, the menu-substate re-lock, the canvas-press recapture); movement keys still walk/fly, Escape still leaves. - playInteract: tap and grab aim through playAimNdc, and the carry target follows the aim ray; in free mode only a press on the VIEWPORT canvas is a world gesture (a HUD button or any page chrome is not). PlayReticle draws nothing when the cursor itself aims. - Grid.svelte: the editor grid is not drawn while playing, on every scene and either cursor (the existing "Show helpers in Play (debug)" toggle is the one way to keep it); the mesh is named `editor-grid`. - debug hook: playCursor added to all three tails (213/213/213). Suite play-free-cursor (new) 27/27: the field omitted at default, 0 pointer-lock requests (spy on Element.prototype.requestPointerLock), no reticle, a REAL click at an OFF-centre object's pixel fires its On Click, a REAL press-drag carries a dynamic body under the cursor (792px vs cursor 792px), page chrome is not a world tap, the grid hidden in play in both cursor modes, a locked game still asks for the lock (the spy works). Counterfactual: lock requested anyway + grid always drawn + aim pinned to the crosshair -> 7 red (5 lock requests; both grid checks; click, grab, carry position), restored -> green. Held: play-interact 46 · game-towers 20 (base 46/20). Also green: play-menu-mode 32, touch-play 39, scene-physics-state 37. svelte-check 336/47, no new message. Build green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- gameStorage.js (new leaf, safeStorage only): THE KEY CONTRACT, byte for byte -
`tp:mod:<moduleId>:<key>` for a module, `tp:scene:<scene name or 'untitled'>:<key>` for
the flow nodes; values are JSON text; a missing or unparseable entry hands back the
fallback; a parsed-value cache kept current by every write through the leaf.
- moduleSDK makeApi: `api.storage.{get(key, fallback), set(key, value) -> bool, remove,
keys, clear, bytes}` - 256 KB per module measured as the JSON text + key of every entry
(a set over it returns false and ONE toast per module per session), LOCAL (never
replicated, never in a scene file, never undone), deliberately NOT journalled: storage
outlives the module (a reinstall keeps progress; clear() is the module's own reset).
A throwing localStorage falls back to memory for the session through safeStorage.
- Flow nodes, palette group Game, in BOTH registries (nodeCatalog + CORE_NODE_TYPES) with
manual lines: Store Value (trigger + value; key, mode set|max|min|add) acts on the stamp
EDGE inside the actionSeenAt family and sends NOTHING - its card says "Saved on this
device only"; Stored Value (key, output number|text). flowSockets types both.
- hudActions: "Save best score" on a button - Store Value (best, max) fed by the `score`
variable; the press role gains an additive `via` (absent on every other press action).
- nodeDocs: a manual line for `onhit`, which 24-A shipped without one (flow-node-docs had
been red on base for it) - one line outside this lane's nodes.
- debug hook: gameStorage added to all three tails.
tests/unit/gameStorage.test.js (8, no browser) · module-storage (new) 36/36: the api
round-trips, the raw key, two modules apart, the cap, a throwing localStorage (no error,
memory fallback, nothing reached storage), Counter -> Store Value (max) keeping the best
under tp:scene:Arcade:best with nothing on the wire and nothing on the second peer, a
reload keeping both halves, another scene's namespace not seeing it, and "Save best score"
built and pressed.
Counterfactual: the module namespace dropped from the key -> 5 red (the key contract, two
modules colliding 7/7, keys and clear crossing, the reload), restored -> green.
Held: hud-actions 65 (base 65). flow-node-docs green (was red on base: onhit),
flow-unknown-node 24. palette-groups 10/11 with its one red ("Music" has no card accent)
identical on base - not this lane. vitest 204 (+8). svelte-check 336/47, no new message.
Build green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t height
- Towers def (scripts/author-templates.cjs, the DEFS region + towersGraph): a sky gradient
with a far fog and a solid ground disc (daylight, exposure 1.05); a 2 m floor tile from a
shader graph (uv -> split -> GLSL expression -> Surface albedo, multiplying the colour);
chamfered walls with a glowing trim; wooden crates (physical + sheen + thin clearcoat,
bevelSegments 1 to keep the file small); the pad under a clearcoat with a bright rim and a
blue light; the height markers are thin glowing TORUS ring sensors (select-through) beside
a marked pole instead of stacked translucent squares; faceted stars that turn and breathe
(the Turntable + Pulse presets folded into ONE `Star glow` clip, played on round start);
`view` + `thumb.camera`; collectible module version 1.1.1; Play again beside Menu on
Round over.
- BEST HEIGHT (Store Value / Stored Value, 30-core-flow's nodes): `towers-best` (max) is
written on every ring crossing and shown on the menu ("Your best tower: N m");
`towers-last` holds THIS round, zeroed by its own `set` node on round start. Never on
`over`: a perRound latch reads un-set the instant the round ends, which is also why the
old Round-over line always read "0 m". `max`, not `set`, for the round's value too: a
crate jittering in a ring re-fires the crossing after the round ended, when the height
reads 0. Game-specific keys (a Games-tab load leaves the scene unnamed).
- core, outside the defs (minimal, named in the handover): HudElement.svelte — a centred or
right-aligned SINGLE-LINE HUD text rendered flush left, in every game. The text box is a
flex row, so its line is an anonymous flex item shrunk to the words and text-align had
nothing to align; the row now takes justify-content from the same `align`. Scoped to
text/timer (the list is a flex column).
- game-towers: TOWERS_TPSCENE env (a lane's staged build); 24 -> 36 objects (the trims, the
pad rim + its light, the pole + 4 marks, the card camera); new checks for the look as
data, no warning toast on load, the menu's best, centred texts, the star clips playing,
centre luminance >= 0.25 in play, the saved best/last, Round over from storage, a
re-fired crossing on the over screen, Play again zeroing the round, Quit showing the best.
The pad is cleared before the round ends: a crate standing through a ring re-crosses it
in the next round — measured, that was the base's red on "Restart cleared the height
latches" (19/20 at base).
Counterfactuals (each broke, went red, restored):
- storelast mode 'set' -> "a crossing re-fired on the Round-over screen leaves this round's
height alone" red ({"best":3,"last":0})
- HudElement back to boxStyle -> "centred HUD texts are centred on their box" red
(offsets TOWERS -124.5, Your best -133 px)
- no `shaders` in the def -> "the arena floor carries its tile shader graph" red
- no `animations` -> "every star carries the Turntable + Pulse clip" and "Start sets all
three stars turning" red
Suites (staged scene): game-towers 40/40 (base 19/20), game-editor-flow 41/41 (base 41).
svelte-check 336 errors / 47 warnings, list identical to base; vitest 204/204; build green.
Staged: cloud-lane-30-staging/towers (scene 48.8 KB, was 13.2 KB; thumb 6.5 KB). Centre
luminance in play 0.547 (before 0.593, both at the probe viewport).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…reen
- Stars Room def (the DEFS region + starObjects/starsGraph): a custom sky (a deep-blue
gradient, a cool hemisphere, a soft key light; exposure 0.55 -> 1.1), a starfield from ONE
continuous emitter flung to a ~20 m shell (speed / drag) that fades in only once it is past
the glass, glass walls (clearcoat, 10% opacity, select-through) hung in a glowing frame, a
glossy deck with a light ring, crystal stars (physical icosahedra, flat-shaded, emissive,
SPHERE colliders), sheen planets, `view` + `thumb.camera`.
- THE EMITTER-CAP TOAST ON LOAD, fixed without touching the cap: a burst node per star was
24 of the runtime's 8 emitters. Now ONE pooled burst on a `Burst anchor` (parked under the
deck, since a burst emitter auto-fires once when it is built) that a Script node moves onto
the star just hit — the star's number is sum(i * hit_i) (each On Hit reads 1 for its 0.3 s
pulse), and the same sum is the burst's trigger. 2 emitters on load (was 24).
- A START SCREEN: Start round or Free play. Free play is a game STATE of its own ('free' —
setGameState keeps an unknown state), so its screen is state-bound and the P menu's Resume
(a `hide`) falls back to it; an override would have fallen back to the start screen.
The round is two minutes (Game Time remaining; time up -> over "Time's up!").
- ROUND OVER FROM STORAGE: every perRound latch reads un-lit the instant the round ends, so
the over screen reads stored values — `stars-best` and `stars-last` (max, written on every
hit; max so a star knocked on the over screen cannot wipe them) and `stars-time` (max of a
value that is 0 until the 24th star lights), each round zeroing its own two by `set` nodes.
The title and the line pick their FORMAT through a Select (the Select-passes-a-string
trick the lit colour already used): EVERY STAR LIT / "Every star lit in Ns" on a win,
TIME'S UP / "Lit N / 24 in time" otherwise; the best round shows on the start screen and
Round over, which also gets the touch leaderboard, Play again and Menu.
- game-stars-room: 36 -> 50 objects; starts on the start screen, Free play enters `free`;
new checks for the glass, the crystals, the starfield/exposure, the emitter budget and the
load toast, the pooled burst standing on the knocked star, centre luminance >= 0.25 in
play, the stored round, and the best on the start screen. "Back to free play" -> "Menu".
Counterfactuals (one broken build, 4 red, restored):
- the Script edge removed -> "the pooled burst moved onto the knocked star" red (anchor
still parked at 0,-3,0)
- a burst node per star again -> "the emitters fit the runtime's cap of 8" (25 + 1) and
"no emitter-cap toast on load" red (the toast is back)
- the over title's Select unwired -> "the over screen names the time and the best, from
storage" red
Suites (staged scene): game-stars-room 47/47 (base 36/36). No src change in this phase
(svelte-check/vitest/build as at p0: 336/47 identical, 204, green).
Staged: cloud-lane-30-staging/stars-room (scene 49.7 KB, was 34.7 KB; thumb 9.4 KB).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Jam Room def (JAM_DEF + jamGraph, replacing the inline DEFS entry; same place in DEFS, so the index order is unchanged): a small studio around the same twelve cabled devices and layout — a warm wooden floor, two rugs, plastered walls and a ceiling (select-through; the ceiling casts no shadow and glows faintly, with two light discs), five fabric acoustic panels, two floor lamps with their lights, a spot on the beat lab, an amp, a plant, a stool; a warm custom sky, the post floor, `view` inside the room (Play starts from the editor camera's spot) + `thumb.camera`. - `play.cursor: 'free'` (30-core-flow's P3): you play the instruments by pointing at them — no pointer lock, the real cursor clicks keys, pads and the drum grid. No simulation. - THE SHELL: a start screen (Start jam, this device's best tempo), a three-second COUNT-IN (its own screen, the number derived from Game Time), then the goal: keep the band playing for EIGHT BARS. Round over is "Session complete — 8 bars at N BPM", with Play again and Menu; the P menu has Resume / Restart / Quit. - WHAT THE GOAL IS BUILT ON: core's Transport node (musicClock's shared transport as a flow value — beat, bpm, playing, identical on every peer). The music modules publish nothing to the flow (no notes-played count, no transport control), so the round cannot START the transport: the HUD says "Press ▶ on the Transport" until it runs, then "Keep the band going · N BPM". The beat the count-in ended on is kept with Set Variable (a replicated sample-and-hold), so bars count from THIS round; a transport restarted mid-round counts from its own zero (Select on beat < start). The goal gates on bars >= 8 AND the transport playing AND the round on and counted in, through All Players. - THE SCORE is the tempo you finished at (the Transport's BPM −/+), `jam-best-bpm` (Store Value max) shown on the start screen and Round over; `jam-last-bpm` for the line. - NEW suite game-jam-room (30): the twelve devices + the studio as data, the start screen, the free cursor in Play, the count-in, the goal waiting for the transport, the bars counting at 240 BPM, Session complete + the saved best, Play again counting from its own start, Quit showing the best, and centre luminance >= 0.25 on the goal screen (measured AFTER Start: the start screen's panel covers the centre, so it would measure the menu). Loads JAM_ROOM_TPSCENE or the sibling scenes checkout; the real music-lab/music-fx zips. Counterfactuals (one broken build, 7 red, restored): - the count-in's Set Variable trigger removed -> "the count-in kept the beat it ended on" and "the new round counts from its own start" red (bars 18.9 on Play again) - no `play.cursor` -> "a free-cursor game" and "in Play the cursor stays free" red - the best-tempo Store Value untriggered -> "Session complete names the tempo and the best", "the best tempo is saved", "the start screen shows the saved best" red Suites (staged scene): game-jam-room 30/30 (new; luminance 0.425). No src change in this phase (as at p0: svelte-check 336/47 identical, vitest 204, build green). Staged: cloud-lane-30-staging/jam-room (scene 67.4 KB, was 35.6 KB; thumb 8.4 KB). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/lib/playInteract.js
…tcheted to 335/47 - moduleSDK's crosshairAims read `cursor` off resolvePlaySettings defensively (30-core-modes P4 was written before 30-core-flow's P3 landed); it now asks `playCursorSetting()`, the leaf playInteract, PointerLockControls and PlayReticle already ask, so the four cannot disagree about where a free-cursor player aims - play-free-cursor section 4 gains the seam's own check: in a free-cursor game api.pointerRay() is the CURSOR's ray (dot 1.00000 to the clicked target) and not the view axis (dot 0.95) - counterfactual: crosshairAims returning true whenever playing -> both new checks red (dot 0.952 / 1.000); restored -> play-free-cursor 29/0 - check-baseline.json 336 -> 335: the union removes one pre-existing Scene implicit-any (30-core-modes); full list diffed against origin/release/next, nothing new - union gates: build green, vitest 222/222, App.svelte debug tails 214/214/214 in order Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…k the camera at NaN - api.editorMode() -> 'edit' | 'interact' (modules DEVX #35, LOCAL, read-only). Untangle 2.1.0 feature-detects it to stand its own window-capture drag down in Edit; neither core lane had built it, so on the union an Edit click on the board was eaten by the module and never selected its proxy (object-list-modules 5.2 red with the new zip) - objectActions.flyTo: a rAF timestamp is the frame's START and can precede the call, so a 0 ms fly divided a zero/negative elapsed by 0 and left the camera at NaN/-Infinity for a frame (the AudioParam "non-finite" throw 30-mod-audit reported). t is clamped to [0, 1] and a 0 ms fly is t = 1 - object-list-modules 5.0: flyTo(..., 0) on a frame stamped 20 ms before the call lands at the target, finite - counterfactuals: without api.editorMode -> 5.2 red ("nothing"); the old flyTo line -> 5.0 red (NaN,NaN,NaN) and the section dies in elementFromPoint; restored -> object-list-modules 25/0 - also found on the way (fixed in the modules repo): untangle's rim TubeGeometry was built from a 2D Shape, every vertex NaN, so the board's bounds were NaN and "Frame it" flew the camera into NaN (2.3/2.4 red with the new zip) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es on towers and stars room - author-kit: the Waves card check read a NAMED preset's background, and 30-visuals-mod gave Waves a custom dusk gradient — it compares the card's corner with the def's own sky (a gradient's top) now, and asserts the 0.2 luminance bar QUESTIONS-30-author-kit fork 1 deferred until Waves had its own readable look (card 0.255) - game-untangle: 30-untangle's expectations diff (level 1 open, the 5..16 dot curve, the custom sky, a nudge not a far move, the SOLVED screen + Next in lockstep) + the "Crossings: N" readout - game-football: 42 objects (the Arena group); the score is read from the scoreboard's two HUD numbers (fb-red-score / fb-blue-score) instead of the removed "RED n — n BLUE" line - game-dungeon-realms: 8 objects (+ Ground, Card camera), the custom sky, the Start menu is a core HUD screen (clicks by label) - game-waves: 11 objects (the Arena group), the custom sky - Towers (orchestrator review): the fog closes in past the arena (near 16, far 75) so the ground fades into the sky's haze instead of ending in an olive band; the round clock reads "Time: 172s". Stars Room: the touches board is sized to four rows (180x96, limit 4), not an eight-row box holding "Me — 0". Card modules: collectible 1.1.2, music-lab 0.2.1 - MODULES.md: click-handler modes, registerListedGroup, free-cursor games, api.storage - union scenes (staging/union), SERIAL under the lock, PEER_CONFIG: game-towers 40/0, game-stars-room 47/0, game-jam-room 30/0, music-template 11/0, game-football 136/0, game-dungeon-realms 65/0, game-untangle 50/0, game-waves 94/1 (1.2 compares the card with the LIVE modules index, which lists waves 1.0.0 until the modules release); author-kit 59/0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ES.md - CHANGELOG ## 1.17.0: the pointer-capture fix first, Edit/Interact, click-through, module content, Test play, the round reset, free-cursor games, storage, the seven games, the author kit, the SDK - CLAUDE.md: three roadmap-30 architecture entries (editor modes + select-through + module content; games start on Start + playCursor + gameStorage + console hygiene; the author kit), eleven gotchas from the lanes and the integrate, the R30 Module SDK additions, a status entry - MODULES.md: api.editorMode() Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…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>
…e load that ends the run
- packRefs.js (new leaf): a piece placed from a pack carries userData.packRef
{pack, item, path, hash, kids}; a save (buildSessionPayload / buildSelectionPayload),
a session-load broadcast, a late joiner's sync and a live placement write a PRISTINE
piece as a hollow STUB; the loader and every peer refill it from PACKS_BASE/<path>,
parsed once per piece, textures shared across every copy (and across pieces whose
images are byte-identical). Pristine is MEASURED: a fingerprint of the descendants
(structure, local matrices, geometry checksums, material params, an 8x8 sample of every
texture) against the parsed file; an edited piece saves in full as before. An
unreachable pack keeps the stub and saves it back (never silently dropped).
- Measured: one kit wall was 7,859,921 B as a toJSON element (PNG data URLs, one
toJSON per top-level child) and is 555 B as a stub; a level of ~180 pieces is ~25 KB.
- Placement stamps the reference (Explorer double-click + viewport drop -> importFile
opts.packRef, before addImported replicates); the wire sends the stub (sendObjects
null-peer branch + sendObject; countObjects does not count refilled children).
- scenePhysics play.spawn {pos, yaw} (OMITTED at default, so saves are byte-identical);
PointerLockControls puts the player there on entering play (no dungeon);
Configure Scene > Physics > Play mode > Spawn point: Set to the view's focus / Clear.
- applySession stops a running LOCAL simulation before it wipes the scene (quietly, the
yielded way): the old world kept bodies for the wiped objects and none for the new
ones, and sim-on-play skipped the start because a sim was "already running" — so the
second level played in a session was walked THROUGH (levels-run2.log).
- App.svelte debug hook tails 215/215/215 (+packRefs); startPackRefs beside
startShadowDefaults.
Suites: pack-refs (NEW) 30/30 (single page + a late joiner + a live placement to a
second peer + a real kit wall when the build serves the kit). Unit scenePhysicsSpawn
3/3; vitest 233/233 (base 222).
Counterfactuals: sameFingerprint -> always true: 3 red (colour, in-place vertex, moved
child all saved as stubs); spawnOf spread removed: unit red; the scene-load stop removed:
the second level's walls/stairs walked through (level suite, 3 red).
svelte-check 335/47 = base (list identical); build green (server stopped).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e kits - scripts/level-templates.cjs: Castle Courtyard (curtain walls, a gate between pillars, two three-storey round towers, a rampart terrace reached by stairs through an archway, a paved avenue to a well, a market stall, trees; a lit afternoon sky), Forest Clearing (a stone path, a footbridge over a pond neck kitbashed with sandstone ramps, a 4 x 4 m plaster hut from the architecture kit, a walkable mound, a cliff, two rings of trees; late-morning sky, fog that closes in past the tree line), Tavern Interior (a double-height hall with a balcony, a kitchen behind a partition doorway with a loft above it, oak stairs, a bar, furniture; warm lamps + a warm hemisphere). Every level: a spawn point, sim-on-play, ONE walk-mode Character Controller node, dynamic props, and colliders that match the pieces — custom compound shapes for the archway/doorways (jambs + lintel), stairs and ramps (wedges), trees (trunks, MEASURED off-centre), the footbridge (deck + rails) and the mound (a measured hull that runs out past its 0.37 m lip); grass, flowers, rugs, water are sensors. - author-templates.cjs: a `kit` object type (a pack item as a REFERENCE stub, resolved through the pack's own default.json on PACKS_BASE, refilled and awaited before the card renders — the run refuses a level whose pack is unreachable), `seed: false` (kit levels stay out of the offline seed), and a kit level's card CLONES the scene instead of the toJSON round trip (which is the 7.9 MB-per-wall cost). Every other def is unchanged. - Measurements that shaped the layouts: the Arch opening is +/-0.408 m (the collider's jambs sit 6 cm inside the stone: a grazing walk stopped 1 run in 2); the Footbridge walks along its LOCAL Z with a flat 0.85 m deck and no approach. Suites: level-templates (NEW) ALL PASS 42 (all three levels: loads from the Templates modal, every piece refilled, spawn + sim-on-play, centre luminance castle 0.47 / forest 0.36 / tavern 0.33 >= 0.25, no black quarter, walls hold, archway/stairs/rampart, footbridge, hut doorway, balcony stairs, kitchen doorway pass); castle x2 green after the arch fix. Unit levelTemplates 8/8. Counterfactuals: CF=arch (the arch's custom collider removed): the archway walk red; CF=spawn (the spawn cleared): the spawn check red. svelte-check 335/47 = base; build green. Staged content: /home/deck/.code/theprototype-app/cloud-lane-30-staging/30c-levels. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/App.svelte # src/components/Scene.svelte # src/lib/moduleSDK.js # src/lib/vrControls.js
# Conflicts: # src/lib/moduleSDK.js
- play.spawn: both 30b-vr-modes and 30c-level-design added it; the union keeps the
contract shape {position, yaw[, vrOnly]} (locomotionPolicy.normalizeSpawn) and still
reads 30c's first shape {pos, yaw}; the level lane's own PLC spawn branch is dropped
(playSpawn.spawnDesktopPlayer already puts the desktop camera on it)
- Inspector spawn row, scripts/level-templates.cjs and the level suites write/read position
- App.svelte debug tails: packRefs appended after playSpawn, 218/218/218
…e peer
- 27-A's wire validator took a rotation of 3 or 4 FINITE NUMBERS only; three 0.185's
Euler.toArray() is [x, y, z, 'XYZ'] - the shape the gizmo drag, the Explorer drop, the
Inspector, Align to ground and ~a dozen senders use - so every such move was refused on
arrival (invalid:move) and the peer kept the old pose. Found by the 30c pack lanes (a
snapped kit drag never reached peer B, 29 -> 42 invalid:move).
- isQuatOrEuler also accepts three numbers + an XYZ-permutation order string (the appliers
read [0..2] only); a NaN or a junk order stays refused.
- NEW suite move-wire-shape (two peers, the REAL alignToGround sender, a pass-through spy
proving the toArray shape left A): 7/7.
- vitest wireValidate +4 expectations.
- Counterfactual: isEulerWithOrder removed from isQuatOrEuler -> move-wire-shape 2 red
("B follows a move whose rotation is [x, y, z, order]", invalid:move 1 -> 2).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A piece placed from a pack was kept in history as its full toJSON: a 0.8 MB architecture-kit wall is ~7.9 MB that way (PNG data URLs), over history's 5 MB snapshot limit, so every placement toasted "Object is too large for undo history" and building a level from the kits had no undo (orchestrator review note, after-30c/30c-pack-arch). - history.registerReferenceSnapshot(fn): captureObjectSnapshot asks the provider first and keeps what it returns; packRefs registers "a PRISTINE piece -> its stub" from startPackRefs through a DYNAMIC import (no static edge from history's subtree to the Explorer). Restore needs nothing new: the stub added back is refilled by the scene's own stub scan, with the recorded child uuids. An edited piece still records in full. - NEW suite pack-undo: ten pieces through the real drop path, each entry a ~620 B stub, undo x10 / redo x10 twice, same uuids + triangle counts; with KIT_URL a real WallStone (jsDelivr, the arch PR branch): 620 B entry, no toast, undo + redo. 15/15. - Held: pack-refs 28/28. - Counterfactual: provider ignored (reference = null) -> pack-undo 4 red (entries 491456 B per Duck; the wall: no entry, the toast, undo cannot remove it). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…keep their camera
- game-waves REWRITTEN for Waves 2.x (the 29-F suite asserted 1.x: 3 waves, knock kills):
the card, the file (17 objects, health + waves 2.x, the custom sky, the play block with the
30b spawn + no teleport/fly), B over the handshake, Play -> the shell + the HUD, THE REPORT
(the enemies walk; B places a walker where A does), a kill through the module's own
engine.hit, a late joiner; A steps out of Play before C joins (a peer cannot approve from
Play). The game itself is the modules flight waves-shooter's (118). 40/0.
- game-football 6.9: the over screen says Rematch + Menu (30b), 'New match' -> 'Menu'. 136/0.
- vr-sweep 10.4/10.5: one held sweep across three mixer MUTE buttons flips each channel, one
over two pedals' FOOTSWITCHES stomps each (music-fx 0.2.0 on the union Jam Room) — the
Quest ask "on the mixer enable/disable". 25/0. Counterfactual: music-fx 0.1.0 has no
handler on either part (its flight section 12 goes 10 red with the handler removed).
- object-sync launches on the GPU backend: on the union a third SOFTWARE-GL page boots past
setupPage's 30 s (measured 27-40 s vs 7 s on round 1) while the boot programs (16 = 16),
draw calls (14/frame = 14) and boot script (0.13 s = 0.13 s) are identical; on the GPU all
three boot in < 2 s. 12/0.
- level-templates: LIVE=1 runs it against a build's own feed + pack CDN (the preview proof).
- scripts/level-templates.cjs: the camera views are `view: {pos}` again (the merge's spawn
rename caught them and the authoring died in the page); author-templates: the Jam Room row
names music-fx 0.2.0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tacle
- The KCC capsule is world.createCollider() with no body: a FIXED, solid collider that
stays in the world where the player last was and is teleported with the camera every
frame. 21-E6 only made one in walk mode; 30b-vr-modes' collideRigStep made EVERY desktop
Play session with a running simulation create and drive it — and on the union a crate in
the health flight was flung ~8 m (x 0.78 -> -8.28) between Play + sim start and the first
knock, so the knock's closing speed read 9.3 m/s instead of 6 and killed it (3 points, not
2). It also stood as an invisible pillar for every dynamic body.
- `.setSolverGroups(0)`: no contact forces with anything; computeColliderMovement still
sweeps the shape, so walls and steps stop the walker exactly as before.
- Proof: modules flight module-health (GPU) 99/0; counterfactual (the solver groups
removed) -> 3 red ("A: Crate2 took 2 points per knock -> 1/3", B, C). Base 2ea91c3: the
crate sits still at (0.78, 0.5, 1.24) when the knock comes (probe).
- Held: vr-walk-interact 20/0, char-controller 55/0, level-templates 42/0 (walls, arch,
stairs), vr-mode-spawn 24/0, game-towers 52/0, physics-ground-bounds 31/0, throw-peer
28/0, play-interact 46/0 (x2; one 6.1 flake before).
- check-baseline.json: 335 -> 333/47 (the merged branches removed two; the message list is
identical to the merge's).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- CHANGELOG 1.17.0 gains three sections: Games in a headset (Interact in VR, left Y, walking with walls, spawn, helpers in both eyes / none in Interact, the VR game menu, hold-and-sweep, module content follows the world), Games that sound and feel like games (sounds, music, banners, bursts, haptics, the six Game nodes, per-game lines incl. Music FX 0.2.0), Kits and levels (four packs, three General levels, kit-piece undo, the move fix). - CLAUDE.md: architecture entries for the Quest round (vrGrip / locomotionPolicy / playSpawn / moduleWorld / helperLayer, the gameKit leaves, gameFeelActions) and the Meshy round (packRefs + history.registerReferenceSnapshot, the packs, the levels); gotchas (HELPER_LAYER 1/2 = the eyes, Euler.toArray on the wire, a blanket shape rename, a body-less rapier collider, the third software-GL page, additive bursts, round-end banners, no DOM HUD in a headset, the 30c known-not-fixed pair); R30b SDK additions; the status entry. - MODULES.md: api.setSpawn / api.respawnPlayer. 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.
Roadmap 30 "Games that look finished", integrated: the four core lane PRs merged in order (#240 author-kit, #241 game-flow, #239 editor-modes, #242 core-games) plus the integrate fixes and the 1.17.0 changelog.
HELD FOR THE USER'S "release" — a Cloudflare Pages preview of this branch is live for the on-device check: https://preview-1-17.theprototype.pages.dev (scenes branch
preview-1-17, modulesdev).Integrate fixes
playCursorleaf (the two lanes' seam);api.editorMode()(untangle's Edit stand-down had nothing to read, modules DEVX [feat] roadmap 13 batch C - physics usability + car playability #35);flyTo(…, 0)could park the camera at NaN for a frameBattery (serial, the union, PEER_CONFIG)
lanes' suites + held suites (20) green: pointer-capture-play 12, editor-modes 45, select-through 17, object-list-modules 25, console-hygiene 13, game-editor-flow 41, play-free-cursor 29, module-storage 36, author-kit 59, selection-extras 19, play-interact 46, sdk-game-seams 78, module-toolbox 46, game-state 45, game-presence 61, hud-actions 65, templates-modal 45, template-modules 27, music-template 11, controls-roster 108.
Seven games on the union-authored scenes: towers 40, stars-room 47, jam-room 30, football 136, dungeon-realms 65, untangle 50, waves 94/1 (1.2 compares the card with the live modules index — resolves at the modules release).
Preview proof pass (https://preview-1-17.theprototype.pages.dev, real Games tab, gallery installs): 100/0 — every game loads, chip not menu in the editor, Test play → Start → play → Esc → menu, untangle 2D + globe drags, progress survives a reload, Edit/Interact, no setPointerCapture / PCFSoftShadowMap / page errors. 1.16.0 production loads all seven new scenes + modules with no page error.
Owed on device
The seven games' look on a real display and in the Quest browser; untangle drag with a Quest controller in 2D mode and the globe; the Edit/Interact toggle and the I key; Test play with two people; a best score surviving an Oculus Browser restart; the Football glass cost on Quest.
🤖 Generated with Claude Code