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
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
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
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
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
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
- 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
- Replace per-body collision with single AABB collider for rigid entities
- Entity has half_w/half_h defining the collider box
- Separate axis resolution: X first (min-penetration push), then Y
(velocity-direction push to handle floor/ceiling correctly)
- Sliding: when hitting a wall, only X velocity is zeroed, Y continues
- check_on_ground uses AABB bottom edge, not individual bodies
- Fix: X resolution uses minimum penetration (was velocity-based,
which pushed player through walls when velocity was zero)
- Default scene: added water pool, lava pool, wood structure, sand dune,
acid pool, stone wall obstacle
- 28/28 tests passing
- Entity: 10 sub-bodies (was 16), radius 0.45 (was 0.7)
- Shape: 3x3 body rectangle + 1 hand cell on right side
- Much more compact, world appears larger relative to character
- 13 constraints (grid pattern + hand attachment)
- Spawn height adjusted for smaller body
- Entity: rigid body mode for alive entities (single object, no wobble)
- Center position + velocity, bodies positioned from rest offsets
- Collision: all 16 bodies checked, push applied to center
- Movement: velocity applied to center, all bodies move together
- Entity: ragdoll mode for corpses (loose Verlet constraints)
- kill() switches rigid->ragdoll, gives each body inherited velocity
- Constraints go slack (stiffness=0), bodies fall independently
- Player: move_left/right now applies velocity to entity center (was: head only)
- Physics: lava heat conductivity 0.05 (was 0.5), initial temp 1500 (was 1200)
Lava cooling threshold 400 (was 800), heat transfer rate 0.1 (was 0.5)
Lava stays hot long enough for entities to take damage
- Tests: 28/28 passing, updated for new lava and rigid body behavior
- Fix: rewrite resolve_grid_collision to push body out of solid cells
when center is inside (was only pushing to small radius from center)
- Fix: max velocity clamp (0.8) + 4x substepping prevents tunneling
- Fix: player/goblin spawn at terrain surface instead of mid-air
- Fix: gravity reduced from 0.3 to 0.08 for substepping
- Add: headless mode (--headless-ticks N) dumps grid state to file
- Add: velocity zeroing on collision for stable resting