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 the periwinkle default preset.
  • _revision: number — monotonic counter, incremented every time the active palette actually changes.
  • The DEFAULT_PALETTE_ID constant ('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-types for the Palette and PaletteSlot types.
  • ./palette-presets for getPaletteByPresetId (preset lookup) and pickPresetForBiome (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 — read getPaletteRevision() 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; resolvePaletteSlot is 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.id differs from the incoming preset id, so caches don’t invalidate on a redundant set call.

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 let bindings (_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 resolvePaletteSlot can 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.