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;
|
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") {
|
if (cardId === "stone-to-water") {
|
||||||
// The cast demands sight of the stone block, so only sighted ones light.
|
// The cast demands sight of the stone block, so only sighted ones light.
|
||||||
// The card equally targets stone WALLS — edges the cell-shadow cannot
|
// The card equally targets stone WALLS — edges the cell-shadow cannot
|
||||||
|
|||||||
@@ -271,6 +271,7 @@
|
|||||||
"create-door": "click a wall for the new door",
|
"create-door": "click a wall for the new door",
|
||||||
"warp-wand": "click a wall to warp it open",
|
"warp-wand": "click a wall to warp it open",
|
||||||
"stone-to-water": "click a stone wall, or a stone-filled square",
|
"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",
|
"dispel-creation": "click the creation — a square, a creature, or a conjured wall",
|
||||||
};
|
};
|
||||||
const EDGE_CARDS = new Set([
|
const EDGE_CARDS = new Set([
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
{ kind: "soul", at: { x: 1, y: 2 } },
|
{ kind: "soul", at: { x: 1, y: 2 } },
|
||||||
{ kind: "fireworks", at: mid },
|
{ kind: "fireworks", at: mid },
|
||||||
{ kind: "chaos-swirl", 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: "pit-fall", at: mid },
|
||||||
{ kind: "ooze-slip", at: mid },
|
{ kind: "ooze-slip", at: mid },
|
||||||
{ kind: "tacks-ow", 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 Burst from "./Burst.svelte";
|
||||||
import ChaosSwirl from "./ChaosSwirl.svelte";
|
import ChaosSwirl from "./ChaosSwirl.svelte";
|
||||||
import Claw from "./Claw.svelte";
|
import Claw from "./Claw.svelte";
|
||||||
|
import DieDrop from "./DieDrop.svelte";
|
||||||
import DustPuff from "./DustPuff.svelte";
|
import DustPuff from "./DustPuff.svelte";
|
||||||
import Fireball from "./Fireball.svelte";
|
import Fireball from "./Fireball.svelte";
|
||||||
import Fireworks from "./Fireworks.svelte";
|
import Fireworks from "./Fireworks.svelte";
|
||||||
@@ -49,6 +50,7 @@ export const FX_SPRITES: { [K in BoardFx["kind"]]: Component<{ fx: FxOf<K> }> }
|
|||||||
soul: Soul,
|
soul: Soul,
|
||||||
fireworks: Fireworks,
|
fireworks: Fireworks,
|
||||||
"chaos-swirl": ChaosSwirl,
|
"chaos-swirl": ChaosSwirl,
|
||||||
|
"die-drop": DieDrop,
|
||||||
"pit-fall": PitFall,
|
"pit-fall": PitFall,
|
||||||
"ooze-slip": OozeSlip,
|
"ooze-slip": OozeSlip,
|
||||||
"tacks-ow": TacksOw,
|
"tacks-ow": TacksOw,
|
||||||
|
|||||||
+10
-1
@@ -22,7 +22,8 @@ export type FxShape =
|
|||||||
| { kind: "portal"; cell: Cell; side: Side }
|
| { kind: "portal"; cell: Cell; side: Side }
|
||||||
| { kind: "sector-spin"; origin: Cell; clockwise: boolean }
|
| { kind: "sector-spin"; origin: Cell; clockwise: boolean }
|
||||||
| { kind: "sector-slide"; from: Cell; to: Cell }
|
| { 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 };
|
export type BoardFx = FxShape & { id: number };
|
||||||
/** The narrow type of one effect kind (for sprite components). */
|
/** The narrow type of one effect kind (for sprite components). */
|
||||||
export type FxOf<K extends BoardFx["kind"]> = BoardFx & { kind: K };
|
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 "portal": case "portal-cell":
|
||||||
case "soul": case "fireworks": case "chaos-swirl":
|
case "soul": case "fireworks": case "chaos-swirl":
|
||||||
case "sector-spin": case "sector-slide": return 1500;
|
case "sector-spin": case "sector-slide": return 1500;
|
||||||
|
case "die-drop": return 1600;
|
||||||
default: return 900;
|
default: return 900;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,6 +273,13 @@ export function fxForEvents(
|
|||||||
push({ kind: "pit-fall", at: e.at });
|
push({ kind: "pit-fall", at: e.at });
|
||||||
beat++;
|
beat++;
|
||||||
break;
|
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": {
|
case "climbedFromPit": {
|
||||||
if (e.success) {
|
if (e.success) {
|
||||||
const at = posOf(e.player);
|
const at = posOf(e.player);
|
||||||
|
|||||||
Reference in New Issue
Block a user