use bevy::prelude::*;
use bevy_stigmergy::{CellFrame, ChannelDef, StigGrid};
use crate::core::require_core;
use crate::{
FactionId, FieldInput, Memberships, Person, PersonIndex, SocietyClock, SocietyDefs,
SocietySettings, SocietySystems,
};
#[derive(Resource, Reflect, Debug, Clone, PartialEq)]
#[reflect(Resource)]
pub struct SocialFieldSettings {
pub frame: CellFrame,
pub presence: ChannelDef,
pub commotion: ChannelDef,
}
impl Default for SocialFieldSettings {
fn default() -> Self {
Self {
frame: CellFrame::new(Vec2::new(-32.0, -32.0), 1.0, 64, 64),
presence: ChannelDef {
evaporate: 6.0,
diffuse: 0.1,
deposit_radius: 1.5,
},
commotion: ChannelDef {
evaporate: 12.0,
diffuse: 0.2,
deposit_radius: 2.5,
},
}
}
}
#[derive(Debug, Clone, Default)]
struct Peaks {
presence: f32,
commotion: f32,
factions: Vec<f32>,
}
#[derive(Resource)]
pub struct SocialFields {
pub crowd: StigGrid<2>,
pub factions: Vec<StigGrid<1>>,
frame: CellFrame,
peaks: Peaks,
}
impl SocialFields {
pub fn new(settings: &SocialFieldSettings, faction_count: usize) -> Self {
let frame = settings.frame;
let cells: Vec<IVec2> = frame.cells().collect();
let crowd = StigGrid::new(
frame.width,
frame.height,
cells.iter().copied(),
[settings.presence, settings.commotion],
);
let factions = (0..faction_count)
.map(|_| {
StigGrid::new(
frame.width,
frame.height,
cells.iter().copied(),
[settings.presence],
)
})
.collect();
Self {
crowd,
factions,
frame,
peaks: Peaks {
presence: 0.0,
commotion: 0.0,
factions: vec![0.0; faction_count],
},
}
}
pub fn world_to_cell(&self, p: Vec2) -> IVec2 {
self.frame.world_to_cell(p)
}
pub fn frame(&self) -> CellFrame {
self.frame
}
pub fn field_value(&self, input: FieldInput, own: &[FactionId], cell: IVec2) -> f32 {
match input {
FieldInput::Crowd => normalised(self.crowd.sample_cell(0, cell), self.peaks.presence),
FieldInput::Commotion => {
normalised(self.crowd.sample_cell(1, cell), self.peaks.commotion)
}
FieldInput::OwnFaction => {
let (value, peak) = self.faction_sum(own, cell, true);
normalised(value, peak)
}
FieldInput::OtherFactions => {
let (value, peak) = self.faction_sum(own, cell, false);
normalised(value, peak)
}
FieldInput::OwnShare => {
let (own_value, _) = self.faction_sum(own, cell, true);
let (other_value, _) = self.faction_sum(own, cell, false);
let total = own_value + other_value;
if total > 0.0 { own_value / total } else { 0.5 }
}
}
}
pub fn gradient_target(
&self,
input: FieldInput,
own: &[FactionId],
pos: Vec2,
toward: bool,
) -> Option<Vec2> {
let cell = self.world_to_cell(pos);
let value = |offset: IVec2| self.field_value(input, own, cell + offset);
let grad = Vec2::new(
0.5 * (value(IVec2::X) - value(IVec2::NEG_X)),
0.5 * (value(IVec2::Y) - value(IVec2::NEG_Y)),
);
if grad.length_squared() <= 0.0 {
return None;
}
let sign = if toward { 1.0 } else { -1.0 };
Some(pos + sign * grad.normalize() * 4.0 * self.frame.cell_size)
}
fn faction_sum(&self, own: &[FactionId], cell: IVec2, member: bool) -> (f32, f32) {
self.factions
.iter()
.enumerate()
.filter(|(i, _)| own.contains(&FactionId(*i as u16)) == member)
.fold((0.0_f32, 0.0_f32), |(value, peak), (i, grid)| {
(
value + grid.sample_cell(0, cell),
peak + self.peaks.factions.get(i).copied().unwrap_or(0.0),
)
})
}
pub(crate) fn refresh_peaks(&mut self) {
self.peaks.presence = self.crowd.hotspot_cell(0).1;
self.peaks.commotion = self.crowd.hotspot_cell(1).1;
self.peaks.factions = self
.factions
.iter()
.map(|grid| grid.hotspot_cell(0).1)
.collect();
}
}
#[derive(Message, Debug, Clone, Copy, PartialEq)]
pub struct Commotion {
pub at: Vec2,
pub amount: f32,
}
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct CommotionQueue(pub(crate) Vec<Commotion>);
fn queue_commotion(mut commotions: MessageReader<Commotion>, mut queue: ResMut<CommotionQueue>) {
for commotion in commotions.read() {
queue.0.push(*commotion);
}
}
pub struct SocialFieldPlugin;
impl Plugin for SocialFieldPlugin {
fn build(&self, app: &mut App) {
app.register_type::<SocialFieldSettings>()
.init_resource::<SocialFieldSettings>()
.add_message::<Commotion>()
.init_resource::<CommotionQueue>()
.add_systems(FixedUpdate, sync_fields.in_set(SocietySystems::Clock))
.add_systems(FixedUpdate, queue_commotion.in_set(SocietySystems::Clock))
.add_systems(FixedUpdate, deposit_fields.in_set(SocietySystems::Relate));
}
fn finish(&self, app: &mut App) {
require_core(app, "SocialFieldPlugin");
}
}
fn normalised(value: f32, peak: f32) -> f32 {
if peak > 0.0 {
(value / peak).clamp(0.0, 1.0)
} else {
0.0
}
}
fn sync_fields(
mut commands: Commands,
settings: Res<SocialFieldSettings>,
defs: Res<SocietyDefs>,
fields: Option<Res<SocialFields>>,
) {
let faction_count = defs.factions.len();
let stale = match &fields {
None => true,
Some(current) => settings.is_changed() || current.factions.len() != faction_count,
};
if stale {
commands.insert_resource(SocialFields::new(&settings, faction_count));
}
}
fn deposit_fields(
fields: Option<ResMut<SocialFields>>,
clock: Res<SocietyClock>,
society: Res<SocietySettings>,
index: Res<PersonIndex>,
persons: Query<(&GlobalTransform, &Memberships), With<Person>>,
mut commotions: ResMut<CommotionQueue>,
) {
let Some(mut fields) = fields else {
return;
};
let presence = clock.dt_days * 24.0;
for &(_, entity) in &index.sorted {
let Ok((transform, memberships)) = persons.get(entity) else {
continue;
};
let cell = fields.world_to_cell(society.plane.project(transform.translation()));
fields.crowd.deposit(0, cell, presence);
for faction in &memberships.0 {
if let Some(grid) = fields.factions.get_mut(usize::from(faction.0)) {
grid.deposit(0, cell, presence);
}
}
}
for commotion in commotions.0.drain(..) {
if !commotion.at.is_finite() || !commotion.amount.is_finite() || commotion.amount <= 0.0 {
continue;
}
let cell = fields.world_to_cell(commotion.at);
fields.crowd.deposit(1, cell, commotion.amount);
}
fields.crowd.evaporate_diffuse(clock.dt_days);
for grid in &mut fields.factions {
grid.evaporate_diffuse(clock.dt_days);
}
fields.refresh_peaks();
}