use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::error::{BhavaError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Emotion {
Joy,
Arousal,
Dominance,
Trust,
Interest,
Frustration,
}
impl Emotion {
pub const ALL: &'static [Emotion] = &[
Self::Joy,
Self::Arousal,
Self::Dominance,
Self::Trust,
Self::Interest,
Self::Frustration,
];
}
impl fmt::Display for Emotion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Joy => "joy",
Self::Arousal => "arousal",
Self::Dominance => "dominance",
Self::Trust => "trust",
Self::Interest => "interest",
Self::Frustration => "frustration",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MoodVector {
pub joy: f32,
pub arousal: f32,
pub dominance: f32,
pub trust: f32,
pub interest: f32,
pub frustration: f32,
}
impl MoodVector {
#[must_use]
pub fn neutral() -> Self {
Self {
joy: 0.0,
arousal: 0.0,
dominance: 0.0,
trust: 0.0,
interest: 0.0,
frustration: 0.0,
}
}
#[inline]
pub fn get(&self, emotion: Emotion) -> f32 {
match emotion {
Emotion::Joy => self.joy,
Emotion::Arousal => self.arousal,
Emotion::Dominance => self.dominance,
Emotion::Trust => self.trust,
Emotion::Interest => self.interest,
Emotion::Frustration => self.frustration,
}
}
#[inline]
pub fn set(&mut self, emotion: Emotion, value: f32) {
let clamped = value.clamp(-1.0, 1.0);
match emotion {
Emotion::Joy => self.joy = clamped,
Emotion::Arousal => self.arousal = clamped,
Emotion::Dominance => self.dominance = clamped,
Emotion::Trust => self.trust = clamped,
Emotion::Interest => self.interest = clamped,
Emotion::Frustration => self.frustration = clamped,
}
}
#[inline]
pub fn nudge(&mut self, emotion: Emotion, delta: f32) {
self.set(emotion, self.get(emotion) + delta);
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn intensity(&self) -> f32 {
let sum = self.joy * self.joy
+ self.arousal * self.arousal
+ self.dominance * self.dominance
+ self.trust * self.trust
+ self.interest * self.interest
+ self.frustration * self.frustration;
sum.sqrt()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn dominant_emotion(&self) -> Emotion {
let mut best = Emotion::Joy;
let mut best_val = 0.0f32;
for &e in Emotion::ALL {
let v = self.get(e).abs();
if v > best_val {
best_val = v;
best = e;
}
}
best
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn decay(&mut self, factor: f32) {
let f = factor.clamp(0.0, 1.0);
self.joy *= 1.0 - f;
self.arousal *= 1.0 - f;
self.dominance *= 1.0 - f;
self.trust *= 1.0 - f;
self.interest *= 1.0 - f;
self.frustration *= 1.0 - f;
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn blend(&self, other: &MoodVector, t: f32) -> MoodVector {
let t = t.clamp(0.0, 1.0);
MoodVector {
joy: self.joy + (other.joy - self.joy) * t,
arousal: self.arousal + (other.arousal - self.arousal) * t,
dominance: self.dominance + (other.dominance - self.dominance) * t,
trust: self.trust + (other.trust - self.trust) * t,
interest: self.interest + (other.interest - self.interest) * t,
frustration: self.frustration + (other.frustration - self.frustration) * t,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveCause {
pub id: String,
pub emotions: Vec<Emotion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionalState {
pub mood: MoodVector,
pub baseline: MoodVector,
pub decay_half_life_secs: f64,
pub last_updated: DateTime<Utc>,
#[serde(default)]
pub active_causes: Vec<ActiveCause>,
}
impl EmotionalState {
#[must_use]
pub fn new() -> Self {
Self {
mood: MoodVector::neutral(),
baseline: MoodVector::neutral(),
decay_half_life_secs: 300.0,
last_updated: Utc::now(),
active_causes: Vec::new(),
}
}
#[must_use]
pub fn with_baseline(baseline: MoodVector) -> Self {
Self {
mood: baseline.clone(),
baseline,
decay_half_life_secs: 300.0,
last_updated: Utc::now(),
active_causes: Vec::new(),
}
}
pub fn set_decay_half_life(&mut self, secs: f64) -> Result<()> {
if secs <= 0.0 {
return Err(BhavaError::InvalidDecayRate { rate: secs as f32 });
}
self.decay_half_life_secs = secs;
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn apply_decay(&mut self, now: DateTime<Utc>) {
let elapsed = (now - self.last_updated).num_milliseconds() as f64 / 1000.0;
if elapsed <= 0.0 {
return;
}
let factor = 1.0 - (2.0f64.powf(-elapsed / self.decay_half_life_secs)) as f32;
let blended = self.mood.blend(&self.baseline, factor);
for &e in Emotion::ALL {
if !self.is_cause_active(e) {
self.mood.set(e, blended.get(e));
}
}
self.last_updated = now;
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn stimulate(&mut self, emotion: Emotion, intensity: f32) {
self.mood.nudge(emotion, intensity);
self.last_updated = Utc::now();
}
#[must_use]
pub fn deviation(&self) -> f32 {
let dj = self.mood.joy - self.baseline.joy;
let da = self.mood.arousal - self.baseline.arousal;
let dd = self.mood.dominance - self.baseline.dominance;
let dt = self.mood.trust - self.baseline.trust;
let di = self.mood.interest - self.baseline.interest;
let df = self.mood.frustration - self.baseline.frustration;
(dj * dj + da * da + dd * dd + dt * dt + di * di + df * df).sqrt()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn classify(&self) -> MoodState {
let intensity = self.mood.intensity();
let dominant = self.mood.dominant_emotion();
let dom_val = self.mood.get(dominant);
if intensity < 0.15 {
return MoodState::Calm;
}
match (dominant, dom_val > 0.0) {
(Emotion::Joy, true) if dom_val > 0.6 => MoodState::Euphoric,
(Emotion::Joy, true) => MoodState::Content,
(Emotion::Joy, false) => MoodState::Melancholy,
(Emotion::Arousal, true) => MoodState::Agitated,
(Emotion::Arousal, false) => MoodState::Calm,
(Emotion::Dominance, true) => MoodState::Assertive,
(Emotion::Dominance, false) => MoodState::Overwhelmed,
(Emotion::Trust, true) => MoodState::Trusting,
(Emotion::Trust, false) => MoodState::Guarded,
(Emotion::Interest, true) => MoodState::Curious,
(Emotion::Interest, false) => MoodState::Disengaged,
(Emotion::Frustration, true) => MoodState::Frustrated,
(Emotion::Frustration, false) => MoodState::Content,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn apply_trigger(&mut self, trigger: &super::MoodTrigger) {
for &(emotion, intensity) in &trigger.responses {
self.mood.nudge(emotion, intensity);
}
self.last_updated = Utc::now();
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn snapshot(&self) -> super::MoodSnapshot {
super::MoodSnapshot {
mood: self.mood.clone(),
state: self.classify(),
deviation: self.deviation(),
timestamp: self.last_updated,
}
}
pub fn add_active_cause(&mut self, cause_id: impl Into<String>, emotions: Vec<Emotion>) {
self.active_causes.push(ActiveCause {
id: cause_id.into(),
emotions,
});
}
pub fn resolve_cause(&mut self, cause_id: &str) -> bool {
let before = self.active_causes.len();
self.active_causes.retain(|c| c.id != cause_id);
self.active_causes.len() < before
}
#[must_use]
pub fn is_cause_active(&self, emotion: Emotion) -> bool {
self.active_causes
.iter()
.any(|c| c.emotions.contains(&emotion))
}
}
impl Default for EmotionalState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MoodState {
Calm,
Content,
Euphoric,
Melancholy,
Agitated,
Assertive,
Overwhelmed,
Trusting,
Guarded,
Curious,
Disengaged,
Frustrated,
}
impl fmt::Display for MoodState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Calm => "calm",
Self::Content => "content",
Self::Euphoric => "euphoric",
Self::Melancholy => "melancholy",
Self::Agitated => "agitated",
Self::Assertive => "assertive",
Self::Overwhelmed => "overwhelmed",
Self::Trusting => "trusting",
Self::Guarded => "guarded",
Self::Curious => "curious",
Self::Disengaged => "disengaged",
Self::Frustrated => "frustrated",
};
f.write_str(s)
}
}