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.
This commit is contained in:
Emil
2026-06-21 16:24:51 +03:00
parent 357db17c2f
commit 0b20f3ef9b
9 changed files with 107 additions and 53 deletions
+42 -10
View File
@@ -25,6 +25,8 @@ pub struct Game {
pub ui: UiLayer,
pub cam_x: i32,
pub cam_y: i32,
pub cam_offset_x: i32,
pub cam_offset_y: i32,
pub running: bool,
pub tick: u64,
pub fixed_dt: Duration,
@@ -56,6 +58,8 @@ impl Game {
ui: UiLayer::new(),
cam_x: 100,
cam_y: 100,
cam_offset_x: 0,
cam_offset_y: 0,
running: true,
tick: 0,
fixed_dt: Duration::from_millis(16),
@@ -239,8 +243,8 @@ impl Game {
let vw = renderer.viewport_w();
let vh = renderer.viewport_h();
let (px, py) = self.player.center(&self.entities);
self.cam_x = px as i32 - (vw as i32 / 2);
self.cam_y = py as i32 - (vh as i32 / 2);
self.cam_x = px as i32 - (vw as i32 / 2) + self.cam_offset_x;
self.cam_y = py as i32 - (vh as i32 / 2) + self.cam_offset_y;
self.build_ui(vw, vh);
@@ -520,12 +524,16 @@ impl Game {
}
pub fn descend(&mut self) {
if let Some(e) = self.player.entity(&self.entities) {
let foot_x = e.cx as i32;
let foot_y = (e.cy + e.half_h).ceil() as i32;
if self.grid.get(foot_x, foot_y).material != MaterialId::Stairs {
return;
let can_descend = match self.player.entity(&self.entities) {
Some(e) => {
let foot_x = e.cx as i32;
let foot_y = (e.cy + e.half_h).ceil() as i32;
self.grid.get(foot_x, foot_y).material == MaterialId::Stairs
}
None => false,
};
if !can_descend {
return;
}
self.depth += 1;
self.grid = Grid::new();
@@ -546,10 +554,18 @@ impl Game {
let item = self.player.inventory[index].clone();
let name = item.name();
if item.is_weapon() {
if let Some(old) = self.player.weapon.take() {
self.player.inventory.push(old);
}
self.player.weapon = Some(item);
self.player.inventory.remove(index);
self.ui.add_message(&format!("Equipped {}", name));
} else if item.is_armor() {
if let Some(old) = self.player.armor.take() {
self.player.inventory.push(old);
}
self.player.armor = Some(item);
self.player.inventory.remove(index);
self.ui.add_message(&format!("Equipped {}", name));
} else if item.is_consumable() {
let heal = item.heal_amount();
@@ -752,11 +768,16 @@ impl Game {
let jump_phase = tick % 60;
if jump_phase == 0 && dist < 40.0 {
let dir_x = dx / dist;
let _dir_y = dy / dist;
let dir_y = dy / dist;
let jump_power = 0.8 + (1.0 - dist / 40.0).min(0.5) * 0.5;
if let Some(e) = self.entities.all_mut().get_mut(idx) {
e.set_horizontal_vel(dir_x * jump_power);
e.set_vertical_vel(-jump_power * 0.8);
let vy = if dy < -1.0 {
-jump_power * 0.8
} else {
-jump_power * 0.6
};
e.set_vertical_vel(vy + dir_y * jump_power * 0.3);
}
} else if jump_phase == 30 {
if let Some(e) = self.entities.all_mut().get_mut(idx) {
@@ -768,6 +789,14 @@ impl Game {
fn update_combat(&mut self) {
let player_id = self.player.entity_id;
let player_alive = self
.entities
.get(player_id)
.map(|e| e.alive)
.unwrap_or(false);
if !player_alive {
return;
}
let player_center = self.player.center(&self.entities);
let player_half_w = self
.entities
@@ -1196,7 +1225,10 @@ impl Game {
substeps: u32,
) {
let grid = &self.grid;
let e = self.entities.all_mut().get_mut(idx).unwrap();
let e = match self.entities.all_mut().get_mut(idx) {
Some(e) => e,
None => return,
};
let alive = e.alive;
let bodies = &mut e.bodies;
let constraints = &e.constraints;
+9 -6
View File
@@ -229,6 +229,9 @@ fn run_gpu_mode<R: GpuRenderer>(title: &str) {
} => {
input.on_key_event(key_event.physical_key, key_event.state);
}
WindowEvent::Focused(false) => {
input.clear_keys();
}
_ => {}
},
Event::AboutToWait => {
@@ -296,16 +299,16 @@ fn run_gpu_mode<R: GpuRenderer>(title: &str) {
}
if input.cam_left {
game.cam_x -= 3;
game.cam_offset_x -= 3;
}
if input.cam_right {
game.cam_x += 3;
game.cam_offset_x += 3;
}
if input.cam_up {
game.cam_y -= 3;
game.cam_offset_y -= 3;
}
if input.cam_down {
game.cam_y += 3;
game.cam_offset_y += 3;
}
if let Some(brush_id) = input.paint {
@@ -344,8 +347,8 @@ fn run_gpu_mode<R: GpuRenderer>(title: &str) {
}
let (px, py) = game.player.center(&game.entities);
game.cam_x = px as i32 - (vw as i32 / 2);
game.cam_y = py as i32 - (vh as i32 / 2);
game.cam_x = px as i32 - (vw as i32 / 2) + game.cam_offset_x;
game.cam_y = py as i32 - (vh as i32 / 2) + game.cam_offset_y;
game.build_ui(vw, vh);
+2 -2
View File
@@ -143,7 +143,7 @@ impl Projectile {
}
}
}
if entity.id != self.owner && entity.id != 0 {
if entity.id != self.owner {
let before = entity.health;
entity.take_damage(self.total_damage());
ui.add_damage_number(
@@ -155,7 +155,7 @@ impl Projectile {
return;
}
if entity.id == self.owner || entity.id == 0 {
if entity.id == self.owner {
return;
}
+1 -1
View File
@@ -86,7 +86,7 @@ fn background_color(wx: i32, wy: i32, vy: i32, view_h: i32) -> [u8; 3] {
let base_g = (10.0 + t * 25.0) as u8;
let base_b = (25.0 + t * 35.0) as u8;
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).abs();
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).wrapping_abs();
if hash % 80 == 0 {
let brightness = (60 + (hash % 120) as u8).min(255);
return [brightness, brightness, brightness + 20];
+1 -1
View File
@@ -28,7 +28,7 @@ fn background_color(wx: i32, wy: i32, vy: i32, view_h: i32) -> [u8; 4] {
let base_g = (10.0 + t * 25.0) as u8;
let base_b = (25.0 + t * 35.0) as u8;
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).abs();
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).wrapping_abs();
if hash % 80 == 0 {
let brightness = (60 + (hash % 120) as u8).min(255);
return [brightness, brightness, brightness + 20, 255];
+1 -1
View File
@@ -121,7 +121,7 @@ pub fn compute_lighting(
let mut grid_light = LightGrid::new(view_w, view_h);
grid_light.clear(ambient);
let sources = gather_sources(grid);
let sources = gather_sources_in_range(grid, cam_x, cam_y, view_w, view_h, 30);
let cap = sources.len().min(32);
let radius_limit = 30u32;
+1 -1
View File
@@ -32,7 +32,7 @@ fn background_color(wx: i32, wy: i32, vy: i32, view_h: i32) -> [u8; 4] {
let base_g = (10.0 + t * 25.0) as u8;
let base_b = (25.0 + t * 35.0) as u8;
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).abs();
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).wrapping_abs();
if hash % 80 == 0 {
let brightness = (60 + (hash % 120) as u8).min(255);
return [brightness, brightness, brightness + 20, 255];
+4
View File
@@ -80,6 +80,10 @@ impl WindowInput {
}
}
pub fn clear_keys(&mut self) {
self.down_keys.clear();
}
pub fn update(&mut self) {
let keys = &self.down_keys;
+46 -31
View File
@@ -183,7 +183,7 @@ impl UiLayer {
};
for i in 0..width {
let x = screen_x + i;
let ch = if i < filled { '' } else { '' };
let ch = if i < filled { '#' } else { '-' };
let fg = if i < filled { color } else { [80, 80, 80] };
self.set(x, screen_y, ch, fg, [0, 0, 0]);
}
@@ -212,19 +212,19 @@ impl UiLayer {
let icon_x = sx + (e.half_w as i32 * UI_SCALE) + 1;
let mut icon_y = sy - (e.half_h as i32 * UI_SCALE) - 7;
if e.on_fire {
self.set(icon_x, icon_y, '🔥', [255, 100, 20], [0, 0, 0]);
self.set(icon_x, icon_y, 'F', [255, 100, 20], [0, 0, 0]);
icon_y += 1;
}
if e.poisoned {
self.set(icon_x, icon_y, '', [80, 255, 80], [0, 0, 0]);
self.set(icon_x, icon_y, 'P', [80, 255, 80], [0, 0, 0]);
icon_y += 1;
}
if e.frozen {
self.set(icon_x, icon_y, '', [120, 220, 255], [0, 0, 0]);
self.set(icon_x, icon_y, 'I', [120, 220, 255], [0, 0, 0]);
icon_y += 1;
}
if e.bleeding {
self.set(icon_x, icon_y, '', [255, 40, 40], [0, 0, 0]);
self.set(icon_x, icon_y, 'B', [255, 40, 40], [0, 0, 0]);
}
}
}
@@ -286,7 +286,7 @@ impl UiLayer {
[40, 40, 50]
};
let bg = [20, 20, 30];
self.set(start_x + dx, start_y + dy, '·', fg, bg);
self.set(start_x + dx, start_y + dy, '.', fg, bg);
}
}
for e in entities {
@@ -308,20 +308,20 @@ impl UiLayer {
}
}
for dx in 0..size {
self.set(start_x + dx, start_y - 1, '', [80, 80, 100], [0, 0, 0]);
self.set(start_x + dx, start_y + size, '', [80, 80, 100], [0, 0, 0]);
self.set(start_x + dx, start_y - 1, '-', [80, 80, 100], [0, 0, 0]);
self.set(start_x + dx, start_y + size, '-', [80, 80, 100], [0, 0, 0]);
}
for dy in 0..size {
self.set(start_x - 1, start_y + dy, '', [80, 80, 100], [0, 0, 0]);
self.set(start_x + size, start_y + dy, '', [80, 80, 100], [0, 0, 0]);
self.set(start_x - 1, start_y + dy, '|', [80, 80, 100], [0, 0, 0]);
self.set(start_x + size, start_y + dy, '|', [80, 80, 100], [0, 0, 0]);
}
self.set(start_x - 1, start_y - 1, '', [80, 80, 100], [0, 0, 0]);
self.set(start_x + size, start_y - 1, '', [80, 80, 100], [0, 0, 0]);
self.set(start_x - 1, start_y + size, '', [80, 80, 100], [0, 0, 0]);
self.set(start_x - 1, start_y - 1, '+', [80, 80, 100], [0, 0, 0]);
self.set(start_x + size, start_y - 1, '+', [80, 80, 100], [0, 0, 0]);
self.set(start_x - 1, start_y + size, '+', [80, 80, 100], [0, 0, 0]);
self.set(
start_x + size,
start_y + size,
'',
'+',
[80, 80, 100],
[0, 0, 0],
);
@@ -351,23 +351,23 @@ impl UiLayer {
}
for x in 0..w {
if (start_x + x) % 2 == 0 {
self.set_alpha(start_x + x, start_y, '·', border, bg, border_alpha);
self.set_alpha(start_x + x, start_y + h - 1, '·', border, bg, border_alpha);
self.set_alpha(start_x + x, start_y, '.', border, bg, border_alpha);
self.set_alpha(start_x + x, start_y + h - 1, '.', border, bg, border_alpha);
}
}
for y in 0..h {
if (start_y + y) % 2 == 0 {
self.set_alpha(start_x, start_y + y, '·', border, bg, border_alpha);
self.set_alpha(start_x + w - 1, start_y + y, '·', border, bg, border_alpha);
self.set_alpha(start_x, start_y + y, '.', border, bg, border_alpha);
self.set_alpha(start_x + w - 1, start_y + y, '.', border, bg, border_alpha);
}
}
self.set_alpha(start_x, start_y, '', border, bg, border_alpha);
self.set_alpha(start_x + w - 1, start_y, '', border, bg, border_alpha);
self.set_alpha(start_x, start_y + h - 1, '', border, bg, border_alpha);
self.set_alpha(start_x, start_y, '*', border, bg, border_alpha);
self.set_alpha(start_x + w - 1, start_y, '*', border, bg, border_alpha);
self.set_alpha(start_x, start_y + h - 1, '*', border, bg, border_alpha);
self.set_alpha(
start_x + w - 1,
start_y + h - 1,
'',
'*',
border,
bg,
border_alpha,
@@ -389,7 +389,7 @@ impl UiLayer {
};
self.draw_text(start_x + 2, start_y + 7, "HP", fg, 255);
for i in 0..32 {
let ch = if i < hp_filled { '' } else { '' };
let ch = if i < hp_filled { '#' } else { '-' };
let c = if i < hp_filled {
bar_color
} else {
@@ -404,7 +404,7 @@ impl UiLayer {
let xp_filled = (xp_ratio * 32.0).round() as i32;
self.draw_text(start_x + 2, start_y + 19, "XP", fg, 255);
for i in 0..32 {
let ch = if i < xp_filled { '' } else { '' };
let ch = if i < xp_filled { '#' } else { '-' };
let c = if i < xp_filled {
[80, 160, 240]
} else {
@@ -431,16 +431,16 @@ impl UiLayer {
let status = if let Some(p) = player {
let mut s = String::new();
if p.on_fire {
s.push('🔥');
s.push_str("[FIRE] ");
}
if p.poisoned {
s.push('☠');
s.push_str("[PSN] ");
}
if p.frozen {
s.push('❄');
s.push_str("[ICE] ");
}
if p.bleeding {
s.push('✚');
s.push_str("[BLD] ");
}
s
} else {
@@ -508,7 +508,7 @@ impl UiLayer {
}
for x in 0..screen_w {
if x % 2 == 0 {
self.set_alpha(x as i32, y_top - 1, '·', border, bg, 220);
self.set_alpha(x as i32, y_top - 1, '.', border, bg, 220);
}
}
@@ -524,7 +524,7 @@ impl UiLayer {
[255, 60, 60]
};
for i in 0..28 {
let ch = if i < hp_filled { '' } else { '' };
let ch = if i < hp_filled { '#' } else { '-' };
let c = if i < hp_filled { bar_fg } else { [70, 70, 90] };
self.set(10 + i, y_row1 + 4, ch, c, bg);
}
@@ -533,7 +533,7 @@ impl UiLayer {
}
let brush_color = brush_color(brush);
self.set(0, y_row2 + 4, '', brush_color, bg);
self.set(0, y_row2 + 4, '#', brush_color, bg);
let brush_text = format!(" {}", brush_name);
self.draw_text(1, y_row2, &brush_text, [200, 200, 140], 255);
@@ -573,6 +573,9 @@ impl UiLayer {
let mut yy = y;
let messages: Vec<(String, u32)> = self.messages.iter().rev().take(8).cloned().collect();
for (msg, life) in messages {
if yy < 0 {
break;
}
let fade = (life as f32 / 300.0).clamp(0.3, 1.0);
let fg = [
(200.0 * fade) as u8,
@@ -732,6 +735,18 @@ fn char_bitmap(c: char) -> Option<[u8; 5]> {
'?' => Some([0b111, 0b001, 0b011, 0b000, 0b010]),
'!' => Some([0b010, 0b010, 0b010, 0b000, 0b010]),
'.' => Some([0b000, 0b000, 0b000, 0b000, 0b010]),
',' => Some([0b000, 0b000, 0b000, 0b010, 0b100]),
'\'' => Some([0b010, 0b010, 0b000, 0b000, 0b000]),
'+' => Some([0b000, 0b010, 0b111, 0b010, 0b000]),
'=' => Some([0b000, 0b111, 0b000, 0b111, 0b000]),
'>' => Some([0b100, 0b010, 0b001, 0b010, 0b100]),
'<' => Some([0b001, 0b010, 0b100, 0b010, 0b001]),
'*' => Some([0b000, 0b101, 0b010, 0b101, 0b000]),
'%' => Some([0b101, 0b001, 0b010, 0b100, 0b101]),
'#' => Some([0b101, 0b111, 0b101, 0b111, 0b101]),
'|' => Some([0b010, 0b010, 0b010, 0b010, 0b010]),
'@' => Some([0b111, 0b101, 0b111, 0b101, 0b111]),
'_' => Some([0b000, 0b000, 0b000, 0b000, 0b111]),
_ => None,
}
}