The cockpit wears the board's ongoing spells
Sticky webs strand across a caught body, MEDUSA drains it to gray stone, the invisible and the misted go translucent, BIG MAN towers and SHRINK dwindles the billboard, and FEAR smoulders a dark-red dread ring at its bearer's feet — the six condition looks the 2D board wore alone. Marks flow billboard → projection → draw, painted only when the walls show the sprite. The workshop poses them: ?wear=card;card dresses the Rival. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
b996c2bc93
commit
203b6101bd
@@ -488,6 +488,7 @@
|
|||||||
// additively. Either way translucency applies.
|
// additively. Either way translucency applies.
|
||||||
if (s.glow) ctx.globalCompositeOperation = "lighter";
|
if (s.glow) ctx.globalCompositeOperation = "lighter";
|
||||||
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = s.alpha ?? 1;
|
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = s.alpha ?? 1;
|
||||||
|
if (s.gaze) ctx.filter = "grayscale(0.9) brightness(0.8)";
|
||||||
for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) {
|
for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) {
|
||||||
if (!spriteVisibleInCol(s, col, zbuf, warpIdCol, warpDistCol)) continue;
|
if (!spriteVisibleInCol(s, col, zbuf, warpIdCol, warpDistCol)) continue;
|
||||||
if (s.depth < spriteZ[col]!) {
|
if (s.depth < spriteZ[col]!) {
|
||||||
@@ -512,8 +513,47 @@
|
|||||||
ctx.fillRect(col, s.top, 1, s.bottom - s.top);
|
ctx.fillRect(col, s.top, 1, s.bottom - s.top);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (s.gaze) ctx.filter = "none";
|
||||||
if (s.glow) ctx.globalCompositeOperation = "source-over";
|
if (s.glow) ctx.globalCompositeOperation = "source-over";
|
||||||
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = 1;
|
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = 1;
|
||||||
|
// STICKY WAND's webs: strands crisscross the caught body, and FEAR's
|
||||||
|
// dread ring smoulders at the feet — both only when the walls show
|
||||||
|
// any of the sprite at all.
|
||||||
|
if ((s.webbed || s.dread) && s.hit) {
|
||||||
|
let seen = false;
|
||||||
|
for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) {
|
||||||
|
if (spriteVisibleInCol(s, col, zbuf, warpIdCol, warpDistCol)) { seen = true; break; }
|
||||||
|
}
|
||||||
|
if (seen) {
|
||||||
|
const w = s.right - s.left, h = s.bottom - s.top;
|
||||||
|
ctx.save();
|
||||||
|
if (s.webbed) {
|
||||||
|
ctx.strokeStyle = "rgba(240,240,235,0.55)";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (const [x1, y1, x2, y2] of [
|
||||||
|
[0.05, 0.2, 0.95, 0.75], [0.1, 0.7, 0.9, 0.15],
|
||||||
|
[0.0, 0.45, 1.0, 0.5], [0.35, 0.05, 0.55, 0.95], [0.75, 0.1, 0.6, 0.9],
|
||||||
|
] as const) {
|
||||||
|
ctx.moveTo(s.left + x1 * w, s.top + y1 * h);
|
||||||
|
ctx.lineTo(s.left + x2 * w, s.top + y2 * h);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
if (s.dread) {
|
||||||
|
const pulse = 0.45 + 0.25 * Math.sin(time / 300);
|
||||||
|
ctx.strokeStyle = `rgba(160,30,30,${pulse})`;
|
||||||
|
ctx.fillStyle = "rgba(160,30,30,0.12)";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse((s.left + s.right) / 2, Math.min(H - 2, s.bottom),
|
||||||
|
Math.max(9, w * 0.55), Math.max(4, w * 0.17), 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warp mouths wear their veil: the warp texture at the opening,
|
// Warp mouths wear their veil: the warp texture at the opening,
|
||||||
@@ -706,6 +746,9 @@
|
|||||||
clampR?: number;
|
clampR?: number;
|
||||||
hit?: { kind: "player" | "creature"; id: string };
|
hit?: { kind: "player" | "creature"; id: string };
|
||||||
cell?: { x: number; y: number };
|
cell?: { x: number; y: number };
|
||||||
|
webbed?: boolean;
|
||||||
|
gaze?: boolean;
|
||||||
|
dread?: boolean;
|
||||||
/** Sort key only: the near-bias orders sprites among THEMSELVES (a
|
/** Sort key only: the near-bias orders sprites among THEMSELVES (a
|
||||||
* hedge over the wizard standing in it) but must never let one
|
* hedge over the wizard standing in it) but must never let one
|
||||||
* cheat past a wall — occlusion always uses the true depth. */
|
* cheat past a wall — occlusion always uses the true depth. */
|
||||||
@@ -717,7 +760,8 @@
|
|||||||
fallback?: string; warped?: boolean; warpId?: number;
|
fallback?: string; warped?: boolean; warpId?: number;
|
||||||
clip?: { x: number; y: number };
|
clip?: { x: number; y: number };
|
||||||
hit?: { kind: "player" | "creature"; id: string };
|
hit?: { kind: "player" | "creature"; id: string };
|
||||||
cell?: { x: number; y: number } },
|
cell?: { x: number; y: number };
|
||||||
|
webbed?: boolean; gaze?: boolean; dread?: boolean },
|
||||||
ex: number, ey: number,
|
ex: number, ey: number,
|
||||||
): Projected | null {
|
): Projected | null {
|
||||||
const relX = b.x - ex, relY = b.y - ey;
|
const relX = b.x - ex, relY = b.y - ey;
|
||||||
@@ -770,6 +814,7 @@
|
|||||||
return {
|
return {
|
||||||
src: b.src, depth, sort: depth - (b.bias ?? 0), warped: b.warped, warpId: b.warpId,
|
src: b.src, depth, sort: depth - (b.bias ?? 0), warped: b.warped, warpId: b.warpId,
|
||||||
alpha: b.alpha, glow: b.glow, fallback: b.fallback, clampL, clampR,
|
alpha: b.alpha, glow: b.glow, fallback: b.fallback, clampL, clampR,
|
||||||
|
webbed: b.webbed, gaze: b.gaze, dread: b.dread,
|
||||||
hit: b.hit, cell: b.cell,
|
hit: b.hit, cell: b.cell,
|
||||||
left: screenX - wide / 2, right: screenX + wide / 2,
|
left: screenX - wide / 2, right: screenX + wide / 2,
|
||||||
top: bottom - size, bottom,
|
top: bottom - size, bottom,
|
||||||
|
|||||||
@@ -62,6 +62,11 @@
|
|||||||
const rival = view.players.find((p) => p.id === "Rival");
|
const rival = view.players.find((p) => p.id === "Rival");
|
||||||
if (rival && Number.isFinite(rx) && Number.isFinite(ry)) rival.position = { x: rx!, y: ry! };
|
if (rival && Number.isFinite(rx) && Number.isFinite(ry)) rival.position = { x: rx!, y: ry! };
|
||||||
}
|
}
|
||||||
|
// ?wear=sticky-web;fear dresses the Rival in ongoing spells, for
|
||||||
|
// rehearsing the sprite's condition looks (webs, gaze, dread, sizes).
|
||||||
|
for (const cardId of (q.get("wear") ?? "").split(";").filter(Boolean)) {
|
||||||
|
view.sustained.push({ id: `pose-${cardId}`, cardId, casterId: "Rival", targetId: "Rival", remainingTurns: 9, data: {} });
|
||||||
|
}
|
||||||
const aimCells = q.has("aim") ? sightedCellsFor(view) : null;
|
const aimCells = q.has("aim") ? sightedCellsFor(view) : null;
|
||||||
|
|
||||||
const start = view.players.find((p) => p.id === povId)!.position;
|
const start = view.players.find((p) => p.id === povId)!.position;
|
||||||
@@ -223,7 +228,7 @@
|
|||||||
<code>?fpv&seed=N</code>, stand anywhere with <code>&x=&y=&dir=</code>,
|
<code>?fpv&seed=N</code>, stand anywhere with <code>&x=&y=&dir=</code>,
|
||||||
or watch the clockwork's reel with <code>&demo=1</code> (toggle 👁).
|
or watch the clockwork's reel with <code>&demo=1</code> (toggle 👁).
|
||||||
<code>&aim=1</code> arms a pretend cast — sighted ground lights, marked
|
<code>&aim=1</code> arms a pretend cast — sighted ground lights, marked
|
||||||
beings wear the ember ring — and <code>&rival=x,y</code> poses the Rival.
|
beings wear the ember ring — and <code>&rival=x,y</code> poses the Rival; <code>&wear=sticky-web;fear</code> dresses them in ongoing spells.
|
||||||
</p>
|
</p>
|
||||||
<div class="stage">
|
<div class="stage">
|
||||||
<FirstPerson {view} {povId} {x} {y} {facing} litCells={aimCells} aimBeings={!!aimCells}
|
<FirstPerson {view} {povId} {x} {y} {facing} litCells={aimCells} aimBeings={!!aimCells}
|
||||||
|
|||||||
@@ -322,6 +322,12 @@ export interface Billboard {
|
|||||||
bias?: number;
|
bias?: number;
|
||||||
/** Procedural stand-in name while the painted file loads. */
|
/** Procedural stand-in name while the painted file loads. */
|
||||||
fallback?: string;
|
fallback?: string;
|
||||||
|
/** STICKY WAND's webs: strands drawn over the body. */
|
||||||
|
webbed?: boolean;
|
||||||
|
/** MEDUSA's gaze: the body drains to gray stone. */
|
||||||
|
gaze?: boolean;
|
||||||
|
/** FEAR: a dark-red dread ring at the feet, the aura's herald. */
|
||||||
|
dread?: boolean;
|
||||||
/** Identity for growth animation: a creature's id, or a cell key. */
|
/** Identity for growth animation: a creature's id, or a cell key. */
|
||||||
key?: string;
|
key?: string;
|
||||||
/** Cell-bound volumes never paint outside their own square: a
|
/** Cell-bound volumes never paint outside their own square: a
|
||||||
@@ -393,12 +399,25 @@ export function billboards(
|
|||||||
posOverride?: Record<string, { x: number; y: number }>,
|
posOverride?: Record<string, { x: number; y: number }>,
|
||||||
): Billboard[] {
|
): Billboard[] {
|
||||||
const out: Billboard[] = [];
|
const out: Billboard[] = [];
|
||||||
|
const wears = (id: string, cardId: string) =>
|
||||||
|
view.sustained.some((e) => e.cardId === cardId && e.targetId === id);
|
||||||
for (const p of view.players) {
|
for (const p of view.players) {
|
||||||
if (!p.alive || p.id === povId) continue;
|
if (!p.alive || p.id === povId) continue;
|
||||||
const o = posOverride?.[p.id];
|
const o = posOverride?.[p.id];
|
||||||
|
// Ongoing spells wear their look here as on the board: giants tower,
|
||||||
|
// the shrunk dwindle, the unseen go translucent, webs and stone and
|
||||||
|
// dread mark their bearers.
|
||||||
|
const big = wears(p.id, "big-man");
|
||||||
|
const small = wears(p.id, "shrink");
|
||||||
|
const ghost = wears(p.id, "invisible") || wears(p.id, "mist-body");
|
||||||
out.push({
|
out.push({
|
||||||
x: o?.x ?? p.position.x + 0.5, y: o?.y ?? p.position.y + 0.5,
|
x: o?.x ?? p.position.x + 0.5, y: o?.y ?? p.position.y + 0.5,
|
||||||
src: art(`wizard-${p.colorIndex}`, "players"), scale: 0.85, rise: 0, label: p.id,
|
src: art(`wizard-${p.colorIndex}`, "players"),
|
||||||
|
scale: big ? 1.2 : small ? 0.53 : 0.85, rise: 0, label: p.id,
|
||||||
|
alpha: ghost ? 0.35 : undefined,
|
||||||
|
webbed: wears(p.id, "sticky-web") || undefined,
|
||||||
|
gaze: wears(p.id, "medusa") || undefined,
|
||||||
|
dread: wears(p.id, "fear") || undefined,
|
||||||
hit: { kind: "player", id: p.id }, cell: { ...p.position },
|
hit: { kind: "player", id: p.id }, cell: { ...p.position },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user