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
This commit is contained in:
Emil
2026-06-20 18:26:33 +03:00
parent 68f6292c4d
commit 3cd94a386a
5 changed files with 642 additions and 82 deletions
+357
View File
@@ -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))
+55 -33
View File
@@ -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<crate::physics::verlet::SubBody>, Vec<crate::physics::verlet::Constraint>)>;
@@ -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;
+128
View File
@@ -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",
}
}
+88 -46
View File
@@ -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
+14 -3
View File
@@ -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;