--- title: "Entity Color System — Visual Specification" description: "Canonical palette and rendering rules for relationship-based entity colors derived from D-033" type: design status: active ticket: "#304" decision_refs: [D-033, D-043, D-044, D-048, D-049, D-059, D-060] author: "Araminta" created: 2026-02-20 updated: 2026-02-20 --- # Entity Color System — Visual Specification **Version:** v0.1 (Sprint 14) **Author:** Araminta (Visual Designer) **Date:** 2026-02-20 **Ticket:** #304 **Status:** Active — input to client implementation (future sprint), constrains #318 (THE FRIEND visual treatment) **Foundation:** [Visual Grammar v0.1](visual-grammar-v01.md) (§3), Decision D-033 --- ## 1. Core Principle Entity color encodes the **player character's subjective relationship** to an NPC — not an objective property of the NPC. The same entity can appear as different colors to the detective and the smuggler simultaneously. Color is a **client-side derivation** from the `RelationshipState` field on each `VisibleEntity` in the server's `ObserverSnapshot`. The server never says "this NPC is hostile" — it sends the relationship state the client derives the color from. This is asymmetric information rendered visually. It is the most important single system in the game to get right because it is the visual language through which the player reads the world. --- ## 2. Relationship → Color Mapping ### 2.1 Canonical Palette | RelationshipState | Color name | Hex | Visual quality | Notes | |-------------------|-----------|-----|----------------|-------| | `Unknown` | Cool teal | `#4a9ebb` | Default for unassessed entities. Cooler than sky, not clinical. | Every new NPC the player hasn't built a view of | | `Known` / `Friendly` | Soft green | `#6bc9a6` | Trusted. Known person. Not safe — just trusted. | People the character knows and has reason to trust | | `PersonOfInterest` | Warm amber | `#e8c547` | Monologue or case file has flagged something. | Not necessarily hostile — flagged | | `Hostile` | Muted red | `#d45d5d` | Player character perceives subjective danger. Not omniscient. | **Red means danger TO YOUR CHARACTER**, not danger in the abstract | ### 2.2 Non-Entity Colors | Entity type | Color name | Hex | Notes | |-------------|-----------|-----|-------| | Static objects | Muted grey | `#8b8ba0` | See §5 for what counts as static | | Player — Detective | Cool blue-white | `#e0e8ff` | Near-neutral. "Self." No relationship loading. | | Player — Smuggler | Warm cream | `#e8e0d0` | Near-neutral. "Self." Slightly warmer than detective. | ### 2.3 Color Psychology Notes The palette was selected for functional warmth (D-043), not conventional danger-coding. Teal (`#4a9ebb`) reads as neutral-curious rather than cold. Green (`#6bc9a6`) reads as familiar rather than "good." Amber (`#e8c547`) reads as noteworthy rather than "warning." Red (`#d45d5d`) is muted — subjective danger, not objective alarm. This matters because the detective might see amber where the smuggler sees green for the same NPC. Both are correct. Neither color is lying — they are rendering different epistemic positions. --- ## 3. Runtime Derivation ### 3.1 Data Source The server sends each `VisibleEntity` in the `ObserverSnapshot` with a `relationship: RelationshipState` field (`server/src/bridge/types.rs`, `VisibleEntity` struct, line 258). This field is computed per-observer on the server using the observer's knowledge graph — it is NOT a shared NPC property. `RelationshipState` variants: - `Unknown` — no assessment, or newly visible - `Known` — character has a relationship but nothing flagged - `PersonOfInterest` — knowledge graph or case file has flagged this entity - `Hostile` — character perceives active danger from this entity ### 3.2 Client Lookup `entity_renderer.gd` maps `RelationshipState` to color via `Constants.color_for_entity_kind()`. The current implementation (`_color_for_kind`, line 181) delegates to this function. The full color lookup should follow: ``` RelationshipState → Color Unknown → #4a9ebb Known / Friendly → #6bc9a6 PersonOfInterest → #e8c547 Hostile → #d45d5d Static object → #8b8ba0 Player entity → #e0e8ff (detective) or #e8e0d0 (smuggler) ``` The player's own entity is identified via `GameState.player_entity_id`. Player color does not participate in the relationship lookup — it is a fixed constant per character selection. --- ## 4. Transition Behavior ### 4.1 Standard Transition When an entity's `RelationshipState` changes between snapshots, the color shift is a **0.5-second smooth fade** (linear lerp). This is already implemented in `entity_renderer.gd` via the `_entity_tweens` dictionary and `COLOR_FADE_DURATION = 0.5` constant. **Never use an instant color swap.** The visual transition is part of the information delivery — the player reads the relationship changing as a small dramatic moment. ### 4.2 THE FRIEND's First Shift — Staged Priority THE FRIEND's transition from `Known/Friendly` (green `#6bc9a6`) to `PersonOfInterest` (amber `#e8c547`) must be the **first relationship color change** the player observes in the session. The opening 20–25 minutes of gameplay must be staged so no other NPC's relationship state changes before THE FRIEND's shift. This requires narrative coordination: the player must have enough time with THE FRIEND as green to internalize what green means. When amber arrives, the player has a reference point. Without that contrast, the color change means nothing. See `docs/design/the-friend-visual-treatment.md` (#318) for staging details. ### 4.3 Edge Cases **Entity in fog (unrecognized):** D-033 colors do **not** show through fog for unrecognized entities. An unrecognized entity in fog renders as a neutral grey `#555566` blob with no silhouette features. Once the cognitive delay resolves (D-060, 0.6s base) and recognition completes, the blob transitions to the entity's D-033 color + identifying silhouette feature. **Entity in fog (recognized):** If the character recognizes an entity in fog (their knowledge graph identifies them), the insert overlay (z-layer 6) can show a D-033 color glow + faint silhouette feature at ±0.5 tile approximate position. This is insert data, not visual data — the character knows where they were, not where they are. **Entity at periphery:** Per D-015 (forward/peripheral/blind vision sectors), entities in the peripheral vision zone are rendered at reduced saturation and alpha. The `modulate.a` value is `Constants.PERIPHERAL_ALPHA` when `visibility == "Peripheral"`. This is already implemented in `entity_renderer.gd`. The D-033 color itself does not change — saturation reduction is handled via the visibility dimming, not a color override. **Simultaneous transitions:** Two entities changing relationship state in the same tick each get independent 0.5s tweens. There is no synchronization between entity transitions. This is intentional. --- ## 5. Static Objects — Definition and Color ### 5.1 What is a Static Object Static objects are non-person entities that do not have a relationship to the player and are not part of the NPC simulation. They always render at `#8b8ba0` regardless of any game state. **Static objects (always `#8b8ba0`):** - Furniture: chairs, tables, desks, counters, crates - Fixtures: terminals, consoles, lockers, shelving - Environmental: doors (in closed state), hatches, panels - Props: datapads, mugs, comms units, cargo containers **Not static objects (use D-033 colors):** - All NPCs including Background-tier - Player characters - Carried items (if items become carriable entities, they follow their carrier's relationship color — this is a future sprint consideration, default to static color in v0.1) ### 5.2 Why Grey, Not Zone Palette Static objects use `#8b8ba0` rather than blending into zone floor/wall colors because they need to be visually distinct from the environment while remaining subordinate to entity colors. Grey sits between structure (very low saturation zone palettes) and entities (moderate-to-high saturation D-033 colors) in the saturation hierarchy. **Saturation hierarchy (D-044):** | Tier | Saturation range | Examples | |------|-----------------|---------| | Entity (D-033) | 40–60% | `#4a9ebb`, `#6bc9a6`, `#e8c547`, `#d45d5d` | | Objects (static + D-052 favorites) | 10–30% | `#8b8ba0`, dusty blue, warm terracotta | | Structure (zone palette) | 5–15% | Zone floor tiles, wall faces | Never add an object color that approaches entity saturation levels. --- ## 6. Color Blindness Assessment ### 6.1 Palette Under Common Deficiencies The D-033 palette (`#4a9ebb` teal / `#6bc9a6` green / `#e8c547` amber / `#d45d5d` red) relies on hue differentiation as its primary signal. This creates accessibility challenges. **Deuteranopia (green-blind, ~5% of males):** The green (`#6bc9a6`) and teal (`#4a9ebb`) may be difficult to distinguish. Both occupy the blue-green range. Under deuteranopia simulation, they compress toward a similar cool-blue appearance. The amber (`#e8c547`) and red (`#d45d5d`) differentiate better — amber reads as yellow, red reads as brownish-orange. **Protanopia (red-blind, ~1% of males):** The red (`#d45d5d`) shifts toward olive/brown. More concerning: the red and green (`#6bc9a6`) may become difficult to distinguish. The amber (`#e8c547`) remains clearly distinct. **Tritanopia (blue-blind, rare):** The teal (`#4a9ebb`) shifts toward green, potentially conflating Unknown and Known. Less critical as tritanopia is uncommon. ### 6.2 Risk Assessment **High risk:** Deuteranopia — teal/green confusion. A deuteranopic player may not clearly distinguish Unknown from Known/Friendly NPCs. **Moderate risk:** Protanopia — red/green confusion. A protanopic player may not clearly distinguish Hostile from Known/Friendly. ### 6.3 Mitigation Recommendation The current palette does not include a secondary differentiation signal beyond hue. For v0.1 (functional placeholder stage with colored rectangles), this is acceptable — the game ships with boxes, not full art, and accessibility features are milestone work. **Recommended for v0.1.2+:** Add a shape or pattern secondary signal to entity sprites — border dash pattern or icon badge — that persists independently of hue. E.g., Hostile gets a diamond border, PersonOfInterest gets a cross-hatch border, Unknown gets no border treatment. This would not require changing the D-033 colors (which have been visually designed and approved) but would layer a non-color signal on top. **Flag:** This is a known gap to be addressed before the game exits early access. Tracked as future accessibility ticket (not yet created — flag during Sprint 15 planning). --- ## 7. Insert Overlay Interaction (D-048) In the insert overlay (z-layer 6), entity D-033 colors gain **soft halos**: 2–3px gaussian blur at ~40% blend. This is the insert's interpretation of the relationship data — rendered with organic neural texture. In the natural vision layer (z-layer 3), entity colors are rendered with **2px outline in the relationship color** — hard edge, no bloom. This is the player's naked visual perception. The difference: the insert annotates, the eye sees. Both use the same color, different rendering treatment. When `insert_active == false`, the bloom halos disappear. The underlying hard-edge entity color remains (the character still sees the NPC). Only the insert's annotation layer suppresses. --- ## 8. Implementation Notes for Stig The core lookup table is already partially implemented. The complete client-side mapping lives in `client/scripts/constants.gd` in the `color_for_entity_kind()` function. The entity renderer at `client/scripts/rendering/entity_renderer.gd` already: - Tracks relationship state per entity in `_entity_relationships` - Detects state changes and starts 0.5s color tweens - Handles peripheral visibility dimming independently **Outstanding for future implementation sprint:** 1. Ensure `Constants.color_for_entity_kind()` uses all five states above (including the player entity case keyed to `GameState.player_entity_id` and `lattice_profile`). 2. Fog-entity rendering (recognized vs unrecognized) is handled by the fog system, not `entity_renderer.gd`. The fog renderer queries the knowledge graph for recognition state. 3. Peripheral saturation reduction: currently implemented as alpha reduction (`modulate.a`). This is sufficient for v0.1. True saturation reduction (keeping brightness, reducing colorfulness) would require a shader and is deferred. --- ## Appendix A — Decision Cross-References | Decision | Relevance | |----------|-----------| | D-033 | Source of truth for relationship state → color mapping. Hex values in this spec are canonical per D-033 approval. | | D-043 | "Functional warmth" art direction. Palette tone and production principle. | | D-044 | Visual hierarchy (entity > object > structure). Saturation rules. | | D-048 | Neural insert overlay — bloom treatment for D-033 colors on z-layer 6. | | D-049 | Z-level rendering stack. Entities at z-layer 3. Insert at z-layer 6. | | D-059 | Fog shader — recognized vs unrecognized entity treatment in fog. | | D-060 | Cognitive delay — controls when grey fog blob transitions to D-033 color. | ## Appendix B — Quick Reference for Stig | State | Hex | Duration of transition | |-------|-----|----------------------| | Unknown | `#4a9ebb` | 0.5s fade from prior color | | Known/Friendly | `#6bc9a6` | 0.5s fade from prior color | | PersonOfInterest | `#e8c547` | 0.5s fade from prior color | | Hostile | `#d45d5d` | 0.5s fade from prior color | | Static object | `#8b8ba0` | Fixed — no transitions | | Player (detective) | `#e0e8ff` | Fixed — not subject to relationship | | Player (smuggler) | `#e8e0d0` | Fixed — not subject to relationship | | Fog blob (unrecognized) | `#555566` | Transitions to D-033 over 0.3s during 0.6s cognitive delay (D-060) | | Peripheral entities | Any D-033 color at reduced alpha | No separate color |