From 3cd94a386a845d97f33c8d2bc7a5273d32965728 Mon Sep 17 00:00:00 2001 From: Emil Date: Sat, 20 Jun 2026 18:26:33 +0300 Subject: [PATCH] fix: collision tunneling, spawn at surface, physics substepping - 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 --- headless_dump.txt | 357 +++++++++++++++++++++++++++++++++++++++ src/game.rs | 88 ++++++---- src/main.rs | 128 ++++++++++++++ src/physics/collision.rs | 134 ++++++++++----- src/physics/verlet.rs | 17 +- 5 files changed, 642 insertions(+), 82 deletions(-) create mode 100644 headless_dump.txt diff --git a/headless_dump.txt b/headless_dump.txt new file mode 100644 index 0000000..5a8a04a --- /dev/null +++ b/headless_dump.txt @@ -0,0 +1,357 @@ +=== Verbatim Headless Run: 120 ticks === + +World: 250x250 +Player start: (125.0, 242.4) +Camera: (85, 230) + +=== Tick 0 (initial state) === + Camera (85, 230): + 9 0 1 2 3 4 5 6 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 @ +42 @@ +43 @@ """"""""""""" +44""" @ "":::::::::::::""" +45:::""" """::::::::::::::::::"" +46::::::"" "":::::::::::::::::::::::""" +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(125.0,242.4) bodies=7/7 on_fire=false" +Entities: 1 + +=== Tick 12 === + Camera (87, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 """"""""""""" +44" "":::::::::::::""" +45:""" @@ """::::::::::::::::::"" +46::::"" @@"":::::::::::::::::::::::""" +47::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(127.1,246.4) bodies=7/7 on_fire=false" +Entities: 1 +Alive entities: Player(hp=100, pos=(127.054695, 246.36816)) + +=== Tick 24 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 """"""""""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.6,246.4) bodies=7/7 on_fire=false" +Entities: 1 +Alive entities: Player(hp=100, pos=(126.642654, 246.37305)) + +=== Tick 36 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 g +42 ggg +43 """""""g""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 2 +Alive entities: Player(hp=100, pos=(126.91838, 246.3771)), Goblin(hp=40, pos=(141.79904, 242.62468)) + +=== Tick 48 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 g +42 ggg +43 """"""g"""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 2 +Alive entities: Player(hp=100, pos=(126.92547, 246.37007)), Goblin(hp=40, pos=(141.31493, 242.59508)) + +=== Tick 60 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 g +38 gg +39 gg +40 g +41 g +42 ggg +43 """"""g"""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 3 +Alive entities: Player(hp=100, pos=(126.89856, 246.37102)), Goblin(hp=40, pos=(141.50412, 242.62273)), Goblin(hp=40, pos=(141.0, 238.42857)) + +=== Tick 72 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 gg +42 ggg +43 """"""gg""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 3 +Alive entities: Player(hp=100, pos=(126.88849, 246.36723)), Goblin(hp=40, pos=(141.31738, 242.59448)), Goblin(hp=40, pos=(141.5151, 242.62154)) + +=== Tick 84 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 gg +42 ggg +43 """""""g""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 3 +Alive entities: Player(hp=100, pos=(126.884605, 246.3688)), Goblin(hp=40, pos=(141.57698, 242.61453)), Goblin(hp=40, pos=(141.43205, 242.62611)) + +=== Tick 96 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 gg +42 ggg +43 """"""gg""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 4 +Alive entities: Player(hp=100, pos=(126.883316, 246.37033)), Goblin(hp=40, pos=(141.46056, 242.62148)), Goblin(hp=40, pos=(141.5288, 242.61539)), Goblin(hp=40, pos=(141.79904, 242.62468)) + +=== Tick 108 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 g +42 ggg +43 """"""gg""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 4 +Alive entities: Player(hp=100, pos=(126.882866, 246.37112)), Goblin(hp=40, pos=(141.4156, 242.61002)), Goblin(hp=40, pos=(141.47725, 242.62231)), Goblin(hp=40, pos=(141.31493, 242.59508)) + +=== Tick 120 === + Camera (86, 234): + 9 0 1 2 3 4 5 6 +34 +35 +36 +37 +38 +39 +40 +41 gg +42 ggg +43 """"""gg""""" +44"" "":::::::::::::""" +45::""" @@ """::::::::::::::::::"" +46:::::"" @@@"":::::::::::::::::::::::""" +47:::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::"""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +56???????????????????????????????????????????????????????????????????????????????? +57???????????????????????????????????????????????????????????????????????????????? +58???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(126.9,246.4) bodies=7/7 on_fire=false" +Entities: 4 +Alive entities: Player(hp=100, pos=(126.88255, 246.37636)), Goblin(hp=40, pos=(141.61978, 242.61577)), Goblin(hp=40, pos=(141.36923, 242.5867)), Goblin(hp=40, pos=(141.50412, 242.62273)) + diff --git a/src/game.rs b/src/game.rs index 8a711c6..f355a1f 100644 --- a/src/game.rs +++ b/src/game.rs @@ -68,14 +68,22 @@ impl Game { } } + self.grid.fill_border(MaterialId::Stone); + let cx = (w / 2) as f32; - let cy = (h as f32 / 2.0) - 20.0; + let surface_x = cx as i32; + let mut surface_y = h as i32 - 3; + for y in 0..h as i32 { + if self.grid.get(surface_x, y).is_solid() && self.grid.get(surface_x, y).material != MaterialId::Stone { + surface_y = y; + break; + } + } + let cy = (surface_y as f32) - 4.0; self.player.spawn_at(&mut self.entities, cx, cy); let (px, py) = self.player.center(&self.entities); self.center_camera_on(px, py); - - self.grid.fill_border(MaterialId::Stone); } fn center_camera_on(&mut self, px: f32, py: f32) { @@ -163,7 +171,7 @@ impl Game { false } - fn fixed_update(&mut self) { + pub fn fixed_update(&mut self) { self.tick += 1; self.ca.step(&mut self.grid); @@ -179,6 +187,7 @@ impl Game { fn update_entities(&mut self) { let solver = self.verlet.clone(); + let substeps = solver.substeps; let grid = &self.grid; let entities_data: Vec<(usize, Vec, Vec)>; @@ -195,7 +204,6 @@ impl Game { if !b.alive { continue; } - if b.on_fire { b.fire_timer += 1; b.health -= 0.3; @@ -206,39 +214,41 @@ impl Game { } } - solver.integrate(&mut bodies); + for _ in 0..substeps { + solver.integrate(&mut bodies); - for b in &mut bodies { - if !b.alive { - continue; - } - let result = resolve_grid_collision(grid, b); - if result.touching_lava { - b.health -= 2.0; - if !b.on_fire { - b.on_fire = true; + for b in &mut bodies { + if !b.alive { + continue; } - } - if result.touching_fire { - b.health -= 0.5; - if !b.on_fire && b.health < 80.0 { - b.on_fire = true; + let result = resolve_grid_collision(grid, b); + if result.touching_lava { + b.health -= 0.5; + if !b.on_fire { + b.on_fire = true; + } } - } - if result.touching_acid { - b.health -= 1.0; - } - if result.in_liquid { - let body_density = crate::world::material::MaterialRegistry::instance() - .get(b.material).density; - if body_density > result.liquid_density { - b.y += 0.02; + if result.touching_fire { + b.health -= 0.15; + if !b.on_fire && b.health < 80.0 { + b.on_fire = true; + } + } + if result.touching_acid { + b.health -= 0.25; + } + if result.in_liquid { + let body_density = crate::world::material::MaterialRegistry::instance() + .get(b.material).density; + if body_density > result.liquid_density { + b.y += 0.005; + } } } + + solver.solve_constraints(&mut bodies, &constraints, 2); } - solver.solve_constraints(&mut bodies, &constraints, 3); - if let Some(e) = self.entities.all_mut().get_mut(idx) { e.bodies = bodies; @@ -295,9 +305,21 @@ impl Game { return; } - let (px, py) = self.player.center(&self.entities); + let (px, _py) = self.player.center(&self.entities); let spawn_x = px as i32 + if px as i32 % 2 == 0 { 15 } else { -15 }; - let spawn_y = py as i32 - 5; + if !self.grid.in_bounds(spawn_x, 0) { + return; + } + + let mut surface_y = self.grid.height as i32 - 3; + for y in 0..self.grid.height as i32 { + let cell = self.grid.get(spawn_x, y); + if cell.is_solid() && cell.material != MaterialId::Stone { + surface_y = y; + break; + } + } + let spawn_y = surface_y - 4; if !self.grid.in_bounds(spawn_x, spawn_y) { return; diff --git a/src/main.rs b/src/main.rs index dd0fb5a..56831a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,17 +8,26 @@ mod game; use clap::Parser; use game::Game; use render::terminal::TerminalRenderer; +use std::io::Write; #[derive(Parser, Debug)] #[command(name = "verbatim", about = "ASCII physics RPG - Noita meets Caves of Qud")] struct Cli { #[arg(long, default_value = "terminal")] render_mode: String, + + #[arg(long, default_value_t = 0)] + headless_ticks: u32, } fn main() { let cli = Cli::parse(); + if cli.headless_ticks > 0 { + run_headless(cli.headless_ticks); + return; + } + match cli.render_mode.as_str() { "terminal" => { let mut renderer = TerminalRenderer::new(); @@ -35,3 +44,122 @@ fn main() { } } } + +fn run_headless(ticks: u32) { + let mut game = Game::new(); + game.init_world(); + + let (px, py) = game.player.center(&game.entities); + let cam_x = px as i32 - 40; + let cam_y = py as i32 - 12; + + let mut log = String::new(); + log.push_str(&format!("=== Verbatim Headless Run: {} ticks ===\n\n", ticks)); + log.push_str(&format!("World: {}x{}\n", game.grid.width, game.grid.height)); + log.push_str(&format!("Player start: ({:.1}, {:.1})\n", px, py)); + log.push_str(&format!("Camera: ({}, {})\n\n", cam_x, cam_y)); + + log.push_str("=== Tick 0 (initial state) ===\n"); + log.push_str(&dump_view(&game.grid, &game.entities, cam_x, cam_y, 80, 25)); + log.push_str(&format!("Player: {:?}\n", player_info(&game))); + log.push_str(&format!("Entities: {}\n\n", game.entities.all().len())); + + let dump_interval = (ticks / 10).max(1); + + for t in 1..=ticks { + game.fixed_update(); + + if t % dump_interval == 0 || t == ticks { + let (px, py) = game.player.center(&game.entities); + let cam_x = px as i32 - 40; + let cam_y = py as i32 - 12; + + log.push_str(&format!("=== Tick {} ===\n", t)); + log.push_str(&dump_view(&game.grid, &game.entities, cam_x, cam_y, 80, 25)); + log.push_str(&format!("Player: {:?}\n", player_info(&game))); + log.push_str(&format!("Entities: {}\n", game.entities.all().len())); + + let alive: Vec<_> = game.entities.all().iter() + .filter(|e| e.alive) + .map(|e| format!("{}(hp={:.0}, pos={:?})", entity_kind_name(e.kind), e.health, e.center())) + .collect(); + log.push_str(&format!("Alive entities: {}\n\n", alive.join(", "))); + } + } + + let mut f = std::fs::File::create("headless_dump.txt").expect("Cannot create dump file"); + f.write_all(log.as_bytes()).expect("Cannot write dump"); + eprintln!("Headless run complete: {} ticks, dump written to headless_dump.txt", ticks); +} + +fn dump_view(grid: &world::grid::Grid, entities: &entity::EntityManager, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String { + let mut buf = String::with_capacity(vw * vh + vh + 100); + buf.push_str(&format!(" Camera ({}, {}):\n", cam_x, cam_y)); + + let mut entity_map = std::collections::HashMap::new(); + for e in entities.all() { + for b in &e.bodies { + if !b.alive { continue; } + let sx = b.x as i32 - cam_x; + let sy = b.y as i32 - cam_y; + if sx >= 0 && sx < vw as i32 && sy >= 0 && sy < vh as i32 { + let ch = match e.kind { + entity::EntityKind::Player if e.alive => '@', + entity::EntityKind::Goblin if e.alive => 'g', + _ => '%', + }; + entity_map.insert((sx, sy), ch); + } + } + } + + for dy in 0..vh { + let y = cam_y + dy as i32; + if dy == 0 { + buf.push_str(" "); + for dx in 0..vw { + let x = cam_x + dx as i32; + if x % 10 == 0 { + buf.push_str(&format!("{}", (x / 10) % 10)); + } else { + buf.push(' '); + } + } + buf.push('\n'); + } + buf.push_str(&format!("{:2}", y % 100)); + for dx in 0..vw { + let x = cam_x + dx as i32; + if let Some(&ch) = entity_map.get(&(dx as i32, dy as i32)) { + buf.push(ch); + } else if !grid.in_bounds(x, y) { + buf.push('?'); + } else { + let cell = grid.get(x, y); + buf.push(cell.material.display_char()); + } + } + buf.push('\n'); + } + buf +} + +fn player_info(game: &Game) -> String { + if let Some(e) = game.player.entity(&game.entities) { + let (cx, cy) = e.center(); + let body_count = e.bodies.iter().filter(|b| b.alive).count(); + let on_fire = e.on_fire; + let kind = entity_kind_name(e.kind); + format!("{} hp={:.1}/{:.1} pos=({:.1},{:.1}) bodies={}/{} on_fire={}", kind, e.health, e.max_health, cx, cy, body_count, e.bodies.len(), on_fire) + } else { + "None".to_string() + } +} + +fn entity_kind_name(kind: entity::EntityKind) -> &'static str { + match kind { + entity::EntityKind::Player => "Player", + entity::EntityKind::Goblin => "Goblin", + entity::EntityKind::Corpse => "Corpse", + } +} diff --git a/src/physics/collision.rs b/src/physics/collision.rs index cd44535..36f84e4 100644 --- a/src/physics/collision.rs +++ b/src/physics/collision.rs @@ -33,65 +33,107 @@ pub fn resolve_grid_collision(grid: &Grid, body: &mut SubBody) -> CollisionResul let min_y = (body.y - r).floor() as i32; let max_y = (body.y + r).ceil() as i32; - for cy in min_y..=max_y { - for cx in min_x..=max_x { - if !grid.in_bounds(cx, cy) { - continue; - } - let cell = grid.get(cx, cy); - if cell.is_empty() { - continue; - } + for _iteration in 0..3 { + let mut any_collision = false; - if cell.is_liquid() { - result.in_liquid = true; - result.liquid_density = result.liquid_density.max(cell.density()); - if cell.material == MaterialId::Lava { - result.touching_lava = true; + for cy in min_y..=max_y { + for cx in min_x..=max_x { + if !grid.in_bounds(cx, cy) { + continue; } - if cell.material == MaterialId::Acid { - result.touching_acid = true; + let cell = grid.get(cx, cy); + if cell.is_empty() { + continue; } - apply_liquid_drag(body, cell.density()); - continue; - } - if cell.material == MaterialId::Fire { - result.touching_fire = true; - continue; - } + if cell.is_liquid() { + result.in_liquid = true; + result.liquid_density = result.liquid_density.max(cell.density()); + if cell.material == MaterialId::Lava { + result.touching_lava = true; + } + if cell.material == MaterialId::Acid { + result.touching_acid = true; + } + apply_liquid_drag(body, cell.density()); + continue; + } - if cell.is_solid() { - let closest_x = body.x.max(cx as f32).min((cx + 1) as f32); - let closest_y = body.y.max(cy as f32).min((cy + 1) as f32); - let dx = body.x - closest_x; - let dy = body.y - closest_y; - let dist_sq = dx * dx + dy * dy; - if dist_sq < r * r { - let dist = dist_sq.sqrt(); - if dist > 0.0001 { - let overlap = r - dist; - let nx = dx / dist; - let ny = dy / dist; - body.x += nx * overlap; - body.y += ny * overlap; - if ny < -0.5 { + if cell.material == MaterialId::Fire { + result.touching_fire = true; + continue; + } + + if cell.is_solid() { + let cell_min_x = cx as f32; + let cell_max_x = (cx + 1) as f32; + let cell_min_y = cy as f32; + let cell_max_y = (cy + 1) as f32; + + let inside_x = body.x >= cell_min_x && body.x < cell_max_x; + let inside_y = body.y >= cell_min_y && body.y < cell_max_y; + + if inside_x && inside_y { + let dist_left = body.x - cell_min_x; + let dist_right = cell_max_x - body.x; + let dist_top = body.y - cell_min_y; + let dist_bottom = cell_max_y - body.y; + + let min_dist = dist_left.min(dist_right).min(dist_top).min(dist_bottom); + + if min_dist == dist_top { + body.y = cell_min_y - r; result.on_ground = true; + } else if min_dist == dist_bottom { + body.y = cell_max_y + r; + } else if min_dist == dist_left { + body.x = cell_min_x - r; + } else { + body.x = cell_max_x + r; } + + let vy = body.vy(); + if min_dist == dist_top && vy > 0.0 { + body.set_vel(body.vx(), 0.0); + } + let vx = body.vx(); + if (min_dist == dist_left || min_dist == dist_right) && vx != 0.0 { + body.set_vel(0.0, body.vy()); + } + + any_collision = true; } else { - let bcx = cx as f32 + 0.5; - let bcy = cy as f32 + 0.5; - let dx = body.x - bcx; - let dy = body.y - bcy; - let dist = (dx * dx + dy * dy).sqrt(); - if dist > 0.0001 { - body.x = bcx + dx / dist * r * 1.1; - body.y = bcy + dy / dist * r * 1.1; + let closest_x = body.x.max(cell_min_x).min(cell_max_x); + let closest_y = body.y.max(cell_min_y).min(cell_max_y); + let dx = body.x - closest_x; + let dy = body.y - 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; + let nx = dx / dist; + let ny = dy / dist; + body.x += nx * overlap; + body.y += ny * overlap; + + if ny < -0.5 { + result.on_ground = true; + if body.vy() > 0.0 { + body.set_vel(body.vx(), 0.0); + } + } + + any_collision = true; } } } } } + + if !any_collision { + break; + } } result diff --git a/src/physics/verlet.rs b/src/physics/verlet.rs index dee1996..7d63a74 100644 --- a/src/physics/verlet.rs +++ b/src/physics/verlet.rs @@ -87,14 +87,18 @@ pub struct VerletSolver { pub gravity: f32, pub damping: f32, pub dt: f32, + pub max_vel: f32, + pub substeps: u32, } impl VerletSolver { pub fn new() -> Self { Self { - gravity: 0.3, + gravity: 0.08, damping: 0.98, dt: 1.0, + max_vel: 0.8, + substeps: 4, } } @@ -103,8 +107,15 @@ impl VerletSolver { if !b.alive { continue; } - let vx = (b.x - b.old_x) * self.damping; - let vy = (b.y - b.old_y) * self.damping; + let mut vx = (b.x - b.old_x) * self.damping; + let mut vy = (b.y - b.old_y) * self.damping; + + let v_mag = (vx * vx + vy * vy).sqrt(); + if v_mag > self.max_vel { + vx = vx / v_mag * self.max_vel; + vy = vy / v_mag * self.max_vel; + } + b.old_x = b.x; b.old_y = b.y; b.x += vx + b.ax * self.dt * self.dt;