diff --git a/decisions/README.md b/decisions/README.md index 3dc26335d..dd3b84ad3 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -10,7 +10,7 @@ Cross-domain decisions live in one file with cross-reference notes in related fi | File | Domain | Decisions | |------|--------|-----------| -| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109 | +| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109, D-113 | | [perception.md](perception.md) | Player observation | D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052, D-056, D-057, D-058, D-059, D-060, D-061, D-067, D-069, D-070, D-071, D-072, D-076, D-077, D-078, D-086 | | [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074, D-075, D-084, D-090, D-092, D-093, D-095, D-098, D-104, D-105, D-107 | | [scope.md](scope.md) | Game concept, prototype | D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065, D-087, D-089, D-091 | diff --git a/decisions/architecture.md b/decisions/architecture.md index 2bcb0f3e1..bd8db264e 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -374,4 +374,68 @@ Technical foundation decisions that constrain implementation: engine, client-ser --- -*32 decisions. Last updated: 2026-02-28 (D-108 amended Sprint 22 — Idle state as stationary installation primitive note added, D-111 cross-reference added)* +### D-113: Tile data model — extensible per-tile properties +- **Date:** 2026-03-05 +- **Decision:** Replace the current single-character tile encoding (`F/W/V/R` strings in location YAML) with a **tile palette/registry** system (option A from the design space). Tiles are typed by a palette ID; per-type properties are defined once in the palette and inherited by all tiles of that type. Per-tile overrides are supported via a sparse overlay map. +- **Current state:** Tiles are single characters in string arrays. Each character maps to a `TileKind` enum (`Floor`, `Wall`, `Door`, `Object`) and a walkability bool. `TileCell` in `WalkabilityMap` stores `{ walkable: bool, kind: TileKind }`. No per-tile properties (material, visual variant, sound, access lists, container contents, damage state, trigger zones) can be expressed. +- **Design survey — what systems need tile-level data:** + 1. **Doors** — access lists (who can open), open/closed state, locked/unlocked. Currently no tile-level door data; `TileKind::Door` exists but carries no properties. + 2. **Containers** — contents, capacity, searched state. Currently handled by entity `ObjectType::Container` on separate entities, not tiles. Containers should remain entities, not tile properties. + 3. **Damage state** — `DamageOverlay` (D-100) modifies tiles post-generation. Damage needs to degrade tile properties (walkability, visual, material) without replacing the base tile type. + 4. **Visual variants** — same logical tile type (e.g., "industrial floor") with per-tile visual variation for visual richness. Currently impossible — all Floor tiles look identical to the client. + 5. **Trigger zones** — tile-level triggers for entry/exit events (zone transitions, alarms, dialogue triggers). Currently handled by `ZoneMap` at zone granularity, not per-tile. + 6. **Material properties** — footstep sound, movement speed modifier, surface type for particle effects. Currently all tiles produce the same footstep sound. + 7. **WallBackside** (D-099) — structural classification behind wall surfaces. Already defined as an enum but not yet integrated into tile data. +- **Chosen approach — Tile Palette + Sparse Override:** + - **Tile palette** (YAML, per-district or global): defines tile types by string ID. Each type specifies: `walkable: bool`, `kind: TileKind`, `material: String` (footstep/SFX), `visual_base: String` (client sprite), `visual_variants: u8` (random variant count), `los_blocking: bool`, `movement_cost: f32` (default 1.0), optional `wall_backside: WallBackside` (D-099). The palette is the type-level contract — most tiles need no per-instance data beyond their palette ID. + - **Tile map** (YAML): retains the string-array format for human readability, but each character is a palette key (single char or short code). Backward-compatible: `F`, `W`, `V`, `R` are reserved palette keys that map to current behavior. New tile types use additional characters or a separate palette layer. + - **Sparse override map** (YAML): `overrides` key on Location — a list of `{ x, y, properties }` entries for tiles that differ from their palette type. Supports: door access lists, initial locked state, visual variant pinning, damage overlay data. Only tiles with non-default properties need entries. Keeps the string map clean for 90%+ of tiles. + - **Runtime representation:** + - `TilePalette` resource: `BTreeMap` loaded at startup. Immutable after load. + - `TileCell` extended: `{ palette_id: char, walkable: bool, kind: TileKind, material_id: u16 }`. Material ID is a compact index into the palette's material table. + - `TileOverrideMap` resource: `BTreeMap<(i32, i32, i32), TileOverride>` for per-tile overrides. Sparse — only tiles with overrides consume memory. + - ECS queries: `WalkabilityMap` remains the primary interface for movement/pathfinding (unchanged API). `TilePalette` provides material/visual data when needed (snapshot construction, sound system). `TileOverrideMap` provides door state, access lists, damage overlays. +- **YAML authoring format:** + ```yaml + # Palette definition (loaded once, reusable across locations) + palette: + F: { walkable: true, kind: Floor, material: metal-grate, visual_base: floor_industrial } + W: { walkable: false, kind: Wall, material: bulkhead, visual_base: wall_heavy, los_blocking: true } + D: { walkable: true, kind: Door, material: metal-door, visual_base: door_standard } + G: { walkable: true, kind: Floor, material: glass-panel, visual_base: floor_glass } + R: { walkable: false, kind: Floor, material: metal-grate, visual_base: floor_restricted } + + # Location tile map (unchanged human-readable format) + tiles: + - "WWWWWWWWWWWWWW" + - "WFFFFDFFFFFFFW" + - "WFFFFFFFFFFGFW" + - "WWWWWWWWWWWWWW" + + # Per-tile overrides (sparse, only for non-default properties) + overrides: + - { x: 5, y: 1, door_access: [faction.commission], locked: true } + - { x: 12, y: 2, visual_variant: 3 } + ``` +- **Loader contract:** `ContentPlugin` loads palette YAML first, then location tiles. The `apply_location_tiles()` function resolves each character via palette lookup instead of the current hardcoded match. Unknown characters fall back to `Floor` with a warning (same as current behavior). Overrides are loaded after tiles and applied to `TileOverrideMap`. +- **Migration effort for existing locations (5 files):** + - **Zero-migration path:** The default palette defines `F/W/V/R` with identical behavior to current hardcoded mapping. Existing location YAMLs work unchanged. No migration required for v0.1. + - **Incremental enrichment:** Locations can opt into the new palette by adding a `palette:` key. Locations without `palette:` use the global default. Migration is per-location, at author pace. + - **Estimated effort:** Palette definition = 0.5 day. Loader refactor = 1-2 days. Override system = 1 day. Total: 2-4 developer-days. No changes to location YAML files required for v0.1. +- **Alternatives considered:** + - **(b) Per-tile property bags** (arbitrary key-value per tile): Maximum flexibility but violates D-010 principle 4 (deterministic — dynamic typing makes serialization non-deterministic). Memory cost: ~100 bytes/tile vs ~6 bytes/tile with palette. Rejected. + - **(c) ECS-style tile components** (tiles as entities): Each tile becomes a bevy_ecs entity with optional components. Elegant in theory but 150x150x3 = 67,500 entities per location, potentially 4M+ entities for a district. ECS entity overhead (~128 bytes each) makes this prohibitively expensive. Queries scale poorly at this count. Rejected for spatial data; tiles remain grid-based. Entities are reserved for interactive objects placed ON tiles. + - **(d) Hybrid (palette + entity overlay):** Palette for base tiles, entities for interactive tile features (doors, containers, triggers). This is *almost* what we chose — the distinction is that our sparse override map is grid-indexed (O(1) lookup by position) rather than entity-query based. Interactive objects that have their own behavior (NPCs, containers, items) remain entities; tile properties that are spatial/static (material, visual variant, access) are grid data. +- **Key design principles:** + - Palette is the type; override is the instance. 90%+ of tiles need only a palette ID. + - String-array tile maps remain human-readable and merge-friendly. No JSON, no complex nested structures. + - `WalkabilityMap` API is unchanged — callers don't know about palettes. + - BTreeMap for deterministic iteration per D-010 principle 4. + - Palette keys are `char` (single Unicode codepoint) for direct mapping from tile string arrays. +- **Raised by:** Tyre (architecture), requested by #586 (Epic: extensible tile data model). +- **Dissent:** None anticipated — this is a design-only D-record for post-v0.1 implementation. +- **Cross-reference:** D-054 (tile-based movement), D-066 (dual-scale grid), D-094 (spatial hierarchy), D-099 (WallBackside classification), D-100 (DamageOverlay), D-012 (chunk architecture) + +--- + +*33 decisions. Last updated: 2026-03-05 (D-113 added — tile data model design, Sprint 24)*