use std::collections::HashMap;
use bevy::app::{App, FixedUpdate, Plugin, Startup};
use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::ecs::lifecycle::{Add, Remove};
use bevy::ecs::observer::On;
use bevy::ecs::query::Has;
use bevy::ecs::reflect::ReflectComponent;
use bevy::ecs::resource::Resource;
use bevy::ecs::schedule::{IntoScheduleConfigs, SystemSet};
use bevy::ecs::system::{Commands, Local, Query, Res, ResMut};
use bevy::ecs::world::World;
use bevy::log::{error, warn};
use bevy::math::{Vec2, Vec3};
use bevy::reflect::Reflect;
use bevy::time::{Fixed, Time};
use bevy::transform::components::{GlobalTransform, Transform};
use rand_chacha::ChaCha8Rng;
use crate::beliefs::{Knowledge, Opinions};
use crate::defs::{SocietyDefs, size_person_state};
use crate::groups::{Memberships, Status};
use crate::mind::{Chronotype, Emotions, Memories, Mood, Needs, Personality, Signals};
use crate::relationships::Ties;
use crate::rumor::RumorBook;
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SocietyPlane {
#[default]
XZ,
XY,
}
impl SocietyPlane {
pub fn project(self, p: Vec3) -> Vec2 {
match self {
Self::XZ => Vec2::new(p.x, p.z),
Self::XY => Vec2::new(p.x, p.y),
}
}
pub fn lift(self, p: Vec2) -> Vec3 {
match self {
Self::XZ => Vec3::new(p.x, 0.0, p.y),
Self::XY => Vec3::new(p.x, p.y, 0.0),
}
}
}
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AssessmentNorm {
ImageScoring,
#[default]
Standing,
}
#[derive(Reflect, Debug, Clone, PartialEq)]
pub struct Tuning {
pub emotion_half_life_days: f32,
pub mood_pull_per_day: f32,
pub mood_return_per_day: f32,
pub positivity_offset: f32,
pub contagion_per_day: f32,
pub familiarity_tau_days: f32,
pub familiarity_decay_exp: f32,
pub exposure_per_day: f32,
pub mere_exposure_per_day: f32,
pub affinity_relax_per_day: f32,
pub balance_samples_per_tick: usize,
pub balance_rate_per_day: f32,
pub dyad_balance_per_day: f32,
pub stock_ceiling: f32,
pub price_full_store: f32,
pub price_floor: f32,
pub price_ceiling: f32,
pub trade_reserve: f32,
pub trade_margin: f32,
pub introduction_familiarity: f32,
pub confidence_base: f32,
pub confidence_openness: f32,
pub influence_mu: f32,
pub similarity_gain: f32,
pub anchor_pull: f32,
pub conformity_per_day: f32,
pub witness_gain: f32,
pub witness_affinity: f32,
pub negativity_ratio: f32,
pub witness_dilution: f32,
pub rumor_threshold: f32,
pub gossip_trust_gain: f32,
pub gossip_affinity_gain: f32,
pub contact_gain: f32,
pub friend_affinity: f32,
pub affinity_ceiling: f32,
pub friend_trust: f32,
pub friend_capacity: f32,
pub friend_familiarity: f32,
pub rival_affinity: f32,
pub rival_familiarity: f32,
pub stranger_familiarity: f32,
pub label_hysteresis: f32,
pub friend_band: f32,
pub rival_band: f32,
pub rejection_affinity_penalty: f32,
pub contest_steepness: f32,
pub tier_cutoff: f32,
pub rejection_distress: f32,
pub bereavement_familiarity: f32,
pub missing_cap: f32,
pub missing_days: f32,
pub reunion_fraction: f32,
pub travel_timeout_hours: Option<f32>,
pub stall_timeout_hours: Option<f32>,
pub stall_epsilon: f32,
pub failed_place_memory_hours: Option<f32>,
pub retaliation_refractory_hours: Option<f32>,
pub rebuff_memory_hours: Option<f32>,
pub forgiveness: f32,
pub walk_speed: f32,
pub contact_gossip_cooldown_days: f32,
pub crowded_arrival_growth: f32,
pub crowding_penalty: f32,
}
impl Tuning {
pub fn affinity_weight(&self, affinity: f32) -> f32 {
if self.affinity_ceiling <= 0.0 {
return 0.0;
}
(affinity / self.affinity_ceiling).clamp(-1.0, 1.0)
}
pub fn bond_scale(&self, friends: usize, already_friend: bool) -> f32 {
if self.friend_capacity <= 0.0 || already_friend {
return 1.0;
}
(1.0 - friends as f32 / self.friend_capacity).clamp(0.0, 1.0)
}
}
impl Default for Tuning {
fn default() -> Self {
Self {
emotion_half_life_days: 0.1,
mood_pull_per_day: 4.0,
mood_return_per_day: 1.0,
positivity_offset: 0.1,
contagion_per_day: 0.3,
familiarity_tau_days: 1.0,
familiarity_decay_exp: 0.5,
exposure_per_day: 2.0,
mere_exposure_per_day: 0.05,
affinity_relax_per_day: 0.01,
balance_samples_per_tick: 8,
balance_rate_per_day: 0.5,
dyad_balance_per_day: 0.5,
stock_ceiling: 6.0,
price_full_store: 1.0,
price_floor: 0.25,
price_ceiling: 4.0,
trade_reserve: 1.0,
trade_margin: 0.1,
introduction_familiarity: 0.1,
confidence_base: 0.2,
confidence_openness: 0.6,
influence_mu: 0.3,
similarity_gain: 0.05,
anchor_pull: 0.1,
conformity_per_day: 0.2,
witness_gain: 0.2,
witness_affinity: 0.15,
negativity_ratio: 5.0,
witness_dilution: 0.5,
rumor_threshold: 0.3,
gossip_trust_gain: 0.3,
gossip_affinity_gain: 0.2,
contact_gain: 0.02,
friend_affinity: 0.3,
affinity_ceiling: 0.35,
friend_trust: 0.5,
friend_capacity: 0.0,
friend_familiarity: 0.3,
rival_affinity: 0.3,
rival_familiarity: 0.2,
stranger_familiarity: 0.1,
label_hysteresis: 0.02,
friend_band: 0.05,
rival_band: 0.1,
rejection_affinity_penalty: 0.05,
contest_steepness: 4.0,
tier_cutoff: 0.25,
rejection_distress: 0.3,
bereavement_familiarity: 0.3,
missing_cap: 0.4,
missing_days: 2.0,
reunion_fraction: 0.75,
travel_timeout_hours: Some(2.0),
stall_timeout_hours: None,
stall_epsilon: 0.25,
failed_place_memory_hours: None,
retaliation_refractory_hours: Some(6.0),
rebuff_memory_hours: None,
forgiveness: 12.0,
walk_speed: 1.4,
contact_gossip_cooldown_days: 2.0,
crowded_arrival_growth: 0.7,
crowding_penalty: 0.6,
}
}
}
pub fn skill_level(traits: &crate::Personality, group: crate::defs::SkillGroup) -> f32 {
traits.value(group.trait_of()).clamp(0.0, 1.0)
}
#[derive(Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ClockSource {
#[default]
Fixed,
Host,
}
#[derive(Resource, Reflect, Debug, Clone, Copy, Default, PartialEq)]
pub struct HostClock {
pub now_days: f64,
}
#[derive(Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Locomotion {
#[default]
Builtin,
Host,
}
#[derive(Resource, Reflect, Debug, Clone)]
pub struct SocietySettings {
pub seed: u64,
pub tick_secs: f32,
pub seconds_per_day: f32,
pub start_hour: f32,
pub start_weekday: u8,
pub clock: ClockSource,
pub locomotion: Locomotion,
pub plane: SocietyPlane,
pub perception_radius: f32,
pub contact_radius: f32,
pub interaction_radius: f32,
pub max_ties: usize,
pub close_ties: usize,
pub memory_capacity: usize,
pub knowledge_capacity: usize,
pub assessment: AssessmentNorm,
pub bark_chance: f32,
pub tuning: Tuning,
}
impl Default for SocietySettings {
fn default() -> Self {
Self {
seed: 0x5EED_5EED,
tick_secs: 0.25,
seconds_per_day: 240.0,
start_hour: 8.0,
start_weekday: 0,
clock: ClockSource::Fixed,
locomotion: Locomotion::Builtin,
plane: SocietyPlane::XZ,
perception_radius: 8.0,
contact_radius: 4.0,
interaction_radius: 1.5,
max_ties: 150,
close_ties: 15,
memory_capacity: 64,
knowledge_capacity: 64,
assessment: AssessmentNorm::Standing,
bark_chance: 0.35,
tuning: Tuning::default(),
}
}
}
#[derive(Resource, Reflect, Debug, Clone, Copy, Default, PartialEq)]
pub struct SocietyClock {
pub tick: u64,
pub now_days: f64,
pub dt_days: f32,
}
impl SocietyClock {
pub fn hour(&self) -> f32 {
(self.now_days.rem_euclid(1.0) * 24.0) as f32
}
pub fn day(&self) -> u32 {
self.now_days.floor() as u32
}
pub fn weekday(&self, start_weekday: u8) -> u8 {
let week = u32::from(crate::model::DAYS_PER_WEEK);
((u32::from(start_weekday) % week + self.day() % week) % week) as u8
}
pub fn week_position(&self, start_weekday: u8) -> f32 {
f32::from(self.weekday(start_weekday)) + self.hour() / 24.0
}
}
#[derive(Resource, Reflect, Debug, Clone, Copy, Default, PartialEq)]
pub struct SocietyTick {
pub accumulator: f32,
pub due: bool,
}
#[derive(Resource, Clone, Debug)]
pub struct SocietyRng(pub ChaCha8Rng);
#[derive(Resource, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct NextPersonId(pub u32);
#[derive(Component, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PersonId(pub u32);
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[reflect(Component)]
pub struct PersonKey(pub u64);
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[reflect(Component)]
pub struct PlaceKey(pub u64);
#[derive(Component, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[require(
Personality,
Chronotype,
Mood,
Emotions,
Needs,
Signals,
Ties,
Opinions,
Knowledge,
Memories,
Memberships,
Status,
Transform,
GlobalTransform
)]
pub struct Person;
#[derive(Component, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Autonomous;
#[derive(Component, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[reflect(Component)]
pub struct Dormant;
#[derive(Component, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[reflect(Component)]
#[require(Dormant)]
pub struct Deceased;
#[derive(Component, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[reflect(Component)]
#[require(Dormant)]
pub struct Away;
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)]
#[reflect(Component)]
pub struct AwaySince(pub f64);
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Home(pub Entity);
#[derive(Component, Reflect, Debug, Clone, Default, PartialEq)]
#[reflect(Component)]
pub struct BoundPlaces(pub Vec<(String, Entity)>);
impl BoundPlaces {
pub fn bind(&mut self, name: impl Into<String>, place: Entity) {
let name = name.into();
match self.0.iter_mut().find(|(existing, _)| *existing == name) {
Some(slot) => slot.1 = place,
None => self.0.push((name, place)),
}
}
pub fn get(&self, name: &str) -> Option<Entity> {
self.0
.iter()
.find(|(existing, _)| existing == name)
.map(|(_, entity)| *entity)
}
}
#[derive(Component, Reflect, Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Place {
pub capacity: u32,
}
#[derive(Component, Reflect, Debug, Clone, Default, PartialEq, Eq)]
pub struct PlaceTags(pub Vec<String>);
#[derive(Resource, Reflect, Debug, Clone, Default)]
pub struct PersonIndex {
pub by_id: HashMap<PersonId, Entity>,
pub sorted: Vec<(PersonId, Entity)>,
}
#[derive(Resource, Reflect, Debug, Clone, Default)]
pub struct Perception {
pub persons: Vec<Entity>,
pub pos: Vec<Vec2>,
pub near: Vec<Vec<(u32, f32)>>,
pub contact: Vec<Vec<u32>>,
}
#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SocietySystems {
Clock,
Perceive,
Mind,
Decide,
Interact,
Propagate,
Relate,
Rules,
}
pub fn society_tick_due(tick: Res<SocietyTick>) -> bool {
tick.due
}
fn advance_clock(
settings: Res<SocietySettings>,
fixed: Res<Time<Fixed>>,
host: Option<Res<HostClock>>,
mut warned: Local<bool>,
mut tick: ResMut<SocietyTick>,
mut clock: ResMut<SocietyClock>,
) {
tick.accumulator += fixed.delta_secs();
if tick.accumulator + 1e-6 < settings.tick_secs {
tick.due = false;
return;
}
tick.accumulator -= settings.tick_secs;
tick.due = true;
clock.tick += 1;
match settings.clock {
ClockSource::Fixed => {
clock.dt_days = settings.tick_secs / settings.seconds_per_day;
clock.now_days += f64::from(clock.dt_days);
}
ClockSource::Host => {
let Some(host) = host.as_deref() else {
if !*warned {
*warned = true;
warn!(
"bevy_society: ClockSource::Host but no HostClock resource; the society clock \
is frozen. Insert a HostClock and write now_days every frame."
);
}
clock.dt_days = 0.0;
return;
};
clock.dt_days = (host.now_days - clock.now_days).max(0.0) as f32;
clock.now_days = host.now_days;
}
}
}
fn setup_society(
mut commands: Commands,
settings: Res<SocietySettings>,
host: Option<Res<HostClock>>,
mut clock: ResMut<SocietyClock>,
) {
commands.insert_resource(SocietyRng(bevy_emerge_rng::seeded(settings.seed)));
clock.now_days = match (settings.clock, host.as_deref()) {
(ClockSource::Host, Some(host)) => host.now_days,
_ => f64::from(settings.start_hour) / 24.0,
};
commands.insert_resource(crate::rules::LastDays(Some(clock.now_days)));
}
fn on_person_added(
add: On<Add<Person>>,
mut commands: Commands,
mut next: ResMut<NextPersonId>,
mut index: ResMut<PersonIndex>,
) {
let entity = add.entity;
let id = PersonId(next.0);
match next.0.checked_add(1) {
Some(n) => next.0 = n,
None => {
error!("bevy_society: PersonId overflow; person {entity:?} gets no id");
return;
}
}
commands.entity(entity).insert(id);
index.by_id.insert(id, entity);
index.sorted.push((id, entity));
}
fn on_person_removed(
remove: On<Remove<Person>>,
mut index: ResMut<PersonIndex>,
mut ties: Query<&mut Ties>,
rumors: Option<ResMut<RumorBook>>,
mut knowledge: Query<&mut Knowledge>,
) {
let entity = remove.entity;
if let Some(pos) = index.sorted.iter().position(|(_, e)| *e == entity) {
let (id, _) = index.sorted.remove(pos);
index.by_id.remove(&id);
}
for mut own in &mut ties {
own.0.retain(|tie| tie.target != entity);
}
if let Some(mut book) = rumors {
for retired in book.retire_about(entity) {
for mut owned in &mut knowledge {
owned.forget(retired);
}
}
}
}
fn perceive(
settings: Res<SocietySettings>,
index: Res<PersonIndex>,
transforms: Query<(&GlobalTransform, Has<Dormant>)>,
mut perception: ResMut<Perception>,
) {
let n = index.sorted.len();
let Perception {
persons,
pos,
near,
contact,
} = &mut *perception;
persons.clear();
pos.clear();
let mut dormant: Vec<bool> = Vec::with_capacity(n);
for (_, entity) in &index.sorted {
persons.push(*entity);
let (at, is_dormant) = transforms
.get(*entity)
.map(|(t, d)| (settings.plane.project(t.translation()), d))
.unwrap_or((Vec2::ZERO, false));
pos.push(at);
dormant.push(is_dormant);
}
near.resize_with(n, Vec::new);
contact.resize_with(n, Vec::new);
for list in near.iter_mut() {
list.clear();
}
for list in contact.iter_mut() {
list.clear();
}
let perception_radius = settings.perception_radius;
let contact_radius = settings.contact_radius;
if perception_radius <= 0.0 || pos.is_empty() {
return;
}
let (mut min_x, mut min_y, mut max_x, mut max_y) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for point in pos.iter() {
min_x = min_x.min(point.x);
min_y = min_y.min(point.y);
max_x = max_x.max(point.x);
max_y = max_y.max(point.y);
}
let cell = perception_radius;
let cols = (((max_x - min_x) / cell).floor() as i64 + 1).max(1) as usize;
let rows = (((max_y - min_y) / cell).floor() as i64 + 1).max(1) as usize;
let column = |x: f32| (((x - min_x) / cell).floor() as i64).clamp(0, cols as i64 - 1) as usize;
let row = |y: f32| (((y - min_y) / cell).floor() as i64).clamp(0, rows as i64 - 1) as usize;
let mut cells: Vec<Vec<u32>> = vec![Vec::new(); cols * rows];
for (index, point) in pos.iter().enumerate() {
if dormant.get(index).copied().unwrap_or(false) {
continue;
}
cells[row(point.y) * cols + column(point.x)].push(index as u32);
}
for (i, pi) in pos.iter().enumerate() {
if dormant.get(i).copied().unwrap_or(false) {
continue;
}
let cx = column(pi.x);
let cy = row(pi.y);
let mut candidates: Vec<u32> = Vec::new();
for ny in cy.saturating_sub(1)..=(cy + 1).min(rows - 1) {
for nx in cx.saturating_sub(1)..=(cx + 1).min(cols - 1) {
candidates.extend_from_slice(&cells[ny * cols + nx]);
}
}
candidates.sort_unstable();
for j in candidates {
let j = j as usize;
if j == i {
continue;
}
let d = pi.distance(pos[j]);
if d <= perception_radius {
near[i].push((j as u32, d));
}
if d <= contact_radius {
contact[i].push(j as u32);
}
}
}
}
pub(crate) fn require_core(app: &App, plugin: &str) {
if !app.is_plugin_added::<SocietyCorePlugin>() {
panic!("bevy_society: add SocietyCorePlugin (or SocietyPlugins) before {plugin}");
}
}
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct PendingActions(pub(crate) Vec<crate::script::ScriptAction>);
pub(crate) fn push_script_actions(
pending: &mut PendingActions,
actions: impl IntoIterator<Item = crate::script::ScriptAction>,
) {
pending.0.extend(actions);
}
fn apply_script_actions(world: &mut World) {
let actions = {
let Some(mut pending) = world.get_resource_mut::<PendingActions>() else {
return;
};
std::mem::take(&mut pending.0)
};
if actions.is_empty() {
return;
}
let now = world
.get_resource::<SocietyClock>()
.map_or(0.0, |clock| clock.now_days);
for action in actions {
crate::script_apply::apply(world, action, now);
}
}
pub struct SocietyCorePlugin;
impl Plugin for SocietyCorePlugin {
fn build(&self, app: &mut App) {
app.register_type::<SocietyPlane>()
.register_type::<AssessmentNorm>()
.register_type::<Tuning>()
.register_type::<SocietySettings>()
.register_type::<SocietyClock>()
.register_type::<SocietyTick>()
.register_type::<NextPersonId>()
.register_type::<PersonId>()
.register_type::<PersonKey>()
.register_type::<PlaceKey>()
.register_type::<Person>()
.register_type::<Autonomous>()
.register_type::<Dormant>()
.register_type::<Deceased>()
.register_type::<Away>()
.register_type::<AwaySince>()
.register_type::<Home>()
.register_type::<BoundPlaces>()
.register_type::<Place>()
.register_type::<PlaceTags>()
.register_type::<PersonIndex>()
.register_type::<Perception>()
.init_resource::<SocietySettings>()
.init_resource::<SocietyClock>()
.init_resource::<SocietyTick>()
.init_resource::<NextPersonId>()
.init_resource::<PersonIndex>()
.init_resource::<Perception>()
.init_resource::<SocietyDefs>()
.init_resource::<crate::script::ScriptEngine>()
.init_resource::<crate::snapshot::SnapshotCell>()
.init_resource::<PendingActions>()
.add_message::<crate::interact::Interact>()
.add_message::<crate::interact::SocietyBark>()
.add_message::<crate::script::ScriptEvent>()
.add_message::<crate::script::ScriptError>()
.add_observer(on_person_added)
.add_observer(on_person_removed)
.add_systems(Startup, setup_society)
.configure_sets(
FixedUpdate,
(
SocietySystems::Clock,
SocietySystems::Perceive,
SocietySystems::Mind,
SocietySystems::Decide,
SocietySystems::Interact,
SocietySystems::Propagate,
SocietySystems::Relate,
SocietySystems::Rules,
)
.chain(),
)
.configure_sets(
FixedUpdate,
(
SocietySystems::Perceive,
SocietySystems::Mind,
SocietySystems::Decide,
SocietySystems::Interact,
SocietySystems::Propagate,
SocietySystems::Relate,
SocietySystems::Rules,
)
.run_if(society_tick_due),
)
.add_systems(
FixedUpdate,
(advance_clock, size_person_state)
.chain()
.in_set(SocietySystems::Clock),
)
.add_systems(
FixedUpdate,
(perceive, crate::snapshot::build_snapshot)
.chain()
.in_set(SocietySystems::Perceive),
)
.add_systems(
FixedUpdate,
apply_script_actions
.after(SocietySystems::Rules)
.run_if(society_tick_due),
);
}
}