80 Commits
Author SHA1 Message Date
Emil 0d54d987ac feat: explosions, particle system, electricity, structural integrity
Explosions:
- trigger_explosion(x, y, radius, damage) in Game
- Destroys non-static solids, ignites empty cells, pressure spike
- Knockback to entities (velocity away from center + upward pop)
- Particle burst on explosion (fire + smoke + debris)

Particle system:
- ParticleManager in src/physics/particle.rs (capacity 2000)
- CPU-side: position, velocity, life, color, gravity, size
- spawn_burst() for radial bursts, individual spawn() for ambient
- Rendered as third draw call in graphics.rs (is_ui: 2 in shader)
- ParticleInstance = ColorInstance compatible (12 bytes)
- upload_particles() via GpuRenderer trait
- Ambient: fire sparks, lava bubbles — scanned around player

Electricity:
- chunk.electricity: Vec<u8> (4KB/chunk) — current strength 0-255
- Material.conductive + conductivity fields (water = conductive)
- electricity_step() in CA: propagates current through conductive
  neighbors, decays over time, skips non-conductive cells
- cells_swap swaps electricity, serialization saves/loads it

Structural integrity:
- Material.structural: bool (stone, wood = true)
- structural_step() in CA (every 30 ticks, infinite mode only):
  checks if structural cells have support (below, below-left, below-right)
  converts unsupported to Sand (falls)
- Only active in infinite mode to avoid breaking test grids

Graphics shader: is_ui==2 branch renders particles in world-space
All 185 tests + 14 scenarios pass. 143 FPS benchmark.
2026-06-22 20:13:07 +03:00
Emil 4a89612081 revert: remove camera zoom, keep day/night + tooltip + crosshair
Zoom caused buffer reallocation issues and cursor misalignment.
Removed: char_size field, adjust_zoom(), MIN/MAX_CHAR, zoom input.
Kept: day/night cycle, tooltip, crosshair, cell_pixel_size() (returns constant 8).
2026-06-22 19:45:09 +03:00
Emil 69e3a6578e fix: zoom crash — check_resize after zoom to reallocate instance buffer
adjust_zoom now calls check_resize() which recalculates grid_w/grid_h
and reallocates the GPU instance buffer to match the new grid size.
Previously zoom changed grid_w/h but the buffer stayed at the old size,
causing index-out-of-bounds panic.
2026-06-22 19:37:59 +03:00
Emil 43085772cd fix: mouse coordinate conversion uses renderer cell_pixel_size
- Replaced hardcoded 1600/900 screen size with renderer.cell_pixel_size()
- Mouse world pos: cam + (screen_pos / cell_px) — correct for any zoom level
- Mouse UI pos: (screen_pos / cell_px) * UI_SCALE — correct for UI coordinate space
- Shoot direction: player_screen uses cell_px instead of hardcoded 8.0
- Added cell_pixel_size() to GpuRenderer trait and both renderers
2026-06-22 19:33:57 +03:00
Emil d6bd77c133 feat: camera zoom, day/night cycle, tooltip, crosshair
- Camera zoom: +/- keys adjust cell size 4-24px, grid_w/h recalculated
  Works in both graphics and ascii renderers via GpuRenderer trait
- Day/night cycle: ambient_light_at_tick() modulates brightness by sine
  Applied to terminal renderer (CPU lighting path)
- Tooltip: shows material name + temperature at mouse cursor position
  Tracked via CursorMoved event, world coords computed from screen pos
- Crosshair: dotted line from player to mouse cursor, fading alpha
  Shows aiming trajectory for projectile combat

All 185 tests + 14 scenarios pass. 162 FPS benchmark.
2026-06-22 17:54:20 +03:00
Emil f09c44f7a0 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
2026-06-22 17:16:47 +03:00
Emil 22de8bc42e fix: wider surface terrain — lower noise frequencies for rolling hills
base freq 0.08→0.012 (~260 cell wide hills, was ~40)
detail freq 0.23→0.04 (~78 cell features, was ~14)
micro freq 0.57→0.11 (~28 cell ripples, was ~6)
amplitudes reduced: base ±20→±15, detail ±10→±7.5, micro ±5→±2.5

Result: wide rolling hills instead of narrow sharp peaks.
2026-06-22 17:05:38 +03:00
Emil 423000e576 fix: uniform tick rate — remove dirty rect limits, optimize cells_swap
Removed all 2048 dirty rect limits:
- CA step: process all dirty cells, no subdivision/skip
- heat_transfer/gas_step/pressure_step: process all dirty cells, no skip
All chunks now simulate at the same rate — no frozen or partial cells.

