PURPOSE
Process-global active palette state. Provides the single source of truth for what palette is currently in use, so any renderer can resolve a slot to a hex color without holding its own copy. The palette is selected once per mission (typically at mission start, often via biome) and remains fixed for the mission’s duration.
OWNS
_active: Palette— module-private active palette object, initialized to theperiwinkledefault preset._revision: number— monotonic counter, incremented every time the active palette actually changes.- The
DEFAULT_PALETTE_IDconstant ('periwinkle') — Nate’s locked-in baseline mood (2026-04-22), a cool lavender-blue pastel chosen to read well across every biome.
READS FROM
./palette-typesfor thePaletteandPaletteSlottypes../palette-presetsforgetPaletteByPresetId(preset lookup) andpickPresetForBiome(seeded biome-to-preset selection).
PUSHES TO
Nothing directly. Consumers pull from this module on their own cadence:
- Any renderer or VFX call site invokes
resolvePaletteSlot(slot)per frame. - Caches keyed by
(slot, paletteRevision)— baked silhouettes, baked fbm noise, and similar precomputed surfaces — readgetPaletteRevision()to detect staleness and rebuild.
DOES NOT
- Does not change the palette mid-mission on its own; callers drive that.
- Does not mutate the palette object it returns;
getActivePalette()is read-only by convention. - Does not allocate on the hot lookup path;
resolvePaletteSlotis a direct object index. - Does not no-op silently on a non-change setter call beyond skipping the revision bump — same-id reassignment short-circuits before swap and before the counter increments.
- Does not persist palette state across processes; the module starts fresh at the default on every load.
- Does not broadcast change events; revision polling is the only stale-detection mechanism.
Signals
_paletteRevision— bumped only when_active.iddiffers from the incoming preset id, so caches don’t invalidate on a redundantsetcall.
Entry points
setActivePaletteByPresetId(id: string): void— swap to a specific preset id; no-op if already active.setActivePaletteForBiome(biomeId: string, seed?: number): void— pick a preset variant for a biome using the optional seed, then swap to it.resolvePaletteSlot(slot: PaletteSlot): string— hot-path slot-to-hex lookup against the active palette.getActivePalette(): Palette— return the active palette object (read-only).getPaletteRevision(): number— return the monotonic change counter for cache-staleness checks.
Pattern notes
- Module-level mutable singleton: state lives in two file-scoped
letbindings (_active,_revision) rather than a class or context object. Imports give every consumer the same view automatically. - Pull-based propagation: there is no subscriber list, no event emit, no observer registration. Consumers either re-resolve every frame (cheap lookups) or compare against a stored revision number (expensive caches).
- Revision-keyed caching contract: any cache that bakes palette colors into a texture, mesh, or noise field must store the revision it was built against and rebuild when
getPaletteRevision()advances. - Identity-based change detection: setter compares preset ids, not deep palette content. Two different presets with identical colors would still be treated as a change; identical ids skip the swap entirely.
- Default-on-load policy: the active palette is always valid from import time — no null state, no init call required before
resolvePaletteSlotcan be used. - Mission-scoped lifetime intent: the documented contract is “picked once at mission start and stays put,” even though the API does not enforce this technically.