use bevy::ecs::component::Component;
use bevy::reflect::Reflect;
use bevy_emerge_rng::DetRng;
use serde::{Deserialize, Serialize};
use crate::core::{
Perception, Person, PersonIndex, SocietyClock, SocietySettings, SocietySystems, require_core,
};
use crate::defs::SocietyDefs;
use crate::defs::{DeedId, InteractionId};
use crate::model::pad;
use crate::model::recall;
use crate::relationships::Ties;
use crate::rumor::RumorId;
use bevy::app::{App, FixedUpdate, Plugin};
use bevy::ecs::entity::Entity;
use bevy::ecs::lifecycle::Add;
use bevy::ecs::message::Message;
use bevy::ecs::message::MessageReader;
use bevy::ecs::observer::On;
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::ecs::system::{Query, Res};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Reflect)]
pub enum Trait {
Openness,
Conscientiousness,
Extraversion,
Agreeableness,
Neuroticism,
}
impl Trait {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"openness" => Some(Self::Openness),
"conscientiousness" => Some(Self::Conscientiousness),
"extraversion" => Some(Self::Extraversion),
"agreeableness" => Some(Self::Agreeableness),
"neuroticism" => Some(Self::Neuroticism),
_ => None,
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Personality {
pub openness: f32,
pub conscientiousness: f32,
pub extraversion: f32,
pub agreeableness: f32,
pub neuroticism: f32,
}
impl Default for Personality {
fn default() -> Self {
Self {
openness: 0.5,
conscientiousness: 0.5,
extraversion: 0.5,
agreeableness: 0.5,
neuroticism: 0.5,
}
}
}
impl Personality {
pub fn as_array(&self) -> [f32; 5] {
[
self.openness,
self.conscientiousness,
self.extraversion,
self.agreeableness,
self.neuroticism,
]
}
pub fn value(&self, which: Trait) -> f32 {
match which {
Trait::Openness => self.openness,
Trait::Conscientiousness => self.conscientiousness,
Trait::Extraversion => self.extraversion,
Trait::Agreeableness => self.agreeableness,
Trait::Neuroticism => self.neuroticism,
}
}
pub fn sample<R: DetRng>(rng: &mut R) -> Self {
let mut mean3 = || (rng.unit() + rng.unit() + rng.unit()) as f32 / 3.0;
Self {
openness: mean3(),
conscientiousness: mean3(),
extraversion: mean3(),
agreeableness: mean3(),
neuroticism: mean3(),
}
}
pub fn baseline_mood(&self) -> Mood {
Mood::from_pad(pad::baseline(self.as_array()))
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Chronotype(pub f32);
impl Chronotype {
pub const POPULATION_SD_HOURS: f32 = 1.5;
pub fn body_hour(self, clock_hour: f32) -> f32 {
(clock_hour - self.0).rem_euclid(24.0)
}
pub fn sample<R: DetRng>(rng: &mut R, sd_hours: f32) -> Self {
let sd = if sd_hours.is_finite() {
sd_hours.abs()
} else {
0.0
};
let normal = (0..12).map(|_| rng.unit()).sum::<f64>() as f32 - 6.0;
Self((normal * sd).clamp(-3.0 * sd, 3.0 * sd))
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mood {
pub pleasure: f32,
pub arousal: f32,
pub dominance: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct MoodRates {
pub pull_per_day: f32,
pub return_per_day: f32,
pub contagion_per_day: f32,
pub positivity_offset: f32,
}
impl Default for MoodRates {
fn default() -> Self {
Self {
pull_per_day: 4.0,
return_per_day: 1.0,
contagion_per_day: 0.3,
positivity_offset: 0.1,
}
}
}
impl Mood {
pub fn from_pad(pad: [f32; 3]) -> Self {
Self {
pleasure: pad[0].clamp(-1.0, 1.0),
arousal: pad[1].clamp(-1.0, 1.0),
dominance: pad[2].clamp(-1.0, 1.0),
}
}
pub fn as_array(&self) -> [f32; 3] {
[self.pleasure, self.arousal, self.dominance]
}
pub fn step(
&mut self,
baseline: Mood,
centre: [f32; 3],
intensity: f32,
contagion: Option<(f32, f32)>,
dt_days: f32,
rates: &MoodRates,
) {
let pull = (rates.pull_per_day * dt_days).min(1.0) * intensity;
self.pleasure += (centre[0] - self.pleasure) * pull;
self.arousal += (centre[1] - self.arousal) * pull;
self.dominance += (centre[2] - self.dominance) * pull;
let resting = baseline.pleasure + rates.positivity_offset;
let back = (rates.return_per_day * dt_days).min(1.0);
self.pleasure += (resting - self.pleasure) * back;
self.arousal += (baseline.arousal - self.arousal) * back;
self.dominance += (baseline.dominance - self.dominance) * back;
if let Some((contacts, weight)) = contagion {
self.pleasure +=
rates.contagion_per_day * weight * dt_days * (contacts - self.pleasure);
}
*self = Self::from_pad(self.as_array());
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Reflect)]
pub enum EmotionKind {
Joy,
Distress,
Hope,
Fear,
Pride,
Shame,
Admiration,
Reproach,
Gratitude,
Anger,
Liking,
Disliking,
}
impl EmotionKind {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"joy" => Some(Self::Joy),
"distress" => Some(Self::Distress),
"hope" => Some(Self::Hope),
"fear" => Some(Self::Fear),
"pride" => Some(Self::Pride),
"shame" => Some(Self::Shame),
"admiration" => Some(Self::Admiration),
"reproach" => Some(Self::Reproach),
"gratitude" => Some(Self::Gratitude),
"anger" => Some(Self::Anger),
"liking" => Some(Self::Liking),
"disliking" => Some(Self::Disliking),
_ => None,
}
}
pub fn pad(self) -> [f32; 3] {
match self {
Self::Joy => [0.40, 0.20, 0.10],
Self::Distress => [-0.40, -0.20, -0.50],
Self::Hope => [0.20, 0.20, -0.10],
Self::Fear => [-0.64, 0.60, -0.43],
Self::Pride => [0.40, 0.30, 0.30],
Self::Shame => [-0.30, 0.10, -0.60],
Self::Admiration => [0.50, 0.30, -0.20],
Self::Reproach => [-0.30, -0.10, 0.40],
Self::Gratitude => [0.40, 0.20, -0.30],
Self::Anger => [-0.51, 0.59, 0.25],
Self::Liking => [0.40, 0.16, -0.24],
Self::Disliking => [-0.40, 0.20, 0.10],
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct ActiveEmotion {
pub kind: EmotionKind,
pub intensity: f32,
pub toward: Option<Entity>,
}
#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
pub struct Emotions(pub Vec<ActiveEmotion>);
impl Emotions {
pub fn appraise(&mut self, kind: EmotionKind, intensity: f32, toward: Option<Entity>) {
if let Some(e) = self
.0
.iter_mut()
.find(|e| e.kind == kind && e.toward == toward)
{
e.intensity = (e.intensity + intensity).min(1.0);
} else {
self.0.push(ActiveEmotion {
kind,
intensity: intensity.clamp(0.0, 1.0),
toward,
});
}
}
pub fn raise_to(&mut self, kind: EmotionKind, intensity: f32, toward: Option<Entity>) {
let intensity = intensity.clamp(0.0, 1.0);
if let Some(e) = self
.0
.iter_mut()
.find(|e| e.kind == kind && e.toward == toward)
{
e.intensity = e.intensity.max(intensity);
} else {
self.0.push(ActiveEmotion {
kind,
intensity,
toward,
});
}
}
pub fn decay(&mut self, dt_days: f32, half_life_days: f32) {
let factor = 0.5f32.powf(dt_days / half_life_days);
for e in &mut self.0 {
e.intensity *= factor;
}
self.0.retain(|e| e.intensity >= 0.05);
}
pub fn virtual_centre(&self) -> ([f32; 3], f32) {
let total: f32 = self.0.iter().map(|e| e.intensity).sum();
if total <= 0.0 {
return ([0.0; 3], 0.0);
}
let mut centre = [0.0f32; 3];
for e in &self.0 {
let pad = e.kind.pad();
for axis in 0..3 {
centre[axis] += pad[axis] * e.intensity;
}
}
for c in &mut centre {
*c /= total;
}
(centre, total.min(1.0))
}
pub fn strongest(&self) -> Option<ActiveEmotion> {
let mut best: Option<ActiveEmotion> = None;
for e in &self.0 {
if best.is_none_or(|b| e.intensity > b.intensity) {
best = Some(*e);
}
}
best
}
pub fn intensity_of(&self, kind: EmotionKind) -> f32 {
self.0
.iter()
.filter(|e| e.kind == kind)
.map(|e| e.intensity)
.sum::<f32>()
.min(1.0)
}
}
#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Needs(pub Vec<f32>);
impl Needs {
pub fn level(&self, index: usize) -> f32 {
self.0.get(index).copied().unwrap_or(0.0)
}
pub fn add(&mut self, index: usize, delta: f32) {
if let Some(level) = self.0.get_mut(index) {
*level = (*level + delta).clamp(0.0, 1.0);
}
}
pub fn decay(&mut self, decay_per_day: &[f32], dt_days: f32) {
for (level, rate) in self.0.iter_mut().zip(decay_per_day) {
*level = (*level - rate * dt_days).max(0.0);
}
}
}
#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Signals(pub Vec<f32>);
impl Signals {
pub fn get(&self, id: crate::defs::SignalId) -> f32 {
self.0.get(usize::from(id.0)).copied().unwrap_or(0.0)
}
pub fn set_id(&mut self, id: crate::defs::SignalId, value: f32) {
let index = usize::from(id.0);
if self.0.len() <= index {
self.0.resize(index + 1, 0.0);
}
if let Some(slot) = self.0.get_mut(index) {
*slot = if value.is_finite() { value } else { 0.0 };
}
}
pub fn set(
&mut self,
defs: &SocietyDefs,
name: &str,
value: f32,
) -> Result<(), crate::defs::DefsError> {
let id = defs
.signal_id(name)
.ok_or_else(|| crate::defs::DefsError::UnknownId {
kind: "signal",
id: name.to_owned(),
owner: "person signals".to_owned(),
})?;
self.set_id(id, value);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Reflect)]
pub enum MemoryKind {
Interaction {
id: InteractionId,
accepted: bool,
initiated: bool,
},
Witnessed {
deed: DeedId,
actor: Entity,
},
Suffered {
deed: DeedId,
actor: Entity,
},
HeardRumor(RumorId),
Talked,
Custom(String),
}
#[derive(Clone, Debug, PartialEq, Reflect)]
pub struct Memory {
pub day: f32,
pub kind: MemoryKind,
pub other: Option<Entity>,
pub valence: f32,
pub importance: f32,
}
#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
pub struct Memories {
pub entries: Vec<Memory>,
}
impl Memories {
fn score(memory: &Memory, now_days: f64, about: Option<Entity>) -> f32 {
let hours = ((now_days - memory.day as f64) * 24.0).max(0.0) as f32;
let about_bonus = match (about, memory.other) {
(Some(a), Some(o)) if a == o => 1.0,
_ => 0.0,
};
recall::recency(hours) + memory.importance + about_bonus
}
pub fn record(&mut self, memory: Memory, now_days: f64, capacity: usize) {
if capacity == 0 {
return;
}
while self.entries.len() >= capacity {
let worst = self
.entries
.iter()
.enumerate()
.map(|(i, m)| (i, Self::score(m, now_days, None)))
.fold(None::<(usize, f32)>, |acc, (i, s)| match acc {
Some((_, best)) if best <= s => acc,
_ => Some((i, s)),
});
match worst {
Some((i, _)) => {
self.entries.remove(i);
}
None => break,
}
}
self.entries.push(memory);
}
pub fn recall(&self, now_days: f64, about: Option<Entity>, n: usize) -> Vec<&Memory> {
let mut scored: Vec<(f32, &Memory)> = self
.entries
.iter()
.map(|m| (Self::score(m, now_days, about), m))
.collect();
scored.sort_by(|a, b| b.0.total_cmp(&a.0));
scored.into_iter().take(n).map(|(_, m)| m).collect()
}
pub fn grudge(&self, other: Entity, now_days: f64) -> f32 {
self.grudge_at_pace(other, now_days, 1.0)
}
pub fn grudge_at_pace(&self, other: Entity, now_days: f64, pace: f32) -> f32 {
recall::grudge(
self.entries
.iter()
.filter(|m| m.other == Some(other))
.map(|m| {
let hours = ((now_days - m.day as f64) * 24.0).max(0.0) as f32;
(m.valence, recall::recency(hours * pace))
}),
)
}
}
#[derive(Message, Clone, Copy, Debug, PartialEq)]
pub struct Appraise {
pub person: Entity,
pub kind: EmotionKind,
pub intensity: f32,
pub toward: Option<Entity>,
}
pub struct MindPlugin;
impl Plugin for MindPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Personality>()
.register_type::<Mood>()
.register_type::<MoodRates>()
.register_type::<EmotionKind>()
.register_type::<ActiveEmotion>()
.register_type::<Emotions>()
.register_type::<Needs>()
.register_type::<Signals>()
.register_type::<Memory>()
.register_type::<MemoryKind>()
.register_type::<Memories>()
.add_message::<Appraise>()
.add_observer(set_baseline_mood)
.add_systems(
FixedUpdate,
crate::absence::announce_returns
.in_set(SocietySystems::Clock)
.before(apply_appraisals),
)
.add_systems(FixedUpdate, apply_appraisals.in_set(SocietySystems::Clock))
.add_systems(
FixedUpdate,
(
crate::absence::miss_the_away.after(update_minds),
update_minds,
decay_needs,
)
.in_set(SocietySystems::Mind),
);
crate::absence::build(app);
}
fn finish(&self, app: &mut App) {
require_core(app, "MindPlugin");
}
}
fn set_baseline_mood(add: On<Add<Person>>, mut people: Query<(&Personality, &mut Mood)>) {
if let Ok((personality, mut mood)) = people.get_mut(add.entity) {
*mood = personality.baseline_mood();
}
}
pub(crate) fn apply_appraisals(
mut reader: MessageReader<Appraise>,
mut emotions: Query<&mut Emotions>,
) {
for msg in reader.read() {
if let Ok(mut e) = emotions.get_mut(msg.person) {
e.appraise(msg.kind, msg.intensity, msg.toward);
}
}
}
fn update_minds(
settings: Res<SocietySettings>,
clock: Res<SocietyClock>,
index: Res<PersonIndex>,
perception: Res<Perception>,
mut people: Query<(&Personality, &mut Emotions, &mut Mood, &Ties)>,
) {
let tuning = &settings.tuning;
let rates = MoodRates {
pull_per_day: tuning.mood_pull_per_day,
return_per_day: tuning.mood_return_per_day,
contagion_per_day: tuning.contagion_per_day,
positivity_offset: tuning.positivity_offset,
};
let pleasure: Vec<f32> = index
.sorted
.iter()
.map(|(_, e)| people.get(*e).map_or(0.0, |(_, _, m, _)| m.pleasure))
.collect();
for (i, (_, entity)) in index.sorted.iter().enumerate() {
let Ok((personality, mut emotions, mut mood, ties)) = people.get_mut(*entity) else {
continue;
};
emotions.decay(clock.dt_days, tuning.emotion_half_life_days);
let (centre, intensity) = emotions.virtual_centre();
let contacts: &[u32] = match perception.contact.get(i) {
Some(list) => list,
None => &[],
};
let mut sum_w = 0.0_f32;
let mut sum_wp = 0.0_f32;
for &j in contacts {
let Some((_, other)) = index.sorted.get(j as usize) else {
continue;
};
let Some(tie) = ties.get(*other) else {
continue;
};
let w = tie.affinity.max(0.0);
if w > 0.0 {
sum_w += w;
sum_wp += w * pleasure.get(j as usize).copied().unwrap_or(0.0);
}
}
let contagion = if sum_w > 0.0 {
Some((sum_wp / sum_w, personality.extraversion))
} else {
None
};
mood.step(
personality.baseline_mood(),
centre,
intensity,
contagion,
clock.dt_days,
&rates,
);
}
}
fn decay_needs(
clock: Res<SocietyClock>,
index: Res<PersonIndex>,
defs: Res<SocietyDefs>,
mut needs: Query<&mut Needs>,
) {
let rates: Vec<f32> = defs.needs.iter().map(|n| n.decay_per_day).collect();
for (_, entity) in &index.sorted {
if let Ok(mut n) = needs.get_mut(*entity) {
n.decay(&rates, clock.dt_days);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy_emerge_rng::seeded;
fn ent(bits: u64) -> Entity {
Entity::from_bits(bits)
}
#[test]
fn personality_sample_is_deterministic_and_in_range() {
let mut a = seeded(3);
let mut b = seeded(3);
let pa = Personality::sample(&mut a);
let pb = Personality::sample(&mut b);
assert_eq!(pa, pb);
for v in pa.as_array() {
assert!((0.0..=1.0).contains(&v));
}
}
#[test]
fn appraise_merges_same_kind_and_target_and_caps() {
let mut e = Emotions::default();
let who = Some(ent(1));
e.appraise(EmotionKind::Anger, 0.7, who);
e.appraise(EmotionKind::Anger, 0.7, who);
e.appraise(EmotionKind::Anger, 0.2, None);
assert_eq!(e.0.len(), 2);
assert_eq!(e.0[0].intensity, 1.0);
}
#[test]
fn emotions_decay_below_threshold_is_dropped() {
let mut e = Emotions::default();
e.appraise(EmotionKind::Joy, 0.5, None);
e.decay(0.1, 0.1);
assert!((e.0[0].intensity - 0.25).abs() < 1e-6);
e.decay(0.5, 0.1);
assert!(e.0.is_empty());
}
#[test]
fn centre_is_weighted_mean_and_intensity_capped() {
let mut e = Emotions::default();
e.appraise(EmotionKind::Joy, 0.5, None);
e.appraise(EmotionKind::Distress, 0.5, None);
let (centre, total) = e.virtual_centre();
assert!((total - 1.0).abs() < 1e-6);
assert!((centre[0] - 0.0).abs() < 1e-6);
}
#[test]
fn needs_clamp_and_decay_floor_at_zero() {
let mut n = Needs(vec![0.5, 0.2]);
n.add(0, 1.0);
assert_eq!(n.level(0), 1.0);
n.decay(&[2.0, 0.1], 1.0);
assert_eq!(n.level(0), 0.0);
assert!((n.level(1) - 0.1).abs() < 1e-6);
}
#[test]
fn mood_relaxes_to_baseline_plus_the_positivity_offset() {
let base = Mood::from_pad([0.3, -0.2, 0.1]);
let mut m = Mood::from_pad([-0.9, 0.9, -0.9]);
let rates = MoodRates::default();
for _ in 0..200 {
m.step(base, [0.0; 3], 0.0, None, 0.05, &rates);
}
assert!((m.pleasure - (base.pleasure + rates.positivity_offset)).abs() < 1e-3);
assert!((m.arousal - base.arousal).abs() < 1e-3);
}
#[test]
fn a_zero_offset_returns_pleasure_to_the_bare_baseline() {
let base = Mood::from_pad([0.3, -0.2, 0.1]);
let mut m = Mood::from_pad([-0.9, 0.9, -0.9]);
let rates = MoodRates {
positivity_offset: 0.0,
..MoodRates::default()
};
for _ in 0..200 {
m.step(base, [0.0; 3], 0.0, None, 0.05, &rates);
}
assert!((m.pleasure - base.pleasure).abs() < 1e-3);
}
fn mem(day: f32, other: Option<Entity>, valence: f32, importance: f32) -> Memory {
Memory {
day,
kind: MemoryKind::Talked,
other,
valence,
importance,
}
}
#[test]
fn recall_prefers_about_then_recency() {
let mut m = Memories::default();
let a = ent(10);
let b = ent(11);
m.record(mem(0.0, Some(b), 0.0, 0.1), 0.0, 8);
m.record(mem(0.0, Some(a), 0.0, 0.1), 0.0, 8);
let top = m.recall(0.0, Some(a), 1);
assert_eq!(top[0].other, Some(a));
}
#[test]
fn record_evicts_lowest_score_when_full() {
let mut m = Memories::default();
m.record(mem(0.0, None, 0.0, 0.0), 0.0, 2);
m.record(mem(0.0, None, 0.0, 0.9), 0.0, 2);
m.record(mem(0.0, None, 0.0, 0.5), 0.0, 2);
assert_eq!(m.entries.len(), 2);
let importances: Vec<f32> = m.entries.iter().map(|e| e.importance).collect();
assert!(!importances.contains(&0.0));
}
#[test]
fn grudge_is_the_worst_offence_not_the_count_of_them() {
let mut m = Memories::default();
let a = ent(20);
m.record(mem(0.0, Some(a), -0.6, 0.5), 0.0, 8);
m.record(mem(0.0, Some(a), -0.6, 0.5), 0.0, 8);
assert!((m.grudge(a, 0.0) - 0.6).abs() < 1e-6);
assert_eq!(m.grudge(ent(21), 0.0), 0.0);
let mut positive = Memories::default();
positive.record(mem(0.0, Some(a), 0.8, 0.5), 0.0, 8);
assert_eq!(positive.grudge(a, 0.0), 0.0);
let mut mixed = Memories::default();
mixed.record(mem(0.0, Some(a), -0.9, 0.5), 0.0, 8);
mixed.record(mem(0.0, Some(a), -0.2, 0.5), 0.0, 8);
assert!((mixed.grudge(a, 0.0) - 0.9).abs() < 1e-6);
}
}