use bevy::prelude::*;
use crate::core::{Away, AwaySince, Deceased, PersonIndex, SocietyClock, SocietySettings, Tuning};
use crate::mind::{Appraise, EmotionKind, Emotions};
use crate::relationships::{Tie, Ties};
#[derive(Message, Debug, Clone, Copy, PartialEq)]
pub struct PersonReturned {
pub person: Entity,
pub days_away: f64,
}
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct ReturnQueue(pub(crate) Vec<(Entity, f64)>);
pub(crate) fn closeness(tie: &Tie, tuning: &Tuning) -> Option<f32> {
let close = tie.label == crate::TieLabel::Friend
|| tie.familiarity_peak >= tuning.bereavement_familiarity;
close.then(|| (0.5 * (1.0 + tie.affinity)).clamp(0.0, 1.0))
}
fn missing_level(tuning: &Tuning, closeness: f32, days: f64) -> f32 {
let grown = 1.0 - (-(days.max(0.0) as f32) / tuning.missing_days.max(f32::EPSILON)).exp();
tuning.missing_cap * closeness * grown
}
fn on_away_added(
add: On<Add<Away>>,
clock: Res<SocietyClock>,
since: Query<Has<AwaySince>>,
mut commands: Commands,
) {
if matches!(since.get(add.entity), Ok(false)) {
commands
.entity(add.entity)
.insert(AwaySince(clock.now_days));
}
}
fn on_away_removed(
remove: On<Remove<Away>>,
clock: Res<SocietyClock>,
since: Query<&AwaySince>,
mut queue: ResMut<ReturnQueue>,
mut commands: Commands,
) {
let days = since
.get(remove.entity)
.map_or(0.0, |since| (clock.now_days - since.0).max(0.0));
queue.0.push((remove.entity, days));
commands.entity(remove.entity).try_remove::<AwaySince>();
}
fn on_deceased_ends_absence(add: On<Add<Deceased>>, mut commands: Commands) {
commands.entity(add.entity).try_remove::<Away>();
}
type Holders<'w, 's> =
Query<'w, 's, (&'static Ties, &'static mut Emotions), (Without<Away>, Without<Deceased>)>;
pub(crate) fn miss_the_away(
settings: Res<SocietySettings>,
clock: Res<SocietyClock>,
index: Res<PersonIndex>,
aways: Query<&AwaySince, (With<Away>, Without<Deceased>)>,
mut holders: Holders,
) {
let tuning = &settings.tuning;
if tuning.missing_cap <= 0.0 {
return;
}
let gone: Vec<(Entity, f64)> = index
.sorted
.iter()
.filter_map(|(_, entity)| {
aways
.get(*entity)
.ok()
.map(|since| (*entity, clock.now_days - since.0))
})
.collect();
if gone.is_empty() {
return;
}
for (_, holder) in &index.sorted {
let Ok((ties, mut emotions)) = holders.get_mut(*holder) else {
continue;
};
for &(away, days) in &gone {
let Some(tie) = ties.get(away) else {
continue;
};
let Some(closeness) = closeness(tie, tuning) else {
continue;
};
let level = missing_level(tuning, closeness, days);
if level > 0.0 {
emotions.raise_to(EmotionKind::Distress, level, Some(away));
}
}
}
}
pub(crate) fn announce_returns(
settings: Res<SocietySettings>,
index: Res<PersonIndex>,
standing: Query<(Has<Deceased>, Has<Away>)>,
mut queue: ResMut<ReturnQueue>,
mut returned: MessageWriter<PersonReturned>,
mut appraisals: MessageWriter<Appraise>,
ties: Query<&Ties>,
) {
let returns = std::mem::take(&mut queue.0);
if returns.is_empty() {
return;
}
let tuning = &settings.tuning;
let mut ordered: Vec<(u32, Entity, f64)> = returns
.into_iter()
.filter_map(|(entity, days)| {
index
.sorted
.iter()
.find(|(_, e)| *e == entity)
.map(|(id, _)| (id.0, entity, days))
})
.collect();
ordered.sort_by(|a, b| a.0.cmp(&b.0).then(a.2.total_cmp(&b.2)));
ordered.dedup_by_key(|(id, _, _)| *id);
for (_, person, days_away) in ordered {
if standing.get(person).is_ok_and(|(dead, _)| dead) {
continue;
}
returned.write(PersonReturned { person, days_away });
if tuning.missing_cap <= 0.0 {
continue;
}
for (_, holder) in &index.sorted {
if *holder == person || standing.get(*holder).is_ok_and(|(dead, gone)| dead || gone) {
continue;
}
let Some(tie) = ties.get(*holder).ok().and_then(|t| t.get(person)) else {
continue;
};
let Some(closeness) = closeness(tie, tuning) else {
continue;
};
let lift = tuning.reunion_fraction * missing_level(tuning, closeness, days_away);
if lift > 0.0 {
appraisals.write(Appraise {
person: *holder,
kind: EmotionKind::Joy,
intensity: lift,
toward: Some(person),
});
}
}
}
}
pub(crate) fn build(app: &mut App) {
app.add_message::<PersonReturned>()
.init_resource::<ReturnQueue>()
.add_observer(on_away_added)
.add_observer(on_away_removed)
.add_observer(on_deceased_ends_absence);
}