@genex-ai/cli-demo 0.54.0-dev.123 → 0.55.0-dev.124

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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ genex character "<prompt>" # generate a controller-ready Meshy humanoid
20
20
  genex character animate <id> --action <action-id> # add a same-rig Meshy action
21
21
  genex animations search "<intent>" --json # search the committed Meshy catalog locally
22
22
  genex wait <id> # attach to a --no-wait generation and print its URL(s) when done
23
- genex controller <type> # character|car|drone|networked-physics → src/controllers/
23
+ genex controller <type> # character|car|drone|touch|networked-physics → src/controllers/ (touch = the standalone mobile-input kit)
24
24
  genex controller character --character <id> # install the Meshy-native adapter + manifest
25
25
  genex controller anims <sel…> # download extra character animation clips (by tag or name) → public/assets/anims/
26
26
  genex ui <tool> # local pixel toolbox for generated UI art: extract | masks | text-color | trim
package/dist/index.js CHANGED
@@ -2996,6 +2996,7 @@ var CONTROLLER_KINDS = [
2996
2996
  "character",
2997
2997
  "car",
2998
2998
  "drone",
2999
+ "touch",
2999
3000
  "networked-physics"
3000
3001
  ];
3001
3002
  var SHARED = [
@@ -3003,10 +3004,16 @@ var SHARED = [
3003
3004
  "shared/physics-world.ts",
3004
3005
  "shared/colliders.ts"
3005
3006
  ];
3007
+ var TOUCH_KIT = [
3008
+ "touch/touch-joystick.ts",
3009
+ "touch/drag-zone.ts",
3010
+ "touch/rotate-overlay.ts"
3011
+ ];
3006
3012
  var INPUT_AND_CAMERA = [
3007
3013
  "character/follow-camera.ts",
3008
3014
  "character/keyboard-input.ts",
3009
- "character/touch-joystick.ts"
3015
+ "character/touch-joystick.ts",
3016
+ ...TOUCH_KIT
3010
3017
  ];
3011
3018
  var NOTICE = "NOTICE.md";
3012
3019
  var CONTROLLER_FILE_SETS = {
@@ -3078,6 +3085,17 @@ var CONTROLLER_FILE_SETS = {
3078
3085
  `physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
3079
3086
  ]
3080
3087
  },
3088
+ touch: {
3089
+ code: [...TOUCH_KIT, NOTICE],
3090
+ assets: [],
3091
+ skill: "genex-threejs-touch-controls",
3092
+ sketch: [
3093
+ `const joy = new TouchJoystick({ floating: true }); // safe-area-aware defaults; static circle without the flag`,
3094
+ `const jump = new VirtualButton({ label: "Jump", onPress: () => player.jump() });`,
3095
+ `const look = new DragZone(); // right half; per frame: const { dx, dy } = look.consumeDelta()`,
3096
+ `[joy, jump, look].forEach((w) => w.setVisible(navigator.maxTouchPoints > 0));`
3097
+ ]
3098
+ },
3081
3099
  "networked-physics": {
3082
3100
  code: [
3083
3101
  ...SHARED,
@@ -3108,7 +3126,7 @@ async function runController(opts) {
3108
3126
  if (!kind || !CONTROLLER_KINDS.includes(kind)) {
3109
3127
  log.error(
3110
3128
  `Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
3111
- "genex controller <character|car|drone|networked-physics> [--force]"
3129
+ "genex controller <character|car|drone|touch|networked-physics> [--force]"
3112
3130
  )} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
3113
3131
  );
3114
3132
  process.exitCode = 1;
@@ -3175,13 +3193,20 @@ async function runController(opts) {
3175
3193
  log.plain("");
3176
3194
  }
3177
3195
  log.plain(c.bold("Next steps"));
3196
+ if (kind !== "touch") {
3197
+ log.plain(
3198
+ ` 1. ${c.cyan(
3199
+ kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
3200
+ )} (three is already in the scaffold).`
3201
+ );
3202
+ }
3203
+ const stepOffset = kind === "touch" ? 0 : 1;
3204
+ log.plain(
3205
+ ` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
3206
+ );
3178
3207
  log.plain(
3179
- ` 1. ${c.cyan(
3180
- kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
3181
- )} (three is already in the scaffold).`
3208
+ kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
3182
3209
  );
3183
- log.plain(` 2. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`);
3184
- log.plain(" 3. Wiring sketch (controllers update BEFORE the physics step):");
3185
3210
  const sketch = kind === "character" && opts.character ? [
3186
3211
  `const physics = await PhysicsWorld.create();`,
3187
3212
  `const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
@@ -13492,14 +13517,18 @@ ${c.bold("Usage")}
13492
13517
  genex wait <id> Attach to a generation enqueued with --no-wait and
13493
13518
  print its asset URL(s) when it finishes. Safe to
13494
13519
  re-run \u2014 it never creates a new generation.
13495
- genex controller <type> [--force] Install a physics controller
13496
- (character|car|drone|networked-physics)
13520
+ genex controller <type> [--force] Install a bundled controller
13521
+ (character|car|drone|touch|networked-physics)
13497
13522
  into src/controllers (+ assets into public/assets).
13523
+ "touch" = the standalone mobile touch kit (joystick,
13524
+ buttons, drag zone, rotate overlay) for games
13525
+ without a physics controller.
13498
13526
  Character defaults to the player's VRM + UAL pack.
13499
13527
  genex controller character --character <id>
13500
13528
  Install the Meshy-native controller variant.
13501
13529
  genex controller anims <sel \u2026> Install legacy VRM + UAL animation clips by tag/name
13502
- into public/assets/anims/.
13530
+ into public/assets/anims/. --list shows the catalog;
13531
+ --reset forgets the previous selection first.
13503
13532
  genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
13504
13533
  Three.js systems you can clone or borrow parts
13505
13534
  from. No query lists the whole catalog.
@@ -13647,6 +13676,7 @@ ${c.bold("Examples")}
13647
13676
  genex character animate <character-id> --action <action-id>
13648
13677
  genex controller character --character <character-id>
13649
13678
  genex controller anims sword pistol
13679
+ genex controller touch
13650
13680
  genex controller networked-physics
13651
13681
  genex explore "grass"
13652
13682
  genex explore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.54.0-dev.123",
3
+ "version": "0.55.0-dev.124",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,387 +1,5 @@
1
- // SPDX-FileCopyrightText: 2023-2026 Erdong Chen
2
- // SPDX-License-Identifier: MIT
3
- // Vanilla-TS port of the ecctrl controller's touch input (Joystick + VirtualButton DOM/CSS
4
- // widgets; React/zustand shells removed per-instance state + callbacks instead of stores).
5
- //
6
- // Port notes / deliberate deviations from upstream:
7
- // - NEW: `setPointerCapture` on pointerdown keeps drags alive outside the 200px wrapper
8
- // (upstream relied on `pointerleave` alone; that reset path is kept for parity when
9
- // capture is unavailable).
10
- // - `VirtualButton.dispose()` resets only ITS OWN state (upstream unmount reset ALL buttons).
11
- // - Upstream's hard-coded duplicate DOM ids (joystick/base/knob/button/cap) are dropped;
12
- // instances are the identity now.
13
- // - The zustand store keys (`id` props) are gone — create one instance per stick/button.
14
-
15
- // ---------------------------------------------------------------------------
16
- // Shared style plumbing
17
- // ---------------------------------------------------------------------------
18
-
19
- function applyStyle(
20
- element: HTMLElement,
21
- base: Partial<CSSStyleDeclaration>,
22
- override?: Partial<CSSStyleDeclaration>
23
- ): void {
24
- Object.assign(element.style, base);
25
- if (override) Object.assign(element.style, override);
26
- }
27
-
28
- /** Legacy vendor prefixes upstream shipped via React CSSProperties (Moz/ms). */
29
- function applyLegacyUserSelectNone(element: HTMLElement): void {
30
- element.style.setProperty("-moz-user-select", "none");
31
- element.style.setProperty("-ms-user-select", "none");
32
- }
33
-
34
- // ---------------------------------------------------------------------------
35
- // TouchJoystick
36
- // ---------------------------------------------------------------------------
37
-
38
- // Default styles for the joystick wrapper (the 200px interactive hit area).
39
- // NOTE: upstream ships NO position (left/bottom commented out) — position via wrapperStyle.
40
- const DEFAULT_JOYSTICK_WRAPPER_STYLE: Partial<CSSStyleDeclaration> = {
41
- userSelect: "none",
42
- webkitUserSelect: "none",
43
- touchAction: "none",
44
- overscrollBehavior: "none",
45
- position: "fixed",
46
- zIndex: "10",
47
- height: "200px",
48
- width: "200px",
49
- borderRadius: "50%",
50
- };
51
-
52
- // Default styles for the joystick base (the 100px reference circle — center math uses THIS).
53
- const DEFAULT_JOYSTICK_BASE_STYLE: Partial<CSSStyleDeclaration> = {
54
- width: "100px",
55
- height: "100px",
56
- background: "rgba(0, 0, 0, 0.1)",
57
- border: "2px solid white",
58
- borderRadius: "50%",
59
- position: "absolute",
60
- top: "50%",
61
- left: "50%",
62
- transform: "translate(-50%, -50%)",
63
- touchAction: "none",
64
- };
65
-
66
- // Default styles for the joystick knob. The cubic-bezier transition IS the spring-back:
67
- // the state snaps to 0 instantly on release while the knob overshoots home in CSS.
68
- const DEFAULT_JOYSTICK_KNOB_STYLE: Partial<CSSStyleDeclaration> = {
69
- width: "70px",
70
- height: "70px",
71
- background: "rgba(255, 255, 255, 0.8)",
72
- borderRadius: "50%",
73
- position: "absolute",
74
- top: "50%",
75
- left: "50%",
76
- transform: "translate(-50%, -50%)",
77
- transition: "transform 0.2s cubic-bezier(0.25, 1.5, 0.5, 1)",
78
- willChange: "transform",
79
- pointerEvents: "none",
80
- };
81
-
82
- export interface TouchJoystickOptions {
83
- /** DOM parent; default `document.body`. */
84
- parent?: HTMLElement;
85
- /** Max knob travel in px; default 50. Larger = more finger travel for full deflection. */
86
- maxRadius?: number;
87
- /**
88
- * Style overrides merged over the defaults. Positioning (`left`/`bottom`/`right`) MUST come
89
- * through here — no default position ships; e.g. `{ left: "0", bottom: "0" }` for a
90
- * bottom-left movement stick.
91
- */
92
- wrapperStyle?: Partial<CSSStyleDeclaration>;
93
- baseStyle?: Partial<CSSStyleDeclaration>;
94
- knobStyle?: Partial<CSSStyleDeclaration>;
95
- /** Optional change hook: fires on every move/reset with the new state. */
96
- onChange?: (x: number, y: number, active: boolean) => void;
97
- }
98
-
99
- /**
100
- * DOM+CSS touch joystick. Read `x`/`y` each render frame and pass them into the character
101
- * controller: `setMovement({ ...kb.getCharacterMovement(), joystick: { x: joy.x, y: joy.y } })`.
102
- * A nonzero joystick overrides the digital keys inside the controller — pass both through and
103
- * let it pick. Note the controller normalizes direction, so deflection magnitude is not speed.
104
- */
105
- export class TouchJoystick {
106
- #maxRadius: number;
107
- #wrapper: HTMLDivElement;
108
- #base: HTMLDivElement;
109
- #knob: HTMLDivElement;
110
- #x = 0;
111
- #y = 0;
112
- #active = false;
113
- /** Pointer-drag latch — distinct from the public `active` (deflection ≠ 0) flag. */
114
- #pointerActive = false;
115
- #onChange: ((x: number, y: number, active: boolean) => void) | undefined;
116
- #disposed = false;
117
-
118
- #onContextMenu = (event: Event): void => {
119
- event.preventDefault();
120
- };
121
-
122
- #onPointerDown = (event: PointerEvent): void => {
123
- event.preventDefault();
124
- event.stopPropagation();
125
- this.#move(event.clientX, event.clientY);
126
- this.#pointerActive = true;
127
- // NEW vs upstream: capture keeps the drag working outside the 200px wrapper.
128
- if (typeof this.#wrapper.setPointerCapture === "function") {
129
- try {
130
- this.#wrapper.setPointerCapture(event.pointerId);
131
- } catch {
132
- // pointer already gone — the pointerleave reset path still covers us
133
- }
134
- }
135
- };
136
-
137
- #onPointerMove = (event: PointerEvent): void => {
138
- if (this.#pointerActive) this.#move(event.clientX, event.clientY);
139
- };
140
-
141
- #onPointerEnd = (): void => {
142
- this.#reset();
143
- };
144
-
145
- constructor(options: TouchJoystickOptions = {}) {
146
- this.#maxRadius = options.maxRadius ?? 50;
147
- this.#onChange = options.onChange;
148
-
149
- this.#wrapper = document.createElement("div");
150
- applyStyle(this.#wrapper, DEFAULT_JOYSTICK_WRAPPER_STYLE, options.wrapperStyle);
151
- applyLegacyUserSelectNone(this.#wrapper);
152
-
153
- this.#base = document.createElement("div");
154
- applyStyle(this.#base, DEFAULT_JOYSTICK_BASE_STYLE, options.baseStyle);
155
- this.#wrapper.appendChild(this.#base);
156
-
157
- this.#knob = document.createElement("div");
158
- applyStyle(this.#knob, DEFAULT_JOYSTICK_KNOB_STYLE, options.knobStyle);
159
- this.#base.appendChild(this.#knob);
160
-
161
- this.#wrapper.addEventListener("contextmenu", this.#onContextMenu);
162
- this.#wrapper.addEventListener("pointerdown", this.#onPointerDown);
163
- this.#wrapper.addEventListener("pointermove", this.#onPointerMove);
164
- this.#wrapper.addEventListener("pointerup", this.#onPointerEnd);
165
- this.#wrapper.addEventListener("pointerleave", this.#onPointerEnd);
166
-
167
- (options.parent ?? document.body).appendChild(this.#wrapper);
168
- }
169
-
170
- /** Normalized horizontal deflection in [-1, 1] (right-positive). */
171
- get x(): number {
172
- return this.#x;
173
- }
174
-
175
- /** Normalized vertical deflection in [-1, 1], UP-positive (screen dy is negated once here). */
176
- get y(): number {
177
- return this.#y;
178
- }
179
-
180
- /** True iff (x, y) !== (0, 0). */
181
- get active(): boolean {
182
- return this.#active;
183
- }
184
-
185
- /** The wrapper element (for ad-hoc styling / conditional display). */
186
- get element(): HTMLDivElement {
187
- return this.#wrapper;
188
- }
189
-
190
- /** Show/hide helper — e.g. show only when `navigator.maxTouchPoints > 0`. */
191
- setVisible(visible: boolean): void {
192
- this.#wrapper.style.display = visible ? "" : "none";
193
- }
194
-
195
- /** Reset state + remove DOM + listeners. Safe to call mid-drag. */
196
- dispose(): void {
197
- if (this.#disposed) return;
198
- this.#disposed = true;
199
- this.#reset();
200
- this.#wrapper.removeEventListener("contextmenu", this.#onContextMenu);
201
- this.#wrapper.removeEventListener("pointerdown", this.#onPointerDown);
202
- this.#wrapper.removeEventListener("pointermove", this.#onPointerMove);
203
- this.#wrapper.removeEventListener("pointerup", this.#onPointerEnd);
204
- this.#wrapper.removeEventListener("pointerleave", this.#onPointerEnd);
205
- this.#wrapper.remove();
206
- }
207
-
208
- #move(clientX: number, clientY: number): void {
209
- // Center math uses the BASE rect (100px reference circle), not the 200px wrapper.
210
- const rect = this.#base.getBoundingClientRect();
211
- const centerX = rect.left + rect.width / 2;
212
- const centerY = rect.top + rect.height / 2;
213
- let dx = clientX - centerX;
214
- let dy = clientY - centerY;
215
- const distance = Math.hypot(dx, dy);
216
- // If the distance exceeds the maximum radius, scale down the movement.
217
- if (distance > this.#maxRadius) {
218
- dx *= this.#maxRadius / distance;
219
- dy *= this.#maxRadius / distance;
220
- }
221
-
222
- // The -50% self-centering and the pixel offset ride in ONE translate — replacing this
223
- // with left/top would break the CSS spring-back.
224
- this.#knob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
225
-
226
- this.#x = dx / this.#maxRadius;
227
- this.#y = -dy / this.#maxRadius; // screen-down-positive dy → game-forward-positive y
228
- this.#active = !(this.#x === 0 && this.#y === 0);
229
- this.#onChange?.(this.#x, this.#y, this.#active);
230
- }
231
-
232
- #reset(): void {
233
- this.#pointerActive = false;
234
- // State snaps to 0 immediately; the knob eases back via the CSS transition.
235
- this.#knob.style.transform = "translate(-50%, -50%)";
236
- this.#x = 0;
237
- this.#y = 0;
238
- this.#active = false;
239
- this.#onChange?.(0, 0, false);
240
- }
241
- }
242
-
243
- // ---------------------------------------------------------------------------
244
- // VirtualButton
245
- // ---------------------------------------------------------------------------
246
-
247
- // Default style for the virtual button wrapper (the 60px hit area).
248
- const DEFAULT_BUTTON_WRAPPER_STYLE: Partial<CSSStyleDeclaration> = {
249
- userSelect: "none",
250
- webkitUserSelect: "none",
251
- touchAction: "none",
252
- overscrollBehavior: "none",
253
- position: "fixed",
254
- zIndex: "10",
255
- height: "60px",
256
- width: "60px",
257
- background: "rgba(0, 0, 0, 0.1)",
258
- borderRadius: "50%",
259
- };
260
-
261
- // Default style for the virtual button cap (the 45px visible disc with the label).
262
- const DEFAULT_BUTTON_CAP_STYLE: Partial<CSSStyleDeclaration> = {
263
- width: "45px",
264
- height: "45px",
265
- background: "rgba(255, 255, 255, 0.8)",
266
- borderRadius: "50%",
267
- position: "absolute",
268
- top: "50%",
269
- left: "50%",
270
- transform: "translate(-50%, -50%)",
271
- transition: "transform 0.2s cubic-bezier(0.25, 1.5, 0.5, 1)",
272
- willChange: "transform",
273
- display: "flex",
274
- justifyContent: "center",
275
- alignItems: "center",
276
- fontSize: "12px",
277
- fontWeight: "bold",
278
- fontFamily: "Arial, sans-serif",
279
- color: "LightGray",
280
- userSelect: "none",
281
- pointerEvents: "none",
282
- };
283
-
284
- export interface VirtualButtonOptions {
285
- /** Text rendered on the cap, e.g. "Jump" (plain text — set via textContent). */
286
- label?: string;
287
- /** DOM parent; default `document.body`. */
288
- parent?: HTMLElement;
289
- /** Style overrides; position the button via `wrapperStyle` (e.g. `{ right: "40px", bottom: "90px" }`). */
290
- wrapperStyle?: Partial<CSSStyleDeclaration>;
291
- capStyle?: Partial<CSSStyleDeclaration>;
292
- /** Rising-edge press hook (e.g. an on-screen Enter/Exit button → `mgr.requestInteract()`). */
293
- onPress?: () => void;
294
- onRelease?: () => void;
295
- }
296
-
297
- /**
298
- * DOM+CSS virtual button for touch controls. Read `pressed` each frame (e.g.
299
- * `jump: kb.space || btnJump.pressed`) or use the `onPress`/`onRelease` edge callbacks.
300
- */
301
- export class VirtualButton {
302
- #wrapper: HTMLDivElement;
303
- #cap: HTMLDivElement;
304
- #pressed = false;
305
- #onPress: (() => void) | undefined;
306
- #onRelease: (() => void) | undefined;
307
- #disposed = false;
308
-
309
- #onContextMenu = (event: Event): void => {
310
- event.preventDefault();
311
- };
312
-
313
- #onPointerDown = (event: PointerEvent): void => {
314
- this.#press(event);
315
- };
316
-
317
- #onPointerEnd = (): void => {
318
- this.#release();
319
- };
320
-
321
- constructor(options: VirtualButtonOptions = {}) {
322
- this.#onPress = options.onPress;
323
- this.#onRelease = options.onRelease;
324
-
325
- this.#wrapper = document.createElement("div");
326
- applyStyle(this.#wrapper, DEFAULT_BUTTON_WRAPPER_STYLE, options.wrapperStyle);
327
- applyLegacyUserSelectNone(this.#wrapper);
328
-
329
- this.#cap = document.createElement("div");
330
- applyStyle(this.#cap, DEFAULT_BUTTON_CAP_STYLE, options.capStyle);
331
- this.#cap.textContent = options.label ?? "";
332
- this.#wrapper.appendChild(this.#cap);
333
-
334
- this.#wrapper.addEventListener("contextmenu", this.#onContextMenu);
335
- this.#wrapper.addEventListener("pointerdown", this.#onPointerDown);
336
- this.#wrapper.addEventListener("pointerup", this.#onPointerEnd);
337
- this.#wrapper.addEventListener("pointerleave", this.#onPointerEnd);
338
-
339
- (options.parent ?? document.body).appendChild(this.#wrapper);
340
- }
341
-
342
- /** True while the button is held. */
343
- get pressed(): boolean {
344
- return this.#pressed;
345
- }
346
-
347
- /** The wrapper element (for ad-hoc styling / conditional display). */
348
- get element(): HTMLDivElement {
349
- return this.#wrapper;
350
- }
351
-
352
- /** Show/hide helper. */
353
- setVisible(visible: boolean): void {
354
- this.#wrapper.style.display = visible ? "" : "none";
355
- }
356
-
357
- /** Release (if held) + remove DOM + listeners. Resets only this button's own state. */
358
- dispose(): void {
359
- if (this.#disposed) return;
360
- this.#disposed = true;
361
- this.#release();
362
- this.#wrapper.removeEventListener("contextmenu", this.#onContextMenu);
363
- this.#wrapper.removeEventListener("pointerdown", this.#onPointerDown);
364
- this.#wrapper.removeEventListener("pointerup", this.#onPointerEnd);
365
- this.#wrapper.removeEventListener("pointerleave", this.#onPointerEnd);
366
- this.#wrapper.remove();
367
- }
368
-
369
- #press(event: PointerEvent): void {
370
- event.preventDefault();
371
- event.stopPropagation();
372
- const wasPressed = this.#pressed;
373
- this.#pressed = true;
374
- this.#cap.style.transform = "translate(-50%, -50%) scale(1.3)";
375
- this.#cap.style.opacity = "0.5";
376
- // Rising edge (pointerdown cannot re-fire while down, but guard anyway).
377
- if (!wasPressed) this.#onPress?.();
378
- }
379
-
380
- #release(): void {
381
- if (!this.#pressed) return;
382
- this.#pressed = false;
383
- this.#cap.style.transform = "translate(-50%, -50%) scale(1)";
384
- this.#cap.style.opacity = "1";
385
- this.#onRelease?.();
386
- }
387
- }
1
+ // Compatibility re-export the touch primitives moved to the shared touch kit
2
+ // (../touch/touch-joystick.ts) so games without a character controller can install
3
+ // them via `genex controller touch`. Existing imports of this path keep working;
4
+ // new code should import from "./controllers/touch/touch-joystick.ts".
5
+ export * from "../touch/touch-joystick.ts";