Optimized cells_swap for bounded mode cross-chunk case:
- split_at_mut for simultaneous access to two chunks
- Eliminated 6 redundant get_temp/get_pressure/get_gas calls
  (read directly from chunk arrays instead of HashMap lookups)

Results: 131 FPS surface, 152 FPS caves, 142 FPS dungeon
p99 16ms — stable across all biomes
2026-06-22 16:58:25 +03:00
Emil 5ca8b7e917 fix: all loaded chunks now simulate at same tick rate
- stream_chunks: generate all chunks in radius 2 immediately (was gen_budget=2)
  No more half-generated chunks in active area
- update_active_chunks: activate ALL loaded chunks (was radius 2 around player)
  Physics consistent across all loaded chunks — no frozen boundaries
- streaming radius 3→2 (5x5=25 chunks, all active and simulated)
- 131 FPS surface, 138 FPS caves, p99 17ms
2026-06-22 16:45:24 +03:00
Emil 01a350aebd fix: fire chain reaction — newly ignited cells now have updated_this_tick=true
Root cause: when update_fire ignited a neighbor via grid.set(), the new Fire
cell had updated_this_tick=false. If the dirty rect iteration hadn't reached
that cell yet, it was processed in the same tick, igniting ITS neighbors,
creating exponential spread within a single tick.

Fix: set updated_this_tick=true on all newly created Fire cells so they
wait until the next tick before spreading.

Applied to:
- update_fire: igniting flammable neighbors
- lava_interact: lava igniting wood/grass/flesh
- update_flesh: flesh turning to fire from heat
- update_grass: grass turning to fire from heat
- update_water: water turning to steam from heat
- projectile.rs: fireball impact igniting cells

Result: fire spreads 1 cell per tick (linear, not exponential).
171 FPS avg, p99 14ms — no lag when spamming fireballs.
2026-06-22 16:35:17 +03:00
Emil 93a6bcb35c perf: light_step source cap 64, minimap step_by(4), resize early-return, warning cleanup
- light_step: cap sources at 64 (was unlimited — 400+ lava cells = 250K ray-casts)
- minimap: step_by(4) sampling (65K→4K grid.get calls, still every frame — no flicker)
- resize(): early return if size unchanged (skip 350K element reallocation)
- Clean unused mut warning
- 172 FPS avg, p99 14ms (was 66 FPS, p99 180ms)
2026-06-22 16:26:51 +03:00
Emil 3e53d709f7 perf: fix lag — chunk gen budget, dirty rect limit 2048, active chunk radius 2
- stream_chunks: max 2 chunk generations per tick (was unlimited)
- stream_chunks: save every 10 ticks, 1 chunk at a time (was every tick)
- stream_chunks: unload every 120 ticks (was 60)
- CA dirty rect limit 4096→2048, subdivide instead of skip
- Layer step dirty rect limit 4096→2048
- update_active_chunks: simplified — radius 2 around player for infinite,
  direct activation for bounded, no HashSet allocation
- ensure_chunk: early return after cache load (skip redundant insert)
- 129 FPS avg, p99 11ms (was 50 FPS, p99 265ms)
2026-06-22 13:54:47 +03:00
Emil 17aa9ab502 chore: clean up compiler warnings — unused imports, variables, dead code 2026-06-22 13:35:41 +03:00
Emil b52798c634 perf: UiLayer HashMap → flat Vec array — 62 FPS → 95 FPS
Replaced HashMap<(i32,i32), UiCell> with Vec<Option<UiCell>> indexed
by y*width+x. dirty_keys Vec tracks non-None cells for iteration.
- set/set_alpha: O(1) array write (was O(1) amortized HashMap insert
  with hashing overhead)
- get: O(1) array index (was HashMap lookup)
- keys(): iterate dirty_keys Vec (was HashMap keys iterator)
- clear(): memset None over flat array (was HashMap::clear)
- resize() called in build_ui to size array to viewport
- All 185 tests + 14 scenarios pass
- Benchmark: 95.1 FPS (was 61.9 — 53% improvement)
2026-06-22 13:32:47 +03:00
Emil a16645d2d8 feat: procedurally scalable UI — font_scale auto-computed from screen size
- UiLayer.font_scale: dynamically computed as (ui_height / 200).clamp(2, 6)
  Larger screens get bigger UI text automatically
- draw_text: renders each font pixel as font_scale×font_scale block
  (was 1×1, now 2×2 to 6×6 depending on screen size)
