@myvillage/cli 2.7.0 → 2.8.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/package.json +1 -1
- package/src/assets/game-toolkit/myvillage-game.js +434 -0
- package/src/assets/templates/arena/AGENTS.md +93 -0
- package/src/assets/templates/arena/README.md +36 -0
- package/src/assets/templates/arena/index.html +74 -0
- package/src/assets/templates/arena/src/arena-game.js +443 -0
- package/src/assets/templates/arena/src/arena.json +114 -0
- package/src/assets/templates/arena/src/lobby.js +94 -0
- package/src/assets/templates/arena/src/main.js +46 -0
- package/src/commands/create-game.js +6 -1
- package/src/commands/deploy.js +48 -0
- package/src/index.js +1 -1
- package/src/utils/templates.js +74 -82
- package/vendor/realtime/myvillage-realtime-client.js +634 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
// myvillage-realtime client v0.2.0 — generated by myvillage-realtime (npm run build:client).
|
|
2
|
+
// Do not edit: regenerate from https://github.com/myvillageproject/myvillage-realtime
|
|
3
|
+
//#region src/shared/protocol.ts
|
|
4
|
+
/** Server simulation/snapshot rate. */
|
|
5
|
+
var TICK_HZ = 20;
|
|
6
|
+
/** Fixed client input rate; every input advances that player's sim by INPUT_DT. */
|
|
7
|
+
var INPUT_HZ = 30;
|
|
8
|
+
var INPUT_DT = 1 / 30;
|
|
9
|
+
var PROTOCOL_VERSION = 1;
|
|
10
|
+
/** WebSocket close codes the room uses (4000-4999 is app-defined). */
|
|
11
|
+
var CloseCode = {
|
|
12
|
+
Replaced: 4001,
|
|
13
|
+
MatchOver: 4002,
|
|
14
|
+
Full: 4003,
|
|
15
|
+
BadTicket: 4004
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/shared/sim.ts
|
|
19
|
+
function clamp(v, min, max) {
|
|
20
|
+
return v < min ? min : v > max ? max : v;
|
|
21
|
+
}
|
|
22
|
+
/** Unit vector a player at `yaw` faces (yaw 0 → -Z, matching three.js cameras). */
|
|
23
|
+
function forwardOf(yaw) {
|
|
24
|
+
return {
|
|
25
|
+
x: -Math.sin(yaw),
|
|
26
|
+
z: -Math.cos(yaw)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Sanitise raw stick input to a vector of length <= 1. */
|
|
30
|
+
function normalizeMove(mx, mz) {
|
|
31
|
+
const x = Number.isFinite(mx) ? clamp(mx, -1, 1) : 0;
|
|
32
|
+
const z = Number.isFinite(mz) ? clamp(mz, -1, 1) : 0;
|
|
33
|
+
const len = Math.hypot(x, z);
|
|
34
|
+
return len > 1 ? {
|
|
35
|
+
x: x / len,
|
|
36
|
+
z: z / len
|
|
37
|
+
} : {
|
|
38
|
+
x,
|
|
39
|
+
z
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Push a circle out of the arena bounds and every wall. Mutates `p`. */
|
|
43
|
+
function resolveCollisions(p, radius, map) {
|
|
44
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
45
|
+
for (const b of map.walls) pushOutOfBox(p, radius, b);
|
|
46
|
+
const hx = map.width / 2 - radius;
|
|
47
|
+
const hz = map.depth / 2 - radius;
|
|
48
|
+
p.x = clamp(p.x, -hx, hx);
|
|
49
|
+
p.z = clamp(p.z, -hz, hz);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function pushOutOfBox(p, r, b) {
|
|
53
|
+
const cx = clamp(p.x, b.minX, b.maxX);
|
|
54
|
+
const cz = clamp(p.z, b.minZ, b.maxZ);
|
|
55
|
+
const dx = p.x - cx;
|
|
56
|
+
const dz = p.z - cz;
|
|
57
|
+
const d2 = dx * dx + dz * dz;
|
|
58
|
+
if (d2 >= r * r) return;
|
|
59
|
+
if (d2 > 1e-12) {
|
|
60
|
+
const d = Math.sqrt(d2);
|
|
61
|
+
p.x = cx + dx / d * r;
|
|
62
|
+
p.z = cz + dz / d * r;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const left = p.x - b.minX;
|
|
66
|
+
const right = b.maxX - p.x;
|
|
67
|
+
const back = p.z - b.minZ;
|
|
68
|
+
const front = b.maxZ - p.z;
|
|
69
|
+
const m = Math.min(left, right, back, front);
|
|
70
|
+
if (m === left) p.x = b.minX - r;
|
|
71
|
+
else if (m === right) p.x = b.maxX + r;
|
|
72
|
+
else if (m === back) p.z = b.minZ - r;
|
|
73
|
+
else p.z = b.maxZ + r;
|
|
74
|
+
}
|
|
75
|
+
/** Advance a position by one input step. Mutates `p`. */
|
|
76
|
+
function stepMove(p, input, speed, radius, map, dt) {
|
|
77
|
+
const m = normalizeMove(input.mx, input.mz);
|
|
78
|
+
const f = forwardOf(Number.isFinite(input.yaw) ? input.yaw : 0);
|
|
79
|
+
const rx = -f.z;
|
|
80
|
+
const rz = f.x;
|
|
81
|
+
p.x += (rx * m.x + f.x * m.z) * speed * dt;
|
|
82
|
+
p.z += (rz * m.x + f.z * m.z) * speed * dt;
|
|
83
|
+
resolveCollisions(p, radius, map);
|
|
84
|
+
}
|
|
85
|
+
/** Distance along a ray to an AABB, or null if missed (slab method). */
|
|
86
|
+
function rayBox(o, d, b) {
|
|
87
|
+
let tmin = 0;
|
|
88
|
+
let tmax = Infinity;
|
|
89
|
+
for (const [oa, da, lo, hi] of [[
|
|
90
|
+
o.x,
|
|
91
|
+
d.x,
|
|
92
|
+
b.minX,
|
|
93
|
+
b.maxX
|
|
94
|
+
], [
|
|
95
|
+
o.z,
|
|
96
|
+
d.z,
|
|
97
|
+
b.minZ,
|
|
98
|
+
b.maxZ
|
|
99
|
+
]]) {
|
|
100
|
+
if (Math.abs(da) < 1e-12) {
|
|
101
|
+
if (oa < lo || oa > hi) return null;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let t1 = (lo - oa) / da;
|
|
105
|
+
let t2 = (hi - oa) / da;
|
|
106
|
+
if (t1 > t2) [t1, t2] = [t2, t1];
|
|
107
|
+
tmin = Math.max(tmin, t1);
|
|
108
|
+
tmax = Math.min(tmax, t2);
|
|
109
|
+
if (tmin > tmax) return null;
|
|
110
|
+
}
|
|
111
|
+
return tmin;
|
|
112
|
+
}
|
|
113
|
+
/** Distance along a (unit) ray to a circle, or null if missed. */
|
|
114
|
+
function rayCircle(o, d, c, r) {
|
|
115
|
+
const ox = o.x - c.x;
|
|
116
|
+
const oz = o.z - c.z;
|
|
117
|
+
const b = ox * d.x + oz * d.z;
|
|
118
|
+
const cc = ox * ox + oz * oz - r * r;
|
|
119
|
+
const disc = b * b - cc;
|
|
120
|
+
if (disc < 0) return null;
|
|
121
|
+
const s = Math.sqrt(disc);
|
|
122
|
+
const t = -b - s;
|
|
123
|
+
if (t >= 0) return t;
|
|
124
|
+
return -b + s >= 0 ? 0 : null;
|
|
125
|
+
}
|
|
126
|
+
/** Distance to the first wall along the ray, capped at `range`. */
|
|
127
|
+
function wallDistance(o, d, map, range) {
|
|
128
|
+
let best = range;
|
|
129
|
+
for (const b of map.walls) {
|
|
130
|
+
const t = rayBox(o, d, b);
|
|
131
|
+
if (t !== null && t < best) best = t;
|
|
132
|
+
}
|
|
133
|
+
return best;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Hitscan from `origin` along `yaw`. Returns the nearest target hit before any
|
|
137
|
+
* wall and within `range`, plus how far the shot travelled.
|
|
138
|
+
*/
|
|
139
|
+
function castShot(origin, yaw, range, map, targets, radius) {
|
|
140
|
+
const d = forwardOf(yaw);
|
|
141
|
+
const wall = wallDistance(origin, d, map, range);
|
|
142
|
+
let hit = null;
|
|
143
|
+
let dist = wall;
|
|
144
|
+
for (const t of targets) {
|
|
145
|
+
const td = rayCircle(origin, d, t, radius);
|
|
146
|
+
if (td !== null && td < dist) {
|
|
147
|
+
dist = td;
|
|
148
|
+
hit = t.id;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
hit,
|
|
153
|
+
dist
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/typeof.js
|
|
158
|
+
function _typeof(o) {
|
|
159
|
+
"@babel/helpers - typeof";
|
|
160
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
161
|
+
return typeof o;
|
|
162
|
+
} : function(o) {
|
|
163
|
+
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
164
|
+
}, _typeof(o);
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/toPrimitive.js
|
|
168
|
+
function toPrimitive(t, r) {
|
|
169
|
+
if ("object" != _typeof(t) || !t) return t;
|
|
170
|
+
var e = t[Symbol.toPrimitive];
|
|
171
|
+
if (void 0 !== e) {
|
|
172
|
+
var i = e.call(t, r || "default");
|
|
173
|
+
if ("object" != _typeof(i)) return i;
|
|
174
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
175
|
+
}
|
|
176
|
+
return ("string" === r ? String : Number)(t);
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/toPropertyKey.js
|
|
180
|
+
function toPropertyKey(t) {
|
|
181
|
+
var i = toPrimitive(t, "string");
|
|
182
|
+
return "symbol" == _typeof(i) ? i : i + "";
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/defineProperty.js
|
|
186
|
+
function _defineProperty(e, r, t) {
|
|
187
|
+
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
188
|
+
value: t,
|
|
189
|
+
enumerable: !0,
|
|
190
|
+
configurable: !0,
|
|
191
|
+
writable: !0
|
|
192
|
+
}) : e[r] = t, e;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region client/arena-client.ts
|
|
196
|
+
var MAX_STEPS_PER_UPDATE = 4;
|
|
197
|
+
var MAX_PENDING = 120;
|
|
198
|
+
var SNAPSHOT_BUFFER = 20;
|
|
199
|
+
var SNAP_CORRECTION = 2;
|
|
200
|
+
var SMOOTH_HALF_LIFE = .05;
|
|
201
|
+
var PING_EVERY_MS = 2e3;
|
|
202
|
+
function lerpAngle(a, b, t) {
|
|
203
|
+
let d = (b - a) % (Math.PI * 2);
|
|
204
|
+
if (d > Math.PI) d -= Math.PI * 2;
|
|
205
|
+
if (d < -Math.PI) d += Math.PI * 2;
|
|
206
|
+
return a + d * t;
|
|
207
|
+
}
|
|
208
|
+
var ArenaClient = class {
|
|
209
|
+
constructor(opts) {
|
|
210
|
+
_defineProperty(this, "opts", void 0);
|
|
211
|
+
_defineProperty(this, "cfg", null);
|
|
212
|
+
_defineProperty(this, "you", null);
|
|
213
|
+
_defineProperty(this, "phase", "waiting");
|
|
214
|
+
_defineProperty(this, "timeLeft", 0);
|
|
215
|
+
_defineProperty(this, "rttMs", 0);
|
|
216
|
+
_defineProperty(this, "controls", {
|
|
217
|
+
mx: 0,
|
|
218
|
+
mz: 0,
|
|
219
|
+
yaw: 0,
|
|
220
|
+
pitch: 0,
|
|
221
|
+
fire: false
|
|
222
|
+
});
|
|
223
|
+
_defineProperty(this, "self", {
|
|
224
|
+
x: 0,
|
|
225
|
+
z: 0,
|
|
226
|
+
yaw: 0,
|
|
227
|
+
pitch: 0,
|
|
228
|
+
hp: 0,
|
|
229
|
+
alive: true,
|
|
230
|
+
it: false,
|
|
231
|
+
score: 0
|
|
232
|
+
});
|
|
233
|
+
_defineProperty(this, "ws", null);
|
|
234
|
+
_defineProperty(this, "seq", 0);
|
|
235
|
+
_defineProperty(this, "pending", []);
|
|
236
|
+
_defineProperty(this, "acc", 0);
|
|
237
|
+
_defineProperty(this, "snaps", []);
|
|
238
|
+
_defineProperty(this, "clockOffsetMs", null);
|
|
239
|
+
_defineProperty(this, "smooth", {
|
|
240
|
+
x: 0,
|
|
241
|
+
z: 0
|
|
242
|
+
});
|
|
243
|
+
_defineProperty(this, "pingTimer", null);
|
|
244
|
+
_defineProperty(this, "listeners", {
|
|
245
|
+
snapshot: /* @__PURE__ */ new Set(),
|
|
246
|
+
event: /* @__PURE__ */ new Set(),
|
|
247
|
+
end: /* @__PURE__ */ new Set(),
|
|
248
|
+
error: /* @__PURE__ */ new Set(),
|
|
249
|
+
close: /* @__PURE__ */ new Set()
|
|
250
|
+
});
|
|
251
|
+
this.opts = opts;
|
|
252
|
+
}
|
|
253
|
+
on(type, fn) {
|
|
254
|
+
this.listeners[type].add(fn);
|
|
255
|
+
return () => this.listeners[type].delete(fn);
|
|
256
|
+
}
|
|
257
|
+
connect() {
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const url = `${this.opts.wsUrl}?ticket=${encodeURIComponent(this.opts.ticket)}`;
|
|
260
|
+
const ws = new WebSocket(url);
|
|
261
|
+
this.ws = ws;
|
|
262
|
+
let welcomed = false;
|
|
263
|
+
ws.onmessage = (e) => {
|
|
264
|
+
const msg = JSON.parse(String(e.data));
|
|
265
|
+
if (msg.t === "welcome") {
|
|
266
|
+
welcomed = true;
|
|
267
|
+
this.onWelcome(msg);
|
|
268
|
+
resolve(msg);
|
|
269
|
+
} else if (msg.t === "error") {
|
|
270
|
+
this.emit("error", msg);
|
|
271
|
+
if (!welcomed) reject(new Error(msg.message));
|
|
272
|
+
} else this.onMessage(msg);
|
|
273
|
+
};
|
|
274
|
+
ws.onclose = (e) => {
|
|
275
|
+
this.stopPing();
|
|
276
|
+
if (!welcomed) reject(new Error(e.reason || `Connection closed (${e.code})`));
|
|
277
|
+
this.emit("close", {
|
|
278
|
+
code: e.code,
|
|
279
|
+
reason: e.reason
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
close() {
|
|
285
|
+
this.stopPing();
|
|
286
|
+
this.ws?.close(1e3, "leaving");
|
|
287
|
+
}
|
|
288
|
+
/** Call once per rendered frame. Sends fixed-rate inputs and runs prediction. */
|
|
289
|
+
update(dtSec) {
|
|
290
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.cfg) return;
|
|
291
|
+
this.self.yaw = this.controls.yaw;
|
|
292
|
+
this.self.pitch = this.controls.pitch;
|
|
293
|
+
this.acc += dtSec;
|
|
294
|
+
let steps = 0;
|
|
295
|
+
while (this.acc >= .03333333333333333 && steps < MAX_STEPS_PER_UPDATE) {
|
|
296
|
+
this.acc -= INPUT_DT;
|
|
297
|
+
steps++;
|
|
298
|
+
this.sendInput();
|
|
299
|
+
}
|
|
300
|
+
if (steps === MAX_STEPS_PER_UPDATE) this.acc = 0;
|
|
301
|
+
const decay = Math.pow(.5, dtSec / SMOOTH_HALF_LIFE);
|
|
302
|
+
this.smooth.x *= decay;
|
|
303
|
+
this.smooth.z *= decay;
|
|
304
|
+
}
|
|
305
|
+
/** Where to draw yourself this frame (prediction + smoothed corrections). */
|
|
306
|
+
get view() {
|
|
307
|
+
return {
|
|
308
|
+
...this.self,
|
|
309
|
+
x: this.self.x + this.smooth.x,
|
|
310
|
+
z: this.self.z + this.smooth.z
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** Other players, interpolated at the current render time. */
|
|
314
|
+
others(nowMs = performance.now()) {
|
|
315
|
+
const rt = this.renderTick(nowMs);
|
|
316
|
+
if (!this.snaps.length) return [];
|
|
317
|
+
let a = this.snaps[0];
|
|
318
|
+
let b = this.snaps[0];
|
|
319
|
+
for (let i = 0; i < this.snaps.length; i++) {
|
|
320
|
+
if (this.snaps[i].tick <= rt) a = this.snaps[i];
|
|
321
|
+
if (this.snaps[i].tick >= rt) {
|
|
322
|
+
b = this.snaps[i];
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
b = this.snaps[i];
|
|
326
|
+
}
|
|
327
|
+
const t = b.tick === a.tick ? 0 : Math.min(1, Math.max(0, (rt - a.tick) / (b.tick - a.tick)));
|
|
328
|
+
const out = [];
|
|
329
|
+
for (const [id, pb] of b.players) {
|
|
330
|
+
if (id === this.you) continue;
|
|
331
|
+
const pa = a.players.get(id);
|
|
332
|
+
if (!pa || !pa.a || !pb.a || Math.hypot(pb.x - pa.x, pb.z - pa.z) > 4) {
|
|
333
|
+
out.push(pb);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
out.push({
|
|
337
|
+
...pb,
|
|
338
|
+
x: pa.x + (pb.x - pa.x) * t,
|
|
339
|
+
z: pa.z + (pb.z - pa.z) * t,
|
|
340
|
+
y: lerpAngle(pa.y, pb.y, t),
|
|
341
|
+
p: pa.p + (pb.p - pa.p) * t
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
/** Latest authoritative snapshot of every player (scores, names, hp). */
|
|
347
|
+
get roster() {
|
|
348
|
+
const last = this.snaps[this.snaps.length - 1];
|
|
349
|
+
return last ? [...last.players.values()] : [];
|
|
350
|
+
}
|
|
351
|
+
onWelcome(msg) {
|
|
352
|
+
this.cfg = msg.cfg;
|
|
353
|
+
this.you = msg.you;
|
|
354
|
+
this.syncClock(msg.tick);
|
|
355
|
+
this.stopPing();
|
|
356
|
+
this.pingTimer = setInterval(() => this.ws?.send(JSON.stringify({
|
|
357
|
+
t: "ping",
|
|
358
|
+
c: performance.now()
|
|
359
|
+
})), PING_EVERY_MS);
|
|
360
|
+
}
|
|
361
|
+
onMessage(msg) {
|
|
362
|
+
if (msg.t === "s") this.onSnapshot(msg);
|
|
363
|
+
else if (msg.t === "pong") this.rttMs = performance.now() - msg.c;
|
|
364
|
+
else if (msg.t === "end") {
|
|
365
|
+
this.phase = "ended";
|
|
366
|
+
this.emit("end", msg);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
onSnapshot(msg) {
|
|
370
|
+
this.syncClock(msg.k);
|
|
371
|
+
this.phase = msg.ph;
|
|
372
|
+
this.timeLeft = msg.tl;
|
|
373
|
+
this.snaps.push({
|
|
374
|
+
tick: msg.k,
|
|
375
|
+
players: new Map(msg.p.map((p) => [p.id, p]))
|
|
376
|
+
});
|
|
377
|
+
if (this.snaps.length > SNAPSHOT_BUFFER) this.snaps.shift();
|
|
378
|
+
const me = msg.p.find((p) => p.id === this.you);
|
|
379
|
+
if (me && this.cfg) {
|
|
380
|
+
const before = {
|
|
381
|
+
x: this.self.x + this.smooth.x,
|
|
382
|
+
z: this.self.z + this.smooth.z
|
|
383
|
+
};
|
|
384
|
+
this.self.x = me.x;
|
|
385
|
+
this.self.z = me.z;
|
|
386
|
+
this.self.hp = me.hp;
|
|
387
|
+
this.self.alive = me.a;
|
|
388
|
+
this.self.it = !!me.it;
|
|
389
|
+
this.self.score = me.s;
|
|
390
|
+
this.pending = this.pending.filter((i) => i.seq > msg.ack);
|
|
391
|
+
for (const inp of this.pending) this.predict(inp);
|
|
392
|
+
const ex = before.x - this.self.x;
|
|
393
|
+
const ez = before.z - this.self.z;
|
|
394
|
+
if (Math.hypot(ex, ez) > SNAP_CORRECTION) this.smooth = {
|
|
395
|
+
x: 0,
|
|
396
|
+
z: 0
|
|
397
|
+
};
|
|
398
|
+
else this.smooth = {
|
|
399
|
+
x: ex,
|
|
400
|
+
z: ez
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
this.emit("snapshot", msg);
|
|
404
|
+
for (const e of msg.ev ?? []) this.emit("event", e);
|
|
405
|
+
}
|
|
406
|
+
sendInput() {
|
|
407
|
+
const c = this.controls;
|
|
408
|
+
const inp = {
|
|
409
|
+
t: "i",
|
|
410
|
+
seq: ++this.seq,
|
|
411
|
+
mx: c.mx,
|
|
412
|
+
mz: c.mz,
|
|
413
|
+
yaw: c.yaw,
|
|
414
|
+
pitch: c.pitch,
|
|
415
|
+
fire: c.fire,
|
|
416
|
+
vt: Math.round(this.renderTick() * 100) / 100
|
|
417
|
+
};
|
|
418
|
+
this.predict(inp);
|
|
419
|
+
this.pending.push(inp);
|
|
420
|
+
if (this.pending.length > MAX_PENDING) this.pending.shift();
|
|
421
|
+
this.ws.send(JSON.stringify(inp));
|
|
422
|
+
}
|
|
423
|
+
/** Mirror of the server's applyInput movement rules. */
|
|
424
|
+
predict(inp) {
|
|
425
|
+
const cfg = this.cfg;
|
|
426
|
+
if (!this.self.alive || this.phase === "countdown" || this.phase === "ended") return;
|
|
427
|
+
const speed = cfg.moveSpeed * (this.self.it ? cfg.itSpeedMultiplier : 1);
|
|
428
|
+
stepMove(this.self, inp, speed, cfg.playerRadius, cfg.map, INPUT_DT);
|
|
429
|
+
}
|
|
430
|
+
/** Smoothed estimate of the server tick "now", minus the interpolation delay. */
|
|
431
|
+
renderTick(nowMs = performance.now()) {
|
|
432
|
+
if (this.clockOffsetMs === null) return 0;
|
|
433
|
+
return (nowMs + this.clockOffsetMs) * 20 / 1e3 - (this.opts.interpDelayTicks ?? 2);
|
|
434
|
+
}
|
|
435
|
+
syncClock(tick) {
|
|
436
|
+
const sample = tick * 1e3 / 20 - performance.now();
|
|
437
|
+
if (this.clockOffsetMs === null || Math.abs(sample - this.clockOffsetMs) > 1e3) this.clockOffsetMs = sample;
|
|
438
|
+
else this.clockOffsetMs += (sample - this.clockOffsetMs) * .1;
|
|
439
|
+
}
|
|
440
|
+
stopPing() {
|
|
441
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
442
|
+
this.pingTimer = null;
|
|
443
|
+
}
|
|
444
|
+
emit(type, arg) {
|
|
445
|
+
for (const fn of this.listeners[type]) fn(arg);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var DEFAULT_ARENA_CONFIG = {
|
|
449
|
+
mode: "elimination",
|
|
450
|
+
minPlayers: 2,
|
|
451
|
+
maxPlayers: 8,
|
|
452
|
+
durationSec: 180,
|
|
453
|
+
countdownSec: 3,
|
|
454
|
+
moveSpeed: 6,
|
|
455
|
+
playerRadius: .4,
|
|
456
|
+
maxHp: 100,
|
|
457
|
+
damage: 25,
|
|
458
|
+
fireCooldownMs: 300,
|
|
459
|
+
weaponRange: 40,
|
|
460
|
+
respawnSec: 3,
|
|
461
|
+
tagRange: 2,
|
|
462
|
+
tagCooldownSec: 2,
|
|
463
|
+
itSpeedMultiplier: 1.1,
|
|
464
|
+
map: {
|
|
465
|
+
width: 40,
|
|
466
|
+
depth: 40,
|
|
467
|
+
walls: [
|
|
468
|
+
{
|
|
469
|
+
minX: -2,
|
|
470
|
+
minZ: -2,
|
|
471
|
+
maxX: 2,
|
|
472
|
+
maxZ: 2,
|
|
473
|
+
height: 3
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
minX: -12,
|
|
477
|
+
minZ: -12,
|
|
478
|
+
maxX: -8,
|
|
479
|
+
maxZ: -10,
|
|
480
|
+
height: 2
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
minX: 8,
|
|
484
|
+
minZ: 10,
|
|
485
|
+
maxX: 12,
|
|
486
|
+
maxZ: 12,
|
|
487
|
+
height: 2
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
minX: -12,
|
|
491
|
+
minZ: 8,
|
|
492
|
+
maxX: -10,
|
|
493
|
+
maxZ: 14,
|
|
494
|
+
height: 2
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
minX: 10,
|
|
498
|
+
minZ: -14,
|
|
499
|
+
maxX: 12,
|
|
500
|
+
maxZ: -8,
|
|
501
|
+
height: 2
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
minX: -1,
|
|
505
|
+
minZ: -16,
|
|
506
|
+
maxX: 1,
|
|
507
|
+
maxZ: -9,
|
|
508
|
+
height: 2.5
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
minX: -1,
|
|
512
|
+
minZ: 9,
|
|
513
|
+
maxX: 1,
|
|
514
|
+
maxZ: 16,
|
|
515
|
+
height: 2.5
|
|
516
|
+
}
|
|
517
|
+
],
|
|
518
|
+
spawns: [
|
|
519
|
+
{
|
|
520
|
+
x: -16,
|
|
521
|
+
z: -16,
|
|
522
|
+
yaw: -3 * Math.PI / 4
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
x: 16,
|
|
526
|
+
z: 16,
|
|
527
|
+
yaw: Math.PI / 4
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
x: -16,
|
|
531
|
+
z: 16,
|
|
532
|
+
yaw: -Math.PI / 4
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
x: 16,
|
|
536
|
+
z: -16,
|
|
537
|
+
yaw: 3 * Math.PI / 4
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
x: 0,
|
|
541
|
+
z: -18,
|
|
542
|
+
yaw: Math.PI
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
x: 0,
|
|
546
|
+
z: 18,
|
|
547
|
+
yaw: 0
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
x: -18,
|
|
551
|
+
z: 0,
|
|
552
|
+
yaw: -Math.PI / 2
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
x: 18,
|
|
556
|
+
z: 0,
|
|
557
|
+
yaw: Math.PI / 2
|
|
558
|
+
}
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var MAX_WALLS = 200;
|
|
563
|
+
var MAX_SPAWNS = 32;
|
|
564
|
+
function num(v, fallback, min, max) {
|
|
565
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : fallback;
|
|
566
|
+
}
|
|
567
|
+
function resolveMap(input) {
|
|
568
|
+
if (!input || typeof input !== "object") return DEFAULT_ARENA_CONFIG.map;
|
|
569
|
+
const m = input;
|
|
570
|
+
const width = num(m.width, 40, 10, 400);
|
|
571
|
+
const depth = num(m.depth, 40, 10, 400);
|
|
572
|
+
const walls = Array.isArray(m.walls) ? m.walls.slice(0, MAX_WALLS).flatMap((w) => {
|
|
573
|
+
if (!w || typeof w !== "object") return [];
|
|
574
|
+
if (![
|
|
575
|
+
w.minX,
|
|
576
|
+
w.minZ,
|
|
577
|
+
w.maxX,
|
|
578
|
+
w.maxZ
|
|
579
|
+
].every((v) => typeof v === "number" && Number.isFinite(v))) return [];
|
|
580
|
+
if (w.maxX <= w.minX || w.maxZ <= w.minZ) return [];
|
|
581
|
+
return [{
|
|
582
|
+
minX: w.minX,
|
|
583
|
+
minZ: w.minZ,
|
|
584
|
+
maxX: w.maxX,
|
|
585
|
+
maxZ: w.maxZ,
|
|
586
|
+
height: num(w.height, 2, .1, 50)
|
|
587
|
+
}];
|
|
588
|
+
}) : [];
|
|
589
|
+
const spawns = Array.isArray(m.spawns) ? m.spawns.slice(0, MAX_SPAWNS).flatMap((s) => {
|
|
590
|
+
if (!s || typeof s !== "object") return [];
|
|
591
|
+
if (![s.x, s.z].every((v) => typeof v === "number" && Number.isFinite(v))) return [];
|
|
592
|
+
return [{
|
|
593
|
+
x: num(s.x, 0, -width / 2, width / 2),
|
|
594
|
+
z: num(s.z, 0, -depth / 2, depth / 2),
|
|
595
|
+
yaw: num(s.yaw, 0, -10, 10)
|
|
596
|
+
}];
|
|
597
|
+
}) : [];
|
|
598
|
+
return {
|
|
599
|
+
width,
|
|
600
|
+
depth,
|
|
601
|
+
walls,
|
|
602
|
+
spawns: spawns.length ? spawns : [{
|
|
603
|
+
x: 0,
|
|
604
|
+
z: 0,
|
|
605
|
+
yaw: 0
|
|
606
|
+
}]
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
/** Merge untrusted overrides over the defaults, clamping everything. */
|
|
610
|
+
function resolveArenaConfig(overrides) {
|
|
611
|
+
const o = overrides && typeof overrides === "object" ? overrides : {};
|
|
612
|
+
const d = DEFAULT_ARENA_CONFIG;
|
|
613
|
+
const maxPlayers = Math.round(num(o.maxPlayers, d.maxPlayers, 2, 16));
|
|
614
|
+
return {
|
|
615
|
+
mode: o.mode === "tag" || o.mode === "elimination" ? o.mode : d.mode,
|
|
616
|
+
minPlayers: Math.round(num(o.minPlayers, d.minPlayers, 1, maxPlayers)),
|
|
617
|
+
maxPlayers,
|
|
618
|
+
durationSec: num(o.durationSec, d.durationSec, 15, 1800),
|
|
619
|
+
countdownSec: num(o.countdownSec, d.countdownSec, 0, 10),
|
|
620
|
+
moveSpeed: num(o.moveSpeed, d.moveSpeed, 1, 20),
|
|
621
|
+
playerRadius: num(o.playerRadius, d.playerRadius, .2, 2),
|
|
622
|
+
maxHp: num(o.maxHp, d.maxHp, 1, 1e3),
|
|
623
|
+
damage: num(o.damage, d.damage, 1, 1e3),
|
|
624
|
+
fireCooldownMs: num(o.fireCooldownMs, d.fireCooldownMs, 50, 5e3),
|
|
625
|
+
weaponRange: num(o.weaponRange, d.weaponRange, 1, 200),
|
|
626
|
+
respawnSec: num(o.respawnSec, d.respawnSec, 0, 30),
|
|
627
|
+
tagRange: num(o.tagRange, d.tagRange, .5, 10),
|
|
628
|
+
tagCooldownSec: num(o.tagCooldownSec, d.tagCooldownSec, 0, 10),
|
|
629
|
+
itSpeedMultiplier: num(o.itSpeedMultiplier, d.itSpeedMultiplier, .5, 2),
|
|
630
|
+
map: o.map === void 0 ? d.map : resolveMap(o.map)
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
export { ArenaClient, CloseCode, DEFAULT_ARENA_CONFIG, INPUT_DT, INPUT_HZ, PROTOCOL_VERSION, TICK_HZ, castShot, forwardOf, resolveArenaConfig, stepMove };
|