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}