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
This commit is contained in:
Emil
2026-06-20 21:56:13 +03:00
parent a731d5cbb7
commit a66b4ac0cb
4 changed files with 377 additions and 275 deletions
+131 -131
View File
@@ -12,16 +12,16 @@ Camera: (85, 230)
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 @@@@@ 40 T T @@@@@ ...
41 @@@@@@ 41 T T ## @@@@@@ .....
42 @@@@@@ 42 T T ## @@@@@@ .......
43 @@@@@ """"""""""""" 43 T T ## @@@@@ """"""""""""" .........
44""" @@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" """::::::::::::::::::"" 45:::""" ## """::::::::::::::::::"" ...........
46::::::"" "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -41,16 +41,16 @@ Entities: 1
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 @@@@@ 40 T T @@@@@ ...
41 @@@@@@ 41 T T ## @@@@@@ .....
42 @@@@@@ 42 T T ## @@@@@@ .......
43 @@@@@ """"""""""""" 43 T T ## @@@@@ """"""""""""" .........
44""" @@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" """::::::::::::::::::"" 45:::""" ## """::::::::::::::::::"" ...........
46::::::"" "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -71,16 +71,16 @@ Alive entities: Player(hp=100, pos=(125.0, 242.11879))
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 @@@@@ 40 T T @@@@@ ...
41 @@@@@@ 41 T T ## @@@@@@ .....
42 @@@@@@ 42 T T ## @@@@@@ .......
43 @@@@@ """"""""""""" 43 T T ## @@@@@ """"""""""""" .........
44""" @@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" """::::::::::::::::::"" 45:::""" ## """::::::::::::::::::"" ...........
46::::::"" "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -101,16 +101,16 @@ Alive entities: Player(hp=100, pos=(125.0, 242.38817))
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 @@@@@ 40 T T @@@@@ ...
41 @@@@@@ 41 T T ## @@@@@@ .....
42 @@@@@@ 42 T T ## @@@@@@ .......
43 @@@@@ """"""""""""" 43 T T ## @@@@@ """"""""""""" .........
44""" @@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" """::::::::::::::::::"" 45:::""" ## """::::::::::::::::::"" ...........
46::::::"" "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -130,16 +130,16 @@ Alive entities: Player(hp=100, pos=(125.0, 242.79922))
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 40 T T ...
41 @@@@@ 41 T T ## @@@@@ .....
42 @@@@@@ 42 T T ## @@@@@@ .......
43 @@@@@@ """"""""""""" 43 T T ## @@@@@@ """"""""""""" .........
44""" @@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" @@@@@ """::::::::::::::::::"" 45:::""" ## @@@@@ """::::::::::::::::::"" ...........
46::::::"" "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -154,21 +154,22 @@ Entities: 1
Alive entities: Player(hp=100, pos=(125.0, 243.3436)) Alive entities: Player(hp=100, pos=(125.0, 243.3436))
=== Tick 10 === === Tick 10 ===
31
32 32
33 33
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 40 T T ...
41 41 T T ## @@@@@ .....
42 @@@@@ 42 T T ## @@@@@@ .......
43 @@@@@@ """"""""""""" 43 T T ## @@@@@@ """"""""""""" .........
44""" @@@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" @@@@@ """::::::::::::::::::"" 45:::""" ## @@@@@ """::::::::::::::::::"" ...........
46::::::"" @@@@@"":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -178,27 +179,27 @@ Alive entities: Player(hp=100, pos=(125.0, 243.3436))
53???????????????????????????????????????????????????????????????????????????????? 53????????????????????????????????????????????????????????????????????????????????
54???????????????????????????????????????????????????????????????????????????????? 54????????????????????????????????????????????????????????????????????????????????
55???????????????????????????????????????????????????????????????????????????????? 55????????????????????????????????????????????????????????????????????????????????
56???????????????????????????????????????????????????????????????????????????????? Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false"
Player: "Player hp=100.0/100.0 pos=(125.0,244.0) bodies=27/27 on_fire=false"
Entities: 1 Entities: 1
Alive entities: Player(hp=100, pos=(125.0, 244.01338)) Alive entities: Player(hp=100, pos=(125.0, 243.5))
=== Tick 12 === === Tick 12 ===
31
32 32
33 33
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 40 T T ...
41 41 T T ## @@@@@ .....
42 @@@@@ 42 T T ## @@@@@@ .......
43 @@@@@@ """"""""""""" 43 T T ## @@@@@@ """"""""""""" .........
44""" @@@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" @@@@@ """::::::::::::::::::"" 45:::""" ## @@@@@ """::::::::::::::::::"" ...........
46::::::"" @@@@@"":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -208,27 +209,27 @@ Alive entities: Player(hp=100, pos=(125.0, 244.01338))
53???????????????????????????????????????????????????????????????????????????????? 53????????????????????????????????????????????????????????????????????????????????
54???????????????????????????????????????????????????????????????????????????????? 54????????????????????????????????????????????????????????????????????????????????
55???????????????????????????????????????????????????????????????????????????????? 55????????????????????????????????????????????????????????????????????????????????
56???????????????????????????????????????????????????????????????????????????????? Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false"
Player: "Player hp=100.0/100.0 pos=(125.0,244.5) bodies=27/27 on_fire=false"
Entities: 1 Entities: 1
Alive entities: Player(hp=100, pos=(125.0, 244.5)) Alive entities: Player(hp=100, pos=(125.0, 243.5))
=== Tick 14 === === Tick 14 ===
31
32 32
33 33
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 40 T T ...
41 41 T T ## @@@@@ .....
42 @@@@@ 42 T T ## @@@@@@ .......
43 @@@@@@ """"""""""""" 43 T T ## @@@@@@ """"""""""""" .........
44""" @@@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45:::""" @@@@@ """::::::::::::::::::"" 45:::""" ## @@@@@ """::::::::::::::::::"" ...........
46::::::"" @@@@@"":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
@@ -238,28 +239,28 @@ Alive entities: Player(hp=100, pos=(125.0, 244.5))
53???????????????????????????????????????????????????????????????????????????????? 53????????????????????????????????????????????????????????????????????????????????
54???????????????????????????????????????????????????????????????????????????????? 54????????????????????????????????????????????????????????????????????????????????
55???????????????????????????????????????????????????????????????????????????????? 55????????????????????????????????????????????????????????????????????????????????
56???????????????????????????????????????????????????????????????????????????????? Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false"
Player: "Player hp=100.0/100.0 pos=(125.0,244.5) bodies=27/27 on_fire=false"
Entities: 1 Entities: 1
Alive entities: Player(hp=100, pos=(125.0, 244.5)) Alive entities: Player(hp=100, pos=(125.0, 243.5))
=== Tick 16 === === Tick 16 ===
31
32 32
33 33
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 40 T T ...
41 41 T T ## @@@@@ .....
42 @@@@@ 42 T T ## @@@@@@ .......
43" @@@@@@ """"""""""""" 43 T T ## @@@@@@ """"""""""""" .........
44:""" @@@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45::::""" @@@@@ """::::::::::::::::::"" 45:::""" ## @@@@@ """::::::::::::::::::"" ...........
46:::::::"" @@@@@ "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47:::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
50???????????????????????????????????????????????????????????????????????????????? 50????????????????????????????????????????????????????????????????????????????????
@@ -268,28 +269,28 @@ Alive entities: Player(hp=100, pos=(125.0, 244.5))
53???????????????????????????????????????????????????????????????????????????????? 53????????????????????????????????????????????????????????????????????????????????
54???????????????????????????????????????????????????????????????????????????????? 54????????????????????????????????????????????????????????????????????????????????
55???????????????????????????????????????????????????????????????????????????????? 55????????????????????????????????????????????????????????????????????????????????
56???????????????????????????????????????????????????????????????????????????????? Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false"
Player: "Player hp=100.0/100.0 pos=(124.5,244.5) bodies=27/27 on_fire=false"
Entities: 1 Entities: 1
Alive entities: Player(hp=100, pos=(124.5, 244.5)) Alive entities: Player(hp=100, pos=(125.0, 243.5))
=== Tick 18 === === Tick 18 ===
31
32 32
33 33
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 40 T T ...
41 41 T T ## @@@@@ .....
42 @@@@@ 42 T T ## @@@@@@ .......
43" @@@@@@ """"""""""""" 43 T T ## @@@@@@ """"""""""""" .........
44:""" @@@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45::::""" @@@@@ """::::::::::::::::::"" 45:::""" ## @@@@@ """::::::::::::::::::"" ...........
46:::::::"" @@@@@ "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47:::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
50???????????????????????????????????????????????????????????????????????????????? 50????????????????????????????????????????????????????????????????????????????????
@@ -298,28 +299,28 @@ Alive entities: Player(hp=100, pos=(124.5, 244.5))
53???????????????????????????????????????????????????????????????????????????????? 53????????????????????????????????????????????????????????????????????????????????
54???????????????????????????????????????????????????????????????????????????????? 54????????????????????????????????????????????????????????????????????????????????
55???????????????????????????????????????????????????????????????????????????????? 55????????????????????????????????????????????????????????????????????????????????
56???????????????????????????????????????????????????????????????????????????????? Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false"
Player: "Player hp=100.0/100.0 pos=(124.5,244.5) bodies=27/27 on_fire=false"
Entities: 1 Entities: 1
Alive entities: Player(hp=100, pos=(124.5, 244.5)) Alive entities: Player(hp=100, pos=(125.0, 243.5))
=== Tick 20 === === Tick 20 ===
31
32 32
33 33
34 34
35 35
36 36
37 37 TTTTT
38 38 T T
39 39 T T .
40 40 T T ...
41 41 T T ## @@@@@ .....
42 @@@@@ 42 T T ## @@@@@@ .......
43" @@@@@@ """"""""""""" 43 T T ## @@@@@@ """"""""""""" .........
44:""" @@@@@@ "":::::::::::::""" 44""" T T ## @@@@@ "":::::::::::::""" ..........
45::::""" @@@@@ """::::::::::::::::::"" 45:::""" ## @@@@@ """::::::::::::::::::"" ...........
46:::::::"" @@@@@ "":::::::::::::::::::::::""" 46::::::"" ## "":::::::::::::::::::::::""".........
47:::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""" 47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""
48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
49################################################################################ 49################################################################################
50???????????????????????????????????????????????????????????????????????????????? 50????????????????????????????????????????????????????????????????????????????????
@@ -328,8 +329,7 @@ Alive entities: Player(hp=100, pos=(124.5, 244.5))
53???????????????????????????????????????????????????????????????????????????????? 53????????????????????????????????????????????????????????????????????????????????
54???????????????????????????????????????????????????????????????????????????????? 54????????????????????????????????????????????????????????????????????????????????
55???????????????????????????????????????????????????????????????????????????????? 55????????????????????????????????????????????????????????????????????????????????
56???????????????????????????????????????????????????????????????????????????????? Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false"
Player: "Player hp=100.0/100.0 pos=(124.5,244.5) bodies=27/27 on_fire=false"
Entities: 1 Entities: 1
Alive entities: Player(hp=100, pos=(124.5, 244.5)) Alive entities: Player(hp=100, pos=(125.0, 243.5))
+6
View File
@@ -22,6 +22,8 @@ pub struct Entity {
pub cy: f32, pub cy: f32,
pub cvx: f32, pub cvx: f32,
pub cvy: f32, pub cvy: f32,
pub half_w: f32,
pub half_h: f32,
pub health: f32, pub health: f32,
pub max_health: f32, pub max_health: f32,
pub on_fire: bool, pub on_fire: bool,
@@ -46,6 +48,8 @@ impl Entity {
max_health: 100.0, max_health: 100.0,
on_fire: false, on_fire: false,
fire_timer: 0, fire_timer: 0,
half_w: 3.5,
half_h: 3.0,
} }
} }
@@ -106,6 +110,8 @@ impl Entity {
self.cy = cy; self.cy = cy;
self.cvx = 0.0; self.cvx = 0.0;
self.cvy = 0.0; self.cvy = 0.0;
self.half_w = 3.5;
self.half_h = 2.5;
let r = 0.5; let r = 0.5;
let mat = match self.kind { let mat = match self.kind {
+235 -139
View File
@@ -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); self.grid.fill_border(MaterialId::Stone);
let cx = (w / 2) as f32; let cx = (w / 2) as f32;
@@ -159,14 +232,11 @@ impl Game {
pub fn check_on_ground(&self) -> bool { pub fn check_on_ground(&self) -> bool {
if let Some(e) = self.player.entity(&self.entities) { if let Some(e) = self.player.entity(&self.entities) {
for b in &e.bodies { let bottom = (e.cy + e.half_h + 0.1) as i32;
if !b.alive { let left = (e.cx - e.half_w) as i32;
continue; let right = (e.cx + e.half_w) as i32;
} for x in left..=right {
let bx = b.x as i32; if self.grid.in_bounds(x, bottom) && self.grid.get(x, bottom).is_solid() {
let by = b.y as i32;
let below = self.grid.get(bx, by + 1);
if below.is_solid() {
return true; return true;
} }
} }
@@ -232,147 +302,76 @@ impl Game {
} }
fn update_rigid_entity(&mut self, idx: usize, gravity: f32, damping: f32, max_vel: f32) { fn update_rigid_entity(&mut self, idx: usize, gravity: f32, damping: f32, max_vel: f32) {
let grid = &self.grid; let (cx, cy, cvx, cvy, half_w, half_h) = {
let (cx, cy, cvx, cvy) = {
let e = &self.entities.all()[idx]; 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 nx = cx;
let mut new_cy = cy; let mut ny = cy;
let mut new_cvx = cvx * damping; let mut nvx = cvx * damping;
let mut new_cvy = cvy * damping; let mut nvy = cvy * damping;
nvy += gravity;
new_cvy += gravity; let v_mag = (nvx * nvx + nvy * nvy).sqrt();
let v_mag = (new_cvx * new_cvx + new_cvy * new_cvy).sqrt();
if v_mag > max_vel { if v_mag > max_vel {
new_cvx = new_cvx / v_mag * max_vel; nvx = nvx / v_mag * max_vel;
new_cvy = new_cvy / v_mag * max_vel; nvy = nvy / v_mag * max_vel;
} }
new_cx += new_cvx; // Move X then resolve X collisions
new_cy += new_cvy; 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(); // Move Y then resolve Y collisions
let radii: Vec<f32> = self.entities.all()[idx].bodies.iter().map(|b| b.radius).collect(); 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 { // Check material contacts
let mut total_push_x = 0.0; let (touching_lava, touching_fire, touching_acid, in_liquid) = {
let mut total_push_y = 0.0; let grid = &self.grid;
let mut push_count = 0; let mut tl = false;
let mut tf = false;
for (i, &(ox, oy)) in offsets.iter().enumerate() { let mut ta = false;
let bx = new_cx + ox; let mut il = false;
let by = new_cy + oy; let min_x = (nx - half_w).floor() as i32;
let r = radii[i]; let max_x = (nx + half_w).ceil() as i32;
let min_y = (ny - half_h).floor() as i32;
let min_x = (bx - r).floor() as i32; let max_y = (ny + half_h).ceil() as i32;
let max_x = (bx + r).ceil() as i32; for y in min_y..=max_y {
let min_y = (by - r).floor() as i32; for x in min_x..=max_x {
let max_y = (by + r).ceil() as i32; if !grid.in_bounds(x, y) { continue; }
let cell = grid.get(x, y);
for cy_cell in min_y..=max_y { if cell.material == MaterialId::Lava { tl = true; }
for cx_cell in min_x..=max_x { if cell.material == MaterialId::Fire { tf = true; }
if !grid.in_bounds(cx_cell, cy_cell) { if cell.material == MaterialId::Acid { ta = true; }
continue; if cell.is_liquid() { il = true; }
}
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; }
}
}
}
} }
} }
(tl, tf, ta, il)
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; }
}
if let Some(e) = self.entities.all_mut().get_mut(idx) { if let Some(e) = self.entities.all_mut().get_mut(idx) {
e.cx = new_cx; e.cx = nx;
e.cy = new_cy; e.cy = ny;
e.cvx = new_cvx; e.cvx = nvx;
e.cvy = new_cvy; e.cvy = nvy;
if in_liquid {
e.cvy *= 0.6;
e.cvx *= 0.8;
}
e.sync_bodies_to_center(); e.sync_bodies_to_center();
if touching_lava { if touching_lava {
@@ -396,11 +395,108 @@ impl Game {
if b.alive { b.health -= 0.25; } 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) { fn update_ragdoll_entity(&mut self, idx: usize, solver: &crate::physics::verlet::VerletSolver, substeps: u32) {
+5 -5
View File
@@ -13,17 +13,17 @@ fn setup_empty() -> GameSession {
fn player_falls_and_lands() { fn player_falls_and_lands() {
let mut s = GameSession::new_seeded(42); let mut s = GameSession::new_seeded(42);
s.init_empty(); 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.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"); let player = s.get_player().expect("player should exist");
assert!(player.alive, "player should be alive"); assert!(player.alive, "player should be alive");
let y = player.pos[1]; let y = player.pos[1];
assert!(y < 140.0, "player should not fall through stone floor, got y={}", y); assert!(y < 135.0, "player should not fall through stone floor, got y={}", y);
s.step(20); s.step(30);
let player2 = s.get_player().expect("player should exist"); let player2 = s.get_player().expect("player should exist");
let dy = (player2.pos[1] - y).abs(); 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] #[test]