macro_rules! for_each_field {
($self:ident, $other:ident, $op:tt) => {
Self {
valence: $self.valence $op $other.valence,
arousal: $self.arousal $op $other.arousal,
dominance: $self.dominance $op $other.dominance,
fatigue: $self.fatigue $op $other.fatigue,
stress: $self.stress $op $other.stress,
purpose: $self.purpose $op $other.purpose,
loneliness: $self.loneliness $op $other.loneliness,
prc: $self.prc $op $other.prc,
perceived_liability: $self.perceived_liability $op $other.perceived_liability,
self_hate: $self.self_hate $op $other.self_hate,
perceived_competence: $self.perceived_competence $op $other.perceived_competence,
depression: $self.depression $op $other.depression,
self_worth: $self.self_worth $op $other.self_worth,
hopelessness: $self.hopelessness $op $other.hopelessness,
interpersonal_hopelessness: $self.interpersonal_hopelessness $op $other.interpersonal_hopelessness,
acquired_capability: $self.acquired_capability $op $other.acquired_capability,
impulse_control: $self.impulse_control $op $other.impulse_control,
empathy: $self.empathy $op $other.empathy,
aggression: $self.aggression $op $other.aggression,
grievance: $self.grievance $op $other.grievance,
reactance: $self.reactance $op $other.reactance,
trustor_propensity: $self.trustor_propensity $op $other.trustor_propensity,
}
};
}
macro_rules! scale_fields {
($self:ident, $scalar:ident) => {
Self {
valence: $self.valence * $scalar,
arousal: $self.arousal * $scalar,
dominance: $self.dominance * $scalar,
fatigue: $self.fatigue * $scalar,
stress: $self.stress * $scalar,
purpose: $self.purpose * $scalar,
loneliness: $self.loneliness * $scalar,
prc: $self.prc * $scalar,
perceived_liability: $self.perceived_liability * $scalar,
self_hate: $self.self_hate * $scalar,
perceived_competence: $self.perceived_competence * $scalar,
depression: $self.depression * $scalar,
self_worth: $self.self_worth * $scalar,
hopelessness: $self.hopelessness * $scalar,
interpersonal_hopelessness: $self.interpersonal_hopelessness * $scalar,
acquired_capability: $self.acquired_capability * $scalar,
impulse_control: $self.impulse_control * $scalar,
empathy: $self.empathy * $scalar,
aggression: $self.aggression * $scalar,
grievance: $self.grievance * $scalar,
reactance: $self.reactance * $scalar,
trustor_propensity: $self.trustor_propensity * $scalar,
}
};
}
macro_rules! mask_fields {
($flags:ident, $impact:ident, $keep_when:expr, $ac_value:expr) => {
EventImpact {
valence: if $flags.valence == $keep_when { $impact.valence } else { 0.0 },
arousal: if $flags.arousal == $keep_when { $impact.arousal } else { 0.0 },
dominance: if $flags.dominance == $keep_when { $impact.dominance } else { 0.0 },
fatigue: if $flags.fatigue == $keep_when { $impact.fatigue } else { 0.0 },
stress: if $flags.stress == $keep_when { $impact.stress } else { 0.0 },
purpose: if $flags.purpose == $keep_when { $impact.purpose } else { 0.0 },
loneliness: if $flags.loneliness == $keep_when { $impact.loneliness } else { 0.0 },
prc: if $flags.prc == $keep_when { $impact.prc } else { 0.0 },
perceived_liability: if $flags.perceived_liability == $keep_when { $impact.perceived_liability } else { 0.0 },
self_hate: if $flags.self_hate == $keep_when { $impact.self_hate } else { 0.0 },
perceived_competence: if $flags.perceived_competence == $keep_when { $impact.perceived_competence } else { 0.0 },
depression: if $flags.depression == $keep_when { $impact.depression } else { 0.0 },
self_worth: if $flags.self_worth == $keep_when { $impact.self_worth } else { 0.0 },
hopelessness: if $flags.hopelessness == $keep_when { $impact.hopelessness } else { 0.0 },
interpersonal_hopelessness: if $flags.interpersonal_hopelessness == $keep_when { $impact.interpersonal_hopelessness } else { 0.0 },
acquired_capability: $ac_value,
impulse_control: if $flags.impulse_control == $keep_when { $impact.impulse_control } else { 0.0 },
empathy: if $flags.empathy == $keep_when { $impact.empathy } else { 0.0 },
aggression: if $flags.aggression == $keep_when { $impact.aggression } else { 0.0 },
grievance: if $flags.grievance == $keep_when { $impact.grievance } else { 0.0 },
reactance: if $flags.reactance == $keep_when { $impact.reactance } else { 0.0 },
trustor_propensity: if $flags.trustor_propensity == $keep_when { $impact.trustor_propensity } else { 0.0 },
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct EventImpact {
pub valence: f32,
pub arousal: f32,
pub dominance: f32,
pub fatigue: f32,
pub stress: f32,
pub purpose: f32,
pub loneliness: f32,
pub prc: f32,
pub perceived_liability: f32,
pub self_hate: f32,
pub perceived_competence: f32,
pub depression: f32,
pub self_worth: f32,
pub hopelessness: f32,
pub interpersonal_hopelessness: f32,
pub acquired_capability: f32,
pub impulse_control: f32,
pub empathy: f32,
pub aggression: f32,
pub grievance: f32,
pub reactance: f32,
pub trustor_propensity: f32,
}
impl EventImpact {
pub fn scale(&self, s: f32) -> Self {
scale_fields!(self, s)
}
pub fn add(&self, other: &Self) -> Self {
for_each_field!(self, other, +)
}
pub fn mul_permanence(&self, p: &PermanenceValues) -> Self {
Self {
valence: self.valence * p.valence,
arousal: self.arousal * p.arousal,
dominance: self.dominance * p.dominance,
fatigue: self.fatigue * p.fatigue,
stress: self.stress * p.stress,
purpose: self.purpose * p.purpose,
loneliness: self.loneliness * p.loneliness,
prc: self.prc * p.prc,
perceived_liability: self.perceived_liability * p.perceived_liability,
self_hate: self.self_hate * p.self_hate,
perceived_competence: self.perceived_competence * p.perceived_competence,
depression: self.depression * p.depression,
self_worth: self.self_worth * p.self_worth,
hopelessness: self.hopelessness * p.hopelessness,
interpersonal_hopelessness: self.interpersonal_hopelessness * p.interpersonal_hopelessness,
acquired_capability: self.acquired_capability, impulse_control: self.impulse_control * p.impulse_control,
empathy: self.empathy * p.empathy,
aggression: self.aggression * p.aggression,
grievance: self.grievance * p.grievance,
reactance: self.reactance * p.reactance,
trustor_propensity: self.trustor_propensity * p.trustor_propensity,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct ChronicFlags {
pub valence: bool,
pub arousal: bool,
pub dominance: bool,
pub fatigue: bool,
pub stress: bool,
pub purpose: bool,
pub loneliness: bool,
pub prc: bool,
pub perceived_liability: bool,
pub self_hate: bool,
pub perceived_competence: bool,
pub depression: bool,
pub self_worth: bool,
pub hopelessness: bool,
pub interpersonal_hopelessness: bool,
pub impulse_control: bool,
pub empathy: bool,
pub aggression: bool,
pub grievance: bool,
pub reactance: bool,
pub trustor_propensity: bool,
}
impl ChronicFlags {
pub fn mask(&self, impact: &EventImpact) -> EventImpact {
mask_fields!(self, impact, true, 0.0)
}
pub fn mask_inverse(&self, impact: &EventImpact) -> EventImpact {
mask_fields!(self, impact, false, 0.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct PermanenceValues {
pub valence: f32,
pub arousal: f32,
pub dominance: f32,
pub fatigue: f32,
pub stress: f32,
pub purpose: f32,
pub loneliness: f32,
pub prc: f32,
pub perceived_liability: f32,
pub self_hate: f32,
pub perceived_competence: f32,
pub depression: f32,
pub self_worth: f32,
pub hopelessness: f32,
pub interpersonal_hopelessness: f32,
pub impulse_control: f32,
pub empathy: f32,
pub aggression: f32,
pub grievance: f32,
pub reactance: f32,
pub trustor_propensity: f32,
}
impl PermanenceValues {
pub fn inverse(&self) -> Self {
Self {
valence: 1.0 - self.valence,
arousal: 1.0 - self.arousal,
dominance: 1.0 - self.dominance,
fatigue: 1.0 - self.fatigue,
stress: 1.0 - self.stress,
purpose: 1.0 - self.purpose,
loneliness: 1.0 - self.loneliness,
prc: 1.0 - self.prc,
perceived_liability: 1.0 - self.perceived_liability,
self_hate: 1.0 - self.self_hate,
perceived_competence: 1.0 - self.perceived_competence,
depression: 1.0 - self.depression,
self_worth: 1.0 - self.self_worth,
hopelessness: 1.0 - self.hopelessness,
interpersonal_hopelessness: 1.0 - self.interpersonal_hopelessness,
impulse_control: 1.0 - self.impulse_control,
empathy: 1.0 - self.empathy,
aggression: 1.0 - self.aggression,
grievance: 1.0 - self.grievance,
reactance: 1.0 - self.reactance,
trustor_propensity: 1.0 - self.trustor_propensity,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct EventSpec {
pub impact: EventImpact,
pub chronic: ChronicFlags,
pub permanence: PermanenceValues,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AppliedDeltas {
pub permanent: EventImpact,
pub acute: EventImpact,
pub chronic: EventImpact,
}
impl EventSpec {
pub fn apply(&self, severity: f32) -> AppliedDeltas {
let s = severity.clamp(0.0, 1.0);
let scaled = self.impact.scale(s);
let temp_portion = self.permanence.inverse();
let temp_scaled = scaled.mul_permanence(&temp_portion);
AppliedDeltas {
permanent: scaled.mul_permanence(&self.permanence),
acute: self.chronic.mask_inverse(&temp_scaled),
chronic: self.chronic.mask(&temp_scaled),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_impact_default_is_zero() {
let impact = EventImpact::default();
assert!((impact.valence - 0.0).abs() < f32::EPSILON);
assert!((impact.arousal - 0.0).abs() < f32::EPSILON);
assert!((impact.dominance - 0.0).abs() < f32::EPSILON);
assert!((impact.fatigue - 0.0).abs() < f32::EPSILON);
assert!((impact.stress - 0.0).abs() < f32::EPSILON);
assert!((impact.purpose - 0.0).abs() < f32::EPSILON);
assert!((impact.loneliness - 0.0).abs() < f32::EPSILON);
assert!((impact.prc - 0.0).abs() < f32::EPSILON);
assert!((impact.perceived_liability - 0.0).abs() < f32::EPSILON);
assert!((impact.self_hate - 0.0).abs() < f32::EPSILON);
assert!((impact.perceived_competence - 0.0).abs() < f32::EPSILON);
assert!((impact.depression - 0.0).abs() < f32::EPSILON);
assert!((impact.self_worth - 0.0).abs() < f32::EPSILON);
assert!((impact.hopelessness - 0.0).abs() < f32::EPSILON);
assert!((impact.interpersonal_hopelessness - 0.0).abs() < f32::EPSILON);
assert!((impact.acquired_capability - 0.0).abs() < f32::EPSILON);
assert!((impact.impulse_control - 0.0).abs() < f32::EPSILON);
assert!((impact.empathy - 0.0).abs() < f32::EPSILON);
assert!((impact.aggression - 0.0).abs() < f32::EPSILON);
assert!((impact.grievance - 0.0).abs() < f32::EPSILON);
assert!((impact.reactance - 0.0).abs() < f32::EPSILON);
assert!((impact.trustor_propensity - 0.0).abs() < f32::EPSILON);
}
#[test]
fn event_impact_scale_multiplies_all_fields() {
let impact = EventImpact {
valence: 1.0,
arousal: 0.5,
dominance: -0.5,
fatigue: 0.3,
stress: 0.4,
purpose: -0.2,
loneliness: 0.6,
prc: -0.3,
perceived_liability: 0.2,
self_hate: 0.4,
perceived_competence: -0.4,
depression: 0.3,
self_worth: -0.3,
hopelessness: 0.2,
interpersonal_hopelessness: 0.3,
acquired_capability: 0.5,
impulse_control: -0.2,
empathy: 0.1,
aggression: 0.3,
grievance: 0.2,
reactance: 0.1,
trustor_propensity: -0.2,
};
let scaled = impact.scale(0.5);
assert!((scaled.valence - 0.5).abs() < f32::EPSILON);
assert!((scaled.arousal - 0.25).abs() < f32::EPSILON);
assert!((scaled.dominance - (-0.25)).abs() < f32::EPSILON);
assert!((scaled.acquired_capability - 0.25).abs() < f32::EPSILON);
}
#[test]
fn event_impact_add_sums_all_fields() {
let a = EventImpact {
valence: 0.5,
arousal: 0.3,
..Default::default()
};
let b = EventImpact {
valence: 0.2,
arousal: -0.1,
..Default::default()
};
let sum = a.add(&b);
assert!((sum.valence - 0.7).abs() < f32::EPSILON);
assert!((sum.arousal - 0.2).abs() < f32::EPSILON);
}
#[test]
fn event_impact_mul_permanence_applies_per_field() {
let impact = EventImpact {
valence: 1.0,
arousal: 1.0,
acquired_capability: 1.0,
..Default::default()
};
let perm = PermanenceValues {
valence: 0.1,
arousal: 0.2,
..Default::default()
};
let result = impact.mul_permanence(&perm);
assert!((result.valence - 0.1).abs() < f32::EPSILON);
assert!((result.arousal - 0.2).abs() < f32::EPSILON);
assert!((result.acquired_capability - 1.0).abs() < f32::EPSILON);
}
#[test]
fn chronic_flags_default_all_false() {
let flags = ChronicFlags::default();
assert!(!flags.valence);
assert!(!flags.arousal);
assert!(!flags.dominance);
assert!(!flags.stress);
assert!(!flags.self_worth);
}
#[test]
fn chronic_flags_mask_keeps_chronic_values() {
let flags = ChronicFlags {
valence: true,
arousal: false,
stress: true,
..Default::default()
};
let impact = EventImpact {
valence: 0.5,
arousal: 0.3,
stress: 0.4,
acquired_capability: 0.2,
..Default::default()
};
let masked = flags.mask(&impact);
assert!((masked.valence - 0.5).abs() < f32::EPSILON);
assert!((masked.stress - 0.4).abs() < f32::EPSILON);
assert!((masked.arousal - 0.0).abs() < f32::EPSILON);
assert!((masked.acquired_capability - 0.0).abs() < f32::EPSILON);
}
#[test]
fn chronic_flags_mask_inverse_keeps_acute_values() {
let flags = ChronicFlags {
valence: true,
arousal: false,
stress: true,
..Default::default()
};
let impact = EventImpact {
valence: 0.5,
arousal: 0.3,
stress: 0.4,
acquired_capability: 0.2,
..Default::default()
};
let masked = flags.mask_inverse(&impact);
assert!((masked.valence - 0.0).abs() < f32::EPSILON);
assert!((masked.stress - 0.0).abs() < f32::EPSILON);
assert!((masked.arousal - 0.3).abs() < f32::EPSILON);
assert!((masked.acquired_capability - 0.0).abs() < f32::EPSILON);
}
#[test]
fn permanence_values_default_all_zero() {
let perm = PermanenceValues::default();
assert!((perm.valence - 0.0).abs() < f32::EPSILON);
assert!((perm.arousal - 0.0).abs() < f32::EPSILON);
assert!((perm.stress - 0.0).abs() < f32::EPSILON);
}
#[test]
fn permanence_values_inverse_computes_temporary_portion() {
let perm = PermanenceValues {
valence: 0.1,
arousal: 0.25,
stress: 0.0,
..Default::default()
};
let inv = perm.inverse();
assert!((inv.valence - 0.9).abs() < f32::EPSILON);
assert!((inv.arousal - 0.75).abs() < f32::EPSILON);
assert!((inv.stress - 1.0).abs() < f32::EPSILON);
}
#[test]
fn event_spec_default_has_zero_impact() {
let spec = EventSpec::default();
assert!((spec.impact.valence - 0.0).abs() < f32::EPSILON);
assert!(!spec.chronic.valence);
assert!((spec.permanence.valence - 0.0).abs() < f32::EPSILON);
}
#[test]
fn event_spec_apply_splits_by_permanence() {
let spec = EventSpec {
impact: EventImpact {
valence: -1.0,
..Default::default()
},
chronic: ChronicFlags::default(),
permanence: PermanenceValues {
valence: 0.10,
..Default::default()
},
};
let deltas = spec.apply(1.0);
assert!((deltas.permanent.valence - (-0.10)).abs() < f32::EPSILON);
assert!((deltas.acute.valence - (-0.90)).abs() < f32::EPSILON);
assert!((deltas.chronic.valence - 0.0).abs() < f32::EPSILON);
}
#[test]
fn event_spec_apply_routes_chronic_to_chronic_delta() {
let spec = EventSpec {
impact: EventImpact {
stress: 0.50,
..Default::default()
},
chronic: ChronicFlags {
stress: true,
..Default::default()
},
permanence: PermanenceValues::default(), };
let deltas = spec.apply(1.0);
assert!((deltas.permanent.stress - 0.0).abs() < f32::EPSILON);
assert!((deltas.acute.stress - 0.0).abs() < f32::EPSILON);
assert!((deltas.chronic.stress - 0.50).abs() < f32::EPSILON);
}
#[test]
fn event_spec_apply_ac_is_always_fully_permanent() {
let spec = EventSpec {
impact: EventImpact {
acquired_capability: 0.80,
..Default::default()
},
chronic: ChronicFlags::default(),
permanence: PermanenceValues::default(), };
let deltas = spec.apply(1.0);
assert!((deltas.permanent.acquired_capability - 0.80).abs() < f32::EPSILON);
assert!((deltas.acute.acquired_capability - 0.0).abs() < f32::EPSILON);
assert!((deltas.chronic.acquired_capability - 0.0).abs() < f32::EPSILON);
}
#[test]
fn event_spec_apply_scales_by_severity() {
let spec = EventSpec {
impact: EventImpact {
valence: -1.0,
..Default::default()
},
chronic: ChronicFlags::default(),
permanence: PermanenceValues {
valence: 0.10,
..Default::default()
},
};
let deltas = spec.apply(0.5);
assert!((deltas.permanent.valence - (-0.05)).abs() < f32::EPSILON);
assert!((deltas.acute.valence - (-0.45)).abs() < f32::EPSILON);
}
#[test]
fn event_spec_apply_clamps_severity() {
let spec = EventSpec {
impact: EventImpact {
valence: -1.0,
..Default::default()
},
chronic: ChronicFlags::default(),
permanence: PermanenceValues::default(),
};
let deltas_high = spec.apply(1.5);
assert!((deltas_high.acute.valence - (-1.0)).abs() < f32::EPSILON);
let deltas_low = spec.apply(-0.5);
assert!((deltas_low.acute.valence - 0.0).abs() < f32::EPSILON);
}
#[test]
fn applied_deltas_default_is_zero() {
let deltas = AppliedDeltas::default();
assert!((deltas.permanent.valence - 0.0).abs() < f32::EPSILON);
assert!((deltas.acute.valence - 0.0).abs() < f32::EPSILON);
assert!((deltas.chronic.valence - 0.0).abs() < f32::EPSILON);
}
#[test]
fn five_breakups_produce_realistic_permanent_shift() {
use crate::event::types::end_relationship_romantic;
let spec = end_relationship_romantic::SPEC;
let mut total_permanent_valence = 0.0;
for _ in 0..5 {
let deltas = spec.apply(1.0);
total_permanent_valence += deltas.permanent.valence;
}
assert!(total_permanent_valence > -0.20);
assert!(total_permanent_valence < -0.10);
}
}