146 lines
4.7 KiB
Rust
146 lines
4.7 KiB
Rust
//! Landform + tunnel + dig at world scale: the acceptance shapes of the
|
|
//! unified destructible world, meshed to completion, with hard bounds on
|
|
//! how far any mesh vertex or heightfield vertex may stand from the ground.
|
|
//! Regression net for the "floating curtain wall" class of bug.
|
|
|
|
use makepad_game_sim::voxel::DigMode;
|
|
use makepad_game_sim::*;
|
|
use makepad_math::*;
|
|
|
|
fn rolling_terrain() -> Terrain {
|
|
let cells = 97;
|
|
let cell_size = 200.0 / 96.0;
|
|
let origin = -100.0;
|
|
let mut heights = vec![0.0f32; cells * cells];
|
|
for gz in 0..cells {
|
|
for gx in 0..cells {
|
|
let x = origin + gx as f32 * cell_size;
|
|
let z = origin + gz as f32 * cell_size;
|
|
heights[gz * cells + gx] = 2.0 + (x * 0.03).sin() + (z * 0.025).cos();
|
|
}
|
|
}
|
|
Terrain {
|
|
cells,
|
|
cell_size,
|
|
origin,
|
|
heights,
|
|
colors: vec![vec4f(0.4, 0.6, 0.4, 1.0); cells * cells],
|
|
revision: 1,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn acceptance_shapes_stay_bounded() {
|
|
let mut w = GameWorld::new();
|
|
w.terrain = Some(rolling_terrain());
|
|
// Mirror of the live acceptance script: dig, mountain, hill, tunnel.
|
|
let g_dig = w.surface_height_at(-20.0, 10.0).unwrap();
|
|
w.apply_voxel_op(VoxelOp::Dig {
|
|
pos: vec3f(-20.0, g_dig, 10.0),
|
|
r: 6.0,
|
|
mode: DigMode::Carve,
|
|
material: 1,
|
|
});
|
|
w.apply_voxel_op(VoxelOp::Landform {
|
|
pos: vec3f(45.0, w.surface_height_at(45.0, -30.0).unwrap(), -30.0),
|
|
kind: LandKind::Mountain.to_u8(),
|
|
r: 34.0,
|
|
height: 22.0,
|
|
seed: 9,
|
|
});
|
|
w.apply_voxel_op(VoxelOp::Landform {
|
|
pos: vec3f(-55.0, w.surface_height_at(-55.0, -50.0).unwrap(), -50.0),
|
|
kind: LandKind::Hill.to_u8(),
|
|
r: 24.0,
|
|
height: 12.0,
|
|
seed: 4,
|
|
});
|
|
let my = w.surface_height_at(-55.0, -20.0).unwrap() + 1.4;
|
|
w.apply_voxel_op(VoxelOp::Tunnel {
|
|
from: vec3f(-55.0, my, -20.0),
|
|
to: vec3f(-55.0, my, -80.0),
|
|
r: 2.4,
|
|
});
|
|
|
|
// Heights stay within base + mountain reach.
|
|
let t = w.terrain.as_ref().unwrap();
|
|
let (mut hmin, mut hmax) = (f32::MAX, f32::MIN);
|
|
for h in &t.heights {
|
|
assert!(h.is_finite(), "non-finite height");
|
|
hmin = hmin.min(*h);
|
|
hmax = hmax.max(*h);
|
|
}
|
|
assert!(hmax < 28.0, "heightfield exploded: max {hmax}");
|
|
assert!(hmin > -10.0, "heightfield exploded: min {hmin}");
|
|
|
|
// Mesh everything, then every voxel mesh vertex must hug the ground
|
|
// band — no floating curtain walls in the sky.
|
|
for _ in 0..4096 {
|
|
update_world_voxel(&mut w, true);
|
|
if w.voxel.as_deref().map_or(0, |v| v.dirty_len()) == 0 {
|
|
break;
|
|
}
|
|
}
|
|
let field = w.voxel.as_deref().unwrap();
|
|
let chunks = field.chunk_count();
|
|
assert!(chunks > 0, "nothing materialized");
|
|
assert!(chunks < 400, "materialization ran away: {chunks} chunks");
|
|
let mut vmax = f32::MIN;
|
|
let mut vmin = f32::MAX;
|
|
for mesh in field.meshes.values() {
|
|
for v in mesh.verts.chunks_exact(makepad_game_sim::voxel::MESH_VERTEX_FLOATS) {
|
|
vmax = vmax.max(v[1]);
|
|
vmin = vmin.min(v[1]);
|
|
}
|
|
}
|
|
println!(
|
|
"chunks {chunks}, heights {hmin:.2}..{hmax:.2}, mesh y {vmin:.2}..{vmax:.2}"
|
|
);
|
|
assert!(vmax < 28.0, "voxel mesh towers into the sky: max y {vmax}");
|
|
assert!(vmin > -30.0, "voxel mesh under the world: min y {vmin}");
|
|
|
|
// The tunnel bore is open air mid-hill.
|
|
assert!(
|
|
field.is_carved_air(vec3f(-55.0, my, -50.0)),
|
|
"tunnel bore closed"
|
|
);
|
|
// The seam sees the mountain.
|
|
let peak = w.surface_height_at(45.0, -30.0).unwrap();
|
|
assert!(peak > 10.0, "mountain missing from the seam: {peak}");
|
|
|
|
// A walker at the mouth strolls INTO the hill through the bore — under
|
|
// the raised ground, not over it (the tunnel is the voxel layer's whole
|
|
// point).
|
|
let id = {
|
|
let mut e = Entity::default();
|
|
w.next_id += 1;
|
|
e.id = w.next_id;
|
|
e.kind = BodyKind::Mover;
|
|
e.pos = vec3f(-55.0, my + 0.1, -16.0);
|
|
e.half = vec3f(0.35, 0.85, 0.35);
|
|
e.collide = true;
|
|
let id = e.id;
|
|
w.push_entity(e);
|
|
id
|
|
};
|
|
let mouth_y = my;
|
|
for _ in 0..600 {
|
|
if let Some(e) = w.entity_mut(id) {
|
|
e.vel.x = 0.0;
|
|
e.vel.z = -3.0;
|
|
}
|
|
step_world(&mut w);
|
|
}
|
|
let e = w.entity(id).unwrap();
|
|
assert!(
|
|
e.pos.z < -30.0,
|
|
"walker never entered the tunnel (z {})",
|
|
e.pos.z
|
|
);
|
|
let hill_here = w.terrain.as_ref().unwrap().height_at(e.pos.x, e.pos.z).unwrap();
|
|
assert!(
|
|
e.pos.y < mouth_y + 2.5 && e.pos.y < hill_here - 1.0,
|
|
"walker went OVER the hill instead of through it (y {} vs hill {hill_here})",
|
|
e.pos.y
|
|
);
|
|
}
|