veloren/common/src/comp/agent.rs

978 lines
34 KiB
Rust
Raw Normal View History

use crate::{
2021-10-22 22:11:09 +00:00
comp::{
2023-10-08 11:35:01 +00:00
arthropod, biped_large, biped_small, bird_medium, humanoid, quadruped_low,
quadruped_medium, quadruped_small, ship, Body, UtteranceKind,
2021-10-22 22:11:09 +00:00
},
2021-02-23 02:16:07 +00:00
path::Chaser,
2023-05-04 10:23:46 +00:00
rtsim::{NpcInput, RtSimController},
2021-04-07 20:11:29 +00:00
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
uid::Uid,
};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage, Entity as EcsEntity};
use std::{collections::VecDeque, fmt};
use strum::{EnumIter, IntoEnumIterator};
2019-07-29 19:54:58 +00:00
use vek::*;
use super::{dialogue::Subject, Pos};
2021-03-29 14:47:42 +00:00
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
Implement /price_list (work in progress), stub for /buy and /sell remove outdated economic simulation code remove old values, document add natural resources to economy Remove NaturalResources from Place (now in Economy) find closest site to each chunk implement natural resources (the distance scale is wrong) cargo fmt working distance calculation this collection of natural resources seem to make sense, too much Wheat though use natural resources and controlled area to replenish goods increase the amount of chunks controlled by one guard to 50 add new professions and goods to the list implement multiple products per worker remove the old code and rename the new code to the previous name correctly determine which goods guards will give you access to correctly estimate the amount of natural resources controlled adapt to new server API instrument tooltips Now I just need to figure out how to store a (reference to) a closure closures for tooltip content generation pass site/cave id to the client Add economic information to the client structure (not yet exchanged with the server) Send SiteId to the client, prepare messages for economy request Make client::sites a HashMap Specialize the Crafter into Brewer,Bladesmith and Blacksmith working server request for economic info from within tooltip fully operational economic tooltips I need to fix the id clash between caves and towns though fix overlapping ids between caves and sites display stock amount correctly handle invalid (cave) ids in the request some initial balancing, turn off most info logging less intrusive way of implementing the dynamic tool tips in map further tooltip cleanup further cleanup, dynamic tooltip not fully working as intended correctly working tooltip visibility logic cleanup, display labor value separate economy info request in a separate translation unit display values as well nicer display format for economy add last_exports and coin to the new economy do not allocate natural resources to Dungeons (making town so much larger) balancing attempt print town size statistics cargo fmt (dead code) resource tweaks, csv debugging output a more interesting town (and then all sites) fix the labor value logic (now we have meaningful prices) load professions from ron (WIP) use assets manager in economy loading professions works use professions from ron file fix Labor debug logic count chunks per type separately (preparing for better resource control) better structured resource data traders, more professions (WIP) fix exception when starting the simulation fix replenish function TODO: - use area_ratio for resource usage (chunks should be added to stock, ratio on usage?) - fix trading documentation clean up fix merge artifact Revise trader mechanic start Coin with a reasonable default remove the outdated economy code preserve documentation from removed old structure output neighboring sites (preparation) pass list of neighbors to economy add trade structures trading stub Description of purpose by zesterer on Discord remember prices (needed for planning) avoid growing the order vector unboundedly into_iter doesn't clear the Vec, so clear it manually use drain to process Vecs, avoid clone fix the test server implement a test stub (I need to get it faster than 30 seconds to be more useful) enable info output in test debug missing and extra goods use the same logging extension as water, activate feature update dependencies determine good prices, good exchange goods a working set of revisions a cozy world which economy tests in 2s first order planning version fun with package version buy according to value/priority, not missing amount introduce min price constant, fix order priority in depth debugging with a correct sign the trading plans start to make sense move the trade planning to a separate function rename new function reorganize code into subroutines (much cleaner) each trading step now has its own function cut down the number of debugging output introduce RoadSecurity and Transportation transport capacity bookkeeping only plan to pay with valuable goods, you can no longer stockpile unused options (which interestingly shows a huge impact, to be investigated) Coin is now listed as a payment (although not used) proper transportation estimation (although 0) remove more left overs uncovered by viewing the code in a merge request use the old default values, handle non-pileable stocks directly before increasing it (as economy is based on last year's products) don't order the missing good multiple times also it uses coin to buy things! fix warnings and use the transportation from stock again cargo fmt prepare evaluation of trade don't count transportation multiple times fix merge artifact operational trade planning trade itself is still misleading make clippy happy clean up correct labor ratio of merchants (no need to multiply with amount produced) incomplete merchant labor_value computation correct last commit make economy of scale more explicit make clippy happy (and code cleaner) more merchant tweaks (more pop=better) beginning of real trading code revert the update of dependencies remove stale comments/unused code trading implementation complete (but untested) something is still strange ... fix sign in trading another sign fix some bugfixes and plenty of debugging code another bug fixed, more to go fix another invariant (rounding will lead to very small negative value) introduce Terrain and Territory fix merge mistakes
2021-03-14 03:18:32 +00:00
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
2022-01-18 03:02:43 +00:00
const SECONDS_BEFORE_FORGET_SOUNDS: f64 = 180.0;
2022-10-25 02:52:37 +00:00
//intentionally very few concurrent action state variables are allowed. This is
// to keep the complexity of our AI from getting too large, too quickly.
// Originally I was going to provide 30 of these, but if we decide later that
// this is too many and somebody is already using 30 in one of their AI, it will
// be difficult to go back.
/// The number of timers that a single Action node can track concurrently
/// Define constants within a given action node to index between them.
2022-10-25 02:52:37 +00:00
const ACTIONSTATE_NUMBER_OF_CONCURRENT_TIMERS: usize = 5;
/// The number of float counters that a single Action node can track
/// concurrently Define constants within a given action node to index between
/// them.
const ACTIONSTATE_NUMBER_OF_CONCURRENT_COUNTERS: usize = 5;
/// The number of integer counters that a single Action node can track
/// concurrently Define constants within a given action node to index between
/// them.
const ACTIONSTATE_NUMBER_OF_CONCURRENT_INT_COUNTERS: usize = 5;
/// The number of booleans that a single Action node can track concurrently
/// Define constants within a given action node to index between them.
2022-10-25 02:52:37 +00:00
const ACTIONSTATE_NUMBER_OF_CONCURRENT_CONDITIONS: usize = 5;
2023-01-21 19:02:27 +00:00
/// The number of positions that can be remembered by an agent
const ACTIONSTATE_NUMBER_OF_CONCURRENT_POSITIONS: usize = 5;
2022-09-08 19:51:02 +00:00
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Alignment {
/// Wild animals and gentle giants
Wild,
/// Dungeon cultists and bandits
Enemy,
/// Friendly folk in villages
Npc,
/// Farm animals and pets of villagers
Tame,
/// Pets you've tamed with a collar
2020-07-07 00:01:39 +00:00
Owned(Uid),
/// Passive objects like training dummies
Passive,
}
2022-09-08 19:51:02 +00:00
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
Split LodoutBuilder::build_loadout LoadoutBuilder::build_loadout is a function which has four parameters and 3 of them are Option<>, and although fourth (body) isn't Option<>, it's optional too because it is used only in some combinations of another arguments. Because these combinations produces quirky code flow, it will be better to split it to different methods. So we did following changes to remove it and rewrite code that was using it to use better methods. * Introduce LoadoutPreset as new LoadoutConfig, currently it's only used in Summon ability, because SummonInfo uses Copy and we can't specify String for specifying asset path for loadout. Everything else is rewritten to use asset path to create loadouts. * More builder methods for LoadoutBuilder. Namely: - from_default which is used in server/src/cmd.rs in "/spawn" command. - with_default_equipment, with_default_maintool to use default loadout for specific body - with_preset to use LoadoutPreset * Add new make_loadout field with `fn (loadout_builder, trading_info) -> loadout_builder` to EntityInfo which allows to lazily construct loadout without modifying LoadoutBuilder code * Fix Merchants not having trade site We had heuristic that if something has Merchant LoadoutConfig - it's merchant, which can be false, especially if we create Merchant loadout lazily As side note, we do same check for Guards and it fails too. Too fix it, we introduce new agent::Mark, which explicitly specifies kind of agent for entity * `LoadoutBuilder::build_loadout` was written in a such way that depending on main_tool you will have different loadout. Turns out it was this way only for Adlets though and this behaviour is reproduced by specifying different loadouts directly in world code.
2021-06-05 18:05:31 +00:00
pub enum Mark {
Merchant,
Guard,
}
impl Alignment {
// Always attacks
pub fn hostile_towards(self, other: Alignment) -> bool {
match (self, other) {
(Alignment::Passive, _) => false,
(_, Alignment::Passive) => false,
(Alignment::Enemy, Alignment::Enemy) => false,
(Alignment::Enemy, Alignment::Wild) => false,
(Alignment::Wild, Alignment::Enemy) => false,
(Alignment::Wild, Alignment::Wild) => false,
(Alignment::Npc, Alignment::Wild) => false,
(Alignment::Npc, Alignment::Enemy) => true,
(_, Alignment::Enemy) => true,
(Alignment::Enemy, _) => true,
_ => false,
}
}
2023-04-03 19:33:06 +00:00
// Usually never attacks
pub fn passive_towards(self, other: Alignment) -> bool {
match (self, other) {
2020-04-19 11:50:25 +00:00
(Alignment::Enemy, Alignment::Enemy) => true,
(Alignment::Owned(a), Alignment::Owned(b)) if a == b => true,
(Alignment::Npc, Alignment::Npc) => true,
(Alignment::Npc, Alignment::Tame) => true,
(Alignment::Enemy, Alignment::Wild) => true,
(Alignment::Wild, Alignment::Enemy) => true,
(Alignment::Tame, Alignment::Npc) => true,
(Alignment::Tame, Alignment::Tame) => true,
(_, Alignment::Passive) => true,
_ => false,
}
}
2023-04-03 19:33:06 +00:00
// Never attacks
pub fn friendly_towards(self, other: Alignment) -> bool {
match (self, other) {
(Alignment::Enemy, Alignment::Enemy) => true,
(Alignment::Owned(a), Alignment::Owned(b)) if a == b => true,
(Alignment::Npc, Alignment::Npc) => true,
(Alignment::Npc, Alignment::Tame) => true,
(Alignment::Tame, Alignment::Npc) => true,
(Alignment::Tame, Alignment::Tame) => true,
(_, Alignment::Passive) => true,
_ => false,
}
}
}
impl Component for Alignment {
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
}
2021-04-07 20:11:29 +00:00
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, Default)]
2021-04-07 20:11:29 +00:00
pub struct BehaviorCapability: u8 {
const SPEAK = 0b00000001;
2022-09-21 23:21:46 +00:00
const TRADE = 0b00000010;
2021-04-07 20:11:29 +00:00
}
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, Default)]
2021-04-07 20:11:29 +00:00
pub struct BehaviorState: u8 {
const TRADING = 0b00000001;
const TRADING_ISSUER = 0b00000010;
}
}
#[derive(Default, Copy, Clone, Debug)]
pub enum TradingBehavior {
#[default]
None,
RequireBalanced {
trade_site: SiteId,
},
AcceptFood,
}
impl TradingBehavior {
fn can_trade(&self, alignment: Option<Alignment>, counterparty: Uid) -> bool {
match self {
TradingBehavior::RequireBalanced { .. } => true,
TradingBehavior::AcceptFood => alignment == Some(Alignment::Owned(counterparty)),
TradingBehavior::None => false,
}
}
}
2021-04-07 20:11:29 +00:00
/// # Behavior Component
/// This component allow an Entity to register one or more behavior tags.
2021-06-15 16:15:58 +00:00
/// These tags act as flags of what an Entity can do, or what it is doing.
2021-04-07 20:11:29 +00:00
/// Behaviors Tags can be added and removed as the Entity lives, to update its
/// state when needed
#[derive(Default, Copy, Clone, Debug)]
pub struct Behavior {
capabilities: BehaviorCapability,
state: BehaviorState,
pub trading_behavior: TradingBehavior,
2021-04-07 20:11:29 +00:00
}
impl From<BehaviorCapability> for Behavior {
fn from(capabilities: BehaviorCapability) -> Self {
Behavior {
capabilities,
state: BehaviorState::default(),
trading_behavior: TradingBehavior::None,
2021-04-07 20:11:29 +00:00
}
}
}
impl Behavior {
2021-06-15 16:15:58 +00:00
/// Builder function
2021-04-08 16:33:00 +00:00
/// Set capabilities if Option is Some
#[must_use]
2021-04-08 16:33:00 +00:00
pub fn maybe_with_capabilities(
mut self,
maybe_capabilities: Option<BehaviorCapability>,
) -> Self {
if let Some(capabilities) = maybe_capabilities {
self.allow(capabilities)
}
self
}
/// Builder function
/// Set trade_site if Option is Some
#[must_use]
2021-04-08 16:33:00 +00:00
pub fn with_trade_site(mut self, trade_site: Option<SiteId>) -> Self {
if let Some(trade_site) = trade_site {
self.trading_behavior = TradingBehavior::RequireBalanced { trade_site };
}
2021-04-08 16:33:00 +00:00
self
}
2021-04-07 20:11:29 +00:00
/// Set capabilities to the Behavior
pub fn allow(&mut self, capabilities: BehaviorCapability) {
self.capabilities.set(capabilities, true)
}
/// Unset capabilities to the Behavior
pub fn deny(&mut self, capabilities: BehaviorCapability) {
self.capabilities.set(capabilities, false)
}
/// Check if the Behavior is able to do something
pub fn can(&self, capabilities: BehaviorCapability) -> bool {
self.capabilities.contains(capabilities)
}
/// Check if the Behavior is able to trade
2022-09-22 16:53:35 +00:00
pub fn can_trade(&self, alignment: Option<Alignment>, counterparty: Uid) -> bool {
self.trading_behavior.can_trade(alignment, counterparty)
2022-09-21 23:21:46 +00:00
}
2021-04-07 20:11:29 +00:00
/// Set a state to the Behavior
pub fn set(&mut self, state: BehaviorState) { self.state.set(state, true) }
/// Unset a state to the Behavior
pub fn unset(&mut self, state: BehaviorState) { self.state.set(state, false) }
/// Check if the Behavior has a specific state
pub fn is(&self, state: BehaviorState) -> bool { self.state.contains(state) }
/// Get the trade site at which this behavior evaluates prices, if it does
pub fn trade_site(&self) -> Option<SiteId> {
if let TradingBehavior::RequireBalanced { trade_site } = self.trading_behavior {
Some(trade_site)
} else {
None
}
}
2021-04-07 20:11:29 +00:00
}
2020-07-29 19:58:14 +00:00
#[derive(Clone, Debug, Default)]
pub struct Psyche {
/// The proportion of health below which entities will start fleeing.
/// 0.0 = never flees, 1.0 = always flees, 0.5 = flee at 50% health.
pub flee_health: f32,
/// The distance below which the agent will see enemies if it has line of
/// sight.
pub sight_dist: f32,
/// The distance below which the agent can hear enemies without seeing them.
pub listen_dist: f32,
/// The distance below which the agent will attack enemies. Should be lower
/// 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,
2023-07-09 20:03:09 +00:00
/// Aggro range is multiplied by this factor. `1.0` is normal.
///
/// This includes scaling the effective `sight_dist` and `listen_dist`
/// when finding new targets to attack, adjusting the strength of
/// wandering behavior in the idle state, and scaling `aggro_dist` in
/// certain situations.
pub aggro_range_multiplier: f32,
2020-07-29 19:58:14 +00:00
}
2020-07-29 19:58:14 +00:00
impl<'a> From<&'a Body> for Psyche {
fn from(body: &'a Body) -> Self {
Self {
flee_health: match body {
Body::Humanoid(humanoid) => match humanoid.species {
2021-10-22 22:11:09 +00:00
humanoid::Species::Danari => 0.4,
humanoid::Species::Dwarf => 0.3,
humanoid::Species::Elf => 0.4,
humanoid::Species::Human => 0.4,
2021-10-22 22:11:09 +00:00
humanoid::Species::Orc => 0.3,
2022-07-16 11:13:24 +00:00
humanoid::Species::Draugr => 0.3,
},
Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
2020-08-14 01:22:25 +00:00
quadruped_small::Species::Pig => 0.5,
quadruped_small::Species::Fox => 0.7,
2021-10-22 22:11:09 +00:00
quadruped_small::Species::Sheep => 0.6,
quadruped_small::Species::Boar => 0.1,
quadruped_small::Species::Skunk => 0.4,
2021-10-22 22:11:09 +00:00
quadruped_small::Species::Cat => 0.9,
quadruped_small::Species::Batfox => 0.1,
quadruped_small::Species::Raccoon => 0.6,
2021-10-22 22:11:09 +00:00
quadruped_small::Species::Hyena => 0.2,
quadruped_small::Species::Dog => 0.8,
quadruped_small::Species::Rabbit => 0.7,
quadruped_small::Species::Truffler => 0.2,
2021-10-22 22:11:09 +00:00
quadruped_small::Species::Hare => 0.3,
2021-03-12 00:20:05 +00:00
quadruped_small::Species::Goat => 0.5,
2021-10-22 22:11:09 +00:00
quadruped_small::Species::Porcupine => 0.7,
quadruped_small::Species::Turtle => 0.7,
2023-08-15 09:53:28 +00:00
quadruped_small::Species::Beaver => 0.7,
2022-07-15 16:59:37 +00:00
// FIXME: This is to balance for enemy rats in dungeons
2021-10-22 22:11:09 +00:00
// Normal rats should probably always flee.
2023-08-15 09:53:28 +00:00
quadruped_small::Species::Rat
| quadruped_small::Species::TreantSapling
| quadruped_small::Species::Holladon
| quadruped_small::Species::Jackalope => 0.0,
_ => 1.0,
},
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Frostfang => 0.1,
quadruped_medium::Species::Catoblepas => 0.2,
quadruped_medium::Species::Darkhound => 0.1,
quadruped_medium::Species::Dreadhorn => 0.2,
2021-10-22 22:11:09 +00:00
quadruped_medium::Species::Bonerattler => 0.0,
quadruped_medium::Species::Tiger => 0.1,
2022-11-11 09:22:23 +00:00
quadruped_medium::Species::Roshwalr => 0.0,
2024-01-20 17:45:23 +00:00
quadruped_medium::Species::ClaySteed => 0.0,
2021-10-22 22:11:09 +00:00
_ => 0.3,
},
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
2022-09-13 17:32:53 +00:00
quadruped_low::Species::Salamander | quadruped_low::Species::Elbst => 0.2,
quadruped_low::Species::Monitor => 0.3,
quadruped_low::Species::Pangolin => 0.6,
2021-10-22 22:11:09 +00:00
quadruped_low::Species::Tortoise => 0.2,
quadruped_low::Species::Rocksnapper => 0.05,
2022-09-06 10:35:10 +00:00
quadruped_low::Species::Rootsnapper => 0.05,
quadruped_low::Species::Reefsnapper => 0.05,
2021-10-22 22:11:09 +00:00
quadruped_low::Species::Asp => 0.05,
2023-07-09 20:03:09 +00:00
quadruped_low::Species::HermitAlligator => 0.0,
2021-10-22 22:11:09 +00:00
_ => 0.0,
},
Body::BipedSmall(biped_small) => match biped_small.species {
biped_small::Species::Gnarling => 0.2,
biped_small::Species::Adlet => 0.2,
biped_small::Species::Haniwa => 0.1,
biped_small::Species::Sahagin => 0.1,
biped_small::Species::Myrmidon => 0.0,
2023-07-09 20:03:09 +00:00
biped_small::Species::Husk
| biped_small::Species::Boreal
| biped_small::Species::Clockwork
| biped_small::Species::Flamekeeper => 0.0,
2021-10-22 22:11:09 +00:00
_ => 0.5,
},
Body::BirdMedium(bird_medium) => match bird_medium.species {
2023-02-14 23:17:20 +00:00
bird_medium::Species::SnowyOwl => 0.4,
bird_medium::Species::HornedOwl => 0.4,
bird_medium::Species::Duck => 0.6,
bird_medium::Species::Cockatiel => 0.6,
bird_medium::Species::Chicken => 0.5,
bird_medium::Species::Bat => 0.1,
bird_medium::Species::Penguin => 0.5,
2021-10-22 22:11:09 +00:00
bird_medium::Species::Goose => 0.4,
2023-02-14 23:17:20 +00:00
bird_medium::Species::Peacock => 0.3,
bird_medium::Species::Eagle => 0.2,
2021-10-22 22:11:09 +00:00
bird_medium::Species::Parrot => 0.8,
2023-02-14 23:17:20 +00:00
bird_medium::Species::Crow => 0.4,
bird_medium::Species::Dodo => 0.8,
bird_medium::Species::Parakeet => 0.8,
bird_medium::Species::Puffin => 0.8,
bird_medium::Species::Toucan => 0.4,
},
2023-08-02 07:55:10 +00:00
Body::BirdLarge(_) => 0.0,
Body::FishSmall(_) => 1.0,
2021-10-22 22:11:09 +00:00
Body::FishMedium(_) => 0.75,
Body::BipedLarge(_) => 0.0,
Body::Object(_) => 0.0,
Body::ItemDrop(_) => 0.0,
Body::Golem(_) => 0.0,
Body::Theropod(_) => 0.0,
Body::Ship(_) => 0.0,
2021-10-22 22:11:09 +00:00
Body::Dragon(_) => 0.0,
Body::Arthropod(arthropod) => match arthropod.species {
arthropod::Species::Tarantula => 0.0,
arthropod::Species::Blackwidow => 0.0,
arthropod::Species::Antlion => 0.0,
arthropod::Species::Hornbeetle => 0.1,
arthropod::Species::Leafbeetle => 0.1,
arthropod::Species::Stagbeetle => 0.1,
arthropod::Species::Weevil => 0.0,
arthropod::Species::Cavespider => 0.0,
arthropod::Species::Moltencrawler => 0.2,
arthropod::Species::Mosscrawler => 0.2,
arthropod::Species::Sandcrawler => 0.2,
2023-05-24 18:13:29 +00:00
arthropod::Species::Dagonite => 0.2,
2023-06-03 13:59:52 +00:00
arthropod::Species::Emberfly => 0.1,
},
2023-12-16 13:47:55 +00:00
Body::Crustacean(_) => 0.0,
},
2023-08-02 07:55:10 +00:00
sight_dist: match body {
Body::BirdLarge(_) => 250.0,
2023-10-08 11:35:01 +00:00
Body::BipedLarge(biped_large) => match biped_large.species {
biped_large::Species::Gigasfrost => 200.0,
_ => 100.0,
},
2023-08-02 07:55:10 +00:00
_ => 40.0,
},
listen_dist: 30.0,
aggro_dist: match body {
Body::Humanoid(_) => Some(20.0),
_ => None, // Always aggressive if detected
2020-07-29 19:58:14 +00:00
},
idle_wander_factor: 1.0,
2023-07-09 20:03:09 +00:00
aggro_range_multiplier: 1.0,
2020-07-29 19:58:14 +00:00
}
}
}
impl Psyche {
2023-07-09 20:03:09 +00:00
/// The maximum distance that targets to attack might be detected by this
/// agent.
pub fn search_dist(&self) -> f32 {
self.sight_dist.max(self.listen_dist) * self.aggro_range_multiplier
}
}
#[derive(Clone, Debug)]
/// Events that affect agent behavior from other entities/players/environment
pub enum AgentEvent {
/// Engage in conversation with entity with Uid
2021-03-29 14:47:42 +00:00
Talk(Uid, Subject),
Implement /price_list (work in progress), stub for /buy and /sell remove outdated economic simulation code remove old values, document add natural resources to economy Remove NaturalResources from Place (now in Economy) find closest site to each chunk implement natural resources (the distance scale is wrong) cargo fmt working distance calculation this collection of natural resources seem to make sense, too much Wheat though use natural resources and controlled area to replenish goods increase the amount of chunks controlled by one guard to 50 add new professions and goods to the list implement multiple products per worker remove the old code and rename the new code to the previous name correctly determine which goods guards will give you access to correctly estimate the amount of natural resources controlled adapt to new server API instrument tooltips Now I just need to figure out how to store a (reference to) a closure closures for tooltip content generation pass site/cave id to the client Add economic information to the client structure (not yet exchanged with the server) Send SiteId to the client, prepare messages for economy request Make client::sites a HashMap Specialize the Crafter into Brewer,Bladesmith and Blacksmith working server request for economic info from within tooltip fully operational economic tooltips I need to fix the id clash between caves and towns though fix overlapping ids between caves and sites display stock amount correctly handle invalid (cave) ids in the request some initial balancing, turn off most info logging less intrusive way of implementing the dynamic tool tips in map further tooltip cleanup further cleanup, dynamic tooltip not fully working as intended correctly working tooltip visibility logic cleanup, display labor value separate economy info request in a separate translation unit display values as well nicer display format for economy add last_exports and coin to the new economy do not allocate natural resources to Dungeons (making town so much larger) balancing attempt print town size statistics cargo fmt (dead code) resource tweaks, csv debugging output a more interesting town (and then all sites) fix the labor value logic (now we have meaningful prices) load professions from ron (WIP) use assets manager in economy loading professions works use professions from ron file fix Labor debug logic count chunks per type separately (preparing for better resource control) better structured resource data traders, more professions (WIP) fix exception when starting the simulation fix replenish function TODO: - use area_ratio for resource usage (chunks should be added to stock, ratio on usage?) - fix trading documentation clean up fix merge artifact Revise trader mechanic start Coin with a reasonable default remove the outdated economy code preserve documentation from removed old structure output neighboring sites (preparation) pass list of neighbors to economy add trade structures trading stub Description of purpose by zesterer on Discord remember prices (needed for planning) avoid growing the order vector unboundedly into_iter doesn't clear the Vec, so clear it manually use drain to process Vecs, avoid clone fix the test server implement a test stub (I need to get it faster than 30 seconds to be more useful) enable info output in test debug missing and extra goods use the same logging extension as water, activate feature update dependencies determine good prices, good exchange goods a working set of revisions a cozy world which economy tests in 2s first order planning version fun with package version buy according to value/priority, not missing amount introduce min price constant, fix order priority in depth debugging with a correct sign the trading plans start to make sense move the trade planning to a separate function rename new function reorganize code into subroutines (much cleaner) each trading step now has its own function cut down the number of debugging output introduce RoadSecurity and Transportation transport capacity bookkeeping only plan to pay with valuable goods, you can no longer stockpile unused options (which interestingly shows a huge impact, to be investigated) Coin is now listed as a payment (although not used) proper transportation estimation (although 0) remove more left overs uncovered by viewing the code in a merge request use the old default values, handle non-pileable stocks directly before increasing it (as economy is based on last year's products) don't order the missing good multiple times also it uses coin to buy things! fix warnings and use the transportation from stock again cargo fmt prepare evaluation of trade don't count transportation multiple times fix merge artifact operational trade planning trade itself is still misleading make clippy happy clean up correct labor ratio of merchants (no need to multiply with amount produced) incomplete merchant labor_value computation correct last commit make economy of scale more explicit make clippy happy (and code cleaner) more merchant tweaks (more pop=better) beginning of real trading code revert the update of dependencies remove stale comments/unused code trading implementation complete (but untested) something is still strange ... fix sign in trading another sign fix some bugfixes and plenty of debugging code another bug fixed, more to go fix another invariant (rounding will lead to very small negative value) introduce Terrain and Territory fix merge mistakes
2021-03-14 03:18:32 +00:00
TradeInvite(Uid),
2021-03-29 14:47:42 +00:00
TradeAccepted(Uid),
Implement /price_list (work in progress), stub for /buy and /sell remove outdated economic simulation code remove old values, document add natural resources to economy Remove NaturalResources from Place (now in Economy) find closest site to each chunk implement natural resources (the distance scale is wrong) cargo fmt working distance calculation this collection of natural resources seem to make sense, too much Wheat though use natural resources and controlled area to replenish goods increase the amount of chunks controlled by one guard to 50 add new professions and goods to the list implement multiple products per worker remove the old code and rename the new code to the previous name correctly determine which goods guards will give you access to correctly estimate the amount of natural resources controlled adapt to new server API instrument tooltips Now I just need to figure out how to store a (reference to) a closure closures for tooltip content generation pass site/cave id to the client Add economic information to the client structure (not yet exchanged with the server) Send SiteId to the client, prepare messages for economy request Make client::sites a HashMap Specialize the Crafter into Brewer,Bladesmith and Blacksmith working server request for economic info from within tooltip fully operational economic tooltips I need to fix the id clash between caves and towns though fix overlapping ids between caves and sites display stock amount correctly handle invalid (cave) ids in the request some initial balancing, turn off most info logging less intrusive way of implementing the dynamic tool tips in map further tooltip cleanup further cleanup, dynamic tooltip not fully working as intended correctly working tooltip visibility logic cleanup, display labor value separate economy info request in a separate translation unit display values as well nicer display format for economy add last_exports and coin to the new economy do not allocate natural resources to Dungeons (making town so much larger) balancing attempt print town size statistics cargo fmt (dead code) resource tweaks, csv debugging output a more interesting town (and then all sites) fix the labor value logic (now we have meaningful prices) load professions from ron (WIP) use assets manager in economy loading professions works use professions from ron file fix Labor debug logic count chunks per type separately (preparing for better resource control) better structured resource data traders, more professions (WIP) fix exception when starting the simulation fix replenish function TODO: - use area_ratio for resource usage (chunks should be added to stock, ratio on usage?) - fix trading documentation clean up fix merge artifact Revise trader mechanic start Coin with a reasonable default remove the outdated economy code preserve documentation from removed old structure output neighboring sites (preparation) pass list of neighbors to economy add trade structures trading stub Description of purpose by zesterer on Discord remember prices (needed for planning) avoid growing the order vector unboundedly into_iter doesn't clear the Vec, so clear it manually use drain to process Vecs, avoid clone fix the test server implement a test stub (I need to get it faster than 30 seconds to be more useful) enable info output in test debug missing and extra goods use the same logging extension as water, activate feature update dependencies determine good prices, good exchange goods a working set of revisions a cozy world which economy tests in 2s first order planning version fun with package version buy according to value/priority, not missing amount introduce min price constant, fix order priority in depth debugging with a correct sign the trading plans start to make sense move the trade planning to a separate function rename new function reorganize code into subroutines (much cleaner) each trading step now has its own function cut down the number of debugging output introduce RoadSecurity and Transportation transport capacity bookkeeping only plan to pay with valuable goods, you can no longer stockpile unused options (which interestingly shows a huge impact, to be investigated) Coin is now listed as a payment (although not used) proper transportation estimation (although 0) remove more left overs uncovered by viewing the code in a merge request use the old default values, handle non-pileable stocks directly before increasing it (as economy is based on last year's products) don't order the missing good multiple times also it uses coin to buy things! fix warnings and use the transportation from stock again cargo fmt prepare evaluation of trade don't count transportation multiple times fix merge artifact operational trade planning trade itself is still misleading make clippy happy clean up correct labor ratio of merchants (no need to multiply with amount produced) incomplete merchant labor_value computation correct last commit make economy of scale more explicit make clippy happy (and code cleaner) more merchant tweaks (more pop=better) beginning of real trading code revert the update of dependencies remove stale comments/unused code trading implementation complete (but untested) something is still strange ... fix sign in trading another sign fix some bugfixes and plenty of debugging code another bug fixed, more to go fix another invariant (rounding will lead to very small negative value) introduce Terrain and Territory fix merge mistakes
2021-03-14 03:18:32 +00:00
FinishedTrade(TradeResult),
UpdatePendingTrade(
// This data structure is large so box it to keep AgentEvent small
Implement /price_list (work in progress), stub for /buy and /sell remove outdated economic simulation code remove old values, document add natural resources to economy Remove NaturalResources from Place (now in Economy) find closest site to each chunk implement natural resources (the distance scale is wrong) cargo fmt working distance calculation this collection of natural resources seem to make sense, too much Wheat though use natural resources and controlled area to replenish goods increase the amount of chunks controlled by one guard to 50 add new professions and goods to the list implement multiple products per worker remove the old code and rename the new code to the previous name correctly determine which goods guards will give you access to correctly estimate the amount of natural resources controlled adapt to new server API instrument tooltips Now I just need to figure out how to store a (reference to) a closure closures for tooltip content generation pass site/cave id to the client Add economic information to the client structure (not yet exchanged with the server) Send SiteId to the client, prepare messages for economy request Make client::sites a HashMap Specialize the Crafter into Brewer,Bladesmith and Blacksmith working server request for economic info from within tooltip fully operational economic tooltips I need to fix the id clash between caves and towns though fix overlapping ids between caves and sites display stock amount correctly handle invalid (cave) ids in the request some initial balancing, turn off most info logging less intrusive way of implementing the dynamic tool tips in map further tooltip cleanup further cleanup, dynamic tooltip not fully working as intended correctly working tooltip visibility logic cleanup, display labor value separate economy info request in a separate translation unit display values as well nicer display format for economy add last_exports and coin to the new economy do not allocate natural resources to Dungeons (making town so much larger) balancing attempt print town size statistics cargo fmt (dead code) resource tweaks, csv debugging output a more interesting town (and then all sites) fix the labor value logic (now we have meaningful prices) load professions from ron (WIP) use assets manager in economy loading professions works use professions from ron file fix Labor debug logic count chunks per type separately (preparing for better resource control) better structured resource data traders, more professions (WIP) fix exception when starting the simulation fix replenish function TODO: - use area_ratio for resource usage (chunks should be added to stock, ratio on usage?) - fix trading documentation clean up fix merge artifact Revise trader mechanic start Coin with a reasonable default remove the outdated economy code preserve documentation from removed old structure output neighboring sites (preparation) pass list of neighbors to economy add trade structures trading stub Description of purpose by zesterer on Discord remember prices (needed for planning) avoid growing the order vector unboundedly into_iter doesn't clear the Vec, so clear it manually use drain to process Vecs, avoid clone fix the test server implement a test stub (I need to get it faster than 30 seconds to be more useful) enable info output in test debug missing and extra goods use the same logging extension as water, activate feature update dependencies determine good prices, good exchange goods a working set of revisions a cozy world which economy tests in 2s first order planning version fun with package version buy according to value/priority, not missing amount introduce min price constant, fix order priority in depth debugging with a correct sign the trading plans start to make sense move the trade planning to a separate function rename new function reorganize code into subroutines (much cleaner) each trading step now has its own function cut down the number of debugging output introduce RoadSecurity and Transportation transport capacity bookkeeping only plan to pay with valuable goods, you can no longer stockpile unused options (which interestingly shows a huge impact, to be investigated) Coin is now listed as a payment (although not used) proper transportation estimation (although 0) remove more left overs uncovered by viewing the code in a merge request use the old default values, handle non-pileable stocks directly before increasing it (as economy is based on last year's products) don't order the missing good multiple times also it uses coin to buy things! fix warnings and use the transportation from stock again cargo fmt prepare evaluation of trade don't count transportation multiple times fix merge artifact operational trade planning trade itself is still misleading make clippy happy clean up correct labor ratio of merchants (no need to multiply with amount produced) incomplete merchant labor_value computation correct last commit make economy of scale more explicit make clippy happy (and code cleaner) more merchant tweaks (more pop=better) beginning of real trading code revert the update of dependencies remove stale comments/unused code trading implementation complete (but untested) something is still strange ... fix sign in trading another sign fix some bugfixes and plenty of debugging code another bug fixed, more to go fix another invariant (rounding will lead to very small negative value) introduce Terrain and Territory fix merge mistakes
2021-03-14 03:18:32 +00:00
Box<(
TradeId,
PendingTrade,
SitePrices,
[Option<ReducedInventory>; 2],
)>,
),
ServerSound(Sound),
2021-06-17 05:49:09 +00:00
Hurt,
}
#[derive(Copy, Clone, Debug)]
pub struct Sound {
pub kind: SoundKind,
pub pos: Vec3<f32>,
pub vol: f32,
pub time: f64,
}
impl Sound {
pub fn new(kind: SoundKind, pos: Vec3<f32>, vol: f32, time: f64) -> Self {
Sound {
kind,
pos,
vol,
time,
}
}
#[must_use]
pub fn with_new_vol(mut self, new_vol: f32) -> Self {
self.vol = new_vol;
self
}
}
#[derive(Copy, Clone, Debug)]
pub enum SoundKind {
Unknown,
Utterance(UtteranceKind, Body),
Movement,
Melee,
Projectile,
Explosion,
Beam,
Shockwave,
2024-01-20 17:45:23 +00:00
Trap,
}
#[derive(Clone, Copy, Debug)]
2021-02-10 20:57:47 +00:00
pub struct Target {
pub target: EcsEntity,
/// Whether the target is hostile
2021-02-10 20:57:47 +00:00
pub hostile: bool,
/// The time at which the target was selected
pub selected_at: f64,
/// Whether the target has come close enough to trigger aggro.
pub aggro_on: bool,
pub last_known_pos: Option<Vec3<f32>>,
2021-02-10 20:57:47 +00:00
}
2022-01-18 03:02:43 +00:00
impl Target {
pub fn new(
target: EcsEntity,
hostile: bool,
selected_at: f64,
aggro_on: bool,
last_known_pos: Option<Vec3<f32>>,
) -> Self {
2022-01-18 03:02:43 +00:00
Self {
target,
hostile,
selected_at,
aggro_on,
last_known_pos,
2022-01-18 03:02:43 +00:00
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, EnumIter)]
pub enum TimerAction {
Interact,
}
/// A time used for managing agent-related timeouts. The timer is designed to
/// keep track of the start of any number of previous actions. However,
/// starting/progressing an action will end previous actions. Therefore, the
/// timer should be used for actions that are mutually-exclusive.
#[derive(Clone, Debug)]
pub struct Timer {
action_starts: Vec<Option<f64>>,
last_action: Option<TimerAction>,
}
impl Default for Timer {
fn default() -> Self {
Self {
action_starts: TimerAction::iter().map(|_| None).collect(),
last_action: None,
}
}
}
impl Timer {
fn idx_for(action: TimerAction) -> usize {
TimerAction::iter()
.enumerate()
.find(|(_, a)| a == &action)
.unwrap()
.0 // Can't fail, EnumIter is exhaustive
}
/// Reset the timer for the given action, returning true if the timer was
/// not already reset.
pub fn reset(&mut self, action: TimerAction) -> bool {
self.action_starts[Self::idx_for(action)].take().is_some()
}
/// Start the timer for the given action, even if it was already started.
pub fn start(&mut self, time: f64, action: TimerAction) {
self.action_starts[Self::idx_for(action)] = Some(time);
self.last_action = Some(action);
}
/// Continue timing the given action, starting it if it was not already
/// started.
pub fn progress(&mut self, time: f64, action: TimerAction) {
if self.last_action != Some(action) {
self.start(time, action);
}
}
/// Return the time that the given action was last performed at.
pub fn time_of_last(&self, action: TimerAction) -> Option<f64> {
self.action_starts[Self::idx_for(action)]
}
/// Return `true` if the time since the action was last started exceeds the
/// given timeout.
pub fn time_since_exceeds(&self, time: f64, action: TimerAction, timeout: f64) -> bool {
self.time_of_last(action)
.map_or(true, |last_time| (time - last_time).max(0.0) > timeout)
}
/// Return `true` while the time since the action was last started is less
/// than the given period. Once the time has elapsed, reset the timer.
pub fn timeout_elapsed(
&mut self,
time: f64,
action: TimerAction,
timeout: f64,
) -> Option<bool> {
if self.time_since_exceeds(time, action, timeout) {
Some(self.reset(action))
} else {
self.progress(time, action);
None
}
}
}
2022-02-07 23:56:03 +00:00
/// For use with the builder pattern <https://doc.rust-lang.org/1.0.0/style/ownership/builders.html>
2021-08-02 12:08:39 +00:00
#[derive(Clone, Debug)]
pub struct Agent {
pub rtsim_controller: RtSimController,
pub patrol_origin: Option<Vec3<f32>>,
2021-02-10 20:57:47 +00:00
pub target: Option<Target>,
2021-02-23 02:16:07 +00:00
pub chaser: Chaser,
pub behavior: Behavior,
2020-07-29 19:58:14 +00:00
pub psyche: Psyche,
pub inbox: VecDeque<AgentEvent>,
pub combat_state: ActionState,
pub behavior_state: ActionState,
pub timer: Timer,
2021-02-10 20:57:47 +00:00
pub bearing: Vec2<f32>,
pub sounds_heard: Vec<Sound>,
2021-05-30 15:38:47 +00:00
pub position_pid_controller: Option<PidController<fn(Vec3<f32>, Vec3<f32>) -> f32, 16>>,
/// Position from which to flee. Intended to be the agent's position plus a
/// random position offset, to be used when a random flee direction is
/// required and reset each time the flee timer is reset.
pub flee_from_pos: Option<Pos>,
2022-08-14 21:08:14 +00:00
pub awareness: Awareness,
2023-05-21 09:42:44 +00:00
pub stay_pos: Option<Pos>,
2023-05-04 10:23:46 +00:00
/// Inputs sent up to rtsim
pub rtsim_outbox: Option<VecDeque<NpcInput>>,
2022-08-14 21:08:14 +00:00
}
#[derive(Clone, Debug)]
/// Always clamped between `0.0` and `1.0`.
pub struct Awareness {
level: f32,
reached: bool,
2022-08-14 21:08:14 +00:00
}
impl fmt::Display for Awareness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}", self.level) }
}
impl Awareness {
const ALERT: f32 = 1.0;
2022-08-14 21:08:14 +00:00
const HIGH: f32 = 0.6;
const LOW: f32 = 0.1;
const MEDIUM: f32 = 0.3;
const UNAWARE: f32 = 0.0;
pub fn new(level: f32) -> Self {
Self {
level: level.clamp(Self::UNAWARE, Self::ALERT),
reached: false,
2022-08-14 21:08:14 +00:00
}
}
/// The level of awareness as a decimal.
2022-08-14 21:08:14 +00:00
pub fn level(&self) -> f32 { self.level }
/// The level of awareness in English. To see if awareness has been fully
/// reached, use `self.reached()`.
2022-08-14 21:08:14 +00:00
pub fn state(&self) -> AwarenessState {
if self.level == Self::ALERT {
AwarenessState::Alert
} else if self.level.is_between(Self::HIGH, Self::ALERT) {
2022-08-14 21:08:14 +00:00
AwarenessState::High
} else if self.level.is_between(Self::MEDIUM, Self::HIGH) {
AwarenessState::Medium
} else if self.level.is_between(Self::LOW, Self::MEDIUM) {
AwarenessState::Low
} else {
AwarenessState::Unaware
}
}
/// Awareness was reached at some point and has not been reset.
pub fn reached(&self) -> bool { self.reached }
2022-11-06 19:10:17 +00:00
pub fn change_by(&mut self, amount: f32) {
2022-11-07 22:20:24 +00:00
self.level = (self.level + amount).clamp(Self::UNAWARE, Self::ALERT);
if self.state() == AwarenessState::Alert {
self.reached = true;
} else if self.state() == AwarenessState::Unaware {
self.reached = false;
}
2022-08-14 21:08:14 +00:00
}
pub fn set_maximally_aware(&mut self) {
self.reached = true;
self.level = Self::ALERT;
}
2022-08-14 21:08:14 +00:00
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq)]
2022-08-14 21:08:14 +00:00
pub enum AwarenessState {
Unaware = 0,
Low = 1,
Medium = 2,
High = 3,
Alert = 4,
}
/// State persistence object for the behavior tree
2022-10-25 02:52:37 +00:00
/// Allows for state to be stored between subsequent, sequential calls of a
/// single action node. If the executed action node changes between ticks, then
/// the state should be considered lost.
#[derive(Clone, Debug, Default)]
pub struct ActionState {
pub timers: [f32; ACTIONSTATE_NUMBER_OF_CONCURRENT_TIMERS],
pub counters: [f32; ACTIONSTATE_NUMBER_OF_CONCURRENT_COUNTERS],
pub conditions: [bool; ACTIONSTATE_NUMBER_OF_CONCURRENT_CONDITIONS],
pub int_counters: [u8; ACTIONSTATE_NUMBER_OF_CONCURRENT_INT_COUNTERS],
2023-01-21 19:02:27 +00:00
pub positions: [Option<Vec3<f32>>; ACTIONSTATE_NUMBER_OF_CONCURRENT_POSITIONS],
2022-02-02 05:17:06 +00:00
pub initialized: bool,
}
impl Agent {
2022-02-07 23:56:03 +00:00
/// Instantiates agent from body using the body's psyche
2021-08-02 12:08:39 +00:00
pub fn from_body(body: &Body) -> Self {
Agent {
rtsim_controller: RtSimController::default(),
patrol_origin: None,
target: None,
chaser: Chaser::default(),
behavior: Behavior::default(),
psyche: Psyche::from(body),
inbox: VecDeque::new(),
combat_state: ActionState::default(),
behavior_state: ActionState::default(),
timer: Timer::default(),
2021-08-02 12:08:39 +00:00
bearing: Vec2::zero(),
sounds_heard: Vec::new(),
position_pid_controller: None,
flee_from_pos: None,
2023-05-21 09:42:44 +00:00
stay_pos: None,
2022-08-14 21:08:14 +00:00
awareness: Awareness::new(0.0),
2023-05-04 10:23:46 +00:00
rtsim_outbox: None,
2021-08-02 12:08:39 +00:00
}
}
#[must_use]
2021-04-08 16:33:00 +00:00
pub fn with_patrol_origin(mut self, origin: Vec3<f32>) -> Self {
self.patrol_origin = Some(origin);
self
}
#[must_use]
2021-08-02 12:08:39 +00:00
pub fn with_behavior(mut self, behavior: Behavior) -> Self {
self.behavior = behavior;
self
}
#[must_use]
2021-10-22 22:11:09 +00:00
pub fn with_no_flee_if(mut self, condition: bool) -> Self {
if condition {
self.psyche.flee_health = 0.0;
2021-08-02 12:08:39 +00:00
}
self
}
2022-02-07 23:56:03 +00:00
pub fn set_no_flee(&mut self) { self.psyche.flee_health = 0.0; }
2021-10-22 22:11:09 +00:00
// FIXME: Only one of *three* things in this method sets a location.
#[must_use]
2021-04-08 16:33:00 +00:00
pub fn with_destination(mut self, pos: Vec3<f32>) -> Self {
self.psyche.flee_health = 0.0;
2021-04-08 16:33:00 +00:00
self.rtsim_controller = RtSimController::with_destination(pos);
self.behavior.allow(BehaviorCapability::SPEAK);
self
2021-03-12 02:58:57 +00:00
}
#[must_use]
pub fn with_idle_wander_factor(mut self, idle_wander_factor: f32) -> Self {
self.psyche.idle_wander_factor = idle_wander_factor;
self
}
2023-07-09 20:03:09 +00:00
pub fn with_aggro_range_multiplier(mut self, aggro_range_multiplier: f32) -> Self {
self.psyche.aggro_range_multiplier = aggro_range_multiplier;
self
}
#[must_use]
2021-05-30 15:38:47 +00:00
pub fn with_position_pid_controller(
mut self,
pid: PidController<fn(Vec3<f32>, Vec3<f32>) -> f32, 16>,
) -> Self {
2021-05-30 15:38:47 +00:00
self.position_pid_controller = Some(pid);
self
}
2021-09-04 16:25:04 +00:00
2022-07-15 16:59:37 +00:00
/// Makes agent aggressive without warning
#[must_use]
2021-09-04 16:25:04 +00:00
pub fn with_aggro_no_warn(mut self) -> Self {
self.psyche.aggro_dist = None;
self
}
2022-01-18 03:02:43 +00:00
pub fn forget_old_sounds(&mut self, time: f64) {
if !self.sounds_heard.is_empty() {
// Keep (retain) only newer sounds
self.sounds_heard
.retain(|&sound| time - sound.time <= SECONDS_BEFORE_FORGET_SOUNDS);
}
}
pub fn allowed_to_speak(&self) -> bool { self.behavior.can(BehaviorCapability::SPEAK) }
}
impl Component for Agent {
type Storage = specs::DenseVecStorage<Self>;
}
2021-04-07 20:11:29 +00:00
#[cfg(test)]
mod tests {
2022-02-07 23:56:03 +00:00
use super::{humanoid, Agent, Behavior, BehaviorCapability, BehaviorState, Body};
2021-04-07 20:11:29 +00:00
/// Test to verify that Behavior is working correctly at its most basic
/// usages
#[test]
pub fn behavior_basic() {
let mut b = Behavior::default();
// test capabilities
assert!(!b.can(BehaviorCapability::SPEAK));
b.allow(BehaviorCapability::SPEAK);
assert!(b.can(BehaviorCapability::SPEAK));
b.deny(BehaviorCapability::SPEAK);
assert!(!b.can(BehaviorCapability::SPEAK));
// test states
assert!(!b.is(BehaviorState::TRADING));
b.set(BehaviorState::TRADING);
assert!(b.is(BehaviorState::TRADING));
b.unset(BehaviorState::TRADING);
assert!(!b.is(BehaviorState::TRADING));
// test `from`
let b = Behavior::from(BehaviorCapability::SPEAK);
2021-04-07 20:11:29 +00:00
assert!(b.can(BehaviorCapability::SPEAK));
}
2022-02-07 23:56:03 +00:00
/// Makes agent flee
#[test]
pub fn enable_flee() {
let body = Body::Humanoid(humanoid::Body::random());
let mut agent = Agent::from_body(&body);
agent.psyche.flee_health = 1.0;
agent = agent.with_no_flee_if(false);
assert_eq!(agent.psyche.flee_health, 1.0);
}
/// Makes agent not flee
#[test]
pub fn set_no_flee() {
let body = Body::Humanoid(humanoid::Body::random());
let mut agent = Agent::from_body(&body);
agent.psyche.flee_health = 1.0;
agent.set_no_flee();
assert_eq!(agent.psyche.flee_health, 0.0);
}
#[test]
pub fn with_aggro_no_warn() {
let body = Body::Humanoid(humanoid::Body::random());
let mut agent = Agent::from_body(&body);
agent.psyche.aggro_dist = Some(1.0);
agent = agent.with_aggro_no_warn();
assert_eq!(agent.psyche.aggro_dist, None);
}
2021-04-07 20:11:29 +00:00
}
/// PID controllers are used for automatically adapting nonlinear controls (like
/// buoyancy for airships) to target specific outcomes (i.e. a specific height)
#[derive(Clone)]
pub struct PidController<F: Fn(Vec3<f32>, Vec3<f32>) -> f32, const NUM_SAMPLES: usize> {
/// The coefficient of the proportional term
pub kp: f32,
/// The coefficient of the integral term
pub ki: f32,
/// The coefficient of the derivative term
pub kd: f32,
/// The setpoint that the process has as its goal
pub sp: Vec3<f32>,
/// A ring buffer of the last NUM_SAMPLES measured process variables
pv_samples: [(f64, Vec3<f32>); NUM_SAMPLES],
/// The index into the ring buffer of process variables
pv_idx: usize,
/// The total integral error
integral_error: f64,
/// The error function, to change how the difference between the setpoint
/// and process variables are calculated
e: F,
}
impl<F: Fn(Vec3<f32>, Vec3<f32>) -> f32, const NUM_SAMPLES: usize> fmt::Debug
for PidController<F, NUM_SAMPLES>
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("PidController")
.field("kp", &self.kp)
.field("ki", &self.ki)
.field("kd", &self.kd)
.field("sp", &self.sp)
.field("pv_samples", &self.pv_samples)
.field("pv_idx", &self.pv_idx)
.finish()
}
}
impl<F: Fn(Vec3<f32>, Vec3<f32>) -> f32, const NUM_SAMPLES: usize> PidController<F, NUM_SAMPLES> {
/// Constructs a PidController with the specified weights, setpoint,
/// starting time, and error function
pub fn new(kp: f32, ki: f32, kd: f32, sp: Vec3<f32>, time: f64, e: F) -> Self {
Self {
kp,
ki,
kd,
sp,
pv_samples: [(time, sp); NUM_SAMPLES],
pv_idx: 0,
integral_error: 0.0,
e,
}
}
/// Adds a measurement of the process variable to the ringbuffer
pub fn add_measurement(&mut self, time: f64, pv: Vec3<f32>) {
self.pv_idx += 1;
self.pv_idx %= NUM_SAMPLES;
self.pv_samples[self.pv_idx] = (time, pv);
self.update_integral_err();
}
/// The amount to set the control variable to is a weighed sum of the
/// proportional error, the integral error, and the derivative error.
/// https://en.wikipedia.org/wiki/PID_controller#Mathematical_form
pub fn calc_err(&self) -> f32 {
self.kp * self.proportional_err()
+ self.ki * self.integral_err()
+ self.kd * self.derivative_err()
}
/// The proportional error is the error function applied to the set point
/// and the most recent process variable measurement
pub fn proportional_err(&self) -> f32 { (self.e)(self.sp, self.pv_samples[self.pv_idx].1) }
/// The integral error is the error function integrated over all previous
/// values, updated per point. The trapezoid rule for numerical integration
/// was chosen because it's fairly easy to calculate and sufficiently
/// accurate. https://en.wikipedia.org/wiki/Trapezoidal_rule#Uniform_grid
pub fn integral_err(&self) -> f32 { self.integral_error as f32 }
fn update_integral_err(&mut self) {
let f = |x| (self.e)(self.sp, x) as f64;
let (a, x0) = self.pv_samples[(self.pv_idx + NUM_SAMPLES - 1) % NUM_SAMPLES];
let (b, x1) = self.pv_samples[self.pv_idx];
let dx = b - a;
// Discard updates with too long between them, likely caused by either
// initialization or latency, since they're likely to be spurious
if dx < 5.0 {
self.integral_error += dx * (f(x1) + f(x0)) / 2.0;
}
}
/// The derivative error is the numerical derivative of the error function
/// based on the most recent 2 samples. Using more than 2 samples might
/// improve the accuracy of the estimate of the derivative, but it would be
/// an estimate of the derivative error further in the past.
/// https://en.wikipedia.org/wiki/Numerical_differentiation#Finite_differences
pub fn derivative_err(&self) -> f32 {
let f = |x| (self.e)(self.sp, x);
let (a, x0) = self.pv_samples[(self.pv_idx + NUM_SAMPLES - 1) % NUM_SAMPLES];
let (b, x1) = self.pv_samples[self.pv_idx];
let h = b - a;
(f(x1) - f(x0)) / h as f32
}
}
2021-05-30 15:38:47 +00:00
/// Get the PID coefficients associated with some Body, since it will likely
/// need to be tuned differently for each body type
2023-01-14 22:49:43 +00:00
pub fn pid_coefficients(body: &Body) -> Option<(f32, f32, f32)> {
// A pure-proportional controller is { kp: 1.0, ki: 0.0, kd: 0.0 }
2021-05-30 15:38:47 +00:00
match body {
Body::Ship(ship::Body::DefaultAirship) => {
let kp = 1.0;
let ki = 0.1;
let kd = 1.2;
2023-01-14 22:49:43 +00:00
Some((kp, ki, kd))
},
Body::Ship(ship::Body::AirBalloon) => {
let kp = 1.0;
let ki = 0.1;
let kd = 0.8;
2023-01-14 22:49:43 +00:00
Some((kp, ki, kd))
2021-05-30 15:38:47 +00:00
},
2023-01-14 22:49:43 +00:00
_ => None,
2021-05-30 15:38:47 +00:00
}
}