// Colour, keyed on what the mission is FOR. // // A benchmark run and a security sweep were rendered in the same palette as a // research mission, so the only thing distinguishing them on screen was the // label. Colour is the cheapest signal a viewer reads before any text, and it // was carrying no information. // // This also collapses two uncoordinated kind→colour maps that had drifted // apart: `LEVEL_COLOR` in engine.ts (node colour by tier) and the `fireColor` // if-chain in `onTouch` (beam colour by id prefix). They answered the same // question in different places with different answers. import type { Tier } from "./engine"; export type PhaseKind = "research" | "coding" | "benchmark" | "security_scan"; export interface Palette { /** The mission landmark at the centre. */ mission: string; /** Station colour per phase kind. */ phase: Record; dir: string; file: string; tool: string; finding: string; comm: { message: string; delegate: string; room: string; web: string }; } /** The default — what the World looked like before palettes, so an unrecognised * template renders exactly as it always did rather than as something odd. */ const BASE: Palette = { mission: "#ff8a7a", phase: { research: "#c98af0", coding: "#8ec5ff", benchmark: "#f0b866", security_scan: "#ff5f57", }, dir: "#5ec8d8", file: "#5ec8d8", tool: "#5ec8d8", finding: "#ff8a7a", comm: { message: "#ff5eae", delegate: "#ff8a7a", room: "#c98af0", web: "#5ec8d8" }, }; /** Measurement work reads amber/green — instrument tones, not creative ones. */ const BENCHMARK: Palette = { ...BASE, mission: "#f0b866", phase: { ...BASE.phase, benchmark: "#5fd08a", coding: "#e8b465" }, dir: "#a8b06a", file: "#d8c06a", tool: "#5fd08a", comm: { ...BASE.comm, web: "#a8b06a" }, }; /** Adversarial work reads crimson/steel. */ const SECURITY: Palette = { ...BASE, mission: "#ff5f57", phase: { ...BASE.phase, security_scan: "#ff5f57", coding: "#8a9af0" }, dir: "#7a8a9a", file: "#9aa8b8", tool: "#ff8a7a", finding: "#ff3b30", comm: { ...BASE.comm, web: "#7a8a9a" }, }; /** Reading and mapping an existing codebase — cooler, quieter. */ const REFACTOR: Palette = { ...BASE, mission: "#6fd0c0", phase: { ...BASE.phase, coding: "#6fd0c0" }, dir: "#4aa3b8", file: "#6fd0c0", comm: { ...BASE.comm, web: "#4aa3b8" }, }; /** Pure research — violet and sky, the existing landmark tones. */ const RESEARCH: Palette = { ...BASE, mission: "#c98af0", phase: { ...BASE.phase, research: "#c98af0" }, dir: "#8a9af0", file: "#8ec5ff", }; const PALETTES: Record = { research_only: RESEARCH, research_and_code: BASE, security_hardening: SECURITY, refactor: REFACTOR, benchmark: BENCHMARK, self_audit: RESEARCH, custom: BASE, }; export function paletteFor(templateKind?: string | null): Palette { return (templateKind && PALETTES[templateKind]) || BASE; } /** Node colour for an id + tier. Replaces `LEVEL_COLOR[tier]` for everything a * mission scope draws; structural tiers keep their own constants. */ export function colorFor(p: Palette, nodeId: string, tier: Tier): string | undefined { if (tier === "mission") return p.mission; if (nodeId.startsWith("finding:")) return p.finding; if (nodeId.startsWith("dir:")) return p.dir; if (nodeId.startsWith("file:")) return p.file; if (nodeId.startsWith("tool:")) return p.tool; // `phase` deliberately absent: a station's colour comes from its KIND, which // the node id does not carry. `applyMissionPlan` passes it explicitly. return undefined; } /** Beam colour when a pawn touches a node — the old `fireColor` if-chain. */ export function fireFor(p: Palette, nodeId: string, weight: number): string | undefined { if (nodeId.startsWith("file:")) return weight >= 0.9 ? p.file : p.tool; if (nodeId.startsWith("tool:")) return p.tool; if (nodeId.startsWith("phase:")) return p.mission; if (nodeId.startsWith("finding:")) return p.finding; return undefined; }