The cockpit's webs and dread ring learn to sit right
Sticky-web silk becomes a real web — hub, spokes, two sagging rings — each strand shadowed so it reads against busy token art. The dread ring moves behind the body onto the floor plane, sharing the ember ring's flattened perspective instead of smouldering over the sprite. 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
19888f9e84
commit
c8f719f467
@@ -488,18 +488,32 @@ import type { GameView } from "@wizwar/engine";
|
|||||||
: null);
|
: null);
|
||||||
const iw = img instanceof HTMLImageElement ? img.naturalWidth : (img?.width ?? 0);
|
const iw = img instanceof HTMLImageElement ? img.naturalWidth : (img?.width ?? 0);
|
||||||
const ih = img instanceof HTMLImageElement ? img.naturalHeight : (img?.height ?? 0);
|
const ih = img instanceof HTMLImageElement ? img.naturalHeight : (img?.height ?? 0);
|
||||||
// A being standing on castable ground wears an ember ring while a
|
// Rings live on the floor, behind the body, so the art stays clean:
|
||||||
// targeted card is armed: the cockpit's answer to the board's lit
|
// FEAR's dread ring smoulders at the feet, and a being standing on
|
||||||
// squares, drawn behind the body so the art stays clean. Only a
|
// castable ground wears the ember ring while a targeted card is
|
||||||
// sprite the walls actually show earns its ring.
|
// armed — the cockpit's answer to the board's lit squares. Only a
|
||||||
if (ontarget && aimBeings && litCells && s.hit && s.cell &&
|
// sprite the walls actually show earns its marks.
|
||||||
litCells.has(`${s.cell.x},${s.cell.y}`) &&
|
|
||||||
!(s.hit.kind === "player" && s.hit.id === view.you)) {
|
|
||||||
let seen = false;
|
let seen = false;
|
||||||
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)) { seen = true; break; }
|
if (spriteVisibleInCol(s, col, zbuf, warpIdCol, warpDistCol)) { seen = true; break; }
|
||||||
}
|
}
|
||||||
if (seen) {
|
const ringY = Math.min(H - 2, s.bottom);
|
||||||
|
if (seen && s.dread && s.hit) {
|
||||||
|
const w = s.right - s.left;
|
||||||
|
const pulse = 0.45 + 0.25 * Math.sin(time / 300);
|
||||||
|
ctx.save();
|
||||||
|
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, ringY, Math.max(9, w * 0.5), Math.max(3, w * 0.155), 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
if (seen && ontarget && aimBeings && litCells && s.hit && s.cell &&
|
||||||
|
litCells.has(`${s.cell.x},${s.cell.y}`) &&
|
||||||
|
!(s.hit.kind === "player" && s.hit.id === view.you)) {
|
||||||
const w = s.right - s.left;
|
const w = s.right - s.left;
|
||||||
const cx = (s.left + s.right) / 2;
|
const cx = (s.left + s.right) / 2;
|
||||||
const pulse = 0.55 + 0.25 * Math.sin(time / 220);
|
const pulse = 0.55 + 0.25 * Math.sin(time / 220);
|
||||||
@@ -508,12 +522,11 @@ import type { GameView } from "@wizwar/engine";
|
|||||||
ctx.fillStyle = "rgba(232,160,60,0.16)";
|
ctx.fillStyle = "rgba(232,160,60,0.16)";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.ellipse(cx, Math.min(H - 2, s.bottom), Math.max(7, w * 0.42), Math.max(3, w * 0.13), 0, 0, Math.PI * 2);
|
ctx.ellipse(cx, ringY, Math.max(7, w * 0.42), Math.max(3, w * 0.13), 0, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Painted art composites normally (inks stay true); light glows
|
// Painted art composites normally (inks stay true); light glows
|
||||||
// additively. Either way translucency applies.
|
// additively. Either way translucency applies.
|
||||||
if (s.glow) ctx.globalCompositeOperation = "lighter";
|
if (s.glow) ctx.globalCompositeOperation = "lighter";
|
||||||
@@ -546,45 +559,42 @@ import type { GameView } from "@wizwar/engine";
|
|||||||
if (s.gaze) ctx.filter = "none";
|
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
|
// STICKY WAND's webs cling over the caught body: spokes from a hub
|
||||||
// dread ring smoulders at the feet — both only when the walls show
|
// and two sagging rings, each strand shadowed so the silk reads
|
||||||
// any of the sprite at all.
|
// against busy token art. Only when the walls show the sprite.
|
||||||
if ((s.webbed || s.dread) && s.hit) {
|
if (s.webbed && s.hit && seen) {
|
||||||
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;
|
const w = s.right - s.left, h = s.bottom - s.top;
|
||||||
|
const hubX = s.left + w * 0.5, hubY = s.top + h * 0.45;
|
||||||
|
const rim: [number, number][] = [
|
||||||
|
[0.02, 0.06], [0.5, 0.0], [0.98, 0.08], [1.0, 0.5],
|
||||||
|
[0.96, 0.94], [0.5, 1.0], [0.04, 0.92], [0.0, 0.5],
|
||||||
|
].map(([fx, fy]) => [s.left + fx * w, s.top + fy * h]);
|
||||||
|
const web = () => {
|
||||||
|
ctx.beginPath();
|
||||||
|
for (const [rx, ry] of rim) {
|
||||||
|
ctx.moveTo(hubX, hubY);
|
||||||
|
ctx.lineTo(rx, ry);
|
||||||
|
}
|
||||||
|
for (const t of [0.45, 0.75]) {
|
||||||
|
for (let i = 0; i <= rim.length; i++) {
|
||||||
|
const [rx, ry] = rim[i % rim.length]!;
|
||||||
|
const px = hubX + (rx - hubX) * t, py = hubY + (ry - hubY) * t + h * 0.02;
|
||||||
|
if (i === 0) ctx.moveTo(px, py);
|
||||||
|
else ctx.lineTo(px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
ctx.save();
|
ctx.save();
|
||||||
if (s.webbed) {
|
ctx.strokeStyle = "rgba(25,25,30,0.45)";
|
||||||
ctx.strokeStyle = "rgba(240,240,235,0.55)";
|
ctx.lineWidth = 2.5;
|
||||||
|
web();
|
||||||
|
ctx.strokeStyle = "rgba(245,245,240,0.8)";
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath();
|
web();
|
||||||
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();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Warp mouths wear their veil: the warp texture at the opening,
|
// Warp mouths wear their veil: the warp texture at the opening,
|
||||||
// translucent, swirling — the doorway between rooms that are not
|
// translucent, swirling — the doorway between rooms that are not
|
||||||
|
|||||||
Reference in New Issue
Block a user