use crate::check::{Check, CheckCtx, Readiness};
use crate::checks::root_motion_readiness;
use crate::finding::{Finding, Severity};
use crate::metrics::root_motion_speed_mps;
pub const TRAVEL_THRESHOLD_MPS: f64 = 0.5;
pub struct InPlace;
impl Check for InPlace {
fn id(&self) -> &'static str {
"in-place"
}
fn readiness(&self, ctx: &CheckCtx) -> Readiness {
let any = ctx.clip_expectations().iter().any(|e| e.in_place.is_some());
if any {
root_motion_readiness(ctx.roles)
} else {
Readiness::Idle
}
}
fn run(&self, ctx: &CheckCtx, out: &mut Vec<Finding>) {
for (index, clip) in ctx.doc.clips.iter().enumerate() {
let Some(expected) = ctx.expectations(index).in_place else {
continue;
};
let measured = ctx
.grid(index)
.and_then(|grid| root_motion_speed_mps(&grid, ctx.roles));
let Some(speed) = measured else {
continue; };
let travels = speed >= TRAVEL_THRESHOLD_MPS;
if expected && travels {
out.push(
Finding::new(
self.id(),
Severity::Error,
format!(
"declared in-place but the root travels at {speed:.2} m/s — \
the character will glide at runtime"
),
)
.clip(&clip.name)
.measured(speed)
.expected(0.0f64),
);
} else if !expected && !travels {
out.push(
Finding::new(
self.id(),
Severity::Error,
format!(
"declared root-motion but the root is stationary \
({speed:.2} m/s) — the character will run in place"
),
)
.clip(&clip.name)
.measured(speed),
);
}
}
}
}