diff --git a/assets/common/entity/dungeon/tier-4/miniboss.ron b/assets/common/entity/dungeon/tier-4/miniboss.ron index b17bb8aef1..ea8622e781 100644 --- a/assets/common/entity/dungeon/tier-4/miniboss.ron +++ b/assets/common/entity/dungeon/tier-4/miniboss.ron @@ -7,5 +7,8 @@ inventory: ( loadout: FromBody, ), + agent: ( + idle_wander_factor: 0.0, + ), meta: [], -) \ No newline at end of file +) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 86ec1403d4..86efcd3543 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -252,6 +252,9 @@ pub struct Psyche { /// than `sight_dist`. `None` implied that the agent is always aggro /// towards enemies that it is aware of. pub aggro_dist: Option, + /// A factor that controls how much further an agent will wander when in the + /// idle state. `1.0` is normal. + pub idle_wander_factor: f32, } impl<'a> From<&'a Body> for Psyche { @@ -369,6 +372,7 @@ impl<'a> From<&'a Body> for Psyche { Body::Humanoid(_) => Some(20.0), _ => None, // Always aggressive if detected }, + idle_wander_factor: 1.0, } } } @@ -710,6 +714,12 @@ impl Agent { self } + #[must_use] + pub fn with_idle_wander_factor(mut self, idle_wander_factor: f32) -> Self { + self.psyche.idle_wander_factor = idle_wander_factor; + self + } + #[must_use] pub fn with_position_pid_controller( mut self, diff --git a/common/src/generation.rs b/common/src/generation.rs index 091421aa1f..24d058a71a 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -38,6 +38,14 @@ impl Default for AlignmentMark { fn default() -> Self { Self::Alignment(Alignment::Wild) } } +#[derive(Default, Debug, Deserialize, Clone)] +#[serde(default)] +pub struct AgentConfig { + pub has_agency: Option, + pub no_flee: Option, + pub idle_wander_factor: Option, +} + #[derive(Debug, Deserialize, Clone)] pub enum LoadoutKind { FromBody, @@ -109,6 +117,10 @@ pub struct EntityConfig { /// Alignment, can be Uninit pub alignment: AlignmentMark, + /// Parameterises agent behaviour + #[serde(default)] + pub agent: AgentConfig, + /// Loot /// See LootSpec in lottery pub loot: LootSpec, @@ -154,11 +166,12 @@ pub fn try_all_entity_configs() -> Result, Error> { pub struct EntityInfo { pub pos: Vec3, pub is_waypoint: bool, // Edge case, overrides everything else - // Agent - pub has_agency: bool, pub alignment: Alignment, + /// Parameterises agent behaviour + pub has_agency: bool, pub agent_mark: Option, pub no_flee: bool, + pub idle_wander_factor: f32, // Stats pub body: Body, pub name: Option, @@ -186,9 +199,13 @@ impl EntityInfo { Self { pos, is_waypoint: false, - has_agency: true, alignment: Alignment::Wild, + + has_agency: true, agent_mark: None, + no_flee: false, + idle_wander_factor: 1.0, + body: Body::Humanoid(humanoid::Body::random()), name: None, scale: 1.0, @@ -199,7 +216,6 @@ impl EntityInfo { skillset_asset: None, pet: None, trading_information: None, - no_flee: false, } } @@ -230,6 +246,7 @@ impl EntityInfo { name, body, alignment, + agent, inventory, loot, meta, @@ -270,6 +287,16 @@ impl EntityInfo { // NOTE: set loadout after body, as it's used with default equipement self = self.with_inventory(inventory, config_asset, loadout_rng); + // Prefer the new configuration, if possible + let AgentConfig { + has_agency, + no_flee, + idle_wander_factor, + } = agent; + self.has_agency = has_agency.unwrap_or(self.has_agency); + self.no_flee = no_flee.unwrap_or(self.no_flee); + self.idle_wander_factor = idle_wander_factor.unwrap_or(self.idle_wander_factor); + for field in meta { match field { Meta::SkillSetAsset(asset) => { @@ -619,11 +646,12 @@ mod tests { for config_asset in entity_configs { let EntityConfig { body, + agent: _, inventory, name, loot, meta, - alignment: _alignment, // can't fail if serialized, it's a boring enum + alignment: _, // can't fail if serialized, it's a boring enum } = EntityConfig::from_asset_expect_owned(&config_asset); validate_body(&body, &config_asset); diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index d2c02a3eca..16eaeac655 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -1,7 +1,7 @@ use crate::{ consts::{ - AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, PARTIAL_PATH_DIST, - SEPARATION_BIAS, SEPARATION_DIST, STD_AWARENESS_DECAY_RATE, + AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST, + PARTIAL_PATH_DIST, SEPARATION_BIAS, SEPARATION_DIST, STD_AWARENESS_DECAY_RATE, }, data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData}, util::{ @@ -442,11 +442,26 @@ impl<'a> AgentData<'a> { controller.push_basic_input(InputKind::Jump); } } - agent.bearing += Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5) * 0.1 - - agent.bearing * 0.003 - - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { - (self.pos.0 - patrol_origin).xy() * 0.0002 - }); + + let diff = Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5); + agent.bearing += (diff * 0.1 - agent.bearing * 0.01) + * agent.psyche.idle_wander_factor.max(0.0).sqrt(); + if let Some(patrol_origin) = agent.patrol_origin + // Use owner as patrol origin otherwise + .or_else(|| if let Some(Alignment::Owned(owner_uid)) = self.alignment + && let Some(owner) = get_entity_by_id(owner_uid.id(), read_data) + && let Some(pos) = read_data.positions.get(owner) + { + Some(pos.0) + } else { + None + }) + { + agent.bearing += ((patrol_origin.xy() - self.pos.0.xy()) + / (0.01 + MAX_PATROL_DIST * agent.psyche.idle_wander_factor)) + * 0.015 + * agent.psyche.idle_wander_factor; + } // Stop if we're too close to a wall // or about to walk off a cliff @@ -491,7 +506,7 @@ impl<'a> AgentData<'a> { }; if agent.bearing.magnitude_squared() > 0.5f32.powi(2) { - controller.inputs.move_dir = agent.bearing * 0.65; + controller.inputs.move_dir = agent.bearing; } // Put away weapon diff --git a/server/agent/src/consts.rs b/server/agent/src/consts.rs index e49f11af89..fb1c828280 100644 --- a/server/agent/src/consts.rs +++ b/server/agent/src/consts.rs @@ -1,7 +1,7 @@ pub const DAMAGE_MEMORY_DURATION: f64 = 0.25; pub const FLEE_DURATION: f32 = 3.0; pub const NPC_PICKUP_RANGE: f32 = 2.5; -pub const MAX_FOLLOW_DIST: f32 = 12.0; +pub const MAX_PATROL_DIST: f32 = 50.0; pub const MAX_PATH_DIST: f32 = 170.0; pub const PARTIAL_PATH_DIST: f32 = 50.0; pub const SEPARATION_DIST: f32 = 10.0; diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 963495615c..ccfd38df08 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1506,8 +1506,10 @@ fn handle_spawn( let pos = position(server, target, "target")?; let mut agent = comp::Agent::from_body(&body()); - // If unowned, the agent should stay in a particular place - if !matches!(alignment, comp::Alignment::Owned(_)) { + if matches!(alignment, comp::Alignment::Owned(_)) { + agent.psyche.idle_wander_factor = 0.25; + } else { + // If unowned, the agent should stay in a particular place agent = agent.with_patrol_origin(pos.0); } diff --git a/server/src/pet.rs b/server/src/pet.rs index 5d4ea39df6..148d9426b4 100644 --- a/server/src/pet.rs +++ b/server/src/pet.rs @@ -54,10 +54,14 @@ fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet: // Create an agent for this entity using its body if let Some(body) = ecs.read_storage().get(pet_entity) { + // Pets can trade with their owner let mut agent = Agent::from_body(body).with_behavior( Behavior::default().maybe_with_capabilities(Some(BehaviorCapability::TRADE)), ); agent.behavior.trading_behavior = TradingBehavior::AcceptFood; + // Pets shouldn't wander too far from their owner + agent.psyche.idle_wander_factor = 0.25; + agent.patrol_origin = None; let _ = ecs.write_storage().insert(pet_entity, agent); } diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 6aed9a4e91..6b68b91fd7 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -27,7 +27,7 @@ use self::interaction::{ use super::{ consts::{ - DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_FOLLOW_DIST, + DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST, NORMAL_FLEE_DIR_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS, STD_AWARENESS_DECAY_RATE, }, @@ -415,7 +415,7 @@ fn follow_if_far_away(bdata: &mut BehaviorData) -> bool { if let Some(tgt_pos) = bdata.read_data.positions.get(target) { let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0); - if dist_sqrd > (MAX_FOLLOW_DIST).powi(2) { + if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) { bdata .agent_data .follow(bdata.agent, bdata.controller, bdata.read_data, tgt_pos); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 62355c566b..14aa481cbb 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -426,6 +426,7 @@ impl NpcData { agent_mark, alignment, no_flee, + idle_wander_factor, // stats body, name, @@ -518,7 +519,9 @@ impl NpcData { agent = agent.with_patrol_origin(pos); } - agent.with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee) + agent + .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee) + .with_idle_wander_factor(idle_wander_factor) }); let agent = if matches!(alignment, comp::Alignment::Enemy)