Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5de8a84
[feat] 30b P0: game sounds and game music (C5) - procedural sfx set, …
AlexZ005 Sep 23, 2026
552d090
[feat] 30b P1: haptics (C4) - silent in Edit, api.hapticPattern, tap/…
AlexZ005 Sep 23, 2026
f47d9f2
[feat] 30b P2: effects and announce (C6) - pooled particle bursts, ap…
AlexZ005 Sep 23, 2026
616d6d2
[fix] 30b P0: the one-eye light helper — helpers leave three's XR eye…
AlexZ005 Sep 23, 2026
4b46f71
[feat] 30b P1: Interact and Play draw no editor scaffolding
AlexZ005 Sep 23, 2026
8677f38
[fix] 30b P2: grips by mode — Edit moves the world in game scenes, In…
AlexZ005 Sep 23, 2026
35079f9
[feat] 30b P3: walk like a game — Interact collides, falls and climbs…
AlexZ005 Sep 23, 2026
a09a247
[feat] 30b P4: games enter Interact in VR, left Y switches modes, and…
AlexZ005 Sep 23, 2026
af38e55
[fix] 30b P5: module content follows the VR world (Untangle's dots sp…
AlexZ005 Sep 23, 2026
57a623f
[feat] 30b P3: the game UI in VR (C2) - the HUD menu as a world board…
AlexZ005 Sep 23, 2026
fbf517d
[feat] 30b P4: hold the trigger and sweep (C3) - tip and laser sweep …
AlexZ005 Sep 23, 2026
e89aed5
[docs] 30b P5: MODULES.md - game feel (sound, music, haptics, effects…
AlexZ005 Sep 23, 2026
ee0378a
[docs] 30b P5: refresh static/llms-full.txt from MODULES.md (npm run …
AlexZ005 Sep 23, 2026
770f93e
[fix] 30b: the first burst of a session was lost; the timing-bound ch…
AlexZ005 Sep 23, 2026
da87d8a
Merge origin/feat/30b-vr-modes into feat/30b-core-games (on top of fe…
AlexZ005 Sep 23, 2026
8b604de
[feat] 30b core-games P0: game feel as flow nodes - announce, game so…
AlexZ005 Sep 23, 2026
accd772
[feat] 30b core-games P1: towers rings are milestones, stars room lig…
AlexZ005 Sep 23, 2026
4602922
[feat] 30b core-games P2: the jam room is a vr cockpit - every instru…
AlexZ005 Sep 23, 2026
9fb5c7c
[fix] 30b core-games P3: what the shots showed - no banner over a res…
AlexZ005 Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions MODULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,10 @@ api.registerClickHandler(pressKey, { modes: ['interact', 'play'] }); // the defa
api.registerClickHandler(pickTool, { modes: ['edit'] }); // an editor TOOL
```

The VR trigger has no editor mode yet and still offers every handler.
In a headset the same modes apply (1.17, 30b): Play in VR enters Interact, where the
trigger clicks (module handlers, On Click) and selects nothing; in Edit it selects as
before. Handlers also get `ctx = {source, mode}` — see *Game feel* below for the
hold-and-sweep and `{sweep: false}`.
`api.editorMode()` reads the mode on this screen (`'edit'` | `'interact'`) — for a module
whose own pointer listeners run outside click routing (a drag), so it can stand down in Edit.

Expand Down Expand Up @@ -478,6 +481,76 @@ if (api.storage) {
Feature-detect it; on an older app write the SAME key yourself
(`localStorage['tp:mod:<id>:<key>'] = JSON.stringify(value)`) so progress carries over.

### Game feel: sound, music, haptics, effects, banners (1.17, roadmap 30b)

Everything here is LOCAL to the device it runs on — broadcast your own op
(`api.send`) when every peer should hear, feel or see it — and all of it is
feature-detected, so a module written against it still loads on an older app.

```js
register(api) {
api.registerClickHandler((object, ctx) => {
if (!object.userData.ring) return false;
const at = object.getWorldPosition(new api.THREE.Vector3()).toArray();
api.playSound?.('success', at);
api.effects?.burst(at, { kind: 'sparkle' });
api.announce?.('Ring ' + object.userData.ring + ' reached', { sub: '+50' });
api.hapticPattern?.('success'); // both hands; 'left' | 'right' for one
return true;
});
api.game.onChange?.(() => (api.game.roundUnderway() ? api.music?.play('arcade') : api.music?.stop()));
}
```

- **`api.playSound(name, position?)`** → `true` when a sound started. Twenty
procedural game sounds, no assets: `click` `pop` `whoosh` `success` `fail` `hit`
`kick` `shoot` `laser` `explosion` `coin` `levelup` `goal` `whistle` `cheer`
`step` `ring` `sparkle` `hurt` `portal`, plus the ping chimes `ding` (the default),
`chime`, `pluck`, `bell`. An unknown name is a quiet no-op (it used to play the
ding). `position` spatialises it. Played at the player's "Game sounds" volume.
- **`api.music.play(preset, {volume?})` / `api.music.stop()`** — a looping
procedural track under the effects, at the player's "Music" volume: `arcade`
`ambient` `dungeon` `stadium` `space` `puzzle` `studio`. It is TEMPO-SYNCED to the
session clock (two peers on one preset hear the same bar), plays only in Interact
or Play (`false` from the editor) and stops by itself when the player goes back to
editing. `api.music.current()` names what is playing; `api.music.presets()` lists them.
- **`api.hapticPattern(name, hand?)`** — `tap` `bump` `hit` `success` `fail`
`rumble` `heartbeat` on the VR controllers. `api.haptic(intensity, ms, hand?)`
still exists. BOTH are silent in Edit mode (vibration is for playing), and core
already plays the defaults for you in Interact/Play: a `tap` when the laser enters
something clickable, a `bump` when a press lands, a `hit` on a grab, and a knock
sized by how hard the hand hit.
- **`api.effects.burst([x, y, z], {kind?, color?, count?})`** — a short, pooled
particle burst at the scene root: `sparkle` (default), `confetti`, `smoke`,
`sparks`; `count` 1..96. Nothing is saved or replicated.
- **`api.announce(text, {sub?, ms?, color?})`** — a big centred banner ("GOAL!",
"Level 3"): the desktop HUD, and in a headset a banner fixed in front of the
player. A new one replaces the one showing; `ms` 300..15000 (default 1800).

**In VR, your game's HUD is in the player's hands.** A screen with `input: 'menu'`,
a control on it, or bound to the `menu` / `paused` / `over` game state is drawn on
a board ~1.2 m in front of the player that follows their head lazily; its buttons
work with the laser and trigger and with a poke. The other screens (score, timer,
level) read as short lines on a card on the left wrist and a strip across the top
of the view. The board and the wrist card always carry an **Edit mode** button.
Nothing to do on your side — author the HUD once and it works on both.

**Hold the trigger and sweep (VR, Interact/Play).** While the trigger is held, the
controller TIP clicks each clickable it passes over — the one it is NEAREST to, so a
glide along a keyboard plays one key at a time — once, and again after it leaves and
comes back: piano keys, pads, toggles, HUD buttons; the laser does the same for things
out of reach. "Clickable" (also what earns the hover `tap`) means: under a group you
passed to `registerInteractiveGroup`, a part of an audio device (`userData.device` on
an ancestor — each key/pad is its own control), an object an On Click node targets, or
an object you mark `userData.clickable = true`. Your handler gets a second argument
`ctx = {source, mode}`: `source` is `'click'` (desktop), `'trigger'` (the VR press
itself) or `'sweep'` (a later entry while held). A control that must not be swept
(a knob you drag, a dot you carry) opts out:

```js
api.registerClickHandler(pickDot, { sweep: false }); // presses still reach it; sweeps do not
```

### Physics (P-A)

All mutations are INITIATOR-ONLY — the peer that started the simulation steps
Expand Down Expand Up @@ -625,8 +698,9 @@ api.scene(); // THREE.Scene
api.objectsGroup(); // the replicated objects root (add scene content here)
api.peerId(); // our peer id (undefined before the mesh is up)
api.toast('hi'); // toast in the corner
api.haptic(0.6, 60); // buzz the VR controllers (no-op on desktop);
api.haptic(0.6, 60); // buzz the VR controllers (no-op on desktop and in Edit);
api.haptic(0.6, 60, 'right'); // optional hand targets one controller (17-A1)
api.hapticPattern('success'); // a named pattern (1.17, see Game feel)
api.isVR(); // true inside a VR session
api.vrHand('left'); // one hand's WORLD pose + buttons, or null:
// {position, quaternion, trigger, gripped, connected}
Expand Down
Loading
Loading