diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8fe5b9b..4910b8f 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -108,6 +108,8 @@ interface ShareData { steps: { actor: PlayerId; events: unknown[]; view: unknown }[]; actor: string; round: number; + /** A whole finished game rather than one turn. */ + whole?: boolean; } const shareCache = new Map(); function shareData(id: string): ShareData | null { @@ -117,9 +119,21 @@ function shareData(id: string): ShareData | null { const share = getShare(id); const room = share ? getRoom(share.roomId) : undefined; if (share && room?.state) { - const reel = momentSteps(room, SPECTATOR, share.turn); - if (!("error" in reel) && reel.steps.length > 0) { - data = { steps: reel.steps as unknown as ShareData["steps"], actor: reel.owner, round: reel.round }; + if (share.turn < 0) { + // The whole tale: a finished game from the deal to the crown. + const steps = catchUpSteps(room, SPECTATOR, 0, true); + if (!("error" in steps) && steps.length > 0) { + let winner = ""; + for (const st of steps) { + for (const e of st.events) if (e.type === "gameWon" && "player" in e) winner = e.player; + } + data = { steps: steps as unknown as ShareData["steps"], actor: winner, round: 0, whole: true }; + } + } else { + const reel = momentSteps(room, SPECTATOR, share.turn); + if (!("error" in reel) && reel.steps.length > 0) { + data = { steps: reel.steps as unknown as ShareData["steps"], actor: reel.owner, round: reel.round }; + } } } if (shareCache.size > 50) shareCache.clear(); @@ -143,9 +157,13 @@ function shareHtml(id: string, data: ShareData, rawHost: string, rawProto: strin .replace(/]*>\s*/g, "") .replace(/[^<]*<\/title>\s*/, ""); const base = `${proto}://${host}`; - const title = `${escapeHtml(data.actor)}'s turn — a Wiz-War instant replay`; - const desc = `Round ${data.round || "?"} of a game of Wiz-War, magical combat in a stone labyrinth. ` + - `Watch the turn through ${escapeHtml(data.actor)}'s own eyes, then deal yourself in.`; + const title = data.whole + ? "The whole tale — a game of Wiz-War, replayed" + : `${escapeHtml(data.actor)}'s turn — a Wiz-War instant replay`; + const desc = data.whole + ? `A full game of Wiz-War, magical combat in a stone labyrinth — every turn through its wizard's own eyes${data.actor ? `, to ${escapeHtml(data.actor)}'s triumph` : ""}. Watch it all, then deal yourself in.` + : `Round ${data.round || "?"} of a game of Wiz-War, magical combat in a stone labyrinth. ` + + `Watch the turn through ${escapeHtml(data.actor)}'s own eyes, then deal yourself in.`; const metas = [ `<title>${title}`, ``, @@ -193,7 +211,7 @@ const httpServer = createServer((req, res) => { "cache-control": "public, max-age=60", "access-control-allow-origin": "*", }); - res.end(JSON.stringify({ steps: data.steps, actor: data.actor, round: data.round })); + res.end(JSON.stringify({ steps: data.steps, actor: data.actor, round: data.round, whole: data.whole === true })); return; } const watch = url.match(/^\/watch\/([a-z0-9]{4,20})(\/og\.png)?$/); @@ -607,14 +625,17 @@ wss.on("connection", (socket) => { const room = session.roomId ? getRoom(session.roomId) : undefined; if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" }); const turn = Number(msg.turn); - if (!Number.isInteger(turn) || turn < 0) return send(socket, { type: "error", message: "no such turn" }); + if (!Number.isInteger(turn) || turn < -1) return send(socket, { type: "error", message: "no such turn" }); const now = Date.now(); if (now - session.lastCatchUpAt < CATCHUP_COOLDOWN_MS) { return send(socket, { type: "error", message: "one moment" }); } session.lastCatchUpAt = now; - const reel = momentSteps(room, session.playerId, turn); - if ("error" in reel) return send(socket, { type: "error", message: reel.error }); + // turn -1 shares the whole finished game; anything else, one turn. + const check = turn === -1 + ? catchUpSteps(room, session.playerId, 0, true) + : momentSteps(room, session.playerId, turn); + if ("error" in check) return send(socket, { type: "error", message: check.error }); const share = mintShare(room.id, turn); send(socket, { type: "share", id: share.id, turn }); break; diff --git a/packages/web/public/terrain3d/safe.png b/packages/web/public/terrain3d/safe.png new file mode 100644 index 0000000..7cfb3b6 Binary files /dev/null and b/packages/web/public/terrain3d/safe.png differ diff --git a/packages/web/public/textures/cracks.png b/packages/web/public/textures/cracks.png new file mode 100644 index 0000000..fe46b76 Binary files /dev/null and b/packages/web/public/textures/cracks.png differ diff --git a/packages/web/public/textures/doorframe.png b/packages/web/public/textures/doorframe.png new file mode 100644 index 0000000..204fd7d Binary files /dev/null and b/packages/web/public/textures/doorframe.png differ diff --git a/packages/web/public/textures/underbrush.png b/packages/web/public/textures/underbrush.png new file mode 100644 index 0000000..b3f85a4 Binary files /dev/null and b/packages/web/public/textures/underbrush.png differ diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 1930d96..a0f4ab8 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1623,7 +1623,9 @@ net.requestShare()} onclose={() => net.closeMoment()} /> {:else if net.catchUp && net.catchUp.length > 0} - net.closeCatchUp()} /> + net.requestShare(-1) : null} + onclose={() => net.closeCatchUp()} /> {:else if local.replaySteps && local.replaySteps.length > 0} (local.replaySteps = null)} /> {/if} diff --git a/packages/web/src/Replay.svelte b/packages/web/src/Replay.svelte index 88a4f51..72e6aaa 100644 --- a/packages/web/src/Replay.svelte +++ b/packages/web/src/Replay.svelte @@ -462,7 +462,7 @@ {/if} - {#if moment && onshare} + {#if onshare}