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:
+42
-10
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user