docs(architecture): D-113 tile data model — extensible per-tile properties (#594)
Tile palette + sparse override design. Zero-migration path for existing location YAMLs. Runtime: TilePalette resource, TileCell with material_id, sparse TileOverrideMap. Unblocks post-v0.1 door mechanics and visual variants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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 |
|
||||
|
||||
@@ -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<char, TileType>` 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)*
|
||||
|
||||
Reference in New Issue
Block a user