use crate::entity::Entity;
use crate::enums::{DevelopmentalCategory, LifeStage};
use crate::event::Event;
use crate::types::Timestamp;
const PLASTICITY_MAX: f64 = 2.0;
const PLASTICITY_MIN: f64 = 0.5;
const PLASTICITY_DECAY_RATE: f64 = 0.023;
const DAYS_PER_YEAR: f64 = 365.25;
#[must_use]
pub(crate) fn apply_developmental_effects(
entity: &Entity,
event: &Event,
event_impact: f64,
current_age_days: u64,
current_timestamp: Timestamp,
) -> f64 {
let species = entity.species();
let time_scale = f64::from(species.time_scale());
let age_days_f64 = current_age_days as f64;
let age_years = (age_days_f64 / DAYS_PER_YEAR) * time_scale;
let raw_age_years = age_days_f64 / DAYS_PER_YEAR;
let life_stage = LifeStage::from_age_years_for_species(species, raw_age_years);
let plasticity = get_plasticity_modifier(&life_stage, age_years);
let turning_point_boost = entity
.context()
.chronosystem()
.turning_point_plasticity_boost(current_timestamp);
let category = DevelopmentalCategory::from(&event.event_type());
let sensitive_multiplier = get_sensitive_period_multiplier(&life_stage, &category);
let effective_plasticity = plasticity + turning_point_boost;
let stage_multiplier = f64::from(life_stage.event_impact_multiplier());
event_impact * effective_plasticity * sensitive_multiplier * stage_multiplier
}
#[must_use]
pub(crate) fn get_plasticity_modifier(_life_stage: &LifeStage, age_years: f64) -> f64 {
let age = age_years.max(0.0);
let plasticity = PLASTICITY_MAX - (age * PLASTICITY_DECAY_RATE);
plasticity.max(PLASTICITY_MIN)
}
#[must_use]
pub(crate) fn get_sensitive_period_multiplier(
life_stage: &LifeStage,
category: &DevelopmentalCategory,
) -> f64 {
match (life_stage, category) {
(LifeStage::Child, DevelopmentalCategory::Attachment) => 2.0,
(LifeStage::Child, DevelopmentalCategory::Autonomy) => 2.0,
(LifeStage::Child, DevelopmentalCategory::Initiative) => 1.5,
(LifeStage::Child, DevelopmentalCategory::Industry) => 1.5,
(LifeStage::Adolescent, DevelopmentalCategory::Identity) => 1.8,
(LifeStage::YoungAdult, DevelopmentalCategory::Intimacy) => 1.3,
(LifeStage::Adult, DevelopmentalCategory::Generativity) => 1.3,
(LifeStage::MatureAdult, DevelopmentalCategory::Generativity) => 1.3,
(LifeStage::Elder, DevelopmentalCategory::Integrity) => 1.2,
_ => 1.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::EntityBuilder;
use crate::enums::{EventType, Species};
use crate::event::EventBuilder;
use crate::types::{Duration, Timestamp};
fn timestamp_for_days(days: u64) -> Timestamp {
Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0) + Duration::days(days)
}
#[test]
fn child_event_impact_higher_than_adult() {
let child_plasticity = get_plasticity_modifier(&LifeStage::Child, 5.0);
let adult_plasticity = get_plasticity_modifier(&LifeStage::Adult, 40.0);
assert!(child_plasticity > adult_plasticity);
}
#[test]
fn personality_crystallizes_by_age_25() {
let age_0_plasticity = get_plasticity_modifier(&LifeStage::Child, 0.0);
let age_25_plasticity = get_plasticity_modifier(&LifeStage::YoungAdult, 25.0);
assert!((age_0_plasticity - 2.0).abs() < f64::EPSILON);
let expected_25 = 2.0 - (25.0 * 0.023);
assert!((age_25_plasticity - expected_25).abs() < 0.001);
let reduction = (age_0_plasticity - age_25_plasticity) / age_0_plasticity;
assert!(reduction > 0.25);
}
#[test]
fn plasticity_modifier_returns_expected_range() {
let test_ages = [0.0, 5.0, 12.0, 18.0, 25.0, 40.0, 65.0, 80.0, 100.0];
for age in test_ages {
let plasticity = get_plasticity_modifier(&LifeStage::Adult, age);
assert!(plasticity >= 0.5 && plasticity <= 2.0);
}
}
#[test]
fn plasticity_at_age_zero_is_maximum() {
let plasticity = get_plasticity_modifier(&LifeStage::Child, 0.0);
assert!((plasticity - 2.0).abs() < f64::EPSILON);
}
#[test]
fn plasticity_at_age_65_is_minimum() {
let plasticity = get_plasticity_modifier(&LifeStage::Elder, 65.0);
assert!((plasticity - 0.505).abs() < 0.01);
}
#[test]
fn plasticity_floors_at_minimum() {
let plasticity_80 = get_plasticity_modifier(&LifeStage::Elder, 80.0);
let plasticity_100 = get_plasticity_modifier(&LifeStage::Elder, 100.0);
assert!((plasticity_80 - 0.5).abs() < f64::EPSILON);
assert!((plasticity_100 - 0.5).abs() < f64::EPSILON);
}
#[test]
fn plasticity_negative_age_clamps_to_zero() {
let plasticity = get_plasticity_modifier(&LifeStage::Child, -5.0);
assert!((plasticity - 2.0).abs() < f64::EPSILON);
}
#[test]
fn attachment_event_amplified_in_childhood() {
let multiplier =
get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Attachment);
assert!((multiplier - 2.0).abs() < f64::EPSILON);
}
#[test]
fn identity_events_amplified_in_adolescence() {
let multiplier = get_sensitive_period_multiplier(
&LifeStage::Adolescent,
&DevelopmentalCategory::Identity,
);
assert!((multiplier - 1.8).abs() < f64::EPSILON);
}
#[test]
fn sensitive_period_returns_one_outside_period() {
let adult_attachment =
get_sensitive_period_multiplier(&LifeStage::Adult, &DevelopmentalCategory::Attachment);
assert!((adult_attachment - 1.0).abs() < f64::EPSILON);
let child_identity =
get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Identity);
assert!((child_identity - 1.0).abs() < f64::EPSILON);
}
#[test]
fn autonomy_amplified_in_childhood() {
let multiplier =
get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Autonomy);
assert!((multiplier - 2.0).abs() < f64::EPSILON);
}
#[test]
fn initiative_amplified_in_childhood() {
let multiplier =
get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Initiative);
assert!((multiplier - 1.5).abs() < f64::EPSILON);
}
#[test]
fn industry_amplified_in_childhood() {
let multiplier =
get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Industry);
assert!((multiplier - 1.5).abs() < f64::EPSILON);
}
#[test]
fn intimacy_amplified_in_young_adult() {
let multiplier = get_sensitive_period_multiplier(
&LifeStage::YoungAdult,
&DevelopmentalCategory::Intimacy,
);
assert!((multiplier - 1.3).abs() < f64::EPSILON);
}
#[test]
fn generativity_amplified_in_adult() {
let multiplier = get_sensitive_period_multiplier(
&LifeStage::Adult,
&DevelopmentalCategory::Generativity,
);
assert!((multiplier - 1.3).abs() < f64::EPSILON);
}
#[test]
fn generativity_amplified_in_mature_adult() {
let multiplier = get_sensitive_period_multiplier(
&LifeStage::MatureAdult,
&DevelopmentalCategory::Generativity,
);
assert!((multiplier - 1.3).abs() < f64::EPSILON);
}
#[test]
fn integrity_amplified_in_elder() {
let multiplier =
get_sensitive_period_multiplier(&LifeStage::Elder, &DevelopmentalCategory::Integrity);
assert!((multiplier - 1.2).abs() < f64::EPSILON);
}
#[test]
fn neutral_category_returns_one() {
for stage in LifeStage::all() {
let multiplier =
get_sensitive_period_multiplier(&stage, &DevelopmentalCategory::Neutral);
assert!((multiplier - 1.0).abs() < f64::EPSILON);
}
}
#[test]
fn dog_plasticity_uses_scaled_age() {
let entity = EntityBuilder::new()
.species(Species::Dog)
.age(Duration::years(2))
.build()
.unwrap();
assert_eq!(entity.life_stage(), LifeStage::YoungAdult);
let event = EventBuilder::new(EventType::EndRelationshipRomantic).build().unwrap();
let modified =
apply_developmental_effects(&entity, &event, 1.0, 730, timestamp_for_days(730));
assert!(modified > 2.4 && modified < 2.9);
}
#[test]
fn human_plasticity_uses_raw_age() {
let entity = EntityBuilder::new()
.species(Species::Human)
.age(Duration::years(25))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceCombatMilitary).build().unwrap();
let modified = apply_developmental_effects(
&entity,
&event,
1.0,
25 * 365,
timestamp_for_days(25 * 365),
);
assert!((modified - 1.71).abs() < 0.15);
}
#[test]
fn apply_developmental_effects_modifies_event_impact() {
let entity = EntityBuilder::new()
.species(Species::Human)
.age(Duration::years(8))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceBetrayalTrust).build().unwrap();
let base_impact = 0.5;
let modified = apply_developmental_effects(
&entity,
&event,
base_impact,
8 * 365,
timestamp_for_days(8 * 365),
);
assert!(modified > base_impact);
assert!((modified - 3.632).abs() < 0.2);
}
#[test]
fn developmental_effects_pure_function() {
let entity = EntityBuilder::new()
.species(Species::Human)
.age(Duration::years(30))
.build()
.unwrap();
let event = EventBuilder::new(EventType::AchieveGoalMajor).build().unwrap();
let original_age = entity.age();
let result1 = apply_developmental_effects(
&entity,
&event,
1.0,
30 * 365,
timestamp_for_days(30 * 365),
);
let result2 = apply_developmental_effects(
&entity,
&event,
1.0,
30 * 365,
timestamp_for_days(30 * 365),
);
assert_eq!(entity.age(), original_age);
assert!((result1 - result2).abs() < f64::EPSILON);
}
#[test]
fn adolescent_identity_event_amplified() {
let entity = EntityBuilder::new()
.species(Species::Human)
.age(Duration::years(15))
.build()
.unwrap();
let event = EventBuilder::new(EventType::DevelopIllnessChronic).build().unwrap();
let modified = apply_developmental_effects(
&entity,
&event,
1.0,
15 * 365,
timestamp_for_days(15 * 365),
);
assert!(modified > 4.0 && modified < 5.0);
}
#[test]
fn elder_integrity_event_amplified() {
let entity = EntityBuilder::new()
.species(Species::Human)
.age(Duration::years(75))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceAwarenessMortality).build().unwrap();
let modified = apply_developmental_effects(
&entity,
&event,
1.0,
75 * 365,
timestamp_for_days(75 * 365),
);
assert!((modified - 0.48).abs() < 0.1);
}
#[test]
fn zero_impact_remains_zero() {
let entity = EntityBuilder::new()
.species(Species::Human)
.age(Duration::years(10))
.build()
.unwrap();
let event = EventBuilder::new(EventType::AchieveGoalMajor).build().unwrap();
let modified = apply_developmental_effects(
&entity,
&event,
0.0,
10 * 365,
timestamp_for_days(10 * 365),
);
assert!(modified.abs() < f64::EPSILON);
}
#[test]
fn negative_impact_scales_correctly() {
let entity = EntityBuilder::new()
.species(Species::Human)
.age(Duration::years(8))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceBetrayalTrust).build().unwrap();
let modified = apply_developmental_effects(
&entity,
&event,
-0.5,
8 * 365,
timestamp_for_days(8 * 365),
);
assert!(modified < 0.0);
assert!((modified - (-3.632)).abs() < 0.2);
}
}