diffs / #6

feat(#6): presence

SHIPPED commit 2a41aed +20 −0 request “see other players as ghost cats” SEED · built by us pre-launch

Requested-by: SEED (built by us pre-launch)
+ BUILD+ UNIT TESTS+ LINT+ DIFF GATE+ PAGE CLASSIFIER+ PLAYWRIGHT
src/presence.jsNEW+20 −0
1+// Seed #6: presence. Other players show up as ghost cats (max 40). Positions and 4 fixed emotes only, never free text.
2+export function connect(w) {
3+ let ws = null, tick = null, backoff = 1000;
4+ const open = () => {
5+ try { ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/arcade/ws`); } catch { return retry(); }
6+ ws.onopen = () => { backoff = 1000; tick = setInterval(send, 200); };
7+ ws.onmessage = (m) => {
8+ let d; try { d = JSON.parse(m.data); } catch { return; }
9+ if (d.t === "snap" && Array.isArray(d.p)) w.ghosts = d.p.slice(0, 40).map(([id, room, x, y, f, a, e]) => ({ id, room, x, y, f, a, e }));
10+ };
11+ ws.onclose = () => { clearInterval(tick); w.ghosts = []; retry(); };
12+ ws.onerror = () => ws.close();
13+ };
14+ const retry = () => { setTimeout(open, backoff); backoff = Math.min(30000, backoff * 2); };
15+ const send = () => {
16+ const p = w.player; if (!p || ws.readyState !== 1) return;
17+ ws.send(JSON.stringify({ t: "pos", room: w.room, x: Math.round(p.x), y: Math.round(p.y), f: p.f, a: p.walk > 0 ? 1 : 0, e: p.emote }));
18+ };
19+ open();
20+}
1+// Seed #6: presence. Other players show up as ghost cats (max 40). Positions and 4 fixed emotes only, never free text.
2+export function connect(w) {
3+ let ws = null, tick = null, backoff = 1000;
4+ const open = () => {
5+ try { ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/arcade/ws`); } catch { return retry(); }
6+ ws.onopen = () => { backoff = 1000; tick = setInterval(send, 200); };
7+ ws.onmessage = (m) => {
8+ let d; try { d = JSON.parse(m.data); } catch { return; }
9+ if (d.t === "snap" && Array.isArray(d.p)) w.ghosts = d.p.slice(0, 40).map(([id, room, x, y, f, a, e]) => ({ id, room, x, y, f, a, e }));
10+ };
11+ ws.onclose = () => { clearInterval(tick); w.ghosts = []; retry(); };
12+ ws.onerror = () => ws.close();
13+ };
14+ const retry = () => { setTimeout(open, backoff); backoff = Math.min(30000, backoff * 2); };
15+ const send = () => {
16+ const p = w.player; if (!p || ws.readyState !== 1) return;
17+ ws.send(JSON.stringify({ t: "pos", room: w.room, x: Math.round(p.x), y: Math.round(p.y), f: p.f, a: p.walk > 0 ? 1 : 0, e: p.emote }));
18+ };
19+ open();
20+}
Before
BEFORE
After
AFTER
CI log tail
$ ci:build  (658 ms, exit 0)

$ ci:test  (148 ms, exit 0)
TAP version 13
# Subtest: palette: valid hex, at most 40 colours
ok 1 - palette: valid hex, at most 40 colours
  ---
  duration_ms: 0.73159
  type: 'test'
  ...
# Subtest: sprites: rectangular-ish rows of known keys
ok 2 - sprites: rectangular-ish rows of known keys
  ---
  duration_ms: 1.032724
  type: 'test'
  ...
# Subtest: levels: every room has a name, platforms and a draw function
ok 3 - levels: every room has a name, platforms and a draw function
  ---
  duration_ms: 2.484121
  type: 'test'
  ...
# Subtest: bosses: every file exports a class with update/draw
ok 4 - bosses: every file exports a class with update/draw
  ---
  duration_ms: 0.893293
  type: 'test'
  ...
# Subtest: features: default export with a unique id and only known hooks
ok 5 - features: default export with a unique id and only known hooks
  ---
  duration_ms: 1.695976
  type: 'test'
  ...
# Subtest: no network, storage or parent access anywhere in src
ok 6 - no network, storage or parent access anywhere in src
  ---
  duration_ms: 5.904195
  type: 'test'
  ...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 94.309779

$ ci:lint  (632 ms, exit 0)

$ playwright 960x540  fps 60  long tasks 0  console errors 0