@genex-ai/cli-demo 0.11.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +203 -4
  3. package/package.json +7 -2
  4. package/templates/controllers/NOTICE.md +65 -0
  5. package/templates/controllers/assets/animation-library.glb +0 -0
  6. package/templates/controllers/assets/character.glb +0 -0
  7. package/templates/controllers/assets/default-avatar.vrm +0 -0
  8. package/templates/controllers/character/character-animations.ts +682 -0
  9. package/templates/controllers/character/character-controller.ts +1636 -0
  10. package/templates/controllers/character/follow-camera.ts +644 -0
  11. package/templates/controllers/character/keyboard-input.ts +277 -0
  12. package/templates/controllers/character/presets.ts +176 -0
  13. package/templates/controllers/character/touch-joystick.ts +387 -0
  14. package/templates/controllers/character/vrm/capsule-fit.ts +52 -0
  15. package/templates/controllers/character/vrm/foot-ik.ts +341 -0
  16. package/templates/controllers/character/vrm/vrm-loader.ts +44 -0
  17. package/templates/controllers/character/vrm/vrm-retarget.ts +195 -0
  18. package/templates/controllers/drone/drone-controller.ts +1073 -0
  19. package/templates/controllers/drone/presets.ts +225 -0
  20. package/templates/controllers/interact/enter-exit.ts +502 -0
  21. package/templates/controllers/shared/colliders.ts +456 -0
  22. package/templates/controllers/shared/math.ts +230 -0
  23. package/templates/controllers/shared/physics-world.ts +622 -0
  24. package/templates/controllers/vehicle/presets.ts +297 -0
  25. package/templates/controllers/vehicle/vehicle-controller.ts +615 -0
  26. package/templates/controllers/vehicle/wheel.ts +1200 -0
  27. package/templates/skills/genex-getting-started/SKILL.md +5 -0
  28. package/templates/skills/genex-threejs-character-controller/SKILL.md +205 -0
  29. package/templates/skills/genex-threejs-character-controller/references/animations.md +235 -0
  30. package/templates/skills/genex-threejs-character-controller/references/tuning-and-presets.md +102 -0
  31. package/templates/skills/genex-threejs-character-controller/references/wiring.md +198 -0
  32. package/templates/skills/genex-threejs-embed-auth/SKILL.md +126 -54
  33. package/templates/skills/genex-threejs-multiplayer/SKILL.md +17 -11
  34. package/templates/skills/genex-threejs-physics-rapier/SKILL.md +128 -0
  35. package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +202 -0
  36. package/templates/skills/genex-threejs-physics-rapier/references/physics-setup.md +207 -0
  37. package/templates/skills/genex-threejs-skill-router/SKILL.md +3 -0
  38. package/templates/skills/genex-threejs-skill-router/references/routing-map.md +15 -7
  39. package/templates/skills/genex-threejs-vehicle-controllers/SKILL.md +110 -0
  40. package/templates/skills/genex-threejs-vehicle-controllers/references/car.md +162 -0
  41. package/templates/skills/genex-threejs-vehicle-controllers/references/drone.md +150 -0
  42. package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +199 -0
@@ -47,6 +47,11 @@ Each has a focused skill with the exact loader code — `$genex-ai-model`,
47
47
 
48
48
  ## Publishing
49
49
 
50
+ Before your first `genex preview`, the play URL (`https://<slug>.genex.technology/`)
51
+ already serves a **standard placeholder world** — the owner's avatar in a small
52
+ playground, unrelated to your code. It is NOT in your repo; never treat it as a
53
+ starting point or try to modify it. Your first `preview` replaces it automatically.
54
+
50
55
  `npx genex preview` deploys to your unlisted draft URL; `npx genex publish`
51
56
  lists the game in the public gallery. When publishing, pick 1–3 gallery
52
57
  categories from what you actually built — `games`, `assets`, `physics`,
