Tests added: - physics_materials.rs: 15 tests (fire, smoke, steam, grass, dirt, bone, wood, stone, lava temp) - physics_interactions.rs: 12 tests (lava+water, fire spread, acid interactions, sand+water) - player_controls.rs: 12 tests (move L/R, jump, double jump, rapid input, wait, continuous) - collision_robust.rs: 10 tests (walls, ceiling, sliding, corridor, slope, dirt wall, unstick) - ragdoll_death.rs: 7 tests (death transition, ragdoll fall, progressive damage, corpse) - determinism.rs: 8 tests (same seed, replay exact, replay partial, recording, 100-tick) - edge_cases.rs: 19 tests (boundary, stress 20 goblins, fill/clear/paint, gravity, camera) - 6 new JSON scenarios (fire chain, lava pool, sand bury, entity fall, steam, acid wall) Fixes: - AABB resolve_aabb_y: add vertical overlap check (was pushing player down when jumping) - AABB resolve_aabb_x: velocity-aware resolution (player was stuck against walls) - check_on_ground: use fractional position check (was always true) - jump_force 1.5 (was 0.8) for 27-body entity - Gravity read from self.verlet (was from clone, SetGravity didn't work) - u8 overflow in lava color (saturating_add) 109 Rust tests, 14 JSON scenarios, 0 warnings, 0 failures
163 lines
6.0 KiB
Rust
163 lines
6.0 KiB
Rust
use verbatim::ai::GameSession;
|
|
use verbatim::ai::AiAction;
|
|
|
|
fn setup() -> GameSession {
|
|
let mut s = GameSession::new_seeded(42);
|
|
s.init_empty();
|
|
s.clear_area(90, 90, 50, 50);
|
|
s.perform_action(&AiAction::FillRect { x: 80, y: 130, w: 80, h: 15, material: "stone".into() });
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn player_blocked_by_left_wall() {
|
|
let mut s = setup();
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
let wall_x = (p.pos[0] as i32) - 6;
|
|
s.perform_action(&AiAction::FillRect { x: wall_x, y: 125, w: 1, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::MoveLeft);
|
|
s.step(20);
|
|
let p2 = s.get_player().unwrap();
|
|
assert!(p2.pos[0] > wall_x as f32, "player should not pass through left wall: wall={} player={}", wall_x, p2.pos[0]);
|
|
}
|
|
|
|
#[test]
|
|
fn player_blocked_by_right_wall() {
|
|
let mut s = setup();
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
let wall_x = (p.pos[0] as i32) + 6;
|
|
s.perform_action(&AiAction::FillRect { x: wall_x, y: 125, w: 1, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.step(20);
|
|
let p2 = s.get_player().unwrap();
|
|
assert!(p2.pos[0] < wall_x as f32, "player should not pass through right wall: wall={} player={}", wall_x, p2.pos[0]);
|
|
}
|
|
|
|
#[test]
|
|
fn player_slides_along_wall() {
|
|
let mut s = setup();
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
let wall_x = (p.pos[0] as i32) + 6;
|
|
s.perform_action(&AiAction::FillRect { x: wall_x, y: 125, w: 1, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.step(30);
|
|
let p2 = s.get_player().unwrap();
|
|
assert!(p2.pos[0] < wall_x as f32, "player should stay left of wall");
|
|
assert!(p2.alive, "player should be alive");
|
|
let y_diff = (p2.pos[1] - p.pos[1]).abs();
|
|
assert!(y_diff < 5.0, "player should not fall through floor while sliding: dy={}", y_diff);
|
|
}
|
|
|
|
#[test]
|
|
fn player_blocked_by_ceiling() {
|
|
let mut s = setup();
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
let ceiling_y = (p.pos[1] as i32) - 8;
|
|
s.perform_action(&AiAction::FillRect { x: p.pos[0] as i32 - 5, y: ceiling_y, w: 10, h: 1, material: "stone".into() });
|
|
s.perform_action(&AiAction::Jump);
|
|
s.step(10);
|
|
let p2 = s.get_player().unwrap();
|
|
assert!(p2.pos[1] > ceiling_y as f32, "player should not pass through ceiling: ceiling={} player={}", ceiling_y, p2.pos[1]);
|
|
}
|
|
|
|
#[test]
|
|
fn player_navigates_corridor() {
|
|
let mut s = GameSession::new_seeded(42);
|
|
s.init_empty();
|
|
s.clear_area(100, 100, 40, 30);
|
|
s.perform_action(&AiAction::FillRect { x: 95, y: 120, w: 50, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::FillRect { x: 115, y: 110, w: 1, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::FillRect { x: 125, y: 110, w: 1, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::FillRect { x: 115, y: 108, w: 11, h: 1, material: "stone".into() });
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
assert!(p.pos[0] < 115.0 || p.pos[0] > 125.0, "player should be outside corridor initially");
|
|
}
|
|
|
|
#[test]
|
|
fn player_does_not_stick_to_wall() {
|
|
let mut s = setup();
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
let wall_x = (p.pos[0] as i32) + 8;
|
|
s.perform_action(&AiAction::FillRect { x: wall_x, y: 125, w: 1, h: 10, material: "stone".into() });
|
|
for _ in 0..10 {
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.step(2);
|
|
}
|
|
let p_at_wall = s.get_player().unwrap();
|
|
let x_at_wall = p_at_wall.pos[0];
|
|
for _ in 0..20 {
|
|
s.perform_action(&AiAction::MoveLeft);
|
|
s.step(2);
|
|
}
|
|
let p_away = s.get_player().unwrap();
|
|
assert!(p_away.pos[0] < x_at_wall - 1.0, "player should move away from wall: {} -> {}", x_at_wall, p_away.pos[0]);
|
|
}
|
|
|
|
#[test]
|
|
fn player_squeezes_through_gap() {
|
|
let mut s = setup();
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
let px = p.pos[0] as i32;
|
|
s.perform_action(&AiAction::FillRect { x: px + 8, y: 128, w: 1, h: 2, material: "stone".into() });
|
|
s.perform_action(&AiAction::FillRect { x: px + 8, y: 122, w: 1, h: 2, material: "stone".into() });
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.step(20);
|
|
let p2 = s.get_player().unwrap();
|
|
assert!(p2.alive, "player should survive squeeze attempt");
|
|
}
|
|
|
|
#[test]
|
|
fn player_blocked_by_two_walls_both_sides() {
|
|
let mut s = setup();
|
|
s.step(30);
|
|
let p = s.get_player().unwrap();
|
|
let px = p.pos[0] as i32;
|
|
s.perform_action(&AiAction::FillRect { x: px - 6, y: 125, w: 1, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::FillRect { x: px + 6, y: 125, w: 1, h: 10, material: "stone".into() });
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.step(10);
|
|
let p_right = s.get_player().unwrap();
|
|
s.perform_action(&AiAction::MoveLeft);
|
|
s.step(10);
|
|
let p_left = s.get_player().unwrap();
|
|
assert!(p_right.pos[0] < (px + 6) as f32, "blocked right");
|
|
assert!(p_left.pos[0] > (px - 6) as f32, "blocked left");
|
|
assert!((p_left.pos[0] - p_right.pos[0]).abs() < 12.0, "player should stay between walls");
|
|
}
|
|
|
|
#[test]
|
|
fn player_walks_up_slope() {
|
|
let mut s = GameSession::new_seeded(42);
|
|
s.init_empty();
|
|
s.clear_area(90, 100, 50, 35);
|
|
for x in 100..130 {
|
|
let h = ((x - 100) / 3).min(15);
|
|
for y in 0..h {
|
|
s.perform_action(&AiAction::SetCell { x, y: 135 - 1 - y, material: "stone".into() });
|
|
}
|
|
}
|
|
s.step(40);
|
|
let p = s.get_player().unwrap();
|
|
assert!(p.alive, "player should survive on slope");
|
|
assert!(p.pos[1] < 135.0, "player should be above floor on slope");
|
|
}
|
|
|
|
#[test]
|
|
fn entity_collision_with_dirt_wall() {
|
|
let mut s = setup();
|
|
s.perform_action(&AiAction::FillRect { x: 140, y: 125, w: 1, h: 5, material: "dirt".into() });
|
|
s.perform_action(&AiAction::Spawn { kind: "goblin".into(), x: 135.0, y: 120.0 });
|
|
s.step(40);
|
|
let entities = s.get_entities();
|
|
if let Some(g) = entities.into_iter().find(|e| e.kind == "Goblin" && e.alive) {
|
|
assert!(g.pos[0] < 140.0, "goblin should be blocked by dirt wall: x={}", g.pos[0]);
|
|
}
|
|
}
|