Thumb of God gets its ceremony: prompt, sighted dimming, and the die
The cast always worked (click a square), but nothing guided it and nothing marked its arrival. Now: a targeting prompt, eligibility dimming to the caster's sighted squares (the engine demands LOS to the aim point), and a flourish — the aiming mark, a shadow gathering, the die crashing down out of a clear sky with a bounce and flung grit, a drift line when fate moves it, and every scattered token streaking to where it lands. In the /?fx gallery as die-drop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
co-authored by
Claude Fable 5
parent
5351e345c3
commit
e412463aae
@@ -352,6 +352,10 @@ export function eligibleCellsFor(view: GameView, cardId: string): Set<string> |
|
||||
return out;
|
||||
}
|
||||
|
||||
if (cardId === "thumb-of-god") {
|
||||
// The die is aimed by sight; where it lands after drifting is fate's.
|
||||
return sighted;
|
||||
}
|
||||
if (cardId === "stone-to-water") {
|
||||
// The cast demands sight of the stone block, so only sighted ones light.
|
||||
// The card equally targets stone WALLS — edges the cell-shadow cannot
|
||||
|
||||
@@ -271,6 +271,7 @@
|
||||
"create-door": "click a wall for the new door",
|
||||
"warp-wand": "click a wall to warp it open",
|
||||
"stone-to-water": "click a stone wall, or a stone-filled square",
|
||||
"thumb-of-god": "aim the die at a square in sight — it may drift on impact",
|
||||
"dispel-creation": "click the creation — a square, a creature, or a conjured wall",
|
||||
};
|
||||
const EDGE_CARDS = new Set([
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
{ kind: "soul", at: { x: 1, y: 2 } },
|
||||
{ kind: "fireworks", at: mid },
|
||||
{ kind: "chaos-swirl", at: mid },
|
||||
{ kind: "die-drop", at: { x: 1, y: 1 }, aim: { x: 0, y: 2 } },
|
||||
{ kind: "pit-fall", at: mid },
|
||||
{ kind: "ooze-slip", at: mid },
|
||||
{ kind: "tacks-ow", at: mid },
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
let { fx }: { fx: FxOf<"die-drop"> } = $props();
|
||||
const c = $derived(center(fx.at));
|
||||
const aim = $derived(center(fx.aim));
|
||||
const drifted = $derived(fx.at.x !== fx.aim.x || fx.at.y !== fx.aim.y);
|
||||
const DEBRIS = [15, 80, 150, 210, 275, 340];
|
||||
</script>
|
||||
|
||||
<!-- The aiming mark: where the caster prayed it would land -->
|
||||
<circle cx={aim.x} cy={aim.y} r="15" class="aim" />
|
||||
{#if drifted}
|
||||
<line x1={aim.x} y1={aim.y} x2={c.x} y2={c.y} class="drift" />
|
||||
{/if}
|
||||
<!-- The gathering shadow, then the die itself, out of a clear sky -->
|
||||
<ellipse cx={c.x} cy={c.y + 8} rx="16" ry="7" class="die-shadow" />
|
||||
<g class="die" style={`transform-origin: ${c.x}px ${c.y}px`}>
|
||||
<g transform={`translate(${c.x} ${c.y})`}>
|
||||
<rect x="-15" y="-15" width="30" height="30" rx="6" class="die-body" />
|
||||
<!-- a four: the drift roll made manifest -->
|
||||
<circle cx="-7" cy="-7" r="3.2" class="pip" />
|
||||
<circle cx="7" cy="-7" r="3.2" class="pip" />
|
||||
<circle cx="-7" cy="7" r="3.2" class="pip" />
|
||||
<circle cx="7" cy="7" r="3.2" class="pip" />
|
||||
</g>
|
||||
</g>
|
||||
<!-- Impact: ring and flung grit -->
|
||||
<circle cx={c.x} cy={c.y} r="14" class="impact" />
|
||||
{#each DEBRIS as deg, i (deg)}
|
||||
<line
|
||||
x1={c.x + 14 * Math.cos((deg * Math.PI) / 180)}
|
||||
y1={c.y + 14 * Math.sin((deg * Math.PI) / 180)}
|
||||
x2={c.x + 26 * Math.cos((deg * Math.PI) / 180)}
|
||||
y2={c.y + 26 * Math.sin((deg * Math.PI) / 180)}
|
||||
class={`grit g${i % 3}`}
|
||||
style={`transform-origin: ${c.x}px ${c.y}px`}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.aim {
|
||||
fill: none;
|
||||
stroke: #8d2f23;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5 4;
|
||||
opacity: 0;
|
||||
animation: aim-mark 0.5s ease-out forwards;
|
||||
}
|
||||
.drift {
|
||||
stroke: #8d2f23;
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 3 4;
|
||||
opacity: 0;
|
||||
animation: drift-line 0.5s ease-out 0.55s forwards;
|
||||
}
|
||||
.die-shadow {
|
||||
fill: rgba(43, 34, 24, 0.5);
|
||||
animation: shadow-loom 0.55s ease-in forwards;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
.die {
|
||||
animation: die-fall 0.55s cubic-bezier(0.5, 0, 0.9, 0.6) forwards,
|
||||
die-settle 0.9s ease-out 0.55s forwards;
|
||||
}
|
||||
.die-body {
|
||||
fill: #efe8d4;
|
||||
stroke: #2b2218;
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
.pip { fill: #2b2218; }
|
||||
.impact {
|
||||
fill: none;
|
||||
stroke: #7c6a4f;
|
||||
stroke-width: 3;
|
||||
opacity: 0;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: impact-ring 0.5s ease-out 0.55s forwards;
|
||||
}
|
||||
.grit {
|
||||
stroke: #7c6a4f;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
opacity: 0;
|
||||
animation: grit-fly 0.4s ease-out 0.58s forwards;
|
||||
}
|
||||
.grit.g1 { animation-delay: 0.62s; }
|
||||
.grit.g2 { animation-delay: 0.66s; }
|
||||
@keyframes aim-mark {
|
||||
0% { opacity: 0; transform: scale(1.6); transform-origin: center; transform-box: fill-box; }
|
||||
100% { opacity: 0.8; transform: scale(1); }
|
||||
}
|
||||
@keyframes drift-line {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 0.7; }
|
||||
}
|
||||
@keyframes shadow-loom {
|
||||
0% { opacity: 0; transform: scale(0.2); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@keyframes die-fall {
|
||||
0% { opacity: 0; transform: translateY(-150px) scale(2.4) rotate(35deg); }
|
||||
30% { opacity: 1; }
|
||||
100% { transform: translateY(0) scale(1) rotate(0deg); }
|
||||
}
|
||||
@keyframes die-settle {
|
||||
0% { transform: translateY(0) scale(1.15, 0.85); }
|
||||
25% { transform: translateY(-6px) scale(1); }
|
||||
45% { transform: translateY(0) scale(1.05, 0.95); }
|
||||
100% { transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes impact-ring {
|
||||
0% { opacity: 0.9; transform: scale(0.4); }
|
||||
100% { opacity: 0; transform: scale(2.4); }
|
||||
}
|
||||
@keyframes grit-fly {
|
||||
0% { opacity: 0.9; transform: scale(0.7); }
|
||||
100% { opacity: 0; transform: scale(1.8); }
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@ import Bolt from "./Bolt.svelte";
|
||||
import Burst from "./Burst.svelte";
|
||||
import ChaosSwirl from "./ChaosSwirl.svelte";
|
||||
import Claw from "./Claw.svelte";
|
||||
import DieDrop from "./DieDrop.svelte";
|
||||
import DustPuff from "./DustPuff.svelte";
|
||||
import Fireball from "./Fireball.svelte";
|
||||
import Fireworks from "./Fireworks.svelte";
|
||||
@@ -49,6 +50,7 @@ export const FX_SPRITES: { [K in BoardFx["kind"]]: Component<{ fx: FxOf<K> }> }
|
||||
soul: Soul,
|
||||
fireworks: Fireworks,
|
||||
"chaos-swirl": ChaosSwirl,
|
||||
"die-drop": DieDrop,
|
||||
"pit-fall": PitFall,
|
||||
"ooze-slip": OozeSlip,
|
||||
"tacks-ow": TacksOw,
|
||||
|
||||
+10
-1
@@ -22,7 +22,8 @@ export type FxShape =
|
||||
| { kind: "portal"; cell: Cell; side: Side }
|
||||
| { kind: "sector-spin"; origin: Cell; clockwise: boolean }
|
||||
| { kind: "sector-slide"; from: Cell; to: Cell }
|
||||
| { kind: "edge-dust"; cell: Cell; side: Side };
|
||||
| { kind: "edge-dust"; cell: Cell; side: Side }
|
||||
| { kind: "die-drop"; at: Cell; aim: Cell };
|
||||
export type BoardFx = FxShape & { id: number };
|
||||
/** The narrow type of one effect kind (for sprite components). */
|
||||
export type FxOf<K extends BoardFx["kind"]> = BoardFx & { kind: K };
|
||||
@@ -37,6 +38,7 @@ export function fxTtl(kind: BoardFx["kind"]): number {
|
||||
case "portal": case "portal-cell":
|
||||
case "soul": case "fireworks": case "chaos-swirl":
|
||||
case "sector-spin": case "sector-slide": return 1500;
|
||||
case "die-drop": return 1600;
|
||||
default: return 900;
|
||||
}
|
||||
}
|
||||
@@ -271,6 +273,13 @@ export function fxForEvents(
|
||||
push({ kind: "pit-fall", at: e.at });
|
||||
beat++;
|
||||
break;
|
||||
case "thumbOfGod":
|
||||
push({ kind: "die-drop", at: e.landedAt, aim: e.aimedAt });
|
||||
beat += 2; // the scattered tokens' streaks follow the impact
|
||||
break;
|
||||
case "tokenScattered":
|
||||
push({ kind: "streak", a: cellMid(e.from), b: cellMid(e.to) });
|
||||
break;
|
||||
case "climbedFromPit": {
|
||||
if (e.success) {
|
||||
const at = posOf(e.player);
|
||||
|
||||
Reference in New Issue
Block a user