Selecting a creature opens its card, with living stats
Tapping a creature you command now does both things at once: selects it for orders AND opens its card in the inspector corner, with a stats line pinned beneath — hits left, moves left, attack ready or spent (or barred on the turn it appears; the shadow reports itself unharmed by ordinary damage). The same stats ride along on any hold-to-peek of a creature, and they are derived live, so marching the monster updates the numbers as you spend its moves. While commanding, the hint strip carries the same line beside the hand, with a "done" to dismiss. Deselecting closes the card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d34173df9f
commit
7ad3e7acc8
@@ -5,7 +5,7 @@
|
|||||||
import Help from "./Help.svelte";
|
import Help from "./Help.svelte";
|
||||||
import Replay from "./Replay.svelte";
|
import Replay from "./Replay.svelte";
|
||||||
import { local } from "./local.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";
|
import type { CardInstance, Side } from "@wizwar/engine";
|
||||||
|
|
||||||
net.connect();
|
net.connect();
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
let attackNoticeSeen = $state<string | null>(null);
|
let attackNoticeSeen = $state<string | null>(null);
|
||||||
/** A face-up card being peeked at from the scoresheet (view-only). */
|
/** A face-up card being peeked at from the scoresheet (view-only). */
|
||||||
let peekCard = $state<CardInstance | null>(null);
|
let peekCard = $state<CardInstance | null>(null);
|
||||||
|
/** When the peeked card is a creature on the board, its live stats ride along. */
|
||||||
|
let peekCreatureId = $state<string | null>(null);
|
||||||
let helpTab = $state<"play" | "rules" | "cards" | "about">("play");
|
let helpTab = $state<"play" | "rules" | "cards" | "about">("play");
|
||||||
let hotseatCount = $state(2);
|
let hotseatCount = $state(2);
|
||||||
let setupName = $state("");
|
let setupName = $state("");
|
||||||
@@ -151,6 +153,7 @@
|
|||||||
function selectCard(card: CardInstance) {
|
function selectCard(card: CardInstance) {
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
peekCard = null;
|
peekCard = null;
|
||||||
|
peekCreatureId = null;
|
||||||
if (youMustRespond) {
|
if (youMustRespond) {
|
||||||
dispatch({ type: "counteract", instanceId: card.instanceId });
|
dispatch({ type: "counteract", instanceId: card.instanceId });
|
||||||
return;
|
return;
|
||||||
@@ -390,6 +393,20 @@
|
|||||||
tacks: "handful-of-tacks", pit: "create-pit", safe: "safe",
|
tacks: "handful-of-tacks", pit: "create-pit", safe: "safe",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function creatureStats(c: NonNullable<GameView["creatures"]>[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). */
|
/** Show the card behind whatever occupies this square (view-only). */
|
||||||
function peekAt(cell: { x: number; y: number }): boolean {
|
function peekAt(cell: { x: number; y: number }): boolean {
|
||||||
if (!view) return false;
|
if (!view) return false;
|
||||||
@@ -406,6 +423,7 @@
|
|||||||
: null;
|
: null;
|
||||||
if (!cardId) return false;
|
if (!cardId) return false;
|
||||||
peekCard = { instanceId: `peek-${cardId}`, cardId };
|
peekCard = { instanceId: `peek-${cardId}`, cardId };
|
||||||
|
peekCreatureId = creature?.id ?? null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +473,15 @@
|
|||||||
}
|
}
|
||||||
const mine = creature.controllerId === view.you || creature.kind === "democratic-monster";
|
const mine = creature.controllerId === view.you || creature.kind === "democratic-monster";
|
||||||
if (mine) {
|
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 {
|
} else {
|
||||||
peekAt(creature.position);
|
peekAt(creature.position);
|
||||||
}
|
}
|
||||||
@@ -645,8 +671,9 @@
|
|||||||
|
|
||||||
{#if peekCard}
|
{#if peekCard}
|
||||||
<div class="inspector" role="img" aria-label="displayed card, enlarged">
|
<div class="inspector" role="img" aria-label="displayed card, enlarged">
|
||||||
<button class="inspector-close" onclick={() => (peekCard = null)} aria-label="hide card">×</button>
|
<button class="inspector-close" onclick={() => { peekCard = null; peekCreatureId = null; }} aria-label="hide card">×</button>
|
||||||
<div class="inspector-card"><Card card={peekCard} /></div>
|
<div class="inspector-card"><Card card={peekCard} /></div>
|
||||||
|
{#if peekNote}<div class="peek-note">{peekNote}</div>{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -1003,8 +1030,16 @@
|
|||||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if youMustRespond || selectedDef || youMustDiscard || discardMode || discardSelection.size > 0}
|
{#if youMustRespond || selectedDef || selectedCreature || youMustDiscard || discardMode || discardSelection.size > 0}
|
||||||
<div class="hint-strip">
|
<div class="hint-strip">
|
||||||
|
{#if selectedCreature && !selectedDef}
|
||||||
|
{@const sc = view.creatures.find((c) => c.id === selectedCreature)}
|
||||||
|
{#if sc}
|
||||||
|
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}.
|
||||||
|
Tap a square beside it to march, a target in its square to attack.</span>
|
||||||
|
<button class="hint-cancel" onclick={() => { selectedCreature = null; peekCard = null; peekCreatureId = null; }}>done</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
{#if youMustRespond && view.stack}
|
{#if youMustRespond && view.stack}
|
||||||
<span class="hint-alert">
|
<span class="hint-alert">
|
||||||
{#if view.stack.defenderId === view.you}
|
{#if view.stack.defenderId === view.you}
|
||||||
@@ -1673,6 +1708,18 @@
|
|||||||
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
||||||
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
.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 {
|
.inspector {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0.9rem;
|
right: 0.9rem;
|
||||||
@@ -1740,6 +1787,18 @@
|
|||||||
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
||||||
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
.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 { right: 0.5rem; bottom: 0.5rem; }
|
||||||
.inspector-card { transform: scale(1.3); }
|
.inspector-card { transform: scale(1.3); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user