use bevy::math::Vec3;
use crate::CarnageSettings;
use crate::spatter::Stain;
const POOL_CAP_SLACK: f32 = 3.0;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Pool {
pub at: Vec3,
pub wetted: f32,
pub radius: f32,
pub age: u32,
pub seed: u32,
}
pub fn absorb(pools: &mut Vec<Pool>, stains: &[Stain], s: &CarnageSettings) {
let merge_r2 = s.pool_merge_radius * s.pool_merge_radius;
let slack_r2 = merge_r2 * POOL_CAP_SLACK * POOL_CAP_SLACK;
for stain in stains {
let full = pools.len() >= s.max_pools as usize;
let reach2 = if full { slack_r2 } else { merge_r2 };
let hit = pools.iter_mut().find(|p| p.at.distance_squared(stain.at) <= reach2);
if let Some(p) = hit {
p.wetted += stain.radius * stain.radius;
continue;
}
if full {
continue;
}
pools.push(Pool {
at: stain.at,
wetted: stain.radius * stain.radius,
radius: stain.radius,
age: 0,
seed: stain.seed,
});
}
}
pub fn spread_pools(pools: &mut [Pool], s: &CarnageSettings) {
for p in pools.iter_mut() {
let target = (p.wetted / std::f32::consts::PI).sqrt() * s.pool_spread;
p.radius += (target - p.radius) * s.pool_spread_rate;
p.age = p.age.saturating_add(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wound::{Wound, WoundKind};
fn fixed_wound() -> Wound {
Wound {
at: Vec3::new(0.1, 0.9, -0.2),
normal: Vec3::X,
area: 0.004,
severity: 1.0,
kind: WoundKind::Severance,
}
}
fn settled() -> Vec<Pool> {
let s = CarnageSettings::default();
let stains = crate::spatter::stains(&fixed_wound(), &s, 0.0);
let mut pools = Vec::new();
absorb(&mut pools, &stains, &s);
for _ in 0..60 {
spread_pools(&mut pools, &s);
}
pools
}
#[test]
fn the_pool_model_is_frozen() {
let s = CarnageSettings::default();
let stains = [
Stain { at: Vec3::new(0.5, 0.0, -0.25), radius: 0.25, seed: 0x1111_1111 },
Stain { at: Vec3::new(0.53, 0.0, -0.25), radius: 0.125, seed: 0x2222_2222 },
Stain { at: Vec3::new(1.5, 0.0, 0.75), radius: 0.5, seed: 0x3333_3333 },
];
let mut pools = Vec::new();
absorb(&mut pools, &stains, &s);
assert_eq!(pools.len(), 2, "the middle stain must have merged into the first pool");
assert_eq!(pools[0].seed, 0x1111_1111, "a pool keeps the seed of the stain that formed it");
for _ in 0..60 {
spread_pools(&mut pools, &s);
}
let expect: [([u32; 3], u32, u32); 2] = [
([0x3F000000, 0x00000000, 0xBE800000], 0x3E5A4104, 0x3DA00000),
([0x3FC00000, 0x00000000, 0x3F400000], 0x3EC364D3, 0x3E800000),
];
let actual: Vec<([u32; 3], u32, u32)> = pools
.iter()
.map(|p| {
(
[p.at.x.to_bits(), p.at.y.to_bits(), p.at.z.to_bits()],
p.radius.to_bits(),
p.wetted.to_bits(),
)
})
.collect();
let rendered: Vec<String> = actual
.iter()
.map(|(at, r, w)| {
format!("([0x{:08X}, 0x{:08X}, 0x{:08X}], 0x{r:08X}, 0x{w:08X}),", at[0], at[1], at[2])
})
.collect();
assert_eq!(
actual.as_slice(),
expect.as_slice(),
"the pool model moved. If that was deliberate, the new bits are:\n{}",
rendered.join("\n")
);
assert!(pools.iter().all(|p| p.age == 60), "one `spread_pools` is one tick of age");
}
#[test]
fn absorbing_the_same_stains_twice_gives_bit_identical_pools() {
let a = settled();
let b = settled();
assert!(!a.is_empty(), "precondition: the wound pooled at all");
assert_eq!(a.len(), b.len(), "two folds of one stain list produced different pool counts");
for (i, (x, y)) in a.iter().zip(&b).enumerate() {
assert_eq!(
(x.at.x.to_bits(), x.at.y.to_bits(), x.at.z.to_bits(), x.radius.to_bits(), x.wetted.to_bits()),
(y.at.x.to_bits(), y.at.y.to_bits(), y.at.z.to_bits(), y.radius.to_bits(), y.wetted.to_bits()),
"pool {i} differs between two identical folds"
);
}
}
#[test]
fn the_pool_cap_holds_under_ten_thousand_scattered_stains() {
let s = CarnageSettings::default();
let stains: Vec<Stain> = (0..10_000u32)
.map(|i| {
let x = crate::soup::hash_f32(i.wrapping_mul(2_654_435_761)) * 10.0 - 5.0;
let z = crate::soup::hash_f32(i.wrapping_mul(2_246_822_519) ^ 0x5EED) * 10.0 - 5.0;
Stain { at: Vec3::new(x, 0.0, z), radius: 0.03, seed: i }
})
.collect();
let mut pools = Vec::new();
absorb(&mut pools, &stains, &s);
assert_eq!(
pools.len(),
s.max_pools as usize,
"a scatter this wide must fill the cap exactly, and never exceed it"
);
for (i, p) in pools.iter().enumerate() {
assert!(p.wetted > 0.0 && p.wetted.is_finite(), "pool {i} has a broken area {}", p.wetted);
}
}
#[test]
fn repeated_stains_in_one_place_become_one_growing_slick() {
let s = CarnageSettings::default();
let at = Vec3::new(0.4, 0.0, -0.25);
let batch: Vec<Stain> =
(0..40u32).map(|i| Stain { at, radius: 0.03, seed: i }).collect();
let mut pools = Vec::new();
absorb(&mut pools, &batch, &s);
assert_eq!(pools.len(), 1, "forty stains on one point must be one pool");
let before = pools[0].radius;
for _ in 0..60 {
spread_pools(&mut pools, &s);
}
let after = pools[0].radius;
assert!(after > before, "the slick did not spread: {before} -> {after}");
assert_eq!(pools[0].age, 60, "age is the caller's tick count, one per `spread_pools`");
}
}