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);
|
||||
const iw = img instanceof HTMLImageElement ? img.naturalWidth : (img?.width ?? 0);
|
||||
const ih = img instanceof HTMLImageElement ? img.naturalHeight : (img?.height ?? 0);
|
||||
// A being standing on castable ground wears an ember ring while a
|
||||
// targeted card is armed: the cockpit's answer to the board's lit
|
||||
// squares, drawn behind the body so the art stays clean. Only a
|
||||
// sprite the walls actually show earns its ring.
|
||||
if (ontarget && aimBeings && litCells && s.hit && s.cell &&
|
||||
litCells.has(`${s.cell.x},${s.cell.y}`) &&
|
||||
!(s.hit.kind === "player" && s.hit.id === view.you)) {
|
||||
// Rings live on the floor, behind the body, so the art stays clean:
|
||||
// FEAR's dread ring smoulders at the feet, and a being standing on
|
||||
// castable ground wears the ember ring while a targeted card is
|
||||
// armed — the cockpit's answer to the board's lit squares. Only a
|
||||
// sprite the walls actually show earns its marks.
|
||||
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 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 cx = (s.left + s.right) / 2;
|
||||
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.lineWidth = 2;
|
||||
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.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
// Painted art composites normally (inks stay true); light glows
|
||||
// additively. Either way translucency applies.
|
||||
if (s.glow) ctx.globalCompositeOperation = "lighter";
|
||||
@@ -546,45 +559,42 @@ import type { GameView } from "@wizwar/engine";
|
||||
if (s.gaze) ctx.filter = "none";
|
||||
if (s.glow) ctx.globalCompositeOperation = "source-over";
|
||||
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) {
|
||||
// STICKY WAND's webs cling over the caught body: spokes from a hub
|
||||
// and two sagging rings, each strand shadowed so the silk reads
|
||||
// against busy token art. Only when the walls show the sprite.
|
||||
if (s.webbed && s.hit && seen) {
|
||||
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();
|
||||
if (s.webbed) {
|
||||
ctx.strokeStyle = "rgba(240,240,235,0.55)";
|
||||
ctx.strokeStyle = "rgba(25,25,30,0.45)";
|
||||
ctx.lineWidth = 2.5;
|
||||
web();
|
||||
ctx.strokeStyle = "rgba(245,245,240,0.8)";
|
||||
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();
|
||||
}
|
||||
web();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warp mouths wear their veil: the warp texture at the opening,
|
||||
// translucent, swirling — the doorway between rooms that are not
|
||||
|
||||
Reference in New Issue
Block a user