Design pass: the client becomes Eric's table
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c9524a37db
commit
9501a85be4
@@ -4,6 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Wiz-War</title>
|
<title>Wiz-War</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&family=Archivo+Narrow:wght@400;600&family=Courier+Prime&family=Caveat:wght@500;700&display=swap" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
+414
-185
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { net } from "./net.svelte";
|
import { net } from "./net.svelte";
|
||||||
import Board from "./Board.svelte";
|
import Board from "./Board.svelte";
|
||||||
|
import Card from "./Card.svelte";
|
||||||
import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey } from "@wizwar/engine";
|
import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey } from "@wizwar/engine";
|
||||||
import type { CardInstance, Side } from "@wizwar/engine";
|
import type { CardInstance, Side } from "@wizwar/engine";
|
||||||
|
|
||||||
@@ -350,6 +351,12 @@
|
|||||||
function drop() { net.command({ type: "dropTreasure" }); }
|
function drop() { net.command({ type: "dropTreasure" }); }
|
||||||
function pass() { net.command({ type: "pass" }); }
|
function pass() { net.command({ type: "pass" }); }
|
||||||
|
|
||||||
|
let chronicleEl = $state<HTMLElement | null>(null);
|
||||||
|
$effect(() => {
|
||||||
|
void net.log.length;
|
||||||
|
if (chronicleEl) chronicleEl.scrollTop = chronicleEl.scrollHeight;
|
||||||
|
});
|
||||||
|
|
||||||
const PLAYER_COLORS = ["#1a9c46", "#d3352b", "#c9308f", "#3a3ac0", "#2ab0c9", "#c9a72a"];
|
const PLAYER_COLORS = ["#1a9c46", "#d3352b", "#c9308f", "#3a3ac0", "#2ab0c9", "#c9a72a"];
|
||||||
function playerColor(id: string): string {
|
function playerColor(id: string): string {
|
||||||
const idx = view?.players.findIndex((p) => p.id === id) ?? 0;
|
const idx = view?.players.findIndex((p) => p.id === id) ?? 0;
|
||||||
@@ -357,50 +364,73 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<div class="table">
|
||||||
<h1>Wiz-War <span class="subtitle">6th edition</span></h1>
|
<header class="masthead">
|
||||||
|
<span class="mast-title">Wiz-War</span>
|
||||||
|
<span class="mast-sub">sixth edition</span>
|
||||||
|
{#if net.roomId}
|
||||||
|
<span class="mast-room">room <b>{net.roomId}</b></span>
|
||||||
|
{/if}
|
||||||
|
<span class="mast-status" class:offline={net.status !== "connected"}>
|
||||||
|
{net.status === "connected" ? "" : "reconnecting…"}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
{#if net.error}
|
{#if net.error}
|
||||||
<div class="toast">{net.error}</div>
|
<div class="toast" role="alert">{net.error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !net.roomId}
|
{#if !net.roomId}
|
||||||
<section class="lobby">
|
<section class="boxlid">
|
||||||
<p class="status">server: {net.status}</p>
|
<div class="boxlid-inner">
|
||||||
<input placeholder="your wizard's name" bind:value={name} maxlength="20" />
|
<div class="boxlid-title">Wiz-War</div>
|
||||||
<div class="lobby-actions">
|
<div class="boxlid-tag">A game of magical combat in a stone labyrinth</div>
|
||||||
<button disabled={!name.trim()} onclick={() => net.create(name)}>Create game</button>
|
<label class="field">
|
||||||
<span>or</span>
|
<span>Your wizard's name</span>
|
||||||
<input placeholder="room code" bind:value={joinCode} maxlength="4" class="code" />
|
<input bind:value={name} maxlength="20" placeholder="e.g. Mordecai" />
|
||||||
<button disabled={!name.trim() || !joinCode.trim()} onclick={() => net.join(joinCode, name)}>
|
</label>
|
||||||
|
<div class="boxlid-actions">
|
||||||
|
<button class="stamp" disabled={!name.trim()} onclick={() => net.create(name)}>
|
||||||
|
Create a game
|
||||||
|
</button>
|
||||||
|
<span class="or">or join one</span>
|
||||||
|
<input class="code" bind:value={joinCode} maxlength="4" placeholder="CODE" />
|
||||||
|
<button class="stamp" disabled={!name.trim() || !joinCode.trim()} onclick={() => net.join(joinCode, name)}>
|
||||||
Join
|
Join
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{:else if !net.started}
|
{:else if !net.started}
|
||||||
<section class="lobby">
|
<section class="boxlid">
|
||||||
<h2>Room <code>{net.roomId}</code></h2>
|
<div class="boxlid-inner">
|
||||||
<p>Share the code with your opponents.</p>
|
<div class="boxlid-title small">Room {net.roomId}</div>
|
||||||
<ul>
|
<div class="boxlid-tag">Share the code. Two or four wizards enter the maze.</div>
|
||||||
|
<ul class="roster">
|
||||||
{#each net.players as p (p)}
|
{#each net.players as p (p)}
|
||||||
<li>{p}{p === net.hostId ? " (host)" : ""}</li>
|
<li><span class="dot" style:background={playerColor(p)}></span>{p}{p === net.hostId ? " — host" : ""}</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{#if net.you === net.hostId}
|
{#if net.you === net.hostId}
|
||||||
<label><input type="checkbox" bind:checked={withExpansion} /> include Expansion Set #1 (monsters & wands)</label>
|
<label class="check">
|
||||||
|
<input type="checkbox" bind:checked={withExpansion} />
|
||||||
|
Include Expansion Set #1 — monsters & wands
|
||||||
|
</label>
|
||||||
<button
|
<button
|
||||||
|
class="stamp big"
|
||||||
disabled={net.players.length !== 2 && net.players.length !== 4}
|
disabled={net.players.length !== 2 && net.players.length !== 4}
|
||||||
onclick={() => net.start(withExpansion)}
|
onclick={() => net.start(withExpansion)}
|
||||||
>
|
>
|
||||||
Start game ({net.players.length} wizards — need 2 or 4)
|
Flip the boards ({net.players.length} of 2 or 4)
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Waiting for {net.hostId} to start…</p>
|
<p class="waiting">Waiting for {net.hostId} to flip the boards…</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{:else if view}
|
{:else if view}
|
||||||
<div class="game">
|
<div class="game">
|
||||||
<div class="board-pane">
|
<section class="board-zone">
|
||||||
<Board
|
<Board
|
||||||
{view}
|
{view}
|
||||||
edgeSelectMode={edgeSelectMode && isYourTurn}
|
edgeSelectMode={edgeSelectMode && isYourTurn}
|
||||||
@@ -410,64 +440,93 @@
|
|||||||
onPlayerClick={clickPlayer}
|
onPlayerClick={clickPlayer}
|
||||||
onCreatureClick={clickCreature}
|
onCreatureClick={clickCreature}
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div class="side-pane">
|
<aside class="paper-rail">
|
||||||
{#if view.phase === "finished"}
|
{#if view.phase === "finished"}
|
||||||
<div class="banner winner">🏆 {view.winner} wins!</div>
|
<div class="slip winner">🏆 {view.winner} wins!</div>
|
||||||
{:else if youMustRespond}
|
{:else if youMustRespond}
|
||||||
<div class="banner respond">
|
<div class="slip urgent">
|
||||||
{#if view.stack?.defenderId === view.you}
|
{#if view.stack?.defenderId === view.you}
|
||||||
<strong>{view.stack.attackerId}</strong> attacks you
|
<strong>{view.stack.attackerId}</strong> attacks you
|
||||||
{#if view.stack.attackCard}with <strong>{cardDef(view.stack.attackCard.cardId).name}</strong>{:else}with a punch{/if}!
|
{#if view.stack.attackCard}with <strong>{cardDef(view.stack.attackCard.cardId).name}</strong>{:else}with a punch{/if}!
|
||||||
Click a counteraction card, or
|
Play a counteraction, or
|
||||||
{:else}
|
{:else}
|
||||||
Respond to the counteraction, or
|
Respond to the counteraction, or
|
||||||
{/if}
|
{/if}
|
||||||
<button onclick={pass}>let it resolve</button>
|
<button class="stamp tiny" onclick={pass}>let it resolve</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if view.stack}
|
{:else if view.stack}
|
||||||
<div class="banner">Waiting for {view.stack.waitingOn}…</div>
|
<div class="slip">Waiting on {view.stack.waitingOn}…</div>
|
||||||
|
{:else if view.outOfTurnWindow?.playerId === view.you}
|
||||||
|
<div class="slip urgent">Your interruption — cast now, or <button class="stamp tiny" onclick={pass}>waste it</button></div>
|
||||||
{:else if isYourTurn}
|
{:else if isYourTurn}
|
||||||
<div class="banner your-turn">
|
<div class="slip yours">
|
||||||
Your turn — round {view.turn.round}.
|
Your turn — round {view.turn.round}.
|
||||||
Moves: {view.turn.movementAllowance - view.turn.movementUsed}
|
{view.turn.movementAllowance - view.turn.movementUsed} moves left{view.turn.attackUsed ? " · attack spent" : ""}
|
||||||
{view.turn.attackUsed ? "· attack used" : ""}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="banner">{view.activePlayerId} is taking their turn…</div>
|
<div class="slip">{view.activePlayerId} is taking their turn…</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="players">
|
<div class="scoresheet">
|
||||||
|
<div class="scoresheet-head">life-points</div>
|
||||||
{#each view.players as p (p.id)}
|
{#each view.players as p (p.id)}
|
||||||
<div class="player" class:dead={!p.alive} class:active={p.id === view.activePlayerId}>
|
<div class="score-row" class:dead={!p.alive} class:active={p.id === view.activePlayerId}>
|
||||||
<span class="dot" style:background={playerColor(p.id)}></span>
|
<span class="dot" style:background={playerColor(p.id)}></span>
|
||||||
<span class="pname">{p.id}{p.id === view.you ? " (you)" : ""}</span>
|
<span class="score-name">{p.id}{p.id === view.you ? " (you)" : ""}</span>
|
||||||
<span class="life">♥ {p.life}</span>
|
<span class="score-life">{p.life}</span>
|
||||||
<span class="cards">🂠 {p.handCount}</span>
|
<span class="score-marks">
|
||||||
{#if p.lostTurns > 0}<span title="lost turns">💫{p.lostTurns}</span>{/if}
|
{p.handCount} cards{#if p.lostTurns > 0} · dazed {p.lostTurns}{/if}{#if p.carriedTreasureId} · 💰{/if}
|
||||||
{#if p.carriedTreasureId}<span title="carrying treasure">💰</span>{/if}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="deck-info">deck {view.deckCount} · discard {view.discardCount}</div>
|
<div class="deck-line">draw pile {view.deckCount} · discards {view.discardCount}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectedDef}
|
<div class="chronicle" aria-label="game log" bind:this={chronicleEl}>
|
||||||
<div class="cast-hint">
|
{#each net.log.slice(-60) as line, i (i)}
|
||||||
<strong>{selectedDef.name}</strong>
|
<div>{line}</div>
|
||||||
{#if edgeSelectMode}
|
{/each}
|
||||||
— click a wall line on the board
|
</div>
|
||||||
{:else if selectedDef.cardType === "attack"}
|
</aside>
|
||||||
— click a target wizard{attachedNumber ? ` (powered by a ${numberTotal})` : " (click a number card to power it)"}
|
</div>
|
||||||
|
|
||||||
|
<div class="table-edge">
|
||||||
|
{#if selectedDef || youMustDiscard || discardSelection.size > 0}
|
||||||
|
<div class="hint-strip">
|
||||||
|
{#if youMustDiscard}
|
||||||
|
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedCard?.cardId === "waterbolt"}
|
{#if discardSelection.size > 0}
|
||||||
<label>damage <input type="number" min="0" max={numberTotal} bind:value={wbDamage} /></label>
|
<button class="stamp tiny" onclick={doDiscard}>Discard {discardSelection.size} marked</button>
|
||||||
(knockback {numberTotal - wbDamage})
|
{/if}
|
||||||
|
{#if selectedDef}
|
||||||
|
<strong class="hint-name">{selectedDef.name}</strong>
|
||||||
|
{#if edgeSelectMode}<span>— click a wall line</span>{/if}
|
||||||
|
{#if selectedDef.cardType === "attack" && !edgeSelectMode && !cellSelectMode}
|
||||||
|
<span>— click a target{attachedNumber ? ` (powered by a ${numberTotal})` : " (tap a number card to power it)"}</span>
|
||||||
|
{/if}
|
||||||
|
{#if attachedMods.length > 0}
|
||||||
|
<span class="hint-mods">[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]</span>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCard?.cardId === "boobytrap"}
|
||||||
|
<span>— place 4 tokens ({trapCells.length}/4; the first is real)</span>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)}
|
||||||
|
<span>{tradeFrom ? "— now the second square" : "— click the first square"}</span>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCard?.cardId === "rotate-sector"}
|
||||||
|
<label class="inline"><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
|
||||||
|
<span>— click the sector</span>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCard?.cardId === "relocate-sector"}
|
||||||
|
<span>{pendingSectorFrom ? "— now the destination area" : "— click the sector to move"}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)}
|
{#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)}
|
||||||
<label>card name <input bind:value={nameInput} placeholder="e.g. Fireball or treasure" /></label>
|
<label class="inline">name <input class="text-input" bind:value={nameInput} placeholder="e.g. Fireball" /></label>
|
||||||
{#if selectedCard.cardId === "deja-vu"}
|
{#if selectedCard.cardId === "deja-vu"}
|
||||||
<button onclick={() => {
|
<button class="stamp tiny" onclick={() => {
|
||||||
const id = nameToCardId(nameInput);
|
const id = nameToCardId(nameInput);
|
||||||
if (id && selectedCard) {
|
if (id && selectedCard) {
|
||||||
net.command({ type: "cast", instanceId: selectedCard.instanceId, params: { cardId: id } });
|
net.command({ type: "cast", instanceId: selectedCard.instanceId, params: { cardId: id } });
|
||||||
@@ -475,95 +534,39 @@
|
|||||||
}
|
}
|
||||||
}}>Retrieve</button>
|
}}>Retrieve</button>
|
||||||
{:else}
|
{:else}
|
||||||
— then click the target wizard
|
<span>— then click the target</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if attachedMods.length > 0}
|
{#if selectedCard?.cardId === "waterbolt"}
|
||||||
[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]
|
<label class="inline">damage <input class="num-input" type="number" min="0" max={numberTotal} bind:value={wbDamage} /></label>
|
||||||
{/if}
|
<span>(knockback {numberTotal - wbDamage})</span>
|
||||||
{#if selectedCard?.cardId === "rotate-sector"}
|
|
||||||
<label><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
|
|
||||||
— 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}
|
{/if}
|
||||||
{#if selectedCard?.cardId === "power-run"}
|
{#if selectedCard?.cardId === "power-run"}
|
||||||
<label>life to trade <input type="number" min="1" max="10" bind:value={runPoints} /></label>
|
<label class="inline">life to trade <input class="num-input" type="number" min="1" max="10" bind:value={runPoints} /></label>
|
||||||
<button onclick={castPowerRun}>Run!</button>
|
<button class="stamp tiny" onclick={castPowerRun}>Run!</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedCard && SELF_CARDS.has(selectedCard.cardId)}
|
{#if selectedCard && SELF_CARDS.has(selectedCard.cardId)}
|
||||||
<button onclick={castSelfWithNumber}>
|
<button class="stamp tiny" onclick={castSelfWithNumber}>
|
||||||
Cast{attachedNumber ? ` with the ${numberTotal}` : " (duration 1)"}
|
Cast{attachedNumber ? ` with the ${numberTotal}` : " (duration 1)"}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if pendingCellFor}
|
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||||
— now click the destination square for {pendingCellFor}
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedCard && CELL_CARDS.has(selectedCard.cardId)}
|
|
||||||
— click a square on the board
|
|
||||||
{/if}
|
|
||||||
<button class="link" onclick={clearSelection}>cancel</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="hand">
|
|
||||||
{#each view.yourHand as card (card.instanceId)}
|
|
||||||
{@const def = cardDef(card.cardId)}
|
|
||||||
<button
|
|
||||||
class="card"
|
|
||||||
class:selected={selectedCard?.instanceId === card.instanceId}
|
|
||||||
class:attached={attachedNumber?.instanceId === card.instanceId}
|
|
||||||
class:marked={discardSelection.has(card.instanceId)}
|
|
||||||
title={def.text ?? ""}
|
|
||||||
onclick={() => selectCard(card)}
|
|
||||||
>
|
|
||||||
<span class="card-type">{def.cardType}</span>
|
|
||||||
<span class="card-name">{def.name}</span>
|
|
||||||
{#if view.players.find((p) => p.id === view.you)?.displayed.some((c) => c.instanceId === card.instanceId)}
|
|
||||||
<span class="displayed-badge">
|
|
||||||
displayed{view.wandCharges[card.instanceId] != null ? ` · ${view.wandCharges[card.instanceId]} ⚡` : ""}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if youMustDiscard}
|
|
||||||
<div class="banner respond">Hand over 7 — select cards and discard.</div>
|
|
||||||
{/if}
|
|
||||||
{#if discardSelection.size > 0}
|
|
||||||
<button onclick={doDiscard}>Discard {discardSelection.size} selected</button>
|
|
||||||
{/if}
|
|
||||||
{#if isYourTurn && view.dimWarps.some((w) => {
|
{#if isYourTurn && view.dimWarps.some((w) => {
|
||||||
const me = view.players.find((p) => p.id === view.you)!;
|
const me = view.players.find((p) => p.id === view.you)!;
|
||||||
return (w.a.x === me.position.x && w.a.y === me.position.y) ||
|
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);
|
(w.b.x === me.position.x && w.b.y === me.position.y);
|
||||||
})}
|
})}
|
||||||
<button onclick={() => net.command({ type: "warpStep" })}>Step through the warp</button>
|
<button class="stamp" onclick={() => net.command({ type: "warpStep" })}>Step through the warp</button>
|
||||||
{/if}
|
|
||||||
{#if view.outOfTurnWindow?.playerId === view.you}
|
|
||||||
<div class="banner respond">Your interruption! Cast your spell (or pass).</div>
|
|
||||||
<button onclick={pass}>Pass (waste it)</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if isYourTurn}
|
{#if isYourTurn}
|
||||||
<button onclick={pickUp}>Pick up treasure</button>
|
<button class="stamp" onclick={pickUp}>Pick up treasure</button>
|
||||||
<button onclick={drop}>Drop treasure</button>
|
<button class="stamp" onclick={drop}>Drop treasure</button>
|
||||||
<label>
|
<label class="inline draw-pick">
|
||||||
draw
|
draw
|
||||||
<select bind:value={drawCount}>
|
<select bind:value={drawCount}>
|
||||||
<option value={0}>0</option>
|
<option value={0}>0</option>
|
||||||
@@ -571,85 +574,311 @@
|
|||||||
<option value={2}>2</option>
|
<option value={2}>2</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button class="primary" onclick={endTurn}>End turn</button>
|
<button class="stamp primary" onclick={endTurn}>End turn</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="log">
|
<div class="hand" aria-label="your hand">
|
||||||
{#each net.log.slice(-40) as line, i (i)}
|
{#each view.yourHand as card (card.instanceId)}
|
||||||
<div>{line}</div>
|
<Card
|
||||||
|
{card}
|
||||||
|
selected={selectedCard?.instanceId === card.instanceId}
|
||||||
|
attached={attachedNumber?.instanceId === card.instanceId ||
|
||||||
|
attachedMods.some((m) => 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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
:global(html, body) {
|
||||||
font-family: system-ui, sans-serif;
|
margin: 0;
|
||||||
padding: 1rem 1.5rem;
|
background: #171a20;
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
h1 { margin: 0 0 0.75rem; }
|
.table {
|
||||||
.subtitle { font-size: 0.55em; color: #887; font-weight: normal; }
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at 50% 0%, rgba(64, 74, 92, 0.25), transparent 55%),
|
||||||
|
repeating-linear-gradient(115deg, rgba(255,255,255,0.012) 0 3px, transparent 3px 7px),
|
||||||
|
#171a20;
|
||||||
|
color: #d8d2c0;
|
||||||
|
font-family: "Archivo Narrow", system-ui, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 1.1rem 1.1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.masthead {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.7rem;
|
||||||
|
padding: 0.7rem 0.2rem 0.6rem;
|
||||||
|
border-bottom: 1px solid rgba(216, 210, 192, 0.15);
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
.mast-title {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #e9e1cb;
|
||||||
|
}
|
||||||
|
.mast-sub {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8d8672;
|
||||||
|
}
|
||||||
|
.mast-room { margin-left: auto; font-size: 0.85rem; color: #a49c86; }
|
||||||
|
.mast-room b { color: #e9e1cb; letter-spacing: 0.12em; }
|
||||||
|
.mast-status { font-size: 0.8rem; color: #c98a2a; }
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
background: #b33; color: white; padding: 0.5rem 0.75rem;
|
background: #6d2119;
|
||||||
border-radius: 6px; margin-bottom: 0.5rem;
|
color: #f2e6d8;
|
||||||
}
|
border: 1px solid #96382c;
|
||||||
.lobby { display: flex; flex-direction: column; gap: 0.75rem; max-width: 420px; }
|
padding: 0.5rem 0.85rem;
|
||||||
.lobby input { padding: 0.5rem; font-size: 1rem; }
|
border-radius: 4px;
|
||||||
.lobby-actions { display: flex; gap: 0.5rem; align-items: center; }
|
margin-bottom: 0.7rem;
|
||||||
.code { width: 6ch; text-transform: uppercase; }
|
font-size: 0.95rem;
|
||||||
.status { color: #887; margin: 0; }
|
|
||||||
|
|
||||||
.game { display: grid; grid-template-columns: minmax(320px, 640px) minmax(280px, 1fr); gap: 1rem; }
|
|
||||||
@media (max-width: 800px) { .game { grid-template-columns: 1fr; } }
|
|
||||||
.side-pane { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0; }
|
|
||||||
|
|
||||||
.banner { padding: 0.5rem 0.75rem; border-radius: 6px; background: #eee; }
|
|
||||||
.banner.your-turn { background: #d8efd8; }
|
|
||||||
.banner.respond { background: #f6dcb5; }
|
|
||||||
.banner.winner { background: gold; font-size: 1.2em; }
|
|
||||||
|
|
||||||
.players { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
||||||
.player { display: flex; gap: 0.5rem; align-items: center; padding: 0.2rem 0.4rem; border-radius: 4px; }
|
|
||||||
.player.active { background: #eef4ff; }
|
|
||||||
.player.dead { opacity: 0.45; text-decoration: line-through; }
|
|
||||||
.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
|
|
||||||
.pname { font-weight: 600; }
|
|
||||||
.deck-info { color: #887; font-size: 0.85em; }
|
|
||||||
|
|
||||||
.cast-hint { background: #eef; padding: 0.4rem 0.6rem; border-radius: 6px; }
|
|
||||||
.cast-hint input { width: 4ch; }
|
|
||||||
.link { background: none; border: none; color: #36c; cursor: pointer; text-decoration: underline; }
|
|
||||||
|
|
||||||
.hand { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
|
||||||
.card {
|
|
||||||
display: flex; flex-direction: column; align-items: flex-start;
|
|
||||||
border: 1.5px solid #998; border-radius: 6px; background: #f6f2e6;
|
|
||||||
padding: 0.35rem 0.5rem; cursor: pointer; min-width: 7.5rem; text-align: left;
|
|
||||||
}
|
|
||||||
.card:hover { border-color: #333; }
|
|
||||||
.card.selected { border-color: #26c; background: #e6ecfc; }
|
|
||||||
.card.attached { border-color: #2a2; background: #e2f4e2; }
|
|
||||||
.card.marked { border-color: #c33; background: #fce6e6; }
|
|
||||||
.card-type { font-size: 0.65em; text-transform: uppercase; color: #776; }
|
|
||||||
.card-name { font-weight: 600; font-size: 0.9em; }
|
|
||||||
.displayed-badge {
|
|
||||||
font-size: 0.6em; text-transform: uppercase; color: #fff;
|
|
||||||
background: #7a5f2a; border-radius: 3px; padding: 0 4px; margin-top: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
.boxlid {
|
||||||
button { padding: 0.4rem 0.7rem; border-radius: 6px; border: 1px solid #998; background: #fff; cursor: pointer; }
|
flex: 1;
|
||||||
button.primary { background: #26c; color: white; border-color: #26c; }
|
display: grid;
|
||||||
button:disabled { opacity: 0.5; cursor: default; }
|
place-items: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
.boxlid-inner {
|
||||||
|
background: #e9e1cb;
|
||||||
|
color: #43331f;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #b3a687;
|
||||||
|
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55);
|
||||||
|
padding: 2.2rem 2.6rem 2rem;
|
||||||
|
max-width: 26rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.boxlid-title {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.boxlid-title.small { font-size: 1.7rem; }
|
||||||
|
.boxlid-tag {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #6b5a41;
|
||||||
|
margin: 0.5rem 0 1.4rem;
|
||||||
|
}
|
||||||
|
.field { display: block; text-align: left; margin-bottom: 1rem; }
|
||||||
|
.field span {
|
||||||
|
display: block;
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #6b5a41;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
.field input, .code {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f4eede;
|
||||||
|
border: 1px solid #b3a687;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #43331f;
|
||||||
|
}
|
||||||
|
.boxlid-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; justify-content: center; }
|
||||||
|
.or { font-size: 0.85rem; color: #6b5a41; }
|
||||||
|
.code { width: 7.5ch; text-transform: uppercase; text-align: center; letter-spacing: 0.15em; }
|
||||||
|
.roster { list-style: none; padding: 0; margin: 0 0 1rem; text-align: left; }
|
||||||
|
.roster li { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0; font-size: 1.05rem; }
|
||||||
|
.check { display: flex; gap: 0.5rem; align-items: center; justify-content: center; font-size: 0.92rem; margin-bottom: 1rem; }
|
||||||
|
.waiting { color: #6b5a41; font-style: italic; }
|
||||||
|
|
||||||
.log {
|
.stamp {
|
||||||
background: #1e1c18; color: #cfc9ba; font-size: 0.8em;
|
font-family: "Oswald", sans-serif;
|
||||||
border-radius: 6px; padding: 0.5rem 0.7rem; max-height: 240px;
|
font-weight: 500;
|
||||||
overflow-y: auto; font-family: ui-monospace, monospace;
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: #e9e1cb;
|
||||||
|
color: #43331f;
|
||||||
|
border: 1.5px solid #43331f;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.42rem 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.stamp:hover:not(:disabled) { background: #f4eede; }
|
||||||
|
.stamp:active:not(:disabled) { transform: translate(1px, 1px); box-shadow: 1px 1px 0 rgba(0,0,0,0.35); }
|
||||||
|
.stamp:disabled { opacity: 0.45; cursor: default; }
|
||||||
|
.stamp.primary { background: #43331f; color: #e9e1cb; }
|
||||||
|
.stamp.primary:hover:not(:disabled) { background: #5a4429; }
|
||||||
|
.stamp.big { font-size: 0.95rem; padding: 0.6rem 1.3rem; }
|
||||||
|
.stamp.tiny { font-size: 0.62rem; padding: 0.22rem 0.5rem; box-shadow: 1px 1px 0 rgba(0,0,0,0.3); }
|
||||||
|
.stamp:focus-visible { outline: 2px solid #d8b23a; outline-offset: 2px; }
|
||||||
|
|
||||||
|
.game {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(340px, 1fr) minmax(250px, 330px);
|
||||||
|
gap: 1.1rem;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.board-zone { display: flex; align-items: flex-start; justify-content: center; min-width: 0; }
|
||||||
|
.board-zone :global(svg.board) {
|
||||||
|
max-height: calc(100vh - 21.5rem);
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.paper-rail { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0; }
|
||||||
|
|
||||||
|
.slip {
|
||||||
|
background: #e9e1cb;
|
||||||
|
color: #43331f;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.55rem 0.8rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
|
||||||
|
transform: rotate(-0.4deg);
|
||||||
|
}
|
||||||
|
.slip.yours { border-left: 4px solid #2e7d32; }
|
||||||
|
.slip.urgent { border-left: 4px solid #b3372b; transform: rotate(0.4deg); }
|
||||||
|
.slip.winner {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
border-left: 4px solid #c9a72a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoresheet {
|
||||||
|
background: #f2ecdb;
|
||||||
|
color: #43331f;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0.6rem 0.8rem 0.5rem;
|
||||||
|
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.45);
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
transparent 0 1.55rem,
|
||||||
|
rgba(95, 74, 51, 0.18) 1.55rem calc(1.55rem + 1px)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.scoresheet-head {
|
||||||
|
font-family: "Caveat", cursive;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #6b5a41;
|
||||||
|
border-bottom: 1.5px solid #6b5a41;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.score-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
height: 1.55rem;
|
||||||
|
}
|
||||||
|
.score-row.active .score-name { font-weight: 700; }
|
||||||
|
.score-row.dead { opacity: 0.5; text-decoration: line-through; }
|
||||||
|
.score-name { font-family: "Caveat", cursive; font-size: 1.25rem; line-height: 1; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.score-life { font-family: "Caveat", cursive; font-size: 1.4rem; font-weight: 700; min-width: 1.6rem; text-align: right; }
|
||||||
|
.score-marks { font-size: 0.75rem; color: #6b5a41; }
|
||||||
|
.deck-line {
|
||||||
|
font-family: "Courier Prime", monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #8a7a5e;
|
||||||
|
padding-top: 0.3rem;
|
||||||
|
}
|
||||||
|
.dot { width: 11px; height: 11px; border-radius: 50%; flex: 0 0 auto; border: 1px solid rgba(0,0,0,0.4); }
|
||||||
|
|
||||||
|
.chronicle {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 8rem;
|
||||||
|
max-height: 40vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #efe8d4;
|
||||||
|
color: #3a2f1f;
|
||||||
|
font-family: "Courier Prime", monospace;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
.chronicle div + div { margin-top: 0.05rem; }
|
||||||
|
|
||||||
|
.table-edge {
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.hint-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background: rgba(233, 225, 203, 0.12);
|
||||||
|
border: 1px dashed rgba(233, 225, 203, 0.35);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.hint-name { font-family: "Oswald", sans-serif; text-transform: uppercase; letter-spacing: 0.06em; color: #e9e1cb; }
|
||||||
|
.hint-mods { color: #9fca6a; }
|
||||||
|
.hint-alert { color: #e8a03c; }
|
||||||
|
.hint-cancel {
|
||||||
|
margin-left: auto;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #a49c86;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.inline { display: inline-flex; align-items: center; gap: 0.35rem; }
|
||||||
|
.num-input { width: 3.2rem; }
|
||||||
|
.text-input { width: 9rem; }
|
||||||
|
.num-input, .text-input, .draw-pick select {
|
||||||
|
background: #e9e1cb;
|
||||||
|
border: 1px solid #43331f;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.2rem 0.35rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #43331f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; min-height: 2rem; }
|
||||||
|
.draw-pick { color: #a49c86; font-size: 0.9rem; }
|
||||||
|
|
||||||
|
.hand {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0.9rem 0.3rem 0.35rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.game { grid-template-columns: 1fr; }
|
||||||
|
.paper-rail { order: 2; }
|
||||||
|
.chronicle { max-height: 22vh; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -91,17 +91,28 @@
|
|||||||
|
|
||||||
<!-- homes & treasure spaces -->
|
<!-- homes & treasure spaces -->
|
||||||
{#each view.players as p (p.id)}
|
{#each view.players as p (p.id)}
|
||||||
<text
|
{@const hx = p.home.x * CELL + CELL / 2}
|
||||||
x={p.home.x * CELL + CELL / 2} y={p.home.y * CELL + CELL / 2 + 2}
|
{@const hy = p.home.y * CELL + CELL / 2}
|
||||||
class="home" fill={playerColor(p.id)}
|
{@const R = CELL * 0.36}
|
||||||
>✦</text>
|
{@const r = CELL * 0.14}
|
||||||
|
<polygon
|
||||||
|
points={Array.from({ length: 16 }, (_, i) => {
|
||||||
|
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}
|
||||||
{#each view.treasures as t (t.id)}
|
{#each view.treasures as t (t.id)}
|
||||||
{#if t.position}
|
{#if t.position}
|
||||||
<circle
|
{@const tx = t.position.x * CELL + CELL / 2}
|
||||||
cx={t.position.x * CELL + CELL / 2} cy={t.position.y * CELL + CELL * 0.72}
|
{@const ty = t.position.y * CELL + CELL * 0.72}
|
||||||
r={7} class="treasure" fill={playerColor(t.owner)}
|
<g class="treasure-g">
|
||||||
/>
|
<circle cx={tx} cy={ty} r={8} class="treasure" fill={playerColor(t.owner)} />
|
||||||
|
<circle cx={tx} cy={ty} r={4.5} class="treasure-swirl" />
|
||||||
|
<circle cx={tx} cy={ty} r={1.6} class="treasure-core" />
|
||||||
|
</g>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -210,7 +221,7 @@
|
|||||||
onkeydown={() => {}}
|
onkeydown={() => {}}
|
||||||
class="wizard"
|
class="wizard"
|
||||||
>
|
>
|
||||||
<circle {cx} {cy} r={12} fill={playerColor(p.id)} stroke="#111" stroke-width="1.5" />
|
<circle {cx} {cy} r={12} fill={playerColor(p.id)} stroke="#2b2218" stroke-width="2" />
|
||||||
<text x={cx} y={cy + 4} class="wizard-label">{p.id[0]?.toUpperCase()}</text>
|
<text x={cx} y={cy + 4} class="wizard-label">{p.id[0]?.toUpperCase()}</text>
|
||||||
{#if p.carriedTreasureId}
|
{#if p.carriedTreasureId}
|
||||||
<circle cx={cx + 9} cy={cy + 9} r={5} class="carried" />
|
<circle cx={cx + 9} cy={cy + 9} r={5} class="carried" />
|
||||||
@@ -256,21 +267,38 @@
|
|||||||
<style>
|
<style>
|
||||||
.board {
|
.board {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 640px;
|
max-width: 680px;
|
||||||
background: #d8d2c4;
|
background: #e3dcc7;
|
||||||
border: 3px solid #4a4438;
|
border: 7px solid #2b2218;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px #b7ad92,
|
||||||
|
0 12px 34px rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
.floor {
|
.floor {
|
||||||
fill: #e8e2d4;
|
fill: #e3dcc7;
|
||||||
stroke: #b8b0a0;
|
stroke: #b7ad92;
|
||||||
stroke-width: 1;
|
stroke-width: 1.1;
|
||||||
|
stroke-dasharray: 4 2.5;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.floor:hover { fill: #f2ecda; }
|
.floor:hover { fill: #efe8d2; }
|
||||||
.wall { fill: #4a4438; }
|
.wall {
|
||||||
.door { fill: #8b5a2b; }
|
fill: #f2ecd8;
|
||||||
.firewall { fill: #e0442a; }
|
stroke: #2b2218;
|
||||||
|
stroke-width: 1.6;
|
||||||
|
}
|
||||||
|
.door {
|
||||||
|
fill: #8b5a2b;
|
||||||
|
stroke: #2b2218;
|
||||||
|
stroke-width: 1.4;
|
||||||
|
rx: 2;
|
||||||
|
}
|
||||||
|
.firewall {
|
||||||
|
fill: #d0342c;
|
||||||
|
stroke: #7c1a14;
|
||||||
|
stroke-width: 1.2;
|
||||||
|
}
|
||||||
.stone { fill: #6a6458; stroke: #3a362e; stroke-width: 2; }
|
.stone { fill: #6a6458; stroke: #3a362e; stroke-width: 2; }
|
||||||
.bush { fill: #2e7d32; stroke: #1b4d1e; stroke-width: 2; }
|
.bush { fill: #2e7d32; stroke: #1b4d1e; stroke-width: 2; }
|
||||||
.rose { fill: #2e7d32; stroke: #b0245a; stroke-width: 3; }
|
.rose { fill: #2e7d32; stroke: #b0245a; stroke-width: 3; }
|
||||||
@@ -289,12 +317,15 @@
|
|||||||
.creature-body { fill: #3b3428; stroke-width: 2.5; }
|
.creature-body { fill: #3b3428; stroke-width: 2.5; }
|
||||||
.creature-body.selected { fill: #6b5a34; }
|
.creature-body.selected { fill: #6b5a34; }
|
||||||
.creature-label { font-size: 11px; font-weight: bold; fill: #f4ead2; text-anchor: middle; pointer-events: none; }
|
.creature-label { font-size: 11px; font-weight: bold; fill: #f4ead2; text-anchor: middle; pointer-events: none; }
|
||||||
.home { font-size: 26px; text-anchor: middle; dominant-baseline: middle; opacity: 0.85; }
|
.home-star { stroke: #2b2218; stroke-width: 1.4; opacity: 0.92; }
|
||||||
.treasure { stroke: #111; stroke-width: 1.2; }
|
.treasure { stroke: #2b2218; stroke-width: 1.3; }
|
||||||
.warp { font-size: 13px; text-anchor: middle; fill: #6a5f4b; font-weight: bold; }
|
.treasure-swirl { fill: none; stroke: #2b2218; stroke-width: 1.2; stroke-dasharray: 5 2.2; }
|
||||||
|
.treasure-core { fill: #2b2218; }
|
||||||
|
.warp { font-size: 14px; text-anchor: middle; fill: #2b2218; font-weight: bold; opacity: 0.65; }
|
||||||
.wizard { cursor: pointer; }
|
.wizard { cursor: pointer; }
|
||||||
.wizard-label {
|
.wizard-label {
|
||||||
font-size: 13px; font-weight: bold; fill: white;
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 13px; font-weight: 600; fill: #f4ead2;
|
||||||
text-anchor: middle; pointer-events: none;
|
text-anchor: middle; pointer-events: none;
|
||||||
}
|
}
|
||||||
.carried { fill: gold; stroke: #111; stroke-width: 1; }
|
.carried { fill: gold; stroke: #111; stroke-width: 1; }
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cardDef } from "@wizwar/engine";
|
||||||
|
import type { CardInstance } from "@wizwar/engine";
|
||||||
|
|
||||||
|
let {
|
||||||
|
card,
|
||||||
|
selected = false,
|
||||||
|
attached = false,
|
||||||
|
marked = false,
|
||||||
|
displayed = false,
|
||||||
|
charges = null,
|
||||||
|
onclick,
|
||||||
|
}: {
|
||||||
|
card: CardInstance;
|
||||||
|
selected?: boolean;
|
||||||
|
attached?: boolean;
|
||||||
|
marked?: boolean;
|
||||||
|
displayed?: boolean;
|
||||||
|
charges?: number | null;
|
||||||
|
onclick?: () => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const def = $derived(cardDef(card.cardId));
|
||||||
|
const isNumber = $derived(def.cardType === "number");
|
||||||
|
|
||||||
|
const leftRail = $derived.by(() => {
|
||||||
|
switch (def.cardType) {
|
||||||
|
case "attack": return "ATTACK";
|
||||||
|
case "neutral": return "NEUTRAL";
|
||||||
|
case "counteraction": return "COUNTERACTION";
|
||||||
|
case "neutral/counteraction": return "NEUTRAL/COUNTERACTION";
|
||||||
|
case "trap": return "TRAP";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const rightRail = $derived(def.los ? "L.O.S." : def.adjacent ? "ADJACENT" : "");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="card"
|
||||||
|
class:selected
|
||||||
|
class:attached
|
||||||
|
class:marked
|
||||||
|
class:number={isNumber}
|
||||||
|
{onclick}
|
||||||
|
title={def.text ?? def.name}
|
||||||
|
>
|
||||||
|
{#if isNumber}
|
||||||
|
<span class="corner tl">{def.value}</span>
|
||||||
|
<span class="numeral" data-value={def.value}>{def.value}</span>
|
||||||
|
<span class="brand">Wiz-War</span>
|
||||||
|
<span class="corner br">{def.value}</span>
|
||||||
|
{:else}
|
||||||
|
{#if leftRail}<span class="rail left">{leftRail}</span>{/if}
|
||||||
|
{#if rightRail}<span class="rail right">{rightRail}</span>{/if}
|
||||||
|
<span class="name">{def.name}</span>
|
||||||
|
{#if def.text}<span class="text">{def.text}</span>{/if}
|
||||||
|
{/if}
|
||||||
|
{#if displayed}
|
||||||
|
<span class="badge">DISPLAYED{charges != null ? ` · ${charges}` : ""}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
width: 8.2rem;
|
||||||
|
height: 11.4rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at 30% 20%, rgba(255, 252, 240, 0.5), transparent 60%),
|
||||||
|
#e9e1cb;
|
||||||
|
border: 1px solid #b3a687;
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.5) inset,
|
||||||
|
0 3px 8px rgba(10, 8, 4, 0.45);
|
||||||
|
color: #5f4a33;
|
||||||
|
padding: 0.55rem 1.05rem 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-0.55rem);
|
||||||
|
box-shadow: 0 8px 18px rgba(10, 8, 4, 0.55);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.card:focus-visible {
|
||||||
|
outline: 2px solid #d8b23a;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.card.selected {
|
||||||
|
transform: translateY(-0.8rem);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2.5px #d8b23a,
|
||||||
|
0 10px 20px rgba(10, 8, 4, 0.6);
|
||||||
|
}
|
||||||
|
.card.attached {
|
||||||
|
transform: translateY(-0.5rem);
|
||||||
|
box-shadow: 0 0 0 2.5px #2e7d32, 0 8px 16px rgba(10, 8, 4, 0.55);
|
||||||
|
}
|
||||||
|
.card.marked {
|
||||||
|
box-shadow: 0 0 0 2.5px #b3372b, 0 4px 10px rgba(10, 8, 4, 0.5);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.55rem;
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 0.42rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
color: #4a3a26;
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
text-orientation: upright;
|
||||||
|
line-height: 1;
|
||||||
|
max-height: 9.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.rail.left { left: 0.28rem; }
|
||||||
|
.rail.right { right: 0.28rem; }
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #43331f;
|
||||||
|
margin: 0.35rem 0 0.3rem;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
font-family: "Archivo Narrow", sans-serif;
|
||||||
|
font-size: 0.56rem;
|
||||||
|
line-height: 1.28;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 9;
|
||||||
|
line-clamp: 9;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Number cards: a big numeral in a bordered plate, like the originals. */
|
||||||
|
.numeral {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #43331f;
|
||||||
|
margin-top: 1.6rem;
|
||||||
|
padding: 0.4rem 1rem 0.5rem;
|
||||||
|
border: 2px solid #5f4a33;
|
||||||
|
}
|
||||||
|
.numeral[data-value="3"] { border-style: dashed; }
|
||||||
|
.numeral[data-value="4"] { border-radius: 12px; }
|
||||||
|
.numeral[data-value="5"] { border-style: double; border-width: 4px; }
|
||||||
|
.numeral[data-value="6"] { border-style: none; outline: 2px dotted #5f4a33; outline-offset: 4px; }
|
||||||
|
.brand {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
}
|
||||||
|
.corner {
|
||||||
|
position: absolute;
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.corner.tl { top: 0.3rem; left: 0.45rem; }
|
||||||
|
.corner.br { bottom: 0.3rem; right: 0.45rem; transform: rotate(180deg); }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.3rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #43331f;
|
||||||
|
color: #e9e1cb;
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 0.45rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
padding: 0.12rem 0.4rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.card { transition: none; }
|
||||||
|
.card:hover, .card.selected, .card.attached { transform: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user