@genex-ai/cli-demo 0.30.0 → 0.32.0

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/dist/index.js CHANGED
@@ -1299,7 +1299,11 @@ async function pushSource(cwd, ctx, log) {
1299
1299
  }
1300
1300
  const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
1301
1301
  await run("git", ["update-ref", "refs/heads/main", commit], base);
1302
- const push = await run("git", ["push", "-q", pushUrl, "+refs/heads/main:main"], base);
1302
+ const push = await run(
1303
+ "git",
1304
+ ["-c", "http.postBuffer=536870912", "push", "-q", pushUrl, "+refs/heads/main:main"],
1305
+ base
1306
+ );
1303
1307
  if (push.code === 0) return true;
1304
1308
  if (!managed) {
1305
1309
  log.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
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": {
@@ -34,24 +34,43 @@ body; when you don't you draw the smoothed stream and park the kinematic body on
34
34
 
35
35
  ```ts
36
36
  import RAPIER from "@dimforge/rapier3d-compat";
37
- // Proxy body: kinematic to start (owned by nobody, or by someone else).
38
- const body = physics.createBody({ type: "kinematicPosition", position: [x, y, z], ccd: true });
37
+ const r2 = (n: number) => Math.round(n * 100) / 100;
38
+ interface BallState { x: number; y: number; z: number; q?: number[]; vx?: number; vy?: number; vz?: number }
39
+
40
+ // Proxy body: kinematic to start (owned by nobody, or by someone else). `excludeCharacterRay` keeps the
41
+ // character's GROUND query from treating it as walkable, so players can't stand on / ride the ball (the
42
+ // solver collision that drives claim-by-bump + the shove is unaffected).
43
+ const body = physics.createBody({
44
+ type: "kinematicPosition", position: [x, y, z], ccd: true,
45
+ userData: { controller: { excludeCharacterRay: true } },
46
+ });
39
47
  ballCollider(physics.world, body, RADIUS, { friction: 0.5, restitution: 0.35, density: 0.6 });
40
48
  let wasTouching = false, wasMine = false, claimCd = 0;
49
+ const MAX_SPEED = 24;
50
+
51
+ // The host seeds the canonical spawn ONCE so the object exists on `objects` for late joiners.
52
+ function ensureInit() {
53
+ if (room.isHost && room.objects.get("ball") === undefined) {
54
+ room.objects.claim("ball");
55
+ room.objects.set("ball", { x, y, z, q: [0, 0, 0, 1], vx: 0, vy: 0, vz: 0 });
56
+ }
57
+ }
41
58
 
42
59
  // Per-frame, BEFORE physics.step (so the kinematic proxy is in place for the character to hit it):
43
60
  function updateBall(dt: number) {
44
- let v = room.objects.get("ball");
61
+ let v = room.objects.get<BallState>("ball");
45
62
  let mine = !!v?.isMine;
46
63
 
47
- // Rising-EDGE claim: sustained contact yields NO new rising edge, so ONE owner holds it
48
- // (no per-frame ownership ping-pong). A short cooldown throttles a steal-war.
49
- const touching = onFoot && feet.distanceTo(ballMesh.position) < RADIUS + REACH;
64
+ // Rising-EDGE claim: sustained contact yields NO new rising edge, so ONE owner holds it (no per-frame
65
+ // ownership ping-pong). A short cooldown throttles a steal-war. Use HORIZONTAL distance — the feet sit
66
+ // at ground level while the ball centre is at y≈RADIUS, so a 3D distance would eat into your reach.
67
+ const dx = ballMesh.position.x - feet.x, dz = ballMesh.position.z - feet.z;
68
+ const touching = onFoot && Math.hypot(dx, dz) < RADIUS + REACH;
50
69
  if (claimCd > 0) claimCd -= dt;
51
70
  if (touching && !wasTouching && !mine && claimCd <= 0) {
52
71
  room.objects.claim("ball"); // optimistic: isMine flips true locally THIS frame
53
72
  claimCd = 0.25;
54
- v = room.objects.get("ball"); mine = !!v?.isMine;
73
+ v = room.objects.get<BallState>("ball"); mine = !!v?.isMine;
55
74
  }
56
75
  wasTouching = touching;
57
76
 
@@ -67,22 +86,37 @@ function updateBall(dt: number) {
67
86
  wasMine = mine;
68
87
 
69
88
  if (mine) {
89
+ // Cap the owned body's speed: CCD stops single-step tunnelling, the cap stops runaway speed
90
+ // (a hard shove into a wall) from flinging the ball across the map.
91
+ const lv = body.linvel(), sp = Math.hypot(lv.x, lv.y, lv.z);
92
+ if (sp > MAX_SPEED) { const k = MAX_SPEED / sp; body.setLinvel({ x: lv.x * k, y: lv.y * k, z: lv.z * k }, true); }
70
93
  ballMesh.position.copy(body.translation()); // draw the dynamic body I simulate
71
94
  ballMesh.quaternion.copy(body.rotation());
72
95
  } else if (v?.state) {
73
96
  ballMesh.position.set(v.state.x, v.state.y, v.state.z); // draw the smoothed stream
74
97
  body.setNextKinematicTranslation(ballMesh.position); // keep the proxy on it → I can bump → claim
98
+ if (Array.isArray(v.state.q)) {
99
+ // Rotate the collider too, or a tumbled cube collides as an axis-aligned box and pops on claim.
100
+ ballMesh.quaternion.set(v.state.q[0], v.state.q[1], v.state.q[2], v.state.q[3]);
101
+ body.setNextKinematicRotation({ x: v.state.q[0], y: v.state.q[1], z: v.state.q[2], w: v.state.q[3] });
102
+ }
75
103
  }
76
104
  }
77
105
 
78
- // Owner-gated publish on your fixed tick (30Hz for a fast ball) moved-only + a ~2Hz keepalive.
79
- // Publish VELOCITY too, so the next owner seeds from it and the handoff doesn't stall.
80
- if (room.objects.get("ball")?.isMine) {
106
+ // Owner-gated publish on your fixed tick (30Hz for a fast ball). Publish MOVED-ONLY + a ~2Hz keepalive
107
+ // NEVER every tick (a resting owned object republished each tick is the #1 budget-blower; it matters the
108
+ // moment you own more than one prop). Include VELOCITY so the next owner seeds from it without stalling.
109
+ let lastSent = "", lastAt = 0;
110
+ function publishBall() {
111
+ if (!room.objects.get<BallState>("ball")?.isMine) return;
81
112
  const t = body.translation(), q = body.rotation(), lv = body.linvel();
82
- room.objects.set("ball", { x: r2(t.x), y: r2(t.y), z: r2(t.z),
83
- q: [r2(q.x), r2(q.y), r2(q.z), r2(q.w)], vx: r2(lv.x), vy: r2(lv.y), vz: r2(lv.z) });
113
+ const net = { x: r2(t.x), y: r2(t.y), z: r2(t.z),
114
+ q: [r2(q.x), r2(q.y), r2(q.z), r2(q.w)], vx: r2(lv.x), vy: r2(lv.y), vz: r2(lv.z) };
115
+ const json = JSON.stringify(net), nowMs = Date.now();
116
+ if (json === lastSent && nowMs - lastAt < 500) return; // unchanged + within keepalive window → skip
117
+ lastSent = json; lastAt = nowMs;
118
+ room.objects.set("ball", net);
84
119
  }
85
- const r2 = (v: number) => Math.round(v * 100) / 100;
86
120
  ```
87
121
 
88
122
  Rules that make it correct:
@@ -101,6 +135,14 @@ Rules that make it correct:
101
135
  budget spreads across players instead of funnelling every object through one host's send budget. This
102
136
  is why Tier 1 scales to many pushable objects where a host-authoritative fleet (Tier 2) would blow one
103
137
  client's rate cap.
138
+ - **Publish moved-only + a low-rate keepalive**, never every tick — a resting owned object republished at
139
+ full rate is the classic budget-blower, and it bites the moment one player owns several props.
140
+ - **Exclude the proxy from the character's ground query** (`userData.controller.excludeCharacterRay`), or
141
+ players stand on the ball/cube and it squirts out from under them the instant they claim it.
142
+ - **Rotate the follower, not just its position** (`setNextKinematicRotation`) — a cube another player
143
+ tumbled otherwise collides as an axis-aligned box (wrong contact) and pops to identity on claim.
144
+ - **Cap the owned body's speed** — CCD stops single-step tunnelling; the cap stops runaway speed (a hard
145
+ shove into a wall) from flinging the object across the map.
104
146
  - **Do NOT `registerBody(proxy, mesh)`** — drive the mesh yourself (from the body when you own it, from
105
147
  the stream when you don't). Registering it fights the SDK's smoothing on the stream half.
106
148