From a66b4ac0cbec244ec8b81e0c306e69896d31d804 Mon Sep 17 00:00:00 2001 From: Emil Date: Sat, 20 Jun 2026 21:56:13 +0300 Subject: [PATCH] fix: AABB capsule collider for rigid entities, sliding on surfaces - 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 --- headless_dump.txt | 262 +++++++++++++-------------- src/entity/entity.rs | 6 + src/game.rs | 374 ++++++++++++++++++++++++--------------- tests/entity_movement.rs | 10 +- 4 files changed, 377 insertions(+), 275 deletions(-) diff --git a/headless_dump.txt b/headless_dump.txt index 70447a7..6f08e00 100644 --- a/headless_dump.txt +++ b/headless_dump.txt @@ -12,16 +12,16 @@ Camera: (85, 230) 34 35 36 -37 -38 -39 -40 @@@@@ -41 @@@@@@ -42 @@@@@@ -43 @@@@@ """"""""""""" -44""" @@@@@ "":::::::::::::""" -45:::""" """::::::::::::::::::"" -46::::::"" "":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T @@@@@ ... +41 T T ## @@@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -41,16 +41,16 @@ Entities: 1 34 35 36 -37 -38 -39 -40 @@@@@ -41 @@@@@@ -42 @@@@@@ -43 @@@@@ """"""""""""" -44""" @@@@@ "":::::::::::::""" -45:::""" """::::::::::::::::::"" -46::::::"" "":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T @@@@@ ... +41 T T ## @@@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -71,16 +71,16 @@ Alive entities: Player(hp=100, pos=(125.0, 242.11879)) 34 35 36 -37 -38 -39 -40 @@@@@ -41 @@@@@@ -42 @@@@@@ -43 @@@@@ """"""""""""" -44""" @@@@@ "":::::::::::::""" -45:::""" """::::::::::::::::::"" -46::::::"" "":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T @@@@@ ... +41 T T ## @@@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -101,16 +101,16 @@ Alive entities: Player(hp=100, pos=(125.0, 242.38817)) 34 35 36 -37 -38 -39 -40 @@@@@ -41 @@@@@@ -42 @@@@@@ -43 @@@@@ """"""""""""" -44""" @@@@@ "":::::::::::::""" -45:::""" """::::::::::::::::::"" -46::::::"" "":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T @@@@@ ... +41 T T ## @@@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -130,16 +130,16 @@ Alive entities: Player(hp=100, pos=(125.0, 242.79922)) 34 35 36 -37 -38 -39 -40 -41 @@@@@ -42 @@@@@@ -43 @@@@@@ """"""""""""" -44""" @@@@@ "":::::::::::::""" -45:::""" @@@@@ """::::::::::::::::::"" -46::::::"" "":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -154,21 +154,22 @@ Entities: 1 Alive entities: Player(hp=100, pos=(125.0, 243.3436)) === Tick 10 === +31 32 33 34 35 36 -37 -38 -39 -40 -41 -42 @@@@@ -43 @@@@@@ """"""""""""" -44""" @@@@@@ "":::::::::::::""" -45:::""" @@@@@ """::::::::::::::::::"" -46::::::"" @@@@@"":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -178,27 +179,27 @@ Alive entities: Player(hp=100, pos=(125.0, 243.3436)) 53???????????????????????????????????????????????????????????????????????????????? 54???????????????????????????????????????????????????????????????????????????????? 55???????????????????????????????????????????????????????????????????????????????? -56???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,244.0) bodies=27/27 on_fire=false" +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 244.01338)) +Alive entities: Player(hp=100, pos=(125.0, 243.5)) === Tick 12 === +31 32 33 34 35 36 -37 -38 -39 -40 -41 -42 @@@@@ -43 @@@@@@ """"""""""""" -44""" @@@@@@ "":::::::::::::""" -45:::""" @@@@@ """::::::::::::::::::"" -46::::::"" @@@@@"":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -208,27 +209,27 @@ Alive entities: Player(hp=100, pos=(125.0, 244.01338)) 53???????????????????????????????????????????????????????????????????????????????? 54???????????????????????????????????????????????????????????????????????????????? 55???????????????????????????????????????????????????????????????????????????????? -56???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,244.5) bodies=27/27 on_fire=false" +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 244.5)) +Alive entities: Player(hp=100, pos=(125.0, 243.5)) === Tick 14 === +31 32 33 34 35 36 -37 -38 -39 -40 -41 -42 @@@@@ -43 @@@@@@ """"""""""""" -44""" @@@@@@ "":::::::::::::""" -45:::""" @@@@@ """::::::::::::::::::"" -46::::::"" @@@@@"":::::::::::::::::::::::""" +37 TTTTT +38 T T +39 T T . +40 T T ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ @@ -238,28 +239,28 @@ Alive entities: Player(hp=100, pos=(125.0, 244.5)) 53???????????????????????????????????????????????????????????????????????????????? 54???????????????????????????????????????????????????????????????????????????????? 55???????????????????????????????????????????????????????????????????????????????? -56???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,244.5) bodies=27/27 on_fire=false" +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 244.5)) +Alive entities: Player(hp=100, pos=(125.0, 243.5)) === Tick 16 === +31 32 33 34 35 36 -37 -38 -39 -40 -41 -42 @@@@@ -43" @@@@@@ """"""""""""" -44:""" @@@@@@ "":::::::::::::""" -45::::""" @@@@@ """::::::::::::::::::"" -46:::::::"" @@@@@ "":::::::::::::::::::::::""" -47:::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""" +37 TTTTT +38 T T +39 T T . +40 T T ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ 50???????????????????????????????????????????????????????????????????????????????? @@ -268,28 +269,28 @@ Alive entities: Player(hp=100, pos=(125.0, 244.5)) 53???????????????????????????????????????????????????????????????????????????????? 54???????????????????????????????????????????????????????????????????????????????? 55???????????????????????????????????????????????????????????????????????????????? -56???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(124.5,244.5) bodies=27/27 on_fire=false" +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 -Alive entities: Player(hp=100, pos=(124.5, 244.5)) +Alive entities: Player(hp=100, pos=(125.0, 243.5)) === Tick 18 === +31 32 33 34 35 36 -37 -38 -39 -40 -41 -42 @@@@@ -43" @@@@@@ """"""""""""" -44:""" @@@@@@ "":::::::::::::""" -45::::""" @@@@@ """::::::::::::::::::"" -46:::::::"" @@@@@ "":::::::::::::::::::::::""" -47:::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""" +37 TTTTT +38 T T +39 T T . +40 T T ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ 50???????????????????????????????????????????????????????????????????????????????? @@ -298,28 +299,28 @@ Alive entities: Player(hp=100, pos=(124.5, 244.5)) 53???????????????????????????????????????????????????????????????????????????????? 54???????????????????????????????????????????????????????????????????????????????? 55???????????????????????????????????????????????????????????????????????????????? -56???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(124.5,244.5) bodies=27/27 on_fire=false" +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 -Alive entities: Player(hp=100, pos=(124.5, 244.5)) +Alive entities: Player(hp=100, pos=(125.0, 243.5)) === Tick 20 === +31 32 33 34 35 36 -37 -38 -39 -40 -41 -42 @@@@@ -43" @@@@@@ """"""""""""" -44:""" @@@@@@ "":::::::::::::""" -45::::""" @@@@@ """::::::::::::::::::"" -46:::::::"" @@@@@ "":::::::::::::::::::::::""" -47:::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""" +37 TTTTT +38 T T +39 T T . +40 T T ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 49################################################################################ 50???????????????????????????????????????????????????????????????????????????????? @@ -328,8 +329,7 @@ Alive entities: Player(hp=100, pos=(124.5, 244.5)) 53???????????????????????????????????????????????????????????????????????????????? 54???????????????????????????????????????????????????????????????????????????????? 55???????????????????????????????????????????????????????????????????????????????? -56???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(124.5,244.5) bodies=27/27 on_fire=false" +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 -Alive entities: Player(hp=100, pos=(124.5, 244.5)) +Alive entities: Player(hp=100, pos=(125.0, 243.5)) diff --git a/src/entity/entity.rs b/src/entity/entity.rs index 9ec671e..0fa3652 100644 --- a/src/entity/entity.rs +++ b/src/entity/entity.rs @@ -22,6 +22,8 @@ pub struct Entity { pub cy: f32, pub cvx: f32, pub cvy: f32, + pub half_w: f32, + pub half_h: f32, pub health: f32, pub max_health: f32, pub on_fire: bool, @@ -46,6 +48,8 @@ impl Entity { max_health: 100.0, on_fire: false, fire_timer: 0, + half_w: 3.5, + half_h: 3.0, } } @@ -106,6 +110,8 @@ impl Entity { self.cy = cy; self.cvx = 0.0; self.cvy = 0.0; + self.half_w = 3.5; + self.half_h = 2.5; let r = 0.5; let mat = match self.kind { diff --git a/src/game.rs b/src/game.rs index 56c259d..b3348ec 100644 --- a/src/game.rs +++ b/src/game.rs @@ -68,6 +68,79 @@ impl Game { } } + // Water pool (left side) + let water_x = 40; + let water_surface = (h as i32 - 3) - ((water_x as f32 * 0.1).sin() * 5.0) as i32; + let water_surface = water_surface.max(10).min(h as i32 - 3); + for x in water_x - 12..=water_x + 12 { + let s = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32; + let s = s.max(10).min(h as i32 - 3); + for y in s - 8..s { + if self.grid.get(x as i32, y).is_empty() { + self.grid.set_material(x as i32, y, MaterialId::Water); + } + } + } + + // Lava pool (right side) + let lava_x = 200; + for x in lava_x - 10..=lava_x + 10 { + let s = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32; + let s = s.max(10).min(h as i32 - 3); + for y in s - 5..s { + if self.grid.get(x as i32, y).is_empty() { + self.grid.set_material(x as i32, y, MaterialId::Lava); + } + } + } + + // Wood structure near center-left + let wood_x = 90; + let wood_surface = (h as i32 - 3) - ((wood_x as f32 * 0.1).sin() * 5.0) as i32; + let wood_surface = wood_surface.max(10).min(h as i32 - 3); + for y in wood_surface - 8..wood_surface { + self.grid.set_material(wood_x, y, MaterialId::Wood); + self.grid.set_material(wood_x + 4, y, MaterialId::Wood); + } + for x in wood_x..=wood_x + 4 { + self.grid.set_material(x, wood_surface - 8, MaterialId::Wood); + } + + // Sand dune (right of center) + let sand_x = 160; + let sand_surface = (h as i32 - 3) - ((sand_x as f32 * 0.1).sin() * 5.0) as i32; + let sand_surface = sand_surface.max(10).min(h as i32 - 3); + for dx in -8..=8 { + let pile_h = (8.0 - (dx as f32).abs()) as i32; + for dy in 0..pile_h { + let y = sand_surface - 1 - dy; + if self.grid.get(sand_x + dx, y).is_empty() { + self.grid.set_material(sand_x + dx, y, MaterialId::Sand); + } + } + } + + // Acid pool (far left) + let acid_x = 15; + for x in acid_x - 5..=acid_x + 5 { + let s = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32; + let s = s.max(10).min(h as i32 - 3); + for y in s - 4..s { + if self.grid.get(x as i32, y).is_empty() { + self.grid.set_material(x as i32, y, MaterialId::Acid); + } + } + } + + // Stone wall obstacle (between player and water) + let wall_x = 110; + let wall_surface = (h as i32 - 3) - ((wall_x as f32 * 0.1).sin() * 5.0) as i32; + let wall_surface = wall_surface.max(10).min(h as i32 - 3); + for y in wall_surface - 6..wall_surface { + self.grid.set_material(wall_x, y, MaterialId::Stone); + self.grid.set_material(wall_x + 1, y, MaterialId::Stone); + } + self.grid.fill_border(MaterialId::Stone); let cx = (w / 2) as f32; @@ -159,14 +232,11 @@ impl Game { pub fn check_on_ground(&self) -> bool { if let Some(e) = self.player.entity(&self.entities) { - for b in &e.bodies { - if !b.alive { - continue; - } - let bx = b.x as i32; - let by = b.y as i32; - let below = self.grid.get(bx, by + 1); - if below.is_solid() { + let bottom = (e.cy + e.half_h + 0.1) as i32; + let left = (e.cx - e.half_w) as i32; + let right = (e.cx + e.half_w) as i32; + for x in left..=right { + if self.grid.in_bounds(x, bottom) && self.grid.get(x, bottom).is_solid() { return true; } } @@ -232,147 +302,76 @@ impl Game { } fn update_rigid_entity(&mut self, idx: usize, gravity: f32, damping: f32, max_vel: f32) { - let grid = &self.grid; - let (cx, cy, cvx, cvy) = { + let (cx, cy, cvx, cvy, half_w, half_h) = { let e = &self.entities.all()[idx]; - (e.cx, e.cy, e.cvx, e.cvy) + (e.cx, e.cy, e.cvx, e.cvy, e.half_w, e.half_h) }; - let mut new_cx = cx; - let mut new_cy = cy; - let mut new_cvx = cvx * damping; - let mut new_cvy = cvy * damping; + let mut nx = cx; + let mut ny = cy; + let mut nvx = cvx * damping; + let mut nvy = cvy * damping; + nvy += gravity; - new_cvy += gravity; - - let v_mag = (new_cvx * new_cvx + new_cvy * new_cvy).sqrt(); + let v_mag = (nvx * nvx + nvy * nvy).sqrt(); if v_mag > max_vel { - new_cvx = new_cvx / v_mag * max_vel; - new_cvy = new_cvy / v_mag * max_vel; + nvx = nvx / v_mag * max_vel; + nvy = nvy / v_mag * max_vel; } - new_cx += new_cvx; - new_cy += new_cvy; + // Move X then resolve X collisions + nx += nvx; + let (resolved_x, hit_wall_x) = self.resolve_aabb_x(idx, nx, ny, half_w, half_h, nvx > 0.0); + nx = resolved_x; + if hit_wall_x { + if nvx > 0.0 { nvx = 0.0; } + else if nvx < 0.0 { nvx = 0.0; } + } - let offsets = self.entities.all()[idx].rest_offsets.clone(); - let radii: Vec = self.entities.all()[idx].bodies.iter().map(|b| b.radius).collect(); + // Move Y then resolve Y collisions + ny += nvy; + let (resolved_y, hit_floor, hit_ceiling) = self.resolve_aabb_y(idx, nx, ny, half_w, half_h, nvy > 0.0); + ny = resolved_y; + if hit_floor { + nvy = 0.0; + } + if hit_ceiling { + nvy = 0.0; + } - for substep in 0..4 { - let mut total_push_x = 0.0; - let mut total_push_y = 0.0; - let mut push_count = 0; - - for (i, &(ox, oy)) in offsets.iter().enumerate() { - let bx = new_cx + ox; - let by = new_cy + oy; - let r = radii[i]; - - let min_x = (bx - r).floor() as i32; - let max_x = (bx + r).ceil() as i32; - let min_y = (by - r).floor() as i32; - let max_y = (by + r).ceil() as i32; - - for cy_cell in min_y..=max_y { - for cx_cell in min_x..=max_x { - if !grid.in_bounds(cx_cell, cy_cell) { - continue; - } - let cell = grid.get(cx_cell, cy_cell); - if cell.is_empty() || cell.is_liquid() { - continue; - } - if cell.material == MaterialId::Fire { - continue; - } - if !cell.is_solid() { - continue; - } - - let cell_min_x = cx_cell as f32; - let cell_max_x = (cx_cell + 1) as f32; - let cell_min_y = cy_cell as f32; - let cell_max_y = (cy_cell + 1) as f32; - - let inside_x = bx >= cell_min_x && bx < cell_max_x; - let inside_y = by >= cell_min_y && by < cell_max_y; - - if inside_x && inside_y { - let dl = bx - cell_min_x; - let dr = cell_max_x - bx; - let dt = by - cell_min_y; - let db = cell_max_y - by; - let md = dl.min(dr).min(dt).min(db); - if md == dt { - total_push_y -= r + md; - push_count += 1; - if new_cvy > 0.0 { new_cvy = 0.0; } - } else if md == db { - total_push_y += r + md; - push_count += 1; - if new_cvy < 0.0 { new_cvy = 0.0; } - } else if md == dl { - total_push_x -= r + md; - push_count += 1; - if new_cvx > 0.0 { new_cvx = 0.0; } - } else { - total_push_x += r + md; - push_count += 1; - if new_cvx < 0.0 { new_cvx = 0.0; } - } - } else { - let closest_x = bx.max(cell_min_x).min(cell_max_x); - let closest_y = by.max(cell_min_y).min(cell_max_y); - let dx = bx - closest_x; - let dy = by - closest_y; - let dist_sq = dx * dx + dy * dy; - if dist_sq < r * r && dist_sq > 0.0001 { - let dist = dist_sq.sqrt(); - let overlap = r - dist; - total_push_x += dx / dist * overlap; - total_push_y += dy / dist * overlap; - push_count += 1; - if dy < -0.5 && new_cvy > 0.0 { new_cvy = 0.0; } - } - } - } + // Check material contacts + let (touching_lava, touching_fire, touching_acid, in_liquid) = { + let grid = &self.grid; + let mut tl = false; + let mut tf = false; + let mut ta = false; + let mut il = false; + let min_x = (nx - half_w).floor() as i32; + let max_x = (nx + half_w).ceil() as i32; + let min_y = (ny - half_h).floor() as i32; + let max_y = (ny + half_h).ceil() as i32; + for y in min_y..=max_y { + for x in min_x..=max_x { + if !grid.in_bounds(x, y) { continue; } + let cell = grid.get(x, y); + if cell.material == MaterialId::Lava { tl = true; } + if cell.material == MaterialId::Fire { tf = true; } + if cell.material == MaterialId::Acid { ta = true; } + if cell.is_liquid() { il = true; } } } - - if push_count > 0 { - let inv = 1.0 / push_count as f32; - let px = total_push_x * inv; - let py = total_push_y * inv; - let mag = (px * px + py * py).sqrt(); - if mag > 0.001 { - new_cx += px; - new_cy += py; - } - } - } - - let mut touching_lava = false; - let mut touching_fire = false; - let mut touching_acid = false; - let mut in_liquid = false; - - for (i, &(ox, oy)) in offsets.iter().enumerate() { - let bx = (new_cx + ox) as i32; - let by = (new_cy + oy) as i32; - if !grid.in_bounds(bx, by) { - continue; - } - let cell = grid.get(bx, by); - if cell.material == MaterialId::Lava { touching_lava = true; } - if cell.material == MaterialId::Fire { touching_fire = true; } - if cell.material == MaterialId::Acid { touching_acid = true; } - if cell.is_liquid() { in_liquid = true; } - } + (tl, tf, ta, il) + }; if let Some(e) = self.entities.all_mut().get_mut(idx) { - e.cx = new_cx; - e.cy = new_cy; - e.cvx = new_cvx; - e.cvy = new_cvy; + e.cx = nx; + e.cy = ny; + e.cvx = nvx; + e.cvy = nvy; + if in_liquid { + e.cvy *= 0.6; + e.cvx *= 0.8; + } e.sync_bodies_to_center(); if touching_lava { @@ -396,11 +395,108 @@ impl Game { if b.alive { b.health -= 0.25; } } } - if in_liquid { - new_cvy *= 0.5; - e.cvy = new_cvy; + } + } + + fn resolve_aabb_x(&self, _idx: usize, cx: f32, cy: f32, hw: f32, hh: f32, _moving_right: bool) -> (f32, bool) { + let grid = &self.grid; + let left = cx - hw; + let right = cx + hw; + let top = cy - hh; + let bottom = cy + hh; + + let min_x = left.floor() as i32; + let max_x = right.ceil() as i32; + let min_y = top.floor() as i32; + let max_y = bottom.ceil() as i32; + + let mut best_push = 0.0f32; + let mut hit = false; + + for y in min_y..=max_y { + for x in min_x..=max_x { + if !grid.in_bounds(x, y) { continue; } + let cell = grid.get(x, y); + if !cell.is_solid() { continue; } + + let cell_left = x as f32; + let cell_right = (x + 1) as f32; + let cell_top = y as f32; + let cell_bottom = (y + 1) as f32; + + if bottom <= cell_top || top >= cell_bottom { + continue; + } + + let pen_left = right - cell_left; + let pen_right = cell_right - left; + + let push = if pen_left < pen_right { -pen_left } else { pen_right }; + if push.abs() > best_push.abs() { + best_push = push; + hit = true; + } } } + + (cx + best_push, hit) + } + + fn resolve_aabb_y(&self, _idx: usize, cx: f32, cy: f32, hw: f32, hh: f32, moving_down: bool) -> (f32, bool, bool) { + let grid = &self.grid; + let left = cx - hw; + let right = cx + hw; + let top = cy - hh; + let bottom = cy + hh; + + let min_x = left.floor() as i32; + let max_x = right.ceil() as i32; + let min_y = top.floor() as i32; + let max_y = bottom.ceil() as i32; + + let mut max_pen = 0.0f32; + let mut hit_floor = false; + let mut hit_ceiling = false; + + for y in min_y..=max_y { + for x in min_x..=max_x { + if !grid.in_bounds(x, y) { continue; } + let cell = grid.get(x, y); + if !cell.is_solid() { continue; } + + let cell_left = x as f32; + let cell_right = (x + 1) as f32; + let cell_top = y as f32; + let cell_bottom = (y + 1) as f32; + + if right <= cell_left || left >= cell_right { + continue; + } + + if moving_down { + let pen = bottom - cell_top; + if pen > max_pen { + max_pen = pen; + hit_floor = true; + } + } else { + let pen = cell_bottom - top; + if pen > max_pen { + max_pen = pen; + hit_ceiling = true; + } + } + } + } + + let new_cy = if hit_floor { + cy - max_pen + } else if hit_ceiling { + cy + max_pen + } else { + cy + }; + (new_cy, hit_floor, hit_ceiling) } fn update_ragdoll_entity(&mut self, idx: usize, solver: &crate::physics::verlet::VerletSolver, substeps: u32) { diff --git a/tests/entity_movement.rs b/tests/entity_movement.rs index 67ba960..8976517 100644 --- a/tests/entity_movement.rs +++ b/tests/entity_movement.rs @@ -13,17 +13,17 @@ fn setup_empty() -> GameSession { fn player_falls_and_lands() { let mut s = GameSession::new_seeded(42); s.init_empty(); - s.clear_area(115, 120, 20, 10); + s.clear_area(115, 115, 20, 15); s.perform_action(&AiAction::FillRect { x: 110, y: 130, w: 30, h: 15, material: "stone".into() }); - s.step(40); + s.step(80); let player = s.get_player().expect("player should exist"); assert!(player.alive, "player should be alive"); let y = player.pos[1]; - assert!(y < 140.0, "player should not fall through stone floor, got y={}", y); - s.step(20); + assert!(y < 135.0, "player should not fall through stone floor, got y={}", y); + s.step(30); let player2 = s.get_player().expect("player should exist"); let dy = (player2.pos[1] - y).abs(); - assert!(dy < 2.0, "player should have stopped falling (dy={:.2}), y={} -> {}", dy, y, player2.pos[1]); + assert!(dy < 3.0, "player should have stopped falling (dy={:.2}), y={} -> {}", dy, y, player2.pos[1]); } #[test]