Commit Graph
59 Commits
Author SHA1 Message Date
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
Emil 020914ab99 refactor: --mode window is now Vulkan, remove softbuffer fallback
- --mode window = Vulkan renderer (was softbuffer CPU)
- --mode vulkan removed (merged into --mode window)
- softbuffer dependency removed
- log dependency removed (unused)
- Old window.rs (softbuffer CPU renderer) deleted
- Fallback: if Vulkan init fails, falls back to --mode terminal
- 109 tests, 0 failures
2026-06-21 00:53:20 +03:00
Emil 8b79b99ab2 docs: update PLAN.md — two render modes, cross-platform, current state
Render modes:
- --mode window: ASCII mode (Vulkan instanced, glyph atlas) — DONE
- --mode graphics: Graphics mode (colored cells, no glyphs) — FUTURE
  Each material gets unique base color, no lighting yet (Phase 4b)
- --mode terminal: ANSI ASCII — always available

Cross-platform section added:
- All deps cross-platform: winit, ash, ash-window, softbuffer, fontdue
- ash_window auto-selects surface extension per platform
- Layout-agnostic input via winit PhysicalKey
- Render mode availability table per platform
- Vulkan surface extensions per OS

Updated:
- Current state: 109 tests, 14 scenarios, Vulkan renderer working
- Numbers: ~6500 lines, 30+ commits
- Testing strategy: detailed test counts per category
- File structure: window.rs, window_input.rs, graphics.rs
- Milestones: 0.5 = graphics mode + Phase 4b
- Performance: Vulkan ~14ms measured
2026-06-21 00:48:47 +03:00
Emil fdc1f416c7 fix: platform-agnostic Vulkan instance extensions
Use ash_window::enumerate_required_extensions() instead of hardcoded
VK_KHR_xlib_surface / VK_KHR_wayland_surface. This automatically
selects the correct surface extension per platform:
- Linux X11: VK_KHR_xlib_surface
- Linux Wayland: VK_KHR_wayland_surface
- Windows: VK_KHR_win32_surface
- macOS: VK_EXT_metal_surface (via MoltenVK)

No platform-specific code in the renderer — fully cross-platform.

109 tests, 0 failures
2026-06-21 00:43:26 +03:00
Emil 139b60b8af fix: flip Y in vertex shader, dynamic viewport/scissor, clean debug
- Fix: Y coordinate was inverted (1.0 - 2.0*y → 2.0*y - 1.0)
  Vulkan clip space Y is already down-up, no need to flip
- Fix: dynamic viewport/scissor with viewport_count(1) + scissor_count(1)
  Required by validation layer even with dynamic state
- Clean: removed debug eprintlns, restored default clear color
- Shaders recompiled to SPIR-V
2026-06-21 00:41:34 +03:00
Emil 6e9e90fb34 feat: Vulkan renderer with instanced rendering (Phase 4)
VulkanRenderer using ash 0.38 + winit:
- Instance with KHR_surface + xlib + wayland extensions
- Physical device selection (first GPU with graphics+present)
- Swapchain (FIFO present mode, B8G8R8A8_UNORM)
- Render pass with color attachment
- Graphics pipeline with instanced rendering:
  - Vertex buffer: unit quad (4 verts, 6 indices)
  - Instance buffer: 8000 CellInstance structs (32 bytes each)
  - 2 vertex bindings: per-vertex (quad pos) + per-instance (grid pos,
    atlas UV, fg/bg colors)
  - Push constants: screen size + cell size
- Glyph atlas: R8_UNORM texture, uploaded via staging buffer
  - fontdue rasterizes DejaVu Sans Mono at startup
  - Linear filtering sampler
- Descriptor set: combined image sampler for atlas
- 2 frames in flight with semaphores + fences
- Persistent mapped instance buffer (zero-copy per frame)

Shaders (pre-compiled SPIR-V):
- cell.vert: positions quad from instance data, maps to clip space
- cell.frag: samples atlas alpha, blends fg/bg

--mode vulkan: tries Vulkan, falls back to softbuffer window mode
109 tests, 0 failures
2026-06-21 00:34:01 +03:00
Emil 390eca9cda fix: winit + softbuffer for layout-agnostic input and better FPS
Replaced minifb with winit + softbuffer:

Input (layout-agnostic):
- winit uses PhysicalKey<KeyCode> which maps to physical key positions
- KeyCode::KeyA = physical A key, regardless of keyboard layout
- Works on Russian, Arabic, any layout — no key mapping needed
- HashSet<KeyCode> tracks pressed/released state
- Proper Press/Release events from OS, no timeout hacks

FPS improvements:
- Glyph atlas pre-built at startup (alpha bitmap, zero per-frame alloc)
- render_to_buffer: skip empty cells entirely (no draw call)
- blend_fast: bitshift instead of division for alpha blending
- Buffer fill via .fill() instead of nested loop
- copy_from_slice for pixel transfer to softbuffer (memcpy speed)
- ControlFlow::Poll for maximum frame rate

