fuzzyworld

explodorama / files / src / render / decor.ts

decor.ts

Blame: the commit that last changed each line · file · history

5f066960 2026-10-26 Pavel Safronov1import { drawTile, rgb, vec2, type Color } from 'littlejsengine'
2import { stream } from '../sim/rng'
3import type { GameState, Vec } from '../sim/types'
4import { toWorld } from './board'
5import { SPLATS, sprite } from './sprites'
6
7/**
8 * A worn dungeon (user request, 2026-10-25): stains on the floor and the walls from Kenney's Splat
9 * Pack — damp puddles, moss, dirt, dust, soot, rust — each level its own mix and amount, from the
10 * seed and level, so a level always looks the same and levels differ. Decoration only: not in the
11 * sim, drawn faint under everything, so it never reads as a hazard (oil is dark and glossy, PLAN.md
12 * §19.3). Heavier along the walls and in corners, as dirt gathers there.
13 */
14interface Stain {
15 at: Vec
16 /** Offset within the tile, size and turn, in tiles and radians. */
17 dx: number
18 dy: number
19 size: number
20 angle: number
21 splat: (typeof SPLATS)[number]
22 color: Color
23 /** On a wall (rust, moss on stone) rather than the floor. */
24 wall: boolean
25}
26
27/** The looks a level can mix: each a few colours (faint: alpha about 0.15–0.25). */
28const THEMES: Record<string, Color[]> = {
29 damp: [rgb(0.32, 0.42, 0.58, 0.22), rgb(0.4, 0.5, 0.62, 0.18)],
30 mossy: [rgb(0.3, 0.52, 0.25, 0.2), rgb(0.42, 0.55, 0.3, 0.16)],
31 dirty: [rgb(0.42, 0.32, 0.2, 0.2), rgb(0.55, 0.45, 0.3, 0.16)],
32 dusty: [rgb(0.72, 0.64, 0.48, 0.2), rgb(0.62, 0.58, 0.5, 0.16)],
33 scorched: [rgb(0.14, 0.13, 0.13, 0.2), rgb(0.25, 0.22, 0.2, 0.16)],
34 rusty: [rgb(0.62, 0.32, 0.14, 0.22), rgb(0.5, 0.28, 0.16, 0.18)],
35}
36const THEME_NAMES = Object.keys(THEMES)
37
38/** Off for now (user, 2026-10-25: the splats look too alike, and not the look wanted). */
39const ENABLED = false
40
41let cached: { key: string; stains: Stain[] } | null = null
42
43/** The level's stains, worked out once per level (and board size). */
44export function stainsOf(s: GameState): Stain[] {
45 const key = `${s.seed}:${s.level}:${s.width}x${s.height}`
46 if (cached?.key === key) return cached.stains
47 const rng = stream(s.seed, 'decor', s.level)
48 // Two looks per level, and how worn it is: from lightly to heavily.
49 const first = THEME_NAMES[rng.int(THEME_NAMES.length)]!
50 const second = THEME_NAMES[rng.int(THEME_NAMES.length)]!
51 const colors = [...THEMES[first]!, ...THEMES[second]!]
52 const wear = 0.12 + rng.next() * 0.25
53 const wall = (x: number, y: number) => x < 0 || y < 0 || x >= s.width || y >= s.height || s.cells[y * s.width + x] === 'wall'
54 const stains: Stain[] = []
55 for (let y = 0; y < s.height; y++) {
56 for (let x = 0; x < s.width; x++) {
57 const onWall = wall(x, y)
58 // Next to walls more stains gather; walls themselves get a few, smaller.
59 const walls = [wall(x - 1, y), wall(x + 1, y), wall(x, y - 1), wall(x, y + 1)].filter(Boolean).length
60 const chance = onWall ? wear * 0.35 : wear * (1 + walls * 0.6)
61 if (rng.next() >= chance) continue
62 stains.push({
63 at: { x, y },
64 dx: (rng.next() - 0.5) * 0.5,
65 dy: (rng.next() - 0.5) * 0.5,
66 size: onWall ? 0.35 + rng.next() * 0.35 : 0.5 + rng.next() * 0.8,
67 angle: rng.next() * Math.PI * 2,
68 splat: SPLATS[rng.int(SPLATS.length)]!,
69 color: colors[rng.int(colors.length)]!,
70 wall: onWall,
71 })
72 }
73 }
74 cached = { key, stains }
75 return stains
76}
77
78/** The floor's stains: after the floor, before the walls and everything on the board. */
79export function drawFloorStains(s: GameState): void {
80 if (!ENABLED) return
81 for (const st of stainsOf(s)) if (!st.wall) drawStain(s, st)
82}
83
84/** The walls' stains: over the walls, before anything that moves. */
85export function drawWallStains(s: GameState): void {
86 if (!ENABLED) return
87 for (const st of stainsOf(s)) if (st.wall) drawStain(s, st)
88}
89
90function drawStain(s: GameState, st: Stain): void {
91 drawTile(toWorld(s, st.at).add(vec2(st.dx, st.dy)), vec2(st.size), sprite(st.splat), st.color, st.angle)
92}