use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EmotionalState {
Fear,
Aggression,
Calm,
Excitement,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Susceptibility {
pub base: f32,
pub rank: f32,
pub arousal: f32,
}
impl Susceptibility {
#[must_use]
pub fn new(base: f32, rank: f32, arousal: f32) -> Self {
Self {
base: base.clamp(0.0, 1.0),
rank: rank.clamp(0.0, 1.0),
arousal: arousal.clamp(0.0, 1.0),
}
}
#[must_use]
#[inline]
pub fn effective(&self) -> f32 {
let rank_factor = 1.0 - self.rank * 0.5;
(self.base * rank_factor * (1.0 + self.arousal * 0.5)).clamp(0.0, 1.0)
}
}
#[must_use]
pub fn emotional_influence(
emitter_intensity: f32,
emitter_rank: f32,
distance: f32,
max_range: f32,
) -> f32 {
if distance >= max_range || max_range <= 0.0 {
return 0.0;
}
let emitter_intensity = emitter_intensity.clamp(0.0, 1.0);
let emitter_rank = emitter_rank.clamp(0.0, 1.0);
let normalized_dist = distance / max_range;
let proximity = (1.0 - normalized_dist) * (1.0 - normalized_dist);
let rank_boost = 0.5 + emitter_rank * 0.5;
(emitter_intensity * proximity * rank_boost).clamp(0.0, 1.0)
}
#[must_use]
pub fn aggregate_pressure(influences: &[(f32, EmotionalState)]) -> Option<(EmotionalState, f32)> {
if influences.is_empty() {
return None;
}
let mut fear_total = 0.0_f32;
let mut aggression_total = 0.0_f32;
let mut calm_total = 0.0_f32;
let mut excitement_total = 0.0_f32;
for &(magnitude, state) in influences {
match state {
EmotionalState::Fear => fear_total += magnitude,
EmotionalState::Aggression => aggression_total += magnitude,
EmotionalState::Calm => calm_total += magnitude,
EmotionalState::Excitement => excitement_total += magnitude,
}
}
let candidates = [
(fear_total, EmotionalState::Fear),
(aggression_total, EmotionalState::Aggression),
(calm_total, EmotionalState::Calm),
(excitement_total, EmotionalState::Excitement),
];
candidates
.iter()
.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal))
.map(|&(total, state)| (state, total))
}
#[must_use]
pub fn contagion_transfer(influence: f32, susceptibility: f32, state_match: bool) -> f32 {
let base = influence * susceptibility;
let match_bonus = if state_match { 1.5 } else { 1.0 };
(base * match_bonus).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn close_creatures_more_influenced() {
let close = emotional_influence(0.8, 0.5, 1.0, 100.0);
let far = emotional_influence(0.8, 0.5, 80.0, 100.0);
assert!(close > far, "close={close}, far={far}");
}
#[test]
fn beyond_range_no_influence() {
let inf = emotional_influence(1.0, 1.0, 101.0, 100.0);
assert_eq!(inf, 0.0);
}
#[test]
fn higher_rank_more_contagious() {
let alpha = emotional_influence(0.8, 0.9, 10.0, 100.0);
let omega = emotional_influence(0.8, 0.1, 10.0, 100.0);
assert!(alpha > omega, "alpha={alpha}, omega={omega}");
}
#[test]
fn susceptibility_rank_effect() {
let low_rank = Susceptibility::new(0.7, 0.1, 0.3);
let high_rank = Susceptibility::new(0.7, 0.9, 0.3);
assert!(
low_rank.effective() > high_rank.effective(),
"lower rank should be more susceptible"
);
}
#[test]
fn aggregate_fear_dominates() {
let influences = vec![
(0.8, EmotionalState::Fear),
(0.6, EmotionalState::Fear),
(0.5, EmotionalState::Calm),
];
let (state, _) = aggregate_pressure(&influences).unwrap();
assert_eq!(state, EmotionalState::Fear);
}
#[test]
fn aggregate_empty_returns_none() {
assert!(aggregate_pressure(&[]).is_none());
}
#[test]
fn contagion_transfer_amplified_by_match() {
let matched = contagion_transfer(0.5, 0.8, true);
let unmatched = contagion_transfer(0.5, 0.8, false);
assert!(matched > unmatched);
}
#[test]
fn contagion_transfer_clamped() {
let result = contagion_transfer(1.0, 1.0, true);
assert!(result <= 1.0);
}
#[test]
fn serde_roundtrip_emotional_state() {
for s in [
EmotionalState::Fear,
EmotionalState::Aggression,
EmotionalState::Calm,
EmotionalState::Excitement,
] {
let json = serde_json::to_string(&s).unwrap();
let s2: EmotionalState = serde_json::from_str(&json).unwrap();
assert_eq!(s, s2);
}
}
#[test]
fn serde_roundtrip_susceptibility() {
let s = Susceptibility::new(0.7, 0.3, 0.5);
let json = serde_json::to_string(&s).unwrap();
let s2: Susceptibility = serde_json::from_str(&json).unwrap();
assert!((s.base - s2.base).abs() < f32::EPSILON);
assert!((s.rank - s2.rank).abs() < f32::EPSILON);
assert!((s.arousal - s2.arousal).abs() < f32::EPSILON);
}
}