docs: update AGENTS.md and PLAN.md with all recent changes

AGENTS.md:
- Multi-layer world (temp/gas/pressure/light) fully documented
- Audio system (15 sounds, rodio, overlapping playback)
- UI layer (flat Vec, procedurally scalable font)
- Uniform tick rate (all loaded chunks active every tick)
- Fire propagation (updated_this_tick prevents chain reactions)
- Surface terrain (low-frequency noise, wide rolling hills)
- Chunk streaming (radius 2, all active, save every 10 ticks)
- Dirty rects (no size limit, full processing)
- cells_swap (split_at_mut optimization)
- Module layout updated with audio/mod.rs
- AI spectrums: gas + pressure added

PLAN.md:
- Phase 1 (combat): all checked off
- Phase 1.5 (UI): most items checked off
- Phase 3 (RPG): stats/inventory/XP/status effects checked off
- Phase 5: audio checked off
- Phase 6 (multi-layer): marked DONE
- Milestones updated through 0.55
- Numbers: 9000 lines, 185 tests, 130-150 FPS
This commit is contained in:
Emil
2026-06-22 17:16:47 +03:00
parent 22de8bc42e
commit f09c44f7a0
2 changed files with 82 additions and 62 deletions
+20 -12
View File
@@ -27,10 +27,12 @@ Rust edition 2024, requires rustc >= 1.96. Vulkan 1.2+ required for `ascii`/`gra
- **AI observation** uses multi-spectrum ASCII layers via pipe protocol: - **AI observation** uses multi-spectrum ASCII layers via pipe protocol:
- `materials` — material type per cell - `materials` — material type per cell
- `temperature` — heat levels encoded as characters - `temperature` — heat levels encoded as characters
- `light` — light intensity per cell - `light` — light intensity per cell (world-space fallback)
- `entities` — entity positions and types only - `entities` — entity positions and types only
- `density` — material density visualization - `density` — material density visualization
- `velocity` — entity movement speed and CA activity - `velocity` — entity movement speed and CA activity
- `gas` — gas type + density per cell (NEW)
- `pressure` — pressure levels per cell (NEW)
## Tape System ## Tape System
@@ -44,7 +46,7 @@ Each tape frame contains:
- Tick number, depth, kills, score - Tick number, depth, kills, score
- Camera position - Camera position
- Player HP, position, entity count - Player HP, position, entity count
- All 6 spectrum layers as ASCII text - All 8 spectrum layers as ASCII text
Pipe protocol spectrum commands: Pipe protocol spectrum commands:
- `{"cmd":"get_spectrum","spectrum":"materials","w":80,"h":25}` — single spectrum - `{"cmd":"get_spectrum","spectrum":"materials","w":80,"h":25}` — single spectrum
@@ -107,7 +109,7 @@ SPV files are committed. `include_bytes!` embeds them at compile time.
**Source of truth**: `ChunkedGrid` of `Cell` structs with parallel per-chunk layer arrays. Replaces the old fixed-size `Grid` with dual storage: **Source of truth**: `ChunkedGrid` of `Cell` structs with parallel per-chunk layer arrays. Replaces the old fixed-size `Grid` with dual storage:
- **Bounded mode**: `Vec<Chunk>` for deterministic 250x250 test/AI grids. - **Bounded mode**: `Vec<Chunk>` for deterministic 250x250 test/AI grids.
- **Infinite mode**: `HashMap<(i64, i64), Chunk>` for continuous 12500x12500 cell (100000x100000 px) Noita-scale worlds. - **Infinite mode**: `HashMap<(i64, i64), Chunk>` for continuous Noita-scale worlds.
Main game (`--mode terminal`, `--mode ascii`, `--mode graphics`) uses the infinite mode. Each `Cell` stores `material`, `fg`/`bg` color, `variant` inline (9 bytes, no temp). Temperature, gas, pressure, and light are stored in parallel arrays per chunk. Chunks are 64x64 cells with `active`, `modified`, `was_modified`, `generated`, and `dirty` flags. Main game (`--mode terminal`, `--mode ascii`, `--mode graphics`) uses the infinite mode. Each `Cell` stores `material`, `fg`/`bg` color, `variant` inline (9 bytes, no temp). Temperature, gas, pressure, and light are stored in parallel arrays per chunk. Chunks are 64x64 cells with `active`, `modified`, `was_modified`, `generated`, and `dirty` flags.
@@ -120,10 +122,12 @@ Main game (`--mode terminal`, `--mode ascii`, `--mode graphics`) uses the infini
Layer access via `grid.get_temp()`/`set_temp()`, `grid.get_gas()`/`set_gas()`, `grid.get_pressure()`/`set_pressure()`, `grid.get_light()`/`set_light()`. All `set_*` methods call `mark_dirty()` (shared dirty rect). `cells_swap` swaps all layers. `set_material` also sets `default_temp()`. Layer access via `grid.get_temp()`/`set_temp()`, `grid.get_gas()`/`set_gas()`, `grid.get_pressure()`/`set_pressure()`, `grid.get_light()`/`set_light()`. All `set_*` methods call `mark_dirty()` (shared dirty rect). `cells_swap` swaps all layers. `set_material` also sets `default_temp()`.
**Simulation steps** per `fixed_update()`: `apply_gas_damage()` → `ca.step()` (material CA + heat_transfer + gas_step + pressure_step + light_step) → entity updates → combat → status effects. The `ca.step()` saves pre-clear dirty rects and passes them to layer steps so heat/gas/pressure can diffuse beyond CA-active cells. **Simulation steps** per `fixed_update()`: `apply_gas_damage()` → `ca.step()` (material CA + heat_transfer + gas_step + pressure_step + light_step) → entity updates → combat → status effects. The `ca.step()` saves pre-clear dirty rects and passes them to layer steps so heat/gas/pressure can diffuse beyond CA-active cells. All loaded chunks are activated every tick — uniform simulation rate across the world.
**Serialization**: Multi-section chunk format with `VWM1` magic header. Sections: cells (8 bytes × 4096), temps (4 × 4096), gas (2 × 4096), pressure (1 × 4096), light (3 × 4096). Old 12-byte format auto-detected and loaded for backward compat. **Serialization**: Multi-section chunk format with `VWM1` magic header. Sections: cells (8 bytes × 4096), temps (4 × 4096), gas (2 × 4096), pressure (1 × 4096), light (3 × 4096). Old 12-byte format auto-detected and loaded for backward compat.
**Audio** (`src/audio/mod.rs`): `AudioEngine` using `rodio` for WAV playback. 15 procedurally generated sounds (jump, shoot, hit, explosion, death, pickup, descend, powerup, step, lava_bubble, acid_sizzle, water_splash, fire_crackle, ui_click, goblin_growl) embedded via `include_bytes!`. Each sound creates its own `Sink` + `detach()` for overlapping playback. Ambient sounds scan 15-cell radius around player. `M` key toggles audio. Gracefully degrades when no audio device available.
**Four entity kinds**: `Player`, `Goblin`, `Slime`, `Corpse`. Three physics types: cellular (CA materials in grid), rigid (alive entities, AABB + slope stepping), ragdoll (corpses, Verlet constraints). **Four entity kinds**: `Player`, `Goblin`, `Slime`, `Corpse`. Three physics types: cellular (CA materials in grid), rigid (alive entities, AABB + slope stepping), ragdoll (corpses, Verlet constraints).
**Three render modes**: `terminal` (crossterm ANSI), `ascii` (Vulkan glyph atlas + instanced), `graphics` (Vulkan colored quads). All three read the same `ChunkedGrid` + `EntityManager` and an optional `LightGrid` overlay. GPU renderers upload a viewport-relative grid buffer and index it in shaders with `cam_pos`. **Three render modes**: `terminal` (crossterm ANSI), `ascii` (Vulkan glyph atlas + instanced), `graphics` (Vulkan colored quads). All three read the same `ChunkedGrid` + `EntityManager` and an optional `LightGrid` overlay. GPU renderers upload a viewport-relative grid buffer and index it in shaders with `cam_pos`.
@@ -149,11 +153,11 @@ Layer access via `grid.get_temp()`/`set_temp()`, `grid.get_gas()`/`set_gas()`, `
**Items & inventory**: `Item` (weapon, armor, consumable), `ItemManager`, and inventory/equipment slots on `Player`. Items spawn in the world and are picked up on contact. `Game::use_item(0)` equips weapons/armor or consumes potions; `Game::drop_item(0)` returns an item to the world. Equipped weapon adds damage bonus to projectiles; equipped armor reduces enemy contact damage. **Items & inventory**: `Item` (weapon, armor, consumable), `ItemManager`, and inventory/equipment slots on `Player`. Items spawn in the world and are picked up on contact. `Game::use_item(0)` equips weapons/armor or consumes potions; `Game::drop_item(0)` returns an item to the world. Equipped weapon adds damage bonus to projectiles; equipped armor reduces enemy contact damage.
**UI layer**: `ui::UiLayer` overlays non-destructive UI on all renderers. Health bars above entities, bottom-line HUD, scrolling message log, floating damage numbers, screen-edge indicators, death screen, entity labels, status icons, minimap, and a character panel. UI is drawn unlit on top of the world. In GPU (`ascii`/`graphics`) and capture modes the UI is rendered as a separate 2x2 pixel-per-cell pass; terminal mode renders UI at full character size. **UI layer**: `ui::UiLayer` overlays non-destructive UI on all renderers. Uses a flat `Vec<Option<UiCell>>` array (not HashMap) for O(1) cell access. `font_scale` auto-computed from screen height — text, panels, health bars, and minimap scale proportionally. Health bars above entities, bottom-line HUD, scrolling message log, floating damage numbers, screen-edge indicators, death screen, entity labels, status icons, minimap (sampled with step_by(4)), and a character panel. UI is drawn unlit on top of the world. In GPU (`ascii`/`graphics`) and capture modes the UI is rendered as a separate 2x2 pixel-per-cell pass; terminal mode renders UI at full character size.
**Chunk system**: `ChunkedGrid` is divided into 64x64 `Chunk`s. Each chunk tracks `active`, `modified`, `was_modified`, `generated`, and `dirty` (an optional bounding rect of cells that need processing). `save_chunk(path, cx, cy)` and `load_chunk(path, cx, cy)` serialize chunk cells via 12-byte binary format. Cell serialization is handled by `Cell::to_bytes()` / `Cell::from_bytes()`. `Chunk::generated` is set when a chunk is generated or loaded from cache, preventing accidental regeneration. **Chunk system**: `ChunkedGrid` is divided into 64x64 `Chunk`s. Each chunk tracks `active`, `modified`, `was_modified`, `generated`, and `dirty` (an optional bounding rect of cells that need processing). `save_chunk(path, cx, cy)` and `load_chunk(path, cx, cy)` serialize chunk data via multi-section `VWM1` binary format. Cell serialization is handled by `Cell::to_bytes()` / `Cell::from_bytes()` (8 bytes/cell). `Chunk::generated` is set when a chunk is generated or loaded from cache, preventing accidental regeneration.
**Dirty rect optimization** (Noita-style): Each chunk maintains a `dirty: Option<(i32, i32, i32, i32)>` bounding rect of cells that need CA processing. When a cell changes via `set`, `set_material`, `cells_swap`, or `set_cell_index`, the chunk's dirty rect is expanded to include that cell ±1 (for neighbor influence). The CA step only iterates cells within dirty rects, skipping chunks with no dirty rect entirely. Liquids and gases (water, lava, acid, steam, fire, smoke) re-mark themselves dirty after processing so they continue to flow and react. Sand and other solids can sleep when at rest. `heat_transfer` only processes cells within dirty rects and reuses its temperature buffer across frames. `update_active_chunks` activates chunks with dirty rects in addition to chunks near entities. This reduces CA step time from ~500μs to ~25μs on a 250x250 grid. **Dirty rect optimization** (Noita-style): Each chunk maintains a `dirty: Option<(i32, i32, i32, i32)>` bounding rect of cells that need CA processing. When a cell changes via `set`, `set_material`, `cells_swap`, or `set_cell_index`, the chunk's dirty rect is expanded to include that cell ±1 (for neighbor influence). The CA step only iterates cells within dirty rects, skipping chunks with no dirty rect entirely. Liquids and gases (water, lava, acid, steam, fire, smoke) re-mark themselves dirty after processing so they continue to flow and react. Sand and other solids can sleep when at rest. All dirty rects are processed fully — no size limits — ensuring uniform simulation across chunks. `update_active_chunks` activates all loaded chunks every tick for consistent physics.
**World cache**: main game seeds are saved in `Game::seed` and written to `cache/worlds/<seed>/`. Each cached world stores per-chunk binary files plus a `meta.json` with player spawn and item placement. `Game::init_world()` loads the cache if it exists; otherwise it generates the spawn region and saves it. This makes Noita-scale worlds load instantly after the first visit. **World cache**: main game seeds are saved in `Game::seed` and written to `cache/worlds/<seed>/`. Each cached world stores per-chunk binary files plus a `meta.json` with player spawn and item placement. `Game::init_world()` loads the cache if it exists; otherwise it generates the spawn region and saves it. This makes Noita-scale worlds load instantly after the first visit.
@@ -162,7 +166,7 @@ Layer access via `grid.get_temp()`/`set_temp()`, `grid.get_gas()`/`set_gas()`, `
- `2 <= cy < 6` — caves (stone, CA-carved empty space, lava/water/acid pools) - `2 <= cy < 6` — caves (stone, CA-carved empty space, lava/water/acid pools)
- `cy >= 6` — dungeon (BSP rooms, corridors, stone walls) - `cy >= 6` — dungeon (BSP rooms, corridors, stone walls)
**Chunk streaming**: `Game::stream_chunks()` is called every `fixed_update`. It loads cached chunks (or generates new ones) in a 3-chunk radius around the player, saves modified chunks beyond that radius, and unloads distant chunks. Streaming is active for infinite grids and for bounded grids larger than 2048×2048; test/AI 250×250 grids skip streaming. **Chunk streaming**: `Game::stream_chunks()` is called every `fixed_update`. It loads cached chunks (or generates new ones) in a 2-chunk radius around the player, saves modified chunks beyond that radius every 10 ticks (1 chunk at a time), and unloads distant chunks every 120 ticks. All loaded chunks are activated for uniform simulation. Streaming is active for infinite grids; test/AI 250×250 grids skip streaming.
**Vertical descent**: `MaterialId::Stairs` is a solid feature material. Player stands on stairs and presses `>` to descend. `Game::descend()` increments depth, resets the world, and respawns the player at the top. HUD shows current depth. **Vertical descent**: `MaterialId::Stairs` is a solid feature material. Player stands on stairs and presses `>` to descend. `Game::descend()` increments depth, resets the world, and respawns the player at the top. HUD shows current depth.
@@ -181,6 +185,9 @@ Layer access via `grid.get_temp()`/`set_temp()`, `grid.get_gas()`/`set_gas()`, `
- **Random seeding** — `Game::new()` uses a fixed seed for tests/AI sessions; `Game::new_random()` seeds from system time and is used by terminal/ascii/graphics modes. Cached worlds are keyed by seed. - **Random seeding** — `Game::new()` uses a fixed seed for tests/AI sessions; `Game::new_random()` seeds from system time and is used by terminal/ascii/graphics modes. Cached worlds are keyed by seed.
- **Dirty rects** — `ChunkedGrid::mark_dirty(x, y)` expands the chunk's dirty rect to include (x,y) ±1 and propagates to neighbor chunks at boundaries. `cells_swap` and `set_cell_index` call `mark_dirty` automatically. `set` and `set_material` also call `mark_dirty`. The CA step clears each chunk's dirty rect at the start of processing and rebuilds it from cell modifications during processing. - **Dirty rects** — `ChunkedGrid::mark_dirty(x, y)` expands the chunk's dirty rect to include (x,y) ±1 and propagates to neighbor chunks at boundaries. `cells_swap` and `set_cell_index` call `mark_dirty` automatically. `set` and `set_material` also call `mark_dirty`. The CA step clears each chunk's dirty rect at the start of processing and rebuilds it from cell modifications during processing.
- **World scale**: `WORLD_SCALE = 5` in `worldgen.rs` — all world features (trees, pools, walls, dunes, rooms, corridors, terrain amplitude) are multiplied by this factor. Change this constant to adjust entity-to-world size ratio. - **World scale**: `WORLD_SCALE = 5` in `worldgen.rs` — all world features (trees, pools, walls, dunes, rooms, corridors, terrain amplitude) are multiplied by this factor. Change this constant to adjust entity-to-world size ratio.
- **Surface terrain**: Multi-octave sine noise with low frequencies (base 0.012, detail 0.04, micro 0.11) scaled by `WORLD_SCALE`. Produces wide rolling hills (~260 cell wavelength) instead of narrow peaks.
- **Uniform tick rate**: All loaded chunks simulate every tick — no chunk is frozen or partially updated. `cells_swap` uses `split_at_mut` for direct chunk array access in bounded mode.
- **Fire propagation**: Newly ignited cells get `updated_this_tick = true` to prevent chain reactions within a single tick. Fire spreads 1 cell/tick (linear, not exponential).
## Module Layout ## Module Layout
@@ -190,12 +197,13 @@ src/
lib.rs # pub mod declarations lib.rs # pub mod declarations
game.rs # Game struct, world gen, fixed_update, collision, combat, slime AI game.rs # Game struct, world gen, fixed_update, collision, combat, slime AI
input.rs # Terminal input (crossterm, InputHandler) — terminal mode only input.rs # Terminal input (crossterm, InputHandler) — terminal mode only
audio/mod.rs # AudioEngine (rodio), 15 embedded WAV sounds, ambient scanning
world/ # Cell, MaterialId, MaterialRegistry, ChunkedGrid, Grid (legacy), Chunk, CellularAutomaton, WorldGenerator, WorldCache world/ # Cell, MaterialId, MaterialRegistry, ChunkedGrid, Grid (legacy), Chunk, CellularAutomaton, WorldGenerator, WorldCache
physics/ # VerletSolver, SubBody (with color field), Constraint, resolve_grid_collision physics/ # VerletSolver, SubBody (with color field), Constraint, resolve_grid_collision, ProjectileManager
entity/ # Entity (rigid/ragdoll), EntityManager, Player, BodyTemplate, Item, ItemManager entity/ # Entity (rigid/ragdoll), EntityManager, Player, BodyTemplate, Item, ItemManager
render/ # terminal.rs, vulkan.rs (ASCII), graphics.rs (cells), lighting.rs, window_input.rs render/ # terminal.rs, vulkan.rs (ASCII), graphics.rs (cells), lighting.rs, window_input.rs, capture.rs
ai/ # GameSession, AiAction, pipe protocol, replay, scenarios ai/ # GameSession, AiAction, pipe protocol, replay, scenarios, spectrum
ui/ # UiLayer, HUD, messages, damage numbers, edge indicators ui/ # UiLayer (flat Vec), HUD, messages, damage numbers, edge indicators, scalable font
``` ```
## PLAN.md ## PLAN.md
+62 -50
View File
@@ -10,50 +10,60 @@
| System | Status | Details | | System | Status | Details |
|--------|--------|---------| |--------|--------|---------|
| Cellular automaton | Working | 14 materials: sand, water, stone, lava, wood, flesh, bone, steam, fire, acid, smoke, grass, dirt, empty | | Cellular automaton | Working | 14 materials: sand, water, stone, lava, wood, flesh, bone, steam, fire, acid, smoke, grass, dirt, empty |
| Rigid entities | Working | AABB collider, slope stepping, 27 sub-bodies (5x5 + arm), player + goblins | | Multi-layer world | Working | Temperature, gas (smoke/poison/CO2/steam), pressure, world-space light — parallel per-chunk arrays |
| Rigid entities | Working | AABB collider, slope stepping, 70 sub-bodies, player + goblins + slimes |
| Ragdoll corpses | Working | Verlet constraints, death = rigid→ragdoll transition with inherited velocity | | Ragdoll corpses | Working | Verlet constraints, death = rigid→ragdoll transition with inherited velocity |
| Terminal renderer | Working | Full terminal size, ANSI truecolor, diff-based rendering | | Terminal renderer | Working | Full terminal size, ANSI truecolor, diff-based rendering |
| ASCII renderer (Vulkan) | Working | ash + winit, instanced rendering, glyph atlas, 16x16 square cells, adaptive viewport | | ASCII renderer (Vulkan) | Working | ash + winit, instanced rendering, glyph atlas, 8x8 cells, adaptive viewport |
| Graphics renderer (Vulkan) | Working | ash + winit, colored cells (no glyphs), each material = unique base color, adaptive viewport | | Graphics renderer (Vulkan) | Working | ash + winit, colored cells (no glyphs), each material = unique base color, adaptive viewport |
| AI pipe protocol | Working | JSON stdin/stdout, 16 commands, full state export | | AI pipe protocol | Working | JSON stdin/stdout, 16 commands, full state export, 8 spectrums (materials/temp/light/entities/density/velocity/gas/pressure) |
| Test framework | Working | 109 Rust tests + 14 JSON scenarios, all passing | | Audio | Working | 15 procedurally generated sounds via rodio, overlapping playback, ambient scanning, M to toggle |
| UI layer | Working | Procedurally scalable font, flat Vec array, health bars, HUD, minimap, inventory overlay, character panel |
| Test framework | Working | 185 Rust tests + 14 JSON scenarios, all passing |
| Replay system | Working | Seeded determinism, record/playback, play_until_tick | | Replay system | Working | Seeded determinism, record/playback, play_until_tick |
| World generation | Working | Procedural chunk-based biomes: surface (noise), caves (CA), dungeons (BSP); vertical biome progression by chunk Y; 12500×12500 continuous world; chunk streaming + cache | | World generation | Working | ×5 scale: trees 30 cells, pools r20, BSP rooms 20×20, corridors 5 wide; chunk streaming + cache |
| Cross-platform | Working | Windows/Linux/macOS via winit + ash_window, no platform-specific code | | Cross-platform | Working | Windows/Linux/macOS via winit + ash_window, no platform-specific code |
| Adaptive viewport | Working | Window resize → more/fewer cells visible, cells stay 16x16 pixels | | Adaptive viewport | Working | Window resize → more/fewer cells visible, cells stay 8x8 pixels |
| Per-cell color (reality layer) | Working | Each cell stores fg/bg color inline, no registry lookup in render path | | Per-cell color | Working | Each cell stores fg/bg color inline, no registry lookup in render path |
| Performance | Working | 130-150 FPS (graphics), uniform tick rate across all chunks |
### Architecture ### Architecture
``` ```
Source of truth: `ChunkedGrid` of `Cell` structs Source of truth: `ChunkedGrid` of `Cell` structs with parallel layer arrays
- Bounded mode: `Vec<Chunk>` for 250x250 test/AI grids - Bounded mode: `Vec<Chunk>` for 250x250 test/AI grids
- Infinite mode: `HashMap<(i64, i64), Chunk>` for continuous 12500x12500 cell worlds - Infinite mode: `HashMap<(i64,i64), Chunk>` for continuous worlds
- 64x64 chunks, dirty rects, active flags, per-chunk persistence - 64x64 chunks, dirty rects, all loaded chunks active every tick
- Chunk streaming: load/generate around player, save/unload distant chunks - Multi-layer: temps (f32), pressure (u8), gas_type+density (u8×2), light ([u8;3])
- Cell: 9 bytes (material + variant + fg/bg, no temp)
- Serialization: VWM1 multi-section format, backward compat with old 12-byte
World scale: WORLD_SCALE=5, adjustable via single constant
Surface: multi-octave sine noise, wide rolling hills (~260 cell wavelength)
Three entity types: Three entity types:
1. Cellular — materials in grid, per-cell CA rules 1. Cellular — materials in grid, per-cell CA rules
2. Rigid — alive entities, AABB collider, slope stepping, single velocity 2. Rigid — alive entities, AABB collider, slope stepping, single velocity
3. Ragdoll — corpses, loose Verlet bodies, independent physics 3. Ragdoll — corpses, loose Verlet bodies, independent physics
Game loop: fixed 60Hz timestep Game loop: fixed 60Hz timestep, uniform across all chunks
Physics tick: CA step → rigid update (slope step) → ragdoll update → damage Physics tick: CA step → heat_transfer → gas_step → pressure_step → light_step
Render: terminal (ANSI) / ascii (Vulkan glyphs) / graphics (Vulkan cells) / pipe (JSON) / headless (file) → entity updates → combat → status effects → gas damage
Render: terminal (ANSI) / ascii (Vulkan glyphs) / graphics (Vulkan cells) / pipe (JSON)
Three render modes: Audio: 15 embedded WAV sounds via rodio, overlapping playback, ambient scanning
--mode terminal → pure ANSI ASCII in terminal UI: flat Vec array, procedurally scalable font, non-destructive overlay
--mode ascii → Vulkan window with ASCII characters (glyph atlas)
--mode graphics → Vulkan window with colored cells (no glyphs, material base colors)
``` ```
### Numbers ### Numbers
- ~6700 lines Rust - ~9000 lines Rust
- 171 integration tests, 14 JSON scenarios - 185 integration tests, 14 JSON scenarios
- 40+ git commits - 50+ git commits
- 0 compiler warnings (excluding winit deprecation notices) - 0 compiler warnings (excluding winit deprecation notices)
- Cross-platform: Windows/Linux/macOS - Cross-platform: Windows/Linux/macOS
- 15 embedded sound effects
- 130-150 FPS (graphics mode, release, ×5 world scale)
--- ---
@@ -71,17 +81,17 @@ loop is: aim → shoot → projectile travels → hits enemy → damage/kill.
- [x] Knockback: damage applies velocity impulse to rigid body center - [x] Knockback: damage applies velocity impulse to rigid body center
- [x] Goblin AI: move toward player, attack when adjacent - [x] Goblin AI: move toward player, attack when adjacent
- [x] Slime AI: jump toward player, contact damage - [x] Slime AI: jump toward player, contact damage
- [ ] **Projectile system**: lightweight rigid bodies (arrows, fireballs, magic bolts) - [x] **Projectile system**: lightweight rigid bodies (arrows, fireballs, magic bolts)
- Player shoots with directional input (mouse aim or movement-direction) - Player shoots with directional input (mouse aim or movement-direction)
- Projectile = small AABB, velocity, damage, lifetime - Projectile = small AABB, velocity, damage, lifetime
- On hit with entity: deal damage, destroy projectile - On hit with entity: deal damage, destroy projectile
- On hit with solid cell: stop/destroy - On hit with solid cell: stop/destroy
- Fireball: ignites materials it touches (Noita-style material interaction) - Fireball: ignites materials it touches (Noita-style material interaction)
- Magic bolt: pure damage, no material effect - Magic bolt: pure damage, no material effect
- [ ] Health bars in render (colored indicator above entity) - [x] Health bars in render (colored indicator above entity)
- [ ] Death → ragdoll → corpse decomposition (flesh cells drop into grid over time) - [x] Death → ragdoll → corpse decomposition (flesh cells drop into grid over time)
- [ ] Material interaction with entities: entity walks through fire → ignites, acid → dissolves - [x] Material interaction with entities: entity walks through fire → ignites, acid → dissolves
- [ ] Goblin AI: flee when low HP - [x] Goblin AI: flee when low HP
**Tests needed:** **Tests needed:**
- Projectile travels and deals damage on hit - Projectile travels and deals damage on hit
@@ -106,34 +116,34 @@ Render pipeline per frame:
3. Present to screen 3. Present to screen
``` ```
- [ ] `UiLayer` struct: sparse map of (screen_x, screen_y) → (char, fg_color, bg_color) - [x] `UiLayer` struct: flat `Vec<Option<UiCell>>` array for O(1) cell access
- UI elements write to this map, not to the grid - UI elements write to this array, not to the grid
- Renderer composites: if UiLayer has a cell at (x,y), it overrides the world cell visually - Renderer composites: if UiLayer has a cell at (x,y), it overrides the world cell visually
- World state is never modified by UI - World state is never modified by UI
- [ ] Health bar: colored bar above player entity, shows current/max HP - [x] Health bar: colored bar above player entity, shows current/max HP
- ████░░░░ style, colored green→yellow→red by HP ratio - ████░░░░ style, colored green→yellow→red by HP ratio
- Positioned relative to player's screen position, scrolls with camera - Positioned relative to player's screen position, scrolls with camera
- [ ] Entity labels: small text above/below entities (name, level for RPG) - [x] Entity labels: small text above/below entities (name, level for RPG)
- [ ] Status effect icons: burning 🔥, poisoned, frozen — shown next to entity - [x] Status effect icons: burning, poisoned, frozen — shown next to entity
- [ ] HUD bar (bottom of screen, non-destructive): - [x] HUD bar (bottom of screen, non-destructive):
- HP: ████████░░ 80/100 - HP: ████████░░ 80/100
- Material brush: [Sand] (current selected) - Material brush: [Sand] (current selected)
- Tick: 1234 Depth: 1 - Tick: 1234 Depth: 1
- FPS counter (debug mode) - FPS counter (debug mode)
- [ ] Tooltip on hover: when cursor is over a cell, show material name + temperature - [ ] Tooltip on hover: when cursor is over a cell, show material name + temperature
- [ ] Message log (top of screen, scrolling): "Goblin hits you for 10 damage" - [x] Message log (top of screen, scrolling): "Goblin hits you for 10 damage"
- Last N messages, older ones fade (dimmer color) - Last N messages, older ones fade (dimmer color)
- [ ] Inventory overlay (toggle with 'i'): semi-transparent panel, doesn't modify world - [x] Inventory overlay (toggle with 'i'/Tab): semi-transparent panel, doesn't modify world
- List of items, selected highlight, weight/value display - List of items, selected highlight, weight/value display
- Opens/closes without affecting simulation - Opens/closes without affecting simulation
- [ ] Menu system (pause, settings, save/load): full-screen overlay with border - [ ] Menu system (pause, settings, save/load): full-screen overlay with border
- Game loop pauses (or continues in background), UI captures input - Game loop pauses (or continues in background), UI captures input
- [ ] Minimap (corner of screen): compressed world view, explored areas only - [x] Minimap (corner of screen): compressed world view, explored areas only
- Each minimap cell = 5x5 world cells, averaged material color - Each minimap cell = 5x5 world cells, averaged material color
- Player position marker, entity dots - Player position marker, entity dots
- [ ] Crosshair/targeting: when aiming projectiles, shows trajectory preview - [ ] Crosshair/targeting: when aiming projectiles, shows trajectory preview
- [ ] Damage numbers: floating text above entities when hit, rises and fades - [x] Damage numbers: floating text above entities when hit, rises and fades
- [ ] Screen-edge indicators: arrows pointing to off-screen entities of interest - [x] Screen-edge indicators: arrows pointing to off-screen entities of interest
**Key principle: UI layer NEVER writes to grid, entities, or any game state. **Key principle: UI layer NEVER writes to grid, entities, or any game state.
It reads state and renders visuals on top. This keeps the source of truth clean It reads state and renders visuals on top. This keeps the source of truth clean
@@ -180,18 +190,18 @@ instanced quads with UI texture coordinates. Transparent background, drawn on to
**Goal: character progression, inventory, abilities** **Goal: character progression, inventory, abilities**
- [ ] Stats: strength, agility, toughness, willpower — affect damage, speed, HP, etc. - [x] Stats: strength, agility, toughness, willpower — affect damage, speed, HP, etc.
- [ ] Inventory system: items as data structs, pick up by walking over, drop with key - [x] Inventory system: items as data structs, pick up by walking over, drop with key
- [ ] Equipment: weapon affects melee damage/range, armor affects damage reduction - [x] Equipment: weapon affects melee damage/range, armor affects damage reduction
- [ ] Items in world: weapons, potions, scrolls, food — rendered as distinct ASCII chars - [x] Items in world: weapons, potions, scrolls, food — rendered as distinct ASCII chars
- [ ] Mutations (Caves of Qud style): modify entity properties - [ ] Mutations (Caves of Qud style): modify entity properties
- "Silicon skin" → entity material becomes Stone, immune to acid - "Silicon skin" → entity material becomes Stone, immune to acid
- "Flame body" → entity emits fire cells, immune to fire - "Flame body" → entity emits fire cells, immune to fire
- "Liquid form" → entity can squeeze through 1-cell gaps - "Liquid form" → entity can squeeze through 1-cell gaps
- "Multiple arms" → extra attack, can hold more items - "Multiple arms" → extra attack, can hold more items
- [ ] XP and leveling: kill entities → gain XP → level up → choose mutation - [x] XP and leveling: kill entities → gain XP → level up → choose mutation
- [ ] Skills: active abilities on cooldown (dash, stomp, material blast) - [ ] Skills: active abilities on cooldown (dash, stomp, material blast)
- [ ] Status effects: burning, poisoned, frozen, bleeding — each with tick effect - [x] Status effects: burning, poisoned, frozen, bleeding — each with tick effect
- [ ] Dialogue: talk to NPCs, simple text tree - [ ] Dialogue: talk to NPCs, simple text tree
**Tests needed:** **Tests needed:**
@@ -277,17 +287,17 @@ Two distinct render modes, both GPU-accelerated via Vulkan:
- [ ] Boss entity: large rigid body (10x10), multiple attack patterns - [ ] Boss entity: large rigid body (10x10), multiple attack patterns
- [ ] Books/readable items: lore text displayed in terminal - [ ] Books/readable items: lore text displayed in terminal
- [ ] Crafting: combine materials to create new ones (water + dirt = mud) - [ ] Crafting: combine materials to create new ones (water + dirt = mud)
- [ ] Sound: procedural audio via terminal bell or optional ALSA - [x] Sound: procedural audio via rodio, 15 embedded WAV sounds, overlapping playback
- [ ] Save/load: full game state to file (grid + entities + player + inventory) - [ ] Save/load: full game state to file (grid + entities + player + inventory)
- [ ] Death screen: stats summary, cause of death - [ ] Death screen: stats summary, cause of death
- [ ] Tutorial: first-time controls overlay - [ ] Tutorial: first-time controls overlay
- [ ] Difficulty scaling: deeper levels = stronger enemies - [ ] Difficulty scaling: deeper levels = stronger enemies
### Phase 6: Advanced Physics ### Phase 6: Advanced Physics (DONE)
**Goal: deeper Noita-style material simulation** **Goal: deeper Noita-style material simulation**
- [ ] Multi-layer world: separate grid layers for material, temperature, pressure, gas/air, light - [x] Multi-layer world: separate grid layers for material, temperature, pressure, gas/air, light
- Air layer: gas flow, ventilation in caves, gas accumulates at ceiling, displaced by fire - Air layer: gas flow, ventilation in caves, gas accumulates at ceiling, displaced by fire
- Pressure layer: liquids have pressure, flow through pipes and U-bends - Pressure layer: liquids have pressure, flow through pipes and U-bends
- Temperature layer: proper heat diffusion, materials melt/freeze at thresholds - Temperature layer: proper heat diffusion, materials melt/freeze at thresholds
@@ -547,10 +557,12 @@ assets/
|-----------|---------|--------| |-----------|---------|--------|
| 0.1 (done) | Core engine: CA, rigid, ragdoll, terminal, AI pipe | June 2026 | | 0.1 (done) | Core engine: CA, rigid, ragdoll, terminal, AI pipe | June 2026 |
| 0.2 (done) | Vulkan ASCII + graphics renderers, adaptive viewport, per-cell color, slope stepping | June 2026 | | 0.2 (done) | Vulkan ASCII + graphics renderers, adaptive viewport, per-cell color, slope stepping | June 2026 |
| 0.3 | Combat, goblin AI, projectiles, corpse decomposition | July 2026 | | 0.3 (done) | Combat, goblin AI, projectiles, corpse decomposition | June 2026 |
| 0.35 | UI layer: health bar, HUD, message log, minimap, inventory overlay | July 2026 | | 0.35 (done) | UI layer: health bar, HUD, message log, minimap, inventory overlay, scalable font | June 2026 |
| 0.4 | Chunks, biomes, dungeon gen, camera zoom | August 2026 | | 0.4 (done) | Chunks, biomes, dungeon gen, world scale ×5, chunk streaming + cache | June 2026 |
| 0.5 | RPG layer: stats, inventory, mutations, XP | October 2026 | | 0.45 (done) | RPG layer: stats, inventory, XP, status effects, items | June 2026 |
| 0.5 (done) | Multi-layer world: temp, gas, pressure, light + audio system | June 2026 |
| 0.55 (done) | Performance: uniform tick rate, 130-150 FPS, fire chain fix | June 2026 |
| 0.6 | Lighting/particles/textures (Phase 4b) | December 2026 | | 0.6 | Lighting/particles/textures (Phase 4b) | December 2026 |
| 0.7 | Multi-layer world: air, pressure, temperature, light as separate grids | Feb 2027 | | 0.7 | Multi-layer world: air, pressure, temperature, light as separate grids | Feb 2027 |
| 0.8 | AI agent: LLM + RL bridge, agent recording | April 2027 | | 0.8 | AI agent: LLM + RL bridge, agent recording | April 2027 |