makepad/libs/game/render/tests/prop_collision.rs
andodeki 435135ed50 feat(arcade): port makepad game libs + arcade from makepad-internal onto the fork
Reintroduce apps/arcade and the ten game crates that were ripped out of
the fork (assets, audio, blocks, coedit, gen, net, pkg, render, script,
session), from makepad-internal/dev. Reconcile against the fork's layout:

- makepad-game-sim/math come from the fork's existing libs/sim + sim/math;
  the copied internal libs/game/{sim,math} are dropped and the kept game
  crates plus arcade point at ../../sim and ../../sim/math.
- arcade AI is rewritten off the removed makepad-ai agent API onto the
  makepad-ai-hub headless ChatProvider worker (example/cad pattern):
  AiWorker{send,cancel,poll}, ai_worker_loop driving
  ClaudeApiChatProvider(ClaudeCli), AiWorkerEvent availability/delta/
  done/error mapped into the chat feed and authoring land_edit, with
  main.rs send_message/cancel_request/event-drain rebuilt around it.
- the game-script sandbox is adapted to the fork's jailed splash storage:
  every isolate gets mod.fs = splash_storage jail; splaes splash_storage
  set_root_for_heap is now public so the ScriptHost (or Splash) can grant
  a per-game jail root. ENT font: build.rs/dispatch.rs entities fill new
  fork sim Entity/Part fields via ..Default::default(); world_raycast now
  borrows mutably and returns the material id.

cargo check -p makepad-arcade and the game + arcade test suites pass.
2026-09-05 05:49:12 +03:00

155 lines
5.1 KiB
Rust

//! A walker must be stopped by a stock prop's real collider.
//!
//! The existing collision tests pass a house wall and a tree trunk, both of
//! which are large parts of large models. A park bench is small, thin and
//! close to the ground, and the user reported walking straight through one
//! three times — so the only test worth having here uses the ACTUAL bench
//! model, scaled and placed the way the demo places it, rather than a
//! synthetic box that would have passed all along.
//!
//! Skips (rather than fails) when the asset packs are absent, so a fresh
//! checkout without `apps/arcade/download_assets.sh` still runs green.
use makepad_game_render::model::StaticModel;
use makepad_game_sim::*;
use makepad_draw::*;
const PACKS: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../../apps/arcade/resources/models/kenney"
);
fn load(rel: &str) -> Option<StaticModel> {
let bytes = std::fs::read(std::path::Path::new(PACKS).join(rel)).ok()?;
StaticModel::parse_glb(&bytes).ok()
}
fn ent(id: u64, kind: BodyKind, pos: Vec3f, half: Vec3f) -> Entity {
Entity {
id,
kind,
shape: Shape::Box,
pos,
half,
collide: true,
gravity_scale: 1.0,
density: 1.0,
friction: 0.6,
scale: vec3f(1.0, 1.0, 1.0),
scale_target: vec3f(1.0, 1.0, 1.0),
speed_mult: 1.0,
..Default::default()
}
}
/// Reproduces `ArcadeView::compose_village`'s placement maths: scale the model
/// to a target height, then turn each collider part into a world-space box.
fn prop_collider_boxes(m: &StaticModel, target_h: f32, at: Vec3f) -> Vec<(Vec3f, Vec3f)> {
let native_h = (m.max.y - m.min.y).max(0.001);
let s = target_h / native_h;
m.collider_parts()
.iter()
.map(|(a, b)| {
let c = vec3f(
(a.x + b.x) * 0.5 * s,
(a.y + b.y) * 0.5 * s,
(a.z + b.z) * 0.5 * s,
);
let e = vec3f(
(b.x - a.x) * 0.5 * s,
(b.y - a.y) * 0.5 * s,
(b.z - a.z) * 0.5 * s,
);
(
vec3f(at.x + c.x, c.y.max(e.y), at.z + c.z),
vec3f(e.x.max(0.08), e.y.max(0.08), e.z.max(0.08)),
)
})
.collect()
}
/// The bench must produce a collider that is actually in the walker's way:
/// tall enough to intersect a person and wide enough to be hit. A bench that
/// collapses to the 0.08 minimum in any axis is scenery, not furniture.
#[test]
fn a_bench_produces_a_collider_a_person_can_hit() {
let Some(m) = load("graveyard-kit/bench.glb").or_else(|| load("coaster-kit/bench.glb")) else {
eprintln!("skip: no bench model — run apps/arcade/download_assets.sh");
return;
};
let boxes = prop_collider_boxes(&m, 0.9, vec3f(0.0, 0.0, 0.0));
assert!(!boxes.is_empty(), "bench produced no collider at all");
let (pos, half) = boxes[0];
// Reaches from the ground up to roughly seat height...
assert!(
pos.y - half.y < 0.15,
"bench collider floats: base at {}",
pos.y - half.y
);
assert!(
pos.y + half.y > 0.55,
"bench collider too short to stop anyone: top at {}",
pos.y + half.y
);
// ...and is a real obstacle in plan, not a sliver.
assert!(
half.x.min(half.z) > 0.1,
"bench collider is a sliver: half = {half:?}"
);
}
/// The user's actual complaint: walking the pavement into a bench and passing
/// through it. Walks a person-sized mover along the demo's patrol line into a
/// bench placed on that line, and requires that he stops short of it.
#[test]
fn a_walker_is_stopped_by_a_real_bench() {
let Some(m) = load("graveyard-kit/bench.glb").or_else(|| load("coaster-kit/bench.glb")) else {
eprintln!("skip: no bench model — run apps/arcade/download_assets.sh");
return;
};
let mut w = GameWorld::new();
w.reset_content();
w.push_entity(ent(
1,
BodyKind::Static,
vec3f(0.0, -0.5, 0.0),
vec3f(30.0, 0.5, 30.0),
));
// A bench on the walking line, five units along.
let bench_x = 5.0;
let mut next_id = 2;
for (pos, half) in prop_collider_boxes(&m, 0.9, vec3f(bench_x, 0.0, 0.0)) {
w.push_entity(ent(next_id, BodyKind::Static, pos, half));
next_id += 1;
}
let bench_near_face = w
.entities
.iter()
.filter(|e| e.id >= 2)
.map(|e| e.pos.x - e.half.x)
.fold(f32::MAX, f32::min);
// Person-sized mover, same half-extents the demo gives the Knight.
let walker_half = vec3f(0.7, 1.8, 0.7);
w.push_entity(ent(
next_id,
BodyKind::Mover,
vec3f(0.0, walker_half.y, 0.0),
walker_half,
));
// Walk into it for two seconds: unobstructed he would reach x = 6.4.
for _ in 0..120 {
if let Some(e) = w.entity_mut(next_id) {
e.vel.x = 3.2;
}
step_world(&mut w);
}
let x = w.entity(next_id).unwrap().pos.x;
assert!(
x < bench_near_face - walker_half.x + 0.05,
"walked through the bench: stopped at {x}, bench face at {bench_near_face}"
);
}