Relocate Sector gets ground to land on
The card asked for a destination click that did not exist: a sector relocates onto an EMPTY 5x5 slot, and empty slots have no floor to click — the board only rendered cells that are part of the maze. Now lifting a sector outlines the whole 5x5 (not one square) and dashed green landings appear beyond the maze edge, one per legal slot; the svg canvas grows to hold them. Clicking a landing completes the cast. The landings mirror the engine's own relocateSector law — on the 5-grid, non-negative, vacant, every sector still adjacent to at least one other — verified slot-for-slot against engine ground truth across 2p and 4p boards. Clicking the maze again re-picks the sector, and a sector with nowhere legal to go says so in the hint bar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
18d3142f95
commit
53c84ec6cb
@@ -18,6 +18,9 @@
|
||||
onCellPeek,
|
||||
markedCell = null,
|
||||
litCells = null,
|
||||
markedSector = null,
|
||||
ghostSlots = null,
|
||||
onGhostClick,
|
||||
}: {
|
||||
view: GameView;
|
||||
edgeSelectMode?: boolean;
|
||||
@@ -33,8 +36,23 @@
|
||||
markedCell?: { x: number; y: number } | null;
|
||||
/** When set, squares NOT in this set dim — the targeting aid. */
|
||||
litCells?: Set<string> | null;
|
||||
/** Origin of a picked-up 5x5 sector: the whole sector outlines as taken. */
|
||||
markedSector?: { x: number; y: number } | null;
|
||||
/** Empty 5x5 slot origins a sector may land on — drawn as dashed ground
|
||||
* beyond the maze, since an empty slot has no floor of its own to click. */
|
||||
ghostSlots?: { x: number; y: number }[] | null;
|
||||
onGhostClick?: (origin: { x: number; y: number }) => void;
|
||||
} = $props();
|
||||
|
||||
const SECTOR = 5;
|
||||
/** Ghost slots can lie beyond the assembled maze; the canvas grows to hold them. */
|
||||
const boundsW = $derived(
|
||||
Math.max(view.board.width, ...(ghostSlots ?? []).map((g) => g.x + SECTOR)) * CELL + 16,
|
||||
);
|
||||
const boundsH = $derived(
|
||||
Math.max(view.board.height, ...(ghostSlots ?? []).map((g) => g.y + SECTOR)) * CELL + 16,
|
||||
);
|
||||
|
||||
|
||||
// Press-and-hold peeks the square's card; a fired hold swallows the click.
|
||||
let peekTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -153,11 +171,23 @@
|
||||
derive one from the viewBox alone and collapses the board to 0x0 inside
|
||||
a max-height flex column. Doubled so CSS max-* caps still govern. -->
|
||||
<svg
|
||||
viewBox={`-8 -8 ${view.board.width * CELL + 16} ${view.board.height * CELL + 16}`}
|
||||
width={(view.board.width * CELL + 16) * 2}
|
||||
height={(view.board.height * CELL + 16) * 2}
|
||||
viewBox={`-8 -8 ${boundsW} ${boundsH}`}
|
||||
width={boundsW * 2}
|
||||
height={boundsH * 2}
|
||||
class="board"
|
||||
>
|
||||
<!-- open ground a relocated sector may claim -->
|
||||
{#each ghostSlots ?? [] as g (`${g.x},${g.y}`)}
|
||||
<rect
|
||||
x={g.x * CELL + 2} y={g.y * CELL + 2}
|
||||
width={SECTOR * CELL - 4} height={SECTOR * CELL - 4}
|
||||
class="ghost-slot"
|
||||
role="button" tabindex="-1"
|
||||
onclick={() => onGhostClick?.(g)}
|
||||
onkeydown={() => {}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<!-- floor -->
|
||||
{#each cells as c (`${c.x},${c.y}`)}
|
||||
<rect
|
||||
@@ -388,6 +418,15 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- the sector picked up, awaiting its new ground -->
|
||||
{#if markedSector}
|
||||
<rect
|
||||
x={markedSector.x * CELL + 3} y={markedSector.y * CELL + 3}
|
||||
width={SECTOR * CELL - 6} height={SECTOR * CELL - 6}
|
||||
rx="8" class="marked-sector"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- wizards -->
|
||||
{#each [...wizardsByCell.entries()] as [key, group] (key)}
|
||||
{#each group as p, i (p.id)}
|
||||
@@ -607,6 +646,23 @@
|
||||
pointer-events: none;
|
||||
animation: warp-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.marked-sector {
|
||||
fill: rgba(211, 133, 43, 0.12);
|
||||
stroke: #d3852b;
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 10 6;
|
||||
pointer-events: none;
|
||||
animation: warp-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.ghost-slot {
|
||||
fill: rgba(122, 162, 122, 0.10);
|
||||
stroke: #7aa27a;
|
||||
stroke-width: 2.5;
|
||||
stroke-dasharray: 10 6;
|
||||
cursor: pointer;
|
||||
animation: warp-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.ghost-slot:hover { fill: rgba(122, 162, 122, 0.22); }
|
||||
.warp-dest {
|
||||
fill: none;
|
||||
stroke: #2e7d32;
|
||||
@@ -620,7 +676,7 @@
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.marked-cell, .warp-dest { animation: none; }
|
||||
.marked-cell, .marked-sector, .ghost-slot, .warp-dest { animation: none; }
|
||||
}
|
||||
.wizard { cursor: pointer; }
|
||||
.wizard-label {
|
||||
|
||||
Reference in New Issue
Block a user