use bevy::math::Vec3;
use crate::CarnageSettings;
use crate::soup::hash_f32;
use crate::wound::Wound;
pub fn trauma_for(w: &Wound, s: &CarnageSettings) -> f32 {
(s.trauma_per_wound * w.severity.clamp(0.0, 1.0)).clamp(0.0, 1.0)
}
pub fn hitstop_ticks(w: &Wound, hz: u32, s: &CarnageSettings) -> u32 {
let seconds = s.hitstop_seconds * w.severity.clamp(0.0, 1.0);
if !(seconds > 0.0) || !seconds.is_finite() {
return 0;
}
let ticks = seconds * hz as f32;
if !ticks.is_finite() || ticks < 1.0 {
return 0;
}
ticks as u32
}
pub fn shake_offset(trauma: f32, dir: Vec3, tick: u32, s: &CarnageSettings) -> Vec3 {
let trauma = trauma.clamp(0.0, 1.0);
if trauma <= 0.0 || s.shake_ticks == 0 {
return Vec3::ZERO;
}
let axis = dir.normalize_or_zero();
if axis == Vec3::ZERO {
return Vec3::ZERO;
}
let phase = (tick % s.shake_ticks) as f32 / s.shake_ticks as f32;
let ease = (1.0 - phase) * (1.0 - phase);
let wave = hash_f32(tick.wrapping_mul(0x9E37_79B9)) * 2.0 - 1.0;
axis * (s.shake_amplitude * trauma * trauma * ease * wave)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wound::WoundKind;
const HZ: u32 = 60;
fn wound(severity: f32) -> Wound {
Wound {
at: Vec3::new(0.1, 0.9, -0.2),
normal: Vec3::X,
area: 0.004,
severity,
kind: WoundKind::Severance,
}
}
#[test]
fn trauma_tracks_severity_and_stays_bounded() {
let s = CarnageSettings::default();
assert_eq!(trauma_for(&wound(0.0), &s), 0.0, "a wound that is not open costs nothing");
assert_eq!(trauma_for(&wound(1.0), &s), s.trauma_per_wound, "a full wound is the dial");
assert!(
trauma_for(&wound(0.5), &s) < trauma_for(&wound(1.0), &s),
"half a wound must be less trauma"
);
for severity in [-3.0f32, 0.25, 1.0, 17.0] {
let t = trauma_for(&wound(severity), &s);
assert!((0.0..=1.0).contains(&t), "trauma {t} from severity {severity} is out of range");
}
}
#[test]
fn a_graze_gets_no_hitstop() {
let s = CarnageSettings::default();
assert_eq!(hitstop_ticks(&wound(1.0), HZ, &s), 3, "0.055 s at 60 Hz is three whole ticks");
assert_eq!(hitstop_ticks(&wound(0.0), HZ, &s), 0, "no wound, no stop");
assert_eq!(hitstop_ticks(&wound(0.2), HZ, &s), 0, "a graze must not freeze the frame");
assert!(
hitstop_ticks(&wound(1.0), 120, &s) > hitstop_ticks(&wound(1.0), HZ, &s),
"the same duration is more ticks at a higher rate"
);
}
#[test]
fn the_shake_is_a_pure_function_of_the_tick() {
let s = CarnageSettings::default();
for tick in [0u32, 1, 7, 60, 12_345, u32::MAX] {
let a = shake_offset(0.7, Vec3::X, tick, &s);
let b = shake_offset(0.7, Vec3::X, tick, &s);
assert_eq!(a, b, "tick {tick} shook differently the second time");
}
}
#[test]
fn the_shake_is_along_the_direction_it_was_given() {
let s = CarnageSettings::default();
for dir in [Vec3::X, Vec3::Y, Vec3::new(-0.3, 0.9, 0.2)] {
let axis = dir.normalize();
for tick in 0..200u32 {
let o = shake_offset(1.0, dir, tick, &s);
if o == Vec3::ZERO {
continue;
}
let along = o.normalize();
assert!(
(along.dot(axis).abs() - 1.0).abs() < 1.0e-4,
"tick {tick}: offset {o:?} is not colinear with {axis:?}"
);
}
}
assert_eq!(
shake_offset(1.0, Vec3::ZERO, 5, &s),
Vec3::ZERO,
"a wound with no normal must not shake along an invented axis"
);
}
#[test]
fn the_magnitude_is_trauma_squared_and_bounded() {
let s = CarnageSettings::default();
assert_eq!(shake_offset(0.0, Vec3::X, 3, &s), Vec3::ZERO, "no trauma, no shake");
let (mut peak_half, mut peak_full) = (0.0f32, 0.0f32);
for tick in 0..1_000u32 {
let half = shake_offset(0.5, Vec3::X, tick, &s).length();
let full = shake_offset(1.0, Vec3::X, tick, &s).length();
assert!(
full <= s.shake_amplitude + 1.0e-6,
"tick {tick}: |offset| {full} exceeds the amplitude dial {}",
s.shake_amplitude
);
peak_half = peak_half.max(half);
peak_full = peak_full.max(full);
}
let ratio = peak_full / peak_half;
assert!(
(ratio - 4.0).abs() < 0.2,
"peak shake scaled by {ratio:.3} between trauma 0.5 and 1.0; trauma squared requires ~4"
);
}
#[test]
fn each_shake_cycle_settles_to_nothing() {
let s = CarnageSettings::default();
let last = s.shake_ticks - 1;
let ease_at = |tick: u32| {
let phase = (tick % s.shake_ticks) as f32 / s.shake_ticks as f32;
(1.0 - phase) * (1.0 - phase)
};
for t in 0..last {
assert!(ease_at(t) > ease_at(t + 1), "the ease rose from tick {t} to {}", t + 1);
}
assert!(ease_at(last) < ease_at(0) * 0.05, "the cycle must nearly vanish by its last tick");
}
#[test]
fn a_zero_shake_period_cannot_panic() {
let mut s = CarnageSettings::default();
s.shake_ticks = 0;
assert!(s.validate().is_err(), "the door must refuse it");
assert_eq!(shake_offset(1.0, Vec3::X, 9, &s), Vec3::ZERO, "and it must not panic here");
}
}