Skip to content

[feat] 31 Stars Room: bounded teleport, touch-only, clap-a-star; Jam Room: fly + world grab - #250

Merged
AlexZ005 merged 13 commits into
feat/1.18from
feat/31-stars-jam
Oct 1, 2026
Merged

AlexZ005 merged 13 commits into
feat/1.18from
feat/31-stars-jam

Conversation

@AlexZ005

@AlexZ005 AlexZ005 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Roadmap 31, lane 31-stars-jam — items S1, S2, S3, S4, J1 of the 2026-10-01 Quest 3 feedback.

What changes

Stars Room

  • S1 teleport is allowed, bounded to the inside of the glass (play.locomotion.teleport + play.bounds). Takes effect with 31-vr-core's bounded teleport (K1).
  • S2 a game setting "Point to move stars" (default on). Off: the VR grip ray and the desktop carry take nothing; a hand touching a star still holds it and knocks are unchanged. Edit is never affected.
  • S3 a game setting "Make stars with a clap" (default on): both hands within ~10 cm for 0.25 s make a star between them — sparkle + sparks burst and a portal sound at the point, a buzz in both hands; one clap per second, cap 40 (oldest recycled).
  • S4 the clapped star is a real one: a live dynamic body, pushable, it flies like the others and its hits count as your touches.

Jam Room

  • J1 play.locomotion {fly, worldGrab, teleport} + play.bounds inside the studio. Flags take effect with 31-vr-core (K1); normalizeLocomotion now keeps worldGrab so the scene saves it.

