pub mod floor;
pub mod hops;
pub mod pushout;
pub mod radial;
pub mod seam;
pub mod slope;
pub mod step;
use std::collections::BTreeMap;
use oxijolt::*;
use super::config::Config;
use super::controller::{origin_of, tick, TickReport, TraceTick};
use super::geometry::{classify, Element, FaceClass, Kind};
use super::scenes::{Scene, SceneKey};
use crate::common::math::{add, dot, norm, scale, sub, V3};
use crate::common::walker::{from_y_to, Carry, RADIUS};
use crate::common::DT;
pub const GAP_TOLERANCE: f64 = 0.03;
pub const HOP_RISE: f64 = 1e-3;
pub const STILL_DRIFT: f64 = 1e-3;
pub const STEEP_RISE: f64 = 1e-3;
pub const REST_TOLERANCE: f64 = 0.05;
pub const WALL_PUSH: f64 = 0.04;
pub const PUSH_LEAK: f64 = 2e-3;
pub const SEAM_UP: f64 = 2e-3;
pub const SEAM_ACROSS: f64 = 5e-3;
pub const SEAM_PATH: f64 = 0.01;
pub const SEAM_SURFACE: f64 = 5e-4;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Column {
Slope,
Floor,
StepSharp,
StepRounded,
Hops,
PushVelocity,
PushRecovery,
Seam,
Radial,
}
impl Column {
pub const ALL: [Column; 9] = [
Column::Slope,
Column::Floor,
Column::StepSharp,
Column::StepRounded,
Column::Hops,
Column::PushVelocity,
Column::PushRecovery,
Column::Seam,
Column::Radial,
];
pub fn label(self) -> &'static str {
match self {
Column::Slope => "1",
Column::Floor => "2",
Column::StepSharp => "3s",
Column::StepRounded => "3r",
Column::Hops => "4",
Column::PushVelocity => "5v",
Column::PushRecovery => "5r",
Column::Seam => "6",
Column::Radial => "radial",
}
}
pub fn index(self) -> usize {
Column::ALL.iter().position(|&c| c == self).unwrap()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Outcome {
Pass,
Fail {
tick: usize,
metric: &'static str,
value: f64,
},
Invalid {
reason: String,
},
}
impl Outcome {
pub fn fail(tick: usize, metric: &'static str, value: f64) -> Self {
Self::Fail {
tick,
metric,
value,
}
}
pub fn is_pass(&self) -> bool {
*self == Outcome::Pass
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Input {
Still,
Walk {
dir: V3,
speed: f64,
stop: Option<f64>,
},
StopRestart {
dir: V3,
speed: f64,
walk: usize,
pause: usize,
},
Teleport {
dir: V3,
speed: f64,
at: usize,
to: V3,
},
}
impl Input {
pub fn walk(dir: V3, speed: f64) -> Self {
Self::Walk {
dir,
speed,
stop: None,
}
}
pub fn desired(&self, scene: &Scene, tick: usize, origin: V3) -> V3 {
let (dir, speed) = match *self {
Input::Still => return [0.0; 3],
Input::Walk { dir, speed, stop } => {
let local = scene.frame.to_local_point(origin);
if stop.is_some_and(|stop| dot(local, dir) > stop) {
return [0.0; 3];
}
(dir, speed)
}
Input::StopRestart {
dir,
speed,
walk,
pause,
} => {
if (walk..walk + pause).contains(&tick) {
return [0.0; 3];
}
(dir, speed)
}
Input::Teleport { dir, speed, .. } => (dir, speed),
};
let world_dir = scene.frame.to_world_dir(dir);
scene.up.tangent(origin, world_dir, speed * f64::from(DT))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Check {
Slope(slope::Check),
Floor(floor::Check),
Step(step::Check),
Hops(hops::Check),
Push(pushout::Check),
Seam(seam::Check),
Radial(radial::Check),
}
#[derive(Clone, Debug)]
pub struct Case {
pub id: String,
pub column: Column,
pub scene: SceneKey,
pub start: V3,
pub rest_gap: Option<f64>,
pub carry: Carry,
pub input: Input,
pub ticks: usize,
pub check: Check,
pub reference: Option<Reference>,
pub report_only: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Reference {
pub scene: SceneKey,
pub start: V3,
pub input: Input,
}
pub const PERTURBATIONS: [[f64; 2]; 5] = [
[0.0, 0.0],
[1e-3, 0.0],
[-1e-3, 0.0],
[0.0, 1e-3],
[0.0, -1e-3],
];
#[derive(Clone, Debug)]
pub struct Run {
pub start: V3,
pub padding: f32,
pub startup: GroundState,
pub reports: Vec<TickReport>,
pub traces: Vec<TraceTick>,
}
#[derive(Default)]
pub struct Scenes(pub BTreeMap<SceneKey, Scene>);
impl Scenes {
pub fn get(&mut self, key: SceneKey) -> &mut Scene {
self.0.entry(key).or_insert_with(|| Scene::build(key))
}
}
pub fn rest_at(scene: &Scene, point: V3, gap: f64, padding: f32) -> V3 {
let target = gap + f64::from(RADIUS) + f64::from(padding);
let mut p = point;
for _ in 0..60 {
let up = scene.up.up_at(p);
let Some(distance) = scene.surface.distance(p) else {
return p;
};
let error = distance - target;
if error.abs() < 1e-12 {
break;
}
p = sub(p, scale(up, error));
}
p
}
pub fn resting(scene: &Scene, x: f64, z: f64) -> V3 {
rest_at(scene, scene.frame.to_world_point([x, 60.0, z]), 0.0, 0.02)
}
pub fn perturbed_start(scene: &Scene, case: &Case, offset: [f64; 2], padding: f32) -> V3 {
let up = scene.up.up_at(case.start);
let axis = |local: V3| {
let d = scene.frame.to_world_dir(local);
let flat = sub(d, scale(up, dot(d, up)));
scale(flat, 1.0 / norm(flat))
};
let shifted = add(
case.start,
add(
scale(axis([1.0, 0.0, 0.0]), offset[0]),
scale(axis([0.0, 0.0, 1.0]), offset[1]),
),
);
match case.rest_gap {
Some(gap) => rest_at(scene, shifted, gap, padding),
None => shifted,
}
}
pub fn play(scene: &mut Scene, config: &Config, case: &Case, start: V3, trace: bool) -> Run {
let walker = config.create_character(scene, start);
let padding = config.settings.padding;
let startup = scene.world.character(walker.id).unwrap().ground_state();
let mut carry = case.carry;
let mut reports = Vec::with_capacity(case.ticks);
let mut traces = Vec::new();
for t in 0..case.ticks {
if let Input::Teleport { at, to, .. } = case.input {
if t == at {
let up = scene.up.up_at(to);
scene
.world
.character_mut(walker.id)
.unwrap()
.set_position(super::config::position_for(to, up, padding))
.unwrap();
}
}
let origin = origin_of(&scene.world, &walker, padding);
let desired = case.input.desired(scene, t, origin);
let mut traced = TraceTick::default();
let report = tick(
scene,
&walker,
config,
&mut carry,
desired,
trace.then_some(&mut traced),
);
scene.place_actor(report.end, from_y_to(report.up));
reports.push(report);
if trace {
traces.push(traced);
}
}
scene.world.remove_character(walker.id).unwrap();
if config.settings.inner_body {
scene.world.optimize_broad_phase();
}
Run {
start,
padding,
startup,
reports,
traces,
}
}
pub fn evaluate(scenes: &mut Scenes, config: &Config, case: &Case, offset: [f64; 2]) -> Outcome {
let scene = scenes.get(case.scene);
let start = perturbed_start(scene, case, offset, config.settings.padding);
let run = play(scene, config, case, start, false);
let reference = reference_run(scenes, config, case, start);
judge(scenes, case, &run, reference.as_ref())
}
pub fn reference_run(scenes: &mut Scenes, config: &Config, case: &Case, start: V3) -> Option<Run> {
case.reference.map(|reference| {
let other = scenes.get(reference.scene);
let shifted = add(reference.start, sub(start, case.start));
let mirrored = Case {
input: reference.input,
..case.clone()
};
play(other, config, &mirrored, shifted, false)
})
}
pub fn judge(scenes: &mut Scenes, case: &Case, run: &Run, reference: Option<&Run>) -> Outcome {
let scene = scenes.get(case.scene);
match case.check {
Check::Slope(check) => slope::judge(scene, check, run),
Check::Floor(check) => floor::judge(scene, check, run),
Check::Step(check) => step::judge(scene, check, run),
Check::Hops(check) => hops::judge(scene, check, run),
Check::Push(check) => pushout::judge(scene, check, run, reference),
Check::Seam(check) => {
let continuous = case.reference.map(|reference| reference.scene);
seam::judge(scenes, check, case.scene, continuous, run, reference)
}
Check::Radial(check) => radial::judge(scene, check, run, reference),
}
}
#[derive(Clone, Debug)]
pub struct Ground {
pub gap: f64,
pub support: Vec<Element>,
pub closest: Option<Element>,
}
impl Ground {
pub fn at(scene: &Scene, origin: V3, padding: f32) -> Self {
let support = scene.surface.support_set(origin);
let closest = support.first().copied();
let gap = closest.map_or(f64::INFINITY, |e| {
e.distance - f64::from(RADIUS) - f64::from(padding)
});
Self {
gap,
support,
closest,
}
}
pub fn all_steep_terrain(&self, up: V3) -> bool {
!self.support.is_empty()
&& self
.support
.iter()
.all(|e| e.kind == Kind::Terrain && classify(e.normal, up) == FaceClass::Steep)
}
pub fn walkable_applicable(&self, up: V3) -> bool {
self.closest
.is_some_and(|e| classify(e.normal, up) == FaceClass::Walkable)
&& self
.support
.iter()
.all(|e| classify(e.normal, up) != FaceClass::Boundary)
}
pub fn boundary(&self, up: V3) -> bool {
self.support
.iter()
.any(|e| classify(e.normal, up) == FaceClass::Boundary)
}
}
pub fn grounds(scene: &Scene, run: &Run) -> Vec<Ground> {
run.reports
.iter()
.map(|report| Ground::at(scene, report.end, run.padding))
.collect()
}
pub fn rise(report: &TickReport) -> f64 {
dot(sub(report.end, report.start), report.up)
}
pub fn blocked_tick(run: &Run) -> Option<usize> {
run.reports.iter().position(|report| {
let wanted = norm(report.velocity_wanted());
wanted > 0.0 && {
let along = scale(report.velocity_wanted(), 1.0 / wanted);
let done = sub(report.end, report.start);
let flat = sub(done, scale(report.up, dot(done, report.up)));
dot(flat, along) < 0.1 * wanted * f64::from(DT)
}
})
}
pub fn missing_witness(run: &Run, what: &str) -> Outcome {
match blocked_tick(run) {
Some(tick) => Outcome::fail(tick, "blocked before the witness", 0.0),
None => Outcome::Invalid {
reason: format!("witness not reached: {what}"),
},
}
}
impl TickReport {
pub fn velocity_wanted(&self) -> V3 {
let v = crate::common::math::f3(self.supplied);
sub(v, scale(self.up, dot(v, self.up)))
}
}