From 9501a85be4c5d82ff9a2e8a2c030b38c657d5292 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sat, 15 Aug 2026 23:21:03 -0400 Subject: [PATCH] Design pass: the client becomes Eric's table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design system is the physical set itself, taken from the scans: card-stock cream, warm ink, and the six wizard marker colors, laid on a dark leather tabletop. Cards in your hand are now the actual cards — vertical corner rails (ATTACK / L.O.S. / ADJACENT), condensed gothic titles, brown body text on cream, number cards as big bordered numerals with per-value border styles; selecting lifts a card from the fan. The board matches the physical tiles: ink-outlined white brick walls on cracked flagstone, plank doors, eight-point home stars in each wizard's color, swirled treasure discs, warp arrows. The sidebar is paper: a turn slip, the rulebook's own hand-scrawled life-points score sheet (Caveat over ruled lines), and a typewritten chronicle that scrolls itself. The lobby is the box lid. Type: Oswald condensed display / Archivo Narrow body / Courier Prime log. Buttons are ink stamps. Board height-caps so board, actions, and the full hand share one screen; responsive stack below 900px; reduced motion respected. Verified with live screenshots at 1440x900. Co-Authored-By: Claude Fable 5 --- packages/web/index.html | 3 + packages/web/src/App.svelte | 667 +++++++++++++++++++++++----------- packages/web/src/Board.svelte | 79 ++-- packages/web/src/Card.svelte | 199 ++++++++++ 4 files changed, 705 insertions(+), 243 deletions(-) create mode 100644 packages/web/src/Card.svelte diff --git a/packages/web/index.html b/packages/web/index.html index e6823c7..0b7725a 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -4,6 +4,9 @@ Wiz-War + + +
diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index d181f35..6eb22e5 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1,6 +1,7 @@ -
-

Wiz-War 6th edition