109 tests, 0 failures
2026-06-21 00:11:12 +03:00
Emil 65752b4cfe fix: window input uses get_keys() for held state, glyph atlas for FPS
Input fix: get_keys_pressed(No) only fires on initial press, not held.
Switched to get_keys() which returns all currently-down keys every frame.
Jump still edge-triggered via jump_was_down flag.

Performance fix: pre-build glyph atlas at startup (all ASCII chars
rasterized once into 128x256 alpha bitmap). No per-frame cloning.
draw_cell reads atlas alpha + blends fg/bg inline. Zero allocations
in render loop.

109 tests, 0 warnings
2026-06-21 00:01:28 +03:00
Emil 19bd883645 feat: window mode with minifb — direct keyboard polling, no terminal
New --mode window (now default):
- minifb creates a real OS window (no terminal needed)
- fontdue rasterizes DejaVu Sans Mono glyphs to pixel buffer
- ASCII characters rendered as colored pixels — same aesthetic
- Keyboard polled via get_pressed_keys() every frame:
  - Instant response, no terminal delay
  - Proper multi-key support (W+A strafing works)
  - No Release event hacks needed
  - No key repeat timeout hacks
  - Instant stop when key released

Controls: WASD/arrows, 1-9/0 paint, X erase, HJKL camera, Q/Esc quit
--mode terminal still available as fallback

