Fix pet idle movement, add a way to configure agent behaviour through manifests

This commit is contained in:
Joshua Barretto 2023-05-16 18:51:46 +01:00
parent 3afeca67c5
commit 52b5967914
9 changed files with 85 additions and 20 deletions

View File

@ -7,5 +7,8 @@
inventory: (
loadout: FromBody,
),
agent: (
idle_wander_factor: 0.0,
),
meta: [],
)
)

View File

@ -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<f32>,
/// 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,

View File

@ -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<bool>,
pub no_flee: Option<bool>,
pub idle_wander_factor: Option<f32>,
}
#[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<String>,
@ -154,11 +166,12 @@ pub fn try_all_entity_configs() -> Result<Vec<String>, Error> {
pub struct EntityInfo {
pub pos: Vec3<f32>,
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<agent::Mark>,
pub no_flee: bool,
pub idle_wander_factor: f32,
// Stats
pub body: Body,
pub name: Option<String>,
@ -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);

View File

@ -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::<f32>() - 0.5, rng.gen::<f32>() - 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::<f32>() - 0.5, rng.gen::<f32>() - 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

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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)