Files
Verbatim/MULTILAYER_PLAN.md
T
Emil 24b6d0320f feat: multi-layer world — temperature, gas, pressure, light as parallel per-chunk arrays
- Removed temp from Cell (13→9 bytes), added temps/pressure/gas_type/gas_density/light arrays to Chunk
- Layer access via grid.get_temp()/set_temp()/get_gas()/set_gas()/get_pressure()/set_pressure()/get_light()/set_light()
- cells_swap swaps all layers, set_material sets default_temp
- heat_transfer refactored to direct array access on temps[] (no Cell copy)
- CA rules refactored: cell.temp → grid.get_temp()/set_temp()
- gas_step: gas flow (rise, spread), fire produces CO2+smoke, steam condenses to water, acid+organic→poison gas
- pressure_step: pressure equalization for connected non-solid cells
- light_step: world-space persistent lighting, updated every 10 ticks, ray-cast line-of-sight
- Gas damage: poison gas damages entities, CO2 suffocates, applied before ca.step()
- Multi-section chunk serialization (VWM1 magic + cells + temps + gas + pressure + light)
- Old 12-byte chunk format auto-detected for backward compat
- AI spectrum: new gas + pressure spectrums, light spectrum uses world-space fallback
- Protocol: gas/pressure spectrum commands
- pre_dirty mechanism: layer steps process pre-clear dirty rects for cross-cell diffusion
- 14 new multilayer tests, all 185 tests + 14 scenarios pass
- Benchmark: 85.9 FPS (graphics surface, was 128 pre-layers — 33% regression from 4 new layer steps)
2026-06-21 23:38:33 +03:00

