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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user