Every token on the board can be read as its card
"What does a rosebush do again?" now has an in-game answer: press and hold any square (450ms) to open the card behind whatever occupies it — creature, terrain, dropped object, warp token, or boobytrap marker — in the view-only inspector corner. A plain click that claims no game action peeks too: out of turn, every tap reads; on your turn, taps beyond your reach read instead of doing nothing, and clicking an opponent's creature with nothing selected reads its card. Priority when a square holds several stories: creature, then terrain, then the topmost dropped object. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a9d40d73ac
commit
97a3305d07
@@ -267,7 +267,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clickCell(cell: { x: number; y: number }) {
|
function clickCell(cell: { x: number; y: number }) {
|
||||||
if (!view || !isYourTurn) return;
|
if (!view) return;
|
||||||
|
if (!isYourTurn) { peekAt(cell); return; }
|
||||||
if (pendingCellFor && selectedCard) {
|
if (pendingCellFor && selectedCard) {
|
||||||
// Stage 2 of teleport-opponent: destination chosen.
|
// Stage 2 of teleport-opponent: destination chosen.
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -371,6 +372,34 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The click claimed no action — offer the square's card instead.
|
||||||
|
peekAt(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Card ids behind the terrain tokens, for tap-to-read. */
|
||||||
|
const CONTENT_CARD: Record<string, string> = {
|
||||||
|
stone: "fill-square-with-stone", thornbush: "thornbush", rosebush: "rosebush",
|
||||||
|
ooze: "killer-ooze", dust: "dust-cloud", slime: "fill-square-with-slime",
|
||||||
|
tacks: "handful-of-tacks", pit: "create-pit", safe: "safe",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Show the card behind whatever occupies this square (view-only). */
|
||||||
|
function peekAt(cell: { x: number; y: number }): boolean {
|
||||||
|
if (!view) return false;
|
||||||
|
const key = `${cell.x},${cell.y}`;
|
||||||
|
const creature = view.creatures.find((c) => c.position.x === cell.x && c.position.y === cell.y);
|
||||||
|
const content = view.squareContents[key];
|
||||||
|
const objects = view.groundObjects[key] ?? [];
|
||||||
|
const cardId =
|
||||||
|
creature ? creature.kind
|
||||||
|
: content ? CONTENT_CARD[content.kind]
|
||||||
|
: objects.length > 0 ? objects[objects.length - 1]!.cardId
|
||||||
|
: view.dimWarps.some((w) => (w.a.x === cell.x && w.a.y === cell.y) || (w.b.x === cell.x && w.b.y === cell.y)) ? "dimensional-warp"
|
||||||
|
: view.boobytraps.some((t) => t.cells.some((c) => c.x === cell.x && c.y === cell.y)) ? "boobytrap"
|
||||||
|
: null;
|
||||||
|
if (!cardId) return false;
|
||||||
|
peekCard = { instanceId: `peek-${cardId}`, cardId };
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickWarp(cell: { x: number; y: number }, side: Side) {
|
function clickWarp(cell: { x: number; y: number }, side: Side) {
|
||||||
@@ -395,9 +424,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clickCreature(creatureId: string) {
|
function clickCreature(creatureId: string) {
|
||||||
if (!view || !isYourTurn) return;
|
if (!view) return;
|
||||||
const creature = view.creatures.find((c) => c.id === creatureId);
|
const creature = view.creatures.find((c) => c.id === creatureId);
|
||||||
if (!creature) return;
|
if (!creature) return;
|
||||||
|
if (!isYourTurn) { peekAt(creature.position); return; }
|
||||||
if (selectedCard && (CREATURE_TARGET_CARDS.has(selectedCard.cardId) || cardDef(selectedCard.cardId).cardType === "attack")) {
|
if (selectedCard && (CREATURE_TARGET_CARDS.has(selectedCard.cardId) || cardDef(selectedCard.cardId).cardType === "attack")) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "cast", instanceId: selectedCard.instanceId,
|
type: "cast", instanceId: selectedCard.instanceId,
|
||||||
@@ -416,6 +446,8 @@
|
|||||||
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;
|
selectedCreature = selectedCreature === creatureId ? null : creatureId;
|
||||||
|
} else {
|
||||||
|
peekAt(creature.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,6 +877,7 @@
|
|||||||
onCreatureClick={clickCreature}
|
onCreatureClick={clickCreature}
|
||||||
onWarpClick={clickWarp}
|
onWarpClick={clickWarp}
|
||||||
markedCell={tradeFrom ?? pendingSectorFrom}
|
markedCell={tradeFrom ?? pendingSectorFrom}
|
||||||
|
onCellPeek={(c) => peekAt(c)}
|
||||||
{litCells}
|
{litCells}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
onPlayerClick,
|
onPlayerClick,
|
||||||
onCreatureClick,
|
onCreatureClick,
|
||||||
onWarpClick,
|
onWarpClick,
|
||||||
|
onCellPeek,
|
||||||
markedCell = null,
|
markedCell = null,
|
||||||
litCells = null,
|
litCells = null,
|
||||||
}: {
|
}: {
|
||||||
@@ -25,6 +26,8 @@
|
|||||||
onPlayerClick?: (playerId: string) => void;
|
onPlayerClick?: (playerId: string) => void;
|
||||||
onCreatureClick?: (creatureId: string) => void;
|
onCreatureClick?: (creatureId: string) => void;
|
||||||
onWarpClick?: (cell: { x: number; y: number }, side: Side) => void;
|
onWarpClick?: (cell: { x: number; y: number }, side: Side) => void;
|
||||||
|
/** Long-press on a square: read the card behind whatever occupies it. */
|
||||||
|
onCellPeek?: (cell: { x: number; y: number }) => void;
|
||||||
/** First square of a two-square spell: marked so the click reads as taken. */
|
/** First square of a two-square spell: marked so the click reads as taken. */
|
||||||
markedCell?: { x: number; y: number } | null;
|
markedCell?: { x: number; y: number } | null;
|
||||||
/** When set, squares NOT in this set dim — the targeting aid. */
|
/** When set, squares NOT in this set dim — the targeting aid. */
|
||||||
@@ -33,6 +36,23 @@
|
|||||||
|
|
||||||
const PLAYER_COLORS = ["#1a9c46", "#d3352b", "#c9308f", "#3a3ac0", "#2ab0c9", "#c9a72a"];
|
const PLAYER_COLORS = ["#1a9c46", "#d3352b", "#c9308f", "#3a3ac0", "#2ab0c9", "#c9a72a"];
|
||||||
|
|
||||||
|
// Press-and-hold peeks the square's card; a fired hold swallows the click.
|
||||||
|
let peekTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let peekFired = false;
|
||||||
|
function pressCell(c: { x: number; y: number }) {
|
||||||
|
peekFired = false;
|
||||||
|
if (!onCellPeek) return;
|
||||||
|
peekTimer = setTimeout(() => { peekFired = true; onCellPeek(c); }, 450);
|
||||||
|
}
|
||||||
|
function releasePress() {
|
||||||
|
if (peekTimer) clearTimeout(peekTimer);
|
||||||
|
peekTimer = null;
|
||||||
|
}
|
||||||
|
function tapCell(c: { x: number; y: number }) {
|
||||||
|
if (peekFired) { peekFired = false; return; }
|
||||||
|
onCellClick?.(c);
|
||||||
|
}
|
||||||
|
|
||||||
// Real token art, cropped from the owner's physical set (public/tokens/).
|
// Real token art, cropped from the owner's physical set (public/tokens/).
|
||||||
// Anything without an entry falls back to the vector rendering below.
|
// Anything without an entry falls back to the vector rendering below.
|
||||||
const USE_TOKEN_ART = true;
|
const USE_TOKEN_ART = true;
|
||||||
@@ -137,7 +157,10 @@
|
|||||||
x={c.x * CELL} y={c.y * CELL} width={CELL} height={CELL}
|
x={c.x * CELL} y={c.y * CELL} width={CELL} height={CELL}
|
||||||
class="floor"
|
class="floor"
|
||||||
role="button" tabindex="-1"
|
role="button" tabindex="-1"
|
||||||
onclick={() => onCellClick?.(c)}
|
onclick={() => tapCell(c)}
|
||||||
|
onpointerdown={() => pressCell(c)}
|
||||||
|
onpointerup={releasePress}
|
||||||
|
onpointerleave={releasePress}
|
||||||
onkeydown={() => {}}
|
onkeydown={() => {}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user