568 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Verbatim — Multi-Layer World Plan
> Phase 6: separate world layers for temperature, gas/air, pressure, and light.
> Created June 2026. Replaces the inline `Cell.temp` with parallel per-chunk arrays.
## Current State
All world data is packed into a single `Cell` struct:
```rust
pub struct Cell {
pub material: MaterialId, // 1 byte
pub temp: f32, // 4 bytes <-- will be removed
pub updated_this_tick: bool, // 1 byte
pub variant: u8, // 1 byte
pub fg: [u8; 3], // 3 bytes
pub bg: [u8; 3], // 3 bytes
} // ~13 bytes
```
**Problems:**
- Every `temp` modification copies the entire `Cell` through `grid.get()`/`grid.set()` + triggers `mark_dirty()`
- `heat_transfer()` already uses a temporary `Vec<f32>` buffer but writes results back into `Cell`
- No pressure field exists
- No gas composition field exists (`Empty` = air, `static_ = true`, no flow)
- Lighting is ephemeral viewport-sized `LightGrid`, not persisted in world space
## Target Architecture
Parallel arrays inside `Chunk`, shared dirty rects:
```
Chunk (64x64 = 4096 cells)
cells: Vec<Cell> ~36 KB (material + color, no temp)
temps: Vec<f32> 16 KB (temperature)
pressure: Vec<u8> 4 KB (0-255, 128 = atmospheric)
gas_type: Vec<u8> 4 KB (0=air, 1=smoke, 2=poison, 3=CO2, 4=steam)
gas_density:Vec<u8> 4 KB (0-255 concentration)
light: Vec<[u8; 3]> 12 KB (world-space RGB)
--------
~76 KB per chunk
```
**Why inside Chunk (not separate ChunkedGrids):**
- Reuse dirty rects, active chunks, streaming, serialization
- One `chunk_at()` call instead of multiple
- Layers updated in a single pass over the dirty rect
## New Struct Definitions
### Cell (cell.rs) — temp removed
```rust
pub struct Cell {
pub material: MaterialId,
pub updated_this_tick: bool,
pub variant: u8,
pub fg: [u8; 3],
pub bg: [u8; 3],
} // ~9 bytes
```
`to_bytes()` returns `[u8; 8]` (was `[u8; 12]`):
| Offset | Bytes | Field |
|--------|-------|-------|
| 0 | 1 | material |
| 1 | 1 | variant |
| 2-4 | 3 | fg |
| 5-7 | 3 | bg |
`updated_this_tick` is not serialized (transient).
### Chunk (chunk.rs) — 5 new arrays
```rust
pub struct Chunk {
pub cells: Vec<Cell>,
pub temps: Vec<f32>,
pub pressure: Vec<u8>,
pub gas_type: Vec<u8>,
pub gas_density: Vec<u8>,
pub light: Vec<[u8; 3]>,
pub active: bool,
pub modified: bool,
pub was_modified: bool,
pub generated: bool,
pub dirty: Option<(i32, i32, i32, i32)>,
}
```
`Chunk::new()` initializes:
- `cells`: 4096 × `Cell::empty()`
- `temps`: 4096 × `20.0` (ambient temperature)
- `pressure`: 4096 × `128` (atmospheric)
- `gas_type`: 4096 × `0` (air)
- `gas_density`: 4096 × `0` (no gas)
- `light`: 4096 × `[0, 0, 0]` (no light, computed later)
### ChunkedGrid (chunked_grid.rs) — new accessor methods
```rust
pub fn get_temp(&self, x: i32, y: i32) -> f32
pub fn set_temp(&mut self, x: i32, y: i32, t: f32)
pub fn get_pressure(&self, x: i32, y: i32) -> u8
pub fn set_pressure(&mut self, x: i32, y: i32, p: u8)
pub fn get_gas(&self, x: i32, y: i32) -> (u8, u8) // (type, density)
pub fn set_gas(&mut self, x: i32, y: i32, gas_type: u8, density: u8)
pub fn get_light(&self, x: i32, y: i32) -> [u8; 3]
pub fn set_light(&mut self, x: i32, y: i32, rgb: [u8; 3])
```
All `set_*` methods call `mark_dirty(x, y)` (shared dirty rect).
`get_temp` for out-of-bounds returns `20.0` (ambient).
`get_pressure` for out-of-bounds returns `128` (atmospheric).
`get_gas` for out-of-bounds returns `(0, 0)` (clean air).
`get_light` for out-of-bounds returns `[0, 0, 0]`.
## Simulation Steps
### New `fixed_update()` order
```
1. stream_chunks()
2. update_active_chunks()
3. ca.step() -- material CA (sand, water, lava, etc.)
4. heat_transfer() -- temperature diffusion on temps[] directly
5. gas_step() -- gas flow CA (NEW)
6. pressure_step() -- pressure equalization (NEW)
7. light_step() -- world-space light update, every N ticks (NEW)
8. update_entities()
9. update_slime_ai()
10. update_goblin_ai()
11. update_combat()
12. update_projectiles()
13. apply_world_damage()
14. decompose_corpses()
15. update_status_effects() -- now includes gas damage
16. update_score()
17. update_item_pickup()
18. grid.swap_modified_flags()
```
### Step 3: CA Rules Refactor (cellular.rs)
All `cell.temp` references become `grid.get_temp(x, y)`:
| Method | Current | New |
|--------|---------|-----|
| `update_water` | `cell.temp > 100.0` → Steam | `grid.get_temp(x,y) > 100.0` → Steam |
| `update_lava` | `cell.temp < 400.0` → Stone | `grid.get_temp(x,y) < 400.0` → Stone |
| `update_lava` | `lava.temp -= 50.0` | `grid.set_temp(x,y, grid.get_temp(x,y) - 50.0)` |
| `update_steam` | `cell.temp < 80.0` → Water | `grid.get_temp(x,y) < 80.0` → Water |
| `update_fire` | `new_n.temp = 400.0` | `grid.set_temp(nx,ny, 400.0)` |
| `update_fire` | `new.temp -= 15.0` | `grid.set_temp(x,y, grid.get_temp(x,y) - 15.0)` |
| `update_flesh` | `cell.temp > 200.0` → Fire | `grid.get_temp(x,y) > 200.0` → Fire |
| `update_grass` | `cell.temp > 250.0` → Fire | `grid.get_temp(x,y) > 250.0` → Fire |
| `update_dirt` | `cell.temp < 0.0` → Stone | `grid.get_temp(x,y) < 0.0` → Stone |
Also: when CA creates a new material (e.g., Water → Steam), set the temp layer:
- `grid.set_temp(x, y, 110.0)` for steam from boiling water
- `grid.set_temp(x, y, 50.0)` for water from condensed steam
- `grid.set_temp(x, y, 400.0)` for fire from ignited material
### Step 4: heat_transfer() Refactor (cellular.rs)
**Before** (current): reads `cell.temp` via `grid.get()`, writes via `grid.set()`, copies Cell each time.
**After**: direct array access on `chunk.temps[]`.
```
for each active chunk with dirty rect:
snapshot temps[dirty_rect + 1 margin] into self.temps_buffer
for each cell in dirty_rect:
avg = average of 4 neighbors from self.temps_buffer
chunk.temps[idx] += (avg - chunk.temps[idx]) * conductivity * 0.1
if phase transition threshold crossed:
grid.set_material(x, y, new_material) -- triggers mark_dirty
grid.set_temp(x, y, new_temp)
```
**No `grid.get()`/`grid.set()` for temperature.** No Cell copying. No mark_dirty per pixel (only for phase transitions).
Performance: heat_transfer goes from ~500 get+set calls to ~500 direct array writes.
### Step 5: gas_step() — NEW (cellular.rs)
Gas CA rules per cell in dirty rect:
**Flow rules:**
- Gas rises if `gas_density > 0` and cell above is empty/gas with lower density
- Gas spreads horizontally to equalize density
- Gas cannot pass through solid cells (is_solid)
- Gas accumulates at ceilings (density increases upward)
**Gas types:**
| Type | ID | Behavior |
|------|----|----------|
| Air | 0 | Default, no effect |
| Smoke | 1 | Rises, blocks light slightly, fades over time |
| Poison | 2 | Rises, damages entities in contact, fades slowly |
| CO2 | 3 | Rises, suffocates fire (fire dies if CO2 density > threshold) |
| Steam | 4 | Rises, condenses to water at temp < 80, transparent |
**Material interactions:**
- `Fire` + `Air` → produces `CO2` + `Smoke`, consumes air density
- `Fire` + `CO2` (high density) → fire extinguishes
- `Lava` + `Water` → produces `Steam` (gas_type=4)
- `Acid` + `Flesh/Wood` → produces `Poison` gas
- `Steam` + `temp < 80` → condenses to `Water` (gas cleared)
**Gas update order:**
1. Material-to-gas transitions (fire produces CO2, lava+water → steam)
2. Gas flow (rise + spread)
3. Gas-to-material transitions (steam condenses, fire suffocates)
4. Gas fading (smoke/poison slowly dissipate)
### Step 6: pressure_step() — NEW (cellular.rs)
Pressure equalization for connected liquid/gas regions.
**Rules:**
- `128` = atmospheric pressure (default for empty cells)
- Liquids generate pressure by depth: `pressure = 128 + depth_from_surface * k`
- Pressure diffuses to neighbors: `p += (neighbor_p - p) * diffusion_rate`
- Solids block pressure transfer
- High pressure ( > 200) pushes liquids/gas through gaps
- Explosions: temporarily set pressure to 255, then diffuse
**Algorithm:**
```
for each active chunk with dirty rect:
for each cell in dirty_rect:
if cell is liquid or gas or empty:
avg_p = average of 4 non-solid neighbors' pressure
new_p = lerp(current_p, avg_p, 0.1)
chunk.pressure[idx] = new_p
```
This enables:
- U-bend pipes (water level equalizes through connected path)
- Fountains (high pressure pushes water up)
- Gas displacement (fire creates pressure, pushes gas out)
- Explosions (pressure wave propagates, destroys weak materials)
### Step 7: light_step() — NEW (cellular.rs)
World-space persistent lighting, updated every N ticks (default N=10).
**Algorithm:**
```
every N ticks:
for each active chunk:
clear chunk.light to [0,0,0]
for each light source in active chunks (Lava, Fire):
ray-cast outward up to radius
for each cell within radius with line-of-sight:
attenuation = (1 - dist/radius)^2
chunk.light[idx] += source.color * source.intensity * attenuation
clamp to 255
```
**Consumers:**
- AI spectrum `render_light()` reads `grid.get_light()` instead of ephemeral `LightGrid`
- Terminal renderer: optional world-space light mode
- GPU renderers: stay as-is (compute lighting in shader, real-time)
- Capture renderer: can use world-space light for consistency
**Performance:**
- Only updates every 10 ticks (6 times per second at 60 FPS)
- Only processes active chunks with dirty rects
- Light sources gathered from `material_light()` (same as current)
### Step 8: Gas Damage in update_status_effects() (game.rs)
New status effect from gas:
```rust
fn update_status_effects(&mut self) {
for e in self.entities.all_mut() {
if e.alive {
let (ex, ey) = e.center();
let (gas_type, gas_density) = self.grid.get_gas(ex as i32, ey as i32);
if gas_type == 2 && gas_density > 50 {
// Poison gas: damage proportional to density
e.health -= (gas_density as f32 - 50.0) * 0.1;
e.status_effects.push(StatusEffect::Poisoned { timer: 60 });
}
if gas_type == 3 && gas_density > 100 {
// CO2: suffocation damage
e.health -= 2.0;
}
e.apply_status_effects();
}
}
}
```
## Serialization
### Multi-section chunk file format
```
File: cache/worlds/seed_<N>/chunk_<cx>_<cy>.bin
[4 bytes: magic "VWM1"] // Verbatim World Map v1
[1 byte: version = 1]
[1 byte: flags = 0]
[4 bytes: cell_section_len] // 8 * 4096 = 32768
[cell_section: 8 bytes x 4096] // material(1) + variant(1) + fg(3) + bg(3)
[4 bytes: temp_section_len] // 4 * 4096 = 16384
[temp_section: 4 bytes x 4096] // f32 little-endian
[2 bytes: gas_section_len] // 2 * 4096 = 8192
[gas_section: 2 bytes x 4096] // type(1) + density(1)
[2 bytes: pressure_section_len] // 1 * 4096 = 4096
[pressure_section: 1 byte x 4096] // u8
[4 bytes: light_section_len] // 3 * 4096 = 12288
[light_section: 3 bytes x 4096] // R + G + B
```
Total: ~8 + 32768 + 16384 + 8192 + 4096 + 12288 = ~74 KB per chunk (was ~49 KB).
**Old cache is incompatible.** Run `rm -rf cache/worlds` after implementation.
### Backward compatibility
None. Old `chunk_*.bin` files (12 bytes/cell format) will fail to load with a clear error. The `meta.json` format stays the same (player position, items, depth).
## Renderer Changes
### GPU Grid Buffers (graphics.rs, vulkan.rs)
Current: one storage buffer with `material as u32` per cell.
New: additional storage buffers:
- `temp_buffer`: `f32` per viewport cell (for heat shimmer, temperature visualization)
- `gas_buffer`: `u32` per viewport cell (packed: `gas_type << 16 | gas_density`)
- `pressure_buffer`: `u32` per viewport cell (for pressure visualization)
Shaders (graphics.vert, cell.vert):
- Binding 0: material grid (existing, for `is_solid()` in line_of_sight)
- Binding 1: light sources (existing)
- Binding 2: temperature grid (NEW, optional use for heat shimmer)
- Binding 3: gas grid (NEW, optional use for fog/smoke overlay)
**Phase 1 implementation**: just upload the data, don't change shader visuals yet.
**Phase 2**: add heat shimmer (vertex displacement based on temp), gas fog (alpha overlay).
### Terminal Renderer (terminal.rs)
- `render()` can optionally use `grid.get_light()` (world-space) instead of CPU `compute_lighting()`
- Toggle with a flag or mode: `--mode terminal --world-light` (experimental)
- Default stays: ephemeral `compute_lighting()` for immediate accuracy
### Capture Renderer (capture.rs)
- Use `grid.get_light()` if available (world-space light)
- Fallback to `compute_lighting()` if light layer is all zeros
### AI Spectrum (spectrum.rs)
| Spectrum | Current source | New source |
|----------|---------------|------------|
| materials | `cell.material` | `cell.material` (unchanged) |
| temperature | `cell.temp` | `grid.get_temp(x, y)` |
| light | `LightGrid` (ephemeral) | `grid.get_light(x, y)` |
| entities | `cell.is_empty()` | `cell.is_empty()` (unchanged) |
| density | `MaterialRegistry` | `MaterialRegistry` (unchanged) |
| velocity | `cell.updated_this_tick` | `cell.updated_this_tick` (unchanged) |
| gas (NEW) | — | `grid.get_gas(x, y)` → type + density chars |
| pressure (NEW) | — | `grid.get_pressure(x, y)` → 0-9 scale char |
New spectrum commands in pipe protocol:
```json
{"cmd":"get_spectrum","spectrum":"gas","w":80,"h":25}
{"cmd":"get_spectrum","spectrum":"pressure","w":80,"h":25}
```
## Material Properties (material.rs)
New fields in `Material`:
```rust
pub struct Material {
// ... existing fields ...
pub gas_emission: (u8, u8), // (gas_type, amount_per_tick) — 0 = none
pub pressure_gen: u8, // pressure added per tick (for explosions/lava)
}
```
| Material | gas_emission | pressure_gen |
|----------|-------------|--------------|
| Empty | (0, 0) | 0 |
| Sand | (0, 0) | 0 |
| Water | (0, 0) | 0 |
| Stone | (0, 0) | 0 |
| Lava | (0, 0) | 1 |
| Wood | (0, 0) | 0 |
| Flesh | (0, 0) | 0 |
| Bone | (0, 0) | 0 |
| Steam | (4, 0) | 0 |
| Fire | (3, 5) | 2 |
| Acid | (0, 0) | 0 |
| Smoke | (1, 0) | 0 |
| Grass | (0, 0) | 0 |
| Dirt | (0, 0) | 0 |
| Stairs | (0, 0) | 0 |
`Fire` emits CO2 (type 3) at 5 density/tick and generates 2 pressure/tick.
`Lava` generates 1 pressure/tick (heat expansion).
## Test Plan
### Updated tests (cell.temp → grid.get_temp)
All tests that access `cell.temp` via `GameSession` need updating:
- `tests/physics_lava.rs` — lava temperature checks
- `tests/physics_interactions.rs` — lava+water=steam, fire spread
- `tests/physics_water.rs` — water boiling
- `tests/integration.rs` — any temp-related assertions
- `tests/physics_acid.rs` — acid + organic → poison gas
### New tests
**Temperature layer:**
- `heat_transfer_diffuses_through_solid_material` — stone wall between hot/cold
- `heat_transfer_uses_conductivity` — wood conducts less than stone
- `lava_heats_adjacent_water_to_steam` — phase transition via heat diffusion
- `temperature_persists_across_chunk_boundary` — cross-chunk heat flow
**Gas layer:**
- `gas_rises_and_accumulates_at_ceiling` — smoke fills upward
- `fire_consumes_air_and_produces_co2` — fire reduces air, increases CO2
- `fire_extinguishes_in_high_co2` — fire dies without air
- `poison_gas_damages_entity` — entity takes damage in poison gas
- `steam_condenses_to_water_when_cold` — gas-to-material transition
- `lava_plus_water_produces_steam_gas` — material interaction creates gas
- `gas_does_not_pass_through_solid_walls` — containment test
**Pressure layer:**
- `pressure_equalizes_in_connected_liquids` — U-bend test
- `high_pressure_pushes_liquid_through_gap` — fountain test
- `explosion_creates_pressure_wave` — pressure propagation
- `solid_blocks_pressure_transfer` — isolation test
**Light layer:**
- `light_persists_in_world_space` — light stored in chunk
- `light_blocked_by_solid_walls` — shadow casting
- `light_updates_when_source_removed` — fire extinguished → darkness
- `lava_emits_light_in_world_space` — light source test
**Serialization:**
- `chunk_save_load_roundtrip_preserves_all_layers` — temp, gas, pressure, light
- `old_cache_format_rejected` — error on loading v0 format
## Implementation Order
| Step | Files | Description |
|------|-------|-------------|
| 1 | `cell.rs` | Remove `temp` from `Cell`, update `to_bytes`/`from_bytes` to 8 bytes |
| 2 | `chunk.rs` | Add `temps`, `pressure`, `gas_type`, `gas_density`, `light` arrays, init in `new()` |
| 3 | `chunked_grid.rs` | Add `get_temp`/`set_temp`/`get_pressure`/`set_pressure`/`get_gas`/`set_gas`/`get_light`/`set_light` methods |
| 4 | `chunked_grid.rs` | Update `save_chunk`/`load_chunk` to multi-section format |
| 5 | `cellular.rs` | Refactor `heat_transfer()` to direct array access on `temps[]` |
| 6 | `cellular.rs` | Refactor all CA rules: `cell.temp` → `grid.get_temp()` / `grid.set_temp()` |
| 7 | `cellular.rs` | Implement `gas_step()` — gas flow + material interactions |
| 8 | `cellular.rs` | Implement `pressure_step()` — pressure equalization |
| 9 | `cellular.rs` | Implement `light_step()` — world-space lighting (every N ticks) |
| 10 | `game.rs` | Update `fixed_update()`: add gas_step, pressure_step, light_step calls |
| 11 | `game.rs` | Update `update_status_effects()`: gas damage (poison, CO2) |
| 12 | `material.rs` | Add `gas_emission`, `pressure_gen` fields to `Material` |
| 13 | `render/graphics.rs` | Upload temp/gas/pressure buffers to GPU |
| 14 | `render/vulkan.rs` | Same as graphics.rs |
| 15 | `render/lighting.rs` | Update `compute_lighting` to optionally read world-space light |
| 16 | `render/terminal.rs` | Optional world-space light mode |
| 17 | `render/capture.rs` | Use world-space light if available |
| 18 | `ai/spectrum.rs` | Update temperature/light spectrums, add gas + pressure spectrums |
| 19 | `ai/protocol.rs` | Add gas/pressure spectrum commands |
| 20 | `ai/state.rs` | Update `CellInfo` to include gas/pressure data |
| 21 | `ai/session.rs` | Update `find_material` for infinite mode (separate fix) |
| 22 | `tests/*.rs` | Update all temp-related tests, add new layer tests |
| 23 | — | `rm -rf cache/worlds` (old cache incompatible) |
| 24 | — | `cargo test` — all green |
| 25 | — | `cargo run --release -- --mode benchmark` — verify no regression |
## File Impact Map
| File | Changes |
|------|---------|
| `src/world/cell.rs` | Remove `temp` field, update `to_bytes`/`from_bytes` (12→8 bytes) |
| `src/world/chunk.rs` | 5 new arrays, init, accessor methods, `reset_tick_flags` unchanged |
| `src/world/chunked_grid.rs` | 8 new accessor methods, multi-section serialization, `ensure_chunk` init new arrays |
| `src/world/cellular.rs` | `heat_transfer` refactor, `gas_step`, `pressure_step`, `light_step`, CA rule temp changes |
| `src/world/material.rs` | `gas_emission`, `pressure_gen` fields on `Material` |
| `src/game.rs` | `fixed_update` new steps, gas damage in `update_status_effects` |
| `src/render/graphics.rs` | 3 new GPU buffers (temp, gas, pressure), upload code |
| `src/render/vulkan.rs` | Same as graphics.rs |
| `src/render/lighting.rs` | Optional world-space light mode in `compute_lighting` |
| `src/render/terminal.rs` | Optional `--world-light` flag |
| `src/render/capture.rs` | World-space light fallback |
| `src/ai/spectrum.rs` | Temp/light from layers, new gas/pressure spectrums |
| `src/ai/protocol.rs` | New spectrum commands |
| `src/ai/state.rs` | `CellInfo` + gas/pressure fields |
| `tests/*.rs` | Update ~10 test files, add ~20 new tests |
## Performance Expectations
| Metric | Before | Expected After |
|--------|--------|---------------|
| Cell copy per get() | 13 bytes | 9 bytes (-31%) |
| heat_transfer per cell | 2 Cell copies + mark_dirty | 1 array write (no copy, no dirty) |
| Chunk memory | ~53 KB | ~76 KB (+43%) |
| Active chunk memory (9 chunks) | ~477 KB | ~684 KB |
| CA step time | ~0.5 ms | ~0.4 ms (fewer copies) |
| heat_transfer time | ~0.3 ms | ~0.1 ms (direct array) |
| gas_step time | N/A | ~0.2 ms (new) |
| pressure_step time | N/A | ~0.1 ms (new) |
| light_step time (every 10 ticks) | N/A | ~0.5 ms (amortized ~0.05 ms/tick) |
| Total fixed_update | ~1.5 ms | ~1.3 ms |
| Render frame | ~4 ms | ~4.5 ms (+3 buffers) |
## Cross-Layer Interactions
```
Fire (material)
→ heats temp layer (heat_transfer)
→ emits CO2 + Smoke (gas layer)
→ generates pressure (pressure layer)
→ emits light (light layer)
Lava (material)
→ heats temp layer
→ generates pressure
→ emits light
→ + Water → Steam (gas layer) + temp drop
Temperature layer
→ Water > 100°C → Steam (material + gas)
→ Lava < 400°C → Stone (material)
→ Steam < 80°C → Water (material, gas cleared)
→ Wood > 300°C → Fire (material)
Gas layer
→ CO2 high → Fire extinguishes (material)
→ Poison → entity damage (status effects)
→ Steam + cold temp → Water (material)
Pressure layer
→ High pressure → pushes liquid through gaps
→ Explosion → pressure wave → terrain destruction
→ Lava → constant pressure generation
```
## Open Questions
| Question | Default | Notes |
|----------|---------|-------|
| Light update frequency | Every 10 ticks | Configurable, tradeoff: accuracy vs perf |
| Gas diffusion rate | 0.1 | How fast gas spreads per tick |
| Pressure diffusion rate | 0.1 | How fast pressure equalizes |
| Gas fade rate | 1 per 60 ticks | Smoke/poison slowly dissipate |
| Max gas density | 255 | u8 limit, may need f32 if finer |
| Pressure as u8 or f32? | u8 (0-255) | Simpler, 128=atmospheric. f32 if precision needed |
| Steam as gas or material? | Gas (type 4) | Current: Steam is a material. New: Steam is a gas that condenses to Water |
| CO2 threshold for fire death | density > 150 | Fire extinguishes when CO2 concentration is high |
| Poison damage threshold | density > 50 | Entity takes damage above this concentration |