Files
Emil c7e86663d8
MVP checks / mvp (push) Canceled after 0s
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

625 lines
17 KiB
Rust

use shacraft_physics::{
Aabb, CollisionBlock, Controls, PhysicsBody, PhysicsSettings, PhysicsWorld, Pose, step,
};
fn block(pos: [i32; 3], state: &str, collision: Vec<Aabb>) -> CollisionBlock {
CollisionBlock {
pos,
state: format!("minecraft:{state}"),
collision,
}
}
fn floor(state: &str) -> PhysicsWorld {
let mut world = PhysicsWorld::default();
for x in -12..=12 {
for z in -60..=60 {
world
.blocks
.push(block([x, -1, z], state, vec![Aabb::unit()]));
}
}
world
}
fn player() -> PhysicsBody {
PhysicsBody {
position: [0.5, 0., 0.5],
velocity: [0., -0.0784000015258789, 0.],
on_ground: true,
..Default::default()
}
}
fn run(body: &mut PhysicsBody, input: &Controls, world: &PhysicsWorld, ticks: usize) {
for _ in 0..ticks {
step(body, input, world, &PhysicsSettings::default());
}
}
#[test]
fn original_java26_2_travel_kernel_trajectories() {
let fixture: serde_json::Value =
serde_json::from_str(include_str!("fixtures/java26.2.json")).unwrap();
for (case, trajectory) in fixture["travel_kernel_trajectories"].as_object().unwrap() {
let medium = trajectory["medium"].as_str().unwrap_or("air");
// A fixed shallow-fluid depth is a kernel-only harness condition;
// the integrated world correctly changes depth as a player falls.
if case == "lava_shallow_move_stop" {
continue;
}
let world = if medium == "air" {
floor(trajectory["surface"].as_str().unwrap())
} else {
let mut world = liquid_world(&format!("{medium}[level=0]"));
for block in &mut world.blocks {
block.pos[1] += 10;
}
world
};
let mut body = PhysicsBody {
position: serde_json::from_value(trajectory["initial_position"].clone()).unwrap(),
velocity: serde_json::from_value(trajectory["initial_velocity"].clone()).unwrap(),
flying: trajectory["flying"].as_bool().unwrap_or(false),
on_ground: medium == "air" && !trajectory["flying"].as_bool().unwrap_or(false),
..Default::default()
};
let input_ticks = trajectory["input_ticks"].as_u64().unwrap();
for sample in trajectory["samples"].as_array().unwrap() {
let tick = sample["tick"].as_u64().unwrap();
// Kernel fixture keeps sprint asserted after input release; the
// integrated player controller ends swimming at release instead.
if medium == "water" && trajectory["sprint"].as_bool().unwrap() && tick > input_ticks {
break;
}
let input = Controls {
forward: if tick <= input_ticks { 1. } else { 0. },
sprint: trajectory["sprint"].as_bool().unwrap(),
jump: tick == 1 && trajectory["jump_first_tick"].as_bool().unwrap(),
..Default::default()
};
step(
&mut body,
&input,
&world,
&PhysicsSettings {
allow_flight: true,
..Default::default()
},
);
// Java yaw zero faces +Z; Shacraft yaw zero faces -Z.
for (field, actual) in [("position", body.position), ("velocity", body.velocity)] {
for axis in 0..3 {
let expected =
sample[field][axis].as_f64().unwrap() * if axis == 2 { -1. } else { 1. };
assert!(
(actual[axis] - expected).abs() < 2.0e-6,
"{case} tick {tick}: {field}[{axis}] actual {} expected {expected}",
actual[axis]
);
}
}
assert_eq!(
body.on_ground,
sample["on_ground"].as_bool().unwrap(),
"{case} tick {tick}"
);
}
}
}
#[test]
fn full_cube_stops_player_but_half_slab_is_stepped_without_jump() {
let mut wall = floor("stone");
wall.blocks
.push(block([0, 0, -1], "stone", vec![Aabb::unit()]));
let mut blocked = player();
run(
&mut blocked,
&Controls {
forward: 1.,
..Default::default()
},
&wall,
20,
);
assert!((blocked.position[2] - blocked.width() / 2.).abs() < 1.0e-6);
assert_eq!(blocked.position[1], 0.);
wall.blocks.last_mut().unwrap().collision[0].max[1] = 0.5;
let mut climbed = player();
run(
&mut climbed,
&Controls {
forward: 1.,
..Default::default()
},
&wall,
5,
);
assert!(climbed.position[2] < 0. && climbed.position[1] >= 0.5);
}
#[test]
fn step_up_preserves_clearance_under_low_ceiling() {
let mut world = floor("stone");
world.blocks.push(block(
[0, 0, -1],
"stone_slab[type=bottom]",
vec![Aabb::new([0.; 3], [1., 0.5, 1.])],
));
world
.blocks
.push(block([0, 2, -1], "stone", vec![Aabb::unit()]));
let mut body = player();
run(
&mut body,
&Controls {
forward: 1.,
..Default::default()
},
&world,
12,
);
assert!(
body.position[2] >= body.width() / 2. - 1.0e-6,
"standing body must not step into a ceiling"
);
}
#[test]
fn sneaking_keeps_support_at_straight_and_diagonal_edges() {
let world = PhysicsWorld {
blocks: vec![block([0, -1, 0], "stone", vec![Aabb::unit()])],
};
for strafe in [0., 1.] {
let mut body = player();
run(
&mut body,
&Controls {
forward: 1.,
strafe,
sneak: true,
..Default::default()
},
&world,
120,
);
assert_eq!(body.position[1], 0.);
assert!(body.position[2] > -body.width() / 2.);
assert!(body.position[0] < 1. + body.width() / 2.);
assert_eq!(body.pose, Pose::Crouching);
}
let mut walking = player();
run(
&mut walking,
&Controls {
forward: 1.,
..Default::default()
},
&world,
20,
);
assert!(walking.position[1] < -1.);
}
#[test]
fn crawl_pose_persists_until_there_is_headroom() {
let mut world = floor("stone");
world
.blocks
.push(block([0, 1, 0], "stone", vec![Aabb::unit()]));
let mut body = PhysicsBody {
pose: Pose::Swimming,
..player()
};
step(
&mut body,
&Controls::default(),
&world,
&PhysicsSettings::default(),
);
assert_eq!(body.pose, Pose::Swimming);
world.blocks.pop();
step(
&mut body,
&Controls::default(),
&world,
&PhysicsSettings::default(),
);
assert_eq!(body.pose, Pose::Standing);
}
#[test]
fn diagonal_input_is_normalized_and_air_control_is_weaker() {
let world = floor("stone");
let mut straight = player();
let mut diagonal = player();
run(
&mut straight,
&Controls {
forward: 1.,
..Default::default()
},
&world,
20,
);
run(
&mut diagonal,
&Controls {
forward: 1.,
strafe: 1.,
..Default::default()
},
&world,
20,
);
let a = (straight.position[0] - 0.5).hypot(straight.position[2] - 0.5);
let b = (diagonal.position[0] - 0.5).hypot(diagonal.position[2] - 0.5);
assert!(
(b / a - 1. / 0.98).abs() < 1.0e-6,
"vanilla normalizes a diagonal after multiplying input by .98"
);
let mut air = PhysicsBody {
position: [0., 20., 0.],
..Default::default()
};
step(
&mut air,
&Controls {
forward: 1.,
..Default::default()
},
&world,
&PhysicsSettings::default(),
);
assert!((air.position[2] + 0.019600000381469584).abs() < 1.0e-6);
}
#[test]
fn held_jump_has_ten_tick_cooldown_and_release_rearms() {
let world = floor("stone");
let mut body = player();
let jump = Controls {
jump: true,
..Default::default()
};
step(&mut body, &jump, &world, &PhysicsSettings::default());
assert_eq!(body.jump_cooldown, 10);
body.position[1] = 0.;
body.velocity = [0., -0.08, 0.];
body.on_ground = true;
step(&mut body, &jump, &world, &PhysicsSettings::default());
assert_eq!(body.position[1], 0.);
step(
&mut body,
&Controls::default(),
&world,
&PhysicsSettings::default(),
);
step(&mut body, &jump, &world, &PhysicsSettings::default());
assert!(body.position[1] > 0.4);
}
fn liquid_world(state: &str) -> PhysicsWorld {
let mut world = PhysicsWorld::default();
for x in -3..=3 {
for z in -12..=3 {
for y in -4..=4 {
world.blocks.push(block([x, y, z], state, vec![]));
}
}
}
world
}
#[test]
fn water_swimming_lava_and_waterlogged_cells_apply_distinct_motion() {
let mut water = PhysicsBody {
position: [0.5, 0., 0.5],
..Default::default()
};
let mut lava = water.clone();
let input = Controls {
forward: 1.,
..Default::default()
};
run(&mut water, &input, &liquid_world("water[level=0]"), 20);
run(&mut lava, &input, &liquid_world("lava[level=0]"), 20);
assert!(water.in_water && !water.in_lava && lava.in_lava);
assert!(water.position[2] < lava.position[2]);
assert!(water.velocity[1] > lava.velocity[1]);
let mut swimmer = PhysicsBody {
position: [0.5, 0., 0.5],
..Default::default()
};
run(
&mut swimmer,
&Controls {
sprint: true,
forward: 1.,
pitch: 0.6,
..Default::default()
},
&liquid_world("water[level=0]"),
10,
);
assert!(swimmer.swimming && swimmer.pose == Pose::Swimming);
assert!(swimmer.position[1] > 0.);
let mut logged = PhysicsBody {
position: [0.5, 0., 0.5],
..Default::default()
};
run(
&mut logged,
&input,
&liquid_world("oak_sign[waterlogged=true]"),
2,
);
assert!(logged.in_water);
}
#[test]
fn climbable_clamps_fall_and_jump_climbs() {
let world = liquid_world("ladder[facing=north]");
let mut body = PhysicsBody {
position: [0.5, 2., 0.5],
velocity: [0., -2., 0.],
..Default::default()
};
step(
&mut body,
&Controls::default(),
&world,
&PhysicsSettings::default(),
);
assert!(body.on_climbable && body.position[1] > 1.84);
let height = body.position[1];
run(
&mut body,
&Controls {
jump: true,
..Default::default()
},
&world,
5,
);
assert!(body.position[1] > height);
let height = body.position[1];
body.velocity[1] = -0.1;
run(
&mut body,
&Controls {
sneak: true,
..Default::default()
},
&world,
5,
);
assert!((body.position[1] - height).abs() < 1.0e-6);
}
#[test]
fn slime_bounces_without_sneak_and_honey_limits_jump() {
let world = floor("slime_block");
let mut body = PhysicsBody {
position: [0.5, 0.1, 0.5],
velocity: [0., -0.9, 0.],
..Default::default()
};
step(
&mut body,
&Controls::default(),
&world,
&PhysicsSettings::default(),
);
assert!(body.velocity[1] > 0.7);
let mut sneaking = PhysicsBody {
position: [0.5, 0.1, 0.5],
velocity: [0., -0.9, 0.],
..Default::default()
};
step(
&mut sneaking,
&Controls {
sneak: true,
..Default::default()
},
&world,
&PhysicsSettings::default(),
);
assert!(sneaking.velocity[1] < 0.);
let mut honey = player();
step(
&mut honey,
&Controls {
jump: true,
..Default::default()
},
&floor("honey_block"),
&PhysicsSettings::default(),
);
assert!((honey.position[1] - 0.21).abs() < 1.0e-6);
}
#[test]
fn cobweb_slows_falling_and_bubble_column_lifts() {
let mut web = PhysicsBody {
position: [0.5, 2., 0.5],
velocity: [0., -1., 0.],
..Default::default()
};
step(
&mut web,
&Controls::default(),
&liquid_world("cobweb"),
&PhysicsSettings::default(),
);
assert!((web.position[1] - 1.95).abs() < 1.0e-6);
let mut bubble = PhysicsBody {
position: [0.5, 0., 0.5],
..Default::default()
};
run(
&mut bubble,
&Controls::default(),
&liquid_world("bubble_column[drag=false]"),
10,
);
assert!(bubble.position[1] > 0.5 && bubble.velocity[1] > 0.);
}
#[test]
fn powder_snow_and_scaffolding_have_entity_sensitive_support() {
for material in ["powder_snow", "scaffolding"] {
let world = PhysicsWorld {
blocks: vec![block([0, -1, 0], material, vec![Aabb::unit()])],
};
let mut body = player();
let settings = PhysicsSettings {
leather_boots: true,
..Default::default()
};
step(&mut body, &Controls::default(), &world, &settings);
assert_eq!(body.position[1], 0., "{material}");
run(
&mut body,
&Controls {
sneak: true,
..Default::default()
},
&world,
5,
);
assert!(body.position[1] < 0., "{material}");
}
}
#[test]
fn flight_is_authorized_and_frozen_body_does_not_move() {
let mut denied = player();
step(
&mut denied,
&Controls {
fly_toggle: true,
..Default::default()
},
&PhysicsWorld::default(),
&PhysicsSettings::default(),
);
assert!(!denied.flying);
let mut flight = player();
let settings = PhysicsSettings {
allow_flight: true,
..Default::default()
};
step(
&mut flight,
&Controls {
fly_toggle: true,
jump: true,
..Default::default()
},
&PhysicsWorld::default(),
&settings,
);
assert!(flight.flying && flight.position[1] > 0.);
let position = flight.position;
step(
&mut flight,
&Controls {
jump: true,
forward: 1.,
..Default::default()
},
&PhysicsWorld::default(),
&PhysicsSettings {
frozen: true,
..settings
},
);
assert_eq!(flight.position, position);
assert_eq!(flight.velocity, [0.; 3]);
}
#[test]
fn fluid_and_callback_results_do_not_depend_on_block_serialization_order() {
let mut world = PhysicsWorld {
blocks: vec![
block([0, 0, 0], "water[level=5]", vec![]),
block([1, 0, 0], "water[level=7]", vec![]),
block([0, 0, 1], "water[level=2]", vec![]),
block([1, 0, 1], "water[level=6]", vec![]),
block([-1, 0, 0], "water[level=0]", vec![]),
],
};
let start = PhysicsBody {
position: [0.95, 0., 0.95],
..Default::default()
};
let mut forward = start.clone();
step(
&mut forward,
&Controls::default(),
&world,
&PhysicsSettings::default(),
);
world.blocks.reverse();
let mut reversed = start;
step(
&mut reversed,
&Controls::default(),
&world,
&PhysicsSettings::default(),
);
assert_eq!(forward.position, reversed.position);
assert_eq!(forward.velocity, reversed.velocity);
assert!(
forward.velocity[0].abs() + forward.velocity[2].abs() > 0.,
"fixture must exercise current pushing"
);
}
#[test]
fn shallow_grounded_water_jump_uses_the_normal_jump_impulse() {
let mut world = floor("stone");
world
.blocks
.push(block([0, 0, 0], "water[level=7]", vec![]));
let mut body = player();
step(
&mut body,
&Controls {
jump: true,
..Default::default()
},
&world,
&PhysicsSettings::default(),
);
assert!((body.position[1] - 0.41999998688697815).abs() < 1.0e-12);
assert_eq!(body.jump_cooldown, 10);
}
#[test]
fn ground_flight_shortcut_hovers_and_landing_still_cancels_flight() {
let world = floor("stone");
let settings = PhysicsSettings {
allow_flight: true,
..Default::default()
};
let mut body = player();
step(&mut body, &Controls::default(), &world, &settings);
assert!(body.on_ground && body.velocity[1] < 0.);
step(
&mut body,
&Controls {
fly_toggle: true,
..Default::default()
},
&world,
&settings,
);
assert!(body.flying && !body.on_ground);
for _ in 0..5 {
step(&mut body, &Controls::default(), &world, &settings);
assert!(body.flying && !body.on_ground);
assert_eq!(body.position[1], 0.);
assert_eq!(body.velocity[1], 0.);
}
body.position[1] = 0.1;
body.velocity[1] = -0.2;
step(&mut body, &Controls::default(), &world, &settings);
assert!(body.on_ground && !body.flying);
assert_eq!(body.position[1], 0.);
}