- text_width: now instance method, returns scaled width
- draw_health_bar: bar width scales with font_scale
- draw_character_panel: panel dimensions and row spacing scale
- draw_hud: bar height, row spacing, bar width scale
- draw_inventory_overlay: slot sizes scale with font_scale
- draw_messages: line spacing scales with font_height
- draw_death_screen: centered text uses scaled text_width
- All 185 tests + 14 scenarios pass
- Benchmark: 61.9 FPS (was 82.7 — 25% regression from more set_alpha calls)
2026-06-22 13:23:48 +03:00
Emil f35f56b970 perf: CA optimization — 18 FPS → 83 FPS (4.6× speedup) with ×5 world scale
- Cache active_chunks(): one allocation instead of 5 per tick
- heat_transfer/gas_step/pressure_step: direct chunk array access
  (chunk.temps[idx], chunk.gas_*[idx], chunk.pressure[idx]) instead of
  grid.get_temp()/set_temp() (eliminates 5 chunk-lookups per cell)
- In-place updated_this_tick reset via get_chunk_mut (no Cell copy, no mark_dirty)
- light_step: gather sources first (one pass), skip if no sources,
  reduced radius (lava 25→12, fire 15→6), frequency 10→20 ticks
- gas_step: skip chunks with no gas (gas_density all 0)
- pressure_step: skip chunks with all atmospheric (128), add mark_dirty
- pre_dirty: HashMap instead of Vec (O(1) lookup vs O(n) linear search)
- Cross-chunk heat transfer: edge_temps pre-loaded before mutable borrow
- Spread chunk save I/O: 1 chunk/tick instead of all every 60 ticks
- All 185 tests + 14 scenarios pass
- Benchmark: 82.7 FPS avg, p99 86ms (was 18 FPS, p99 442ms)
2026-06-22 08:55:40 +03:00
Emil 4c9a074bb0 feat: world scale ×5 — trees, pools, walls, rooms, corridors all 5× larger
- WORLD_SCALE=5 constant in worldgen.rs, adjustable via single constant
- Trees: trunk 30 cells tall, canopy radius 10 (was 6/2)
- Pools: radius 10-30 (was 3-6)
- Walls: height 10-35, width 5 (was 3-7/2)
- Sand dunes: width 20-80 (was 6-16)
- BSP rooms: min 20×20, corridors width 5 (was 9×9/2)
- Surface terrain: amplitude ×5 (±20 vs ±4)
- Dirt layer depth: 40 cells (was 8)
- Spawn offsets ×5 for goblins/slimes
- Dirty rects cleared after world generation to avoid CA processing static terrain
- Chunk activation by dirty rects only (not modified flag)
- Dirty rect size limit 4096 cells to prevent CA spikes
- CA fill_prob for caves reduced 0.45→0.38 for larger open spaces
- All 185 tests + 14 scenarios pass
- Benchmark: ~18 FPS (regression from 85 FPS due to larger world features)
- Performance optimization deferred to separate pass
2026-06-22 08:27:15 +03:00
Emil 4c1e3987ec fix: audio sounds now overlap instead of queueing sequentially
Each sound creates its own Sink via OutputStreamHandle and calls detach(),
so sounds play simultaneously and self-clean when finished.
2026-06-22 00:06:30 +03:00
Emil 9ce45cff4a feat: audio system with 15 procedurally generated sounds via soundgen
- AudioEngine with rodio (WAV playback, throttling, volume, toggle)
- 15 embedded sounds: jump, shoot, hit, explosion, death, pickup, descend,
  powerup, step, lava_bubble, acid_sizzle, water_splash, fire_crackle,
  ui_click, goblin_growl
- Sound events: shoot, hit, explosion (fireball), pickup, descend, powerup,
  jump, combat hit, ambient material sounds (lava/fire/acid/water)
- M key toggles audio in GPU modes
- Ambient sounds: scans 15-cell radius around player, plays throttled sounds
  for nearby lava/fire/acid/water
- AudioEngine gracefully degrades when no audio device available
- All 185 tests + 14 scenarios pass
2026-06-22 00:03:27 +03:00
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
Emil a5436897ed wip: current project state 2026-06-21 22:27:20 +03:00
Emil 88892db493 fix: bigger UI panels with more spacing between text lines
Character panel:
- Width 44 -> 52, height 44 -> 62
- Text rows spaced 8 apart (was 6) — no more squished text
- HP/XP bars 38 wide (was 32)
- Stats with double space between pairs
- Inventory section at bottom with more room

HUD bar:
- Height 18 -> 28
- Rows at y_top+2, +12, +22 (was +1, +7, +13)

