@genex-ai/cli-demo 0.12.1 → 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 (40) 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-physics-rapier/SKILL.md +128 -0
  33. package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +202 -0
  34. package/templates/skills/genex-threejs-physics-rapier/references/physics-setup.md +207 -0
  35. package/templates/skills/genex-threejs-skill-router/SKILL.md +3 -0
  36. package/templates/skills/genex-threejs-skill-router/references/routing-map.md +15 -7
  37. package/templates/skills/genex-threejs-vehicle-controllers/SKILL.md +110 -0
  38. package/templates/skills/genex-threejs-vehicle-controllers/references/car.md +162 -0
  39. package/templates/skills/genex-threejs-vehicle-controllers/references/drone.md +150 -0
  40. package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +199 -0
@@ -0,0 +1,277 @@
1
+ // SPDX-FileCopyrightText: 2023-2026 Erdong Chen
2
+ // SPDX-License-Identifier: MIT
3
+ // Vanilla-TS port of the ecctrl character controller's keyboard layer (replaces drei
4
+ // KeyboardControls + the demo wrapper's key polling; React/zustand shells removed).
5
+ //
6
+ // Port notes / deliberate deviations from upstream:
7
+ // - NEW: window `blur` + `visibilitychange`(hidden) clear ALL keys — prevents stuck keys
8
+ // after alt-tab (drei does not do this; upstream slept bodies on tab-hide instead).
9
+ // - NEW: `preventDefault` on handled keys defaults to true (stops Space scrolling the page).
10
+ // - `onInteract` is rising-edge + `event.repeat`-guarded, replacing the zustand
11
+ // change-only subscription semantics upstream got for free.
12
+
13
+ /** Movement intent for the character. Field names match the controller's `MovementInput`, so
14
+ * `character.setMovement({ ...kb.getCharacterMovement(), joystick: { x, y } })` works verbatim. */
15
+ export interface CharacterMovementIntent {
16
+ forward: boolean;
17
+ backward: boolean;
18
+ leftward: boolean;
19
+ rightward: boolean;
20
+ run: boolean;
21
+ jump: boolean;
22
+ }
23
+
24
+ /** Movement intent for the car. Field names match the vehicle controller's `VehicleInput`. */
25
+ export interface CarMovementIntent {
26
+ forward: boolean;
27
+ backward: boolean;
28
+ steerLeft: boolean;
29
+ steerRight: boolean;
30
+ brake: boolean;
31
+ }
32
+
33
+ /** Movement intent for the drone. Field names match the drone controller's `DroneInput`.
34
+ * Deliberately asymmetric: WASD = throttle/yaw, arrows = pitch/roll (they are NOT aliases). */
35
+ export interface DroneMovementIntent {
36
+ throttleUp: boolean;
37
+ throttleDown: boolean;
38
+ yawLeft: boolean;
39
+ yawRight: boolean;
40
+ pitchForward: boolean;
41
+ pitchBackward: boolean;
42
+ rollLeft: boolean;
43
+ rollRight: boolean;
44
+ }
45
+
46
+ export interface KeyboardInputOptions {
47
+ /** Event target; default `window` (a non-focusable element would never receive key events). */
48
+ target?: Window | HTMLElement;
49
+ /** Call `preventDefault()` on handled keys (stops Space scrolling the page). Default true. */
50
+ preventDefault?: boolean;
51
+ }
52
+
53
+ type NamedKey =
54
+ | "w"
55
+ | "s"
56
+ | "a"
57
+ | "d"
58
+ | "space"
59
+ | "shift"
60
+ | "f"
61
+ | "up"
62
+ | "down"
63
+ | "left"
64
+ | "right";
65
+
66
+ // Bindings ported from the upstream keyboard map: letters/arrows/space match by
67
+ // KeyboardEvent.code; Shift matches by event.key so ShiftLeft AND ShiftRight both work.
68
+ const CODE_BINDINGS: Readonly<Partial<Record<string, NamedKey>>> = {
69
+ KeyW: "w",
70
+ KeyS: "s",
71
+ KeyA: "a",
72
+ KeyD: "d",
73
+ Space: "space",
74
+ KeyF: "f",
75
+ ArrowUp: "up",
76
+ ArrowDown: "down",
77
+ ArrowLeft: "left",
78
+ ArrowRight: "right",
79
+ };
80
+ const KEY_BINDINGS: Readonly<Partial<Record<string, NamedKey>>> = {
81
+ Shift: "shift",
82
+ };
83
+
84
+ const ALL_NAMED_KEYS: readonly NamedKey[] = [
85
+ "w",
86
+ "s",
87
+ "a",
88
+ "d",
89
+ "space",
90
+ "shift",
91
+ "f",
92
+ "up",
93
+ "down",
94
+ "left",
95
+ "right",
96
+ ];
97
+
98
+ /**
99
+ * Event-driven keyboard state: WASD/arrows/Space/Shift/F → movement intents.
100
+ * Nothing to call per frame — read `getCharacterMovement()` (or the car/drone variants) once
101
+ * per render frame and pass the result into the controller's `setMovement()`. Always send the
102
+ * complete intent object: the controller merges defined fields, so a stale partial would leave
103
+ * old `true`s behind.
104
+ */
105
+ export class KeyboardInput {
106
+ #keys: Record<NamedKey, boolean> = {
107
+ w: false,
108
+ s: false,
109
+ a: false,
110
+ d: false,
111
+ space: false,
112
+ shift: false,
113
+ f: false,
114
+ up: false,
115
+ down: false,
116
+ left: false,
117
+ right: false,
118
+ };
119
+
120
+ #target: Window | HTMLElement;
121
+ #preventDefault: boolean;
122
+ #interactCallbacks = new Set<() => void>();
123
+ #disposed = false;
124
+
125
+ #onKeyDown = (event: Event): void => {
126
+ const e = event as KeyboardEvent;
127
+ const named = CODE_BINDINGS[e.code] ?? KEY_BINDINGS[e.key];
128
+ if (named === undefined) return;
129
+ if (this.#preventDefault) e.preventDefault();
130
+ const wasDown = this.#keys[named];
131
+ this.#keys[named] = true;
132
+ // Rising edge only, guarded against OS key auto-repeat.
133
+ if (named === "f" && !e.repeat && !wasDown) {
134
+ for (const callback of this.#interactCallbacks) callback();
135
+ }
136
+ };
137
+
138
+ #onKeyUp = (event: Event): void => {
139
+ const e = event as KeyboardEvent;
140
+ const named = CODE_BINDINGS[e.code] ?? KEY_BINDINGS[e.key];
141
+ if (named === undefined) return;
142
+ this.#keys[named] = false;
143
+ };
144
+
145
+ #onBlur = (): void => {
146
+ this.#clearAll();
147
+ };
148
+
149
+ #onVisibilityChange = (): void => {
150
+ if (document.hidden) this.#clearAll();
151
+ };
152
+
153
+ constructor(options: KeyboardInputOptions = {}) {
154
+ this.#target = options.target ?? window;
155
+ this.#preventDefault = options.preventDefault ?? true;
156
+ this.#target.addEventListener("keydown", this.#onKeyDown);
157
+ this.#target.addEventListener("keyup", this.#onKeyUp);
158
+ // NEW vs upstream: clear all keys when focus/visibility is lost (no stuck keys after alt-tab).
159
+ window.addEventListener("blur", this.#onBlur);
160
+ document.addEventListener("visibilitychange", this.#onVisibilityChange);
161
+ }
162
+
163
+ // ---- Raw named-key state (upstream keyboard-map names) ----
164
+
165
+ get w(): boolean {
166
+ return this.#keys.w;
167
+ }
168
+ get s(): boolean {
169
+ return this.#keys.s;
170
+ }
171
+ get a(): boolean {
172
+ return this.#keys.a;
173
+ }
174
+ get d(): boolean {
175
+ return this.#keys.d;
176
+ }
177
+ get space(): boolean {
178
+ return this.#keys.space;
179
+ }
180
+ get shift(): boolean {
181
+ return this.#keys.shift;
182
+ }
183
+ get f(): boolean {
184
+ return this.#keys.f;
185
+ }
186
+ get up(): boolean {
187
+ return this.#keys.up;
188
+ }
189
+ get down(): boolean {
190
+ return this.#keys.down;
191
+ }
192
+ get left(): boolean {
193
+ return this.#keys.left;
194
+ }
195
+ get right(): boolean {
196
+ return this.#keys.right;
197
+ }
198
+
199
+ /** True while any bound key is held (useful for waking a sleeping body). */
200
+ get anyPressed(): boolean {
201
+ for (const named of ALL_NAMED_KEYS) {
202
+ if (this.#keys[named]) return true;
203
+ }
204
+ return false;
205
+ }
206
+
207
+ // ---- Derived intents (exact upstream wrapper mappings, touch terms merged by the caller) ----
208
+
209
+ /** WASD or arrows to move, Shift to run, Space to jump. Merge touch input caller-side:
210
+ * `{ ...kb.getCharacterMovement(), run: kb.shift || btnRun.pressed, joystick: {...} }`. */
211
+ getCharacterMovement(): CharacterMovementIntent {
212
+ const k = this.#keys;
213
+ return {
214
+ forward: k.w || k.up,
215
+ backward: k.s || k.down,
216
+ leftward: k.a || k.left,
217
+ rightward: k.d || k.right,
218
+ run: k.shift,
219
+ jump: k.space,
220
+ };
221
+ }
222
+
223
+ /** WASD or arrows to drive/steer, Space to brake. */
224
+ getCarMovement(): CarMovementIntent {
225
+ const k = this.#keys;
226
+ return {
227
+ forward: k.w || k.up,
228
+ backward: k.s || k.down,
229
+ steerLeft: k.a || k.left,
230
+ steerRight: k.d || k.right,
231
+ brake: k.space,
232
+ };
233
+ }
234
+
235
+ /** W/S throttle, A/D yaw, arrows pitch/roll. WASD and arrows are NOT aliases here — do not
236
+ * "unify" them; the asymmetry is the upstream control scheme. */
237
+ getDroneMovement(): DroneMovementIntent {
238
+ const k = this.#keys;
239
+ return {
240
+ throttleUp: k.w,
241
+ throttleDown: k.s,
242
+ yawLeft: k.a,
243
+ yawRight: k.d,
244
+ pitchForward: k.up,
245
+ pitchBackward: k.down,
246
+ rollLeft: k.left,
247
+ rollRight: k.right,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Rising-edge interact hook (F key), auto-repeat-guarded — wire it to the enter/exit
253
+ * manager: `kb.onInteract(() => mgr.requestInteract())`. Returns an unsubscribe function.
254
+ */
255
+ onInteract(callback: () => void): () => void {
256
+ this.#interactCallbacks.add(callback);
257
+ return () => {
258
+ this.#interactCallbacks.delete(callback);
259
+ };
260
+ }
261
+
262
+ /** Remove all listeners and clear state. */
263
+ dispose(): void {
264
+ if (this.#disposed) return;
265
+ this.#disposed = true;
266
+ this.#target.removeEventListener("keydown", this.#onKeyDown);
267
+ this.#target.removeEventListener("keyup", this.#onKeyUp);
268
+ window.removeEventListener("blur", this.#onBlur);
269
+ document.removeEventListener("visibilitychange", this.#onVisibilityChange);
270
+ this.#interactCallbacks.clear();
271
+ this.#clearAll();
272
+ }
273
+
274
+ #clearAll(): void {
275
+ for (const named of ALL_NAMED_KEYS) this.#keys[named] = false;
276
+ }
277
+ }
@@ -0,0 +1,176 @@
1
+ // SPDX-FileCopyrightText: 2023-2026 Erdong Chen
2
+ // SPDX-License-Identifier: MIT
3
+ // Vanilla-TypeScript port of the ecctrl character controller (React/R3F removed).
4
+ // The "default" and "heavy-body-reference" values are upstream tuning
5
+ // (library defaults / demo leva settings); the other presets are Genex-authored.
6
+
7
+ import type { CharacterControllerOptions } from "./character-controller.ts";
8
+
9
+ /**
10
+ * A named tuning set for the character controller.
11
+ *
12
+ * IMPORTANT: the float spring (`springK`/`dampingC`) and the auto-balance
13
+ * springs scale roughly linearly with body mass, so every preset states the
14
+ * collider `density` it was tuned for (`assumedDensity`, always mirrored into
15
+ * `options.density`). If you change density or capsule size, scale those
16
+ * spring constants in the same proportion.
17
+ */
18
+ export interface CharacterPreset {
19
+ /** Collider density the spring constants were tuned for. */
20
+ assumedDensity: number;
21
+ /** Plain-language description of the feel this preset gives. */
22
+ description: string;
23
+ /** Partial options — unset keys fall back to the library defaults. */
24
+ options: CharacterControllerOptions;
25
+ }
26
+
27
+ /**
28
+ * Ready-made character tunings. Spread into the controller options:
29
+ *
30
+ * ```ts
31
+ * const character = new CharacterController(world, camera, {
32
+ * ...characterPresets["default"].options,
33
+ * userData: { controller: { excludeVehicleRay: true } },
34
+ * });
35
+ * ```
36
+ *
37
+ * Quick tuning map: "slippery" -> lower `slideGripFactor`; "floaty" -> lower
38
+ * `fallingGravityScale`; "sluggish" -> raise `accDeltaTime`; "jumps too weak"
39
+ * -> raise `jumpVel`; "tips over" -> raise `autoBalanceSpringK`.
40
+ */
41
+ export const characterPresets: Readonly<
42
+ Record<
43
+ | "default"
44
+ | "heavy-body-reference"
45
+ | "platformer-snappy"
46
+ | "souls-heavy"
47
+ | "moon-bounce"
48
+ | "ice-slide",
49
+ CharacterPreset
50
+ >
51
+ > = {
52
+ /** Upstream library defaults — a balanced third-person feel at density 1. */
53
+ "default": {
54
+ assumedDensity: 1,
55
+ description:
56
+ "Balanced third-person feel (the library defaults). Walk 2 m/s, run 5 m/s, " +
57
+ "decisive jump, moderate grip. Tuned for collider density 1.",
58
+ options: {
59
+ density: 1,
60
+ },
61
+ },
62
+
63
+ /**
64
+ * UPSTREAM PARITY — the source demo's tuned settings at density 200.
65
+ * Every value below must match the upstream demo exactly; the float and
66
+ * balance springs are ~80x stiffer because the body is ~200x heavier.
67
+ */
68
+ "heavy-body-reference": {
69
+ assumedDensity: 200,
70
+ description:
71
+ "The upstream demo tuning: a heavy (density 200) body with proportionally " +
72
+ "stiff float/balance springs. The reference for how spring constants scale " +
73
+ "with mass — copy this ratio when you raise density.",
74
+ options: {
75
+ density: 200,
76
+ capsuleHalfHeight: 0.3,
77
+ capsuleRadius: 0.3,
78
+ maxWalkVel: 1.1,
79
+ maxRunVel: 5.5,
80
+ jumpVel: 6,
81
+ jumpDuration: 0.1,
82
+ moveImpulsePointOffset: 0,
83
+ slopeMaxAngle: 1,
84
+ floatHeight: 0.3,
85
+ rayOriginOffset: -0.35,
86
+ rayHitForgiveness: 0.3,
87
+ rayLength: 1.3,
88
+ rayRadius: 0.15,
89
+ springK: 6400,
90
+ dampingC: 860,
91
+ autoBalanceSpringK: 50,
92
+ autoBalanceDampingC: 3,
93
+ autoBalanceSpringOnY: 8,
94
+ autoBalanceDampingOnY: 0.76,
95
+ },
96
+ },
97
+
98
+ /** Genex-authored: fast accel/brake, decisive jumps, hold-to-run. */
99
+ "platformer-snappy": {
100
+ assumedDensity: 1,
101
+ description:
102
+ "Snappy platformer feel: quick starts and stops, strong jump with a heavy " +
103
+ "fall, extra air control, high grip. Run is hold-to-run (no toggle). " +
104
+ "Tuned for collider density 1.",
105
+ options: {
106
+ density: 1,
107
+ maxWalkVel: 3,
108
+ maxRunVel: 7,
109
+ accDeltaTime: 0.35,
110
+ decDeltaTime: 0.35,
111
+ jumpVel: 7,
112
+ fallingGravityScale: 4,
113
+ airDragFactor: 0.3,
114
+ slideGripFactor: 0.8,
115
+ enableToggleRun: false,
116
+ },
117
+ },
118
+
119
+ /** Genex-authored: weighty, committed movement. */
120
+ "souls-heavy": {
121
+ assumedDensity: 1,
122
+ description:
123
+ "Weighty, committed movement: slow to start and stop, low deliberate jump, " +
124
+ "gentle fall, pronounced run lean. Tuned for collider density 1.",
125
+ options: {
126
+ density: 1,
127
+ maxWalkVel: 1.6,
128
+ maxRunVel: 4,
129
+ accDeltaTime: 0.12,
130
+ decDeltaTime: 0.15,
131
+ jumpVel: 4.2,
132
+ jumpDuration: 0.15,
133
+ fallingGravityScale: 2.2,
134
+ moveImpulsePointOffset: 0.6,
135
+ },
136
+ },
137
+
138
+ /**
139
+ * Genex-authored: long, floaty jumps. PAIR WITH LOW WORLD GRAVITY — set the
140
+ * physics world gravity to `(0, -1.62, 0)` (the controller never changes
141
+ * world gravity itself).
142
+ */
143
+ "moon-bounce": {
144
+ assumedDensity: 1,
145
+ description:
146
+ "Low-gravity moonwalk: long floaty jumps and drifty air control. Pair with " +
147
+ "world gravity (0, -1.62, 0) — the preset does not set world gravity for " +
148
+ "you. Tuned for collider density 1.",
149
+ options: {
150
+ density: 1,
151
+ jumpVel: 4,
152
+ fallingGravityScale: 1,
153
+ fallingMaxVel: 10,
154
+ airDragFactor: 0.05,
155
+ },
156
+ },
157
+
158
+ /** Genex-authored: near-zero grip, everything slides. */
159
+ "ice-slide": {
160
+ assumedDensity: 1,
161
+ description:
162
+ "Ice feel: near-zero grip, slow acceleration, barely any braking, and " +
163
+ "sideways momentum is mostly kept — turns become wide slides. Tuned for " +
164
+ "collider density 1.",
165
+ options: {
166
+ density: 1,
167
+ slideGripFactor: 0.05,
168
+ accDeltaTime: 0.08,
169
+ decDeltaTime: 0.03,
170
+ rejectVelFactor: 0.2,
171
+ },
172
+ },
173
+ };
174
+
175
+ /** Name of a shipped character preset. */
176
+ export type CharacterPresetName = keyof typeof characterPresets;