diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 7ec8d20..7dde936 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -5,7 +5,7 @@ import Help from "./Help.svelte"; import Replay from "./Replay.svelte"; import { local } from "./local.svelte"; - import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor } from "@wizwar/engine"; + import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor, type GameView } from "@wizwar/engine"; import type { CardInstance, Side } from "@wizwar/engine"; net.connect(); @@ -19,6 +19,8 @@ let attackNoticeSeen = $state(null); /** A face-up card being peeked at from the scoresheet (view-only). */ let peekCard = $state(null); + /** When the peeked card is a creature on the board, its live stats ride along. */ + let peekCreatureId = $state(null); let helpTab = $state<"play" | "rules" | "cards" | "about">("play"); let hotseatCount = $state(2); let setupName = $state(""); @@ -151,6 +153,7 @@ function selectCard(card: CardInstance) { if (!view) return; peekCard = null; + peekCreatureId = null; if (youMustRespond) { dispatch({ type: "counteract", instanceId: card.instanceId }); return; @@ -390,6 +393,20 @@ tacks: "handful-of-tacks", pit: "create-pit", safe: "safe", }; + function creatureStats(c: NonNullable[number]): string { + const hp = Number.isFinite(c.maxDamage) + ? `${c.maxDamage - c.damage} of ${c.maxDamage} hits left` + : "unharmed by ordinary damage"; + const moves = `${c.movesPerTurn - c.movementUsed} of ${c.movesPerTurn} moves`; + const atk = c.justCreated ? "no attack the turn it appears" : c.attackUsed ? "attack spent" : "attack ready"; + return `${hp} · ${moves} · ${atk}`; + } + const peekNote = $derived.by(() => { + if (!view || !peekCreatureId) return null; + const c = view.creatures.find((c) => c.id === peekCreatureId); + return c ? creatureStats(c) : null; + }); + /** Show the card behind whatever occupies this square (view-only). */ function peekAt(cell: { x: number; y: number }): boolean { if (!view) return false; @@ -406,6 +423,7 @@ : null; if (!cardId) return false; peekCard = { instanceId: `peek-${cardId}`, cardId }; + peekCreatureId = creature?.id ?? null; return true; } @@ -455,7 +473,15 @@ } const mine = creature.controllerId === view.you || creature.kind === "democratic-monster"; if (mine) { - selectedCreature = selectedCreature === creatureId ? null : creatureId; + const selecting = selectedCreature !== creatureId; + selectedCreature = selecting ? creatureId : null; + if (selecting) { + peekCard = { instanceId: `peek-${creature.kind}`, cardId: creature.kind }; + peekCreatureId = creature.id; + } else { + peekCard = null; + peekCreatureId = null; + } } else { peekAt(creature.position); } @@ -645,8 +671,9 @@ {#if peekCard} {/if} @@ -1003,8 +1030,16 @@ {/if} - {#if youMustRespond || selectedDef || youMustDiscard || discardMode || discardSelection.size > 0} + {#if youMustRespond || selectedDef || selectedCreature || youMustDiscard || discardMode || discardSelection.size > 0}
+ {#if selectedCreature && !selectedDef} + {@const sc = view.creatures.find((c) => c.id === selectedCreature)} + {#if sc} + Commanding {cardDef(sc.kind).name} — {creatureStats(sc)}. + Tap a square beside it to march, a target in its square to attack. + + {/if} + {/if} {#if youMustRespond && view.stack} {#if view.stack.defenderId === view.you} @@ -1673,6 +1708,18 @@ .attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; } .attack-fist { font-size: 1.1rem; color: #43331f; } + .peek-note { + margin-top: 0.4rem; + background: #2b2218; + color: #e8dfc6; + font-family: "Courier Prime", monospace; + font-size: 0.72rem; + padding: 0.25rem 0.5rem; + border-radius: 3px; + text-align: center; + max-width: 11rem; + } + .inspector { position: fixed; right: 0.9rem; @@ -1740,6 +1787,18 @@ .attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; } .attack-fist { font-size: 1.1rem; color: #43331f; } + .peek-note { + margin-top: 0.4rem; + background: #2b2218; + color: #e8dfc6; + font-family: "Courier Prime", monospace; + font-size: 0.72rem; + padding: 0.25rem 0.5rem; + border-radius: 3px; + text-align: center; + max-width: 11rem; + } + .inspector { right: 0.5rem; bottom: 0.5rem; } .inspector-card { transform: scale(1.3); } }