diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 59568b5..7b434b8 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -2350,7 +2350,7 @@ const CARD_EFFECTS: Record const caster = activePlayer(state); if (!target) return null; const d = Math.abs(target.position.x - caster.position.x) + Math.abs(target.position.y - caster.position.y); - return d <= 1 ? null : "disease spreads by touch — you must be adjacent"; + return d <= 1 ? null : "disease spreads by touch, so you must be adjacent"; }, sustains: true, }, @@ -2434,7 +2434,7 @@ const CARD_EFFECTS: Record validate: (state, _cmd, _caster, target) => { if ((state.config.deckRev ?? 1) < 10 || !target) return null; const { d, legs } = charge(state, activePlayer(state), target); - if (d > legs) return "the goat charges on legs, not wings — you cannot walk that far this turn"; + if (d > legs) return "the goat's legs cannot carry the charge that far this turn"; return null; }, onResolved: (ctx) => { @@ -2758,11 +2758,11 @@ const CARD_EFFECTS: Record }, "opportunity-fire": { kind: "neutral", - resolve: () => "played out of turn — wait for another player's turn, then use it", + resolve: () => "this is played during another player's turn", }, interrupt: { kind: "neutral", - resolve: () => "played out of turn — use it during another player's turn", + resolve: () => "this is played during another player's turn", }, "thumb-of-god": { kind: "neutral", @@ -3061,7 +3061,7 @@ function emptySquareTarget( if ((state.groundObjects[key] ?? []).length > 0) return "an object lies there"; // "You can't create an object on a DIMENSIONAL WARP". if (state.dimWarps.some((w) => cellKey(w.a) === key || cellKey(w.b) === key)) { - return "the warp shimmers there — nothing can form on it"; + return "nothing can form where the warp shimmers"; } if (!castSight(state, caster, cmd, cell)) return "no line of sight"; return cell; @@ -3417,7 +3417,7 @@ function doCreatureAttack(prev: GameState, creatureId: string, targetId: string) if (creature.justCreated) return err("it cannot attack the turn it was created"); if (creature.attackUsed) return err("that creature has already attacked this turn"); if (creature.kind !== "troll" && creature.kind !== "skeleton" && creature.kind !== "shadow") { - return err("that creature attacks on its own, not on command"); + return err("that creature picks its own victims and takes no orders to attack"); } const events: GameEvent[] = []; @@ -4068,7 +4068,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman if (playerId !== state.stack.waitingOn) return err("waiting for another player's response"); if (command.type === "counteract") return doCounteract(state, playerId, command.instanceId, command.params, command.numberInstanceIds); if (command.type === "pass") return doPass(state, playerId); - return err("an attack is being resolved — counteract or pass"); + return err("an attack is being resolved, so counteract or pass"); } if (state.chaosPending) { @@ -4095,7 +4095,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman finishChaosIfReady(st, events); return { ok: true, state: st, events }; } - return err("chaos is coming — shield your hand or pass"); + return err("chaos is coming, so shield your hand or pass"); } // INTERRUPT / OPPORTUNITY FIRE: an out-of-turn action window. @@ -4131,7 +4131,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman if (!fx || fx.kind !== "attack") return err("opportunity fire permits an attack"); } if (kind === "interrupt" && command.type === "punch") { - return err("interrupt lets you cast a spell, not brawl"); + return err("an interruption allows a spell but not a punch"); } st.outOfTurnWindow = null; const result = command.type === "cast" ? doCast(st, command) : doPunch(st, command.targetId); @@ -4362,7 +4362,7 @@ function doMove(prev: GameState, direction: Side, over = false, exit?: Side, sho const shimmer = state.illusionWalls[key]; if (shimmer && shimmer.createdBy !== p.id && !shimmer.belief[p.id]) { if (isBlinded(state, p)) return blindBump(state, events, p, direction); - return err("the wall shimmers oddly — test your eyes before you can pass"); + return err("the wall shimmers oddly, and you must test your eyes before you can pass"); } if (shimmer && illusionBelief(state, p.id, key) === "believes") { if (isBlinded(state, p)) return blindBump(state, events, p, direction); @@ -4517,7 +4517,7 @@ function doMove(prev: GameState, direction: Side, over = false, exit?: Side, sho } landing = canShoveTo(exits.includes(direction) ? direction : exits[0]!); } else { - if (!exits.includes(chosen)) { p.position = from; return err(`no way to push them ${chosen} — the corridor leads ${exits.join(" or ")}`); } + if (!exits.includes(chosen)) { p.position = from; return err(`no way to push them ${chosen}, because the corridor leads ${exits.join(" or ")}`); } landing = canShoveTo(chosen); } } else { @@ -4568,16 +4568,16 @@ function doMove(prev: GameState, direction: Side, over = false, exit?: Side, sho const bare = (state.config.deckRev ?? 1) >= 17 || floor.length === 0 ? exits : floor; if (exits.length === 0) { p.position = from; - return err("the pit cannot be crossed from here — nothing to land on beside it"); + return err("the pit cannot be crossed from here, with nothing to land on beside it"); } if (exit !== undefined && !exits.includes(exit)) { p.position = from; - return err(`no footing that way — the pit's rim leads ${exits.join(" or ")}`); + return err(`no footing that way, because the pit's rim leads ${exits.join(" or ")}`); } landing = exit ?? (bare.length === 1 ? bare[0]! : null); if (landing === null) { p.position = from; - return err(`the rim leads more than one way — click the square to land on: ${exits.join(" or ")}`); + return err(`the rim leads ${exits.join(" or ")}, so click the square to land on`); } const foot = stepTarget(view, pitCell, landing) as { kind: "step" | "warp"; to: Cell }; landingCell = foot.to; @@ -4762,7 +4762,7 @@ function doPlayNumberForMovement(prev: GameState, instanceId: string, addInstanc if (blocked) return err(blocked); const mover = activePlayer(prev); if (sustainedOn(prev, mover.id, "slow").length > 0) { - return err("you are slowed — no number cards for movement"); + return err("a slowed wizard cannot play number cards for movement"); } // "You may add two NUMBER cards together for any single action" — an ADD // permits one extra movement number this turn. @@ -4815,7 +4815,7 @@ function attackPreconditions(state: GameState): string | null { } return "you may attack only once per turn"; } - if (state.turn.attackForbidden) return "you are slowed — no attack this turn"; + if (state.turn.attackForbidden) return "a slowed wizard cannot attack this turn"; return null; } @@ -4834,7 +4834,7 @@ function castingBlocked( opts?: { counteraction?: boolean; cardId?: string; targetPlayerId?: PlayerId; pickedCardId?: string }, ): string | null { if (sustainedOn(state, playerId, "medusa").length > 0) return "you are paralyzed by Medusa"; - if (sustainedOn(state, playerId, "no-spell").length > 0) return "No Spell — you cannot cast"; + if (sustainedOn(state, playerId, "no-spell").length > 0) return "No Spell holds you, and you cannot cast"; // IDIOT: "He can do nothing else but cast COUNTERACTION spells (if // needed)" — and per the FAQ, spells that aid the march to the treasure. if (sustainedOn(state, playerId, "idiot").length > 0 && @@ -4887,17 +4887,17 @@ function liftIdiotIfSatisfied(state: GameState, events: GameEvent[], p: PlayerSt function attackBlockedByStatus(state: GameState, attacker: PlayerState, target: PlayerState): string | null { if (sustainedOn(state, target.id, "big-man").length > 0 && cellKey(attacker.position) === cellKey(target.position)) { - return "he fills the corridor — there is no room to swing"; + return "he fills the corridor and leaves no room to swing"; } if (inThornbush(state, attacker)) return "you cannot attack from inside a thornbush"; if (inThornbush(state, target)) return "you cannot attack someone in a thornbush"; - if (isMisted(state, attacker.id)) return "you are mist — you may not attack"; + if (isMisted(state, attacker.id)) return "as mist, you may not attack"; if (isMisted(state, target.id)) return "your target is mist and cannot be attacked"; // BUDDY: "Opponent will not attack you unless you attack first." const buddy = state.sustained.find( (s) => s.cardId === "buddy" && s.casterId === target.id && s.targetId === attacker.id, ); - if (buddy) return "the Buddy pact holds — you cannot bring yourself to attack them"; + if (buddy) return "the Buddy pact holds, and you cannot bring yourself to attack them"; return null; } @@ -4986,7 +4986,7 @@ function doWardChoice(prev: GameState, play: boolean): CommandResult { /** Arm (or stand down) the WARD trap on your treasures. Your secret. */ /** Compat stub for stale clients: the Ward has no arming step. */ function doArmWard(): CommandResult { - return err("the Ward is played in the moment — you will be asked when your treasure is grabbed"); + return err("the Ward is played in the moment, and you will be asked when your treasure is grabbed"); } /** Player ids in seat order, starting after `fromId`. */ @@ -5032,7 +5032,7 @@ function doTestIllusion(prev: GameState, cell: Cell, side: Side): CommandResult const key = edgeKey(cell, side); const wall = state.illusionWalls[key]; if (!wall) return err("there is no illusion there"); - if (wall.createdBy === p.id) return err("you made it — you know exactly what it is"); + if (wall.createdBy === p.id) return err("you made it, so you know exactly what it is"); if (wall.belief[p.id]) return err("your eyes have already ruled on that wall"); const events: GameEvent[] = []; const board = doorsAjar(state, p.id, openedDoors(state, perceivedBoard(state, p.id))); @@ -5368,13 +5368,13 @@ function doCast(prev: GameState, cmd: Extract): Comma const ego = state.creatures.find((c) => c.id === cmd.via); if (!ego || ego.kind !== "alter-ego" || ego.controllerId !== caster.id) return err("that is not your double"); if (!cmd.target) return err("your double casts only at a target"); - if (inHand.cardId === "butt-head" || inHand.cardId === "swap") return err(`${def.name} moves your own body — not your double's to cast`); + if (inHand.cardId === "butt-head" || inHand.cardId === "swap") return err(`${def.name} moves your own body, so your double cannot cast it`); origin = ego.position; } const effect = CARD_EFFECTS[inHand.cardId]; if (!effect) return err(`${def.name} is not implemented yet`); if (effect.kind === "counter") { - return err(`${def.name} is a counteraction — play it in response to an attack`); + return err(`${def.name} is a counteraction, played in response to an attack`); } if (effect.kind === "neutral" && effect.displayOnce && caster.displayed.includes(inHand.instanceId)) { return err(`${def.name} is already displayed`); @@ -5914,7 +5914,7 @@ function doSetAmbush(prev: GameState, cmd: Extract { type: "cast", instanceId: bh.instanceId, target: { kind: "player", playerId: defender.id }, }); if (!r.ok) { - if (/legs, not wings/.test(r.error)) refusals += 1; + if (/cannot carry the charge that far/.test(r.error)) refusals += 1; continue; } const st = drain(r.state); @@ -839,7 +839,7 @@ describe("butt-head's charge reaches as far as its legs (rev 16)", () => { const card = giveCard(state, goat.id, "butt-head"); const r = applyCommand(state, goat.id, { type: "cast", instanceId: card.instanceId, target: { kind: "player", playerId: mark.id } }); expect(r.ok).toBe(ok); - if (!r.ok) expect(r.error).toMatch(/legs, not wings/); + if (!r.ok) expect(r.error).toMatch(/cannot carry the charge that far/); }); } }); diff --git a/packages/server/src/banter.ts b/packages/server/src/banter.ts index 07a797b..ba4bf0b 100644 --- a/packages/server/src/banter.ts +++ b/packages/server/src/banter.ts @@ -97,7 +97,7 @@ export const BOT_LINES: RecordWiz-War — clips from the labyrinth`, + `Wiz-War clips from the labyrinth`, ``, ``, - ``, - ``, + ``, + ``, ``, ...(clips[0] ? cardImageMetas(clips[0], base) : []), ``, @@ -115,13 +115,13 @@ export function clipsIndexHtml(clips: ClipMeta[], base: string): string { Each clip has a first-person take and a board take of the same moves.

${cards}

Wiz-War is Tom Jolly's classic of magical combat. - This is a fan-built table — deal yourself in.

`; + This is a fan-built table, and you can deal yourself in.

`; return shell(metas, body); } export function clipPageHtml(clip: ClipMeta, base: string): string { const metas = [ - `${esc(clip.title)} — a Wiz-War clip`, + `${esc(clip.title)} · Wiz-War clip`, ``, ``, ``, @@ -152,7 +152,7 @@ export function clipPageHtml(clip: ClipMeta, base: string): string { -

Played by the real rules engine — every move on screen is a legal move. +

Played by the real rules engine, so every move on screen is a legal move. Deal yourself in.

`; return shell(metas, body); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 71c72c5..8098580 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -257,7 +257,7 @@ function ogPage(metas: string[]): string { /** Host and proto arrive from request headers — attacker-writable text * that must never reach an HTML attribute raw. */ const noSuchClip = (res: ServerResponse) => - res.writeHead(404, { "content-type": "text/plain" }).end("no such clip — see /clips"); + res.writeHead(404, { "content-type": "text/plain" }).end("no such clip (see /clips)"); /** The absolute origin a request came in on, as the proxy saw it. */ function requestBase(req: IncomingMessage): string { @@ -282,9 +282,9 @@ function headlineOf(data: ShareData): string | null { for (const step of data.steps) { for (const e of step.events) { switch (e.type) { - case "gameWon": offer(9, e.reason === "treasures" ? `${e.player} wins with two treasures home` : `${e.player} is the last wizard standing`); break; + case "gameWon": offer(9, e.reason === "treasures" ? `${e.player} wins with two stolen treasures home` : `${e.player} is the last wizard standing`); break; case "playerEliminated": offer(8, e.reason === "killed" ? `${e.player} falls in the maze` : `${e.player} loses both treasures`); break; - case "treasureDropped": if (e.onHomeOf === e.player) offer(7, `${e.player} carries a stolen treasure home`); break; + case "treasureDropped": if (e.onHomeOf === e.player) offer(7, `${e.player} carries a treasure home`); break; case "treasurePickedUp": if (e.owner !== e.player) offer(5, `${e.player} snatches ${e.owner}'s treasure`); break; case "attackResolved": if (e.damageDealt > 0) offer(6, `${e.attacker} hits ${e.defender} with ${name(e.attackCardId)} for ${e.damageDealt}`); else if (e.fullyStopped) offer(4, `${e.defender} stops ${e.attacker}'s ${name(e.attackCardId)} cold`); break; case "punched": offer(3, `${e.attacker} punches ${e.target}`); break; @@ -306,12 +306,12 @@ function shareHtml(id: string, data: ShareData, rawHost: string, rawProto: strin const base = safeBase(rawHost, rawProto); const headline = data.whole ? null : headlineOf(data); const title = data.whole - ? "The whole tale — a game of Wiz-War, replayed" + ? "A whole game of Wiz-War, replayed" : headline - ? `${escapeHtml(headline)} — a Wiz-War instant replay` - : `${escapeHtml(data.actor)}'s turn — a Wiz-War instant replay`; + ? `${escapeHtml(headline)} · Wiz-War instant replay` + : `${escapeHtml(data.actor)}'s turn · 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.` + ? `A full game of Wiz-War, magical combat in a stone labyrinth, with every turn seen 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 = [ @@ -336,11 +336,11 @@ function inviteHtml(room: Room, rawHost: string, rawProto: string): string { const base = safeBase(rawHost, rawProto); const seats = room.players.length; const wizards = `${seats} wizard${seats === 1 ? "" : "s"}`; - const title = `You're summoned — Wiz-War room ${room.id}`; + const title = `You're summoned to Wiz-War room ${room.id}`; const desc = room.state?.phase === "finished" - ? `The tale is told — ${wizards} fought in this labyrinth. Follow the link to see how it ended.` + ? `The tale is told. ${wizards} fought in this labyrinth. Follow the link to see how it ended.` : room.state - ? `The duel is underway, ${wizards} in the labyrinth. Follow the link to watch it live from the Peanut Gallery.` + ? `The game is underway, with ${wizards} in the labyrinth. Follow the link to watch it live from the Peanut Gallery.` : `${wizards} at the table, waiting to flip the boards. Follow the link, pick a name, and take a seat.`; const metas = [ `${title}`, @@ -727,7 +727,7 @@ function broadcastRoomState(room: Room): void { wss.on("connection", (socket, req) => { if (sessions.size >= MAX_SOCKETS) { - send(socket, { type: "error", message: "the tavern is packed — try again shortly" }); + send(socket, { type: "error", message: "the tavern is packed; try again shortly" }); socket.close(); return; } @@ -773,10 +773,10 @@ wss.on("connection", (socket, req) => { const name = cleanName(msg.name); if (!name) return send(socket, { type: "error", message: "name required" }); if (session.roomsCreated >= MAX_ROOMS_PER_CONN || roomCount() >= MAX_ROOMS) { - return send(socket, { type: "error", message: "no new rooms right now — try again later" }); + return send(socket, { type: "error", message: "no new rooms are opening right now, so try again later" }); } if (!roomsPerAddress.allow(session.address)) { - return send(socket, { type: "error", message: "that's a lot of new tables from one place — try again in an hour" }); + return send(socket, { type: "error", message: "this address can open more tables in an hour" }); } session.roomsCreated++; leaveGallery(session); @@ -847,7 +847,7 @@ wss.on("connection", (socket, req) => { const room = getRoom(roomId); if (!room) return send(socket, { type: "error", message: "no such room" }); if (audienceCount(room) >= MAX_AUDIENCE) { - return send(socket, { type: "error", message: "the gallery is packed — try again later" }); + return send(socket, { type: "error", message: "the gallery is full for now; try again later" }); } leaveGallery(session); // switching galleries updates the old room's count session.playerId = null; @@ -1014,7 +1014,7 @@ wss.on("connection", (socket, req) => { } case "claimTransfer": { if (session.claimFails >= 5) { - return send(socket, { type: "error", message: "too many attempts on this connection — reconnect and mint a fresh phrase" }); + return send(socket, { type: "error", message: "too many attempts on this connection, so reconnect and mint a fresh phrase" }); } const result = claimTransferCode(String(msg.code ?? "")); if ("error" in result) { @@ -1031,7 +1031,7 @@ wss.on("connection", (socket, req) => { // A catch-up replays the whole game server-side; one at a time, please. const now = Date.now(); if (now - session.lastCatchUpAt < CATCHUP_COOLDOWN_MS) { - return send(socket, { type: "error", message: "catching up already — one moment" }); + return send(socket, { type: "error", message: "already catching up, so give it a moment" }); } session.lastCatchUpAt = now; const steps = catchUpSteps(room, session.playerId, Number(msg.sinceSeq ?? 0), msg.full === true); @@ -1047,7 +1047,7 @@ wss.on("connection", (socket, req) => { 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: "catching up already — one moment" }); + return send(socket, { type: "error", message: "already catching up, so give it a moment" }); } session.lastCatchUpAt = now; // turn -1 shares the whole finished game; anything else, one turn. @@ -1065,7 +1065,7 @@ wss.on("connection", (socket, req) => { if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" }); const now = Date.now(); if (now - session.lastCatchUpAt < CATCHUP_COOLDOWN_MS) { - return send(socket, { type: "error", message: "catching up already — one moment" }); + return send(socket, { type: "error", message: "already catching up, so give it a moment" }); } session.lastCatchUpAt = now; const reel = momentSteps(room, session.playerId, Number(msg.turn ?? -1)); @@ -1083,13 +1083,13 @@ wss.on("connection", (socket, req) => { } case "feedback": { const room = session.roomId ? getRoom(session.roomId) : undefined; - if (!room) return send(socket, { type: "error", message: "join a room first — a report rides its ledger" }); + if (!room) return send(socket, { type: "error", message: "join a room first, since every report is filed with its game" }); const clean = (raw: unknown) => String(raw ?? "").replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, " ").trim().slice(0, 2000); const happened = clean(msg.happened); if (!happened) return send(socket, { type: "error", message: "say what happened" }); if (!reportsPerAddress.allow(session.address)) { - return send(socket, { type: "error", message: "the desk has plenty from you for now — more in an hour" }); + return send(socket, { type: "error", message: "the desk has plenty from you for now; write again in an hour" }); } const reportId = randomBytes(4).toString("hex"); appendFeedback({ @@ -1117,7 +1117,7 @@ wss.on("connection", (socket, req) => { // seat is proven as the ledger proves it, and the report must be // theirs. Each answer rings the desk's bell. if (!reportsPerAddress.allow(session.address)) { - return send(socket, { type: "error", message: "the desk has plenty from you for now — more in an hour" }); + return send(socket, { type: "error", message: "the desk has plenty from you for now; write again in an hour" }); } const seat = (msg.seat ?? {}) as Record; const roomId = String(seat.roomId ?? "").toUpperCase(); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 33bdaec..8a33665 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -319,8 +319,8 @@ export function joinRoom( * once the deal happens, every seat is a player in the game's record. */ export function kickSeat(room: Room, byId: PlayerId, name: PlayerId): string | null { if (byId !== room.hostId) return "only the host may clear a seat"; - if (room.state) return "the game has started — seats are settled"; - if (name === room.hostId) return "the host cannot kick themselves — abandon the room instead"; + if (room.state) return "the game has started and its seats are settled"; + if (name === room.hostId) return "the host cannot clear their own seat, but can abandon the room"; if (!room.players.includes(name)) return "no such seat"; room.players = room.players.filter((p) => p !== name); room.tokens.delete(name); @@ -361,7 +361,7 @@ export function resolveColors(room: Room): number[] { } export function pickColor(room: Room, playerId: PlayerId, color: number): string | null { - if (room.state) return "the game has started — your robes are dyed"; + if (room.state) return "the game has started and your robes are already dyed"; if (!room.players.includes(playerId)) return "you hold no seat in this room"; if (!Number.isInteger(color) || color < 0 || color > 5) return "no such wizard"; for (const [other, c] of room.colorChoices) { @@ -373,7 +373,7 @@ export function pickColor(room: Room, playerId: PlayerId, color: number): string function startInMemory(room: Room, expansion: boolean, colors?: number[], deckRev?: number): { events: GameEvent[] } | { error: string } { const n = room.players.length; - if (n < 2 || n > 6) return { error: "supported player counts: 2 to 6" }; + if (n < 2 || n > 6) return { error: "a table seats 2 to 6 wizards" }; const { state, events } = createGame({ playerIds: room.players, seed: room.seed, @@ -764,7 +764,7 @@ export function makeTransferCode( for (const [code, t] of transfers) { if (t.expiresAt < now) transfers.delete(code); } - if (transfers.size >= 200) return { error: "too many transfers pending — try again in a few minutes" }; + if (transfers.size >= 200) return { error: "too many seat transfers are waiting; try again in a few minutes" }; let code: string; do { code = Array.from({ length: 4 }, () => TRANSFER_WORDS[randomInt(TRANSFER_WORDS.length)]).join("-"); @@ -788,7 +788,7 @@ export function claimTransferCode(code: string): { roomId: string; name: PlayerI failedClaims = 0; } if (failedClaims >= FAIL_LIMIT) { - return { error: "too many failed claims — transfers are cooling off, mint a fresh phrase" }; + return { error: "too many failed claims have cleared every phrase, so wait a few minutes and mint a fresh one" }; } const normalized = code.trim().toLowerCase().replace(/\s+/g, "-"); const t = transfers.get(normalized); diff --git a/packages/web/index.html b/packages/web/index.html index c01963c..8e634c7 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -4,19 +4,19 @@ Wiz-War - + - + - + diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 93e5f30..41c8752 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -167,8 +167,8 @@ if (view && me && me.carriedTreasureId && cellKey(me.position) === cellKey(me.home)) { const t = view.treasures.find((t) => t.id === me!.carriedTreasureId); reasons.push(t && t.owner !== view.you - ? `you stand on your home with ${t.owner}'s treasure in your arms — drop it first and it scores` - : "you stand on your home with your own treasure in your arms — drop it first and it is safe"); + ? `you stand on your home with ${t.owner}'s treasure in your arms, and dropping it first scores it` + : "you stand on your home with your own treasure in your arms, and dropping it first keeps it safe"); } if (view && me) { const misted = view.sustained.some((e) => e.cardId === "mist-body" && e.targetId === view.you); @@ -180,8 +180,8 @@ const bite: Record = { thornbush: "the thorns end your turn, cost you the NEXT one, and prick for 1", tacks: "the tacks bite for 3", - pit: "the pit: roll a 1 and you fall in, hurt and stuck", - ooze: "the ooze burns 1 — and may drop you flat, treasure and all", + pit: "the pit takes you on a roll of 1, hurt and stuck", + ooze: "the ooze burns 1 and may drop you flat, treasure and all", }; if (bite[ground]) reasons.push(bite[ground]); } @@ -205,7 +205,7 @@ if (e.type === "handRevealedPrivate" && e.visibleTo === view.you) { cardReveal = { title: `${e.player}'s mind, laid bare`, - note: "Commit them to memory — the glimpse does not linger.", + note: "Commit them to memory, for the glimpse does not linger.", cards: e.cards, }; } @@ -213,7 +213,7 @@ const pub = events.find((s) => s.type === "cardsStolen" && s.to === view!.you); cardReveal = { title: pub?.type === "cardsStolen" ? `Plucked from ${pub.from}'s thoughts` : "Plucked from their thoughts", - note: "Yours now — already in your hand.", + note: "Yours now, already in your hand.", cards: e.cards, }; } @@ -221,7 +221,7 @@ const pub = events.find((s) => s.type === "handTaken" && s.to === view!.you); cardReveal = { title: pub?.type === "handTaken" ? `The spoils of ${pub.from}` : "The spoils", - note: "The fallen wizard's hand — already in yours.", + note: "The fallen wizard's hand, already in yours.", cards: e.cards, }; } @@ -293,7 +293,7 @@ hunter: "A hunter goes for the treasure and carries it home.", berserker: "A berserker goes for blood, and worries about treasure later.", worrier: "A worrier keeps to the shadows and guards what is theirs.", - mystery: "Mystery deals one of the three at random; you learn which when the game ends.", + mystery: "Mystery deals one of the three at random, and you learn which when the game ends.", } as const; /** Card whose official FAQ rulings are open. */ let faqCardId = $state(null); @@ -522,20 +522,20 @@ const WAND_IDS = new Set(["blaster-wand", "sticky-wand", "shift-wand", "warp-wand"]); /** What the edge click actually is, per card — "wall line" fits few. */ const EDGE_HINTS: Record = { - "create-wall": "click an open corridor line to wall it", - "destroy-wall": "click a wall or door to destroy it", - "illusion-wall": "click an open corridor line for the false wall", - "wall-of-fire": "click a corridor line for the fire", - "waterwall": "click a corridor line, or a slime to wash it away — the wave collapses at once", - "pick-lock": "click a locked door", - "master-key": "click a locked door — or Display it once and walk through every lock", - "jam-lock": "click a door to jam its lock solid", - "remove-lock": "click a door to strip its lock for good", - "create-door": "click a wall for the new door", - "warp-wand": "click a wall to warp it open", - "stone-to-water": "click a stone wall or door, or a stone-filled square", - "thumb-of-god": "aim the die at a square in sight — it may drift on impact", - "dispel-creation": "click the creation — a square, a creature, or a conjured wall", + "create-wall": "Click an open corridor line to wall it.", + "destroy-wall": "Click a wall or door to destroy it.", + "illusion-wall": "Click an open corridor line for the false wall.", + "wall-of-fire": "Click a corridor line for the fire.", + "waterwall": "Click a corridor line, or a slime to wash it away. The wave collapses at once.", + "pick-lock": "Click a locked door.", + "master-key": "Click a locked door, or Display it once and walk through every lock.", + "jam-lock": "Click a door to jam its lock solid.", + "remove-lock": "Click a door to strip its lock for good.", + "create-door": "Click a wall for the new door.", + "warp-wand": "Click a wall to warp it open.", + "stone-to-water": "Click a stone wall or door, or a stone-filled square.", + "thumb-of-god": "Aim the die at a square in sight. It may drift on impact.", + "dispel-creation": "Click the creation (a square, a creature, or a conjured wall).", }; const EDGE_CARDS = new Set([ "create-wall", "destroy-wall", "illusion-wall", "wall-of-fire", "waterwall", @@ -1082,8 +1082,8 @@ if (selectedCard) { const name = cardDef(selectedCard.cardId).name; net.flash(EDGE_CARDS.has(selectedCard.cardId) - ? `${name} is aimed at a line between squares: ${EDGE_HINTS[selectedCard.cardId] ?? "tap a lit line"}` - : `${name} is aimed by tapping a wizard, a creature, a bush, or a slime — put the card down to walk`); + ? `${name} is aimed at a line between squares. ${EDGE_HINTS[selectedCard.cardId] ?? "Tap a lit line."}` + : `${name} is aimed by tapping a wizard, a creature, a bush, or a slime. Put the card down to walk.`); return; } // A cell click is a move if the cell is one legal step away (the server @@ -1373,14 +1373,14 @@ const room = limit - view.yourHand.length; if (drawCount > room) { reasons.push(room <= 0 - ? "your hand is full — the draw will bring nothing" + ? "your hand is full, so the draw will bring nothing" : `your hand only has room for ${room} of the ${drawCount}-card draw`); } if (me.carriedTreasureId && cellKey(me.position) === cellKey(me.home)) { const t = view.treasures.find((t) => t.id === me!.carriedTreasureId); reasons.push(t && t.owner !== view.you - ? `${t.owner}'s treasure is still in your arms — drop it here and it scores` - : "your own treasure is still in your arms — drop it here and it is safe"); + ? `${t.owner}'s treasure is still in your arms, and dropping it here scores it` + : "your own treasure is still in your arms, and dropping it here keeps it safe"); } // Treasure underfoot un-grabbed — unless the auto-grab preference (or // this caution's own "do it anyway") is about to handle it, or the @@ -1399,7 +1399,7 @@ // Auto-grab about to pocket your OWN chest: carrying it does not // score and bares it to capture, so that walk-off asks first. if (autoWillGrab && !grabRefused && treasuresHere[0]!.owner === view.you) { - reasons.push("your OWN treasure lies underfoot — ending the turn grabs it and carries it along"); + reasons.push("your OWN treasure lies underfoot, and ending the turn grabs it and carries it along"); } // The draw picker is sticky: a 0 or 1 chosen turns ago quietly starves. const drawRoom = Math.min(2, room); @@ -1494,22 +1494,22 @@ /** What each ongoing spell does, in a sentence. */ const EFFECT_GLOSS: Record = { slow: "Moves one square a turn, no numbers or speed spells, and attacks only every other turn.", - "no-spell": "Cannot cast any spell; a magic stone's power still works, and cards may be discarded.", - medusa: "Cannot move or cast, counteractions included — and takes no damage from anything.", - "lock-in-place": "Cannot move or be moved in any way, teleportation included; spells may still be cast.", - blind: "Movement, thrown objects, attacks, and spells go in a direction the die decides; self-cast spells work.", + "no-spell": "Cannot cast any spell. A magic stone's power still works, and cards may be discarded.", + medusa: "Cannot move or cast, counteractions included, and takes no damage from anything.", + "lock-in-place": "Cannot move or be moved in any way, teleportation included, but spells may still be cast.", + blind: "Movement, thrown objects, attacks, and spells go in a direction the die decides, though self-cast spells work.", invisible: "An attacker must roll the die for a random direction to see whether the blow finds them.", shrink: "Half as likely to be hit by any attack, and walks only two squares a turn.", adrenaline: "May make two attacks in one turn.", - "big-man": "Fills the corridor: nobody moves, casts, or punches past them, or enters their square; they can push what stands in their way.", - "mist-body": "Passes through anything but stone; neither attacks nor can be attacked; walls of fire still burn.", - disease: "Carries the plague: sharing a square with another wizard costs each of them three points.", + "big-man": "Fills the corridor, so nobody moves, casts, or punches past them, or enters their square. They can push what stands in their way.", + "mist-body": "Passes through anything but stone, and neither attacks nor can be attacked. Walls of fire still burn them.", + disease: "Carries the plague, so sharing a square with another wizard costs each of them three points.", empathy: "Any attack against them lands on the attacker as well.", - fear: "Nobody comes within three squares, walls or no walls; those in range must move away.", + fear: "Nobody comes within three squares, walls or no walls, and those in range must move away.", strength: "Physical damage they deal is doubled, and they may tear a treasure from a wizard sharing their square.", buddy: "Will not attack the wizard who cast it, until attacked first.", lifesaver: "Not eliminated for losing both treasures to enemy homes.", - "force-field": "Stops the spell it answered, then stands: the opponent may not enter this square, nor cast on or past this wizard, on any side.", + "force-field": "Stops the spell it answered, then stands. The opponent may not enter this square, nor cast on or past this wizard, on any side.", "around-the-corner": "May cast line-of-sight spells around one corner.", }; function effectNote(e: Sustained): string { @@ -1518,7 +1518,7 @@ const ends = e.cardId === "force-field" ? "Stands until the end of the opponent's turn." : e.cardId === "buddy" ? "Holds until the pact is broken by an attack." - : isPermanentDuration(e.remainingTurns) ? "Permanent — until dispelled, or the game ends." + : isPermanentDuration(e.remainingTurns) ? "Permanent, until dispelled or the game ends." : e.remainingTurns === 1 ? `Ends at the start of ${e.casterId}'s next turn.` : `Ends at the start of ${e.casterId}'s turn, ${e.remainingTurns} of their turns from now.`; return `${gloss} ${who} ${ends}`; @@ -1557,7 +1557,7 @@ const lost = net.chatUnsent; if (!lost) return; if (!chatDraft.trim()) chatDraft = lost; - net.flash("Your message was not sent — it is back in the box"); + net.flash("Your message was not sent. It is back in the box."); net.chatUnsent = null; }); @@ -1578,7 +1578,7 @@ function pickImage(e: Event) { const file = (e.currentTarget as HTMLInputElement).files?.[0] ?? null; if (fbPreview) URL.revokeObjectURL(fbPreview); - if (file && file.size > 2_500_000) { net.flash("That picture is over 2.5 MB — a smaller one, please"); fbImage = null; fbPreview = null; return; } + if (file && file.size > 2_500_000) { net.flash("That picture is over 2.5 MB. A smaller one, please."); fbImage = null; fbPreview = null; return; } fbImage = file; fbPreview = file ? URL.createObjectURL(file) : null; } @@ -1896,7 +1896,7 @@ const FAVICON_IDLE = "/favicon.png"; const FAVICON_TURN = "/favicon-turn.png"; $effect(() => { - document.title = anyTurnWaiting ? "● Your turn — Wiz-War" : "Wiz-War"; + document.title = anyTurnWaiting ? "● Your turn · Wiz-War" : "Wiz-War"; let link = document.querySelector('link[rel="icon"]') as HTMLLinkElement | null; if (!link) { link = document.createElement("link"); @@ -1926,7 +1926,7 @@ const t = chatDraft.trim(); if (!t) return; if (net.sendChat(t)) chatDraft = ""; - else net.flash("Not sent — the table is out of reach; your words wait in the box"); + else net.flash("Not sent. The table is out of reach, and your words wait in the box."); }}> {#if net.chatPending}sending…{/if} {#if net.chatPending}sending…{/if} @@ -1990,13 +1990,13 @@ {:else if net.roomId} - {#if net.you === net.hostId && net.roomId && (net.audience > 0 || net.galleryTalk)} {:else if net.audience > 0} 👁 {net.audience}{net.galleryTalk ? " 💬" : ""} @@ -2005,7 +2005,7 @@ {/if} - {/if}
@@ -2055,7 +2055,7 @@
{/if}
- {spellName(peekEffect.cardId)} — {effectNote(peekEffect)} + {spellName(peekEffect.cardId)}. {effectNote(peekEffect)}
{:else if peekEdge}
{peekEdge}
@@ -2082,16 +2082,16 @@ onclick={(ev) => ev.stopPropagation()} onkeydown={() => {}}>
That wall shimmers strangely…
- Someone conjured an ILLUSION WALL here. Whether YOUR eyes are fooled - is a 50/50 roll — once. Believe it, and it is stone to you until - dispelled; see through it, and you may walk it like open floor. - Other wizards' eyes rule for themselves. + Someone conjured an ILLUSION WALL here. One 50/50 roll decides, + once, whether YOUR eyes are fooled. Believe it, and it is stone to + you until dispelled. See through it, and you may walk it like open + floor. Every other wizard tests it with their own eyes.
{#if isYourTurn} {:else} - — you can test it on your turn — + You can test it on your turn. {/if}
@@ -2145,7 +2145,7 @@
{view.pushPending.giantId} bears down on you!
The giant fills the corridor and shoves you ahead of him. The way - forks here — which way do you go? + forks here. Which way do you go?
{#each view.pushPending.exits as d (d)} @@ -2166,19 +2166,19 @@
Slow Death bites for {sdPoints}!
- The draw's bites land as one blow — counter the total: Absorb - soaks up to three points, Blunt halves it (fractions round up). + The draw's bites land as one blow, so counter the total. Absorb + soaks up to three points, and Blunt halves it (fractions round up).
{#if sdAbsorb} + Absorb: take {Math.max(0, sdPoints - 3)} instead {/if} {#if sdBlunt} + Blunt: take {Math.ceil(sdPoints / 2)} instead {/if} @@ -2192,9 +2192,9 @@ onclick={() => (feedbackOpen = false)} onkeydown={() => {}}>
{/if} @@ -2458,18 +2458,18 @@ (faqCardId = id)} /> (faqCardId = id)} />
-
an ILLUSIONARY {cardDef(illusionOf).name} — it bites only if believed
+
an ILLUSIONARY {cardDef(illusionOf).name}, which bites only if believed
{:else}
{/if} {#if view.stack.reflectedBase}
- REFLECTED — {view.stack.reflectedBase.damage} damage inbound unless you answer it + REFLECTED, with {view.stack.reflectedBase.damage} damage inbound unless you answer it
{:else if view.stack.numberValue != null || view.stack.amplifyFactor > 1}
{view.stack.numberValue != null ? `powered by a ${view.stack.numberValue}` : ""} - {view.stack.amplifyFactor > 1 ? ` — AMPLIFIED ×${view.stack.amplifyFactor}` : ""} + {view.stack.amplifyFactor > 1 ? `, AMPLIFIED ×${view.stack.amplifyFactor}` : ""}
{/if} {:else if view.stack.defenderId === view.you && view.stack.sourceName} @@ -2479,7 +2479,7 @@
(faqCardId = id)} />
{/if}
- {view.stack.sourceName.toUpperCase()} — {view.stack.params?.damage ?? 0} damage inbound unless you answer it + {view.stack.sourceName.toUpperCase()}, with {view.stack.params?.damage ?? 0} damage inbound unless you answer it
{:else if view.stack.defenderId === view.you}
@@ -2499,12 +2499,12 @@ {#if broken.length > 0 && standing.length === 0}
Your {broken.map((c) => cardDef(c.card.cardId).name).join(" and ")} - {broken.length === 1 ? "was" : "were"} nullified — the attack comes on unchecked. + {broken.length === 1 ? "was" : "were"} nullified, and the attack comes on unchecked.
{:else if standing.length > 0}
Your {standing.map((c) => cardDef(c.card.cardId).name).join(" and ")} - stand{standing.length === 1 ? "s" : ""} — only what slips past + stand{standing.length === 1 ? "s" : ""}, so only what slips past {standing.length === 1 ? "it" : "them"} will land. Counter again, or let it resolve.
{/if} @@ -2531,7 +2531,7 @@ {#if net.transferCode}
- Speak this phrase into your other device (good for 10 minutes, one use): + On your other device, claim this phrase within ten minutes. It works once. {net.transferCode.code}
@@ -2596,7 +2596,7 @@
A turn-based wizard board game for two to six, free in your browser, with friends or clockwork opponents. Carry home two treasures that aren't yours, or be the last wizard standing.
@@ -2619,7 +2619,7 @@
@@ -2635,9 +2635,9 @@
- More ways to play — pass one device around, or claim a transferred seat + More ways to play, passing one device around or claiming a transferred seat
- or play here, passing the device: + Play here on one device, passed from wizard to wizard
{#each [2, 3, 4, 5, 6] as n (n)}
{/each} {#if net.feedbackReports.length > 0} -
your reports to the wizards
+
your reports to the keeper
{#each net.feedbackReports as r (r.id)} {@const thread = r.thread ?? []}
@@ -2715,15 +2715,15 @@
“{r.happened}”{#if r.image} 📷{/if}
{#each thread as line, j (j)} {#if line.from === "player"} -
you — {line.text}
+
you: {line.text}
{:else} -
{line.status} — {line.text}
+
{line.status} {line.text}
{/if} {/each} {#if !thread.length} -
the wizards are studying the moment…
+
the keeper is studying the moment
{:else if thread[thread.length - 1]!.from === "player"} -
your word is with the wizards…
+
your word is with the keeper
{/if} {#if thread.some((l) => l.from === "desk")}
{ @@ -2731,7 +2731,7 @@ const t = (reportAnswers[r.id] ?? "").trim(); if (t && net.answerReport(r, t)) reportAnswers = { ...reportAnswers, [r.id]: "" }; }}> - +
{/if} @@ -2752,8 +2752,8 @@ {#if net.spectating} {#if net.players.length < 6} -
Join {net.hostId ?? "the"}'s game
-
Choose your wizard's name and take a seat; the boards flip when the host starts.
+
{net.hostId ? `Join ${net.hostId}'s game` : "Join the game"}
+
Choose your wizard's name and take a seat. The boards flip when the host begins.
{/if}
@@ -2898,7 +2897,7 @@ {#if net.galleryTalk} {@render gallerySayBox("lobby-say")} {:else} -

👁 The gallery listens; the host has not opened its mouth.

+

👁 The gallery listens. The host has not opened its mouth.

{/if}
{/if} @@ -2906,7 +2905,7 @@ {#if net.you === net.hostId} {/if} {#if selectedCard?.cardId === "boobytrap"} - — place 4 tokens ({trapCells.length}/4; the first is real) + Place 4 tokens ({trapCells.length}/4). The first is real. {/if} {#if selectedCard?.cardId === "redirection"} - {tradeFrom ? "— now the exit it should connect TO" : "— click the first exit"} + {tradeFrom ? "Now the exit it should connect TO." : "Click the first exit."} {:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)} - {tradeFrom ? "— now the second square" : "— click the first square"} + {tradeFrom ? "Now the second square." : "Click the first square."} {/if} {#if selectedCard?.cardId === "teleport" && !youMustRespond} {pendingTeleport - ? "— tap the marked square again to jump, or pick another" - : "— tap a destination (up to four squares, walls ignored)"} + ? "Tap the marked square again to jump, or pick another." + : "Tap a destination up to four squares away, walls ignored."} {#if pendingTeleport} {/if} {/if} {#if selectedCard?.cardId === "mega-monster"} - — tap the monster, then choose whether to double its life-points or its movement + Tap the monster, then choose whether to double its life-points or its movement. {/if} {#if selectedCard?.cardId === "pick-lock" || selectedCard?.cardId === "master-key"} @@ -3038,19 +3037,19 @@ {/if} {#if selectedCard?.cardId === "rotate-sector"} {#if pendingRotate} - — the marked sector turns a quarter + The marked sector turns a quarter. {:else} - — tap a square in the sector to turn + Tap a square in the sector to turn. {/if} {/if} {#if selectedCard?.cardId === "relocate-sector"} {pendingSectorFrom ? (relocateGhosts?.length - ? "— now tap a dashed landing beside the maze" - : "— that sector has nowhere legal to go; pick another") - : "— click the sector to move"} + ? "Now tap a dashed landing beside the maze." + : "That sector has nowhere legal to go. Pick another.") + : "Click the sector to move."} {/if} {#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)} @@ -3066,23 +3065,23 @@ } }}>Retrieve {:else} - — then click the target + Then click the target. {/if} {/if} {#if selectedCard?.cardId === "swap-meet"} {#if !swapMeetTarget} - — click the other trader + Click the other trader. {:else if myTradables.length === 0} - — you carry no items to trade + You carry no items to trade. {:else if !swapMine} - — trade away your: + Trade away your {#each myTradables as c (c.key)} {/each} {:else if theirTradables.length === 0} - — they show no item you could claim + They show no item you could claim. {:else} - — and take their: + and take their {#each theirTradables as c (c.key)} {/each} @@ -3114,14 +3113,14 @@ }}>discard it instead {/if} {#if selectedCard?.cardId === "add"} - — ADD rides a second NUMBER: play one number for movement, - then play another, and the Add spends itself + ADD rides a second NUMBER. Play one number for movement, + then play another, and the Add spends itself. {/if} {#if selectedCard && isNumberCard(selectedCard.cardId)} {@const needsAdd = view.turn.numberPlayedForMovement} {@const haveAdd = view.yourHand.some((c) => c.cardId === "add")} {#if needsAdd && !haveAdd} - — a second movement number needs an ADD card + A second movement number needs an ADD card. {:else} {:else} - {net.rematchCall.by} calls for a rematch — + {net.rematchCall.by} calls for a rematch. {/if} {:else if otherHumans} - + {:else if net.you === net.hostId} {/if} @@ -3256,26 +3255,26 @@ {:else if youMustRespond}
{#if reflectSwapCard} - Your reflection turns the trade over to you — + Your reflection turns the trade over to you. {#if !reflectSwapMine} - give your: + Give your {#each reflectMyTradables as c (c.key)} {/each} {:else} - and take their: + and take their {#each reflectTheirTradables as c (c.key)} {/each} {/if} {:else if view.stack?.defenderId === view.you} - Under attack — respond by your hand. + Under attack. Answer from your hand. {#if view.stack.attackCard} {/if} {:else} - Your spell is countered — respond by your hand. + Your spell is countered. Answer from your hand. {#if view.stack?.counters.length} {@const latest = [...view.stack.counters].reverse().find((c) => !c.nullified)} {#if latest} @@ -3289,30 +3288,30 @@ {:else if view.pushPending && view.pushPending.pusheeId !== view.you}
Waiting on {view.pushPending.pusheeId} to choose which way they are pushed…
{:else if view.outOfTurnWindow?.playerId === view.you} -
Your interruption — pick an attack card and a target, or
+
Your interruption. Pick an attack card and a target, or
{:else if isYourTurn}
- Your turn — round {view.turn.round}. + Your turn, round {view.turn.round}. {#if prefs.finishedGames === 0 && view.turn.round <= 2}
Win by carrying two treasures that aren't yours to your home, or by being the last wizard standing.
{/if} {#if view.turn.actionsEnded} {#if me?.carriedTreasureId} - your actions for this turn are over, and the treasure comes with you — + Your actions for this turn are over, and the treasure comes with you. End turn now, then carry it home to score. {:else} - your actions for this turn are over — End turn when ready. + Your actions for this turn are over. End turn when ready. {/if} {:else} {view.turn.movementAllowance - view.turn.movementUsed} moves left{view.turn.attackUsed ? " · attack spent" : ""} · treasures home {treasuresHome} of 2 {#if movesSpent} -
Out of steps — cast a spell, or End turn.
+
Out of steps. Cast a spell, or End turn.
{/if} {/if}
{#if scoredThisTurn > 0 && treasuresHome < 2}
- 🏆 Scored — {treasuresHome} of 2. One more stolen treasure carried home wins the game. + 🏆 Scored! {treasuresHome} of 2. One more stolen treasure carried home wins the game.
{/if} {#if !prefs.coached && !view.turn.actionsEnded} @@ -3320,8 +3319,8 @@ seven cards and a maze. Dismissed once, remembered forever. -->
How a turn works. Walk up to {view.turn.movementAllowance} squares by tapping - the square beside your wizard, one step at a time; play a NUMBER card first to walk farther. - Select a card, then choose its target — a wizard, a square, or a wall, as the hint bar says. + the square beside your wizard, one step at a time, and play a NUMBER card first to walk farther. + Select a card, then choose its target (a wizard, a square, or a wall, as the hint bar says). When you're done, End turn. Stuck? Ask the automatons for hints.
@@ -3330,11 +3329,11 @@ {@const chest = view.treasures.find((t) => t.id === me!.carriedTreasureId)} {@const away = dreadDistance(view.board, me!.position, me!.home)}
- 💰 Carrying {chest && chest.owner !== view.you ? `${chest.owner}'s treasure` : "your own treasure"} — + 💰 Carrying {chest && chest.owner !== view.you ? `${chest.owner}'s treasure` : "your own treasure"}. {#if chest && chest.owner !== view.you} - your home is {away} square{away === 1 ? "" : "s"} away; drop it there to score. Two scored wins the game. + Your home is {away} square{away === 1 ? "" : "s"} away, and dropping it there scores. Two scored wins the game. {:else} - it scores nothing in your arms; drop it on your home to keep it safe. + It scores nothing in your arms. Drop it on your home to keep it safe. {/if}
{/if} @@ -3343,22 +3342,22 @@ {/if} {#if interruptCards.length > 0}
- ⚡ Your {cardDef(interruptCards[0]!.cardId).name} can seize this moment — tap the glowing card. + ⚡ Your {cardDef(interruptCards[0]!.cardId).name} can seize this moment. Tap the glowing card.
{/if} {#if youMustShield}
- Chaos comes for your hand — tap your Full Shield to sit out, or + Chaos comes for your hand. Tap your Full Shield to sit out, or
{:else if view.chaosPending} -
Chaos gathers — waiting on {view.chaosPending.queue[0]}…
+
Chaos gathers. Waiting on {view.chaosPending.queue[0]}…
{/if} {#each idleCreatures as c (c.id)} {@const moves = c.movesPerTurn - c.movementUsed}
- 🜲 {c.controllerId === view.you ? "Your" : "The"} {cardDef(c.kind).name} awaits orders — + 🜲 {c.controllerId === view.you ? "Your" : "The"} {cardDef(c.kind).name} awaits orders: {#if moves > 0}{moves} move{moves === 1 ? "" : "s"}{/if}{#if moves > 0 && !c.attackUsed && !c.justCreated} · {/if}{#if !c.attackUsed && !c.justCreated}attack ready{/if}. Tap its token to command it.
@@ -3378,7 +3377,7 @@
{#if councilHints.length > 1 && councilHints.every((h) => h.advice === councilHints![0]!.advice)} -
The council agrees: {councilHints[0]!.advice}
+
The council speaks as one. {councilHints[0]!.advice}
{:else} {#each councilHints as h (h.name)}
@@ -3394,7 +3393,7 @@ view.sustained.some((e) => e.cardId === "fear" && e.targetId === p.id) && me && dreadDistance(view.board, p.position, me.position) <= 3)}
- 😱 An unnatural dread grips you — FEAR requires you to move away + 😱 An unnatural dread grips you. FEAR requires you to move away this turn if you can, even if your only escape is a card (Teleport, Pass Through Wall) or a dangerous square. Only a true dead end excuses you. (The maze trusts you to honor it.) @@ -3402,9 +3401,9 @@ {/if} {#if isYourTurn && view.sustained.some((e) => e.cardId === "idiot" && e.targetId === view.you)}
- 🥴 "What am I doing here…?" — IDIOT drives you to the nearest - of your own treasures. Spend your movement marching toward it; - you may cast only counteractions, spells that speed the trip, + 🥴 "What am I doing here…?" IDIOT drives you to the nearest + of your own treasures. Spend your movement marching toward it. + You may cast only counteractions, spells that speed the trip, and Drop Object to shake your own treasure from a thief's arms. It lifts when you stand on your treasure. (The marching is yours to honor.) @@ -3412,16 +3411,16 @@ {/if} {#if actionsSpent} {#if view.turn.drawForbidden} -
Your own bolt has left you reeling — the rest of this turn is lost, and no cards come with it.
+
Your own bolt has left you reeling. The rest of this turn is lost, and no cards come with it.
{:else} -
Your spellwork is spent for this turn — all that remains is to draw and end it.
+
Your spellwork is spent for this turn, and all that remains is to draw and end it.
{/if} {/if} {#if view.yourAmbushes.length > 0} {#each view.yourAmbushes as a (a.id)}
- 🗡 {cardDef(a.spell.cardId).name} waits — + 🗡 {cardDef(a.spell.cardId).name} waits, {a.trigger.kind === "los" ? "when seen" : a.trigger.kind === "near" ? "when approached" : "when treasure is grabbed"} {#if isYourTurn} @@ -3442,9 +3441,9 @@ {p.life} - {p.handCount} cards{#if p.lostTurns > 0} · dazed {p.lostTurns}{/if}{#if home > 0} · 🏆 {home} home{/if}{#if p.carriedTreasureId}{@const ct = view.treasures.find((t) => t.id === p.carriedTreasureId)}{@const chestSrc = `/tokens-svg/treasure-${colorIndexOf(view, ct?.owner ?? p.id) % 6}.svg`} · + {p.handCount} cards{#if p.lostTurns > 0} · dazed {p.lostTurns}{/if}{#if home > 0} · 🏆 {home} home{/if}{#if p.carriedTreasureId}{@const ct = view.treasures.find((t) => t.id === p.carriedTreasureId)}{@const chestSrc = `/tokens-svg/treasure-${colorIndexOf(view, ct?.owner ?? p.id) % 6}.svg`} · {/if}
@@ -3487,9 +3486,9 @@ {#if blowCard}
(faqCardId = id)} />
{/if} -
{defending ? "" : "your "}{view.stack.sourceName} — it bites for {view.stack.params?.damage ?? 0}
+
{defending ? "" : "your "}{view.stack.sourceName}, which bites for {view.stack.params?.damage ?? 0}
{:else} -
{defending ? "a physical blow" : "your punch"} — no card behind it
+
{defending ? "a physical blow" : "your punch"}, with no card behind it
{/if} {#if view.stack.numberValue != null || view.stack.amplifyFactor > 1}
@@ -3581,16 +3580,17 @@
{#if ambushVia}
- {cardDef(ambushVia.cardId).name} — set an ambush + {cardDef(ambushVia.cardId).name} + Set an ambush. {#if !ambushTrigger} - springs when an opponent… + It springs when an opponent… {:else if !ambushSpell} - — now tap the attack card to commit + Now tap the attack card to commit. {:else if ambushNeedsCell && !ambushCell} - {cardDef(ambushSpell.cardId).name} — click where the trap will send them + {cardDef(ambushSpell.cardId).name}. Click where the trap will send them. {:else} {cardDef(ambushSpell.cardId).name}{ambushCell ? ` → (${ambushCell.x}, ${ambushCell.y})` : ""}{attachedNumber ? ` with a ${numberTotal}` : " (tap a number to power it)"} @@ -3638,7 +3638,7 @@ onclick={pickUp}>Pick up treasure {#if pickChoice} - — whose? + whose? {#each treasuresHere as t (t.id)} {/each} @@ -3662,7 +3662,7 @@
+ {punchWallMode ? "Click the wall to punch, or cancel" : "Punch a wall…"}
@@ -3730,7 +3730,7 @@
{#if net.spectating} - 👁 You watch from the Peanut Gallery — hands stay secret, even from you. + 👁 You watch from the Peanut Gallery, and every hand stays secret, even from you. {/if} {#each view.phase === "finished" ? [] : orderedHand as card (card.instanceId)}
{:else} - {@const lockTitle = e.lock === "removed" ? "lock removed — swings free" - : e.lock === "jammed" ? "lock jammed — sealed for good" - : e.lock === "held" ? "held open by a standing wizard" - : e.lock === "ajar" ? "unlocked until end of turn" : null} + {@const lockTitle = e.lock === "removed" ? "a door with its lock removed, swinging free" + : e.lock === "jammed" ? "a door with its lock jammed, sealed for good" + : e.lock === "held" ? "a door held open by a standing wizard" + : e.lock === "ajar" ? "a door unlocked until the end of the turn" : null} - battle-scarred — {dmg} damage taken + battle-scarred, {dmg} damage taken {/each} @@ -488,9 +488,9 @@ {@const ix = Number(key.split(":")[1]?.split(",")[0])} {@const iy = Number(key.split(":")[1]?.split(",")[1])} {#if kind === "V"} - an illusion — your eyes see through it + an illusion your eyes see through {:else} - an illusion — your eyes see through it + an illusion your eyes see through {/if} {/each} @@ -649,7 +649,7 @@ x={-CELL * 0.26} y={-CELL * 0.26} width={CELL * 0.52} height={CELL * 0.52} cls={`token-art hit${c.id === selectedCreatureId ? " selected-art" : ""}`} - title={`${c.kind} (${c.controllerId}) — ${c.damage}/${Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg`} + title={`${c.kind} (${c.controllerId}), ${c.damage}/${Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg`} /> - {c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg + {c.kind} ({c.controllerId}), {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg {c.kind === "fire-imp" ? "I" : c.kind === "democratic-monster" ? "D" : c.kind[0]?.toUpperCase()} {/if} diff --git a/packages/web/src/EdgeGlyph.svelte b/packages/web/src/EdgeGlyph.svelte index c5debba..c360bfc 100644 --- a/packages/web/src/EdgeGlyph.svelte +++ b/packages/web/src/EdgeGlyph.svelte @@ -27,7 +27,7 @@ const hp = $derived(damage > 0 ? `${Math.max(0, needed - damage)} of ${needed} points left` : `${needed} points to destroy`); - const tip = $derived(`${title ?? (state === "door" ? "a locked door" : "a wall")} — ${hp}`); + const tip = $derived(`${title ?? (state === "door" ? "a locked door" : "a wall")} (${hp})`); const { press, release } = holdToPeek(() => onpeek?.(tip)); diff --git a/packages/web/src/Faq.svelte b/packages/web/src/Faq.svelte index 96673ba..343f0de 100644 --- a/packages/web/src/Faq.svelte +++ b/packages/web/src/Faq.svelte @@ -22,7 +22,7 @@ onkeydown={() => {}} >
- {def.name} — Official FAQ + Official FAQ on {def.name}
@@ -31,7 +31,7 @@ {/each}

From Tom Jolly's official Wiz-War Rules FAQ (wizwar.com, September 2002). - Rulings occasionally reference other editions; the cards overrule the rules, + Rulings occasionally reference other editions. The cards overrule the rules, and the designer overrules the table.

diff --git a/packages/web/src/FirewallEdge.svelte b/packages/web/src/FirewallEdge.svelte index 213e3e2..8370396 100644 --- a/packages/web/src/FirewallEdge.svelte +++ b/packages/web/src/FirewallEdge.svelte @@ -8,7 +8,7 @@ /** Press-and-hold (touch has no hover): report what this fire is. */ onpeek?: (tip: string) => void; } = $props(); - const TIP = "a wall of fire — passing through burns for 4"; + const TIP = "a wall of fire that burns for 4 as you pass through"; const { press, release } = holdToPeek(() => onpeek?.(TIP)); const CELL = 48; const WALL = 7; diff --git a/packages/web/src/Help.svelte b/packages/web/src/Help.svelte index c448d9c..4fc5ae0 100644 --- a/packages/web/src/Help.svelte +++ b/packages/web/src/Help.svelte @@ -164,7 +164,7 @@ {typeLabel(entryDef)} · {sightOf(entryDef)} · {setLabel(entryDef)}

As printed

-

{entryDef.text ?? (entryDef.cardType === "number" ? `A NUMBER card worth ${entryDef.value}: add it to your movement, or set a spell's duration or power.` : "")}

+

{entryDef.text ?? (entryDef.cardType === "number" ? `A NUMBER card worth ${entryDef.value}. Add it to your movement, or use it to set a spell's duration or power.` : "")}

{#if entryDef.faqRulings.length > 0}

Official FAQ

{#each entryDef.faqRulings as q, i (i)}

{q}

{/each} @@ -198,7 +198,7 @@ Wiz-War

The ledgers in detail

-
{n(stats.gamesCreated)}
games chronicled — {n(stats.gamesStarted)} begun
+
{n(stats.gamesCreated)}
games chronicled, {n(stats.gamesStarted)} of them begun
{n(stats.wizardsSeated)}
distinct wizard names have taken a seat
{n(stats.commandsPlayed)}
spells, steps, and punches recorded
{n(stats.longestGameCommands)}
actions in the longest game yet played
@@ -230,9 +230,9 @@

{#if stats.firstGameAt}The first game was dealt {new Date(String(stats.firstGameAt)).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}.{/if} - Wizards are counted by name, not by person: one player under two names is two wizards here. + Wizards are counted by name, so one player under two names counts as two wizards. Table time is estimated from the clock between moves, so a game left open overnight counts only the minutes it was actually played. - Games on one device send only their counts — names and moves stay on the device. + Games on one device send only their counts, and their names and moves stay on the device.

{:else}

Counting the ledgers…

@@ -243,15 +243,15 @@

Why it exists

In the early nineties, a group of friends played this exact edition - for countless hours — the slammed doors, the ambushes around - corners, the goat charges, the laughter. That box is long out of + for countless hours of slammed doors, ambushes around corners, + goat charges and laughter. That box is long out of print, and its cardboard is worn soft at the edges. This digital table was built from that very copy: every card was photographed and transcribed word for word, every board layout verified wall by wall, and the tokens on this screen are the actual hand-inked art from those counter sheets. It is a faithful reproduction of the game as - it was played at that table, made so the same friends — and their - friends — can keep playing it. + it was played at that table, made so the same friends, and their + friends, can keep playing it.

What this is

@@ -259,16 +259,16 @@ wizards prowl a maze, hurling fireballs, walking through walls, summoning trolls, and stealing each other's treasure. It was created by Tom Jolly in 1983 and first published under his - own Jolly Games label; the edition reproduced here is the + own Jolly Games label. The edition reproduced here is the sixth edition, published by Chessex in 1993, together - with its one expansion — monsters and magic wands included. + with its one expansion, which brings monsters and magic wands.

Whose game it is

Wiz-War is Tom Jolly's design, and the rights to it now rest with Steve Jackson Games, who publish the current edition. This project is an unofficial, non-commercial fan work, - made with love and admiration for the original — nothing here is + made with love and admiration for the original. Nothing here is endorsed by or affiliated with Tom Jolly, Chessex, or Steve Jackson Games. If this game delights you, honor it the right way: buy @@ -294,21 +294,21 @@ the maze through a wizard's own eyes

-

Watch the demo: two clockwork wizards play a stretch, then the reel replays it.

+

Watch the demo, in which two clockwork wizards play a stretch and the reel replays it.

What the table remembers

An online game is written to this table's ledger move by move, table - talk included, and kept: a finished game can be replayed from above + talk included, and kept. A finished game can be replayed from above or through a wizard's own eyes, and any turn can be shared by link. - A shared replay shows that turn's moves and the sharing wizard's view - of the board — never anyone's hand. A game passed around one device - stays on that device; only its counts reach the tally. Play under + A shared replay shows that turn's moves as an onlooker would have + seen them, and never anyone's hand. A game passed around one device + stays on that device, and only its counts reach the tally. Play under whatever name you like, and know that what you say and do at an online table is part of its record.

Send word

- Feedback, bug reports, and faint praise all welcome — ravens fly to + Feedback, bug reports and faint praise are all welcome. Ravens fly to eric@ericwagoner.com, @kestrelsnest.social on Bluesky, or @eric@toots.kestrelsnest.social on Mastodon. @@ -324,8 +324,8 @@ {/if}

- The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month; anyone who wants to chip in toward the hosting can at - ko-fi.com/kestrelsnest. Nothing is unlocked by it and nothing is withheld without it. + The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at + ko-fi.com/kestrelsnest. Every game stays whole either way.

Wiz-War © Tom Jolly. Sixth edition published by Chessex, 1993. @@ -341,10 +341,10 @@ {:else if tab === "rulings"}

- For players who know the box: how this table resolves what the - cardboard leaves to the players, by card. If the table rules + For players who know the box, here is how this table resolves, card + by card, what the cardboard leaves to the players. If the table rules against your memory of the rules, the 🐞 report button pins the - moment for review — the reply lands in your lobby. + moment for review, and the reply lands in your lobby.

@@ -367,11 +367,12 @@ {/each} {#if !rulingsSearch.trim()}

The table itself

-

Clockwork wizards — the automatons — play from the same redacted - view a human seat gets and play by the rules the engine enforces on - everyone; a tier changes what they draw and know.

+

The clockwork wizards, the automatons, play from the same redacted + view a human seat gets and by the same rules the engine enforces on + everyone. Their tier changes how many cards they draw and how many + tricks they know.

A turn need not be taken at once. Games wait on the lobby ledger - for days; the browser holds your seat; a replay of any game can be + for days while the browser holds your seat. A replay of any game can be watched from above or through a wizard's eyes and shared by link.

{/if}
@@ -399,7 +400,7 @@ {#each refBySource as group (group.source)}

{SOURCE_LABELS[group.source]}

{#if group.source === "summary"} -

Sixth edition rules, condensed from the original rulebook. © 1985 Jolly Games — this digital adaptation is a fan project.

+

Sixth edition rules, condensed from the original rulebook. © 1985 Jolly Games. This digital adaptation is a fan project.

{:else if group.source === "rulebook"}

The full text, word for word, for settling table disputes. {RULEBOOK_COPYRIGHT} Notes marked (6E: …) are this table's, on where the sixth edition differs.

{:else if group.source === "faq"} diff --git a/packages/web/src/IllusionShimmer.svelte b/packages/web/src/IllusionShimmer.svelte index 9e0d271..477671a 100644 --- a/packages/web/src/IllusionShimmer.svelte +++ b/packages/web/src/IllusionShimmer.svelte @@ -23,9 +23,9 @@ {#if onclick} { ev.stopPropagation(); onclick?.(); }} - onkeydown={() => {}}>this wall shimmers oddly — tap to test your eyes + onkeydown={() => {}}>this wall shimmers oddly, so tap to test your eyes {/if}