use std::collections::{HashMap, HashSet};
use std::fmt;
use bevy::ecs::entity::Entity;
use bevy::ecs::query::With;
use bevy::ecs::resource::Resource;
use bevy::ecs::system::{Commands, Query, Res};
use bevy::log::error;
use bevy::reflect::Reflect;
use serde::{Deserialize, Serialize};
use crate::beliefs::{Opinion, Opinions};
use crate::core::{Person, SocietySettings};
use crate::groups::Memberships;
use crate::mind::{EmotionKind, Needs, Signals, Trait};
use crate::model::curves::Curve;
use crate::relationships::TieLabel;
use crate::script::{CompiledScript, ScriptCompiler, ScriptId};
const FIELDS_ENABLED: bool = cfg!(feature = "fields");
pub const IDLE_ACTIVITY: ActivityId = ActivityId(0);
const IDLE: &str = "idle";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NeedId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
pub struct SignalId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GoodId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SkillId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TopicId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeedId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FactionId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ActivityId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InteractionId(pub u16);
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct DefsFile {
pub needs: Vec<NeedDef>,
pub signals: Vec<SignalDef>,
pub skills: Vec<SkillDef>,
pub goods: Vec<GoodDef>,
pub topics: Vec<TopicDef>,
pub deeds: Vec<DeedDef>,
pub factions: Vec<FactionDef>,
pub activities: Vec<ActivityDef>,
pub interactions: Vec<InteractionDef>,
pub triggers: Vec<TriggerDef>,
pub conversations: Vec<ConversationRuleDef>,
pub node_effects: Vec<NodeEffectDef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NeedDef {
pub id: String,
#[serde(default)]
pub decay_per_day: f32,
#[serde(default = "one")]
pub initial: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SignalDef {
pub id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SkillGroup {
Craft,
Creative,
Social,
}
impl SkillGroup {
pub fn trait_of(self) -> crate::defs::Trait {
match self {
Self::Craft => Trait::Conscientiousness,
Self::Creative => Trait::Openness,
Self::Social => Trait::Extraversion,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GoodDef {
pub id: String,
#[serde(default)]
pub initial: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SkillDef {
pub id: String,
pub group: SkillGroup,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TopicDef {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DeedDef {
pub id: String,
pub topic: String,
pub harm: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FactionDef {
pub id: String,
#[serde(default = "default_ingroup_bias")]
pub ingroup_bias: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UrgentDef {
pub need: String,
pub below: f32,
pub rank: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActivityDef {
pub id: String,
pub target: ActivityTarget,
pub duration_hours: f32,
#[serde(default)]
pub rank: u8,
#[serde(default)]
pub urgent: Option<UrgentDef>,
#[serde(default)]
pub unavailable: bool,
#[serde(default = "one")]
pub weight: f32,
#[serde(default)]
pub considerations: Vec<Consideration>,
#[serde(default)]
pub satisfies: Vec<(String, f32)>,
pub interaction: Option<String>,
#[serde(default)]
pub produces: Vec<(String, f32)>,
#[serde(default)]
pub consumes: Vec<(String, f32)>,
#[serde(default)]
pub profession: Option<String>,
#[serde(default)]
pub barks: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ActivityTarget {
Itself,
Wander(f32),
Place(String),
Home,
Bound(String),
Person,
Gradient(FieldInput, bool),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Consideration {
pub input: Input,
pub curve: Curve,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Input {
Need(String),
Signal(String),
Hour,
BodyHour,
Week,
Pleasure,
Arousal,
Dominance,
Trait(Trait),
Emotion(EmotionKind),
Affinity,
Trust,
Respect,
Familiarity,
Distance,
SocialProof,
Skill(String),
Surplus(String),
Stock(String),
Field(FieldInput),
Script(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FieldInput {
Crowd,
Commotion,
OwnFaction,
OtherFactions,
OwnShare,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InteractionDef {
pub id: String,
pub requires: Option<String>,
#[serde(default)]
pub accept: AcceptDef,
#[serde(default)]
pub on_accept: EffectsDef,
#[serde(default)]
pub on_reject: EffectsDef,
#[serde(default)]
pub influence: Vec<String>,
#[serde(default)]
pub gossip: bool,
pub deed: Option<String>,
#[serde(default)]
pub contest: bool,
#[serde(default)]
pub barks: InteractionBarks,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct AcceptDef {
pub base: f32,
pub affinity: f32,
pub trust: f32,
pub agreeableness: f32,
pub pleasure: f32,
pub ingroup: f32,
}
impl Default for AcceptDef {
fn default() -> Self {
Self {
base: 0.5,
affinity: 0.3,
trust: 0.2,
agreeableness: 0.2,
pleasure: 0.1,
ingroup: 0.1,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct TieDelta {
pub affinity: f32,
pub trust: f32,
pub respect: f32,
pub familiarity: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct EffectsDef {
pub actor_to_target: TieDelta,
pub target_to_actor: TieDelta,
pub actor_emotion: Option<(EmotionKind, f32)>,
pub target_emotion: Option<(EmotionKind, f32)>,
pub actor_needs: Vec<(String, f32)>,
pub target_needs: Vec<(String, f32)>,
#[serde(default)]
pub actor_gives: Vec<(String, f32)>,
#[serde(default)]
pub target_gives: Vec<(String, f32)>,
pub importance: f32,
pub script: Option<String>,
}
impl Default for EffectsDef {
fn default() -> Self {
Self {
actor_gives: Vec::new(),
target_gives: Vec::new(),
actor_to_target: TieDelta::default(),
target_to_actor: TieDelta::default(),
actor_emotion: None,
target_emotion: None,
actor_needs: Vec::new(),
target_needs: Vec::new(),
importance: 0.3,
script: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct InteractionBarks {
pub actor: Vec<String>,
pub accepted: Vec<String>,
pub rejected: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TriggerDef {
pub id: String,
pub on: TriggerOn,
#[serde(rename = "if")]
pub condition: Option<String>,
pub then: String,
#[serde(default)]
pub once: bool,
#[serde(default)]
pub cooldown_hours: f32,
#[serde(default)]
pub area: Option<TriggerArea>,
#[serde(default)]
pub factions: Vec<String>,
#[serde(default)]
pub persons: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TriggerArea {
pub min: [f32; 2],
pub max: [f32; 2],
}
impl TriggerArea {
pub fn contains(&self, p: [f32; 2]) -> bool {
p[0] >= self.min[0] && p[0] <= self.max[0] && p[1] >= self.min[1] && p[1] <= self.max[1]
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TriggerOn {
Tick,
Hour(f32),
PersonDied,
Interaction(String),
TieLabel(TieLabel),
RumorReach {
deed: String,
fraction: f32,
},
NormViolated(String),
NeedBelow {
need: String,
below: f32,
},
Activity(String),
DialogueNode {
conversation: String,
node: String,
},
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConversationRuleDef {
pub conversation: String,
#[serde(default)]
pub priority: i32,
#[serde(rename = "if")]
pub condition: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NodeEffectDef {
pub conversation: String,
pub node: String,
pub then: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DefsError {
Ron(String),
Duplicate {
kind: &'static str,
id: String,
},
UnknownId {
kind: &'static str,
id: String,
owner: String,
},
Invalid {
owner: String,
message: String,
},
Script {
owner: String,
message: String,
},
}
impl fmt::Display for DefsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ron(message) => write!(f, "bevy_society: definitions do not parse: {message}"),
Self::Duplicate { kind, id } => {
write!(f, "bevy_society: {kind} '{id}' is defined more than once")
}
Self::UnknownId { kind, id, owner } => {
write!(
f,
"bevy_society: {owner} names {kind} '{id}', which is not defined"
)
}
Self::Invalid { owner, message } => write!(f, "bevy_society: {owner}: {message}"),
Self::Script { owner, message } => {
write!(
f,
"bevy_society: {owner}: script does not compile: {message}"
)
}
}
}
}
impl std::error::Error for DefsError {}
impl DefsFile {
pub fn from_ron(text: &str) -> Result<Self, DefsError> {
ron::from_str(text).map_err(|err| DefsError::Ron(err.to_string()))
}
pub fn to_ron(&self) -> Result<String, DefsError> {
ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.map_err(|err| DefsError::Ron(err.to_string()))
}
pub fn merge(&mut self, other: DefsFile) -> Result<(), DefsError> {
check_disjoint(
"need",
self.needs.iter().map(|d| d.id.as_str()),
other.needs.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"signal",
self.signals.iter().map(|d| d.id.as_str()),
other.signals.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"topic",
self.topics.iter().map(|d| d.id.as_str()),
other.topics.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"deed",
self.deeds.iter().map(|d| d.id.as_str()),
other.deeds.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"faction",
self.factions.iter().map(|d| d.id.as_str()),
other.factions.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"activity",
self.activities.iter().map(|d| d.id.as_str()),
other.activities.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"interaction",
self.interactions.iter().map(|d| d.id.as_str()),
other.interactions.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"good",
self.goods.iter().map(|d| d.id.as_str()),
other.goods.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"skill",
self.skills.iter().map(|d| d.id.as_str()),
other.skills.iter().map(|d| d.id.as_str()),
)?;
check_disjoint(
"trigger",
self.triggers.iter().map(|d| d.id.as_str()),
other.triggers.iter().map(|d| d.id.as_str()),
)?;
self.needs.extend(other.needs);
self.signals.extend(other.signals);
self.skills.extend(other.skills);
self.goods.extend(other.goods);
self.topics.extend(other.topics);
self.deeds.extend(other.deeds);
self.factions.extend(other.factions);
self.activities.extend(other.activities);
self.interactions.extend(other.interactions);
self.triggers.extend(other.triggers);
self.conversations.extend(other.conversations);
self.node_effects.extend(other.node_effects);
Ok(())
}
pub fn starter() -> Result<Self, DefsError> {
Self::from_ron(include_str!("../assets/starter.society.ron"))
}
}
fn check_disjoint<'a>(
kind: &'static str,
mine: impl Iterator<Item = &'a str>,
theirs: impl Iterator<Item = &'a str>,
) -> Result<(), DefsError> {
let mut seen: HashSet<&str> = mine.collect();
for name in theirs {
if !seen.insert(name) {
return Err(DefsError::Duplicate {
kind,
id: name.to_owned(),
});
}
}
Ok(())
}
#[derive(Resource, Clone)]
pub struct SocietyDefs {
pub(crate) needs: Vec<NeedSpec>,
pub(crate) signals: Vec<String>,
pub(crate) skills: Vec<SkillGroup>,
pub goods: Vec<String>,
pub(crate) good_initial: Vec<f32>,
pub(crate) topics: Vec<TopicSpec>,
pub(crate) deeds: Vec<DeedSpec>,
pub(crate) factions: Vec<FactionSpec>,
pub(crate) activities: Vec<ActivitySpec>,
pub(crate) interactions: Vec<InteractionSpec>,
pub(crate) triggers: Vec<TriggerSpec>,
pub(crate) conversations: Vec<ConversationSpec>,
pub(crate) node_effects: Vec<NodeEffectSpec>,
pub(crate) scripts: Vec<CompiledScript>,
need_ids: HashMap<String, u16>,
signal_ids: HashMap<String, u16>,
skill_ids: HashMap<String, u16>,
good_ids: HashMap<String, u16>,
topic_ids: HashMap<String, u16>,
deed_ids: HashMap<String, u16>,
faction_ids: HashMap<String, u16>,
activity_ids: HashMap<String, u16>,
interaction_ids: HashMap<String, u16>,
}
impl Default for SocietyDefs {
fn default() -> Self {
Self::empty()
}
}
impl SocietyDefs {
fn empty() -> Self {
let mut activity_ids = HashMap::new();
activity_ids.insert(IDLE.to_owned(), IDLE_ACTIVITY.0);
Self {
needs: Vec::new(),
signals: Vec::new(),
skills: Vec::new(),
goods: Vec::new(),
good_initial: Vec::new(),
good_ids: HashMap::new(),
topics: Vec::new(),
deeds: Vec::new(),
factions: Vec::new(),
activities: vec![ActivitySpec {
name: IDLE.to_owned(),
target: ActivityTarget::Itself,
duration_hours: 1.0,
rank: 0,
urgent: None,
unavailable: false,
weight: 1.0,
considerations: Vec::new(),
satisfies: Vec::new(),
interaction: None,
produces: Vec::new(),
consumes: Vec::new(),
barks: Vec::new(),
}],
interactions: Vec::new(),
triggers: Vec::new(),
conversations: Vec::new(),
node_effects: Vec::new(),
scripts: Vec::new(),
need_ids: HashMap::new(),
signal_ids: HashMap::new(),
skill_ids: HashMap::new(),
topic_ids: HashMap::new(),
deed_ids: HashMap::new(),
faction_ids: HashMap::new(),
activity_ids,
interaction_ids: HashMap::new(),
}
}
pub fn compile(file: DefsFile) -> Result<Self, DefsError> {
let mut defs = Self::empty();
let mut scripts = ScriptCompiler::new();
for def in file.needs {
defs.add_need(def)?;
}
for def in file.signals {
defs.add_signal(def)?;
}
for def in file.skills {
defs.add_skill(def)?;
}
for def in file.goods {
defs.add_good(def)?;
}
for def in file.topics {
defs.add_topic(def)?;
}
for def in file.deeds {
defs.add_deed(def)?;
}
for def in file.factions {
defs.add_faction(def)?;
}
for def in file.interactions {
defs.add_interaction(def, &mut scripts)?;
}
for def in file.activities {
defs.add_activity(def, &mut scripts)?;
}
let mut trigger_names = HashSet::new();
for def in file.triggers {
defs.add_trigger(def, &mut scripts, &mut trigger_names)?;
}
for def in file.conversations {
defs.add_conversation(def, &mut scripts)?;
}
for def in file.node_effects {
defs.add_node_effect(def, &mut scripts)?;
}
defs.scripts = scripts.into_scripts();
Ok(defs)
}
pub fn from_ron(text: &str) -> Result<Self, DefsError> {
Self::compile(DefsFile::from_ron(text)?)
}
pub fn reload(&mut self, file: DefsFile) -> Result<(), DefsError> {
let next = Self::compile(file)?;
let changed = |table: &str| {
Err(DefsError::Invalid {
owner: "reload".to_owned(),
message: format!(
"the {table} list changed; ids already in the world index it, so a live edit \
may not add, remove or reorder {table}"
),
})
};
fn names<T>(items: &[T], name: impl Fn(&T) -> &str) -> Vec<&str> {
items.iter().map(name).collect()
}
if names(&self.needs, |s| &s.name) != names(&next.needs, |s| &s.name) {
return changed("needs");
}
if self.signals != next.signals {
return changed("signals");
}
if self.skill_ids != next.skill_ids || self.skills != next.skills {
return changed("skills");
}
if self.goods != next.goods {
return changed("goods");
}
if names(&self.topics, |s| &s.name) != names(&next.topics, |s| &s.name) {
return changed("topics");
}
if names(&self.deeds, |s| &s.name) != names(&next.deeds, |s| &s.name) {
return changed("deeds");
}
if names(&self.factions, |s| &s.name) != names(&next.factions, |s| &s.name) {
return changed("factions");
}
if names(&self.activities, |s| &s.name) != names(&next.activities, |s| &s.name) {
return changed("activities");
}
if names(&self.interactions, |s| &s.name) != names(&next.interactions, |s| &s.name) {
return changed("interactions");
}
*self = next;
Ok(())
}
pub fn starter() -> Result<Self, DefsError> {
Self::compile(DefsFile::starter()?)
}
pub fn need_id(&self, name: &str) -> Option<NeedId> {
self.need_ids.get(name).copied().map(NeedId)
}
pub fn good_id(&self, name: &str) -> Option<GoodId> {
self.good_ids.get(name).copied().map(GoodId)
}
pub(crate) fn activity_by_name(&self, name: &str) -> Option<&ActivitySpec> {
self.activity_id(name)
.and_then(|id| self.activities.get(usize::from(id.0)))
}
pub fn skill_id(&self, name: &str) -> Option<SkillId> {
self.skill_ids.get(name).copied().map(SkillId)
}
pub fn skill_group(&self, id: SkillId) -> Option<SkillGroup> {
self.skills.get(usize::from(id.0)).copied()
}
pub fn signal_id(&self, name: &str) -> Option<SignalId> {
self.signal_ids.get(name).copied().map(SignalId)
}
pub fn topic_id(&self, name: &str) -> Option<TopicId> {
self.topic_ids.get(name).copied().map(TopicId)
}
pub fn deed_id(&self, name: &str) -> Option<DeedId> {
self.deed_ids.get(name).copied().map(DeedId)
}
pub fn faction_id(&self, name: &str) -> Option<FactionId> {
self.faction_ids.get(name).copied().map(FactionId)
}
pub fn activity_id(&self, name: &str) -> Option<ActivityId> {
self.activity_ids.get(name).copied().map(ActivityId)
}
pub fn interaction_id(&self, name: &str) -> Option<InteractionId> {
self.interaction_ids.get(name).copied().map(InteractionId)
}
pub fn need_name(&self, id: NeedId) -> &str {
self.needs
.get(usize::from(id.0))
.map_or("", |spec| spec.name.as_str())
}
pub fn signal_name(&self, id: SignalId) -> &str {
self.signals
.get(usize::from(id.0))
.map_or("", String::as_str)
}
pub fn topic_name(&self, id: TopicId) -> &str {
self.topics
.get(usize::from(id.0))
.map_or("", |spec| spec.name.as_str())
}
pub fn deed_name(&self, id: DeedId) -> &str {
self.deeds
.get(usize::from(id.0))
.map_or("", |spec| spec.name.as_str())
}
pub fn faction_name(&self, id: FactionId) -> &str {
self.factions
.get(usize::from(id.0))
.map_or("", |spec| spec.name.as_str())
}
pub fn activity_name(&self, id: ActivityId) -> &str {
self.activities
.get(usize::from(id.0))
.map_or("", |spec| spec.name.as_str())
}
pub fn activity_signals(&self) -> Vec<(&str, Vec<crate::defs::Signal>)> {
self.activities
.iter()
.map(|spec| {
(
spec.name.as_str(),
spec.considerations.iter().map(|term| term.signal).collect(),
)
})
.collect()
}
pub fn need_decay(&self, need: &str) -> Option<f32> {
let id = self.need_id(need)?;
self.needs
.get(usize::from(id.0))
.map(|spec| spec.decay_per_day)
}
pub fn sources_for(&self, need: &str) -> Vec<(&str, f32)> {
let Some(id) = self.need_id(need) else {
return Vec::new();
};
self.activities
.iter()
.filter_map(|activity| {
let rate = activity
.satisfies
.iter()
.find(|(slot, _)| *slot == id)
.map(|(_, rate)| *rate)?;
Some((activity.name.as_str(), rate * activity.duration_hours))
})
.collect()
}
pub fn best_source(&self, need: &str) -> Option<(&str, f32)> {
let id = self.need_id(need)?;
self.activities
.iter()
.filter_map(|activity| {
let rate = activity
.satisfies
.iter()
.find(|(slot, _)| *slot == id)
.map(|(_, rate)| *rate)?;
Some((activity.name.as_str(), rate * activity.duration_hours))
})
.fold(None::<(&str, f32)>, |best, candidate| match best {
Some((_, amount)) if amount >= candidate.1 => best,
_ => Some(candidate),
})
}
pub fn activity_targets_gradient(&self, name: &str) -> bool {
self.activities
.iter()
.any(|spec| spec.name == name && matches!(spec.target, ActivityTarget::Gradient(..)))
}
pub fn has_gradient_activity(&self) -> bool {
self.activities
.iter()
.any(|spec| matches!(spec.target, ActivityTarget::Gradient(..)))
}
pub fn activity_durations(&self) -> Vec<(&str, f32)> {
self.activities
.iter()
.map(|spec| (spec.name.as_str(), spec.duration_hours))
.collect()
}
pub fn interaction_name(&self, id: InteractionId) -> &str {
self.interactions
.get(usize::from(id.0))
.map_or("", |spec| spec.name.as_str())
}
pub fn needs<'a>(
&self,
values: impl IntoIterator<Item = (&'a str, f32)>,
) -> Result<Needs, DefsError> {
let mut levels: Vec<f32> = self.needs.iter().map(|spec| spec.initial).collect();
for (name, level) in values {
let id = require(self.need_id(name), "need", name, "person needs")?;
if let Some(slot) = levels.get_mut(usize::from(id.0)) {
*slot = level;
}
}
Ok(Needs(levels))
}
pub fn signals<'a>(
&self,
values: impl IntoIterator<Item = (&'a str, f32)>,
) -> Result<Signals, DefsError> {
let mut levels = vec![0.0; self.signals.len()];
for (name, value) in values {
let id = require(self.signal_id(name), "signal", name, "person signals")?;
if let Some(slot) = levels.get_mut(usize::from(id.0)) {
*slot = if value.is_finite() { value } else { 0.0 };
}
}
Ok(Signals(levels))
}
pub fn opinions<'a>(
&self,
values: impl IntoIterator<Item = (&'a str, f32)>,
) -> Result<Opinions, DefsError> {
let mut topics = vec![Opinion::default(); self.topics.len()];
for (name, value) in values {
let id = require(self.topic_id(name), "topic", name, "person opinions")?;
if let Some(slot) = topics.get_mut(usize::from(id.0)) {
*slot = Opinion::at(value);
}
}
Ok(Opinions(topics))
}
pub fn memberships<'a>(
&self,
factions: impl IntoIterator<Item = &'a str>,
) -> Result<Memberships, DefsError> {
let mut ids = Vec::new();
for name in factions {
ids.push(require(
self.faction_id(name),
"faction",
name,
"person memberships",
)?);
}
ids.sort();
ids.dedup();
Ok(Memberships(ids))
}
fn add_need(&mut self, def: NeedDef) -> Result<(), DefsError> {
let owner = format!("need '{}'", def.id);
if def.decay_per_day.is_nan() || def.decay_per_day < 0.0 {
return Err(invalid(&owner, "decay_per_day must be 0 or more"));
}
register(&mut self.need_ids, "need", &def.id, self.needs.len())?;
self.needs.push(NeedSpec {
name: def.id,
decay_per_day: def.decay_per_day,
initial: def.initial,
});
Ok(())
}
fn add_signal(&mut self, def: SignalDef) -> Result<(), DefsError> {
register(&mut self.signal_ids, "signal", &def.id, self.signals.len())?;
self.signals.push(def.id);
Ok(())
}
fn add_skill(&mut self, def: SkillDef) -> Result<(), DefsError> {
register(&mut self.skill_ids, "skill", &def.id, self.skills.len())?;
self.skills.push(def.group);
Ok(())
}
fn add_good(&mut self, def: GoodDef) -> Result<(), DefsError> {
let owner = format!("good '{}'", def.id);
if !def.initial.is_finite() || def.initial < 0.0 {
return Err(invalid(&owner, "initial must be a number, 0 or more"));
}
register(&mut self.good_ids, "good", &def.id, self.goods.len())?;
self.goods.push(def.id);
self.good_initial.push(def.initial);
Ok(())
}
fn add_topic(&mut self, def: TopicDef) -> Result<(), DefsError> {
register(&mut self.topic_ids, "topic", &def.id, self.topics.len())?;
self.topics.push(TopicSpec { name: def.id });
Ok(())
}
fn add_deed(&mut self, def: DeedDef) -> Result<(), DefsError> {
let owner = format!("deed '{}'", def.id);
let topic = require(self.topic_id(&def.topic), "topic", &def.topic, &owner)?;
if !(-1.0..=1.0).contains(&def.harm) {
return Err(invalid(&owner, "harm must be in [-1, 1]"));
}
register(&mut self.deed_ids, "deed", &def.id, self.deeds.len())?;
self.deeds.push(DeedSpec {
name: def.id,
topic,
harm: def.harm,
});
Ok(())
}
fn add_faction(&mut self, def: FactionDef) -> Result<(), DefsError> {
register(
&mut self.faction_ids,
"faction",
&def.id,
self.factions.len(),
)?;
self.factions.push(FactionSpec {
name: def.id,
ingroup_bias: def.ingroup_bias,
});
Ok(())
}
fn add_interaction(
&mut self,
def: InteractionDef,
scripts: &mut ScriptCompiler,
) -> Result<(), DefsError> {
let owner = format!("interaction '{}'", def.id);
register(
&mut self.interaction_ids,
"interaction",
&def.id,
self.interactions.len(),
)?;
let requires = match def.requires.as_deref() {
Some(source) => Some(scripts.expression(&format!("{owner} requires"), source)?),
None => None,
};
let on_accept =
self.compile_effects(&format!("{owner} on_accept"), &def.on_accept, scripts)?;
let on_reject =
self.compile_effects(&format!("{owner} on_reject"), &def.on_reject, scripts)?;
let mut influence = Vec::with_capacity(def.influence.len());
for topic in &def.influence {
influence.push(require(self.topic_id(topic), "topic", topic, &owner)?);
}
let deed = match def.deed.as_deref() {
Some(name) => Some(require(self.deed_id(name), "deed", name, &owner)?),
None => None,
};
let barks_owner = format!("{owner} barks");
for line in def
.barks
.actor
.iter()
.chain(&def.barks.accepted)
.chain(&def.barks.rejected)
{
check_template(&barks_owner, line)?;
}
self.interactions.push(InteractionSpec {
name: def.id,
requires,
accept: def.accept,
on_accept,
on_reject,
influence,
gossip: def.gossip,
deed,
contest: def.contest,
barks: def.barks,
});
Ok(())
}
fn compile_effects(
&self,
owner: &str,
def: &EffectsDef,
scripts: &mut ScriptCompiler,
) -> Result<EffectsSpec, DefsError> {
let actor_needs = self.need_deltas(owner, &def.actor_needs)?;
let target_needs = self.need_deltas(owner, &def.target_needs)?;
let actor_gives = self.good_list(owner, &def.actor_gives)?;
let target_gives = self.good_list(owner, &def.target_gives)?;
let script = match def.script.as_deref() {
Some(source) => Some(scripts.statements(owner, source)?),
None => None,
};
Ok(EffectsSpec {
actor_to_target: def.actor_to_target,
target_to_actor: def.target_to_actor,
actor_emotion: def.actor_emotion,
target_emotion: def.target_emotion,
actor_needs,
target_needs,
actor_gives,
target_gives,
importance: def.importance,
script,
})
}
fn need_deltas(
&self,
owner: &str,
pairs: &[(String, f32)],
) -> Result<Vec<(NeedId, f32)>, DefsError> {
let mut out = Vec::with_capacity(pairs.len());
for (name, delta) in pairs {
out.push((require(self.need_id(name), "need", name, owner)?, *delta));
}
Ok(out)
}
fn add_activity(
&mut self,
def: ActivityDef,
scripts: &mut ScriptCompiler,
) -> Result<(), DefsError> {
let owner = format!("activity '{}'", def.id);
if def.duration_hours.is_nan() || def.duration_hours <= 0.0 {
return Err(invalid(&owner, "duration_hours must be above 0"));
}
if def.weight.is_nan() || def.weight < 0.0 {
return Err(invalid(&owner, "weight must be 0 or more"));
}
check_target(&owner, &def.target)?;
let urgent = match &def.urgent {
Some(urgent) => {
let urgent_owner = format!("{owner} urgent");
let need = require(
self.need_id(&urgent.need),
"need",
&urgent.need,
&urgent_owner,
)?;
if !urgent.below.is_finite() || urgent.below <= 0.0 || urgent.below > 1.0 {
return Err(invalid(&urgent_owner, "below must be in (0, 1]"));
}
if urgent.rank <= def.rank {
return Err(invalid(
&urgent_owner,
"rank must be above the activity's own rank, or it changes nothing",
));
}
Some(Urgent {
need,
below: urgent.below,
rank: urgent.rank,
})
}
None => None,
};
register(
&mut self.activity_ids,
"activity",
&def.id,
self.activities.len(),
)?;
let mut considerations = Vec::with_capacity(def.considerations.len());
for (index, term) in def.considerations.iter().enumerate() {
let term_owner = format!("{owner} consideration {index}");
considerations.push(self.compile_term(&term_owner, term, &def.target, scripts)?);
}
let satisfies_owner = format!("{owner} satisfies");
let satisfies = self.need_deltas(&satisfies_owner, &def.satisfies)?;
let interaction = match def.interaction.as_deref() {
Some(name) => {
if !matches!(def.target, ActivityTarget::Person) {
return Err(invalid(&owner, "an interaction needs a Person target"));
}
Some(require(
self.interaction_id(name),
"interaction",
name,
&owner,
)?)
}
None => None,
};
let barks_owner = format!("{owner} barks");
for line in &def.barks {
check_template(&barks_owner, line)?;
}
let produces = self.good_list(&owner, &def.produces)?;
let consumes = self.good_list(&owner, &def.consumes)?;
self.activities.push(ActivitySpec {
name: def.id,
target: def.target,
duration_hours: def.duration_hours,
rank: def.rank,
urgent,
unavailable: def.unavailable,
weight: def.weight,
considerations,
satisfies,
interaction,
produces,
consumes,
barks: def.barks,
});
Ok(())
}
fn good_list(
&self,
owner: &str,
list: &[(String, f32)],
) -> Result<Vec<(GoodId, f32)>, DefsError> {
let mut out = Vec::with_capacity(list.len());
for (name, amount) in list {
if *amount < 0.0 {
return Err(invalid(
owner,
format!("good '{name}' has a negative amount"),
));
}
out.push((require(self.good_id(name), "good", name, owner)?, *amount));
}
Ok(out)
}
fn compile_term(
&self,
owner: &str,
term: &Consideration,
target: &ActivityTarget,
scripts: &mut ScriptCompiler,
) -> Result<Term, DefsError> {
check_input(owner, &term.input, target)?;
if matches!(term.curve, Curve::Hours(..))
&& !matches!(term.input, Input::Hour | Input::BodyHour)
{
return Err(invalid(
owner,
"an Hours curve needs the Hour or BodyHour input",
));
}
if let Curve::Days(from, to) = term.curve {
if !matches!(term.input, Input::Week) {
return Err(invalid(owner, "a Days curve needs the Week input"));
}
let days = 0.0..=f32::from(crate::model::DAYS_PER_WEEK);
if !days.contains(&from) || !days.contains(&to) {
return Err(invalid(
owner,
format!(
"Days({from}, {to}) is outside the week: both ends must be in 0..={}",
crate::model::DAYS_PER_WEEK
),
));
}
}
let signal = match &term.input {
Input::Need(name) => Signal::Need(require(self.need_id(name), "need", name, owner)?),
Input::Signal(name) => {
Signal::Host(require(self.signal_id(name), "signal", name, owner)?)
}
Input::Hour => Signal::Hour,
Input::BodyHour => Signal::BodyHour,
Input::Week => Signal::Week,
Input::Pleasure => Signal::Pleasure,
Input::Arousal => Signal::Arousal,
Input::Dominance => Signal::Dominance,
Input::Trait(personality) => Signal::Trait(*personality),
Input::Emotion(kind) => Signal::Emotion(*kind),
Input::Affinity => Signal::Affinity,
Input::Trust => Signal::Trust,
Input::Respect => Signal::Respect,
Input::Familiarity => Signal::Familiarity,
Input::Distance => Signal::Distance,
Input::SocialProof => Signal::SocialProof,
Input::Skill(name) => {
Signal::Skill(require(self.skill_id(name), "skill", name, owner)?)
}
Input::Surplus(name) => {
Signal::Surplus(require(self.good_id(name), "good", name, owner)?)
}
Input::Stock(name) => Signal::Stock(require(self.good_id(name), "good", name, owner)?),
Input::Field(field) => Signal::Field(*field),
Input::Script(source) => Signal::Script(scripts.expression(owner, source)?),
};
Ok(Term {
signal,
curve: term.curve,
})
}
fn add_trigger(
&mut self,
def: TriggerDef,
scripts: &mut ScriptCompiler,
names: &mut HashSet<String>,
) -> Result<(), DefsError> {
let owner = format!("trigger '{}'", def.id);
if !names.insert(def.id.clone()) {
return Err(DefsError::Duplicate {
kind: "trigger",
id: def.id,
});
}
if def.cooldown_hours.is_nan() || def.cooldown_hours < 0.0 {
return Err(invalid(&owner, "cooldown_hours must be 0 or more"));
}
let on = self.trigger_kind(&owner, &def.on)?;
let binds_no_actor = matches!(
on,
TriggerKind::Tick | TriggerKind::Hour(_) | TriggerKind::RumorReach { .. }
);
if let Some(area) = def.area {
if area.min.iter().chain(&area.max).any(|v| !v.is_finite())
|| area.min[0] > area.max[0]
|| area.min[1] > area.max[1]
{
return Err(invalid(&owner, "area needs finite corners with min <= max"));
}
if binds_no_actor {
return Err(invalid(
&owner,
"area needs an event that binds an actor; Tick, Hour and RumorReach bind none",
));
}
}
if binds_no_actor && (!def.factions.is_empty() || !def.persons.is_empty()) {
return Err(invalid(
&owner,
"factions and persons need an event that binds an actor; Tick, Hour and RumorReach bind none",
));
}
let mut factions = Vec::with_capacity(def.factions.len());
for name in &def.factions {
match self.faction_id(name) {
Some(id) => factions.push(id),
None => {
return Err(invalid(&owner, format!("unknown faction '{name}'")));
}
}
}
let condition = match def.condition.as_deref() {
Some(source) => Some(scripts.expression(&format!("{owner} if"), source)?),
None => None,
};
let then = scripts.statements(&owner, &def.then)?;
self.triggers.push(TriggerSpec {
id: def.id.clone(),
on,
condition,
then,
once: def.once,
cooldown_hours: def.cooldown_hours,
area: def.area,
factions,
persons: def.persons.clone(),
});
Ok(())
}
fn trigger_kind(&self, owner: &str, on: &TriggerOn) -> Result<TriggerKind, DefsError> {
Ok(match on {
TriggerOn::Tick => TriggerKind::Tick,
TriggerOn::PersonDied => TriggerKind::PersonDied,
TriggerOn::Hour(hour) => {
if !(0.0..24.0).contains(hour) {
return Err(invalid(owner, "an Hour trigger needs an hour in [0, 24)"));
}
TriggerKind::Hour(*hour)
}
TriggerOn::Interaction(name) => TriggerKind::Interaction(require(
self.interaction_id(name),
"interaction",
name,
owner,
)?),
TriggerOn::TieLabel(label) => TriggerKind::TieLabel(*label),
TriggerOn::RumorReach { deed, fraction } => {
let deed = require(self.deed_id(deed), "deed", deed, owner)?;
if !(*fraction > 0.0 && *fraction <= 1.0) {
return Err(invalid(owner, "fraction must be in (0, 1]"));
}
TriggerKind::RumorReach {
deed,
fraction: *fraction,
}
}
TriggerOn::NormViolated(deed) => {
TriggerKind::NormViolated(require(self.deed_id(deed), "deed", deed, owner)?)
}
TriggerOn::NeedBelow { need, below } => TriggerKind::NeedBelow {
need: require(self.need_id(need), "need", need, owner)?,
below: *below,
},
TriggerOn::Activity(name) => {
TriggerKind::Activity(require(self.activity_id(name), "activity", name, owner)?)
}
TriggerOn::DialogueNode { conversation, node } => TriggerKind::DialogueNode {
conversation: conversation.clone(),
node: node.clone(),
},
TriggerOn::Custom(name) => TriggerKind::Custom(name.clone()),
})
}
fn add_conversation(
&mut self,
def: ConversationRuleDef,
scripts: &mut ScriptCompiler,
) -> Result<(), DefsError> {
let owner = format!("conversation rule for '{}'", def.conversation);
let condition = match def.condition.as_deref() {
Some(source) => Some(scripts.expression(&owner, source)?),
None => None,
};
self.conversations.push(ConversationSpec {
conversation: def.conversation,
priority: def.priority,
condition,
});
Ok(())
}
fn add_node_effect(
&mut self,
def: NodeEffectDef,
scripts: &mut ScriptCompiler,
) -> Result<(), DefsError> {
let owner = format!("node effect '{}/{}'", def.conversation, def.node);
let then = scripts.statements(&owner, &def.then)?;
self.node_effects.push(NodeEffectSpec {
conversation: def.conversation,
node: def.node,
then,
});
Ok(())
}
}
type PersonStateQuery<'w, 's> = Query<
'w,
's,
(
Entity,
&'static mut Needs,
&'static mut Signals,
&'static mut Opinions,
Option<&'static mut crate::Stock>,
),
With<Person>,
>;
pub fn size_person_state(
defs: Res<SocietyDefs>,
settings: Res<SocietySettings>,
mut commands: Commands,
mut people: PersonStateQuery,
) {
let need_count = defs.needs.len();
let signal_count = defs.signals.len();
let topic_count = defs.topics.len();
let good_count = defs.goods.len();
let ceiling = settings.tuning.stock_ceiling;
let starting = |slot: usize| defs.good_initial.get(slot).map_or(0.0, |&n| n.min(ceiling));
for (entity, mut needs, mut signals, mut opinions, stock) in &mut people {
match stock {
Some(mut stock) => {
let have = stock.0.len();
if have > good_count {
stock.0.truncate(good_count);
} else if have < good_count {
stock.0.extend((have..good_count).map(starting));
}
}
None if good_count > 0 => {
commands
.entity(entity)
.insert(crate::Stock((0..good_count).map(starting).collect()));
}
None => {}
}
let have = needs.0.len();
if have > need_count {
error!(
"bevy_society: {entity} has {have} needs but the defs define {need_count}; truncating"
);
needs.0.truncate(need_count);
} else if have < need_count {
needs
.0
.extend(defs.needs.iter().skip(have).map(|spec| spec.initial));
}
let have = signals.0.len();
if have > signal_count {
error!(
"bevy_society: {entity} has {have} signals but the defs define {signal_count}; truncating"
);
signals.0.truncate(signal_count);
} else if have < signal_count {
signals
.0
.extend(std::iter::repeat_n(0.0, signal_count - have));
}
let have = opinions.0.len();
if have > topic_count {
error!(
"bevy_society: {entity} has {have} opinions but the defs define {topic_count}; truncating"
);
opinions.0.truncate(topic_count);
} else if have < topic_count {
opinions
.0
.extend(std::iter::repeat_n(Opinion::default(), topic_count - have));
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct NeedSpec {
pub(crate) name: String,
pub(crate) decay_per_day: f32,
pub(crate) initial: f32,
}
#[derive(Debug, Clone)]
pub(crate) struct TopicSpec {
pub(crate) name: String,
}
#[derive(Debug, Clone)]
pub(crate) struct DeedSpec {
pub(crate) name: String,
pub(crate) topic: TopicId,
pub(crate) harm: f32,
}
#[derive(Debug, Clone)]
pub(crate) struct FactionSpec {
pub(crate) name: String,
pub(crate) ingroup_bias: f32,
}
#[derive(Debug, Clone)]
pub(crate) struct ActivitySpec {
pub(crate) name: String,
pub(crate) target: ActivityTarget,
pub(crate) duration_hours: f32,
pub(crate) rank: u8,
pub(crate) urgent: Option<Urgent>,
pub(crate) unavailable: bool,
pub(crate) weight: f32,
pub(crate) considerations: Vec<Term>,
pub(crate) satisfies: Vec<(NeedId, f32)>,
pub(crate) interaction: Option<InteractionId>,
pub(crate) produces: Vec<(GoodId, f32)>,
pub(crate) consumes: Vec<(GoodId, f32)>,
pub(crate) barks: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Urgent {
pub(crate) need: NeedId,
pub(crate) below: f32,
pub(crate) rank: u8,
}
impl ActivitySpec {
pub(crate) fn rank_for(&self, levels: &[f32]) -> u8 {
match self.urgent {
Some(urgent)
if levels
.get(usize::from(urgent.need.0))
.is_some_and(|level| *level < urgent.below) =>
{
self.rank.max(urgent.rank)
}
_ => self.rank,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Term {
pub(crate) signal: Signal,
pub(crate) curve: Curve,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Signal {
Need(NeedId),
Host(SignalId),
Hour,
BodyHour,
Week,
Pleasure,
Arousal,
Dominance,
Trait(Trait),
Emotion(EmotionKind),
Affinity,
Trust,
Respect,
Familiarity,
Distance,
SocialProof,
Skill(SkillId),
Surplus(GoodId),
Stock(GoodId),
Field(FieldInput),
Script(ScriptId),
}
#[derive(Debug, Clone)]
pub(crate) struct EffectsSpec {
pub(crate) actor_to_target: TieDelta,
pub(crate) target_to_actor: TieDelta,
pub(crate) actor_emotion: Option<(EmotionKind, f32)>,
pub(crate) target_emotion: Option<(EmotionKind, f32)>,
pub(crate) actor_needs: Vec<(NeedId, f32)>,
pub(crate) target_needs: Vec<(NeedId, f32)>,
pub(crate) actor_gives: Vec<(GoodId, f32)>,
pub(crate) target_gives: Vec<(GoodId, f32)>,
pub(crate) importance: f32,
pub(crate) script: Option<ScriptId>,
}
#[derive(Debug, Clone)]
pub(crate) struct InteractionSpec {
pub(crate) name: String,
pub(crate) requires: Option<ScriptId>,
pub(crate) accept: AcceptDef,
pub(crate) on_accept: EffectsSpec,
pub(crate) on_reject: EffectsSpec,
pub(crate) influence: Vec<TopicId>,
pub(crate) gossip: bool,
pub(crate) deed: Option<DeedId>,
pub(crate) contest: bool,
pub(crate) barks: InteractionBarks,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum TriggerKind {
Tick,
Hour(f32),
PersonDied,
Interaction(InteractionId),
TieLabel(TieLabel),
RumorReach {
deed: DeedId,
fraction: f32,
},
NormViolated(DeedId),
NeedBelow {
need: NeedId,
below: f32,
},
Activity(ActivityId),
DialogueNode {
conversation: String,
node: String,
},
Custom(String),
}
#[derive(Debug, Clone)]
pub(crate) struct TriggerSpec {
pub(crate) id: String,
pub(crate) on: TriggerKind,
pub(crate) condition: Option<ScriptId>,
pub(crate) then: ScriptId,
pub(crate) once: bool,
pub(crate) cooldown_hours: f32,
pub(crate) area: Option<TriggerArea>,
pub(crate) factions: Vec<FactionId>,
pub(crate) persons: Vec<u32>,
}
#[derive(Debug, Clone)]
pub(crate) struct ConversationSpec {
pub(crate) conversation: String,
pub(crate) priority: i32,
pub(crate) condition: Option<ScriptId>,
}
#[derive(Debug, Clone)]
pub(crate) struct NodeEffectSpec {
pub(crate) conversation: String,
pub(crate) node: String,
pub(crate) then: ScriptId,
}
fn invalid(owner: &str, message: impl Into<String>) -> DefsError {
DefsError::Invalid {
owner: owner.to_owned(),
message: message.into(),
}
}
fn require<T>(
found: Option<T>,
kind: &'static str,
name: &str,
owner: &str,
) -> Result<T, DefsError> {
found.ok_or_else(|| DefsError::UnknownId {
kind,
id: name.to_owned(),
owner: owner.to_owned(),
})
}
fn register(
map: &mut HashMap<String, u16>,
kind: &'static str,
name: &str,
index: usize,
) -> Result<u16, DefsError> {
if name.is_empty() {
return Err(invalid(kind, "an id must not be empty"));
}
if map.contains_key(name) {
return Err(DefsError::Duplicate {
kind,
id: name.to_owned(),
});
}
let id = u16::try_from(index).map_err(|_| {
invalid(
&format!("{kind} '{name}'"),
"too many definitions of this kind (limit 65536)",
)
})?;
map.insert(name.to_owned(), id);
Ok(id)
}
fn check_target(owner: &str, target: &ActivityTarget) -> Result<(), DefsError> {
if let ActivityTarget::Bound(name) = target
&& name.trim().is_empty()
{
return Err(invalid(owner, "a Bound target needs a name"));
}
if let ActivityTarget::Wander(radius) = target
&& (radius.is_nan() || *radius < 0.0)
{
return Err(invalid(owner, "Wander radius must be 0 or more"));
}
if matches!(target, ActivityTarget::Gradient(..)) && !FIELDS_ENABLED {
return Err(invalid(
owner,
"a Gradient target needs the `fields` feature",
));
}
Ok(())
}
fn check_input(owner: &str, input: &Input, target: &ActivityTarget) -> Result<(), DefsError> {
let person_only = matches!(
input,
Input::Affinity
| Input::Trust
| Input::Respect
| Input::Familiarity
| Input::SocialProof
| Input::Surplus(_)
);
if person_only && !matches!(target, ActivityTarget::Person) {
return Err(invalid(owner, format!("{input:?} needs a Person target")));
}
if matches!(input, Input::Distance)
&& !matches!(
target,
ActivityTarget::Place(_)
| ActivityTarget::Home
| ActivityTarget::Bound(_)
| ActivityTarget::Person
)
{
return Err(invalid(
owner,
"Distance needs a Place, Home, Bound or Person target",
));
}
if matches!(input, Input::Field(_)) && !FIELDS_ENABLED {
return Err(invalid(owner, "field inputs need the `fields` feature"));
}
Ok(())
}
fn check_template(owner: &str, text: &str) -> Result<(), DefsError> {
let mut rest = text;
while let Some(position) = rest.find(['{', '}']) {
let tail = &rest[position..];
if tail.starts_with('}') {
return Err(invalid(owner, format!("stray '}}' in bark '{text}'")));
}
let Some(end) = tail.find('}') else {
return Err(invalid(owner, format!("unclosed '{{' in bark '{text}'")));
};
let key = &tail[1..end];
if !matches!(key, "self" | "target" | "subject" | "place") {
return Err(invalid(
owner,
format!("unknown placeholder '{{{key}}}' in bark '{text}'"),
));
}
rest = &tail[end + 1..];
}
Ok(())
}
fn default_ingroup_bias() -> f32 {
0.2
}
fn one() -> f32 {
1.0
}