1681-engine 0.5.10 → 0.6.1
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/CHANGELOG.md +61 -1
- package/README.md +12 -6
- package/dist/1681-engine.js +588 -522
- package/dist/ai/behaviorTree.js +198 -0
- package/dist/ai/buildTree.js +54 -0
- package/dist/audio/spatial.js +118 -0
- package/dist/cjs/electron/webPatcher.d.ts +4 -1
- package/dist/cjs/electron/webPatcher.js +9 -2
- package/dist/core/Camera.js +10 -0
- package/dist/core/ObjectFactory.js +23 -21
- package/dist/core/PixelGrid.js +47 -0
- package/dist/core/SharpText.js +72 -0
- package/dist/core/Stage.js +48 -31
- package/dist/core/gridPlacement.js +14 -0
- package/dist/dialogue/DialogueRunner.js +173 -0
- package/dist/dialogue/expr.js +137 -0
- package/dist/dialogue/validateDialogue.js +52 -0
- package/dist/ecs/prefab.js +94 -0
- package/dist/electron/webPatcher.js +16 -10
- package/dist/input/Actions.js +58 -27
- package/dist/input/InputCapture.js +98 -0
- package/dist/input/Keymap.js +32 -26
- package/dist/input/inputTape.js +68 -0
- package/dist/mobile/backButton.js +40 -0
- package/dist/mobile/capacitor.js +48 -0
- package/dist/mobile/screen.js +31 -0
- package/dist/mobile.js +12 -0
- package/dist/nav/flowField.js +58 -0
- package/dist/nav/steering.js +77 -0
- package/dist/net/InterpolationBuffer.js +30 -23
- package/dist/net/LagCompensator.js +100 -0
- package/dist/net/Room.js +122 -43
- package/dist/net/RoomHost.js +13 -4
- package/dist/net/SpatialHash.js +66 -0
- package/dist/types/ai/behaviorTree.d.ts +133 -0
- package/dist/types/ai/behaviorTree.d.ts.map +1 -0
- package/dist/types/ai/buildTree.d.ts +55 -0
- package/dist/types/ai/buildTree.d.ts.map +1 -0
- package/dist/types/audio/spatial.d.ts +107 -0
- package/dist/types/audio/spatial.d.ts.map +1 -0
- package/dist/types/core/Camera.d.ts +8 -0
- package/dist/types/core/Camera.d.ts.map +1 -1
- package/dist/types/core/ObjectFactory.d.ts.map +1 -1
- package/dist/types/core/PixelGrid.d.ts +30 -0
- package/dist/types/core/PixelGrid.d.ts.map +1 -0
- package/dist/types/core/SharpText.d.ts +32 -0
- package/dist/types/core/SharpText.d.ts.map +1 -0
- package/dist/types/core/Stage.d.ts +14 -0
- package/dist/types/core/Stage.d.ts.map +1 -1
- package/dist/types/core/gridPlacement.d.ts +20 -0
- package/dist/types/core/gridPlacement.d.ts.map +1 -0
- package/dist/types/dialogue/DialogueRunner.d.ts +78 -0
- package/dist/types/dialogue/DialogueRunner.d.ts.map +1 -0
- package/dist/types/dialogue/expr.d.ts +12 -0
- package/dist/types/dialogue/expr.d.ts.map +1 -0
- package/dist/types/dialogue/script.d.ts +78 -0
- package/dist/types/dialogue/script.d.ts.map +1 -0
- package/dist/types/dialogue/validateDialogue.d.ts +21 -0
- package/dist/types/dialogue/validateDialogue.d.ts.map +1 -0
- package/dist/types/ecs/prefab.d.ts +84 -0
- package/dist/types/ecs/prefab.d.ts.map +1 -0
- package/dist/types/electron/webPatcher.d.ts +4 -1
- package/dist/types/electron/webPatcher.d.ts.map +1 -1
- package/dist/types/index.d.ts +18 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/input/Actions.d.ts +14 -0
- package/dist/types/input/Actions.d.ts.map +1 -1
- package/dist/types/input/InputCapture.d.ts +53 -0
- package/dist/types/input/InputCapture.d.ts.map +1 -0
- package/dist/types/input/Keymap.d.ts +12 -6
- package/dist/types/input/Keymap.d.ts.map +1 -1
- package/dist/types/input/inputTape.d.ts +66 -0
- package/dist/types/input/inputTape.d.ts.map +1 -0
- package/dist/types/mobile/backButton.d.ts +30 -0
- package/dist/types/mobile/backButton.d.ts.map +1 -0
- package/dist/types/mobile/capacitor.d.ts +62 -0
- package/dist/types/mobile/capacitor.d.ts.map +1 -0
- package/dist/types/mobile/index.d.ts +4 -0
- package/dist/types/mobile/index.d.ts.map +1 -0
- package/dist/types/mobile/screen.d.ts +27 -0
- package/dist/types/mobile/screen.d.ts.map +1 -0
- package/dist/types/nav/flowField.d.ts +31 -0
- package/dist/types/nav/flowField.d.ts.map +1 -0
- package/dist/types/nav/steering.d.ts +47 -0
- package/dist/types/nav/steering.d.ts.map +1 -0
- package/dist/types/net/InterpolationBuffer.d.ts +5 -0
- package/dist/types/net/InterpolationBuffer.d.ts.map +1 -1
- package/dist/types/net/LagCompensator.d.ts +70 -0
- package/dist/types/net/LagCompensator.d.ts.map +1 -0
- package/dist/types/net/Room.d.ts +72 -3
- package/dist/types/net/Room.d.ts.map +1 -1
- package/dist/types/net/RoomHost.d.ts +8 -3
- package/dist/types/net/RoomHost.d.ts.map +1 -1
- package/dist/types/net/SpatialHash.d.ts +25 -0
- package/dist/types/net/SpatialHash.d.ts.map +1 -0
- package/dist/types/ui/ScrollView.d.ts.map +1 -1
- package/dist/ui/ScrollView.js +10 -8
- package/package.json +5 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const M = () => ({ x: 0, y: 0 });
|
|
2
|
+
function h(o, x, t, y) {
|
|
3
|
+
const n = Math.hypot(o, x);
|
|
4
|
+
return n === 0 ? { x: -y.vx, y: -y.vy } : { x: o / n * t - y.vx, y: x / n * t - y.vy };
|
|
5
|
+
}
|
|
6
|
+
function b(o, x, t, y) {
|
|
7
|
+
return h(x - o.x, t - o.y, y, o);
|
|
8
|
+
}
|
|
9
|
+
function I(o, x, t, y, n = 1 / 0) {
|
|
10
|
+
const r = o.x - x, f = o.y - t;
|
|
11
|
+
return r * r + f * f > n * n ? M() : h(r, f, y, o);
|
|
12
|
+
}
|
|
13
|
+
function w(o, x, t, y, n) {
|
|
14
|
+
const r = x - o.x, f = t - o.y, c = Math.hypot(r, f), v = c < n ? y * (c / n) : y;
|
|
15
|
+
return h(r, f, v, o);
|
|
16
|
+
}
|
|
17
|
+
function A(o, x, t, y) {
|
|
18
|
+
let n = 0, r = 0;
|
|
19
|
+
for (const f of x) {
|
|
20
|
+
if (f === o) continue;
|
|
21
|
+
let c = o.x - f.x, v = o.y - f.y, i = Math.hypot(c, v);
|
|
22
|
+
if (i >= t) continue;
|
|
23
|
+
i === 0 ? (c = o.vx - f.vx || 1, v = o.vy - f.vy, i = Math.hypot(c, v), c /= i, v /= i, i = 0) : (c /= i, v /= i);
|
|
24
|
+
const s = (1 - i / t) * y;
|
|
25
|
+
n += c * s, r += v * s;
|
|
26
|
+
}
|
|
27
|
+
return { x: n, y: r };
|
|
28
|
+
}
|
|
29
|
+
function k(o, x, t) {
|
|
30
|
+
let y = 0, n = 0, r = 0;
|
|
31
|
+
for (const f of x)
|
|
32
|
+
f === o || (f.x - o.x) ** 2 + (f.y - o.y) ** 2 > t * t || (y += f.vx, n += f.vy, r++);
|
|
33
|
+
return r === 0 ? M() : { x: y / r - o.vx, y: n / r - o.vy };
|
|
34
|
+
}
|
|
35
|
+
function E(o, x, t, y) {
|
|
36
|
+
let n = 0, r = 0, f = 0;
|
|
37
|
+
for (const c of x)
|
|
38
|
+
c === o || (c.x - o.x) ** 2 + (c.y - o.y) ** 2 > t * t || (n += c.x, r += c.y, f++);
|
|
39
|
+
return f === 0 ? M() : b(o, n / f, r / f, y);
|
|
40
|
+
}
|
|
41
|
+
function O(o, x, t, y) {
|
|
42
|
+
const n = Math.floor(o.x / t), r = Math.floor(o.y / t), f = x.stepAt(n, r);
|
|
43
|
+
if (f) return h(f[0], f[1], y, o);
|
|
44
|
+
if (x.costAt(n, r) === 0) return { x: -o.vx, y: -o.vy };
|
|
45
|
+
let c = 1 / 0, v = 0, i = 0;
|
|
46
|
+
for (let s = 1; s <= 2 && c === 1 / 0; s++)
|
|
47
|
+
for (let u = -s; u <= s; u++)
|
|
48
|
+
for (let l = -s; l <= s; l++) {
|
|
49
|
+
if (Math.max(Math.abs(l), Math.abs(u)) !== s) continue;
|
|
50
|
+
const p = x.costAt(n + l, r + u);
|
|
51
|
+
p < c && (c = p, v = l, i = u);
|
|
52
|
+
}
|
|
53
|
+
return c === 1 / 0 ? { x: -o.vx, y: -o.vy } : b(o, (n + v + 0.5) * t, (r + i + 0.5) * t, y);
|
|
54
|
+
}
|
|
55
|
+
function Z(o, ...x) {
|
|
56
|
+
let t = 0, y = 0;
|
|
57
|
+
for (const [r, f] of x)
|
|
58
|
+
t += r.x * f, y += r.y * f;
|
|
59
|
+
const n = Math.hypot(t, y);
|
|
60
|
+
return n > o && (t = t / n * o, y = y / n * o), { x: t, y };
|
|
61
|
+
}
|
|
62
|
+
function j(o, x, t, y) {
|
|
63
|
+
o.vx += x.x * y, o.vy += x.y * y;
|
|
64
|
+
const n = Math.hypot(o.vx, o.vy);
|
|
65
|
+
n > t && (o.vx = o.vx / n * t, o.vy = o.vy / n * t), o.x += o.vx * y, o.y += o.vy * y;
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
k as alignment,
|
|
69
|
+
j as applySteering,
|
|
70
|
+
w as arrive,
|
|
71
|
+
E as cohesion,
|
|
72
|
+
Z as combineSteering,
|
|
73
|
+
I as flee,
|
|
74
|
+
O as followFlow,
|
|
75
|
+
b as seek,
|
|
76
|
+
A as separation
|
|
77
|
+
};
|
|
@@ -1,55 +1,62 @@
|
|
|
1
1
|
class x {
|
|
2
|
-
constructor(
|
|
3
|
-
this.delayMs =
|
|
2
|
+
constructor(e = 100, a = 120, t = 30) {
|
|
3
|
+
this.delayMs = e, this.maxExtrapolateMs = a, this.keep = t;
|
|
4
4
|
}
|
|
5
5
|
delayMs;
|
|
6
6
|
maxExtrapolateMs;
|
|
7
7
|
keep;
|
|
8
8
|
samples = /* @__PURE__ */ new Map();
|
|
9
9
|
/** Records every body of a snapshot taken at `time`. */
|
|
10
|
-
push(
|
|
11
|
-
for (const [t, n,
|
|
10
|
+
push(e, a) {
|
|
11
|
+
for (const [t, n, r, h, m, p, c] of a) {
|
|
12
12
|
let i = this.samples.get(t);
|
|
13
13
|
i || (i = [], this.samples.set(t, i));
|
|
14
14
|
const l = i[i.length - 1];
|
|
15
|
-
l &&
|
|
15
|
+
l && e <= l.time || (i.push({ time: e, x: n, y: r, aim: h, anim: m, hp: p, flags: c }), i.length > this.keep && i.splice(0, i.length - this.keep));
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The server time bodies are drawn at when it is `now` (`serverNow()`): what a shot should carry, so a server with
|
|
20
|
+
* lag compensation judges it against the picture the player aimed at.
|
|
21
|
+
*/
|
|
22
|
+
viewTime(e) {
|
|
23
|
+
return e - this.delayMs;
|
|
24
|
+
}
|
|
18
25
|
/** Forgets a body (it left view). */
|
|
19
|
-
remove(
|
|
20
|
-
this.samples.delete(
|
|
26
|
+
remove(e) {
|
|
27
|
+
this.samples.delete(e);
|
|
21
28
|
}
|
|
22
|
-
has(
|
|
23
|
-
return this.samples.has(
|
|
29
|
+
has(e) {
|
|
30
|
+
return this.samples.has(e);
|
|
24
31
|
}
|
|
25
32
|
ids() {
|
|
26
33
|
return this.samples.keys();
|
|
27
34
|
}
|
|
28
35
|
/** Where body `id` should be drawn at server time `now`, or null if nothing is known of it. */
|
|
29
|
-
sample(
|
|
30
|
-
const t = this.samples.get(
|
|
36
|
+
sample(e, a) {
|
|
37
|
+
const t = this.samples.get(e);
|
|
31
38
|
if (!t || t.length === 0) return null;
|
|
32
|
-
const n = a - this.delayMs,
|
|
33
|
-
if (n <=
|
|
39
|
+
const n = a - this.delayMs, r = t[0];
|
|
40
|
+
if (n <= r.time || t.length === 1) return g(r);
|
|
34
41
|
for (let i = t.length - 1; i > 0; i--) {
|
|
35
|
-
const l = t[i - 1],
|
|
36
|
-
if (n >= l.time && n <=
|
|
37
|
-
const u =
|
|
38
|
-
return f(l,
|
|
42
|
+
const l = t[i - 1], o = t[i];
|
|
43
|
+
if (n >= l.time && n <= o.time) {
|
|
44
|
+
const u = o.time === l.time ? 1 : (n - l.time) / (o.time - l.time);
|
|
45
|
+
return f(l, o, u);
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
const h = t[t.length - 2], m = t[t.length - 1], p = Math.min(n - m.time, this.maxExtrapolateMs), c = m.time === h.time ? 1 : 1 + p / (m.time - h.time);
|
|
42
49
|
return f(h, m, c);
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
|
-
function g(
|
|
46
|
-
return { x:
|
|
52
|
+
function g(s) {
|
|
53
|
+
return { x: s.x, y: s.y, aim: s.aim, anim: s.anim, hp: s.hp, flags: s.flags };
|
|
47
54
|
}
|
|
48
|
-
function f(
|
|
49
|
-
let t =
|
|
55
|
+
function f(s, e, a) {
|
|
56
|
+
let t = e.aim - s.aim;
|
|
50
57
|
t > Math.PI && (t -= Math.PI * 2), t < -Math.PI && (t += Math.PI * 2);
|
|
51
|
-
const n = a < 1 ?
|
|
52
|
-
return { x:
|
|
58
|
+
const n = a < 1 ? s : e;
|
|
59
|
+
return { x: s.x + (e.x - s.x) * a, y: s.y + (e.y - s.y) * a, aim: s.aim + t * a, anim: n.anim, hp: n.hp, flags: n.flags };
|
|
53
60
|
}
|
|
54
61
|
export {
|
|
55
62
|
x as InterpolationBuffer
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
class k {
|
|
2
|
+
/** `keepMs`: how much history is kept; at least the longest rewind allowed plus a snapshot interval. */
|
|
3
|
+
constructor(s = 1e3) {
|
|
4
|
+
this.keepMs = s;
|
|
5
|
+
}
|
|
6
|
+
keepMs;
|
|
7
|
+
frames = [];
|
|
8
|
+
/** The latest recorded time, or -Infinity before any. */
|
|
9
|
+
get newest() {
|
|
10
|
+
return this.frames.length ? this.frames[this.frames.length - 1].time : -1 / 0;
|
|
11
|
+
}
|
|
12
|
+
get oldest() {
|
|
13
|
+
return this.frames.length ? this.frames[0].time : -1 / 0;
|
|
14
|
+
}
|
|
15
|
+
/** Records every body's position at `time`. A time not after the last one recorded replaces that frame. */
|
|
16
|
+
record(s, t) {
|
|
17
|
+
const e = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const r of t) e.set(r.id, [r.x, r.y]);
|
|
19
|
+
const n = this.frames[this.frames.length - 1];
|
|
20
|
+
n && s <= n.time ? n.pos = e : this.frames.push({ time: s, pos: e });
|
|
21
|
+
const o = s - this.keepMs;
|
|
22
|
+
for (; this.frames.length > 2 && this.frames[1].time <= o; ) this.frames.shift();
|
|
23
|
+
}
|
|
24
|
+
/** Forgets a body everywhere in the history (it left the game, not just the view). */
|
|
25
|
+
forget(s) {
|
|
26
|
+
for (const t of this.frames) t.pos.delete(s);
|
|
27
|
+
}
|
|
28
|
+
clear() {
|
|
29
|
+
this.frames.length = 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A time a client claims it saw, kept within `[now - maxRewindMs, now]`: the window a rewind is honoured in. A
|
|
33
|
+
* claim that is not a finite number is taken as `now` (no rewind).
|
|
34
|
+
*/
|
|
35
|
+
static clampTime(s, t, e) {
|
|
36
|
+
return typeof s != "number" || !Number.isFinite(s) ? t : Math.min(t, Math.max(t - e, s));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Where body `id` was at `time`, blended between the frames around it as `InterpolationBuffer` blends; null when
|
|
40
|
+
* the history has no frame of it. Before the oldest frame it is where it was then; after the newest, where it is.
|
|
41
|
+
*/
|
|
42
|
+
at(s, t) {
|
|
43
|
+
const e = this.around(t);
|
|
44
|
+
return e ? b(e[0], e[1], e[2], s) : null;
|
|
45
|
+
}
|
|
46
|
+
/** Every body in the history at `time`. */
|
|
47
|
+
all(s) {
|
|
48
|
+
const t = this.around(s);
|
|
49
|
+
if (!t) return [];
|
|
50
|
+
const [e, n, o] = t, r = /* @__PURE__ */ new Set([...e.pos.keys(), ...n.pos.keys()]), i = [];
|
|
51
|
+
for (const a of r) {
|
|
52
|
+
const h = b(e, n, o, a);
|
|
53
|
+
i.push({ id: a, x: h.x, y: h.y });
|
|
54
|
+
}
|
|
55
|
+
return i;
|
|
56
|
+
}
|
|
57
|
+
/** The bodies whose centre was within `radius` of (x, y) at `time`, nearest first; `except` (the shooter) left out. */
|
|
58
|
+
near(s, t, e, n, o) {
|
|
59
|
+
return this.all(s).filter((i) => i.id !== o && (i.x - t) ** 2 + (i.y - e) ** 2 <= n * n).sort((i, a) => (i.x - t) ** 2 + (i.y - e) ** 2 - ((a.x - t) ** 2 + (a.y - e) ** 2));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A hitscan at `time`: the bodies a ray from (x, y) along (dx, dy) passed within `radius` of, before `range` px, in
|
|
63
|
+
* the order it met them. `except` (the shooter) is left out. Walls are the game's to check against the first hit.
|
|
64
|
+
*/
|
|
65
|
+
alongRay(s, t, e, n, o, r, i, a) {
|
|
66
|
+
const h = Math.hypot(n, o);
|
|
67
|
+
if (h === 0) return [];
|
|
68
|
+
const u = n / h, m = o / h, p = [];
|
|
69
|
+
for (const f of this.all(s)) {
|
|
70
|
+
if (f.id === a) continue;
|
|
71
|
+
const c = (f.x - t) * u + (f.y - e) * m, g = t + u * c - f.x, y = e + m * c - f.y, x = g * g + y * y;
|
|
72
|
+
if (x > i * i) continue;
|
|
73
|
+
const M = c - Math.sqrt(i * i - x);
|
|
74
|
+
c < -i || M > r || p.push({ id: f.id, x: f.x, y: f.y, along: Math.max(0, M) });
|
|
75
|
+
}
|
|
76
|
+
return p.sort((f, c) => f.along - c.along);
|
|
77
|
+
}
|
|
78
|
+
/** The two frames around `time` and how far between them it is (0 at the first, 1 at the second). */
|
|
79
|
+
around(s) {
|
|
80
|
+
const t = this.frames;
|
|
81
|
+
if (t.length === 0) return null;
|
|
82
|
+
if (s <= t[0].time) return [t[0], t[0], 0];
|
|
83
|
+
const e = t[t.length - 1];
|
|
84
|
+
if (s >= e.time) return [e, e, 0];
|
|
85
|
+
for (let n = t.length - 1; n > 0; n--) {
|
|
86
|
+
const o = t[n - 1], r = t[n];
|
|
87
|
+
if (s >= o.time && s <= r.time) return [o, r, r.time === o.time ? 1 : (s - o.time) / (r.time - o.time)];
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function b(l, s, t, e) {
|
|
93
|
+
const n = l.pos.get(e), o = s.pos.get(e);
|
|
94
|
+
if (n && o) return { x: n[0] + (o[0] - n[0]) * t, y: n[1] + (o[1] - n[1]) * t };
|
|
95
|
+
const r = n ?? o;
|
|
96
|
+
return r ? { x: r[0], y: r[1] } : null;
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
k as LagCompensator
|
|
100
|
+
};
|
package/dist/net/Room.js
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { LagCompensator as f } from "./LagCompensator.js";
|
|
2
|
+
import { createMover as m, stepMover as p } from "./movement.js";
|
|
3
|
+
import { moverToWire as y } from "./protocol.js";
|
|
4
|
+
import { SpatialHash as b } from "./SpatialHash.js";
|
|
5
|
+
const g = 1 << 24;
|
|
6
|
+
class M {
|
|
4
7
|
constructor(e) {
|
|
5
8
|
this.options = e, this.maxQueue = e.maxQueue ?? 30, this.maxSteps = e.maxStepsPerTick ?? 3, this.backlog = e.backlog ?? 4;
|
|
9
|
+
const i = e.interest;
|
|
10
|
+
if (i && !(i.radius > 0)) throw new Error(`Room: interest.radius must be above 0 (got ${i.radius})`);
|
|
11
|
+
this.hash = i ? new b(i.radius) : null, this.rewindMs = Math.max(0, e.rewindMs ?? 0), this.history = this.rewindMs > 0 ? new f(this.rewindMs + 200) : null;
|
|
6
12
|
}
|
|
7
13
|
options;
|
|
8
14
|
players = /* @__PURE__ */ new Map();
|
|
15
|
+
/** The bodies the server moves (`addBody`). */
|
|
16
|
+
bodies = /* @__PURE__ */ new Map();
|
|
9
17
|
tick = 0;
|
|
18
|
+
/** Past positions for judging shots against what a client saw, when the room was made with `rewindMs`. */
|
|
19
|
+
history;
|
|
20
|
+
/** How far back a client's claimed view time is honoured (`rewindMs`, 0 when off). */
|
|
21
|
+
rewindMs;
|
|
10
22
|
known = /* @__PURE__ */ new Map();
|
|
23
|
+
hash;
|
|
24
|
+
nearby = [];
|
|
11
25
|
maxQueue;
|
|
12
26
|
maxSteps;
|
|
13
27
|
backlog;
|
|
28
|
+
nextBodyId = g;
|
|
14
29
|
join(e, i) {
|
|
15
30
|
if (this.players.has(e)) throw new Error(`Room: player ${e} has already joined`);
|
|
16
|
-
|
|
31
|
+
if (this.bodies.has(e)) throw new Error(`Room: id ${e} is a body's`);
|
|
32
|
+
const t = this.options.spawn(e, i), s = {
|
|
33
|
+
kind: "player",
|
|
17
34
|
id: e,
|
|
18
|
-
mover:
|
|
35
|
+
mover: m(t.x, t.y),
|
|
19
36
|
data: i,
|
|
20
37
|
params: { ...this.options.params },
|
|
21
38
|
ack: -1,
|
|
@@ -26,66 +43,128 @@ class g {
|
|
|
26
43
|
flags: 0,
|
|
27
44
|
strikes: 0
|
|
28
45
|
};
|
|
29
|
-
return this.players.set(e,
|
|
46
|
+
return this.players.set(e, s), this.known.set(e, /* @__PURE__ */ new Set()), this.hash?.set(e, t.x, t.y), s;
|
|
30
47
|
}
|
|
31
48
|
leave(e) {
|
|
32
|
-
this.players.delete(e), this.known.delete(e);
|
|
49
|
+
this.players.delete(e), this.known.delete(e), this.hash?.delete(e), this.history?.forget(e);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Records where every body is now, at server time `time`, into `history`; `RoomHost` calls it as each round of
|
|
53
|
+
* snapshots goes out, so the history holds exactly the positions clients interpolate between.
|
|
54
|
+
*/
|
|
55
|
+
record(e) {
|
|
56
|
+
if (!this.history) return;
|
|
57
|
+
const i = [...this.players.values()].map((t) => ({ id: t.id, x: t.mover.x, y: t.mover.y }));
|
|
58
|
+
for (const t of this.bodies.values()) i.push({ id: t.id, x: t.x, y: t.y });
|
|
59
|
+
this.history.record(e, i);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Adds a body the server moves, at (x, y), with the game's `data` (what players are told it looks like). Its id
|
|
63
|
+
* is `id`, or the next from `BODY_ID_BASE` up; an id a player or body has is refused.
|
|
64
|
+
*/
|
|
65
|
+
addBody(e, i, t, s) {
|
|
66
|
+
let o = s;
|
|
67
|
+
if (o === void 0) {
|
|
68
|
+
for (; this.bodies.has(this.nextBodyId) || this.players.has(this.nextBodyId); ) this.nextBodyId++;
|
|
69
|
+
o = this.nextBodyId++;
|
|
70
|
+
} else if (this.bodies.has(o) || this.players.has(o)) throw new Error(`Room: id ${o} is taken`);
|
|
71
|
+
if (!Number.isInteger(o) || o < 0 || o > 2147483647) throw new Error(`Room: a body id must fit an i32 (got ${o})`);
|
|
72
|
+
const a = { kind: "body", id: o, x: i, y: t, aim: 0, anim: 0, hp: 0, flags: 0, data: e };
|
|
73
|
+
return this.bodies.set(o, a), this.hash?.set(o, i, t), a;
|
|
74
|
+
}
|
|
75
|
+
/** Takes a body out: players who saw it get it in `rm` with their next snapshot. */
|
|
76
|
+
removeBody(e) {
|
|
77
|
+
this.bodies.delete(e) && (this.hash?.delete(e), this.history?.forget(e));
|
|
33
78
|
}
|
|
34
79
|
/**
|
|
35
80
|
* Input steps `seq`, `seq + 1`, … from a player. Steps already received are skipped (a resend), any step with
|
|
36
81
|
* numbers no client sends refuses the whole message, and steps past a full queue are dropped. Returns false when
|
|
37
82
|
* something was refused.
|
|
38
83
|
*/
|
|
39
|
-
input(e, i,
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
42
|
-
if (!Number.isInteger(i) || i < 0 || !Array.isArray(
|
|
43
|
-
return
|
|
44
|
-
for (const
|
|
45
|
-
if (!Array.isArray(
|
|
46
|
-
return
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
return
|
|
50
|
-
for (let
|
|
51
|
-
let
|
|
52
|
-
for (let
|
|
53
|
-
if (!(i +
|
|
54
|
-
if (
|
|
55
|
-
|
|
84
|
+
input(e, i, t) {
|
|
85
|
+
const s = this.players.get(e);
|
|
86
|
+
if (!s) return !1;
|
|
87
|
+
if (!Number.isInteger(i) || i < 0 || !Array.isArray(t) || t.length > this.maxQueue)
|
|
88
|
+
return s.strikes++, !1;
|
|
89
|
+
for (const r of t)
|
|
90
|
+
if (!Array.isArray(r) || r.length !== 3 || !r.every((h) => Number.isFinite(h)) || Math.abs(r[0]) > 1 || Math.abs(r[1]) > 1 || !Number.isInteger(r[2]))
|
|
91
|
+
return s.strikes++, !1;
|
|
92
|
+
const o = s.ack + 1 + s.queue.length, a = i - o;
|
|
93
|
+
if (a > this.maxQueue)
|
|
94
|
+
return s.strikes++, !1;
|
|
95
|
+
for (let r = 0; r < a; r++) s.queue.push([0, 0, 0]);
|
|
96
|
+
let u = !0;
|
|
97
|
+
for (let r = 0; r < t.length; r++)
|
|
98
|
+
if (!(i + r < o)) {
|
|
99
|
+
if (s.queue.length >= this.maxQueue) {
|
|
100
|
+
u = !1, s.strikes++;
|
|
56
101
|
break;
|
|
57
102
|
}
|
|
58
|
-
|
|
103
|
+
s.queue.push([t[r][0], t[r][1], t[r][2]]);
|
|
59
104
|
}
|
|
60
|
-
return
|
|
105
|
+
return u;
|
|
61
106
|
}
|
|
62
107
|
/** One simulation step for everyone. */
|
|
63
108
|
step() {
|
|
64
109
|
this.tick++;
|
|
65
110
|
for (const e of this.players.values()) {
|
|
66
|
-
const i = e.queue.length;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const n = e.queue.shift();
|
|
71
|
-
p(e.mover, n, e.params, this.options.grid), e.ack++, this.options.onStep?.(e, n);
|
|
111
|
+
const i = e.queue.length, t = i > this.backlog ? Math.min(this.maxSteps, i - this.backlog + 1) : Math.min(1, i);
|
|
112
|
+
for (let s = 0; s < t; s++) {
|
|
113
|
+
const o = e.queue.shift();
|
|
114
|
+
p(e.mover, o, e.params, this.options.grid), e.ack++, this.options.onStep?.(e, o);
|
|
72
115
|
}
|
|
116
|
+
this.hash?.set(e.id, e.mover.x, e.mover.y);
|
|
73
117
|
}
|
|
118
|
+
if (this.hash) for (const e of this.bodies.values()) this.hash.set(e.id, e.x, e.y);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Moves a player by hand (a teleport, a respawn), and files it in the interest grid at once. Writing `mover.x/y`
|
|
122
|
+
* directly also works; the room then finds it at its new place from the next step.
|
|
123
|
+
*/
|
|
124
|
+
place(e, i, t) {
|
|
125
|
+
const s = this.players.get(e), o = this.bodies.get(e);
|
|
126
|
+
if (s)
|
|
127
|
+
s.mover.x = i, s.mover.y = t;
|
|
128
|
+
else if (o)
|
|
129
|
+
o.x = i, o.y = t;
|
|
130
|
+
else return;
|
|
131
|
+
this.hash?.set(e, i, t);
|
|
74
132
|
}
|
|
75
133
|
/** The snapshot player `id` should get now. `time` is the server's wall clock (ms). */
|
|
76
134
|
snapshotFor(e, i) {
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
79
|
-
const
|
|
80
|
-
for (const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
135
|
+
const t = this.players.get(e), s = this.known.get(e);
|
|
136
|
+
if (!t || !s) return null;
|
|
137
|
+
const o = [], a = [], u = /* @__PURE__ */ new Set();
|
|
138
|
+
for (const n of this.visibleTo(t, s)) {
|
|
139
|
+
u.add(n.id);
|
|
140
|
+
const c = d(n);
|
|
141
|
+
o.push([n.id, c.x, c.y, n.aim, n.anim, n.hp, n.flags]), s.has(n.id) || (s.add(n.id), a.push({ id: n.id, data: n.data }));
|
|
142
|
+
}
|
|
143
|
+
const r = [];
|
|
144
|
+
for (const n of s)
|
|
145
|
+
u.has(n) || (s.delete(n), r.push(n));
|
|
146
|
+
const h = { t: "snap", tick: this.tick, time: i, ack: t.ack, me: y(t.mover), up: o };
|
|
147
|
+
return a.length && (h.sp = a), r.length && (h.rm = r), h;
|
|
148
|
+
}
|
|
149
|
+
/** What `me` is sent: itself always; with interest, the rest in range (a known one until past the margin). */
|
|
150
|
+
visibleTo(e, i) {
|
|
151
|
+
const t = this.options.interest;
|
|
152
|
+
if (!t || !this.hash) return [...this.players.values(), ...this.bodies.values()];
|
|
153
|
+
const s = t.radius, o = s + (t.leaveMargin ?? s / 4), a = this.nearby;
|
|
154
|
+
a.length = 0, this.hash.near(e.mover.x, e.mover.y, o, a);
|
|
155
|
+
const u = [e];
|
|
156
|
+
for (const r of a) {
|
|
157
|
+
if (r === e.id) continue;
|
|
158
|
+
const h = this.players.get(r) ?? this.bodies.get(r);
|
|
159
|
+
if (!h) continue;
|
|
160
|
+
const n = d(h);
|
|
161
|
+
(n.x - e.mover.x) ** 2 + (n.y - e.mover.y) ** 2 > s * s && !i.has(r) || t.canSee && !t.canSee(e, h) || u.push(h);
|
|
162
|
+
}
|
|
163
|
+
return u.sort((r, h) => r.id - h.id);
|
|
87
164
|
}
|
|
88
165
|
}
|
|
166
|
+
const d = (l) => l.kind === "player" ? l.mover : l;
|
|
89
167
|
export {
|
|
90
|
-
g as
|
|
168
|
+
g as BODY_ID_BASE,
|
|
169
|
+
M as Room
|
|
91
170
|
};
|
package/dist/net/RoomHost.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { LagCompensator as n } from "./LagCompensator.js";
|
|
2
|
+
import { encodeSnapshotFrame as r } from "./snapshotCodec.js";
|
|
3
|
+
class c {
|
|
3
4
|
constructor(t, s = {}) {
|
|
4
5
|
this.room = t, this.options = s, this.stepMs = 1e3 / (s.stepRate ?? 60), this.snapEvery = Math.max(1, Math.round((s.stepRate ?? 60) / (s.snapRate ?? 20))), this.now = s.now ?? Date.now;
|
|
5
6
|
}
|
|
@@ -64,7 +65,7 @@ class p {
|
|
|
64
65
|
/** Sends one of the game's own messages to a player. */
|
|
65
66
|
sendTo(t, s) {
|
|
66
67
|
const e = this.sockets.get(t);
|
|
67
|
-
e && (s.t === "snap" && s.up?.length ? e.send(
|
|
68
|
+
e && (s.t === "snap" && s.up?.length ? e.send(r(s)) : e.send(JSON.stringify(s)));
|
|
68
69
|
}
|
|
69
70
|
/** Starts stepping on a timer. Steps are counted by the clock, so a late timer runs the steps it owes. */
|
|
70
71
|
start() {
|
|
@@ -81,11 +82,19 @@ class p {
|
|
|
81
82
|
stepOnce() {
|
|
82
83
|
if (this.room.step(), this.room.tick % this.snapEvery !== 0) return;
|
|
83
84
|
const t = this.now();
|
|
85
|
+
this.room.record(t);
|
|
84
86
|
for (const s of this.joined) {
|
|
85
87
|
const e = this.room.snapshotFor(s, t);
|
|
86
88
|
e && this.sendTo(s, e);
|
|
87
89
|
}
|
|
88
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The server time a client's claimed view time (`InterpolationBuffer.viewTime`, sent with a shot) is judged at:
|
|
93
|
+
* kept within the room's `rewindMs` of now, so no claim reaches further back. `now` with no history kept.
|
|
94
|
+
*/
|
|
95
|
+
rewindTime(t) {
|
|
96
|
+
return n.clampTime(t, this.now(), this.room.rewindMs);
|
|
97
|
+
}
|
|
89
98
|
strike(t) {
|
|
90
99
|
const s = this.room.players.get(t);
|
|
91
100
|
s && (s.strikes++, this.checkStrikes(t));
|
|
@@ -96,5 +105,5 @@ class p {
|
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
export {
|
|
99
|
-
|
|
108
|
+
c as RoomHost
|
|
100
109
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const z = (a, t) => (a + 32768) * 65536 + (t + 32768);
|
|
2
|
+
class M {
|
|
3
|
+
/** `cellSize`: world px per cell. About the radius queries ask for keeps a query to a 3×3 block. */
|
|
4
|
+
constructor(t) {
|
|
5
|
+
if (this.cellSize = t, !(t > 0)) throw new Error(`SpatialHash: cellSize must be above 0 (got ${t})`);
|
|
6
|
+
}
|
|
7
|
+
cellSize;
|
|
8
|
+
cells = /* @__PURE__ */ new Map();
|
|
9
|
+
entries = /* @__PURE__ */ new Map();
|
|
10
|
+
get size() {
|
|
11
|
+
return this.entries.size;
|
|
12
|
+
}
|
|
13
|
+
has(t) {
|
|
14
|
+
return this.entries.has(t);
|
|
15
|
+
}
|
|
16
|
+
/** Puts `id` at (x, y), or moves it there. */
|
|
17
|
+
set(t, s, e) {
|
|
18
|
+
const l = z(Math.floor(s / this.cellSize), Math.floor(e / this.cellSize)), i = this.entries.get(t);
|
|
19
|
+
if (i) {
|
|
20
|
+
if (i.x = s, i.y = e, i.cell === l) return;
|
|
21
|
+
this.unlink(t, i.cell), i.cell = l;
|
|
22
|
+
} else this.entries.set(t, { x: s, y: e, cell: l });
|
|
23
|
+
let n = this.cells.get(l);
|
|
24
|
+
n || this.cells.set(l, n = /* @__PURE__ */ new Set()), n.add(t);
|
|
25
|
+
}
|
|
26
|
+
delete(t) {
|
|
27
|
+
const s = this.entries.get(t);
|
|
28
|
+
s && (this.unlink(t, s.cell), this.entries.delete(t));
|
|
29
|
+
}
|
|
30
|
+
clear() {
|
|
31
|
+
this.cells.clear(), this.entries.clear();
|
|
32
|
+
}
|
|
33
|
+
/** The ids within `radius` of (x, y), edge included, pushed onto `out` in no set order. */
|
|
34
|
+
near(t, s, e, l = []) {
|
|
35
|
+
const i = e * e;
|
|
36
|
+
return this.visit(t - e, s - e, t + e, s + e, (n, c) => {
|
|
37
|
+
const r = c.x - t, o = c.y - s;
|
|
38
|
+
r * r + o * o <= i && l.push(n);
|
|
39
|
+
}), l;
|
|
40
|
+
}
|
|
41
|
+
/** The ids inside the box from (x0, y0) to (x1, y1), edges included. */
|
|
42
|
+
inRect(t, s, e, l, i = []) {
|
|
43
|
+
return this.visit(t, s, e, l, (n, c) => {
|
|
44
|
+
c.x >= t && c.x <= e && c.y >= s && c.y <= l && i.push(n);
|
|
45
|
+
}), i;
|
|
46
|
+
}
|
|
47
|
+
visit(t, s, e, l, i) {
|
|
48
|
+
const n = this.cellSize, c = Math.floor(t / n), r = Math.floor(s / n), o = Math.floor(e / n), S = Math.floor(l / n);
|
|
49
|
+
if ((o - c + 1) * (S - r + 1) > this.entries.size) {
|
|
50
|
+
for (const [h, f] of this.entries) i(h, f);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (let h = r; h <= S; h++)
|
|
54
|
+
for (let f = c; f <= o; f++) {
|
|
55
|
+
const g = this.cells.get(z(f, h));
|
|
56
|
+
if (g) for (const u of g) i(u, this.entries.get(u));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
unlink(t, s) {
|
|
60
|
+
const e = this.cells.get(s);
|
|
61
|
+
e && (e.delete(t), e.size === 0 && this.cells.delete(s));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
M as SpatialHash
|
|
66
|
+
};
|