Core (generic)

  • New flow nodes: On Clap (pulse + point + byMe, enabled input), Point Grab (enabled), Game Setting (registers a row in 31-game-shell's gameSettings.js, outputs this device's value). clapGesture.js (pure) + clap.js (runtime on the knock's hand seam) + pointGrab.js (leaf).
  • nodetrigger may carry an optional at (validated; absent = every older pulse), so a replicated clap carries its point.
  • Spawn takes a position input; Effect Burst / Game Sound accept a place for at.
  • A spawned copy answers to its template's Object Selector for events (transientObjects.spawnedFromOf, derived on both sides — nothing new on the wire).
  • Contains a merge of origin/feat/31-game-shell @40e07fb (its gameSettings.js leaf) — agreed with that lane.

Verification

  • NEW stars-clap 33/33; game-stars-room 70/70 (+13); game-jam-room 44/44 (+5, incl. a 1.5× world where a held sweep plays three keys and three drum steps); music-template 11/11; author-kit 59/59 — on the re-authored scenes.
  • vitest 300 green; svelte-check 336/47, message list identical to base; check:storage clean; npm run build green.
  • Counterfactuals (one run, six guards reverted): 5 red as expected (8.4, 8.6, 2.2, 2.8, 6.3); the inner enabled check in fireClap stayed green because clapWanted() refuses first — it is a second layer, said so in the commit.
  • Staged scenes: cloud-lane-30-staging/31-stars-jam/games/{stars-room,jam-room}. Shots: lanes-30/after-31/31-stars-jam/.

OWED on device (Quest)

Clap reliability / false triggers (10 cm vs the brief's 8 cm — QUESTIONS-31-stars-jam #1); bounded teleport in both rooms and world scale in the Jam Room once 31-vr-core is merged; the settings rows in the game-shell's VR Settings page.

Never merge from here — the integrator merges.

🤖 Generated with Claude Code

AlexZ005 and others added 10 commits October 1, 2026 00:23
- src/lib/gameSettings.js: the ONE settings store every game shares - the core rows
  (music/volume, sfx/volume, haptics, show FPS, VR turning + snap angle, comfort
  vignette, quality preset) plus game rows declared by api.game.addSetting or a flow
  node (registerGameSetting(row, owner) -> off). LOCAL per device via safeStorage,
  per game id: core rows in tp:game:<id>:shell, a game row in tp:game:<id>:<row id>.
- the game id = slug of the last scene FILE opened (sessions.applySession notes the
  payload name, so a Games-tab load of Towers is 'towers'), else the saved scene name,
  else 'untitled'.
- live resolvers core reads: sfxLevel/musicLevel/hapticsAllowed/resolveTurning.
- vitest gameSettings (14): per-game isolation (SFX off in Towers, on in Waves), row
  coercion, reserved ids, a game row surviving re-registration.
  Counterfactual: drop the gid from shellKey -> "SFX off in game A does not reach
  game B" goes red.

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

The Quest report: "In VR, when I open radial menu, i do not see highlights when i move
joystick, fix navigation." The input was never the problem - vrControls set vrHovered to
the right sector every frame (measured through fakeXR: 8/8 directions). VRMenu.svelte is a
LEGACY-mode component, where `color={sectorColor(s.entry)}` compiles to
untrack(() => sectorColor(...)) depending on `s` alone, so the $vrHovered read INSIDE the
helper registered nothing: no sector mesh ever repainted, under the stick or the ray (the
hub, which reads the store inline, did).

- VRMenu: sectorColor/labelColor take the hovered id as a PARAMETER ($vrHovered passed in
  the template), so the colour depends on it.
- vrControls: a stick-lit sector is remembered per frame (radialStickSelection); Scene's
  trigger picks it when its own ray misses the ring (before, only a ray hit could be
  triggered and a stick-lit sector fell through to an object select). Stick-click and the
  hold-mode release are unchanged.
- each stick is read from its OWN inputSource found by handedness (the pointer stick used
  sources[<controller slot>], the 194/210 divergence after a hands<->controllers swap);
  a greyed (disabled) sector is never lit by the stick.
- one haptic tick per sector change stays behind hapticPulse's C4 gate (Interact ticks,
  Edit is silent - the user's earlier rule; recorded in QUESTIONS).

Suite vr-radial-stick (12, fakeXR through the real per-frame path + Scene's select handler):
8 directions light exactly their sector MESH, centred = nothing, 3 ticks for 3 changes,
trigger picks the stick-lit Add, stick-click picks Scene, the other hand's stick, the ray
lights what it hits, a greyed sector stays dark.
Counterfactuals (one batch, all restored): colour helper without the hovered argument ->
mesh-repaint, other-hand, ray and both greyed checks red; trigger without
radialStickSelection -> "the trigger opens the stick-lit sector" red; disabled guard
removed -> "a greyed sector is not lit" red. 6 red in total.
svelte-check 333/47 (base 333/47, identical list modulo line shifts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, a filled hover, module menus as beam ends

The Quest report: "i want to be able to see from controller ray where i point in menu (now
its hard to navigate)". The beam and reticle existed; four things made them hard to use.

- vrControls: the beam is NORMAL-blended (the additive glow vanished against a bright sky
  - the documented additive-burst trap) and a touch wider; the reticle ring gains a solid
  DOT at the exact hit point (the ring left the point itself empty, which is where a small
  button is).
- vrControls.beamTarget: a module's INTERACTIVE scene-root content ends the beam too (it
  lives under module-world-root since 30b P5, outside objectsGroup, so the laser passed
  through a module's own menu/board with no dot). The name list is cached on the registry
  revision - no sort or allocation per frame.
- vrGamePanel: the button under the laser is FILLED (+ a thicker ring), the footer buttons
  too - a 4 px outline on a board 1.2 m away was too faint to tell which one the ray was on.
- vrGameInput.interactiveGroupOf: accept a group under module-world-root. The old
  `node.parent === scene` test matched nothing after 30b P5 re-homed module groups, so a
  module's buttons never resolved to a click target for the laser hover or the sweep.

Suite vr-laser-panels (23, fakeXR + the REAL Towers, Stars Room and Jam Room templates
from scenes preview-1-17 via SCENES_DIR): per game the beam ends on the menu button
(length = distance), reticle AND dot on its centre (< 1 cm), the board's hover id is that
button and a pixel inside it changes (filled); the beam is normal-blended; an inline
module's interactive group is re-homed, ends the beam at 1.49 m and resolves to its
group; the reticle sits on a radial sector.
Counterfactuals (one batch, restored): no dot -> 3 red; no fill -> 3 red; additive beam ->
1 red; no module roots in beamTarget -> beam 2.90 m red; parent === scene -> group null
red. 9 red total.
svelte-check 333/47 (identical list modulo line shifts).

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

Roadmap 31 (Stars Room S2/S3/S4), the core half:
- clapGesture.js (pure leaf): two hands within 10 cm, held 0.25 s, re-armed only by
  parting past 20 cm, one per second. vitest (9).
- clap.js: the runtime, fed by the knock's hand seam from Scene (never imports
  vrControls), hands carried into the objects group's frame; gated on the game (Interact
  or Play) and on a listening On Clap node. feedClap() is the test hook.
- On Clap node (Game): pulse + `point` + `byMe` outputs, an `enabled` input, `who`
  anyone|me. `anyone` replicates as the ordinary nodetrigger, which now CARRIES the point
  (optional `at`, validated in wireValidate; absent = every pre-31 pulse), so the
  initiator's spawner and every peer's burst agree on where. `me` stays local (a buzz).
- Spawn gains a `position` input (a place in the objects group's frame; wins over the
  offset); Effect Burst / Game Sound accept a place for `at`.
- Point Grab node + pointGrab.js leaf: while `enabled` reads off, the VR grip RAY
  (gripTargetOf) and the desktop carry (crosshair and Interact cursor) take nothing; a hand
  INSIDE an object still holds it, the knock is untouched, Edit is never affected.
- Game Setting node: declares a row through 31-game-shell's gameSettings.js
  (registerGameSetting, owner node:<id>, dropped with the node) and outputs this device's
  value.
- a spawned transient copy remembers its template (transientObjects.spawnedFromOf, derived
  on both sides of `duplicate`, nothing new on the wire) and answers to the template's
  Object Selector for events, so an On Hit written for the template counts every copy.
- normalizeLocomotion keeps a boolean `worldGrab` (K1; 31-vr-core implements it).
- gameKit exports clap / clapGesture / pointGrab / gameSettings for the suites.

Suite stars-clap (NEW, 33/33). Counterfactuals, one run with six guards reverted:
- grip ray gate removed (byRay = true) -> 8.4 red (the ray took the ball with pointing off)
- desktop cursor gate removed -> 8.6 red
- spawn ignores the wired place -> 2.2 red (copy at the template's offset, [0,1,0]; 6.2 with it)
- nodetrigger sent without `at` -> 2.8 red
- template-selector match for copies removed -> 6.3 red (count 0 -> 0)
- fireClap's own `enabled` check removed -> 4.1 stayed GREEN: clap.js asks clapWanted()
  first, which already refuses; the inner check is the second layer for a graph holding one
  enabled and one disabled On Clap node (not separately proven).
All restored; worktree clean.
svelte-check 336/47, message list identical to base 40e07fb; vitest 300 green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… clapped stars; Jam Room: fly, world grab, teleport

Roadmap 31 items S1-S4 and J1 (the user's Quest 3 feedback, 2026-10-01).
- Stars Room: play.locomotion.teleport + play.bounds = the inside of the glass (walls at
  +-5.75); two Game Setting rows, "Point to move stars" (Point Grab node) and "Make stars
  with a clap" (both On Clap nodes); a clap spawns ONE star from the template at the point
  (maxAlive 40, oldest recycled) with a sparkle + sparks burst and a portal sound there, and
  a `me` buzz in both hands; the template's On Hit (anyone: a ring; me: a touch) makes a
  clapped (or More-stars) star count as one of your touches. Copy: how to play mentions the
  clap and teleport.
- Jam Room: play.locomotion {fly, worldGrab, teleport} + play.bounds inside the studio.
  The flags take effect once 31-vr-core lands (K1); they survive the save now.
- game-stars-room +13 checks (S1-S4 on the real scene: bounds, the two rows, a clap makes a
  crystal dynamic star with its burst/sound/buzz, a push flies it and counts a touch, clap
  off makes nothing, pointing off: ray refuses, touch holds) -> 70/70.
- game-jam-room +5 checks (flags + bounds survive the load; in a world grown 1.5x by the
  Edit gesture's own maths a held sweep plays three piano keys and flips three drum steps)
  -> 44/44. music-template 11/11, author-kit 59/59.
Counterfactual: normalizeLocomotion without the worldGrab line -> vitest locomotionPolicy
red (1 failed / 13 passed), restored green.
Staged: cloud-lane-30-staging/31-stars-jam/games/{stars-room 52729 B, jam-room 86324 B}.

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

The Quest report: "menu buttons during game covered by scene objects below untangle (like
base/floor objects)". A VR panel was a mesh like any other, so whatever stood between the
eyes and it covered its buttons - while the laser, which prefers a panel, still pressed them.

- vrPanelOverlay.js (leaf, THREE only): every open panel joins the transparent list at
  PANEL_ORDER after ONE depth clear (a colourless, never-culled sentinel whose
  onBeforeRender clears depth), so panels draw over the finished scene and still depth-test
  against EACH OTHER (a panel's own layers keep their order; a nearer panel covers a
  farther one - which a blanket depthTest:false gets wrong). Both eyes: without multiview
  three renders one eye completely, then the other, so the clear touches a finished half;
  with multiview both views clear together. Raycasts read geometry, so hit tests are
  untouched.
- vrControls: core panels re-marked each frame (idempotent, allocation-free); the sentinel
  is in the scene only while a panel is open (a frame without one is the old frame); a
  panel the ray reaches ENDS THE BEAM even with a floor in front (the press already
  preferred the panel - now the beam agrees); beam + reticle draw at BEAM_ORDER while on a
  panel. registerOverlayPanel / panelOverlayDebug exported.
- moduleSDK: api.vrPanel(object) -> undo (journalled with the module) for a module's own VR
  menu / level bar - Untangle's bar is the case the user saw.
- vrGamePanel surfaces sit at PANEL_ORDER (they already ignored depth); splineEdit's handle
  group opts out (userData.vrOverlay = false - world handles, not a panel).

Suite vr-panels-on-top (12, fakeXR, PIXELS with a red unlit blocker in objectsGroup between
the camera and each panel): premise - the blocker hides an ordinary mesh (0.00 not red);
the radial ring (1.00), the game board's Start (1.00) and an api.vrPanel module bar (1.00)
draw over it; the beam passes the blocker and ends on the board (1.400 m) and on the module
bar (0.900 m) at BEAM_ORDER; the trigger presses Start through the blocker; the depth clear
ran and stands down with no panel open.
Counterfactuals (restored after each): overlay disabled -> ring 0.00, module bar 0.00,
clears 0 (3 red); panel hits compared by distance -> beam 0.660 m / 0.475 m and order 0
(3 red).
svelte-check 333/47 (identical list).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…world gestures; play.bounds

The Quest reports: "Entangle game does not allow me to scale scene with grips within game,
only in edit mode" and "In Jam Room I would like to be able to fly around and also scale the
entire environment with grips and move around same as in edit mode".

- locomotionPolicy: `play.locomotion.worldGrab: true` -> Interact's worldGestures; the
  normaliser keeps exactly the typed booleans teleport / fly / worldGrab (empty -> absent,
  so a scene that never used them saves byte-identically).
- vrGrip.gripMovesWorld(mode, worldGrab): Edit always, Interact only with the flag. vrControls'
  empty-air grip asks it with the live policy - so two grips scale/rotate the world, the right
  grip alone pans it, and a grip on a grabbable body (dynamic, interaction 'grab') still
  takes the BODY (pickGripTarget runs first, unchanged).
- teleportRules.js (new pure leaf, P4's rules) - here for normalizeBounds: `play.bounds
  {min:[x,y,z], max:[x,y,z]}` is a SIBLING of locomotion; six finite numbers |v| <= 1e5, each
  axis ordered, else dropped. scenePhysics keeps it in the play block (omitted when absent);
  resolvePlaySettings resolves it field-by-field like spawn, a publisher's in ITS local frame
  (`boundsOwner`). Vitest teleportRules (18): bounds, shrink, slab test, raster segment, the
  verdict order.
- moduleSDK: `api.locomotion = {boundedTeleport: true, worldGrab: true}` (frozen) - the
  feature probe the game lanes publish against (agreed with 31-fb-dungeon and 31-stars-jam).
- wireValidate: no change - scenephysics has no shape there, and the singleton's own
  normaliser is its boundary.

Suite vr-locomotion-flags (13, fakeXR through the real grip path): the normaliser (typed flags
only, bounds ordered, bad bounds dropped, untouched scene writes neither key); WITHOUT the
flag Interact's two grips start nothing and the world stays 1:1; WITH it two grips start the
world grab and a spread scales x2.00, a right grip on the floor pans, a grip on the dynamic
cube holds the cube; api.locomotion probe; a module publisher's userData.play turns it on.
Vitest +5 (locomotionPolicy 2, vrGrip 1, + the teleportRules file): 296 green (base 275).
Counterfactuals (one batch, restored): gripMovesWorld(mode) -> 5 red; worldGrab dropped by
the normaliser -> 1 red; bounds not normalised -> 1 red. 7 red.
svelte-check 333/47 (identical list).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he play area, never through a wall; red when refused

The Quest reports: "Dungeon Realms ... I would like to be able to teleport but not outside the
dungeon walls" and "In Stars room I would like to be able to teleport but not outside the
scene". Edit's teleport lands anywhere the arc does; a game's (Interact/Play with
`play.locomotion.teleport: true`) asks teleportRules for a verdict now.

- computeTeleportArc(origin, dir, group, {bounded, roots}): a bounded arc stops at the FIRST
  surface it meets, of any slope (a wall face ends it instead of the arc passing through the
  wall to the floor behind), over objectsGroup AND registered module content (a dungeon's
  walls live under module-world-root), and reports the landing's world normal.y.
- teleportVerdict(from, to, normalY) (exported; the dungeon lane's e2e entry point), the rules
  in order, each in ITS OWN frame so a world grab never skews one:
  1 walkable: normal.y >= 0.7 (or the floor plane) else 'steep';
  2 inside `play.bounds` (scene frame, or the publisher group's local frame) - else the
    scene's content box pulled in 0.3 m on x/z (measured once per aim) - else 'outside';
  3 a dungeon raster (dungeonPlay): the target cell is floor ('off-floor') and no wall cell lies
    on the straight line ('wall-cell');
  4 nothing solid crossed 1.1 m above both ends: a publisher's `play.colliders` AABBs, then a
    mesh raycast ('blocked').
- updateTeleport: the arc is GREEN when valid and RED when refused (teleportPreview() reads
  it); release teleports only on a valid landing - a red release does nothing.
- executeTeleport(target, bounded): a game's teleport stands your FEET on the landing (a step
  onto a platform is a step up); Edit keeps its head-height move exactly as before.

Suite vr-bounded-teleport (19, a walled test room through fakeXR's real right-stick path):
the verdict (floor ok, behind the wall blocked, beyond the bounds outside, a wall face steep;
no bounds -> the content box shrunk 0.3 m); the arc short = green on the floor, the release
moves you there; onto a 0.5 m platform (a sim running) = green, head 0.5 m higher; over the
wall = RED behind it, release does nothing; at the wall = ends on the face, steep; a raster
wall cell on the way refused; Interact without the flag = no arc; Edit = the over-wall
landing green and taken.
Counterfactuals: mesh probe / raster / content bounds / bounded arc removed (one batch) ->
6 red; feet not placed (y: 0) -> "stands you ON it" 1.60 red. Both restored.
svelte-check 333/47 (identical list); vitest 296.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… play.bounds, the bounded teleport, api.vrPanel)

- New section "Moving around in a game: locomotion, bounds, VR panels (1.18, roadmap 31)":
  `locomotion.teleport` (the bounded rules, red arc, feet on the landing), `locomotion.worldGrab`,
  `locomotion.fly`, `play.bounds` (a sibling of locomotion; a module's in its group's local
  frame), `play.colliders`, the `api.locomotion` feature probe, the teleportVerdict /
  teleportPreview debug entry points, and `api.vrPanel(group)` for a module's own VR menu
  (drawn over the scene, ends the laser).
- static/llms-full.txt regenerated (npm run sync-llms).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
31-vr-core asked consumers to take its normalizer: locomotionPolicy resolved to theirs
(identical worldGrab line, mine dropped); vrControls = the union of the two import blocks
(the pointGrab gate in gripTargetOf auto-merged). The two held suites gain the bounded-
teleport verdicts (feature-detected: SKIP without vr-core).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexZ005 and others added 3 commits October 1, 2026 04:29
… Stars Room's floating stars)

Found by 31-stars-jam on the union: in the Stars Room, teleportVerdict from the spawn to the
floor across the room returned 'blocked'. The wall probe runs 1.1 m above both ends, which is
the stars' layer (24 dynamic icosahedra at 0.8-2.6 m), so any floating star in the line
refused every landing. meshBetween now skips hits whose top-level object is a DYNAMIC physics
body or a spawner's transient copy - a body you can knock aside is not a wall. Static and
pick-through solids (the room's glass) still block.

vr-bounded-teleport 21/21 (+2: a dynamic box at 1.1 m in the line -> ok; the same box static
-> blocked). Counterfactual: the filter removed -> the dynamic check red.
svelte-check 333/47 (identical list).

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

On the union with 31-vr-core: teleportVerdict takes FEET, so the check stands on the spawn
floor; a new check teleports across the room through the floating-star layer, which was
refused (reason blocked) until vr-core 8189daa stopped dynamic bodies counting as walls.
game-stars-room 72/72 on the union.

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

AlexZ005 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up: merged origin/feat/31-vr-core (through 8189daa) into this branch and resolved the two conflicts (locomotionPolicy = vr-core's; vrControls import union). On the union: stars-clap 33/33, game-stars-room 72/72 (+ bounded-teleport verdicts: inside lands, beyond the glass refused, across the room past floating stars lands — that last one found a real bug fixed in vr-core 8189daa), game-jam-room 45/45, music-template 11/11, author-kit 59/59; svelte-check 336/47 identical; vitest 321. Land #253 (vr-core) first; this PR then diffs only the stars/jam work.

@AlexZ005
AlexZ005 merged commit 3140674 into feat/1.18 Oct 1, 2026
3 of 4 checks passed
@AlexZ005
AlexZ005 deleted the feat/31-stars-jam branch October 1, 2026 02:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant