@sadhaka/loom-engine 0.12.0 → 0.13.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.
@@ -0,0 +1,185 @@
1
+ // PeerPool - tracks all known remote peers and their interpolated
2
+ // world position.
3
+ //
4
+ // Each peer keeps the last two known positions (prev, current) with
5
+ // timestamps. At frame time the system asks the pool for the
6
+ // interpolated (x, y) per peer, which is computed as
7
+ // factor = clamp01((nowMs - prevTsMs) / (currentTsMs - prevTsMs))
8
+ // x = lerp(prevX, currentX, factor)
9
+ // y = lerp(prevY, currentY, factor)
10
+ //
11
+ // When a new presence.update arrives, prev <- current, current <- new.
12
+ // The factor saturates at 1 once nowMs passes currentTsMs, so a peer
13
+ // who stops sending updates simply freezes at their last known
14
+ // position rather than extrapolating off into the distance.
15
+ //
16
+ // "Acceptable lag" per the phase 15.1 spec is ~150ms (one update
17
+ // interval at the 10Hz wire rate), which is imperceptible at
18
+ // walk-speed. No CRDT, no client-side prediction beyond the
19
+ // straight-line lerp - those are deferred until shared state extends
20
+ // past raw position.
21
+ //
22
+ // Self-filter: the local character's own character_id should NOT
23
+ // appear among the rendered peers (we don't render ourselves as a
24
+ // ghost). The PeerPresenceSystem owns this filter via
25
+ // setLocalCharacterId(); peers with that id are silently skipped on
26
+ // upsert and removed if already present.
27
+ function lerp(a, b, t) {
28
+ return a + (b - a) * t;
29
+ }
30
+ function clamp01(t) {
31
+ if (t < 0)
32
+ return 0;
33
+ if (t > 1)
34
+ return 1;
35
+ return t;
36
+ }
37
+ export class PeerPool {
38
+ peers = new Map();
39
+ localCharacterId = null;
40
+ // Reused on every getRenderedPosition / forEachRendered call so the
41
+ // hot per-frame path is allocation-free.
42
+ scratchView = {
43
+ characterId: '',
44
+ x: 0,
45
+ y: 0,
46
+ zone: '',
47
+ name: null,
48
+ };
49
+ setLocalCharacterId(id) {
50
+ this.localCharacterId = id;
51
+ if (id !== null && this.peers.has(id)) {
52
+ this.peers.delete(id);
53
+ }
54
+ }
55
+ getLocalCharacterId() {
56
+ return this.localCharacterId;
57
+ }
58
+ // Apply a new presence update for a peer. If the peer is the local
59
+ // character, the update is ignored (self-filter). If this is the
60
+ // first update for the peer, prev = current = the new position so
61
+ // the lerp factor immediately saturates and the peer renders at the
62
+ // sent position.
63
+ upsert(characterId, x, y, zone, tsMs, name) {
64
+ if (this.localCharacterId !== null && characterId === this.localCharacterId) {
65
+ return;
66
+ }
67
+ const existing = this.peers.get(characterId);
68
+ if (!existing) {
69
+ this.peers.set(characterId, {
70
+ characterId,
71
+ zone,
72
+ name: name ?? null,
73
+ prevX: x,
74
+ prevY: y,
75
+ prevTsMs: tsMs,
76
+ currentX: x,
77
+ currentY: y,
78
+ currentTsMs: tsMs,
79
+ lastRenderedFrame: -1,
80
+ });
81
+ return;
82
+ }
83
+ // Out-of-order: drop messages older than current. Wire protocol
84
+ // is monotonic per character_id, but reorder buffers + reconnect
85
+ // replays can deliver an older ts after a newer one.
86
+ if (tsMs < existing.currentTsMs) {
87
+ return;
88
+ }
89
+ existing.prevX = existing.currentX;
90
+ existing.prevY = existing.currentY;
91
+ existing.prevTsMs = existing.currentTsMs;
92
+ existing.currentX = x;
93
+ existing.currentY = y;
94
+ existing.currentTsMs = tsMs;
95
+ existing.zone = zone;
96
+ if (name !== undefined) {
97
+ existing.name = name;
98
+ }
99
+ }
100
+ // Replace the entire roster with a snapshot. Peers not present in
101
+ // the snapshot are dropped; peers in the snapshot but not yet
102
+ // tracked are inserted (with prev = current so they render at the
103
+ // sent position immediately).
104
+ applySnapshot(peers) {
105
+ const seen = new Set();
106
+ for (let i = 0; i < peers.length; i++) {
107
+ const p = peers[i];
108
+ if (!p)
109
+ continue;
110
+ if (this.localCharacterId !== null && p.characterId === this.localCharacterId) {
111
+ continue;
112
+ }
113
+ seen.add(p.characterId);
114
+ this.upsert(p.characterId, p.x, p.y, p.zone, p.tsMs, p.name);
115
+ }
116
+ // Drop anyone not in the snapshot. Iterate keys snapshot first
117
+ // because Map.delete during iteration is fine but copying makes
118
+ // intent obvious.
119
+ const toRemove = [];
120
+ this.peers.forEach((_v, k) => {
121
+ if (!seen.has(k))
122
+ toRemove.push(k);
123
+ });
124
+ for (let i = 0; i < toRemove.length; i++) {
125
+ const k = toRemove[i];
126
+ if (k)
127
+ this.peers.delete(k);
128
+ }
129
+ }
130
+ remove(characterId) {
131
+ return this.peers.delete(characterId);
132
+ }
133
+ has(characterId) {
134
+ return this.peers.has(characterId);
135
+ }
136
+ size() {
137
+ return this.peers.size;
138
+ }
139
+ get(characterId) {
140
+ return this.peers.get(characterId);
141
+ }
142
+ // Iterate every tracked peer with their interpolated world
143
+ // position at nowMs. The view object is reused; consumers must
144
+ // copy any field they want to retain past the callback.
145
+ forEachRendered(nowMs, frame, fn) {
146
+ this.peers.forEach((entry) => {
147
+ const v = this.scratchView;
148
+ v.characterId = entry.characterId;
149
+ v.zone = entry.zone;
150
+ v.name = entry.name;
151
+ const dt = entry.currentTsMs - entry.prevTsMs;
152
+ if (dt <= 0) {
153
+ v.x = entry.currentX;
154
+ v.y = entry.currentY;
155
+ }
156
+ else {
157
+ const t = clamp01((nowMs - entry.prevTsMs) / dt);
158
+ v.x = lerp(entry.prevX, entry.currentX, t);
159
+ v.y = lerp(entry.prevY, entry.currentY, t);
160
+ }
161
+ entry.lastRenderedFrame = frame;
162
+ fn(v);
163
+ });
164
+ }
165
+ // Single-peer query for tests + the rare ad-hoc lookup. Hot paths
166
+ // use forEachRendered to avoid map lookups.
167
+ getRenderedPosition(characterId, nowMs) {
168
+ const entry = this.peers.get(characterId);
169
+ if (!entry)
170
+ return null;
171
+ const dt = entry.currentTsMs - entry.prevTsMs;
172
+ if (dt <= 0) {
173
+ return { x: entry.currentX, y: entry.currentY };
174
+ }
175
+ const t = clamp01((nowMs - entry.prevTsMs) / dt);
176
+ return {
177
+ x: lerp(entry.prevX, entry.currentX, t),
178
+ y: lerp(entry.prevY, entry.currentY, t),
179
+ };
180
+ }
181
+ clear() {
182
+ this.peers.clear();
183
+ }
184
+ }
185
+ //# sourceMappingURL=peer-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-pool.js","sourceRoot":"","sources":["../../src/network/peer-pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { type IMultiplayerBridge, type MultiplayerBridgeStatus, type MultiplayerBridgeStats, type PresenceMessage } from './multiplayer-bridge.js';
2
+ export interface SSEMultiplayerBridgeOptions {
3
+ baseUrl: string;
4
+ broadcastUrl?: string;
5
+ characterId: string;
6
+ zone: string;
7
+ eventSourceFactory?: (url: string) => EventSource;
8
+ fetchFn?: typeof fetch;
9
+ }
10
+ export declare class SSEMultiplayerBridge implements IMultiplayerBridge {
11
+ private readonly baseUrl;
12
+ private readonly broadcastUrl;
13
+ private readonly characterId;
14
+ private readonly zone;
15
+ private readonly eventSourceFactory;
16
+ private readonly fetchFn;
17
+ private es;
18
+ private queue;
19
+ private statusValue;
20
+ private statsValue;
21
+ private lastBroadcastMs;
22
+ constructor(opts: SSEMultiplayerBridgeOptions);
23
+ connect(): void;
24
+ disconnect(): void;
25
+ status(): MultiplayerBridgeStatus;
26
+ pollMessages(): PresenceMessage[];
27
+ broadcastPosition(x: number, y: number, zone: string, tsMs: number): void;
28
+ stats(): Readonly<MultiplayerBridgeStats>;
29
+ private buildUrl;
30
+ private openConnection;
31
+ private closeConnection;
32
+ private handleUpdate;
33
+ private handleDepart;
34
+ private handleSnapshot;
35
+ }
36
+ //# sourceMappingURL=sse-multiplayer-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-multiplayer-bridge.d.ts","sourceRoot":"","sources":["../../src/network/sse-multiplayer-bridge.ts"],"names":[],"mappings":"AA4BA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,2BAA2B;IAI1C,OAAO,EAAE,MAAM,CAAC;IAKhB,YAAY,CAAC,EAAE,MAAM,CAAC;IAItB,WAAW,EAAE,MAAM,CAAC;IAEpB,IAAI,EAAE,MAAM,CAAC;IAGb,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,WAAW,CAAC;IAClD,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,kBAAkB;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA+B;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,UAAU,CAKhB;IAEF,OAAO,CAAC,eAAe,CAAqB;gBAEhC,IAAI,EAAE,2BAA2B;IAwB7C,OAAO,IAAI,IAAI;IAMf,UAAU,IAAI,IAAI;IAKlB,MAAM,IAAI,uBAAuB;IAIjC,YAAY,IAAI,eAAe,EAAE;IAOjC,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IA0BzE,KAAK,IAAI,QAAQ,CAAC,sBAAsB,CAAC;IAMzC,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,cAAc;IAmCtB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,cAAc;CAsCvB"}
@@ -0,0 +1,264 @@
1
+ // SSEMultiplayerBridge - real EventSource subscription to the
2
+ // backend's presence endpoint, paired with a fetch POST for outbound
3
+ // position broadcasts.
4
+ //
5
+ // Wire protocol (paired with Track B server-side):
6
+ // GET <baseUrl>?character_id=...&zone=... opens an SSE stream that
7
+ // emits three event types:
8
+ // - 'presence.snapshot' { peers: [{ character_id, x, y, zone, ts_ms, name? }] }
9
+ // emitted once on connect with the full current peer roster.
10
+ // - 'presence.update' { character_id, x, y, zone, ts_ms, name? }
11
+ // emitted as peers move.
12
+ // - 'presence.depart' { character_id }
13
+ // emitted when a peer disconnects.
14
+ //
15
+ // POST <broadcastUrl> { character_id, x, y, zone, ts_ms }
16
+ // called by broadcastPosition at most BROADCAST_HZ per second.
17
+ // Engine-side rate limit; the bridge silently drops excess calls
18
+ // and increments rateLimitedDrops.
19
+ //
20
+ // Reconnect strategy: EventSource handles transport-layer reconnect
21
+ // internally. We observe via onerror/onopen and surface 'reconnecting'
22
+ // to the consumer. On reconnect the server is expected to re-emit a
23
+ // fresh 'presence.snapshot', which the PeerPool consumes and treats
24
+ // as authoritative (any peer not in the snapshot is dropped).
25
+ //
26
+ // Browser-only. Constructor throws if EventSource is undefined (Node
27
+ // test environment). Tests use MockMultiplayerBridge instead.
28
+ import { BROADCAST_MIN_INTERVAL_MS, } from './multiplayer-bridge.js';
29
+ export class SSEMultiplayerBridge {
30
+ baseUrl;
31
+ broadcastUrl;
32
+ characterId;
33
+ zone;
34
+ eventSourceFactory;
35
+ fetchFn;
36
+ es = null;
37
+ queue = [];
38
+ statusValue = 'idle';
39
+ statsValue = {
40
+ messagesReceived: 0,
41
+ messagesSent: 0,
42
+ rateLimitedDrops: 0,
43
+ reconnects: 0,
44
+ };
45
+ lastBroadcastMs = -Infinity;
46
+ constructor(opts) {
47
+ this.baseUrl = opts.baseUrl;
48
+ this.broadcastUrl = opts.broadcastUrl ?? defaultBroadcastUrl(opts.baseUrl);
49
+ this.characterId = opts.characterId;
50
+ this.zone = opts.zone;
51
+ if (opts.eventSourceFactory) {
52
+ this.eventSourceFactory = opts.eventSourceFactory;
53
+ }
54
+ else {
55
+ if (typeof EventSource === 'undefined') {
56
+ throw new Error('SSEMultiplayerBridge: EventSource is not available in this environment. Use MockMultiplayerBridge for tests.');
57
+ }
58
+ const ESCtor = EventSource;
59
+ this.eventSourceFactory = (u) => new ESCtor(u, { withCredentials: true });
60
+ }
61
+ if (opts.fetchFn) {
62
+ this.fetchFn = opts.fetchFn;
63
+ }
64
+ else {
65
+ if (typeof fetch === 'undefined') {
66
+ throw new Error('SSEMultiplayerBridge: fetch is not available in this environment.');
67
+ }
68
+ this.fetchFn = fetch.bind(globalThis);
69
+ }
70
+ }
71
+ connect() {
72
+ if (this.es)
73
+ return;
74
+ this.statusValue = 'connecting';
75
+ this.openConnection();
76
+ }
77
+ disconnect() {
78
+ this.statusValue = 'closed';
79
+ this.closeConnection();
80
+ }
81
+ status() {
82
+ return this.statusValue;
83
+ }
84
+ pollMessages() {
85
+ if (this.queue.length === 0)
86
+ return [];
87
+ const out = this.queue;
88
+ this.queue = [];
89
+ return out;
90
+ }
91
+ broadcastPosition(x, y, zone, tsMs) {
92
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
93
+ if (now - this.lastBroadcastMs < BROADCAST_MIN_INTERVAL_MS) {
94
+ this.statsValue.rateLimitedDrops++;
95
+ return;
96
+ }
97
+ this.lastBroadcastMs = now;
98
+ this.statsValue.messagesSent++;
99
+ // Fire-and-forget POST. Errors are surfaced via stats only - the
100
+ // engine doesn't block on the network round trip. The body
101
+ // matches the server contract from the phase 15.1 spec.
102
+ const body = JSON.stringify({
103
+ character_id: this.characterId,
104
+ x,
105
+ y,
106
+ zone,
107
+ ts_ms: tsMs,
108
+ });
109
+ void this.fetchFn(this.broadcastUrl, {
110
+ method: 'POST',
111
+ credentials: 'include',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body,
114
+ }).catch(() => { });
115
+ }
116
+ stats() {
117
+ return this.statsValue;
118
+ }
119
+ // ----- Internal -----
120
+ buildUrl() {
121
+ const sep = this.baseUrl.includes('?') ? '&' : '?';
122
+ return (this.baseUrl +
123
+ sep +
124
+ 'character_id=' + encodeURIComponent(this.characterId) +
125
+ '&zone=' + encodeURIComponent(this.zone));
126
+ }
127
+ openConnection() {
128
+ const url = this.buildUrl();
129
+ let es;
130
+ try {
131
+ es = this.eventSourceFactory(url);
132
+ }
133
+ catch (err) {
134
+ this.statusValue = 'closed';
135
+ throw err;
136
+ }
137
+ this.es = es;
138
+ es.onopen = () => {
139
+ this.statusValue = 'connected';
140
+ };
141
+ es.onerror = () => {
142
+ const closed = es.readyState === 2; // EventSource.CLOSED
143
+ if (closed) {
144
+ this.statusValue = 'closed';
145
+ this.closeConnection();
146
+ return;
147
+ }
148
+ this.statusValue = 'reconnecting';
149
+ this.statsValue.reconnects++;
150
+ };
151
+ es.addEventListener('presence.update', (e) => {
152
+ this.handleUpdate(e);
153
+ });
154
+ es.addEventListener('presence.depart', (e) => {
155
+ this.handleDepart(e);
156
+ });
157
+ es.addEventListener('presence.snapshot', (e) => {
158
+ this.handleSnapshot(e);
159
+ });
160
+ }
161
+ closeConnection() {
162
+ if (!this.es)
163
+ return;
164
+ try {
165
+ this.es.close();
166
+ }
167
+ catch { /* ignore */ }
168
+ this.es = null;
169
+ }
170
+ handleUpdate(e) {
171
+ const data = parseJson(e.data);
172
+ if (!data || typeof data !== 'object')
173
+ return;
174
+ const characterId = data.character_id;
175
+ const x = data.x;
176
+ const y = data.y;
177
+ const zone = data.zone;
178
+ const tsMs = data.ts_ms;
179
+ if (typeof characterId !== 'string')
180
+ return;
181
+ if (typeof x !== 'number' || typeof y !== 'number')
182
+ return;
183
+ if (typeof zone !== 'string')
184
+ return;
185
+ if (typeof tsMs !== 'number')
186
+ return;
187
+ const name = data.name;
188
+ this.statsValue.messagesReceived++;
189
+ this.queue.push({
190
+ kind: 'update',
191
+ characterId,
192
+ x,
193
+ y,
194
+ zone,
195
+ tsMs,
196
+ ...(typeof name === 'string' ? { name } : {}),
197
+ });
198
+ }
199
+ handleDepart(e) {
200
+ const data = parseJson(e.data);
201
+ if (!data || typeof data !== 'object')
202
+ return;
203
+ const characterId = data.character_id;
204
+ if (typeof characterId !== 'string')
205
+ return;
206
+ this.statsValue.messagesReceived++;
207
+ this.queue.push({ kind: 'depart', characterId });
208
+ }
209
+ handleSnapshot(e) {
210
+ const data = parseJson(e.data);
211
+ if (!data || typeof data !== 'object')
212
+ return;
213
+ const peersRaw = data.peers;
214
+ if (!Array.isArray(peersRaw))
215
+ return;
216
+ const peers = [];
217
+ for (let i = 0; i < peersRaw.length; i++) {
218
+ const p = peersRaw[i];
219
+ if (!p || typeof p !== 'object')
220
+ continue;
221
+ const characterId = p.character_id;
222
+ const x = p.x;
223
+ const y = p.y;
224
+ const zone = p.zone;
225
+ const tsMs = p.ts_ms;
226
+ if (typeof characterId !== 'string')
227
+ continue;
228
+ if (typeof x !== 'number' || typeof y !== 'number')
229
+ continue;
230
+ if (typeof zone !== 'string')
231
+ continue;
232
+ if (typeof tsMs !== 'number')
233
+ continue;
234
+ const name = p.name;
235
+ peers.push({
236
+ characterId,
237
+ x,
238
+ y,
239
+ zone,
240
+ tsMs,
241
+ ...(typeof name === 'string' ? { name } : {}),
242
+ });
243
+ }
244
+ this.statsValue.messagesReceived++;
245
+ this.queue.push({ kind: 'snapshot', peers });
246
+ }
247
+ }
248
+ function parseJson(raw) {
249
+ if (typeof raw !== 'string')
250
+ return null;
251
+ try {
252
+ return JSON.parse(raw);
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ }
258
+ function defaultBroadcastUrl(baseUrl) {
259
+ if (baseUrl.endsWith('/events')) {
260
+ return baseUrl.slice(0, -'/events'.length) + '/move';
261
+ }
262
+ return baseUrl + '/move';
263
+ }
264
+ //# sourceMappingURL=sse-multiplayer-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-multiplayer-bridge.js","sourceRoot":"","sources":["../../src/network/sse-multiplayer-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { System } from '../system.js';
2
+ import type { World } from '../world.js';
3
+ export declare class PeerPresenceSystem implements System {
4
+ readonly name: string;
5
+ update(world: World, _dt: number): void;
6
+ }
7
+ export declare class PeerRenderSystem implements System {
8
+ readonly name: string;
9
+ private scratchTextStyle;
10
+ private scratchTint;
11
+ private readonly labelYOffset;
12
+ private readonly showNames;
13
+ constructor(opts?: {
14
+ labelYOffset?: number;
15
+ showNames?: boolean;
16
+ });
17
+ update(world: World, _dt: number): void;
18
+ }
19
+ //# sourceMappingURL=peer-presence-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-presence-system.d.ts","sourceRoot":"","sources":["../../src/systems/peer-presence-system.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAiBzC,qBAAa,kBAAmB,YAAW,MAAM;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAmB;IAExC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAwBxC;AAKD,qBAAa,gBAAiB,YAAW,MAAM;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAiB;IAGtC,OAAO,CAAC,gBAAgB,CAKtB;IACF,OAAO,CAAC,WAAW,CAEjB;IAMF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;gBAExB,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO;IAKrE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAgCxC"}
@@ -0,0 +1,118 @@
1
+ // PeerPresenceSystem - drains the IMultiplayerBridge each tick, applies
2
+ // presence messages to the PeerPool, and (optionally) broadcasts the
3
+ // local character's position via PeerBroadcastBridge.
4
+ //
5
+ // PeerRenderSystem - iterates PeerPool at frame time, computes each
6
+ // peer's interpolated position from prev/current samples, looks up
7
+ // their sprite via PeerSpritePool, and submits drawSprite + drawText
8
+ // (name label) calls to the device.
9
+ //
10
+ // Phasing rationale (matches DirectorSystem):
11
+ // PHASE_INPUT - PeerPresenceSystem drains the bridge so peers are
12
+ // fresh before any logic system reads them. Mirrors
13
+ // the DirectorSystem ordering.
14
+ // PHASE_RENDER - PeerRenderSystem submits draw calls. Per-peer
15
+ // interp factor is computed from the current
16
+ // TimeResource clock, so peers move smoothly even on
17
+ // frames with no inbound update.
18
+ //
19
+ // PeerPresenceSystem also routes the snapshot, update, and depart
20
+ // message kinds to the right PeerPool method:
21
+ // snapshot -> applySnapshot (drops anyone not in the snapshot)
22
+ // update -> upsert
23
+ // depart -> remove
24
+ //
25
+ // Self-filter: callers register the local character_id via
26
+ // peerPool.setLocalCharacterId(...) at startup. The pool refuses to
27
+ // store an entry with that id; if a snapshot or update mentions it,
28
+ // it's silently skipped (we don't render ourselves as a ghost).
29
+ import { RESOURCE_MULTIPLAYER_BRIDGE, RESOURCE_PEER_POOL, } from '../network/multiplayer-bridge.js';
30
+ import { POOL_PEER_SPRITE } from '../components/peer-sprite.js';
31
+ import { RESOURCE_DEVICE, RESOURCE_CAMERA, RESOURCE_TIME, } from '../resources.js';
32
+ export class PeerPresenceSystem {
33
+ name = 'peer-presence';
34
+ update(world, _dt) {
35
+ const bridge = world.resources.get(RESOURCE_MULTIPLAYER_BRIDGE);
36
+ const pool = world.resources.get(RESOURCE_PEER_POOL);
37
+ if (!bridge || !pool)
38
+ return;
39
+ const messages = bridge.pollMessages();
40
+ if (messages.length === 0)
41
+ return;
42
+ for (let i = 0; i < messages.length; i++) {
43
+ const m = messages[i];
44
+ if (!m)
45
+ continue;
46
+ switch (m.kind) {
47
+ case 'update':
48
+ pool.upsert(m.characterId, m.x, m.y, m.zone, m.tsMs, m.name);
49
+ break;
50
+ case 'depart':
51
+ pool.remove(m.characterId);
52
+ break;
53
+ case 'snapshot':
54
+ pool.applySnapshot(m.peers);
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ // Render-phase counterpart. Optional - consumers who only want to
61
+ // surface peer state (e.g. on a minimap) can read PeerPool directly
62
+ // and skip this system.
63
+ export class PeerRenderSystem {
64
+ name = 'peer-render';
65
+ // Reused per-frame to avoid allocation in the per-peer hot path.
66
+ scratchTextStyle = {
67
+ font: '12px sans-serif',
68
+ fill: { r: 1, g: 1, b: 1, a: 1 },
69
+ align: 'center',
70
+ baseline: 'bottom',
71
+ };
72
+ scratchTint = {
73
+ r: 1, g: 1, b: 1, a: 1,
74
+ };
75
+ // Vertical offset (world units) applied to the name label so it
76
+ // sits just above the sprite. Tuned for the engine's standard
77
+ // 64-px sprite cell; consumers with custom art can override via
78
+ // the PeerRenderSystem constructor.
79
+ labelYOffset;
80
+ showNames;
81
+ constructor(opts = {}) {
82
+ this.labelYOffset = opts.labelYOffset ?? -32;
83
+ this.showNames = opts.showNames ?? true;
84
+ }
85
+ update(world, _dt) {
86
+ const pool = world.resources.get(RESOURCE_PEER_POOL);
87
+ const sprites = world.getPool(POOL_PEER_SPRITE);
88
+ const device = world.resources.get(RESOURCE_DEVICE);
89
+ const camera = world.resources.get(RESOURCE_CAMERA);
90
+ const time = world.resources.get(RESOURCE_TIME);
91
+ if (!pool || !sprites || !device || !camera)
92
+ return;
93
+ device.setCamera(camera);
94
+ const nowMs = (time ? time.elapsed * 1000 : 0) + (typeof performance !== 'undefined' ? performance.now() : 0);
95
+ const frame = time ? time.frame : -1;
96
+ // Capture the values we need from each peer before drawing.
97
+ // forEachRendered's view is reused; we have to read fields out
98
+ // before the next iteration mutates the scratch.
99
+ pool.forEachRendered(nowMs, frame, (view) => {
100
+ const entry = sprites.resolve(view.characterId);
101
+ if (entry.tint) {
102
+ const t = this.scratchTint;
103
+ t.r = entry.tint.r;
104
+ t.g = entry.tint.g;
105
+ t.b = entry.tint.b;
106
+ t.a = entry.tint.a;
107
+ device.drawSprite(view.x, view.y, 0, entry.atlas, entry.frame, t);
108
+ }
109
+ else {
110
+ device.drawSprite(view.x, view.y, 0, entry.atlas, entry.frame);
111
+ }
112
+ if (this.showNames && view.name) {
113
+ device.drawText(view.x, view.y + this.labelYOffset, view.name, this.scratchTextStyle);
114
+ }
115
+ });
116
+ }
117
+ }
118
+ //# sourceMappingURL=peer-presence-system.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-presence-system.js","sourceRoot":"","sources":["../../src/systems/peer-presence-system.ts"],"names":[],"mappings":"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"}