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
+45 -11
View File
@@ -227,6 +227,17 @@ export interface Billboard {
/** Lifted off the floor (0 = feet on the ground). */
rise: number;
label: string;
/** Width as a multiple of height (1 = square; 2 = a cell-wide hedge). */
aspect?: number;
/** Translucency (jello, dust); 1 or absent is opaque. */
alpha?: number;
/** Drawn additively, as light (warp pillars). */
glow?: boolean;
/** Pulled this much nearer for draw order: a hedge wins the tie
* against the wizard standing in its own square, who then peeks over. */
bias?: number;
/** Procedural stand-in name while the painted file loads. */
fallback?: string;
/** A reflection seen through a warp mouth, not the thing itself. */
warped?: boolean;
}
@@ -298,19 +309,42 @@ export function billboards(
scale: 0.4, rise: 0, label: "treasure",
});
}
// The floor's furniture: bushes stand tall, hazards squat low. Stone is
// a wall to the rays and needs no sprite.
const TERRAIN_SCALE: Record<string, number> = {
thornbush: 0.7, rosebush: 0.7, safe: 0.55, ooze: 0.35, slime: 0.3,
tacks: 0.25, pit: 0.35, dust: 0.6,
};
// The floor's furniture, standing in the room. Bushes fill the square
// to half height (a wizard peeks over); the killer ooze is a whole
// translucent cube of jello; dust billows. Pits, slime, tacks, and
// dimensional warps are painted INTO the floor by the decal pass, and
// stone is a wall to the rays — none of those need a sprite.
for (const [k, content] of Object.entries(view.squareContents)) {
if (content.kind === "stone") continue;
const file = TERRAIN_ART[content.kind];
const scale = TERRAIN_SCALE[content.kind];
if (!file || !scale) continue;
const [tx, ty] = k.split(",").map(Number) as [number, number];
out.push({ x: tx + 0.5, y: ty + 0.5, src: art(file, "terrain"), scale, rise: 0, label: content.kind });
const at = { x: tx + 0.5, y: ty + 0.5 };
if (content.kind === "thornbush" || content.kind === "rosebush") {
out.push({
...at, src: `/terrain3d/${content.kind}.png`, fallback: content.kind,
scale: 0.5, aspect: 2, rise: 0, bias: 0.18, label: content.kind,
});
} else if (content.kind === "ooze") {
out.push({
...at, src: "/terrain3d/jello.png", fallback: "jello",
scale: 0.96, aspect: 1.04, rise: 0, alpha: 0.6, bias: 0.18, label: "ooze",
});
} else if (content.kind === "dust") {
out.push({
...at, src: "/terrain3d/dust.png", fallback: "dust",
scale: 0.85, aspect: 1.1, rise: 0, alpha: 0.55, bias: 0.18, label: "dust",
});
} else if (content.kind === "safe") {
out.push({ ...at, src: art(TERRAIN_ART["safe"]!, "terrain"), scale: 0.55, rise: 0, label: "safe" });
}
}
// Dimensional warp mouths throw pillars of light from their floor rings.
for (const w of view.dimWarps) {
for (const cellAt of [w.a, w.b]) {
out.push({
x: cellAt.x + 0.5, y: cellAt.y + 0.5,
src: "/terrain3d/warpglow.png", fallback: "warpglow",
scale: 0.95, aspect: 0.5, rise: 0, alpha: 0.6, glow: true, label: "dimwarp",
});
}
}
for (const [k, cards] of Object.entries(view.groundObjects)) {
const file = cards[0] ? objectArt(cards[0].cardId) : null;