Inventory overlay:
- Slots 12x10 (was 8x6), gap 3 (was 2)
- Panel auto-sizes larger
- Click hitboxes in main.rs updated to match

Fixed depth_shown_in_hud test for new HUD height.

All 171 tests + 14 scenarios pass.
2026-06-21 19:03:14 +03:00
Emil a2db99f900 feat: toggleable inventory overlay with mouse navigation
- Press Tab to open/close inventory overlay
- 4x2 grid of item slots (8 slots) centered on screen
- Each slot shows 2-char glyph + item name label
- Mouse hover highlights slot with golden border
- Hover shows action hint (Use/Equip/Equipped)
- Left-click slot: use/equip item at that index
- Right-click slot: drop item at that index
- Equipped items shown at bottom of panel
- Character panel hidden when inventory is open (saves space)
- Shooting disabled while inventory is open (prevents accidental fire)
- Mouse position tracked in UI coordinates for slot detection

All 171 tests + 14 scenarios pass.
2026-06-21 18:51:45 +03:00
Emil 07c7b88521 feat: items render as multi-cell pictures in graphics mode
- Item.shape() returns Vec<(dx, dy, color)> per type:
  Sword: 4-tall blade + crossguard + handle (6 cells)
  Dagger: blade + 2-cell handle (3 cells)
  Bow: curved arc + bowstring (4 cells)
  Leather/Plate Armor: 2x2 body shape (4 cells)
  Shield: 2x2 shield shape (4 cells)
  Health/Mana Potion: bottle with cork + body (4 cells)
  Food: 3-cell round shape
  Scroll: 3-cell horizontal scroll
- Graphics renderer paints all shape cells per item
- ASCII mode still uses 2-char glyph encoding in UI
- Items in world have visual shape, not just single colored cell

