The terrain moves in: pits open downward, hedges stand waist-high, ooze is jello
The flat token billboards give way to terrain that inhabits the room. Pits, slime, tacks, and the dimensional warp's rings are painted INTO the flagstones — the home-tile decal pass grows into a general per-cell overlay grid, one texture per cell, alpha-blended texel by texel. Thorn and rose hedges fill their square wall to wall at half height, drawn with a near-bias so the wizard standing among the thorns peeks over the top. The killer ooze rises as a full translucent cube of jello, bricks and bodies visible through it; dust billows; and each dimensional warp mouth throws an additive pillar of violet light up from its floor ring. Billboards learn aspect, translucency, glow, and draw-order bias to carry it all. Nine paintable starters ship — four floor decals under textures/, five standing volumes under terrain3d/ (spec updated for the artist, hedges wide, jello solid, warpglow as light) — shown in the token workshop, rehearsable in place with /?fpv&terrain=thornbush@3,7;jello@4,7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a8a63cc27b
commit
1e79f2ffb2
@@ -0,0 +1,87 @@
|
||||
// Terrain that stands in the room rather than lying on its floor: bushes
|
||||
// filling a square to half height, the killer ooze as a translucent cube
|
||||
// of jello, dust billowing, dimensional warps throwing light. Each is a
|
||||
// paintable sprite at public/terrain3d/<name>.png (transparent PNG, any
|
||||
// size); the bakes below stand in until painted.
|
||||
|
||||
export const TERRAIN3D = ["thornbush", "rosebush", "jello", "dust", "warpglow"] as const;
|
||||
|
||||
const baked = new Map<string, HTMLCanvasElement>();
|
||||
export function terrainFallback(name: string): HTMLCanvasElement {
|
||||
let t = baked.get(name);
|
||||
if (t) return t;
|
||||
t = document.createElement("canvas");
|
||||
t.width = 128;
|
||||
t.height = 64;
|
||||
const c = t.getContext("2d")!;
|
||||
const h2 = (a: number, b: number) => {
|
||||
const n = Math.sin(a * 127.1 + b * 311.7) * 43758.5453;
|
||||
return n - Math.floor(n);
|
||||
};
|
||||
if (name === "thornbush" || name === "rosebush") {
|
||||
// A hedge of tangles wall to wall; roses get their blooms.
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const x = h2(i, 1) * 128, y = 16 + h2(i, 3) * 46, r = 6 + h2(i, 5) * 9;
|
||||
const g = 60 + (h2(i, 7) * 50 | 0);
|
||||
c.fillStyle = `rgba(${g * 0.35 | 0},${g},${g * 0.3 | 0},0.9)`;
|
||||
c.beginPath();
|
||||
c.arc(x, y, r, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
}
|
||||
c.strokeStyle = "rgba(30,40,18,0.7)";
|
||||
for (let i = 0; i < 14; i++) {
|
||||
c.beginPath();
|
||||
c.moveTo(h2(i, 11) * 128, 18 + h2(i, 13) * 40);
|
||||
c.lineTo(h2(i, 11) * 128 + 10 - h2(i, 17) * 20, 8 + h2(i, 19) * 40);
|
||||
c.stroke();
|
||||
}
|
||||
if (name === "rosebush") {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
c.fillStyle = "rgba(210,40,70,0.95)";
|
||||
c.beginPath();
|
||||
c.arc(6 + h2(i, 23) * 116, 14 + h2(i, 29) * 40, 3.5, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
}
|
||||
}
|
||||
} else if (name === "jello") {
|
||||
// The gelatinous whole of the killer ooze, drawn as one cube face
|
||||
// with a paler top edge — the renderer's alpha keeps it see-through.
|
||||
t.height = 128;
|
||||
const g2 = t.getContext("2d")!;
|
||||
g2.fillStyle = "rgba(90,190,70,0.85)";
|
||||
g2.fillRect(4, 12, 120, 112);
|
||||
g2.fillStyle = "rgba(170,240,140,0.9)";
|
||||
g2.fillRect(4, 12, 120, 10);
|
||||
g2.fillStyle = "rgba(230,255,210,0.5)";
|
||||
g2.fillRect(16, 30, 22, 12);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
g2.fillStyle = "rgba(40,120,30,0.5)";
|
||||
g2.beginPath();
|
||||
g2.arc(16 + h2(i, 31) * 96, 40 + h2(i, 37) * 72, 3 + h2(i, 41) * 3, 0, Math.PI * 2);
|
||||
g2.fill();
|
||||
}
|
||||
} else if (name === "dust") {
|
||||
t.height = 128;
|
||||
const g2 = t.getContext("2d")!;
|
||||
for (let i = 0; i < 26; i++) {
|
||||
const a = 0.12 + h2(i, 43) * 0.16;
|
||||
g2.fillStyle = `rgba(200,190,170,${a})`;
|
||||
g2.beginPath();
|
||||
g2.arc(14 + h2(i, 47) * 100, 14 + h2(i, 53) * 100, 12 + h2(i, 59) * 16, 0, Math.PI * 2);
|
||||
g2.fill();
|
||||
}
|
||||
} else {
|
||||
// warpglow: a pillar of violet light, brightest at its core.
|
||||
t.width = 64;
|
||||
t.height = 128;
|
||||
const g2 = t.getContext("2d")!;
|
||||
const grad = g2.createLinearGradient(0, 0, 64, 0);
|
||||
grad.addColorStop(0, "rgba(140,80,220,0)");
|
||||
grad.addColorStop(0.5, "rgba(200,160,255,0.75)");
|
||||
grad.addColorStop(1, "rgba(140,80,220,0)");
|
||||
g2.fillStyle = grad;
|
||||
g2.fillRect(0, 0, 64, 128);
|
||||
}
|
||||
baked.set(name, t);
|
||||
return t;
|
||||
}
|
||||
Reference in New Issue
Block a user