@@ -0,0 +1,205 @@
1
+ ---
2
+ name: genex-threejs-character-controller
3
+ description: Add a tuned physics character controller to a Genex Three.js game with `npx genex controller character` — dynamic-capsule movement (walk/run/jump, slopes, stairs, moving platforms), follow camera, keyboard + touch input, and animation binding. Use for any on-foot player or third-person movement, and whenever the user asks for ecctrl — this is that controller, ported to plain Three.js.
4
+ ---
5
+
6
+ # Genex Three.js Character Controller
7
+
8
+ ## Run the command first — never hand-write the controller
9
+
10
+ ```bash
11
+ npx genex controller character
12
+ npm i @dimforge/rapier3d-compat @pixiv/three-vrm # three is already in the scaffold
13
+ ```
14
+
15
+ The command vendors tested, tuned controller code into the game: TypeScript
16
+ modules into `src/controllers/` (including `character/vrm/` — VRM loading,
17
+ animation retargeting, capsule auto-fit, foot IK) and the 46-clip
18
+ `animation-library.glb` into `public/assets/`. It also writes the player's
19
+ avatar to `public/assets/avatar.vrm` — **your** avatar when you're signed in,
20
+ otherwise a bundled CC0 default (attribution in `src/controllers/NOTICE.md`).
21
+ The character plays as that VRM. The copied files are then owned by the game —
22
+ edit them freely; re-running skips existing files unless `--force`. Do not write
23
+ a character controller from scratch and do not swap in a kinematic-controller
24
+ tutorial: this one is a real dynamic body that pushes crates, rides moving
25
+ platforms, climbs stairs and slides on too-steep slopes out of the box.
26
+
27
+ ## What you get
28
+
29
+ | Module (under `src/controllers/`) | Exports you use | Job |
30
+ | --- | --- | --- |
31
+ | `shared/physics-world.ts` | `PhysicsWorld` | Rapier WASM init, fixed-timestep loop, body↔Object3D sync, collision events |
32
+ | `shared/colliders.ts` | `cuboidCollider`, `collidersFromObject`, … | colliders for level geometry and GLB props |
33
+ | `character/character-controller.ts` | `CharacterController` | the floating-capsule movement brain |
34
+ | `character/presets.ts` | `characterPresets` | six named tunings |
35
+ | `character/follow-camera.ts` | `FollowCamera` | orbit/zoom chase camera with collision pullback |
36
+ | `character/keyboard-input.ts` | `KeyboardInput` | WASD/arrows/Shift/Space/F state, no per-frame polling setup |
37
+ | `character/touch-joystick.ts` | `TouchJoystick`, `VirtualButton` | mobile controls |
38
+ | `character/character-animations.ts` | `CharacterAnimations` | animation state machine + fuzzy clip binding + `playOneShot` + procedural fallback |
39
+ | `character/vrm/*` | `loadVrm`, `retargetClips`, `capsuleFromModel`, `FootIK` | load the VRM avatar, retarget the 46 UAL clips onto its humanoid rig, auto-fit the capsule, ground the feet |
40
+
41
+ ## Minimal wiring
42
+
43
+ ```ts
44
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
45
+ import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
46
+ import { CharacterController } from "./controllers/character/character-controller.ts";
47
+ import { CharacterAnimations } from "./controllers/character/character-animations.ts";
48
+ import { characterPresets } from "./controllers/character/presets.ts";
49
+ import { FollowCamera } from "./controllers/character/follow-camera.ts";
50
+ import { KeyboardInput } from "./controllers/character/keyboard-input.ts";
51
+ import { loadVrm } from "./controllers/character/vrm/vrm-loader.ts";
52
+ import { retargetClips } from "./controllers/character/vrm/vrm-retarget.ts";
53
+ import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
54
+
55
+ const physics = await PhysicsWorld.create(); // nothing RAPIER-related may run before this resolves
56
+
57
+ // Load the player's avatar + the animation library, then retarget onto the VRM.
58
+ const { scene: avatar, vrm } = await loadVrm("./assets/avatar.vrm");
59
+ const lib = await new GLTFLoader().loadAsync("./assets/animation-library.glb");
60
+
61
+ const fit = capsuleFromModel(avatar); // collider fits THIS avatar's bounds
62
+ const character = new CharacterController(physics.world, camera, {
63
+ ...characterPresets["default"].options,
64
+ ...fit,
65
+ position: { x: 0, y: 2, z: 0 },
66
+ userData: { controller: { excludeVehicleRay: true } }, // car wheels must never drive on the player
67
+ });
68
+ scene.add(character.root);
69
+ character.root.add(avatar); // parent the avatar under the character root
70
+ avatar.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
71
+ physics.registerBody(character.body, character.root); // root now follows the body, interpolated
72
+
73
+ const anims = new CharacterAnimations(avatar, retargetClips(vrm, lib.scene, lib.animations));
74
+ addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // punch on click
75
+
76
+ const kb = new KeyboardInput();
77
+ const followCam = new FollowCamera(camera, {
78
+ domElement: renderer.domElement,
79
+ colliderMeshes: staticWallMeshes, // static environment ONLY — never the character mesh
80
+ });
81
+
82
+ // Fixed-substep phase: input + controller brain, BEFORE world.step().
83
+ physics.onBeforeStep(() => {
84
+ character.setMovement(kb.getCharacterMovement()); // send the COMPLETE intent every step
85
+ character.update(); // ignores any dt argument — uses the fixed world.timestep internally
86
+ });
87
+
88
+ // Render phase: step physics, then camera, then animations.
89
+ const pivot = new THREE.Vector3();
90
+ renderer.setAnimationLoop(() => {
91
+ const delta = clock.getDelta();
92
+ physics.step(delta); // runs the fixed substeps + world.step() + mesh sync
93
+
94
+ pivot.copy(character.currPos).addScaledVector(character.bodyYAxis, 0.5);
95
+ followCam.moveTo(pivot.x, pivot.y, pivot.z, true);
96
+ followCam.setUp(character.upAxis);
97
+ if (physics.stepsLastFrame > 0 && character.isOnPlatform) {
98
+ followCam.applyPlatformTurn(character.turnOnYQuat); // per-physics-step delta — gate on steps
99
+ }
100
+ followCam.update(delta);
101
+
102
+ anims.update(character, delta); // see the animations reference
103
+ vrm.update(delta); // REQUIRED — ticks the humanoid rig + spring bones
104
+ renderer.render(scene, camera);
105
+ });
106
+ ```
107
+
108
+ The loop contract is strict: the controller's `update()` runs once per fixed
109
+ physics substep **before** `world.step()` (that is what `onBeforeStep` gives
110
+ you), and camera + animations run once per **render** frame with the render
111
+ delta, after `physics.step(delta)`. Read
112
+ [references/wiring.md](references/wiring.md) for the full walkthrough — level
113
+ colliders, model placement, moving platforms, camera details, enter/exit
114
+ hooks, and disposal.
115
+
116
+ ## Presets and tuning
117
+
118
+ Spread a preset into the options and override only what feels wrong:
119
+ `"default"`, `"heavy-body-reference"`, `"platformer-snappy"`, `"souls-heavy"`,
120
+ `"moon-bounce"` (needs world gravity `(0, -1.62, 0)`), `"ice-slide"`. Read
121
+ [references/tuning-and-presets.md](references/tuning-and-presets.md) for the
122
+ preset table with provenance, the density/spring scaling rule, and the
123
+ "user says X → tune Y" map. Two traps worth knowing up front:
124
+
125
+ - `slopeMaxAngle` defaults to `Math.PI / 2.5` (72°) — a 50° ramp is **walkable
126
+ out of the box**. Cap it (e.g. `Math.PI / 4`) if steep slopes should slide.
127
+ - The capsule ships with friction `-0.5` **on purpose** (grip is synthesized by
128
+ the controller). Do not "fix" it to a positive value.
129
+
130
+ ## Animations
131
+
132
+ `CharacterAnimations` resolves seven locomotion states (IDLE / WALK / RUN /
133
+ JUMP_START / JUMP_IDLE / JUMP_FALL / JUMP_LAND) from the controller's live flags
134
+ and crossfades mixer actions. Every OTHER library clip — punches, sword swings,
135
+ pistol fire, spells, sit, dance, hit reactions — plays through
136
+ `anims.playOneShot("Punch_Jab")`, which layers over locomotion and returns to it
137
+ when done (punch-on-click is the default). Read
138
+ [references/animations.md](references/animations.md) for the VRM load + retarget
139
+ wiring, the **full 46-clip catalog** with genre hints, `playOneShot` options,
140
+ overrides, foot IK, and remote-player animation.
141
+
142
+ ## Mobile: TouchJoystick + VirtualButton
143
+
144
+ ```ts
145
+ import { TouchJoystick, VirtualButton } from "./controllers/character/touch-joystick.ts";
146
+
147
+ const joy = new TouchJoystick({ wrapperStyle: { left: "20px", bottom: "20px" } }); // position is REQUIRED
148
+ const btnJump = new VirtualButton({ label: "Jump", wrapperStyle: { right: "30px", bottom: "30px" } });
149
+
150
+ physics.onBeforeStep(() => {
151
+ character.setMovement({
152
+ ...kb.getCharacterMovement(),
153
+ jump: kb.space || btnJump.pressed,
154
+ joystick: { x: joy.x, y: joy.y }, // non-zero joystick overrides the digital keys
155
+ });
156
+ character.update();
157
+ });
158
+ ```
159
+
160
+ Show them only on touch devices: `joy.setVisible(navigator.maxTouchPoints > 0)`.
161
+ Give the canvas `touch-action: none` so camera drags aren't hijacked by page
162
+ scrolling. Joystick deflection sets direction only — the controller normalizes
163
+ it, so half-deflection is not half-speed.
164
+
165
+ ## Multiplayer rule (mandatory)
166
+
167
+ **The local player is physics-authoritative; remote players are interpolated
168
+ visuals only.** Exactly one `CharacterController` exists — yours. For every
169
+ remote player: create a plain mesh (or the same GLB), move it with the
170
+ interpolator from `$genex-threejs-multiplayer`, and **never** create a rigid
171
+ body, a `CharacterController`, or any physics for it. Simulating remote
172
+ players' physics locally guarantees divergence — every client would compute a
173
+ different world.
174
+
175
+ - Publish your own `currPos` + yaw on the fixed 10–20 Hz tick, not per frame.
176
+ - To animate remotes, sync the five animation booleans and feed them to a
177
+ per-remote `CharacterAnimations` — see the animations reference.
178
+ - Load `$genex-threejs-multiplayer` before writing any networking code; it is
179
+ mandatory for any 2+ player game.
180
+
181
+ ## If the user asks for ecctrl
182
+
183
+ ecctrl is a React / React Three Fiber component; a Genex game is plain
184
+ Three.js with no React, so the package cannot run here. Tell the user plainly:
185
+ *"the library you named requires React, and this game is plain Three.js —
186
+ Genex ships the same controller, ported for your setup."* This vendored
187
+ controller **is** ecctrl's floating-capsule controller translated to plain
188
+ TypeScript classes (same physics model, same tuning options, same feel; see
189
+ `src/controllers/NOTICE.md`). Run `npx genex controller character` and carry
190
+ on — never add React or install the React package to satisfy the request.
191
+
192
+ ## Known benign warning
193
+
194
+ `@dimforge/rapier3d-compat` logs `using deprecated parameters for the
195
+ initialization function` once at boot. It comes from the library's own
196
+ embedded WASM loader, is harmless, and cannot be fixed from user code — do not
197
+ spend time chasing it.
198
+
199
+ ## Vehicles
200
+
201
+ For a drivable car or flyable drone — and character ↔ vehicle enter/exit —
202
+ run `npx genex controller car` / `npx genex controller drone` and load the
203
+ `$genex-threejs-vehicle-controllers` skill. The character side of enter/exit is
204
+ already built in: `character.park()`, `character.unpark(position, rotation)`
205
+ and the `isParked` getter (skip `character.update()` while parked).
@@ -0,0 +1,235 @@
1
+ # Character animations
2
+
3
+ `CharacterAnimations` turns the controller's live flags into crossfaded
4
+ `THREE.AnimationMixer` playback: seven states, alias-based clip binding that
5
+ works with the bundled library, Mixamo exports, or arbitrary rigs, and a
6
+ procedural bob/lean fallback when a model has no usable clips at all.
7
+
8
+ ## The bundled assets
9
+
10
+ `npx genex controller character` sets the game up to play as a **VRM avatar**:
11
+
12
+ - `public/assets/avatar.vrm` — the player's avatar (yours when signed in, else a
13
+ bundled CC0 default). Always present; always one path.
14
+ - `public/assets/animation-library.glb` (6.4 MB) — 46 clips on a shared
15
+ Quaternius rig (provenance in `src/controllers/NOTICE.md`).
16
+
17
+ VRM helpers live in `src/controllers/character/vrm/`. Install three-vrm once:
18
+ `npm i @pixiv/three-vrm`.
19
+
20
+ Load the avatar, retarget the library clips onto its humanoid rig, and auto-fit
21
+ the capsule — all with **relative** paths so the published game works under its
22
+ subpath:
23
+
24
+ ```ts
25
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
26
+ import { loadVrm } from "./controllers/character/vrm/vrm-loader.ts";
27
+ import { retargetClips } from "./controllers/character/vrm/vrm-retarget.ts";
28
+ import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
29
+ import { CharacterController } from "./controllers/character/character-controller.ts";
30
+ import { CharacterAnimations } from "./controllers/character/character-animations.ts";
31
+ import { characterPresets } from "./controllers/character/presets.ts";
32
+
33
+ const { scene, vrm } = await loadVrm("./assets/avatar.vrm");
34
+ const lib = await new GLTFLoader().loadAsync("./assets/animation-library.glb");
35
+
36
+ // capsuleFromModel derives the collider from the avatar's bounds — no manual
37
+ // per-avatar tuning even as heights/proportions vary across the library.
38
+ const fit = capsuleFromModel(scene);
39
+ const character = new CharacterController(physics.world, camera, {
40
+ ...characterPresets["default"].options,
41
+ ...fit,
42
+ position: { x: 0, y: 2, z: 0 },
43
+ });
44
+ character.root.add(scene);
45
+ scene.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
46
+
47
+ // retargetClips maps the Quaternius rig onto the VRM's normalized humanoid rig
48
+ // (VRM 0.x and 1.0 alike); the result feeds CharacterAnimations unchanged.
49
+ const anims = new CharacterAnimations(scene, retargetClips(vrm, lib.scene, lib.animations));
50
+ ```
51
+
52
+ Per render frame, **after** `physics.step(delta)`:
53
+
54
+ ```ts
55
+ anims.update(character, delta); // the controller itself satisfies the snapshot type
56
+ vrm.update(delta); // REQUIRED — ticks the humanoid rig + spring bones
57
+ ```
58
+
59
+ `vrm.update(delta)` MUST run every frame, AFTER `anims.update`: it applies the
60
+ animated normalized pose onto the render mesh and advances spring bones (hair,
61
+ cloth). `anims.update` takes the RAW render delta — pause/slow-motion go through
62
+ `anims.setPaused(true)` / `anims.setTimeScale(0.5)` (fade durations stretch with
63
+ the time scale so slow motion doesn't pop).
64
+
65
+ ### Foot IK (optional)
66
+
67
+ `vrm/foot-ik.ts` plants feet on uneven ground (no skating/floating on steps and
68
+ slopes). It's **opt-in** — locomotion and combat work without it; enable it once
69
+ the avatar's animations look right, injecting a ground query backed by rapier:
70
+
71
+ ```ts
72
+ import { FootIK } from "./controllers/character/vrm/foot-ik.ts";
73
+ const footIK = new FootIK(vrm, (foot) => {
74
+ const hit = physics.world.castRay(
75
+ new RAPIER.Ray({ x: foot.x, y: foot.y + 0.5, z: foot.z }, { x: 0, y: -1, z: 0 }),
76
+ 1.5, true);
77
+ return hit ? foot.y + 0.5 - hit.timeOfImpact : null;
78
+ });
79
+ // each frame, after vrm.update(delta):
80
+ footIK.update(delta);
81
+ ```
82
+
83
+ ## States and default clips
84
+
85
+ The pure resolver (`resolveAnimationState`) maps controller flags to one of:
86
+
87
+ | State | Bundled clip | Notes |
88
+ | --- | --- | --- |
89
+ | `IDLE` | `Idle_Loop` | starts playing immediately on construction |
90
+ | `WALK` | `Walk_Loop` | |
91
+ | `RUN` | `Jog_Fwd_Loop` | **the default run clip**; `Sprint_Loop` also binds via alias — force it with an override if the user wants an all-out sprint look |
92
+ | `JUMP_START` | `Jump_Start` | one-shot, played at 1.6× so it finishes inside the hop |
93
+ | `JUMP_IDLE` | `Jump_Loop` | airborne, moving up |
94
+ | `JUMP_FALL` | `Jump_Loop` | airborne, moving down (shares the clip — no restart mid-air) |
95
+ | `JUMP_LAND` | `Jump_Land` | one-shot |
96
+
97
+ ## One-shot actions + the 46-clip catalog
98
+
99
+ The seven states above cover locomotion. **Every other clip** plays through
100
+ `anims.playOneShot(clipName, options?)`: it crossfades the clip over the current
101
+ motion, plays it once, then hands control back to the state machine. Returns
102
+ `false` if the clip name isn't in the set you passed to the constructor.
103
+
104
+ Punch-on-click is the controller default:
105
+
106
+ ```ts
107
+ let jab = true;
108
+ addEventListener("pointerdown", () => {
109
+ anims.playOneShot(jab ? "Punch_Jab" : "Punch_Cross");
110
+ jab = !jab;
111
+ // Raycast forward from the character; if it hits another player, YOUR game
112
+ // decides the reaction — e.g. remoteAnims.playOneShot("Hit_Chest").
113
+ });
114
+ ```
115
+
116
+ `options`: `fadeIn` (default 0.1 s), `timeScale`, `clamp` (hold the final pose —
117
+ for deaths), `onDone`.
118
+
119
+ Full catalog (clip → motion → reach for it when):
120
+
121
+ | Clip(s) | Motion | Use for |
122
+ | --- | --- | --- |
123
+ | `Punch_Jab` / `Punch_Cross` | quick / heavy punch | melee, click-to-attack (default) |
124
+ | `Punch_Enter` | raise fists | enter a fighting stance |
125
+ | `Hit_Chest` / `Hit_Head` | flinch | taking damage (multiplayer-friendly reactions) |
126
+ | `Death01` | collapse | death — pair with `clamp: true` |
127
+ | `Sword_Idle` / `Sword_Attack` / `Sword_Attack_RM` | ready / swing / swing+step | melee weapons (`_RM` = root motion, travels) |
128
+ | `Pistol_Idle_Loop` / `Pistol_Aim_Up` / `_Neutral` / `_Down` | aim poses | shooters — pick by camera pitch |
129
+ | `Pistol_Shoot` / `Pistol_Reload` | fire / reload | shooter actions |
130
+ | `Spell_Simple_Enter` / `_Idle_Loop` / `_Shoot` / `_Exit` | cast cycle | magic / RPG |
131
+ | `Roll` / `Roll_RM` | dodge roll | dodge (`_RM` travels) |
132
+ | `Interact` / `PickUp_Table` / `Fixing_Kneeling` | reach / pick up / kneel-work | pickups, levers, crafting |
133
+ | `Sitting_Enter` / `_Idle_Loop` / `_Talking_Loop` / `_Exit` | sit / sit-idle / chat / stand | seats, vehicles, dialogue |
134
+ | `Dance_Loop` / `Idle_Talking_Loop` / `Idle_Torch_Loop` | dance / gesture / torch idle | emotes, NPCs, ambience |
135
+ | `Push_Loop` / `Crouch_Idle_Loop` / `Crouch_Fwd_Loop` | push / crouch / crouch-walk | pushing, stealth |
136
+ | `Swim_Idle_Loop` / `Swim_Fwd_Loop` | tread / swim | water |
137
+ | `Walk_Formal_Loop` | stiff walk | override `WALK` for a formal gait |
138
+ | `Driving_Loop` | seated at a wheel | vehicles (see `$genex-threejs-vehicle-controllers`) |
139
+ | `A_TPose` | rest pose | reference only |
140
+
141
+ For looping poses that should **persist** (aiming, sitting, swimming) rather than
142
+ play once, drive them through the public `anims.mixer` escape hatch instead; the
143
+ `$genex-threejs-vehicle-controllers` skill shows the seated pattern.
144
+
145
+ ## Binding arbitrary rigs (Mixamo included)
146
+
147
+ `buildClipMap(clips, overrides?)` resolves each state in priority order:
148
+ explicit override (exact, then case-insensitive) → library exact names →
149
+ case-insensitive → each alias as a case-insensitive **substring**, shortest
150
+ matching clip name wins (so `Walk_Loop` beats `Walk_Bwd_Loop`, and Mixamo's
151
+ `walking` beats `walking_backwards`). Aliases include `idle`, `walk`, `run`,
152
+ `jog`, `sprint`, `jump_start`, `takeoff`, `fall`, `land`, and a bare `jump`
153
+ catch-all so a rig whose only airborne clip is "Jumping" still binds all four
154
+ jump states. Unbound loop states chain (RUN↔WALK, JUMP_IDLE↔JUMP_FALL);
155
+ unbound one-shots stay silent so the previous loop keeps playing.
156
+
157
+ If a name refuses to bind, pass overrides:
158
+
159
+ ```ts
160
+ const anims = new CharacterAnimations(model, gltf.animations, {
161
+ clipMap: { RUN: "Sprint_Loop", JUMP_START: "MyTakeoff" },
162
+ onChange: (state) => { if (state === "JUMP_LAND") playLandSfx(); },
163
+ });
164
+ console.log(anims.clipMap); // inspect what actually bound, per state
165
+ ```
166
+
167
+ ### Rig mismatch guard
168
+
169
+ Clips whose tracks target bones the model doesn't have spam PropertyBinding
170
+ warnings. When mixing a custom model (e.g. from `$genex-ai-model` — note
171
+ generated props are usually rig-less) with the bundled library, filter first:
172
+
173
+ ```ts
174
+ const clips = libGltf.animations.filter((clip) =>
175
+ clip.tracks.every((track) => {
176
+ const { nodeName } = THREE.PropertyBinding.parseTrackName(track.name);
177
+ return nodeName !== undefined && model.getObjectByName(nodeName) !== undefined;
178
+ })
179
+ );
180
+ ```
181
+
182
+ ## Rig-less models: the procedural fallback
183
+
184
+ Pass an empty clip array (or clips that bind nothing) and the default
185
+ `fallback: "auto"` mode drives the model with a procedural walk/run bob, a
186
+ forward lean while moving, an air lean, and a landing dip — additive over the
187
+ model's transform at construction time, so any capsule, robot, or generated
188
+ prop reads as alive with zero animation work. `fallback: "procedural"` forces
189
+ it even when clips exist; `"none"` leaves the model static. Check
190
+ `anims.usingProceduralFallback` to see which path is live.
191
+
192
+ ```ts
193
+ const anims = new CharacterAnimations(placeholderMesh, []); // procedural fallback kicks in
194
+ ```
195
+
196
+ ## Remote players (multiplayer)
197
+
198
+ The snapshot type is structural — **anything** with the five booleans works,
199
+ which is exactly what remote players need. Remote players have no physics and
200
+ no `CharacterController` (see the SKILL's multiplayer rule): sync the five
201
+ flags from the owner and feed them straight in.
202
+
203
+ ```ts
204
+ // Sender (local player), on the 10-20 Hz tick — alongside position/yaw:
205
+ const flags = {
206
+ isOnGround: character.isOnGround,
207
+ isFalling: character.isFalling,
208
+ isMoving: character.isMoving,
209
+ runActive: character.runActive,
210
+ jumpActive: character.jumpActive,
211
+ };
212
+
213
+ // Receiver: one CharacterAnimations per remote model, fed the synced flags.
214
+ remoteAnims.update(remoteState.flags, delta);
215
+ ```
216
+
217
+ The mixer crossfades exactly as it does locally, so remote players animate
218
+ correctly without simulating anything. In v1 every player loads the same
219
+ `./assets/avatar.vrm` (the game owner's avatar), so remotes look like the owner —
220
+ load each remote through `loadVrm` + `retargetClips` just like the local one, and
221
+ call `vrm.update(delta)` per remote each frame. Relay one-shot events (punch, hit)
222
+ alongside the flags and call `remoteAnims.playOneShot("Hit_Chest")` on receipt.
223
+ Full networking patterns: `$genex-threejs-multiplayer`.
224
+
225
+ ## Gotchas
226
+
227
+ - Call `anims.update` once per render frame, never inside
228
+ `physics.onBeforeStep`.
229
+ - The state machine reads **input-based** `isMoving` — a character shoved by
230
+ physics while the player is idle stays in IDLE by design (matches the feel
231
+ players expect).
232
+ - One-shots lock transitions until they finish (`JUMP_START` → the lock
233
+ releases into `JUMP_IDLE`); this is internal — don't try to manage it.
234
+ - `dispose()` stops all actions and releases the mixer listener; call it when
235
+ the character leaves the scene.
@@ -0,0 +1,102 @@
1
+ # Presets and tuning
2
+
3
+ Every option has a tuned default — start from a preset and override only what
4
+ feels wrong. Spread the preset's `options` into the constructor:
5
+
6
+ ```ts
7
+ import { characterPresets } from "./controllers/character/presets.ts";
8
+
9
+ const character = new CharacterController(world, camera, {
10
+ ...characterPresets["platformer-snappy"].options,
11
+ maxRunVel: 8, // your overrides win
12
+ userData: { controller: { excludeVehicleRay: true } },
13
+ });
14
+ ```
15
+
16
+ ## Preset table
17
+
18
+ | Preset | Provenance | Assumed density | Feel |
19
+ | --- | --- | --- | --- |
20
+ | `default` | ported library defaults, verbatim | 1 | balanced third-person: walk 2 m/s, run 5 m/s, decisive jump, moderate grip |
21
+ | `heavy-body-reference` | ported demo tuning, verbatim | **200** | heavy body with proportionally stiff springs — the reference for the scaling rule below |
22
+ | `platformer-snappy` | Genex-authored | 1 | quick starts/stops, strong jump + heavy fall, extra air control, hold-to-run |
23
+ | `souls-heavy` | Genex-authored | 1 | weighty, committed movement; low deliberate jump; pronounced run lean |
24
+ | `moon-bounce` | Genex-authored | 1 | long floaty jumps — **requires world gravity `(0, -1.62, 0)`**; the preset does not set world gravity for you |
25
+ | `ice-slide` | Genex-authored | 1 | near-zero grip, wide sliding turns |
26
+
27
+ Every preset states the collider `density` it was tuned for
28
+ (`assumedDensity`, mirrored into `options.density`). That matters because of:
29
+
30
+ ## The density/spring scaling rule
31
+
32
+ The float spring and auto-balance springs apply raw impulses, so they scale
33
+ roughly **linearly with body mass** (mass = density × capsule volume). Change
34
+ `density` (or the capsule size) and the springs must scale in the same
35
+ proportion or the character sinks/oscillates/faceplants. The two shipped
36
+ anchor points:
37
+
38
+ | Option | density 1 (`default`) | density 200 (`heavy-body-reference`) |
39
+ | --- | --- | --- |
40
+ | `springK` | 80 | 6400 |
41
+ | `dampingC` | 6 | 860 |
42
+ | `autoBalanceSpringK` | 0.5 | 50 |
43
+ | `autoBalanceDampingC` | 0.03 | 3 |
44
+ | `autoBalanceSpringOnY` | 0.08 | 8 |
45
+ | `autoBalanceDampingOnY` | 0.006 | 0.76 |
46
+
47
+ Recipe for "make the character feel heavier": raise `density`, multiply those
48
+ six constants by the density ratio as a starting estimate (the reference
49
+ values are hand-tuned near that line, not exactly on it), then fine-tune —
50
+ pogo bounce means `dampingC` too low, sticky landings too high, slow-motion
51
+ tip-overs mean the `autoBalance*` pair is too soft.
52
+
53
+ ## "User says X → tune Y" map
54
+
55
+ | The user says | Change |
56
+ | --- | --- |
57
+ | "it's slippery / skates around" | raise `slideGripFactor` (default 0.5; `ice-slide` uses 0.05) |
58
+ | "falling feels floaty" | raise `fallingGravityScale` (default 3) |
59
+ | "jump too weak / too strong" | `jumpVel` (default 5 m/s); `jumpDuration` (default 0.1 s) stretches the takeoff window |
60
+ | "it walks up cliffs / slopes that should slide" | lower `slopeMaxAngle` — **default is `Math.PI / 2.5` = 72°, so a 50° ramp is walkable out of the box**; `Math.PI / 4` makes 45°+ slide |
61
+ | "should climb steeper slopes" | raise `slopeMaxAngle` |
62
+ | "feels heavier / like a tank" | `souls-heavy` preset, or raise `density` + apply the spring scaling rule above |
63
+ | "sluggish to start / stop" | raise `accDeltaTime` / `decDeltaTime` (responsiveness in (0, 1]; default 0.2 — higher is snappier) |
64
+ | "drifts sideways through turns" | raise `rejectVelFactor` toward 1 (default 1; `ice-slide` lowers it to 0.2) |
65
+ | "too slow / too fast" | `maxWalkVel` (default 2) / `maxRunVel` (default 5) |
66
+ | "run should be hold, not toggle" | `enableToggleRun: false` (default true = Shift toggles) |
67
+ | "no control in the air" | raise `airDragFactor` (default 0.1) |
68
+ | "falls too fast at terminal velocity" | `fallingMaxVel` (default 20 m/s) |
69
+ | "leans too much when running" | lower `moveImpulsePointOffset` (default 0.5; 0 = no lean) |
70
+ | "wobbles / tips over" | raise `autoBalanceSpringK` + `autoBalanceDampingC` |
71
+ | "turns to face direction too slowly" | raise `autoBalanceSpringOnY` |
72
+ | "grounded flag flickers on stairs / ledges" | raise `rayHitForgiveness` (default 0.28) |
73
+ | "bounces on landing (pogo)" | raise `dampingC`; "sticks to the ground on landing" → lower it |
74
+ | "hovers too high / feet in the floor" | `floatHeight` (default 0.2) — and re-check the model's foot offset (wiring reference) |
75
+ | "should always face the camera (strafe/shooter)" | `lockForward: true`, or `setLockForward(true)` at runtime |
76
+ | "jump should push off slopes" | `slopeJumpFactor` (default 0 = straight up, 1 = off the slope normal) |
77
+ | "moon / low gravity" | `moon-bounce` preset **plus** `PhysicsWorld.create({ gravity: [0, -1.62, 0] })` |
78
+ | "camera feels laggy / rubber-bandy" | `FollowCamera` `smoothTime` (0.05 snappy → 0.25 cinematic) |
79
+ | "camera clips through walls" | add the static level meshes to `FollowCamera` `colliderMeshes` |
80
+
81
+ ## Sizing a different character
82
+
83
+ `capsuleHalfHeight` (default 0.3) and `capsuleRadius` (default 0.3) define the
84
+ collider: total capsule height = `2 * (capsuleHalfHeight + capsuleRadius)` =
85
+ 1.2 by default, floating `floatHeight` above the ground. For a bigger
86
+ character scale both, remember the ground-query defaults derive from them
87
+ (`rayLength = capsuleRadius + 1`, `rayRadius = capsuleRadius / 2`,
88
+ `rayOriginOffset = -capsuleHalfHeight`), and re-tune the springs — a bigger
89
+ capsule is a heavier body at the same density.
90
+
91
+ ## Do-not-touch list
92
+
93
+ - `friction: -0.5` on the capsule is intentional (negative averages against
94
+ the ground and keeps the capsule from grabbing walls); traction comes from
95
+ the controller's own grip model, not collider friction.
96
+ - Ground detection: `"shapeCast"` (default) is forgiving on stairs and ledge
97
+ edges; `"rayCast"` is cheaper and stricter. Switch at runtime with
98
+ `setGroundDetection(...)` — don't hand-roll a third scheme.
99
+ - Platform behavior (`followPlatform`, `applyCounterMass`,
100
+ `applyCounterJumpImp`, `applyCounterMoveImp`) defaults to physically-honest
101
+ and already handles moving/rotating platforms; only touch these for
102
+ deliberate arcade effects.