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:
Eric Wagoner
2026-08-23 18:25:01 -04:00
co-authored by Claude Fable 5
parent a8a63cc27b
commit 1e79f2ffb2
16 changed files with 296 additions and 43 deletions
+62 -1
View File
@@ -42,7 +42,7 @@ function brickTexture(base: [number, number, number], rows: number, mortar: stri
for (let r = 0; r <= rows; r++) c.fillRect(0, r * rh - 1, TEX, 2);
});
}
export const MATERIALS = ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling", "home"] as const;
export const MATERIALS = ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling", "home", "pit", "slime", "tacks", "dimwarp"] as const;
/** The built-in bake: what ships when no painted file overrides it. */
export function proceduralTextures(): Record<string, HTMLCanvasElement> {
@@ -127,6 +127,67 @@ export function proceduralTextures(): Record<string, HTMLCanvasElement> {
c.arc(TEX / 2, TEX / 2, 7, 0, Math.PI * 2);
c.fill();
}),
pit: bake((c) => {
// A hole in the world: black heart, ragged broken-flag edge, a rim
// highlight on the lit side selling the depth.
c.fillStyle = "rgba(0,0,0,0.92)";
c.beginPath();
c.ellipse(TEX / 2, TEX / 2, TEX * 0.38, TEX * 0.34, 0, 0, Math.PI * 2);
c.fill();
c.strokeStyle = "rgba(20,16,10,0.9)";
c.lineWidth = 4;
c.stroke();
c.strokeStyle = "rgba(180,168,140,0.5)";
c.lineWidth = 2;
c.beginPath();
c.ellipse(TEX / 2, TEX / 2 - 2, TEX * 0.38, TEX * 0.34, 0, Math.PI * 1.1, Math.PI * 1.9);
c.stroke();
}),
slime: bake((c) => {
// A spill of green across the flags, glistening.
c.fillStyle = "rgba(70,140,40,0.55)";
c.beginPath();
c.ellipse(TEX * 0.5, TEX * 0.52, TEX * 0.4, TEX * 0.32, 0.4, 0, Math.PI * 2);
c.fill();
c.fillStyle = "rgba(110,190,60,0.5)";
for (let i = 0; i < 6; i++) {
c.beginPath();
c.ellipse(10 + h2(i, 2) * 44, 12 + h2(i, 5) * 40, 4 + h2(i, 7) * 5, 3 + h2(i, 9) * 4, 0, 0, Math.PI * 2);
c.fill();
}
c.fillStyle = "rgba(220,255,200,0.35)";
c.fillRect(TEX * 0.32, TEX * 0.4, 5, 2);
}),
tacks: bake((c) => {
// A scatter of little iron cruelties.
for (let i = 0; i < 14; i++) {
const x = 8 + h2(i, 3) * 48, y = 8 + h2(i, 6) * 48;
c.fillStyle = "rgba(70,70,78,0.9)";
c.beginPath();
c.moveTo(x, y - 3);
c.lineTo(x + 3, y + 2);
c.lineTo(x - 3, y + 2);
c.closePath();
c.fill();
c.fillStyle = "rgba(190,190,200,0.8)";
c.fillRect(x - 1, y - 4, 2, 2);
}
}),
dimwarp: bake((c) => {
// The dimensional warp's floor mouth: violet rings drawing inward.
for (let i = 5; i >= 0; i--) {
const a = 0.16 + i * 0.1;
c.strokeStyle = `rgba(${140 + i * 12},${80 + i * 14},${220},${a})`;
c.lineWidth = 3;
c.beginPath();
c.arc(TEX / 2, TEX / 2, 5 + i * 5, 0, Math.PI * 2);
c.stroke();
}
c.fillStyle = "rgba(240,225,255,0.8)";
c.beginPath();
c.arc(TEX / 2, TEX / 2, 3.5, 0, Math.PI * 2);
c.fill();
}),
warp: bake((c) => {
const grad = c.createLinearGradient(0, 0, TEX, TEX);
grad.addColorStop(0, "#2c1a4e");