tickcli-js 0.0.0-stage → 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Crystal Clash</title></head><body>
2
+ <script src="lib:room"></script>
3
+ <script src="lib:game"></script>
4
+ <script>
5
+ // Team battle for 2 to 10 players in two teams. Each team has a crystal and sends out minions; break the other team's crystal to win.
6
+ // The host runs everything that is not a player: minions, damage, deaths, respawns, the crystals. Players only move and ask to attack.
7
+ const W = 1600, H = 900, SPEED = 200, RANGE = 140, RESPAWN = 4;
8
+ const BASE = [{ x: 120, y: 450 }, { x: 1480, y: 450 }];
9
+ let x = 0, y = 0, cool = {}, seq = 0, nextWave = 0, sparks = [];
10
+ const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
11
+ Game({
12
+ title: "Crystal Clash", help: "Move with arrows, WASD or the pad. Space or tap attacks the nearest enemy. Break the other crystal.", aspect: 16 / 9,
13
+ multiplayer: { max: 10, teams: 2, player: { x: [0, W], y: [0, H] } },
14
+ setup(g) { const b = BASE[g.me.team || 0]; x = b.x + (g.me.team ? -70 : 70); y = b.y + (Math.random() * 160 - 80); g.me.set({ x, y }); sparks = []; },
15
+ update(g, dt) {
16
+ const s = g.shared, dead = s.dead && s.dead[g.me.id];
17
+ if (dead) { const b = BASE[g.me.team || 0]; x = b.x + (g.me.team ? -70 : 70); y = b.y; g.me.set({ x, y }); return; }
18
+ const a = g.axis();
19
+ x = Math.max(20, Math.min(W - 20, x + a.x * SPEED * dt)); y = Math.max(20, Math.min(H - 20, y + a.y * SPEED * dt));
20
+ g.me.set({ x, y });
21
+ },
22
+ onInput(g, a) { if (a === "action") g.act("attack"); },
23
+ host(g, dt) {
24
+ const s = g.shared;
25
+ if (!s.crystal) { nextWave = g.now + 1; cool = {}; return g.shared.set({ crystal: { 0: 1500, 1: 1500 }, mobs: {}, hp: {}, dead: {}, kills: {}, score: {}, winner: null }); }
26
+ if (s.winner != null) return;
27
+ const team = s._teams || {}, mobs = JSON.parse(JSON.stringify(s.mobs || {})), hp = {}, dead = {};
28
+ const players = g.players.filter((p) => p.online && typeof p.state.x === "number");
29
+ // players: new ones get full health, the dead come back after RESPAWN seconds
30
+ for (const p of players) {
31
+ if (s.hp[p.id] == null && !s.dead[p.id]) hp[p.id] = 100;
32
+ if (s.dead[p.id] && g.now >= s.dead[p.id]) { dead[p.id] = null; hp[p.id] = 100; }
33
+ }
34
+ // a wave of 3 minions per team every 8 seconds (at most 16 per team)
35
+ if (g.now >= nextWave) {
36
+ nextWave = g.now + 8;
37
+ for (const t of [0, 1]) for (let i = 0; i < 3; i++) if (Object.values(mobs).filter((m) => m.team === t).length < 16) mobs[`m${++seq}${Math.random().toString(36).slice(2, 5)}`] = { x: BASE[t].x + (t ? -60 : 60), y: BASE[t].y + (i - 1) * 50, team: t, hp: 60 };
38
+ }
39
+ // minions walk to the nearest enemy (or the enemy crystal) and hit what they touch
40
+ const crystal = { 0: s.crystal[0], 1: s.crystal[1] };
41
+ for (const [id, m] of Object.entries(mobs)) {
42
+ const foes = [...players.filter((p) => team[p.id] !== m.team && !s.dead[p.id]).map((p) => ({ kind: "p", id: p.id, x: p.state.x, y: p.state.y })),
43
+ ...Object.entries(mobs).filter(([, o]) => o.team !== m.team).map(([oid, o]) => ({ kind: "m", id: oid, x: o.x, y: o.y }))];
44
+ let goal = { kind: "c", id: 1 - m.team, ...BASE[1 - m.team] };
45
+ for (const f of foes) if (dist(f, m) < 200 && dist(f, m) < dist(goal, m)) goal = f;
46
+ const d = dist(goal, m);
47
+ if (d > 34) { m.x += (goal.x - m.x) / d * 60 * dt; m.y += (goal.y - m.y) / d * 60 * dt; }
48
+ else if (goal.kind === "p") hp[goal.id] = (hp[goal.id] ?? s.hp[goal.id] ?? 100) - 8 * dt;
49
+ else if (goal.kind === "m" && mobs[goal.id]) mobs[goal.id].hp -= 10 * dt;
50
+ else if (goal.kind === "c") crystal[goal.id] -= 6 * dt;
51
+ void id;
52
+ }
53
+ for (const [id, m] of Object.entries(mobs)) if (m.hp <= 0) delete mobs[id];
54
+ for (const [id, v] of Object.entries(hp)) if (v != null && v <= 0) { hp[id] = null; dead[id] = g.now + RESPAWN; }
55
+ // minions as a full list: removed ones get null so every screen drops them
56
+ const mobPatch = { ...mobs };
57
+ for (const id in s.mobs) if (!mobs[id]) mobPatch[id] = null;
58
+ const patch = { mobs: mobPatch, hp, dead, crystal: { 0: Math.max(0, crystal[0]), 1: Math.max(0, crystal[1]) } };
59
+ if (crystal[0] <= 0 || crystal[1] <= 0) patch.winner = crystal[0] <= 0 ? 1 : 0;
60
+ g.shared.set(patch);
61
+ if (patch.winner != null) g.gameOver();
62
+ },
63
+ onAction(g, from, name) {
64
+ const s = g.shared, me = g.players.find((p) => p.id === from), team = s._teams || {};
65
+ if (name !== "attack" || !me || s.dead[from] || s.winner != null || (cool[from] || 0) > g.now) return false;
66
+ cool[from] = g.now + 0.4;
67
+ const at = { x: me.state.x, y: me.state.y }, t = team[from] || 0;
68
+ let best = null, bd = RANGE;
69
+ for (const p of g.players) if (team[p.id] !== t && !s.dead[p.id] && typeof p.state.x === "number" && dist(p.state, at) < bd) { best = { kind: "p", id: p.id, x: p.state.x, y: p.state.y }; bd = dist(p.state, at); }
70
+ for (const [id, m] of Object.entries(s.mobs)) if (m.team !== t && dist(m, at) < bd) { best = { kind: "m", id, x: m.x, y: m.y }; bd = dist(m, at); }
71
+ if (!best && dist(BASE[1 - t], at) < RANGE + 40) best = { kind: "c", id: 1 - t, ...BASE[1 - t] };
72
+ if (!best) return false;
73
+ if (best.kind === "p") {
74
+ const left = (s.hp[best.id] ?? 100) - 15;
75
+ g.shared.set(left <= 0 ? { hp: { [best.id]: null }, dead: { [best.id]: g.now + RESPAWN }, kills: { [from]: (s.kills[from] || 0) + 1 }, score: { [from]: (s.score[from] || 0) + 3 } } : { hp: { [best.id]: left } });
76
+ } else if (best.kind === "m") {
77
+ const left = s.mobs[best.id].hp - 25;
78
+ g.shared.set(left <= 0 ? { mobs: { [best.id]: null }, score: { [from]: (s.score[from] || 0) + 1 } } : { mobs: { [best.id]: { hp: left } } });
79
+ } else g.shared.set({ crystal: { [best.id]: Math.max(0, s.crystal[best.id] - 30) } });
80
+ g.fx("hit", { x: best.x, y: best.y });
81
+ },
82
+ onFx(g, from, name, d) { if (name === "hit" && sparks.length < 30) sparks.push({ x: d.x, y: d.y, t: performance.now() }); },
83
+ draw(g, ctx, w, h) {
84
+ const s = g.shared || {}, k = w / 960, me = g.players.find((p) => p.me);
85
+ const cx = Math.max(0, Math.min(W - 960, (me && me.state.x || 0) - 480)), cy = Math.max(0, Math.min(H - 540, (me && me.state.y || 0) - 270));
86
+ ctx.fillStyle = "#0b1220"; ctx.fillRect(0, 0, w, h);
87
+ ctx.save(); ctx.scale(k, k); ctx.translate(-cx, -cy);
88
+ ctx.strokeStyle = "#1e293b"; for (let gx = 0; gx <= W; gx += 100) { ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, H); ctx.stroke(); } for (let gy = 0; gy <= H; gy += 100) { ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.stroke(); }
89
+ for (const t of [0, 1]) { const b = BASE[t]; ctx.fillStyle = g.teamColor(t); ctx.beginPath(); ctx.moveTo(b.x, b.y - 46); ctx.lineTo(b.x + 30, b.y); ctx.lineTo(b.x, b.y + 46); ctx.lineTo(b.x - 30, b.y); ctx.fill(); bar(ctx, b.x - 40, b.y - 60, 80, (s.crystal ? s.crystal[t] : 1500) / 1500); }
90
+ for (const m of Object.values(s.mobs || {})) { ctx.fillStyle = g.teamColor(m.team); ctx.globalAlpha = 0.75; ctx.fillRect(m.x - 9, m.y - 9, 18, 18); ctx.globalAlpha = 1; bar(ctx, m.x - 10, m.y - 16, 20, m.hp / 60); }
91
+ for (const p of g.players) {
92
+ if (typeof p.state.x !== "number" || (s.dead && s.dead[p.id])) continue;
93
+ ctx.fillStyle = g.teamColor(p.team); ctx.beginPath(); ctx.arc(p.state.x, p.state.y, 16, 0, 7); ctx.fill();
94
+ if (p.me) { ctx.strokeStyle = "#f8fafc"; ctx.lineWidth = 3; ctx.stroke(); ctx.strokeStyle = "rgba(248,250,252,.15)"; ctx.beginPath(); ctx.arc(p.state.x, p.state.y, RANGE, 0, 7); ctx.stroke(); }
95
+ bar(ctx, p.state.x - 18, p.state.y - 28, 36, ((s.hp && s.hp[p.id]) ?? 100) / 100);
96
+ ctx.fillStyle = "#e2e8f0"; ctx.font = "12px system-ui"; ctx.textAlign = "center"; ctx.fillText(p.name, p.state.x, p.state.y + 32);
97
+ }
98
+ const now = performance.now(); sparks = sparks.filter((sp) => now - sp.t < 300);
99
+ for (const sp of sparks) { ctx.strokeStyle = "#fde047"; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(sp.x, sp.y, 10 + (now - sp.t) / 15, 0, 7); ctx.stroke(); }
100
+ ctx.restore();
101
+ ctx.fillStyle = "#f8fafc"; ctx.font = `${15 * k}px system-ui`; ctx.textAlign = "left";
102
+ const dead = s.dead && s.dead[g.me.id];
103
+ ctx.fillText(dead ? `Back in ${Math.max(0, Math.ceil(dead - g.now))}` : `Kills ${(s.kills && s.kills[g.me.id]) || 0}`, 10 * k, 20 * k);
104
+ if (s.winner != null) { ctx.textAlign = "center"; ctx.font = `${30 * k}px system-ui`; ctx.fillText(s.winner === g.me.team ? "Victory" : "Defeat", w / 2, h / 2); }
105
+ },
106
+ });
107
+ function bar(ctx, x0, y0, w0, f) { ctx.fillStyle = "#334155"; ctx.fillRect(x0, y0, w0, 4); ctx.fillStyle = f > 0.5 ? "#22c55e" : f > 0.25 ? "#eab308" : "#ef4444"; ctx.fillRect(x0, y0, w0 * Math.max(0, Math.min(1, f)), 4); }
108
+ </script></body></html>
@@ -0,0 +1,110 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Gem Run 3D</title></head><body>
2
+ <script src="lib:three"></script>
3
+ <script src="lib:room"></script>
4
+ <script src="lib:game"></script>
5
+ <script src="lib:game3d"></script>
6
+ <script>
7
+ // Gem Run 3D, 1 to 8 players, with three.js. Run to the spinning gem; whoever touches it first scores and it jumps somewhere else. First to 5 wins.
8
+ // Players share where they stand and which way they face (a quaternion: others see it turn smoothly); the host moves and spins the gem and keeps
9
+ // the score. lib:three is three.js as the global THREE (the tickcli server makes it from the installed three package; a bundle that sets THREE works too).
10
+ const R3 = TickcliThree, SIZE = 14, SPEED = 6, WIN = 5, COLORS = [0x60a5fa, 0xf87171, 0x34d399, 0xfbbf24, 0xa78bfa, 0xf472b6, 0x22d3ee, 0xfb923c];
11
+ let renderer, scene, camera, me, others, gem, up = new THREE.Vector3(0, 1, 0), yaw = 0;
12
+ const colorOf = (g, id) => COLORS[Math.max(0, g.players.findIndex((p) => p.id === id)) % COLORS.length];
13
+ function avatar(color) {
14
+ const o = new THREE.Group(), mat = new THREE.MeshStandardMaterial({ color, roughness: 0.35, metalness: 0.1, emissive: color, emissiveIntensity: 0.15 });
15
+ const b = new THREE.Mesh(new THREE.CapsuleGeometry(0.45, 0.7, 6, 16), mat); b.position.y = 0.8; b.castShadow = true; o.add(b);
16
+ const visor = new THREE.Mesh(new THREE.CapsuleGeometry(0.16, 0.42, 4, 12), new THREE.MeshStandardMaterial({ color: 0xe0f2fe, emissive: 0x7dd3fc, emissiveIntensity: 0.9 }));
17
+ visor.rotation.z = Math.PI / 2; visor.position.set(0, 1.08, 0.38); o.add(visor); // faces +z
18
+ const ring = new THREE.Mesh(new THREE.RingGeometry(0.55, 0.7, 32), new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.55 }));
19
+ ring.rotation.x = -Math.PI / 2; ring.position.y = 0.02; o.add(ring);
20
+ return o;
21
+ }
22
+ let burst = null;
23
+ function world() {
24
+ scene = new THREE.Scene(); scene.background = new THREE.Color(0x070b1a); scene.fog = new THREE.Fog(0x070b1a, 18, 42);
25
+ camera = new THREE.PerspectiveCamera(55, 1, 0.1, 200);
26
+ renderer.shadowMap.enabled = true; renderer.toneMapping = THREE.ACESFilmicToneMapping;
27
+ scene.add(new THREE.HemisphereLight(0xc7d2fe, 0x1e1b4b, 1.6));
28
+ const sun = new THREE.DirectionalLight(0xffffff, 2.2); sun.position.set(8, 16, 6); sun.castShadow = true;
29
+ Object.assign(sun.shadow.camera, { left: -SIZE, right: SIZE, top: SIZE, bottom: -SIZE }); sun.shadow.mapSize.set(1024, 1024); scene.add(sun);
30
+ // the arena: a dark disc, a neon rim and soft pillars of light around it
31
+ const floor = new THREE.Mesh(new THREE.CircleGeometry(SIZE * 1.15, 64), new THREE.MeshStandardMaterial({ color: 0x111827, roughness: 0.9 }));
32
+ floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);
33
+ const grid = new THREE.PolarGridHelper(SIZE * 1.1, 12, 8, 64, 0x312e81, 0x1e1b4b); grid.position.y = 0.01; scene.add(grid);
34
+ const rim = new THREE.Mesh(new THREE.TorusGeometry(SIZE * 1.15, 0.12, 8, 96), new THREE.MeshBasicMaterial({ color: 0x818cf8 }));
35
+ rim.rotation.x = Math.PI / 2; scene.add(rim);
36
+ for (let i = 0; i < 10; i++) {
37
+ const a = (i / 10) * Math.PI * 2, c = COLORS[i % COLORS.length];
38
+ const post = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 3, 12), new THREE.MeshStandardMaterial({ color: c, emissive: c, emissiveIntensity: 1.4 }));
39
+ post.position.set(Math.cos(a) * SIZE * 1.15, 1.5, Math.sin(a) * SIZE * 1.15); scene.add(post);
40
+ }
41
+ gem = new THREE.Group();
42
+ const core = new THREE.Mesh(new THREE.OctahedronGeometry(0.6), new THREE.MeshStandardMaterial({ color: 0xfde047, emissive: 0xfacc15, emissiveIntensity: 0.9, roughness: 0.2, metalness: 0.3 }));
43
+ core.castShadow = true; gem.add(core);
44
+ const halo = new THREE.Mesh(new THREE.SphereGeometry(1.0, 24, 16), new THREE.MeshBasicMaterial({ color: 0xfacc15, transparent: true, opacity: 0.12, depthWrite: false }));
45
+ gem.add(halo); gem.add(new THREE.PointLight(0xfacc15, 6, 7)); scene.add(gem);
46
+ // a burst of sparks where a gem is taken
47
+ const n = 40, geo = new THREE.BufferGeometry(); geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(n * 3), 3));
48
+ burst = { pts: new THREE.Points(geo, new THREE.PointsMaterial({ color: 0xfde047, size: 0.25, transparent: true, opacity: 0 })), v: [], t: 9 };
49
+ for (let i = 0; i < n; i++) burst.v.push(new THREE.Vector3(Math.random() - 0.5, Math.random() * 0.9, Math.random() - 0.5).normalize().multiplyScalar(4 + Math.random() * 4));
50
+ scene.add(burst.pts);
51
+ }
52
+ /** A random spot on the disc, away from the rim. */
53
+ function spot() { const a = Math.random() * Math.PI * 2, r = Math.sqrt(Math.random()) * (SIZE - 3); return { x: Math.round(Math.cos(a) * r), z: Math.round(Math.sin(a) * r) }; }
54
+ function pop(x, y, z) { const a = burst.pts.geometry.attributes.position; for (let i = 0; i < a.count; i++) a.setXYZ(i, x, y, z); a.needsUpdate = true; burst.t = 0; }
55
+ Game({
56
+ title: "Gem Run 3D", help: "Arrow keys, WASD or the pad to run. Touch the spinning gem to score. First to " + WIN + " wins.", aspect: 16 / 10,
57
+ context: "webgl",
58
+ multiplayer: { max: 8, player: { x: [-SIZE, SIZE], z: [-SIZE, SIZE], qx: [-1, 1], qy: [-1, 1], qz: [-1, 1], qw: [-1, 1] } },
59
+ setup(g) {
60
+ renderer = renderer || R3.renderer(g);
61
+ if (!scene) world();
62
+ if (me) scene.remove(me);
63
+ me = avatar(colorOf(g, g.me && g.me.id)); scene.add(me);
64
+ others = others || R3.pool(scene, (p) => avatar(colorOf(g, p.id)));
65
+ const at = g.spawnNear({ keys: ["x", "z"], x: [-SIZE + 1, SIZE - 1], z: [-SIZE + 1, SIZE - 1], y: [-SIZE + 1, SIZE - 1], gap: 3 }); // on screen with the others
66
+ me.position.set(at.x, 0, at.z); yaw = 0;
67
+ const r = Math.hypot(me.position.x, me.position.z); if (r > SIZE - 1) { me.position.x *= (SIZE - 1) / r; me.position.z *= (SIZE - 1) / r; }
68
+ if (g.me) g.me.set(R3.pose(me));
69
+ },
70
+ update(g, dt) {
71
+ const a = g.axis();
72
+ if (a.x || a.y) {
73
+ me.position.x += a.x * SPEED * dt; me.position.z += a.y * SPEED * dt;
74
+ const r = Math.hypot(me.position.x, me.position.z); if (r > SIZE - 1) { me.position.x *= (SIZE - 1) / r; me.position.z *= (SIZE - 1) / r; } // stay on the disc
75
+ yaw = Math.atan2(a.x, a.y);
76
+ }
77
+ me.quaternion.setFromAxisAngle(up, yaw);
78
+ if (g.me) g.me.set(R3.pose(me));
79
+ },
80
+ host(g, dt) {
81
+ const s = g.shared;
82
+ if (!s.gem) return g.shared.set({ gem: { x: 4, y: 1, z: 4, qx: 0, qy: 0, qz: 0, qw: 1 }, score: {} });
83
+ // the gem tumbles about a tilted axis: a new quaternion every step, eased on every other screen as one turn
84
+ const q = new THREE.Quaternion(s.gem.qx, s.gem.qy, s.gem.qz, s.gem.qw).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 2, 0.5).normalize(), 2.5 * dt)).normalize();
85
+ const bob = 1 + Math.sin(g.now * 2) * 0.25;
86
+ for (const p of g.players) {
87
+ const st = p.state;
88
+ if (typeof st.x !== "number" || Math.hypot(st.x - s.gem.x, st.z - s.gem.z) > 1.2) continue;
89
+ const pts = ((s.score && s.score[p.id]) || 0) + 1;
90
+ g.shared.set({ score: { [p.id]: pts }, gem: spot() });
91
+ g.fx("gem", { by: p.id });
92
+ if (pts >= WIN) return g.gameOver(p.id);
93
+ return;
94
+ }
95
+ g.shared.set({ gem: { y: bob, qx: q.x, qy: q.y, qz: q.z, qw: q.w } });
96
+ },
97
+ onFx(g, from, name) { if (name === "gem" && gem) { gem.scale.setScalar(1.8); pop(gem.position.x, gem.position.y, gem.position.z); } },
98
+ draw(g, ctx, w, h) {
99
+ if (!renderer) return;
100
+ R3.resize(renderer, camera, g);
101
+ others.sync(g.players.filter((p) => !p.me));
102
+ if (g.shared.gem) R3.place(gem, g.shared.gem);
103
+ camera.position.set(me.position.x, 9, me.position.z + 11); camera.lookAt(me.position.x, 0, me.position.z);
104
+ renderer.render(scene, camera);
105
+ if (gem.scale.x > 1) gem.scale.setScalar(Math.max(1, gem.scale.x - 0.05));
106
+ if (burst.t < 1) { burst.t += 1 / 60; const a = burst.pts.geometry.attributes.position; for (let i = 0; i < a.count; i++) a.setXYZ(i, a.getX(i) + burst.v[i].x / 60, a.getY(i) + burst.v[i].y / 60 - burst.t * 0.05, a.getZ(i) + burst.v[i].z / 60); a.needsUpdate = true; burst.pts.material.opacity = 1 - burst.t; }
107
+ },
108
+ });
109
+ </script>
110
+ </body></html>
@@ -0,0 +1,132 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Chess</title></head><body>
2
+ <script src="lib:room"></script>
3
+ <script src="lib:game"></script>
4
+ <script>
5
+ // Chess for 2 players, with people watching. Two seats (White, Black); everyone else in the room watches the same board live.
6
+ // A player asks the host to move; the host checks the move against the full rules (check, castling, en passant, promotion to a queen), runs the
7
+ // clocks and says who won. The whole position is in g.shared, so a watcher who joins late or a new host carries on from it.
8
+ const START = ["bR","bN","bB","bQ","bK","bB","bN","bR","bP","bP","bP","bP","bP","bP","bP","bP", ...Array(32).fill(""), "wP","wP","wP","wP","wP","wP","wP","wP","wR","wN","wB","wQ","wK","wB","wN","wR"];
9
+ const DIRS = { N: [[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]], B: [[1,1],[1,-1],[-1,1],[-1,-1]], R: [[1,0],[-1,0],[0,1],[0,-1]] };
10
+ DIRS.Q = DIRS.B.concat(DIRS.R); DIRS.K = DIRS.Q;
11
+ const GLYPH = { K: "♚", Q: "♛", R: "♜", B: "♝", N: "♞", P: "♟" };
12
+ const MINUTES = 5, S = 80, TOP = 30;
13
+ const at = (b, r, c) => (r < 0 || r > 7 || c < 0 || c > 7 ? null : b[r * 8 + c]);
14
+ const other = (c) => (c === "w" ? "b" : "w");
15
+
16
+ /** Moves of the piece on square i, before the "own king left in check" test. attacksOnly: the squares it attacks. */
17
+ function moves(b, i, st, attacksOnly) {
18
+ const p = b[i]; if (!p) return [];
19
+ const me = p[0], t = p[1], r = i >> 3, c = i & 7, out = [];
20
+ const add = (rr, cc, x) => out.push(Object.assign({ to: rr * 8 + cc }, x));
21
+ if (t === "P") {
22
+ const d = me === "w" ? -1 : 1, start = me === "w" ? 6 : 1;
23
+ if (!attacksOnly && at(b, r + d, c) === "") { add(r + d, c); if (r === start && at(b, r + 2 * d, c) === "") add(r + 2 * d, c, { two: 1 }); }
24
+ for (const dc of [-1, 1]) {
25
+ const q = at(b, r + d, c + dc); if (q === null) continue;
26
+ if (attacksOnly || (q && q[0] !== me)) add(r + d, c + dc); else if (q === "" && st.ep === (r + d) * 8 + c + dc) add(r + d, c + dc, { ep: 1 });
27
+ }
28
+ } else if (t === "N" || t === "K") {
29
+ for (const [dr, dc] of DIRS[t]) { const q = at(b, r + dr, c + dc); if (q !== null && (q === "" || q[0] !== me)) add(r + dr, c + dc); }
30
+ const home = me === "w" ? 7 : 0, opp = other(me), h = home * 8;
31
+ if (t === "K" && !attacksOnly && r === home && c === 4 && !attacked(b, i, opp)) {
32
+ if (st.castle.includes(me === "w" ? "K" : "k") && b[h + 5] === "" && b[h + 6] === "" && b[h + 7] === me + "R" && !attacked(b, h + 5, opp) && !attacked(b, h + 6, opp)) add(home, 6, { castle: "k" });
33
+ if (st.castle.includes(me === "w" ? "Q" : "q") && b[h + 1] === "" && b[h + 2] === "" && b[h + 3] === "" && b[h] === me + "R" && !attacked(b, h + 3, opp) && !attacked(b, h + 2, opp)) add(home, 2, { castle: "q" });
34
+ }
35
+ } else for (const [dr, dc] of DIRS[t]) for (let k = 1; k < 8; k++) {
36
+ const q = at(b, r + dr * k, c + dc * k); if (q === null) break;
37
+ if (q === "") { add(r + dr * k, c + dc * k); continue; }
38
+ if (q[0] !== me) add(r + dr * k, c + dc * k);
39
+ break;
40
+ }
41
+ return out;
42
+ }
43
+ function attacked(b, sq, by) { for (let i = 0; i < 64; i++) if (b[i] && b[i][0] === by && moves(b, i, {}, true).some((m) => m.to === sq)) return true; return false; }
44
+ /** The position after a move: board, castling rights left, the en passant square. */
45
+ function play(b, i, m, st) {
46
+ const nb = b.slice(), p = b[i], me = p[0], h = (i >> 3) * 8;
47
+ nb[m.to] = p; nb[i] = "";
48
+ if (m.ep) nb[h + (m.to & 7)] = "";
49
+ if (m.castle === "k") { nb[h + 5] = nb[h + 7]; nb[h + 7] = ""; } else if (m.castle === "q") { nb[h + 3] = nb[h]; nb[h] = ""; }
50
+ if (p[1] === "P" && ((m.to >> 3) === 0 || (m.to >> 3) === 7)) nb[m.to] = me + "Q";
51
+ let castle = st.castle;
52
+ if (p[1] === "K") castle = castle.replace(me === "w" ? /[KQ]/g : /[kq]/g, "");
53
+ for (const sq of [i, m.to]) castle = castle.replace(sq === 63 ? "K" : sq === 56 ? "Q" : sq === 7 ? "k" : sq === 0 ? "q" : "#", "");
54
+ return { board: nb, castle, ep: m.two ? (i + m.to) / 2 : -1 };
55
+ }
56
+ const inCheck = (b, color) => attacked(b, b.indexOf(color + "K"), other(color));
57
+ function legal(b, i, st) { const me = b[i][0]; return moves(b, i, st).filter((m) => !inCheck(play(b, i, m, st).board, me)); }
58
+ function anyLegal(b, color, st) { for (let i = 0; i < 64; i++) if (b[i] && b[i][0] === color && legal(b, i, st).length) return true; return false; }
59
+
60
+ let sel = -1, targets = [];
61
+ const colorOf = (g, id) => (g.seats[0] === id ? "w" : g.seats[1] === id ? "b" : "");
62
+ const left = (g, c) => { const s = g.shared; return Math.max(0, s.clock[c] - (s.turn === c && !s.winner ? g.now - s.turnAt : 0)); };
63
+ const fmt = (t) => `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, "0")}`;
64
+ Game({
65
+ title: "Chess", help: "Click or tap a piece, then a marked square. 5 minutes each. Everyone else in the room watches.", size: [640, 700],
66
+ multiplayer: { max: 8, seats: 2, min: 2, seatNames: ["White", "Black"], player: {} },
67
+ setup(g) { sel = -1; targets = []; if (g.isHost) g.shared.set({ board: START, turn: "w", castle: "KQkq", ep: -1, clock: { w: MINUTES * 60, b: MINUTES * 60 }, turnAt: g.now + 0.5, last: [], winner: "", why: "", score: {} }); },
68
+ host(g) {
69
+ const s = g.shared; if (!s.board || s.winner) return;
70
+ const ids = g.players.map((p) => p.id), w = g.seats[0], b = g.seats[1];
71
+ if (!ids.includes(w) || !ids.includes(b)) return finish(g, ids.includes(w) ? w : ids.includes(b) ? b : "draw", "the other player left");
72
+ if (left(g, s.turn) <= 0) finish(g, g.seats[s.turn === "w" ? 1 : 0], "on time");
73
+ },
74
+ onAction(g, from, name, d) {
75
+ const s = g.shared, me = colorOf(g, from);
76
+ if (name !== "move" || !s.board || s.winner || !me || me !== s.turn || !d || !s.board[d.from] || s.board[d.from][0] !== me) return false;
77
+ const st = { castle: s.castle, ep: s.ep }, m = legal(s.board, d.from, st).find((x) => x.to === d.to);
78
+ if (!m) return false;
79
+ const n = play(s.board, d.from, m, st), next = other(me), check = inCheck(n.board, next), stuck = !anyLegal(n.board, next, n);
80
+ g.shared.set({ board: n.board, castle: n.castle, ep: n.ep, turn: next, turnAt: g.now, last: [d.from, d.to], check, clock: { [me]: Math.round(left(g, me) * 10) / 10 } });
81
+ g.fx("move");
82
+ if (stuck) finish(g, check ? from : "draw", check ? "checkmate" : "stalemate");
83
+ },
84
+ onFx() { sel = -1; targets = []; },
85
+ onPointer(g, x, y) {
86
+ const s = g.shared, me = colorOf(g, g.me.id); if (!s.board || s.winner || !me) return;
87
+ let c = Math.floor(x / S), r = Math.floor((y - TOP) / S); if (c < 0 || c > 7 || r < 0 || r > 7) return;
88
+ if (me === "b") { c = 7 - c; r = 7 - r; }
89
+ const i = r * 8 + c;
90
+ if (sel >= 0 && targets.includes(i)) { g.act("move", { from: sel, to: i }); sel = -1; targets = []; return; }
91
+ if (s.turn === me && s.board[i] && s.board[i][0] === me) { sel = i; targets = legal(s.board, i, { castle: s.castle, ep: s.ep }).map((m) => m.to); } else { sel = -1; targets = []; }
92
+ },
93
+ draw(g, ctx, w, h) {
94
+ const s = g.shared, flip = colorOf(g, g.me.id) === "b";
95
+ ctx.fillStyle = "#0f172a"; ctx.fillRect(0, 0, w, h);
96
+ if (!s.board) return;
97
+ const name = (c) => { const p = g.players.find((q) => q.id === g.seats[c === "w" ? 0 : 1]); return (c === "w" ? "White" : "Black") + (p ? ": " + p.name : ""); };
98
+ ctx.textBaseline = "middle"; ctx.font = "600 17px system-ui";
99
+ for (const [c, yy] of [[flip ? "w" : "b", TOP / 2], [flip ? "b" : "w", TOP + 640 + TOP / 2]]) {
100
+ ctx.fillStyle = s.turn === c && !s.winner ? "#fbbf24" : "#cbd5e1"; ctx.textAlign = "left"; ctx.fillText(name(c), 8, yy);
101
+ ctx.textAlign = "right"; ctx.fillText(fmt(left(g, c)), w - 8, yy);
102
+ }
103
+ // a warm wooden board: light and dark squares with a soft light from the top left, a frame shadow, file and rank letters
104
+ ctx.save(); ctx.shadowColor = "rgba(0,0,0,.55)"; ctx.shadowBlur = 24; ctx.fillStyle = "#5b3a1e"; ctx.fillRect(0, TOP, 8 * S, 8 * S); ctx.restore();
105
+ for (let i = 0; i < 64; i++) {
106
+ const r = i >> 3, c = i & 7, x = (flip ? 7 - c : c) * S, y = TOP + (flip ? 7 - r : r) * S, dark = (r + c) % 2;
107
+ const sq = ctx.createLinearGradient(x, y, x + S, y + S);
108
+ sq.addColorStop(0, dark ? "#b7845a" : "#f3dfbf"); sq.addColorStop(1, dark ? "#9c6a43" : "#e6caa0");
109
+ ctx.fillStyle = sq; ctx.fillRect(x, y, S, S);
110
+ ctx.font = "600 12px system-ui"; ctx.fillStyle = dark ? "#f3dfbf" : "#9c6a43";
111
+ if ((flip ? 7 - c : c) === 0) { ctx.textAlign = "left"; ctx.fillText(String(8 - r), x + 4, y + 10); }
112
+ if ((flip ? 7 - r : r) === 7) { ctx.textAlign = "right"; ctx.fillText("abcdefgh"[c], x + S - 4, y + S - 9); }
113
+ if ((s.last || []).includes(i)) { ctx.fillStyle = "rgba(250,204,21,.42)"; ctx.fillRect(x, y, S, S); }
114
+ if (i === sel) { ctx.fillStyle = "rgba(56,189,248,.55)"; ctx.fillRect(x, y, S, S); }
115
+ const p = s.board[i];
116
+ if (p) {
117
+ if (p[1] === "K" && s.check && p[0] === s.turn) { ctx.fillStyle = "rgba(248,113,113,.7)"; ctx.fillRect(x, y, S, S); }
118
+ ctx.font = "62px system-ui"; ctx.textAlign = "center"; ctx.lineWidth = 3;
119
+ ctx.save(); ctx.shadowColor = "rgba(0,0,0,.45)"; ctx.shadowBlur = 6; ctx.shadowOffsetY = 3;
120
+ ctx.strokeStyle = p[0] === "w" ? "#3f2a17" : "#f3dfbf"; ctx.strokeText(GLYPH[p[1]], x + S / 2, y + S / 2 + 3); ctx.restore();
121
+ ctx.fillStyle = p[0] === "w" ? "#fffaf0" : "#1c130b"; ctx.fillText(GLYPH[p[1]], x + S / 2, y + S / 2 + 3);
122
+ }
123
+ if (targets.includes(i)) { ctx.fillStyle = "rgba(21,128,61,.6)"; ctx.beginPath(); ctx.arc(x + S / 2, y + S / 2, p ? 34 : 12, 0, 7); p ? (ctx.lineWidth = 5, ctx.strokeStyle = ctx.fillStyle, ctx.stroke()) : ctx.fill(); }
124
+ }
125
+ if (s.winner) {
126
+ const p = g.players.find((q) => q.id === s.winner), text = s.winner === "draw" ? "Draw: " + s.why : (p ? p.name : "A player") + " wins: " + s.why;
127
+ ctx.fillStyle = "rgba(8,12,24,.8)"; ctx.fillRect(0, h / 2 - 34, w, 68); ctx.fillStyle = "#f8fafc"; ctx.font = "700 26px system-ui"; ctx.textAlign = "center"; ctx.fillText(text, w / 2, h / 2);
128
+ }
129
+ },
130
+ });
131
+ function finish(g, winner, why) { g.shared.set({ winner, why, score: winner === "draw" ? {} : { [winner]: 1 } }); g.gameOver(winner); }
132
+ </script></body></html>
@@ -0,0 +1,52 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Paddle Cup</title></head><body>
2
+ <script src="lib:room"></script>
3
+ <script src="lib:game"></script>
4
+ <script>
5
+ // A knockout tournament for 2 to 8 players. Each match is one-on-one paddle ball, first to 3; everyone else watches it live.
6
+ // The shell runs the tournament (multiplayer.cup): it draws the bracket, seats the two players of each match and moves the winner on.
7
+ // This file is only one match: the two seated players move their paddles, the host moves the ball and ends the match with g.gameOver(winnerId).
8
+ const W = 800, H = 500, PAD = 90, GOAL = 3;
9
+ let y;
10
+ const paddleY = (g, seat) => { const p = g.players.find((q) => q.id === g.seats[seat]); return p && typeof p.state.y === "number" ? p.state.y : H / 2; };
11
+ const serve = (dir) => ({ x: W / 2, y: H / 2, dx: dir * 330, dy: Math.random() * 240 - 120 });
12
+ Game({
13
+ title: "Paddle Cup", help: "A knockout tournament. Up and down move your paddle. First to 3 wins the match and goes on.", size: [W, H],
14
+ multiplayer: { max: 8, seats: 2, cup: true, countdown: 3, seatNames: ["Left", "Right"], player: { y: [0, H] } },
15
+ setup(g) { y = H / 2; g.me.set({ y }); if (g.isHost) g.shared.set({ ball: serve(Math.random() < 0.5 ? -1 : 1), points: { 0: 0, 1: 0 }, score: {}, winner: "" }); },
16
+ update(g, dt) { y = Math.max(PAD / 2, Math.min(H - PAD / 2, y + g.axis().y * 420 * dt)); g.me.set({ y }); },
17
+ host(g, dt) {
18
+ const s = g.shared; if (!s.ball || s.winner) return;
19
+ const ids = g.players.map((p) => p.id), here = g.seats.filter((id) => id && ids.includes(id));
20
+ if (here.length < 2) return end(g, here[0] || g.seats[0]); // the other player left: a walkover
21
+ let { x, y: by, dx, dy } = s.ball;
22
+ x += dx * dt; by += dy * dt;
23
+ if (by < 8 || by > H - 8) { dy = -dy; by = Math.max(8, Math.min(H - 8, by)); }
24
+ for (const seat of [0, 1]) {
25
+ const px = seat ? W - 28 : 28, py = paddleY(g, seat);
26
+ if ((seat ? dx > 0 && x > px - 10 && x < px + 12 : dx < 0 && x < px + 10 && x > px - 12) && Math.abs(by - py) < PAD / 2 + 8) { dx = -dx * 1.05; dy += (by - py) * 5; x = px + (seat ? -10 : 10); }
27
+ }
28
+ if (x < -10 || x > W + 10) {
29
+ const seat = x < 0 ? 1 : 0, n = (s.points[seat] || 0) + 1, id = g.seats[seat];
30
+ g.shared.set({ points: { [seat]: n }, score: { [id]: n }, ball: serve(seat ? -1 : 1) });
31
+ if (n >= GOAL) end(g, id);
32
+ return;
33
+ }
34
+ g.shared.set({ ball: { x, y: by, dx, dy } });
35
+ },
36
+ draw(g, ctx, w, h) {
37
+ const s = g.shared;
38
+ ctx.fillStyle = "#0f172a"; ctx.fillRect(0, 0, w, h);
39
+ ctx.strokeStyle = "#334155"; ctx.lineWidth = 4; ctx.setLineDash([14, 14]); ctx.beginPath(); ctx.moveTo(w / 2, 0); ctx.lineTo(w / 2, h); ctx.stroke(); ctx.setLineDash([]);
40
+ ctx.textAlign = "center"; ctx.textBaseline = "alphabetic";
41
+ for (const seat of [0, 1]) {
42
+ const p = g.players.find((q) => q.id === g.seats[seat]), px = seat ? W - 28 : 28;
43
+ ctx.fillStyle = seat ? "#f87171" : "#60a5fa"; ctx.fillRect(px - 7, paddleY(g, seat) - PAD / 2, 14, PAD);
44
+ ctx.font = "700 44px system-ui"; ctx.fillText(String((s.points && s.points[seat]) || 0), w / 2 + (seat ? 70 : -70), 56);
45
+ ctx.font = "15px system-ui"; ctx.fillText(p ? (p.me ? "You" : p.name) : "", w / 2 + (seat ? 200 : -200), 30);
46
+ }
47
+ if (s.ball) { ctx.fillStyle = "#f8fafc"; ctx.beginPath(); ctx.arc(s.ball.x, s.ball.y, 8, 0, 7); ctx.fill(); }
48
+ if (g.me.watching) { ctx.fillStyle = "rgba(148,163,184,.9)"; ctx.font = "14px system-ui"; ctx.fillText("You are watching this match", w / 2, h - 14); }
49
+ },
50
+ });
51
+ function end(g, id) { g.shared.set({ winner: id }); g.gameOver(id); }
52
+ </script></body></html>
@@ -0,0 +1,137 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Deep Delve</title></head><body>
2
+ <script src="lib:room"></script>
3
+ <script src="lib:game"></script>
4
+ <script>
5
+ // Co-op dungeon for 1 to 4 players with saved characters and a saved dungeon, to come back to days later.
6
+ // - Each player's character (level, xp, gold, potions) is their own: g.profile, kept between visits. Weapon and outfit follow from the level.
7
+ // - The dungeon (depth, monsters and their hp, team lives, everyone's hp and mp) is g.shared. multiplayer.save keeps it, so the lobby offers Continue.
8
+ // - The host runs the monsters and decides rewards; each player writes their own rewards into their own profile.
9
+ const W = 800, H = 600, SPEED = 200, LIVES = 3, MP_COST = 20;
10
+ const WEAPONS = [["Stick", 4], ["Sword", 8], ["Blade", 14]];
11
+ const OUTFITS = [["Tunic", "#94a3b8"], ["Mail", "#38bdf8"], ["Cloak", "#c084fc"]];
12
+ const weaponOf = (lv) => (lv >= 6 ? 2 : lv >= 3 ? 1 : 0), outfitOf = (lv) => (lv >= 5 ? 2 : lv >= 2 ? 1 : 0);
13
+ const maxHp = (lv) => 50 + 10 * lv, maxMp = (lv) => 20 + 5 * lv;
14
+ const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
15
+ let x = 400, y = 300, seen = { xp: 0, gold: 0, potions: 0 }, note = "", noteAt = 0;
16
+ const hero = (g) => Object.assign({ level: 1, xp: 0, gold: 0, potions: 1, deepest: 0 }, g.profile);
17
+ function look(g) { const h = hero(g); g.me.set({ lv: h.level, wd: WEAPONS[weaponOf(h.level)][1], oi: outfitOf(h.level) }); }
18
+ function spawn(depth) {
19
+ const mobs = {}, n = Math.min(12, 3 + depth * 2);
20
+ for (let i = 0; i < n; i++) {
21
+ const edge = i % 4, t = Math.random();
22
+ mobs["d" + depth + "_" + i] = { x: Math.round(edge === 1 ? W - 30 : edge === 3 ? 30 : t * W), y: Math.round(edge === 0 ? 30 : edge === 2 ? H - 30 : t * H),
23
+ hp: 8 + 4 * depth, max: 8 + 4 * depth, speed: 36 + 5 * depth, bite: 6 + depth, xp: 5 + depth, gold: 2 + depth };
24
+ }
25
+ return mobs;
26
+ }
27
+ function gain(g, dx, dg, dp) {
28
+ const h = hero(g); let xp = h.xp + dx, level = h.level;
29
+ while (xp >= 20 * level) { xp -= 20 * level; level++; }
30
+ g.profile.set({ level, xp, gold: h.gold + dg, potions: h.potions + dp, deepest: Math.max(h.deepest, (g.shared.depth || 1)) });
31
+ look(g);
32
+ note = level > h.level ? "Level " + level + "! New gear: " + WEAPONS[weaponOf(level)][0] + ", " + OUTFITS[outfitOf(level)][0] : "+" + dx + " xp +" + dg + " gold" + (dp ? " +potion" : "");
33
+ noteAt = performance.now();
34
+ }
35
+ Game({
36
+ title: "Deep Delve", help: "Move with arrows, WASD or the pad. Space or tap swings; Bolt (E) costs mp; Potion (Q) heals. Your character is saved: level, gear and potions stay for next time.",
37
+ size: [W, H], buttons: [{ id: "bolt", label: "Bolt", key: "e" }, { id: "potion", label: "Potion", key: "q" }],
38
+ multiplayer: { max: 4, save: true, player: { x: [0, W], y: [0, H], lv: [0, 99], wd: [0, 99], oi: [0, 2] } },
39
+ setup(g) {
40
+ x = 360 + Math.random() * 80; y = 270 + Math.random() * 60; g.me.set({ x, y }); look(g);
41
+ seen = Object.assign({ xp: 0, gold: 0, potions: 0 }, (g.shared.earn || {})[g.me.id]); // a continued dungeon: rewards given before now are already in the profile
42
+ note = g.resumed ? "Continued at depth " + g.shared.depth : "";
43
+ noteAt = performance.now();
44
+ },
45
+ update(g, dt) {
46
+ const s = g.shared, a = g.axis(), e = (s.earn || {})[g.me.id];
47
+ x = Math.max(20, Math.min(W - 20, x + a.x * SPEED * dt)); y = Math.max(20, Math.min(H - 20, y + a.y * SPEED * dt));
48
+ g.me.set({ x, y });
49
+ if (e && (e.xp !== seen.xp || e.gold !== seen.gold || (e.potions || 0) !== seen.potions)) {
50
+ const d = { xp: (e.xp || 0) - seen.xp, gold: (e.gold || 0) - seen.gold, p: (e.potions || 0) - seen.potions };
51
+ seen = { xp: e.xp || 0, gold: e.gold || 0, potions: e.potions || 0 };
52
+ gain(g, d.xp, d.gold, d.p);
53
+ }
54
+ },
55
+ onInput(g, a) {
56
+ if (a === "action") { g.act("swing"); g.fx("swing", { x, y }); }
57
+ else if (a === "bolt") g.act("bolt");
58
+ else if (a === "potion") { const h = hero(g); if (h.potions > 0) { g.profile.set({ potions: h.potions - 1 }); g.act("heal", { n: 40 }); } }
59
+ },
60
+ onAction(g, from, name, data) {
61
+ const s = g.shared, p = g.players.find((q) => q.id === from);
62
+ if (!p || s.over || !s.mobs) return false;
63
+ const lv = p.state.lv || 1, hp = {}, mp = {}, mobs = {}, earn = {};
64
+ if (name === "heal") { hp[from] = Math.min(maxHp(lv), (s.hp[from] ?? maxHp(lv)) + Math.min(60, Number(data && data.n) || 0)); return g.shared.set({ hp }); }
65
+ let reach = 70, dmg = p.state.wd || 4;
66
+ if (name === "bolt") {
67
+ const m = s.mp[from] ?? maxMp(lv);
68
+ if (m < MP_COST) return false;
69
+ mp[from] = m - MP_COST; reach = 170; dmg = 10 + 2 * lv;
70
+ } else if (name !== "swing") return false;
71
+ for (const [id, m] of Object.entries(s.mobs)) {
72
+ if (dist(m, p.state) > reach) continue;
73
+ if (m.hp - dmg > 0) { mobs[id] = { hp: m.hp - dmg }; continue; }
74
+ mobs[id] = null;
75
+ const old = (earn[from] || (s.earn || {})[from] || {});
76
+ earn[from] = { xp: (old.xp || 0) + m.xp, gold: (old.gold || 0) + m.gold, potions: (old.potions || 0) + (Math.random() < 0.25 ? 1 : 0) };
77
+ }
78
+ g.shared.set({ mobs, mp, earn });
79
+ },
80
+ host(g, dt) {
81
+ const s = g.shared;
82
+ if (!s.depth) return g.shared.set({ depth: 1, lives: LIVES, over: false, mobs: spawn(1), hp: {}, mp: {}, earn: {}, score: {} });
83
+ if (s.over) { if (g.now > s.endAt) g.gameOver(); return; }
84
+ const players = g.players.filter((p) => p.online && typeof p.state.x === "number"), hp = {}, mp = {};
85
+ for (const p of players) {
86
+ const lv = p.state.lv || 1;
87
+ if (s.hp[p.id] == null) hp[p.id] = maxHp(lv);
88
+ const m = s.mp[p.id] ?? maxMp(lv);
89
+ if (m < maxMp(lv)) mp[p.id] = Math.min(maxMp(lv), Math.round((m + 3 * dt) * 10) / 10);
90
+ else if (s.mp[p.id] == null) mp[p.id] = m;
91
+ }
92
+ const mobs = {};
93
+ for (const [id, m] of Object.entries(s.mobs)) {
94
+ let t = null, td = 1e9;
95
+ for (const p of players) { const d = dist(p.state, m); if (d < td) { t = p; td = d; } }
96
+ if (!t) continue;
97
+ if (td > 24) mobs[id] = { x: m.x + (t.state.x - m.x) / td * m.speed * dt, y: m.y + (t.state.y - m.y) / td * m.speed * dt };
98
+ else hp[t.id] = (hp[t.id] ?? s.hp[t.id] ?? maxHp(t.state.lv || 1)) - m.bite * dt;
99
+ }
100
+ let lives = s.lives;
101
+ for (const p of players) if (hp[p.id] != null && hp[p.id] <= 0) { lives--; hp[p.id] = maxHp(p.state.lv || 1); }
102
+ const patch = { mobs, hp, mp };
103
+ if (lives !== s.lives) patch.lives = lives;
104
+ if (lives <= 0) { const score = {}; for (const p of players) score[p.id] = (s.depth - 1) * 100; Object.assign(patch, { over: true, endAt: g.now + 4, score }); g.clearSave(); }
105
+ else if (!Object.keys(s.mobs).length) { // depth cleared: everyone heals, a deeper level starts
106
+ const full = {}; for (const p of players) full[p.id] = maxHp(p.state.lv || 1);
107
+ Object.assign(patch, { depth: s.depth + 1, mobs: spawn(s.depth + 1), hp: full });
108
+ }
109
+ g.shared.set(patch);
110
+ },
111
+ draw(g, ctx, w, h) {
112
+ const s = g.shared, h0 = hero(g);
113
+ ctx.fillStyle = "#1c1917"; ctx.fillRect(0, 0, w, h);
114
+ ctx.strokeStyle = "#292524"; ctx.lineWidth = 1; for (let i = 0; i <= w; i += 50) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, h); ctx.stroke(); } for (let j = 0; j <= h; j += 50) { ctx.beginPath(); ctx.moveTo(0, j); ctx.lineTo(w, j); ctx.stroke(); }
115
+ for (const m of Object.values(s.mobs || {})) {
116
+ if (typeof m.x !== "number") continue;
117
+ ctx.fillStyle = "#b91c1c"; ctx.beginPath(); ctx.arc(m.x, m.y, 14, 0, 7); ctx.fill();
118
+ ctx.fillStyle = "#450a0a"; ctx.fillRect(m.x - 14, m.y - 24, 28, 4); ctx.fillStyle = "#f87171"; ctx.fillRect(m.x - 14, m.y - 24, 28 * Math.max(0, m.hp / m.max), 4);
119
+ }
120
+ ctx.font = "12px system-ui"; ctx.textAlign = "center";
121
+ for (const p of g.players) {
122
+ const st = p.state; if (typeof st.x !== "number") continue;
123
+ ctx.fillStyle = OUTFITS[st.oi || 0][1]; ctx.beginPath(); ctx.arc(st.x, st.y, 15, 0, 7); ctx.fill();
124
+ ctx.strokeStyle = p.me ? "#fde047" : "#0c0a09"; ctx.lineWidth = 2; ctx.stroke();
125
+ ctx.fillStyle = "#e7e5e4"; ctx.fillText(p.name + " L" + (st.lv || 1), st.x, st.y - 30);
126
+ const f = Math.max(0, (s.hp && s.hp[p.id] != null ? s.hp[p.id] : maxHp(st.lv || 1)) / maxHp(st.lv || 1));
127
+ ctx.fillStyle = "#14532d"; ctx.fillRect(st.x - 16, st.y - 25, 32, 4); ctx.fillStyle = "#4ade80"; ctx.fillRect(st.x - 16, st.y - 25, 32 * f, 4);
128
+ }
129
+ ctx.textAlign = "left"; ctx.font = "600 14px system-ui"; ctx.fillStyle = "#e7e5e4";
130
+ ctx.fillText("Depth " + (s.depth || 1) + " Lives " + (s.lives == null ? LIVES : s.lives) + " Monsters " + Object.keys(s.mobs || {}).length, 12, 22);
131
+ const mine = g.me.id && s.mp ? s.mp[g.me.id] : null;
132
+ ctx.font = "13px system-ui";
133
+ ctx.fillText("Lv " + h0.level + " " + WEAPONS[weaponOf(h0.level)][0] + " / " + OUTFITS[outfitOf(h0.level)][0] + " xp " + h0.xp + "/" + 20 * h0.level + " gold " + h0.gold + " potions " + h0.potions + (mine != null ? " mp " + Math.floor(mine) : "") + (g.saves ? " (saved)" : ""), 12, h - 12);
134
+ if (note && performance.now() - noteAt < 3500) { ctx.textAlign = "center"; ctx.font = "600 18px system-ui"; ctx.fillStyle = "#fde047"; ctx.fillText(note, w / 2, 60); }
135
+ },
136
+ });
137
+ </script></body></html>