use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum WeatherCondition {
#[default]
Clear,
Overcast,
Fog,
Rain,
Snow,
Storm,
}
impl_display!(WeatherCondition {
Clear => "Clear",
Overcast => "Overcast",
Fog => "Fog",
Rain => "Rain",
Snow => "Snow",
Storm => "Storm",
});
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Environment {
pub temperature_c: f32,
pub humidity_pct: f32,
pub pressure_hpa: f32,
pub light_lux: f32,
pub noise_db: f32,
pub wind_speed_ms: f32,
pub air_quality_aqi: f32,
pub altitude_m: f32,
pub weather: WeatherCondition,
}
impl Default for Environment {
fn default() -> Self {
Self::comfortable_indoor()
}
}
impl Environment {
#[must_use]
pub fn comfortable_indoor() -> Self {
Self {
temperature_c: 22.0,
humidity_pct: 45.0,
pressure_hpa: 1013.25,
light_lux: 500.0,
noise_db: 30.0,
wind_speed_ms: 0.0,
air_quality_aqi: 20.0,
altitude_m: 100.0,
weather: WeatherCondition::Clear,
}
}
#[must_use]
pub fn hot_summer_day() -> Self {
Self {
temperature_c: 38.0,
humidity_pct: 70.0,
pressure_hpa: 1010.0,
light_lux: 80_000.0,
noise_db: 40.0,
wind_speed_ms: 2.0,
air_quality_aqi: 60.0,
altitude_m: 100.0,
weather: WeatherCondition::Clear,
}
}
#[must_use]
pub fn cold_winter_night() -> Self {
Self {
temperature_c: -10.0,
humidity_pct: 30.0,
pressure_hpa: 1020.0,
light_lux: 0.1,
noise_db: 20.0,
wind_speed_ms: 3.0,
air_quality_aqi: 15.0,
altitude_m: 100.0,
weather: WeatherCondition::Clear,
}
}
#[must_use]
pub fn storm() -> Self {
Self {
temperature_c: 15.0,
humidity_pct: 90.0,
pressure_hpa: 990.0,
light_lux: 200.0,
noise_db: 75.0,
wind_speed_ms: 15.0,
air_quality_aqi: 30.0,
altitude_m: 100.0,
weather: WeatherCondition::Storm,
}
}
#[must_use]
pub fn office() -> Self {
Self {
temperature_c: 21.0,
humidity_pct: 40.0,
pressure_hpa: 1013.25,
light_lux: 400.0,
noise_db: 45.0,
wind_speed_ms: 0.0,
air_quality_aqi: 25.0,
altitude_m: 100.0,
weather: WeatherCondition::Clear,
}
}
#[must_use]
pub fn forest() -> Self {
Self {
temperature_c: 18.0,
humidity_pct: 60.0,
pressure_hpa: 1015.0,
light_lux: 2000.0,
noise_db: 25.0,
wind_speed_ms: 1.0,
air_quality_aqi: 10.0,
altitude_m: 300.0,
weather: WeatherCondition::Clear,
}
}
#[must_use]
#[inline]
pub fn heat_index(&self) -> f32 {
if self.temperature_c < 27.0 {
return self.temperature_c;
}
let t = self.temperature_c;
let rh = self.humidity_pct;
let hi =
-8.785 + 1.611 * t + 2.339 * rh - 0.1461 * t * rh - 0.01231 * t * t - 0.01642 * rh * rh
+ 0.002212 * t * t * rh
+ 0.000725 * t * rh * rh
- 0.000003582 * t * t * rh * rh;
hi.max(t)
}
#[must_use]
#[inline]
pub fn wind_chill(&self) -> f32 {
let wind_kmh = self.wind_speed_ms * 3.6;
if self.temperature_c > 10.0 || wind_kmh < 4.8 {
return self.temperature_c;
}
let v016 = wind_kmh.powf(0.16);
13.12 + 0.6215 * self.temperature_c - 11.37 * v016 + 0.3965 * self.temperature_c * v016
}
#[must_use]
#[inline]
pub fn apparent_temperature(&self) -> f32 {
if self.temperature_c >= 27.0 {
self.heat_index()
} else if self.temperature_c <= 10.0 {
self.wind_chill()
} else {
self.temperature_c
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentalEffect {
pub energy_drain_multiplier: f32,
pub energy_recovery_multiplier: f32,
pub stress_accumulation_multiplier: f32,
pub alertness_offset: f32,
pub flow_disruption_multiplier: f32,
pub mood_joy_offset: f32,
pub mood_arousal_offset: f32,
pub mood_trust_offset: f32,
pub salience_range_multiplier: f32,
}
impl Default for EnvironmentalEffect {
fn default() -> Self {
Self {
energy_drain_multiplier: 1.0,
energy_recovery_multiplier: 1.0,
stress_accumulation_multiplier: 1.0,
alertness_offset: 0.0,
flow_disruption_multiplier: 1.0,
mood_joy_offset: 0.0,
mood_arousal_offset: 0.0,
mood_trust_offset: 0.0,
salience_range_multiplier: 1.0,
}
}
}
impl EnvironmentalEffect {
#[must_use]
#[inline]
pub fn neutral() -> Self {
Self::default()
}
}
#[derive(Debug, Clone)]
struct PersonalitySensitivity {
patience: f32,
sensitivity: f32,
resilience: f32,
curiosity: f32,
}
impl Default for PersonalitySensitivity {
fn default() -> Self {
Self {
patience: 0.0,
sensitivity: 0.0,
resilience: 0.0,
curiosity: 0.0,
}
}
}
#[cfg(feature = "traits")]
fn extract_sensitivity(profile: &crate::traits::PersonalityProfile) -> PersonalitySensitivity {
use crate::traits::TraitKind;
PersonalitySensitivity {
patience: profile.get_trait(TraitKind::Patience).normalized(),
sensitivity: -profile.get_trait(TraitKind::Patience).normalized() * 0.5
+ profile.get_trait(TraitKind::Empathy).normalized() * 0.5,
resilience: profile.get_trait(TraitKind::Confidence).normalized(),
curiosity: profile.get_trait(TraitKind::Curiosity).normalized(),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn environmental_modifiers(
env: &Environment,
#[cfg(feature = "traits")] personality: Option<&crate::traits::PersonalityProfile>,
#[cfg(not(feature = "traits"))] _personality: Option<&()>,
) -> EnvironmentalEffect {
#[cfg(feature = "traits")]
let sens = personality.map(extract_sensitivity).unwrap_or_default();
#[cfg(not(feature = "traits"))]
let sens = PersonalitySensitivity::default();
let mut effect = EnvironmentalEffect::neutral();
let resilience_factor = 1.0 - sens.resilience * 0.3;
let apparent = env.apparent_temperature();
if apparent > 35.0 {
let heat_severity = ((apparent - 35.0) / 15.0).clamp(0.0, 1.0);
let patience_dampen = 1.0 - sens.patience * 0.3; effect.energy_drain_multiplier += 0.5 * heat_severity * patience_dampen * resilience_factor;
effect.energy_recovery_multiplier -= 0.3 * heat_severity * resilience_factor;
effect.stress_accumulation_multiplier +=
0.4 * heat_severity * patience_dampen * resilience_factor;
effect.mood_arousal_offset += 0.1 * heat_severity;
} else if apparent < 0.0 {
let cold_severity = ((-apparent) / 20.0).clamp(0.0, 1.0);
effect.energy_drain_multiplier += 0.4 * cold_severity * resilience_factor;
effect.stress_accumulation_multiplier += 0.3 * cold_severity * resilience_factor;
}
if env.temperature_c > 30.0 && env.humidity_pct > 60.0 {
let compound = ((env.humidity_pct - 60.0) / 40.0).clamp(0.0, 1.0)
* ((env.temperature_c - 30.0) / 10.0).clamp(0.0, 1.0);
effect.stress_accumulation_multiplier += 0.3 * compound * resilience_factor;
}
let pressure_deviation = (env.pressure_hpa - 1013.25) / 1013.25;
if pressure_deviation < -0.015 {
let low_p = ((-pressure_deviation - 0.015) / 0.03).clamp(0.0, 1.0);
let sensitivity_amp = 1.0 + sens.sensitivity * 0.5;
effect.mood_trust_offset -= 0.1 * low_p * sensitivity_amp * resilience_factor;
effect.mood_arousal_offset += 0.1 * low_p * sensitivity_amp * resilience_factor;
effect.stress_accumulation_multiplier += 0.2 * low_p * sensitivity_amp * resilience_factor;
}
if env.light_lux < 100.0 {
let dark = (1.0 - env.light_lux / 100.0).clamp(0.0, 1.0);
effect.alertness_offset -= 0.2 * dark;
} else if env.light_lux > 10_000.0 {
let bright = ((env.light_lux - 10_000.0) / 70_000.0).clamp(0.0, 1.0);
effect.alertness_offset += 0.15 * bright;
}
if env.noise_db > 70.0 {
let noise_severity = ((env.noise_db - 70.0) / 30.0).clamp(0.0, 1.0);
let patience_dampen = 1.0 - sens.patience * 0.3;
effect.stress_accumulation_multiplier +=
0.3 * noise_severity * patience_dampen * resilience_factor;
effect.flow_disruption_multiplier += 0.6 * noise_severity;
}
if env.noise_db > 55.0 {
let sustained = ((env.noise_db - 55.0) / 15.0).clamp(0.0, 1.0);
effect.flow_disruption_multiplier += 0.2 * sustained;
}
if env.wind_speed_ms > 10.0 {
let wind_severity = ((env.wind_speed_ms - 10.0) / 20.0).clamp(0.0, 1.0);
effect.energy_drain_multiplier += 0.2 * wind_severity * resilience_factor;
}
if env.air_quality_aqi > 150.0 {
let aqi_severity = ((env.air_quality_aqi - 150.0) / 350.0).clamp(0.0, 1.0);
effect.energy_recovery_multiplier -= 0.2 * aqi_severity * resilience_factor;
effect.stress_accumulation_multiplier += 0.2 * aqi_severity * resilience_factor;
}
if env.altitude_m > 2500.0 {
let alt_severity = ((env.altitude_m - 2500.0) / 5000.0).clamp(0.0, 1.0);
effect.energy_drain_multiplier += 0.3 * alt_severity * resilience_factor;
effect.energy_recovery_multiplier -= 0.15 * alt_severity * resilience_factor;
}
match env.weather {
WeatherCondition::Clear => {
effect.mood_joy_offset += 0.05;
}
WeatherCondition::Overcast => {
effect.mood_joy_offset -= 0.02;
}
WeatherCondition::Fog => {
effect.salience_range_multiplier *= 0.5;
let curiosity_flip = sens.curiosity * 0.05;
effect.mood_joy_offset += curiosity_flip - 0.03;
}
WeatherCondition::Rain => {
let sensitivity_reaction = sens.sensitivity * 0.08 * resilience_factor;
let curiosity_reaction = sens.curiosity * 0.05;
effect.mood_joy_offset += curiosity_reaction - sensitivity_reaction;
effect.mood_arousal_offset -= 0.05; }
WeatherCondition::Snow => {
effect.mood_joy_offset += 0.02; effect.energy_drain_multiplier += 0.1 * resilience_factor; effect.salience_range_multiplier *= 0.8; }
WeatherCondition::Storm => {
let sensitivity_amp = 1.0 + sens.sensitivity * 0.5;
effect.stress_accumulation_multiplier += 0.3 * sensitivity_amp * resilience_factor;
effect.mood_arousal_offset += 0.15;
effect.mood_trust_offset -= 0.1 * sensitivity_amp * resilience_factor;
effect.flow_disruption_multiplier += 0.4;
effect.salience_range_multiplier *= 0.6;
}
}
effect.energy_drain_multiplier = effect.energy_drain_multiplier.clamp(0.5, 3.0);
effect.energy_recovery_multiplier = effect.energy_recovery_multiplier.clamp(0.3, 1.5);
effect.stress_accumulation_multiplier = effect.stress_accumulation_multiplier.clamp(0.5, 3.0);
effect.alertness_offset = effect.alertness_offset.clamp(-0.3, 0.3);
effect.flow_disruption_multiplier = effect.flow_disruption_multiplier.clamp(0.5, 3.0);
effect.mood_joy_offset = effect.mood_joy_offset.clamp(-0.2, 0.2);
effect.mood_arousal_offset = effect.mood_arousal_offset.clamp(-0.2, 0.2);
effect.mood_trust_offset = effect.mood_trust_offset.clamp(-0.2, 0.2);
effect.salience_range_multiplier = effect.salience_range_multiplier.clamp(0.2, 1.5);
effect
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn apply_environment(
env: &Environment,
energy: &mut crate::energy::EnergyState,
stress: &mut crate::stress::StressState,
mood: &mut crate::mood::MoodVector,
#[cfg(feature = "traits")] personality: Option<&crate::traits::PersonalityProfile>,
#[cfg(not(feature = "traits"))] personality: Option<&()>,
) {
let effect = environmental_modifiers(env, personality);
energy.drain_rate *= effect.energy_drain_multiplier;
energy.recovery_rate *= effect.energy_recovery_multiplier;
stress.accumulation_rate *= effect.stress_accumulation_multiplier;
use crate::mood::Emotion;
mood.nudge(Emotion::Joy, effect.mood_joy_offset);
mood.nudge(Emotion::Arousal, effect.mood_arousal_offset);
mood.nudge(Emotion::Trust, effect.mood_trust_offset);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comfortable_indoor_is_neutral() {
let env = Environment::comfortable_indoor();
let effect = environmental_modifiers(&env, None);
assert!((effect.energy_drain_multiplier - 1.0).abs() < 0.01);
assert!((effect.energy_recovery_multiplier - 1.0).abs() < 0.01);
assert!((effect.stress_accumulation_multiplier - 1.0).abs() < 0.01);
assert!((effect.flow_disruption_multiplier - 1.0).abs() < 0.01);
assert!((effect.salience_range_multiplier - 1.0).abs() < 0.01);
}
#[test]
fn hot_day_increases_drain() {
let env = Environment::hot_summer_day();
let effect = environmental_modifiers(&env, None);
assert!(effect.energy_drain_multiplier > 1.0);
assert!(effect.energy_recovery_multiplier < 1.0);
assert!(effect.stress_accumulation_multiplier > 1.0);
}
#[test]
fn cold_night_increases_drain() {
let env = Environment::cold_winter_night();
let effect = environmental_modifiers(&env, None);
assert!(effect.energy_drain_multiplier > 1.0);
assert!(effect.stress_accumulation_multiplier > 1.0);
}
#[test]
fn storm_elevates_stress_and_arousal() {
let env = Environment::storm();
let effect = environmental_modifiers(&env, None);
assert!(effect.stress_accumulation_multiplier > 1.0);
assert!(effect.mood_arousal_offset > 0.0);
assert!(effect.flow_disruption_multiplier > 1.0);
assert!(effect.salience_range_multiplier < 1.0);
}
#[test]
fn fog_reduces_salience() {
let mut env = Environment::comfortable_indoor();
env.weather = WeatherCondition::Fog;
let effect = environmental_modifiers(&env, None);
assert!(effect.salience_range_multiplier < 0.6);
}
#[test]
fn low_light_causes_drowsiness() {
let mut env = Environment::comfortable_indoor();
env.light_lux = 10.0;
let effect = environmental_modifiers(&env, None);
assert!(effect.alertness_offset < 0.0);
}
#[test]
fn bright_light_boosts_alertness() {
let mut env = Environment::comfortable_indoor();
env.light_lux = 50_000.0;
let effect = environmental_modifiers(&env, None);
assert!(effect.alertness_offset > 0.0);
}
#[test]
fn high_noise_disrupts_flow() {
let mut env = Environment::comfortable_indoor();
env.noise_db = 85.0;
let effect = environmental_modifiers(&env, None);
assert!(effect.flow_disruption_multiplier >= 1.5);
assert!(effect.stress_accumulation_multiplier > 1.0);
}
#[test]
fn high_altitude_drains_energy() {
let mut env = Environment::comfortable_indoor();
env.altitude_m = 4000.0;
let effect = environmental_modifiers(&env, None);
assert!(effect.energy_drain_multiplier > 1.0);
assert!(effect.energy_recovery_multiplier < 1.0);
}
#[test]
fn poor_air_quality_reduces_recovery() {
let mut env = Environment::comfortable_indoor();
env.air_quality_aqi = 300.0;
let effect = environmental_modifiers(&env, None);
assert!(effect.energy_recovery_multiplier < 1.0);
assert!(effect.stress_accumulation_multiplier > 1.0);
}
#[test]
fn clear_sky_joy_nudge() {
let env = Environment::comfortable_indoor(); let effect = environmental_modifiers(&env, None);
assert!(effect.mood_joy_offset > 0.0);
}
#[test]
fn heat_index_below_threshold_returns_temp() {
let env = Environment {
temperature_c: 20.0,
humidity_pct: 80.0,
..Default::default()
};
assert!((env.heat_index() - 20.0).abs() < 0.01);
}
#[test]
fn heat_index_above_threshold() {
let env = Environment {
temperature_c: 35.0,
humidity_pct: 80.0,
..Default::default()
};
assert!(env.heat_index() > 35.0); }
#[test]
fn wind_chill_below_threshold() {
let env = Environment {
temperature_c: -5.0,
wind_speed_ms: 10.0,
..Default::default()
};
assert!(env.wind_chill() < -5.0); }
#[test]
fn wind_chill_warm_returns_temp() {
let env = Environment {
temperature_c: 15.0,
wind_speed_ms: 10.0,
..Default::default()
};
assert!((env.wind_chill() - 15.0).abs() < 0.01);
}
#[test]
fn low_pressure_anxiety() {
let mut env = Environment::comfortable_indoor();
env.pressure_hpa = 980.0;
let effect = environmental_modifiers(&env, None);
assert!(effect.mood_trust_offset < 0.0);
assert!(effect.mood_arousal_offset > 0.0);
}
#[test]
fn multipliers_clamped() {
let env = Environment {
temperature_c: 55.0,
humidity_pct: 100.0,
pressure_hpa: 900.0,
light_lux: 0.0,
noise_db: 120.0,
wind_speed_ms: 40.0,
air_quality_aqi: 500.0,
altitude_m: 8000.0,
weather: WeatherCondition::Storm,
};
let effect = environmental_modifiers(&env, None);
assert!(effect.energy_drain_multiplier <= 3.0);
assert!(effect.energy_recovery_multiplier >= 0.3);
assert!(effect.stress_accumulation_multiplier <= 3.0);
assert!(effect.flow_disruption_multiplier <= 3.0);
assert!(effect.salience_range_multiplier >= 0.2);
}
#[test]
fn serde_roundtrip_environment() {
let env = Environment::storm();
let json = serde_json::to_string(&env).unwrap();
let deser: Environment = serde_json::from_str(&json).unwrap();
assert!((env.temperature_c - deser.temperature_c).abs() < f32::EPSILON);
assert_eq!(env.weather, deser.weather);
}
#[test]
fn serde_roundtrip_effect() {
let env = Environment::hot_summer_day();
let effect = environmental_modifiers(&env, None);
let json = serde_json::to_string(&effect).unwrap();
let deser: EnvironmentalEffect = serde_json::from_str(&json).unwrap();
assert!(
(effect.energy_drain_multiplier - deser.energy_drain_multiplier).abs() < f32::EPSILON
);
}
#[test]
fn serde_roundtrip_weather() {
let conditions = [
WeatherCondition::Clear,
WeatherCondition::Overcast,
WeatherCondition::Fog,
WeatherCondition::Rain,
WeatherCondition::Snow,
WeatherCondition::Storm,
];
for cond in &conditions {
let json = serde_json::to_string(cond).unwrap();
let deser: WeatherCondition = serde_json::from_str(&json).unwrap();
assert_eq!(*cond, deser);
}
}
#[test]
fn apply_environment_mutates_states() {
let mut energy = crate::energy::EnergyState::new();
let mut stress = crate::stress::StressState::new();
let mut mood = crate::mood::MoodVector::neutral();
let base_drain = energy.drain_rate;
let base_stress = stress.accumulation_rate;
let env = Environment::hot_summer_day();
apply_environment(&env, &mut energy, &mut stress, &mut mood, None);
assert!(energy.drain_rate > base_drain);
assert!(stress.accumulation_rate > base_stress);
}
#[test]
fn display_weather() {
assert_eq!(WeatherCondition::Storm.to_string(), "Storm");
assert_eq!(WeatherCondition::Clear.to_string(), "Clear");
}
#[test]
fn rain_calms_arousal() {
let mut env = Environment::comfortable_indoor();
env.weather = WeatherCondition::Rain;
let effect = environmental_modifiers(&env, None);
assert!(effect.mood_arousal_offset < 0.0);
}
#[test]
fn snow_mild_novelty() {
let mut env = Environment::comfortable_indoor();
env.weather = WeatherCondition::Snow;
let effect = environmental_modifiers(&env, None);
assert!(effect.mood_joy_offset > 0.0);
assert!(effect.salience_range_multiplier < 1.0);
}
#[test]
fn forest_is_pleasant() {
let env = Environment::forest();
let effect = environmental_modifiers(&env, None);
assert!(effect.mood_joy_offset > 0.0);
assert!((effect.stress_accumulation_multiplier - 1.0).abs() < 0.1);
assert!((effect.flow_disruption_multiplier - 1.0).abs() < 0.1);
}
#[test]
fn humidity_heat_compound() {
let env = Environment {
temperature_c: 38.0,
humidity_pct: 90.0,
..Default::default()
};
let effect = environmental_modifiers(&env, None);
assert!(effect.stress_accumulation_multiplier > 1.5);
}
}