109 tests, 0 warnings, 0 failures
2026-06-20 23:58:05 +03:00
Emil 6d98c09ece fix: request keyboard enhancement flags for proper Release events
Root cause of both issues: terminals don't send Release events by
default. When pressing W while holding A, terminal stops repeating A
(but doesn't send Release), so A times out and strafing breaks.
On key release, 400ms timeout means 400ms of extra movement.

Fix: PushKeyboardEnhancementFlags(REPORT_EVENT_TYPES) in terminal init.
This asks the terminal to send proper Press/Release/Repeat events for
ALL keys, not just special ones.

With Release events:
- Key release is instant (no 400ms timeout delay)
- Pressing W doesn't cancel A's held state (terminal sends Release
  only when A is actually released)
- A/D strafing works while W is held

Fallback: if terminal doesn't support enhancement flags (old xterm),
got_release flag stays false and 150ms timeout is used. Once any
Release event is received, all keys switch to Release-based mode
(infinite timeout, rely on actual Release events).

109 tests, 0 warnings, 0 failures
2026-06-20 23:48:03 +03:00
Emil 61e2cf1544 fix: dual-timeout held keys to fix terminal initial repeat delay
Root cause: terminals wait 300-500ms after initial Press before
sending the first Repeat event. With 60ms timeout, the key was
released during this gap, causing step-pause-accelerate pattern.

Fix: two timeout phases:
- After Press (no Repeat seen yet): 400ms timeout
  Covers terminal's initial repeat delay, key stays held
- After first Repeat: 80ms timeout
  Repeats arrive every ~30ms, 80ms is safe margin
  Quick release detection once repeating starts

Result: smooth continuous movement from first keypress, no gap
2026-06-20 23:43:31 +03:00
Emil 9458d265f0 fix: background input thread for responsive non-blocking input
Problem: event::poll(0ms) once per frame missed events between frames,
causing laggy/missed input. Terminal key repeat timing was unpredictable.

Fix: dedicated input thread that blocks on event::read() and pushes
all events to an mpsc channel. Game loop drains channel with try_iter()
each frame — gets every event without blocking, zero missed inputs.

- InputHandler::start() spawns background thread
- InputHandler::stop() cleans up on game exit
- try_iter() collects all accumulated events per frame
- Removed 16ms sleep (no longer needed, input doesn't block)
- Hold timeout reduced to 60ms (events arrive more reliably now)
- 109 tests, 0 warnings, 0 failures
2026-06-20 23:41:33 +03:00
Emil 3aa67140c8 feat: vector-based movement for responsive control and air strafing
Changed from physics-based (accumulate + damping) to vector-based:

- Horizontal velocity is SET directly, not accumulated
  - Press A → cvx = -0.5 (instant, no ramp-up)
  - Release → cvx = 0 (instant stop, no sliding)
  - No horizontal damping = precise control
- Air strafing: same horizontal speed in air as on ground
  - Can change direction mid-jump freely
- Vertical: gravity + minimal damping (0.99) for natural fall
  - Jump sets cvy directly, gravity pulls back
- move_speed 0.5 (was 0.3), max_vel 2.0 (was 1.0)
- Player: move_dir tracks current direction, stop_horizontal() clears velocity
- handle_input: calls stop_horizontal when no movement key held

109 tests, 0 warnings, 0 failures
2026-06-20 23:35:41 +03:00
Emil 4b66be755c docs: add Phase 1.5 UI Layer to PLAN.md
Non-destructive overlay architecture:
- UiLayer: sparse map of screen positions → (char, fg, bg)
- Composites on top of world render, never writes to grid/entities
- Health bars, HUD, message log, inventory overlay, minimap, menus
- Terminal: HashMap overlay after world draw
- Vulkan: separate render pass on top of world
- Pipe protocol exports world state only (not UI)
- Key principle: UI reads state, renders visuals, never mutates game
- Added: ui/ module in file structure, 0.25 milestone, locked decision
2026-06-20 23:32:29 +03:00
Emil e1eb9cc7cd fix: terminal input - timeout-based held keys, press-only jump
Terminals don't send Release events for most keys, so held keys
stayed active forever. Two problems:

1. W = infinite jump: Jump was in held_actions, fired every tick
   while held. check_on_ground returned true immediately after
   landing, causing auto-bounce.
   Fix: Jump fires only on initial Press event (jump_pressed flag),
   not on Repeat or held state.

2. A ignores D: When both A and D were held, both applied velocity
   and cancelled out. But terminal never sends Release for A, so
   D couldn't take over.
   Fix: Left/Right are mutually exclusive — most recently pressed
   key wins (compares last_seen timestamps).

3. Held key timeout: Keys expire after 80ms without a Repeat event,
   simulating Release for terminals that don't send it.

109 tests, 0 warnings, 0 failures
2026-06-20 23:30:37 +03:00
Emil 00c6b3bf87 fix: held-key input model for smooth movement
Problem: terminal key repeat sends one event, then pauses ~500ms,
then floods repeats. This caused stop-then-accelerate movement.

Fix: track held keys (Press/Release events), apply movement every
tick based on held state, not per-event. Jump still fires once per
press. Paint and quit are one-shot actions.

- InputHandler: held: Vec<HeldKey> tracks Left/Right/Jump/Camera
- update(): drains all pending events, updates held state
- held_actions(): returns active movement actions per tick
- handle_input: applies held actions every frame
- Camera pan reduced to 2/tick (was 5) for smoothness
- 109 tests, 0 warnings, 0 failures
2026-06-20 23:28:11 +03:00
Emil 61b5d551ae test: 109 Rust tests + 14 JSON scenarios, polish physics
Tests added:
- physics_materials.rs: 15 tests (fire, smoke, steam, grass, dirt, bone, wood, stone, lava temp)
- physics_interactions.rs: 12 tests (lava+water, fire spread, acid interactions, sand+water)
- player_controls.rs: 12 tests (move L/R, jump, double jump, rapid input, wait, continuous)
- collision_robust.rs: 10 tests (walls, ceiling, sliding, corridor, slope, dirt wall, unstick)
- ragdoll_death.rs: 7 tests (death transition, ragdoll fall, progressive damage, corpse)
- determinism.rs: 8 tests (same seed, replay exact, replay partial, recording, 100-tick)
- edge_cases.rs: 19 tests (boundary, stress 20 goblins, fill/clear/paint, gravity, camera)
- 6 new JSON scenarios (fire chain, lava pool, sand bury, entity fall, steam, acid wall)

Fixes:
- AABB resolve_aabb_y: add vertical overlap check (was pushing player down when jumping)
- AABB resolve_aabb_x: velocity-aware resolution (player was stuck against walls)
- check_on_ground: use fractional position check (was always true)
- jump_force 1.5 (was 0.8) for 27-body entity
- Gravity read from self.verlet (was from clone, SetGravity didn't work)
- u8 overflow in lava color (saturating_add)

109 Rust tests, 14 JSON scenarios, 0 warnings, 0 failures
2026-06-20 22:42:29 +03:00
Emil 8b1f5d4768 fix: u8 overflow in lava color calculation (saturating_add) 2026-06-20 22:14:26 +03:00
Emil bcbf474df6 docs: add 4 new phases to PLAN.md
- Phase 4b: Graphics over ASCII (lighting, particles, textures, post-processing)
- Phase 6: Multi-layer world (separate grids for air, pressure, temp, light)
- Phase 7: AI agent integration (LLM via pipe + RL with tensor state)
- Phase 8: Web arena (WASM render, WebSocket server, multiplayer, training pipeline)
- Updated: milestones to 0.8 + 1.0 Q3 2027
- Updated: open questions, file structure, testing strategy, perf targets
2026-06-20 22:10:33 +03:00
Emil 99d49dd213 docs: add PLAN.md with roadmap, architecture, milestones 2026-06-20 22:05:29 +03:00
Emil e954cacdb5 fix: add sleep to game loop, panic hook restores terminal
- 16ms sleep per frame prevents 100% CPU spin
- Panic hook restores terminal (disable raw mode, leave alternate screen)
  before printing panic message, so errors are visible
- Remove debug eprintlns
2026-06-20 22:03:30 +03:00
Emil 159b7c6f61 fix: surface renderer errors, clean up all warnings
- Renderer init/render/shutdown errors now printed instead of silently
  swallowed with let _ =
- Auto-fixed all unused imports and variables (cargo fix)
- Zero compiler warnings
2026-06-20 22:00:36 +03:00