tickcli-js 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +360 -2
- package/client/game.js +982 -0
- package/client/game3d.js +109 -0
- package/client/relay.js +196 -0
- package/client/room.js +632 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/hub.d.ts +619 -0
- package/dist/hub.js +1777 -0
- package/dist/hub.js.map +1 -0
- package/dist/node.d.ts +106 -0
- package/dist/node.js +301 -0
- package/dist/node.js.map +1 -0
- package/dist/remote.d.ts +75 -0
- package/dist/remote.js +214 -0
- package/dist/remote.js.map +1 -0
- package/dist/serve.d.ts +42 -0
- package/dist/serve.js +231 -0
- package/dist/serve.js.map +1 -0
- package/examples/games/arena.html +108 -0
- package/examples/games/arena3d.html +110 -0
- package/examples/games/chess.html +132 -0
- package/examples/games/cup.html +52 -0
- package/examples/games/delve.html +137 -0
- package/examples/games/dungeon.html +110 -0
- package/examples/games/neon-paint.html +79 -0
- package/examples/games/own-engine.html +41 -0
- package/examples/games/race.html +83 -0
- package/examples/games/shooter.html +95 -0
- package/examples/games/sky-duel.html +136 -0
- package/examples/games/space-squad.html +3627 -0
- package/examples/games/tag.html +51 -0
- package/examples/games/tictactoe.html +53 -0
- package/package.json +101 -4
- package/src/cli.ts +59 -0
- package/src/hub.ts +1521 -0
- package/src/node.ts +245 -0
- package/src/remote.ts +236 -0
- package/src/serve.ts +224 -0
|
@@ -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>
|