All 171 tests + 14 scenarios pass.
2026-06-21 18:39:58 +03:00
Emil 7a225b4320 feat: 2-char item glyphs in ASCII UI inventory
- Item.display_glyph() returns [char; 2] — base symbol + type code:
  Dagger  /)  Sword  ||  Bow  ){  LeatherArmor  [L  PlateArmor  {P
  Shield  OS  HealthPotion  !H  ManaPotion  !M  Food  %F  Scroll  ?S
- Item.display_string() returns the 2-char combo as a string
- Character panel inventory now shows 2-char glyphs with dimmed second char
- HUD weapon/armor display uses 2-char glyphs instead of truncated names
- Items in world grid still use single display_char() (1 cell = 1 char)

All 171 tests + 14 scenarios pass.
2026-06-21 18:35:38 +03:00
Emil 096f2e90a9 feat: multi-spectrum AI observation + tape recording system
Spectrum system (src/ai/spectrum.rs):
- 6 spectrums: materials, temperature, light, entities, density, velocity
- Each renders a different ASCII view of the same world state
- materials: material display chars + entity chars
- temperature: heat levels as .-=+oxX#
- light: brightness as .:+oO*
- entities: entity positions only, background dots
- density: material density as .:+oO#
- velocity: entity speed + CA activity as .:+oO,

Tape recording (src/ai/tape.rs):
- --mode tape CLI with --tape-interval, --tape-output, --tape-json
- Records all 6 spectrums + world state every N ticks
- Each frame: tick, depth, kills, score, camera, player HP/pos, entity count
- Outputs human-readable text and JSON formats

Pipe protocol additions:
- get_spectrum: single spectrum view by name
- get_all_spectrums: all 6 spectrums in one response
- Response.spectrums field added

Architecture priorities updated:
- Graphics mode is PRIMARY renderer
- ASCII mode is DEBUG backend
- Terminal mode is LEGACY
- AI uses multi-spectrum ASCII layers for observation

All 171 tests + 14 scenarios pass.
2026-06-21 18:27:23 +03:00
Emil 586487e61c feat: asymmetric characters that flip when turning
- Player template is now asymmetric:
  - Weapon arm extends forward (right side, x=3..4)
  - Hair flows backward (left side, x=-3..-4)
  - Cape drapes to the left (x=-3..-4)
  - Front hand colored as 'weapon' (bright steel)
- Goblin template asymmetric: weapon arm extends forward (x=4)
- Entity.flip_facing() mirrors all rest_offsets X and body positions
- Entity.facing_right field (default true)
- Game loop detects mouse crossing player center X and calls flip_facing()
- Verlet constraints adapt smoothly to mirrored positions
- 'weapon' label added to preview_ascii

All 171 tests + 14 scenarios pass.
2026-06-21 18:14:34 +03:00
Emil e50df7b51e feat: detailed characters + Noita-style mouse aiming
Character detail:
- Player: 68 body parts (was 42) — eyes, hair, cape, hat, gloves
- Goblin: 56 body parts (was 39) — red eyes, pointed ears, teeth, skin tones
- New body labels: eye, hair, cape, tooth, ear
- Updated preview_ascii label map for new part types

Mouse aiming (Noita-style):
- Track mouse position via CursorMoved events
- Track left-click via MouseInput events
- Left-click shoots projectile toward mouse cursor direction
- Player faces left/right based on mouse X relative to player screen X
- Keyboard shoot (hjkl) still works as fallback
- Player.facing_right field added
- WindowInput: mouse_x, mouse_y, mouse_left, shoot_mouse fields
- on_mouse_move(), on_mouse_button() handlers
- clear_keys() also clears mouse state on focus loss

All 171 tests + 14 scenarios pass.
2026-06-21 18:07:46 +03:00
Emil 876eb031df feat: difficulty scaling by depth + new items (bow, shield, mana potion)
Difficulty scaling:
- Spawn rates increase with depth (goblins every 30-10 ticks, slimes every 45-15)
- Enemy count caps increase with depth (goblins 3-8, slimes 2-5)
- Enemy health scales with depth (+5 hp/goblin, +3 hp/slime per depth)
- Goblin strength stat scales with depth

New items:
- Bow: ranged weapon, +4 damage bonus
- Shield: armor, +3 armor bonus
- Mana Potion: consumable, heals 20 hp
- Items spawn in world gen: sword, bow, shield, health potion, mana potion, leather armor

All 171 tests pass.
2026-06-21 16:34:57 +03:00
Emil 9778154348 feat: improved world gen (biomes, caves, trees) + goblin ranged combat
World generation:
- 4 biomes: grassland, grassland, dirt, stone (left to right)
- Multi-octave terrain noise (base + detail sine waves)
- Random caves carved into underground (8 circular caves)
- Trees with wood trunks and grass canopies (5 trees)
- Stone stalactite formations underground (6 pillars)
- Preserved all material types (water, lava, sand, acid, stone wall)

Goblin AI:
- Goblins now shoot arrows at player from 20-60 cell range
- Arrow fires every 80 ticks with directional velocity
- Uses new spawn_arrow() helper on ProjectileManager

All 171 tests + 14 scenarios pass.
2026-06-21 16:30:29 +03:00
Emil 0b20f3ef9b fix: 15+ bug fixes — item dup, descend, slime AI, combat, camera, UI
CRITICAL:
- Fix use_item duplicating weapons/armor (not removed from inventory)
- Fix unwrap() panic in update_ragdoll_entity
- Fix descend proceeding without stairs when player entity missing

HIGH:
- Fix projectile hardcoded id==0 preventing enemy projectiles hitting player
- Fix previously equipped item lost on re-equip (returned to inventory)
- Fix i32::abs() overflow panic in background_color (use wrapping_abs)

MEDIUM:
- Fix slime AI ignoring vertical direction to player
- Fix combat damage applied to dead player
- Fix camera movement keys non-functional (added cam_offset_x/y)
- Fix Unicode emoji status icons (replaced with ASCII F/P/I/B)
- Fix Unicode UI chars (█░·─│┌┐└┘◆■ → ASCII #-./|++++*#)
- Add missing char_bitmap glyphs (+, =, >, <, *, %, #, |, @, _)

LOW:
- Fix draw_messages writing to negative y coordinates
- Fix WindowInput losing key state on focus loss (clear_keys on Focused(false))
- Fix compute_lighting using full grid scan instead of gather_sources_in_range

All 171 tests + 14 scenarios pass.
2026-06-21 16:24:51 +03:00
Emil 357db17c2f feat: GPU optimization — lighting, viewport CA, benchmark mode, 531 FPS
- Resolution: 8x8 world cells, 2x2 UI cells (UI_SCALE=4)
- GPU lighting: vertex-shader computed, light source list buffer (max 64)
  instead of O(N×R²) grid scan, O(N×S) per cell
- Viewport-aware CA: iterate only active chunks, not all 250×250
- Flat array entity/item/shadow maps instead of HashMaps
- Flat 128-entry ASCII atlas array instead of HashMap lookup
- Partial grid upload: viewport + 30-cell margin only
- Pre-allocated viewport arrays in renderer structs (zero alloc/frame)
- Skip CPU lighting for GPU modes (pass None)
- Benchmark mode: --mode benchmark with per-subsystem timing
- GpuLightSource struct, light_count in push constants
- gather_sources_in_range() for viewport-scoped source gathering

Benchmark (600 ticks, release):
  Graphics: 531 FPS (was 386, +38%), render 1013us (was 1699us, -40%)
  ASCII:    402 FPS (was 313, +28%), render 1502us (was 2346us, -36%)

All 171 tests + 14 scenarios pass.
2026-06-21 16:09:45 +03:00
Emil c25cae9a32 docs: update AGENTS.md — slime, combat, BodyTemplate, 122 tests 2026-06-21 10:47:20 +03:00
Emil 8ac2470546 docs: update Phase 1 — player is ranged (projectiles), not melee
Recorded design decision: player primarily shoots projectiles,
melee is secondary (enemy contact damage only).
Marked completed items: contact damage, knockback, goblin AI, slime AI.
Added projectile system spec: arrows, fireballs, magic bolts.
2026-06-21 10:44:37 +03:00
Emil 2078e3f11e feat: slime enemies — jump AI, contact damage, combat system
New entity: Slime (EntityKind::Slime)
- 19 parts: blobby body (3x5) + 2 glowing eyes
- Green translucent colors, brighter center
- HP: 25, spawns every 45 ticks (max 2 alive)

Slime AI (update_slime_ai):
- Jumps toward player every 60 ticks when within 40 cells
- Jump power scales with proximity (closer = stronger)
- Pauses horizontally between jumps (50/50 hop-stop cycle)
- Uses set_horizontal_vel + set_vertical_vel (vector movement)

Combat system (update_combat):
- AABB overlap check between player and all alive enemies
- Goblin: 8 damage per 20 ticks on contact
- Slime: 5 damage per 20 ticks on contact
- Knockback: player pushed away from enemy on hit
- Player.take_damage() called, death possible from enemies

World gen: try_spawn_slime() spawns at surface, 18 cells from player
Renderers: slime rendered as 's' in ascii/terminal, green blob in graphics

6 new tests (122 total, 0 failures)
2026-06-21 10:42:16 +03:00
Emil e287b0d22a feat: larger detailed entities — 47 parts, hats, belts, ears
Entities are now ~3x larger and more detailed:

Player (47 parts, was 15):
  - Hat: 3 top + 2 brim (dark purple)
  - Head: 3x2 face (lavender skin)
  - Belt: golden accent across torso
  - Shirt: 3x2 torso + arms out to 3 cells each side
  - Hands: 4 cells (2 per arm, pale lavender)
  - Pants: 3 wide hips + 5 wide legs + split
  - Boots: 3 cells + 2 foot tips (dark purple)
  - half_w=4, half_h=6 (was 2.5x3.5)

Goblin (41 parts):
  - Pointed ears (2 cells)
  - Ragged shirt (green, darker shade)
  - Bare feet (green skin, no boots)
  - Loincloth (dark green)

Auto-constraints: all parts connected to all others (n^2)
  - Simplified from manual constraint lists
  - Works for any template shape
  - Ragdoll will look natural — all parts hold together

macro_rules! p! for compact part definitions
117 tests, 0 failures
2026-06-21 10:34:28 +03:00
Emil 61bf21bda0 feat: BodyTemplate system — data-driven entity editor for AI
BodyTemplate: JSON-serializable entity body definition.
- parts: Vec<BodyPart> with x, y, color, label
- constraints: Vec<(usize, usize)> connecting parts
- half_w/half_h/radius: collider and physics params

Built-in templates:
- humanoid_player: purple wizard (15 parts)
- humanoid_goblin: green (15 parts)
- boulder: 4x4 rock (16 parts)

API for AI agents:
- BodyTemplate::to_json() / from_json() — serialize/deserialize
- BodyTemplate::preview_ascii() — text preview of silhouette
- BodyTemplate::apply_to(&mut entity, cx, cy) — build entity from template
- template_for_kind(EntityKind) — get template by entity type
- Custom templates: create any shape (snake, dragon, boss) from code or JSON

Entity::build_humanoid() now delegates to template_for_kind().apply_to()
— single source of truth for body layout.

7 new tests for template system (116 total, 0 failures)
2026-06-21 10:27:37 +03:00
Emil 6504a619f6 feat: profile-view humanoid entities, purple player in clothes
Redesigned entity as side-profile silhouette (Noita-style):

     O          head (skin tone)
     |
    -|-         shoulders with arms out
     |
    -|-         arms lower
     |
    /|\         hips, legs split
    /|    boot boot boot

15 bodies (was 23), narrower profile shape:
  - head: 2 cells (skin)
  - torso: 3 cells (shirt/clothing color)
  - arms: 4 cells (hands, 2 per side)
  - hips: 1 cell (pants)
  - legs: 3 cells (pants)
  - boots: 3 cells

Player color scheme (purple wizard):
  - skin: pale lavender (220,200,240)
  - shirt: purple (140,80,200)
  - pants: dark purple (90,50,150)
  - boots: dark (60,35,100)
  - hands: pale lavender (210,185,235)

Goblin: green skin, dark green rags
Corpse: desaturated grey-brown

109 tests, 0 failures
2026-06-21 10:20:09 +03:00
Emil 5cd1a5e197 feat: Noita-style humanoid entities with per-part colors
Redesigned entity from solid 5x5 block to humanoid silhouette:

Shape (23 bodies, was 27):
     H H H        (head, 3 wide)
     H H H
   A T T T A      (shoulders + arms, 5 wide)
   A T T T A
     T T T        (torso, 3 wide)
     L   R        (legs, split with gap)
     L   R

Per-part colors (Noita-inspired):
- Player: head=bright yellow, torso=golden, arms=amber, legs=dark gold
- Goblin: head=bright green, torso=green, arms=olive, legs=dark green
- Corpse: desaturated browns

SubBody now has color: [u8; 4] field, set by build_humanoid.
All renderers (terminal, ascii, graphics) use b.color directly.
Fire effect: flickering orange overlay on burning entities.

half_w adjusted from 3.5 to 3.0, half_h from 2.5 to 3.5
(taller shape, narrower than old block).

109 tests, 0 failures
2026-06-21 10:13:22 +03:00
Emil 586c25f44c chore: gitignore graphify-out/ 2026-06-21 10:04:37 +03:00
Emil 285688bcad feat: smaller cells (10x10px) for higher detail
Cells reduced from 16x16 to 10x10 pixels. Same window size now
shows more of the world — finer detail, more cells visible.
Adaptive viewport recalculates grid_w/grid_h from new cell size.

109 tests, 0 failures
2026-06-21 10:04:21 +03:00
Emil 8b055ac37f docs: add AGENTS.md for OpenCode sessions 2026-06-21 01:42:57 +03:00
Emil edf3eb8ccc docs: update PLAN.md — Phase 4 done, new features, cleanup reflected
Updated:
- Current state: 3 render modes (terminal/ascii/graphics), per-cell color,
  adaptive viewport, slope stepping, GpuRenderer trait
- Numbers: ~5964 lines, 40+ commits
- Architecture: Cell = material + temp + fg + bg + variant, 3 render modes
- Locked decisions: 3 render modes, per-cell color, square cells, slope
  stepping, GpuRenderer trait
- Cross-platform: removed softbuffer from deps table, updated fallback
  (→ terminal instead of → softbuffer)
- File structure: vulkan.rs (ASCII), graphics.rs (cells), no legacy files
- Phase 4: marked DONE, all checkboxes updated
- Milestones: 0.2 done (Vulkan renderers), renumbered phases
- Binary size: ~8MB with Vulkan
2026-06-21 01:39:25 +03:00
Emil 68fe0b87dd refactor: code cleanup — dead code, warnings, duplication
Dead code removed (21 methods, 4 fields, 3 constants):
- Cell: MaterialId::ALL, MaterialId::from_u8 (unsafe transmute)
- Grid: get_mut, clear, fill_rect, swap, dump_region, next buffer field
- Entity: move_center, EntityManager::iter_mut
- Player: move_dir field, entity_mut
- VerletSolver: step, SubBody::add_vel, SubBody::apply_force
- CellularAutomaton: tick_count
- InputHandler: release_all, poll, Action::None
- WindowInput: clear
- GameSession: perform_action_and_step, is_recording, grid_mut
- ReplayPlayer: from_recording
- Material: empty()
- VulkanRenderer: tick_count field
- MaterialBrush: name()

Warnings fixed:
- Remove unused MaterialRegistry imports from renderers
- Remove unused reg variables in terminal/vulkan/graphics
- Remove unused water_surface in game.rs
- Remove unused p/y_death in tests
- Remove unused qf_slice in graphics.rs

Duplication eliminated:
- main.rs: run_ascii_mode + run_graphics_mode → generic run_gpu_mode<R: GpuRenderer>
  ~140 lines of duplicated event loop code removed
- GpuRenderer trait unifies VulkanRenderer and GraphicsRenderer API

Unsafe code fixed:
- rand_u8: static mut + unsafe → AtomicU8 + fetch_add (thread-safe)

Module cleanup:
- world/mod.rs: removed all unused re-exports
- physics/mod.rs: removed all unused re-exports
- entity/mod.rs: removed unused Entity/EntityId re-exports

Result: ~6500 → ~5964 lines, 0 non-deprecation warnings, 109 tests pass
2026-06-21 01:36:25 +03:00
Emil d889e24cf6 feat: store color directly in Cell (reality layer)
Each cell now stores its own fg/bg color inline, instead of looking
up from MaterialRegistry every frame.

Cell struct:
- fg: [u8; 3] — foreground color (for rendering)
- bg: [u8; 3] — background color (for rendering)
- temp: f32 — temperature (already there)
- material: MaterialId — for physics property lookup
- variant: u8 — visual/behavioral variant
- updated_this_tick: bool — CA flag

Colors are copied from MaterialRegistry at Cell::new() time.
This allows per-cell color variation in the future (water depth
shading, lava gradient, damaged materials, etc.) without registry
changes.

MaterialRegistry still stores physics properties (density, solid,
liquid, flammable, etc.) — looked up only during CA/physics steps,
not during rendering.

Renderers (terminal, ascii, graphics) updated to use cell.fg/cell.bg
directly — no more registry lookup in render path.

Cell size: ~16 bytes (was ~12). 250x250 grid = 1MB (was 750KB).
126 tests, 0 failures
2026-06-21 01:21:38 +03:00
Emil 2eb9de6502 fix: use window inner_size() for resize on Wayland
Wayland surface always reports current_extent as 0xFFFFFFFF (undefined),
meaning the swapchain size is determined by the application, not the
surface. Previous code returned early when this happened, so resize
never worked — cells stretched to fill the window.

Fix: when current_extent is 0xFFFFFFFF, use winit's window.inner_size()
to get the actual pixel dimensions. This works on both X11 and Wayland.

Both renderers (ascii + graphics) updated.
109 tests, 0 failures
2026-06-21 01:17:32 +03:00
Emil 57a88fd41e feat: adaptive viewport — window resize expands camera, no stretching
Both renderers (ascii + graphics) now support dynamic window resize:

- check_resize() called at start of every render() frame
- Compares surface capabilities with current swapchain extent
- If changed: recreates swapchain, image views, framebuffers, command buffers
- Recalculates grid_w/grid_h from new extent / cell_size (16x16)
- Reallocates instance buffer if grid cell count changed
- Old swapchain properly destroyed after new one created
- Handles window minimize (skip if extent = 0)
- physical_device field added back to both renderers (needed for surface caps query)

main.rs: vw/vh queried every frame from renderer.grid_w()/grid_h()
instead of cached once at startup. Camera center uses dynamic dimensions.

Result: resize window → more/fewer cells visible, cells stay 16x16 pixels.
109 tests, 0 failures
2026-06-21 01:13:34 +03:00
Emil 3235957a26 feat: square cells (16x16), slope stepping collision
Square cells:
- CHAR_W and CHAR_H both 16 (was 8x16, non-square)
- Window size updated to 160*16 x 50*16 = 2560x800
- Applies to both ascii and graphics renderers

Slope stepping collision:
- Before resolving X, check if new position overlaps solid
- If overlap, try stepping up 1 cell — if clear, snap up (walk up slope)
- If can't step up, resolve X normally (wall block)
- aabb_overlaps_solid() helper for fast overlap check
- Player can now walk up 1-cell-high steps and slopes

109 tests, 0 failures
2026-06-21 01:06:37 +03:00
Emil 45d2767719 fix: clean up dead code warnings in vulkan.rs
Remove unused fields from VulkanRenderer: pixel_w, pixel_h,
physical_device, queue_family, swapchain_format. Mark unused
device param in create_swapchain as _device.

Only winit deprecation warnings remain (create_window, run).
2026-06-21 01:02:20 +03:00
Emil 019ae8f090 feat: three render modes — terminal, ascii, graphics
Three render modes:
- --mode terminal: pure ANSI ASCII in terminal
- --mode ascii: Vulkan window with ASCII characters (glyph atlas)
- --mode graphics: Vulkan window with colored cells (no glyphs, each
  material = unique base color, no lighting yet)

Graphics renderer:
- Same Vulkan pipeline as ASCII but without glyph atlas
- Simpler shaders: graphics.vert/frag just output instance color
- No descriptor set, no texture sampling
- Each cell = colored quad, material color fills entire cell
- Entities rendered as colored shapes (player=yellow, goblin=green)

Default mode changed to --mode ascii
109 tests, 0 failures
2026-06-21 00:59:19 +03:00