+
+
+ Wiz-War + sixth edition + {#if net.roomId} + room {net.roomId} + {/if} + + {net.status === "connected" ? "" : "reconnecting…"} + +
{#if net.error} -
{net.error}
+ {/if} {#if !net.roomId} -
-

server: {net.status}

- -
- - or - - +
+
+
Wiz-War
+
A game of magical combat in a stone labyrinth
+ +
+ + or join one + + +
{:else if !net.started} -
-

Room {net.roomId}

-

Share the code with your opponents.

-
    - {#each net.players as p (p)} -
  • {p}{p === net.hostId ? " (host)" : ""}
  • - {/each} -
- {#if net.you === net.hostId} - - - {:else} -

Waiting for {net.hostId} to start…

- {/if} +
+
+
Room {net.roomId}
+
Share the code. Two or four wizards enter the maze.
+
    + {#each net.players as p (p)} +
  • {p}{p === net.hostId ? " — host" : ""}
  • + {/each} +
+ {#if net.you === net.hostId} + + + {:else} +

Waiting for {net.hostId} to flip the boards…

+ {/if} +
{:else if view}
-
+
-
+
-
+ +
+ +
+ {#if selectedDef || youMustDiscard || discardSelection.size > 0} +
+ {#if youMustDiscard} + Hand over the limit — mark cards and discard. + {/if} + {#if discardSelection.size > 0} + + {/if} + {#if selectedDef} + {selectedDef.name} + {#if edgeSelectMode}— click a wall line{/if} + {#if selectedDef.cardType === "attack" && !edgeSelectMode && !cellSelectMode} + — click a target{attachedNumber ? ` (powered by a ${numberTotal})` : " (tap a number card to power it)"} {/if} - {#if selectedCard?.cardId === "waterbolt"} - - (knockback {numberTotal - wbDamage}) + {#if attachedMods.length > 0} + [+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}] + {/if} + {#if selectedCard?.cardId === "boobytrap"} + — place 4 tokens ({trapCells.length}/4; the first is real) + {/if} + {#if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)} + {tradeFrom ? "— now the second square" : "— click the first square"} + {/if} + {#if selectedCard?.cardId === "rotate-sector"} + + — click the sector + {/if} + {#if selectedCard?.cardId === "relocate-sector"} + {pendingSectorFrom ? "— now the destination area" : "— click the sector to move"} {/if} {#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)} - + {#if selectedCard.cardId === "deja-vu"} - {:else} - — then click the target wizard + — then click the target {/if} {/if} - {#if attachedMods.length > 0} - [+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}] - {/if} - {#if selectedCard?.cardId === "rotate-sector"} - - — click a square in the sector to rotate - {/if} - {#if selectedCard?.cardId === "boobytrap"} - — place 4 tokens ({trapCells.length}/4; the FIRST is the real trap) - {/if} - {#if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)} - {tradeFrom ? "— now the second square" : "— click the first square"} - {/if} - {#if selectedCard?.cardId === "power-attack"} - (select an attack first, then attach Power Attack via number input) - {/if} - {#if selectedCard?.cardId === "relocate-sector"} - {#if pendingSectorFrom} - — now click the destination area - {:else} - — click a square in the sector to move - {/if} + {#if selectedCard?.cardId === "waterbolt"} + + (knockback {numberTotal - wbDamage}) {/if} {#if selectedCard?.cardId === "power-run"} - - + + {/if} {#if selectedCard && SELF_CARDS.has(selectedCard.cardId)} - {/if} - {#if pendingCellFor} - — now click the destination square for {pendingCellFor} - {/if} - {#if selectedCard && CELL_CARDS.has(selectedCard.cardId)} - — click a square on the board - {/if} - -
+ + {/if} +
+ {/if} + +
+ {#if isYourTurn && view.dimWarps.some((w) => { + const me = view.players.find((p) => p.id === view.you)!; + return (w.a.x === me.position.x && w.a.y === me.position.y) || + (w.b.x === me.position.x && w.b.y === me.position.y); + })} + {/if} + {#if isYourTurn} + + + + + {/if} +
-
- {#each view.yourHand as card (card.instanceId)} - {@const def = cardDef(card.cardId)} - - {/each} -
- -
- {#if youMustDiscard} - - {/if} - {#if discardSelection.size > 0} - - {/if} - {#if isYourTurn && view.dimWarps.some((w) => { - const me = view.players.find((p) => p.id === view.you)!; - return (w.a.x === me.position.x && w.a.y === me.position.y) || - (w.b.x === me.position.x && w.b.y === me.position.y); - })} - - {/if} - {#if view.outOfTurnWindow?.playerId === view.you} - - - {/if} - {#if isYourTurn} - - - - - {/if} -
- -
- {#each net.log.slice(-40) as line, i (i)} -
{line}
- {/each} -
+
+ {#each view.yourHand as card (card.instanceId)} + m.instanceId === card.instanceId)} + marked={discardSelection.has(card.instanceId)} + displayed={view.players.find((p) => p.id === view.you)?.displayed.some((c) => c.instanceId === card.instanceId) ?? false} + charges={view.wandCharges[card.instanceId] ?? null} + onclick={() => selectCard(card)} + /> + {/each}
{/if} -
+ diff --git a/packages/web/src/Board.svelte b/packages/web/src/Board.svelte index 31433d7..a4f0c2c 100644 --- a/packages/web/src/Board.svelte +++ b/packages/web/src/Board.svelte @@ -91,17 +91,28 @@ {#each view.players as p (p.id)} - + {@const hx = p.home.x * CELL + CELL / 2} + {@const hy = p.home.y * CELL + CELL / 2} + {@const R = CELL * 0.36} + {@const r = CELL * 0.14} + { + const rad = (Math.PI / 8) * i - Math.PI / 2; + const len = i % 2 === 0 ? R : r; + return `${hx + Math.cos(rad) * len},${hy + Math.sin(rad) * len}`; + }).join(" ")} + class="home-star" fill={playerColor(p.id)} + /> {/each} {#each view.treasures as t (t.id)} {#if t.position} - + {@const tx = t.position.x * CELL + CELL / 2} + {@const ty = t.position.y * CELL + CELL * 0.72} + + + + + {/if} {/each} @@ -210,7 +221,7 @@ onkeydown={() => {}} class="wizard" > - + {p.id[0]?.toUpperCase()} {#if p.carriedTreasureId} @@ -256,21 +267,38 @@