What you missed, in facts; a timeline on the reel; room for table talk
A returning player's slip now lists what happened while they were away — "Automaton took your treasure.", "You lost 3 life.", "It's your turn." — each fact opening the catch-up reel at its moment, read from the missed steps as the server redacted them for that seat; when nothing touched them it says what each wizard was up to instead. The reel gains a timeline: a tick per move in the mover's color, taller where a turn begins, a blow lands, or gold changes hands, and any tick jumps the reel there. The chronicle takes a filter — All, Game, Table talk — with an unread count on the talk that arrived while the casting panel covered it (a wizard's own words never count), a peek at the latest line above the composer while the panel is up, and the composer itself no longer leaves when a response is owed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
80dd7a7865
commit
8582b7feb7
@@ -0,0 +1,109 @@
|
||||
// What happened while you were away, as a few facts before the reel:
|
||||
// read from the missed steps as the server redacted them for you, so it
|
||||
// tells you only what you would have seen at the table.
|
||||
|
||||
import { cardDef, type GameEvent, type GameView } from "@wizwar/engine";
|
||||
|
||||
export interface MissedStep {
|
||||
seq: number;
|
||||
actor: string;
|
||||
events: GameEvent[];
|
||||
view: GameView;
|
||||
chat?: { player: string; text: string }[];
|
||||
}
|
||||
|
||||
export interface MissedFact {
|
||||
text: string;
|
||||
/** The step of the reel this fact belongs to. */
|
||||
step: number;
|
||||
}
|
||||
|
||||
function spell(id: string | null): string {
|
||||
if (!id) return "a punch";
|
||||
try { return cardDef(id).name; } catch { return id; }
|
||||
}
|
||||
|
||||
function nearHome(view: GameView, you: string, cell: { x: number; y: number }): boolean {
|
||||
const me = view.players.find((p) => p.id === you);
|
||||
if (!me) return false;
|
||||
return Math.abs(me.home.x - cell.x) + Math.abs(me.home.y - cell.y) <= 2;
|
||||
}
|
||||
|
||||
/** The facts of the missed steps that touch you, in order, capped so the
|
||||
* slip stays a slip. Each fact points at its step for the reel. */
|
||||
export function summarizeMissed(steps: readonly MissedStep[], you: string, now: GameView | null): MissedFact[] {
|
||||
const facts: MissedFact[] = [];
|
||||
let lifeLost = 0;
|
||||
let lifeLostStep = -1;
|
||||
let talk = 0;
|
||||
steps.forEach((s, i) => {
|
||||
talk += s.chat?.length ?? 0;
|
||||
for (const e of s.events) {
|
||||
switch (e.type) {
|
||||
case "treasurePickedUp":
|
||||
if (e.owner === you && e.player !== you) facts.push({ text: `${e.player} took your treasure.`, step: i });
|
||||
else if (e.player !== you && e.owner !== e.player) facts.push({ text: `${e.player} picked up ${e.owner}'s treasure.`, step: i });
|
||||
break;
|
||||
case "treasureDropped": {
|
||||
const owner = s.view.treasures.find((t) => t.id === e.treasureId)?.owner;
|
||||
if (e.onHomeOf != null && owner && e.onHomeOf !== owner) {
|
||||
facts.push({ text: owner === you ? `Your treasure was carried home by ${e.player}.` : `${e.player} carried ${owner}'s treasure home.`, step: i });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "damaged":
|
||||
if (e.player === you && e.amount > 0) { lifeLost += e.amount; if (lifeLostStep < 0) lifeLostStep = i; }
|
||||
break;
|
||||
case "spellCast":
|
||||
if (e.target === you) facts.push({ text: `${e.caster} cast ${spell(e.cardId)} at you.`, step: i });
|
||||
break;
|
||||
case "punched":
|
||||
if (e.target === you) facts.push({ text: `${e.attacker} punched you.`, step: i });
|
||||
break;
|
||||
case "wallDestroyed":
|
||||
if (nearHome(s.view, you, e.edge.cell)) facts.push({ text: "A wall near your home was destroyed.", step: i });
|
||||
break;
|
||||
case "playerEliminated":
|
||||
facts.push({ text: e.player === you ? "You were eliminated." : `${e.player} is out of the game.`, step: i });
|
||||
break;
|
||||
case "gameWon":
|
||||
facts.push({ text: e.player === you ? "You won." : `${e.player} won the game.`, step: i });
|
||||
break;
|
||||
case "tableTalk":
|
||||
talk++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (lifeLost > 0) facts.splice(Math.min(facts.length, 1), 0, { text: `You lost ${lifeLost} life.`, step: lifeLostStep });
|
||||
if (facts.length === 0) {
|
||||
// Nothing touched you: say what each wizard was up to, all the same.
|
||||
const doings = new Map<string, { moves: number; casts: number; first: number }>();
|
||||
steps.forEach((s, i) => {
|
||||
if (s.actor === you) return;
|
||||
const d = doings.get(s.actor) ?? { moves: 0, casts: 0, first: i };
|
||||
for (const e of s.events) {
|
||||
if (e.type === "moved" && e.player === s.actor) d.moves++;
|
||||
if (e.type === "spellCast" && e.caster === s.actor) d.casts++;
|
||||
}
|
||||
doings.set(s.actor, d);
|
||||
});
|
||||
for (const [who, d] of doings) {
|
||||
const bits = [d.moves > 0 ? `walked ${d.moves} square${d.moves === 1 ? "" : "s"}` : "", d.casts > 0 ? `cast ${d.casts} spell${d.casts === 1 ? "" : "s"}` : ""].filter(Boolean);
|
||||
facts.push({ text: `${who} ${bits.length ? bits.join(" and ") : "passed the time"}; nothing touched you.`, step: d.first });
|
||||
}
|
||||
}
|
||||
if (talk > 0) facts.push({ text: `${talk} line${talk === 1 ? "" : "s"} of table talk.`, step: -1 });
|
||||
const out = facts.slice(0, 6);
|
||||
if (facts.length > 6) out.push({ text: `…and ${facts.length - 6} more.`, step: -1 });
|
||||
if (now) {
|
||||
const me = now.players.find((p) => p.id === you);
|
||||
if (now.phase === "playing" && me?.alive) {
|
||||
const mine = now.activePlayerId === you || now.stack?.waitingOn === you || now.pendingDiscard === you;
|
||||
out.push({ text: mine ? "It's your turn." : `It's ${now.activePlayerId}'s turn.`, step: -1 });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user