Expand voxel gameplay, lighting, full-height streaming and world imports
MVP checks / mvp (push) Canceled after 0s
MVP checks / mvp (push) Canceled after 0s
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.
This commit is contained in:
+7814
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,624 @@
|
||||
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.);
|
||||
}
|
||||
Reference in New Issue
Block a user