mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added menacing, made agent code less hacky
This commit is contained in:
parent
7acca36629
commit
422e1c30f4
@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Pets are now saved on logout 🐕 🦎 🐼
|
- Pets are now saved on logout 🐕 🦎 🐼
|
||||||
- Dualwielded, one-handed swords as starting weapons (Will be replaced by daggers in the future!)
|
- Dualwielded, one-handed swords as starting weapons (Will be replaced by daggers in the future!)
|
||||||
- Healing sceptre crafting recipe
|
- Healing sceptre crafting recipe
|
||||||
|
- NPCs can now warn players before engaging in combat
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
"You can type /region or /r to only chat with players a couple of hundred blocks around you.",
|
"You can type /region or /r to only chat with players a couple of hundred blocks around you.",
|
||||||
"Admins can use the /build command to enter build mode.",
|
"Admins can use the /build command to enter build mode.",
|
||||||
"You can type /group or /g to only chat with players in your current group.",
|
"You can type /group or /g to only chat with players in your current group.",
|
||||||
"To send private messages type /tell followed by a player name and your message.",
|
"To send private messages type /tell followed by a player name and your message.",
|
||||||
"Keep an eye out for food, chests and other loot spread all around the world!",
|
"Keep an eye out for food, chests and other loot spread all around the world!",
|
||||||
"Inventory filled with food? Try crafting better food from it!",
|
"Inventory filled with food? Try crafting better food from it!",
|
||||||
"Wondering what there is to do? Try out one of the dungeons marked on the map!",
|
"Wondering what there is to do? Try out one of the dungeons marked on the map!",
|
||||||
@ -52,7 +52,7 @@
|
|||||||
"Press 'L-Shift' to open your Glider and conquer the skies.",
|
"Press 'L-Shift' to open your Glider and conquer the skies.",
|
||||||
"Veloren is still in Pre-Alpha. We do our best to improve it every day!",
|
"Veloren is still in Pre-Alpha. We do our best to improve it every day!",
|
||||||
"If you want to join the dev team or just have a chat with us, join our Discord server.",
|
"If you want to join the dev team or just have a chat with us, join our Discord server.",
|
||||||
"You can toggle showing your amount of health on the healthbar in the settings.",
|
"You can toggle showing your amount of health on the healthbar in the settings.",
|
||||||
"Sit near a campfire (with the 'K' key) to slowly recover from your injuries.",
|
"Sit near a campfire (with the 'K' key) to slowly recover from your injuries.",
|
||||||
"Need more bags or better armor to continue your journey? Press 'C' to open the crafting menu!",
|
"Need more bags or better armor to continue your journey? Press 'C' to open the crafting menu!",
|
||||||
"Try jumping when rolling through creatures.",
|
"Try jumping when rolling through creatures.",
|
||||||
@ -228,6 +228,14 @@
|
|||||||
"I have destroyed my enemy!",
|
"I have destroyed my enemy!",
|
||||||
"Finally at peace!",
|
"Finally at peace!",
|
||||||
"... now what was I doing?",
|
"... now what was I doing?",
|
||||||
]
|
],
|
||||||
|
"npc.speech.menacing": [
|
||||||
|
"I'm warning you!",
|
||||||
|
"Any closer and I'll attack!",
|
||||||
|
"You don't scare me!",
|
||||||
|
"Get away from here!",
|
||||||
|
"Turn around if you want to live!",
|
||||||
|
"You're not welcome here!",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -15,7 +15,6 @@ use super::dialogue::Subject;
|
|||||||
|
|
||||||
pub const DEFAULT_INTERACTION_TIME: f32 = 1.0;
|
pub const DEFAULT_INTERACTION_TIME: f32 = 1.0;
|
||||||
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
|
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
|
||||||
pub const MAX_LISTEN_DIST: f32 = 100.0;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Deserialize)]
|
||||||
pub enum Alignment {
|
pub enum Alignment {
|
||||||
@ -43,6 +42,8 @@ impl Alignment {
|
|||||||
// Always attacks
|
// Always attacks
|
||||||
pub fn hostile_towards(self, other: Alignment) -> bool {
|
pub fn hostile_towards(self, other: Alignment) -> bool {
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
|
(Alignment::Passive, _) => false,
|
||||||
|
(_, Alignment::Passive) => false,
|
||||||
(Alignment::Enemy, Alignment::Enemy) => false,
|
(Alignment::Enemy, Alignment::Enemy) => false,
|
||||||
(Alignment::Enemy, Alignment::Wild) => false,
|
(Alignment::Enemy, Alignment::Wild) => false,
|
||||||
(Alignment::Wild, Alignment::Enemy) => false,
|
(Alignment::Wild, Alignment::Enemy) => false,
|
||||||
@ -167,85 +168,107 @@ impl Behavior {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Psyche {
|
pub struct Psyche {
|
||||||
pub aggro: f32, // 0.0 = always flees, 1.0 = always attacks, 0.5 = flee at 50% health
|
/// 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a Body> for Psyche {
|
impl<'a> From<&'a Body> for Psyche {
|
||||||
fn from(body: &'a Body) -> Self {
|
fn from(body: &'a Body) -> Self {
|
||||||
Self {
|
Self {
|
||||||
aggro: match body {
|
flee_health: match body {
|
||||||
Body::Humanoid(humanoid) => match humanoid.species {
|
Body::Humanoid(humanoid) => match humanoid.species {
|
||||||
humanoid::Species::Danari => 0.9,
|
humanoid::Species::Danari => 0.1,
|
||||||
humanoid::Species::Dwarf => 0.8,
|
humanoid::Species::Dwarf => 0.2,
|
||||||
humanoid::Species::Elf => 0.7,
|
humanoid::Species::Elf => 0.3,
|
||||||
humanoid::Species::Human => 0.6,
|
humanoid::Species::Human => 0.4,
|
||||||
humanoid::Species::Orc => 0.9,
|
humanoid::Species::Orc => 0.1,
|
||||||
humanoid::Species::Undead => 0.9,
|
humanoid::Species::Undead => 0.1,
|
||||||
},
|
},
|
||||||
Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
|
Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
|
||||||
quadruped_small::Species::Pig => 0.5,
|
quadruped_small::Species::Pig => 0.5,
|
||||||
quadruped_small::Species::Fox => 0.3,
|
quadruped_small::Species::Fox => 0.7,
|
||||||
quadruped_small::Species::Sheep => 0.5,
|
quadruped_small::Species::Sheep => 0.5,
|
||||||
quadruped_small::Species::Boar => 0.8,
|
quadruped_small::Species::Boar => 0.2,
|
||||||
quadruped_small::Species::Jackalope => 0.4,
|
quadruped_small::Species::Jackalope => 0.6,
|
||||||
quadruped_small::Species::Skunk => 0.6,
|
quadruped_small::Species::Skunk => 0.4,
|
||||||
quadruped_small::Species::Cat => 0.2,
|
quadruped_small::Species::Cat => 0.8,
|
||||||
quadruped_small::Species::Batfox => 0.6,
|
quadruped_small::Species::Batfox => 0.4,
|
||||||
quadruped_small::Species::Raccoon => 0.4,
|
quadruped_small::Species::Raccoon => 0.6,
|
||||||
quadruped_small::Species::Quokka => 0.4,
|
quadruped_small::Species::Quokka => 0.6,
|
||||||
quadruped_small::Species::Dodarock => 0.9,
|
quadruped_small::Species::Dodarock => 0.1,
|
||||||
quadruped_small::Species::Holladon => 1.0,
|
quadruped_small::Species::Holladon => 0.0,
|
||||||
quadruped_small::Species::Hyena => 0.4,
|
quadruped_small::Species::Hyena => 0.6,
|
||||||
quadruped_small::Species::Rabbit => 0.1,
|
quadruped_small::Species::Rabbit => 0.9,
|
||||||
quadruped_small::Species::Truffler => 0.8,
|
quadruped_small::Species::Truffler => 0.2,
|
||||||
quadruped_small::Species::Frog => 0.4,
|
quadruped_small::Species::Frog => 0.6,
|
||||||
quadruped_small::Species::Hare => 0.2,
|
quadruped_small::Species::Hare => 0.8,
|
||||||
quadruped_small::Species::Goat => 0.5,
|
quadruped_small::Species::Goat => 0.5,
|
||||||
_ => 0.0,
|
_ => 1.0,
|
||||||
},
|
},
|
||||||
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
|
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
|
||||||
quadruped_medium::Species::Tuskram => 0.7,
|
quadruped_medium::Species::Tuskram => 0.3,
|
||||||
quadruped_medium::Species::Frostfang => 0.9,
|
quadruped_medium::Species::Frostfang => 0.1,
|
||||||
quadruped_medium::Species::Mouflon => 0.7,
|
quadruped_medium::Species::Mouflon => 0.3,
|
||||||
quadruped_medium::Species::Catoblepas => 0.8,
|
quadruped_medium::Species::Catoblepas => 0.2,
|
||||||
quadruped_medium::Species::Deer => 0.6,
|
quadruped_medium::Species::Deer => 0.4,
|
||||||
quadruped_medium::Species::Hirdrasil => 0.7,
|
quadruped_medium::Species::Hirdrasil => 0.3,
|
||||||
quadruped_medium::Species::Donkey => 0.7,
|
quadruped_medium::Species::Donkey => 0.3,
|
||||||
quadruped_medium::Species::Camel => 0.7,
|
quadruped_medium::Species::Camel => 0.3,
|
||||||
quadruped_medium::Species::Zebra => 0.7,
|
quadruped_medium::Species::Zebra => 0.3,
|
||||||
quadruped_medium::Species::Antelope => 0.6,
|
quadruped_medium::Species::Antelope => 0.4,
|
||||||
quadruped_medium::Species::Horse => 0.7,
|
quadruped_medium::Species::Horse => 0.3,
|
||||||
quadruped_medium::Species::Cattle => 0.7,
|
quadruped_medium::Species::Cattle => 0.3,
|
||||||
quadruped_medium::Species::Darkhound => 0.9,
|
quadruped_medium::Species::Darkhound => 0.1,
|
||||||
quadruped_medium::Species::Dreadhorn => 0.8,
|
quadruped_medium::Species::Dreadhorn => 0.2,
|
||||||
quadruped_medium::Species::Snowleopard => 0.7,
|
quadruped_medium::Species::Snowleopard => 0.3,
|
||||||
quadruped_medium::Species::Llama => 0.6,
|
quadruped_medium::Species::Llama => 0.4,
|
||||||
quadruped_medium::Species::Alpaca => 0.6,
|
quadruped_medium::Species::Alpaca => 0.4,
|
||||||
_ => 0.5,
|
_ => 0.5,
|
||||||
},
|
},
|
||||||
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
|
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
|
||||||
quadruped_low::Species::Salamander => 0.7,
|
quadruped_low::Species::Salamander => 0.3,
|
||||||
quadruped_low::Species::Monitor => 0.7,
|
quadruped_low::Species::Monitor => 0.3,
|
||||||
quadruped_low::Species::Asp => 0.9,
|
quadruped_low::Species::Asp => 0.1,
|
||||||
quadruped_low::Species::Pangolin => 0.4,
|
quadruped_low::Species::Pangolin => 0.6,
|
||||||
_ => 0.6,
|
_ => 0.4,
|
||||||
},
|
},
|
||||||
Body::BipedSmall(_) => 0.5,
|
Body::BipedSmall(_) => 0.5,
|
||||||
Body::BirdMedium(_) => 0.5,
|
Body::BirdMedium(_) => 0.5,
|
||||||
Body::BirdLarge(_) => 0.9,
|
Body::BirdLarge(_) => 0.1,
|
||||||
Body::FishMedium(_) => 0.15,
|
Body::FishMedium(_) => 0.85,
|
||||||
Body::FishSmall(_) => 0.0,
|
Body::FishSmall(_) => 1.0,
|
||||||
Body::BipedLarge(_) => 1.0,
|
Body::BipedLarge(_) => 0.0,
|
||||||
Body::Object(_) => 1.0,
|
Body::Object(_) => 0.0,
|
||||||
Body::Golem(_) => 1.0,
|
Body::Golem(_) => 0.0,
|
||||||
Body::Theropod(_) => 1.0,
|
Body::Theropod(_) => 0.0,
|
||||||
Body::Dragon(_) => 1.0,
|
Body::Dragon(_) => 0.0,
|
||||||
Body::Ship(_) => 1.0,
|
Body::Ship(_) => 0.0,
|
||||||
|
},
|
||||||
|
sight_dist: 40.0,
|
||||||
|
listen_dist: 30.0,
|
||||||
|
aggro_dist: match body {
|
||||||
|
Body::Humanoid(_) => Some(20.0),
|
||||||
|
_ => None, // Always aggressive if detected
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Psyche {
|
||||||
|
/// The maximum distance that targets might be detected by this agent.
|
||||||
|
pub fn search_dist(&self) -> f32 { self.sight_dist.max(self.listen_dist) }
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
/// Events that affect agent behavior from other entities/players/environment
|
/// Events that affect agent behavior from other entities/players/environment
|
||||||
pub enum AgentEvent {
|
pub enum AgentEvent {
|
||||||
@ -309,8 +332,12 @@ pub enum SoundKind {
|
|||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Target {
|
pub struct Target {
|
||||||
pub target: EcsEntity,
|
pub target: EcsEntity,
|
||||||
|
/// Whether the target is hostile
|
||||||
pub hostile: bool,
|
pub hostile: bool,
|
||||||
|
/// The time at which the target was selected
|
||||||
pub selected_at: f64,
|
pub selected_at: f64,
|
||||||
|
/// Whether the target has come close enough to trigger aggro.
|
||||||
|
pub aggro_on: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
@ -345,7 +372,7 @@ impl Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_destination(mut self, pos: Vec3<f32>) -> Self {
|
pub fn with_destination(mut self, pos: Vec3<f32>) -> Self {
|
||||||
self.psyche = Psyche { aggro: 1.0 };
|
self.psyche.flee_health = 0.0;
|
||||||
self.rtsim_controller = RtSimController::with_destination(pos);
|
self.rtsim_controller = RtSimController::with_destination(pos);
|
||||||
self.behavior.allow(BehaviorCapability::SPEAK);
|
self.behavior.allow(BehaviorCapability::SPEAK);
|
||||||
self
|
self
|
||||||
@ -369,7 +396,10 @@ impl Agent {
|
|||||||
Agent {
|
Agent {
|
||||||
patrol_origin,
|
patrol_origin,
|
||||||
psyche: if no_flee {
|
psyche: if no_flee {
|
||||||
Psyche { aggro: 1.0 }
|
Psyche {
|
||||||
|
flee_health: 0.0,
|
||||||
|
..Psyche::from(body)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Psyche::from(body)
|
Psyche::from(body)
|
||||||
},
|
},
|
||||||
|
@ -242,7 +242,10 @@ impl ControllerInputs {
|
|||||||
|
|
||||||
impl Controller {
|
impl Controller {
|
||||||
/// Sets all inputs to default
|
/// Sets all inputs to default
|
||||||
pub fn reset(&mut self) { *self = Self::default(); }
|
pub fn reset(&mut self) {
|
||||||
|
self.inputs = Default::default();
|
||||||
|
self.queued_inputs = Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_events(&mut self) { self.events.clear(); }
|
pub fn clear_events(&mut self) { self.events.clear(); }
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ use common::{
|
|||||||
assets,
|
assets,
|
||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
agent::{AgentEvent, Sound, SoundKind, MAX_LISTEN_DIST},
|
agent::{AgentEvent, Sound, SoundKind},
|
||||||
dialogue::Subject,
|
dialogue::Subject,
|
||||||
inventory::slot::EquipSlot,
|
inventory::slot::EquipSlot,
|
||||||
item,
|
item,
|
||||||
@ -408,7 +408,7 @@ pub fn handle_sound(server: &mut Server, sound: &Sound) {
|
|||||||
let propagated_sound = sound.with_new_vol(sound.vol - vol_dropoff);
|
let propagated_sound = sound.with_new_vol(sound.vol - vol_dropoff);
|
||||||
|
|
||||||
let can_hear_sound = propagated_sound.vol > 0.00;
|
let can_hear_sound = propagated_sound.vol > 0.00;
|
||||||
let should_hear_sound = agent_dist_sqrd < MAX_LISTEN_DIST.powi(2);
|
let should_hear_sound = agent_dist_sqrd < agent.psyche.listen_dist.powi(2);
|
||||||
|
|
||||||
if can_hear_sound && should_hear_sound {
|
if can_hear_sound && should_hear_sound {
|
||||||
agent
|
agent
|
||||||
|
@ -159,6 +159,7 @@ impl Settings {
|
|||||||
max_players: 100,
|
max_players: 100,
|
||||||
start_time: 9.0 * 3600.0,
|
start_time: 9.0 * 3600.0,
|
||||||
max_view_distance: None,
|
max_view_distance: None,
|
||||||
|
safe_spawn: false,
|
||||||
client_timeout: Duration::from_secs(180),
|
client_timeout: Duration::from_secs(180),
|
||||||
..load // Fill in remaining fields from server_settings.ron.
|
..load // Fill in remaining fields from server_settings.ron.
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@ use common::{
|
|||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
agent::{
|
agent::{
|
||||||
AgentEvent, Sound, SoundKind, Target, DEFAULT_INTERACTION_TIME, MAX_LISTEN_DIST,
|
AgentEvent, Sound, SoundKind, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME,
|
||||||
TRADE_INTERACTION_TIME,
|
|
||||||
},
|
},
|
||||||
buff::{BuffKind, Buffs},
|
buff::{BuffKind, Buffs},
|
||||||
compass::{Direction, Distance},
|
compass::{Direction, Distance},
|
||||||
@ -169,7 +168,6 @@ const PARTIAL_PATH_DIST: f32 = 50.0;
|
|||||||
const SEPARATION_DIST: f32 = 10.0;
|
const SEPARATION_DIST: f32 = 10.0;
|
||||||
const SEPARATION_BIAS: f32 = 0.8;
|
const SEPARATION_BIAS: f32 = 0.8;
|
||||||
const MAX_FLEE_DIST: f32 = 20.0;
|
const MAX_FLEE_DIST: f32 = 20.0;
|
||||||
const SEARCH_DIST: f32 = 48.0;
|
|
||||||
const SNEAK_COEFFICIENT: f32 = 0.25;
|
const SNEAK_COEFFICIENT: f32 = 0.25;
|
||||||
const AVG_FOLLOW_DIST: f32 = 6.0;
|
const AVG_FOLLOW_DIST: f32 = 6.0;
|
||||||
const RETARGETING_THRESHOLD_SECONDS: f64 = 10.0;
|
const RETARGETING_THRESHOLD_SECONDS: f64 = 10.0;
|
||||||
@ -270,7 +268,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let event_emitter = event_bus.emitter();
|
let mut event_emitter = event_bus.emitter();
|
||||||
|
|
||||||
if !matches!(char_state, CharacterState::LeapMelee(_)) {
|
if !matches!(char_state, CharacterState::LeapMelee(_)) {
|
||||||
// Default to looking in orientation direction
|
// Default to looking in orientation direction
|
||||||
@ -381,44 +379,45 @@ impl<'a> System<'a> for Sys {
|
|||||||
// are the only parts of this tree that should provide
|
// are the only parts of this tree that should provide
|
||||||
// inputs.
|
// inputs.
|
||||||
|
|
||||||
let idle = |agent: &mut Agent, controller, mut event_emitter| {
|
let idle =
|
||||||
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
|
|agent: &mut Agent,
|
||||||
};
|
controller: &mut Controller,
|
||||||
|
event_emitter: &mut Emitter<ServerEvent>| {
|
||||||
let relax = |agent: &mut Agent, controller, event_emitter| {
|
agent.target = None;
|
||||||
agent.target = None;
|
data.idle_tree(agent, controller, &read_data, event_emitter);
|
||||||
idle(agent, controller, event_emitter);
|
|
||||||
};
|
|
||||||
|
|
||||||
let react_as_pet =
|
|
||||||
|agent: &mut Agent, target: EcsEntity, controller, event_emitter| {
|
|
||||||
if let Some(tgt_pos) = read_data.positions.get(target) {
|
|
||||||
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
|
|
||||||
// If really far away drop everything and follow
|
|
||||||
if dist_sqrd > (2.0 * MAX_FOLLOW_DIST).powi(2) {
|
|
||||||
agent.bearing = Vec2::zero();
|
|
||||||
data.follow(agent, controller, &read_data.terrain, tgt_pos);
|
|
||||||
// Attack target's attacker
|
|
||||||
} else if entity_was_attacked(target, &read_data) {
|
|
||||||
data.attack_target_attacker(agent, &read_data, controller);
|
|
||||||
// Follow owner if too far away and not
|
|
||||||
// fighting
|
|
||||||
} else if dist_sqrd > MAX_FOLLOW_DIST.powi(2) {
|
|
||||||
data.follow(agent, controller, &read_data.terrain, tgt_pos);
|
|
||||||
|
|
||||||
// Otherwise just idle
|
|
||||||
} else {
|
|
||||||
idle(agent, controller, event_emitter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let react_as_pet = |agent: &mut Agent,
|
||||||
|
target: EcsEntity,
|
||||||
|
controller: &mut Controller,
|
||||||
|
event_emitter| {
|
||||||
|
if let Some(tgt_pos) = read_data.positions.get(target) {
|
||||||
|
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
|
||||||
|
// If really far away drop everything and follow
|
||||||
|
if dist_sqrd > (2.0 * MAX_FOLLOW_DIST).powi(2) {
|
||||||
|
agent.bearing = Vec2::zero();
|
||||||
|
data.follow(agent, controller, &read_data.terrain, tgt_pos);
|
||||||
|
// Attack target's attacker
|
||||||
|
} else if entity_was_attacked(target, &read_data) {
|
||||||
|
data.attack_target_attacker(agent, &read_data, controller);
|
||||||
|
// Follow owner if too far away and not
|
||||||
|
// fighting
|
||||||
|
} else if dist_sqrd > MAX_FOLLOW_DIST.powi(2) {
|
||||||
|
data.follow(agent, controller, &read_data.terrain, tgt_pos);
|
||||||
|
|
||||||
|
// Otherwise just idle
|
||||||
|
} else {
|
||||||
|
idle(agent, controller, event_emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let react_to_target =
|
let react_to_target =
|
||||||
|agent: &mut Agent,
|
|agent: &mut Agent,
|
||||||
target: EcsEntity,
|
target: EcsEntity,
|
||||||
hostile: bool,
|
hostile: bool,
|
||||||
controller,
|
controller: &mut Controller,
|
||||||
mut event_emitter| {
|
event_emitter| {
|
||||||
if let Some(tgt_health) = read_data.healths.get(target) {
|
if let Some(tgt_health) = read_data.healths.get(target) {
|
||||||
// If target is dead, leave it
|
// If target is dead, leave it
|
||||||
if tgt_health.is_dead {
|
if tgt_health.is_dead {
|
||||||
@ -427,69 +426,27 @@ impl<'a> System<'a> for Sys {
|
|||||||
{
|
{
|
||||||
rtsim_forget_enemy(&tgt_stats.name, agent);
|
rtsim_forget_enemy(&tgt_stats.name, agent);
|
||||||
}
|
}
|
||||||
relax(agent, controller, event_emitter);
|
agent.target = None;
|
||||||
// If the target is hostile
|
// If the target is hostile
|
||||||
// (either based on alignment or if
|
// (either based on alignment or if
|
||||||
// the target just attacked)
|
// the target just attacked)
|
||||||
} else if hostile {
|
} else if hostile {
|
||||||
data.hostile_tree(
|
data.hostile_tree(agent, controller, &read_data, event_emitter);
|
||||||
agent,
|
|
||||||
controller,
|
|
||||||
&read_data,
|
|
||||||
&mut event_emitter,
|
|
||||||
);
|
|
||||||
// Target is something worth following
|
// Target is something worth following
|
||||||
// methinks
|
// methinks
|
||||||
} else if let Some(Alignment::Owned(uid)) = data.alignment {
|
} else if let Some(Alignment::Owned(uid)) = data.alignment {
|
||||||
if read_data.uids.get(target) == Some(uid) {
|
if read_data.uids.get(target) == Some(uid) {
|
||||||
react_as_pet(agent, target, controller, event_emitter);
|
react_as_pet(agent, target, controller, event_emitter);
|
||||||
} else {
|
} else {
|
||||||
relax(agent, controller, event_emitter);
|
agent.target = None;
|
||||||
|
idle(agent, controller, event_emitter);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
idle(agent, controller, event_emitter);
|
idle(agent, controller, event_emitter);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
relax(agent, controller, event_emitter);
|
agent.target = None;
|
||||||
}
|
idle(agent, controller, event_emitter);
|
||||||
};
|
|
||||||
|
|
||||||
let react_to_damage =
|
|
||||||
|agent: &mut Agent, by: Uid, controller, event_emitter| {
|
|
||||||
if let Some(attacker) =
|
|
||||||
read_data.uid_allocator.retrieve_entity_internal(by.id())
|
|
||||||
{
|
|
||||||
// If the target is dead or in a safezone, remove the
|
|
||||||
// target and idle.
|
|
||||||
if should_stop_attacking(attacker, &read_data) {
|
|
||||||
relax(agent, controller, event_emitter);
|
|
||||||
} else if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
|
||||||
if agent.target.is_none() {
|
|
||||||
controller.push_event(ControlEvent::Utterance(
|
|
||||||
UtteranceKind::Angry,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
agent.target = Some(Target {
|
|
||||||
target: attacker,
|
|
||||||
hostile: true,
|
|
||||||
selected_at: read_data.time.0,
|
|
||||||
});
|
|
||||||
let target_data = TargetData {
|
|
||||||
pos: tgt_pos,
|
|
||||||
body: read_data.bodies.get(attacker),
|
|
||||||
scale: read_data.scales.get(attacker),
|
|
||||||
};
|
|
||||||
data.attack(agent, controller, &target_data, &read_data);
|
|
||||||
// Remember this encounter if an RtSim entity
|
|
||||||
if let Some(tgt_stats) =
|
|
||||||
data.rtsim_entity.and(read_data.stats.get(attacker))
|
|
||||||
{
|
|
||||||
rtsim_new_enemy(&tgt_stats.name, agent, &read_data);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
relax(agent, controller, event_emitter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -507,25 +464,75 @@ impl<'a> System<'a> for Sys {
|
|||||||
data.fly_upward(controller)
|
data.fly_upward(controller)
|
||||||
} else if is_falling_dangerous && data.glider_equipped {
|
} else if is_falling_dangerous && data.glider_equipped {
|
||||||
data.glider_fall(controller);
|
data.glider_fall(controller);
|
||||||
} else if let Some(target_info) = agent.target {
|
|
||||||
let Target {
|
|
||||||
target, hostile, ..
|
|
||||||
} = target_info;
|
|
||||||
react_to_target(agent, target, hostile, controller, event_emitter);
|
|
||||||
} else {
|
} else {
|
||||||
// Target an entity that's attacking us if the attack
|
// Target an entity that's attacking us if the attack
|
||||||
// was recent and we have a health component
|
// was recent and we have a health component
|
||||||
match health {
|
match health {
|
||||||
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => {
|
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => {
|
||||||
if let Some(by) = health.last_change.1.cause.damage_by() {
|
if let Some(by) = health.last_change.1.cause.damage_by() {
|
||||||
react_to_damage(agent, by, controller, event_emitter);
|
if let Some(attacker) =
|
||||||
} else {
|
read_data.uid_allocator.retrieve_entity_internal(by.id())
|
||||||
relax(agent, controller, event_emitter);
|
{
|
||||||
|
// If the target is dead or in a safezone, remove the
|
||||||
|
// target and idle.
|
||||||
|
if should_stop_attacking(attacker, &read_data) {
|
||||||
|
agent.target = None;
|
||||||
|
} else if let Some(tgt_pos) =
|
||||||
|
read_data.positions.get(attacker)
|
||||||
|
{
|
||||||
|
if agent.target.is_none() {
|
||||||
|
controller.push_event(ControlEvent::Utterance(
|
||||||
|
UtteranceKind::Angry,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine whether the new target should be a priority
|
||||||
|
// over the old one
|
||||||
|
// (i.e: because it's either close or because they
|
||||||
|
// attacked us)
|
||||||
|
let more_dangerous_than_old_target =
|
||||||
|
agent.target.map_or(true, |old_tgt| {
|
||||||
|
if let Some(old_tgt_pos) =
|
||||||
|
read_data.positions.get(old_tgt.target)
|
||||||
|
{
|
||||||
|
!old_tgt.aggro_on
|
||||||
|
|| old_tgt_pos.0.distance_squared(pos.0)
|
||||||
|
< tgt_pos.0.distance_squared(pos.0)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select the attacker as the new target
|
||||||
|
if more_dangerous_than_old_target {
|
||||||
|
agent.target = Some(Target {
|
||||||
|
target: attacker,
|
||||||
|
hostile: true,
|
||||||
|
selected_at: read_data.time.0,
|
||||||
|
aggro_on: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember this attack if we're an RtSim entity
|
||||||
|
if let Some(tgt_stats) =
|
||||||
|
data.rtsim_entity.and(read_data.stats.get(attacker))
|
||||||
|
{
|
||||||
|
rtsim_new_enemy(&tgt_stats.name, agent, &read_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {},
|
||||||
idle(agent, controller, event_emitter);
|
}
|
||||||
},
|
|
||||||
|
if let Some(target_info) = agent.target {
|
||||||
|
let Target {
|
||||||
|
target, hostile, ..
|
||||||
|
} = target_info;
|
||||||
|
react_to_target(agent, target, hostile, controller, &mut event_emitter);
|
||||||
|
} else {
|
||||||
|
idle(agent, controller, &mut event_emitter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,7 +579,7 @@ impl<'a> AgentData<'a> {
|
|||||||
if agent.target.is_none() && thread_rng().gen_bool(0.1) {
|
if agent.target.is_none() && thread_rng().gen_bool(0.1) {
|
||||||
if let Some(Alignment::Owned(owner)) = self.alignment {
|
if let Some(Alignment::Owned(owner)) = self.alignment {
|
||||||
if let Some(owner) = get_entity_by_id(owner.id(), read_data) {
|
if let Some(owner) = get_entity_by_id(owner.id(), read_data) {
|
||||||
agent.target = build_target(owner, false, read_data.time.0);
|
agent.target = build_target(owner, false, read_data.time.0, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -647,19 +654,34 @@ impl<'a> AgentData<'a> {
|
|||||||
if let Some(Target {
|
if let Some(Target {
|
||||||
target,
|
target,
|
||||||
selected_at,
|
selected_at,
|
||||||
|
aggro_on,
|
||||||
..
|
..
|
||||||
}) = agent.target
|
}) = &mut agent.target
|
||||||
{
|
{
|
||||||
|
let target = *target;
|
||||||
|
let selected_at = *selected_at;
|
||||||
|
|
||||||
if let Some(tgt_pos) = read_data.positions.get(target) {
|
if let Some(tgt_pos) = read_data.positions.get(target) {
|
||||||
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
|
let dist_sq = self.pos.0.distance_squared(tgt_pos.0);
|
||||||
// Should the agent flee?
|
let in_aggro_range = agent
|
||||||
if 1.0 - agent.psyche.aggro > self.damage && self.flees {
|
.psyche
|
||||||
|
.aggro_dist
|
||||||
|
.map_or(true, |ad| dist_sq < ad.powi(2));
|
||||||
|
// If, at any point, the target comes closer than the aggro distance, switch to
|
||||||
|
// aggro mode
|
||||||
|
if in_aggro_range {
|
||||||
|
*aggro_on = true;
|
||||||
|
}
|
||||||
|
let aggro_on = *aggro_on;
|
||||||
|
|
||||||
|
if self.damage.min(1.0) < agent.psyche.flee_health && self.flees {
|
||||||
|
// Should the agent flee?
|
||||||
if agent.action_state.timer == 0.0 && can_speak(agent) {
|
if agent.action_state.timer == 0.0 && can_speak(agent) {
|
||||||
let msg = "npc.speech.villager_under_attack".to_string();
|
self.chat_general("npc.speech.villager_under_attack", event_emitter);
|
||||||
self.chat_general(msg, event_emitter);
|
|
||||||
self.emit_villager_alarm(read_data.time.0, event_emitter);
|
self.emit_villager_alarm(read_data.time.0, event_emitter);
|
||||||
agent.action_state.timer = 0.01;
|
agent.action_state.timer = 0.01;
|
||||||
} else if agent.action_state.timer < FLEE_DURATION || dist_sqrd < MAX_FLEE_DIST
|
} else if agent.action_state.timer < FLEE_DURATION
|
||||||
|
|| dist_sq < MAX_FLEE_DIST.powi(2)
|
||||||
{
|
{
|
||||||
self.flee(agent, controller, &read_data.terrain, tgt_pos);
|
self.flee(agent, controller, &read_data.terrain, tgt_pos);
|
||||||
agent.action_state.timer += read_data.dt.0;
|
agent.action_state.timer += read_data.dt.0;
|
||||||
@ -668,32 +690,36 @@ impl<'a> AgentData<'a> {
|
|||||||
agent.target = None;
|
agent.target = None;
|
||||||
self.idle(agent, controller, read_data);
|
self.idle(agent, controller, read_data);
|
||||||
}
|
}
|
||||||
|
} else if should_stop_attacking(target, read_data) {
|
||||||
// If not fleeing, attack the hostile entity!
|
if can_speak(agent) {
|
||||||
|
let msg = "npc.speech.villager_enemy_killed".to_string();
|
||||||
|
event_emitter
|
||||||
|
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
||||||
|
}
|
||||||
|
agent.target = None;
|
||||||
|
self.idle(agent, controller, read_data);
|
||||||
} else {
|
} else {
|
||||||
// If the hostile entity is dead or has an invulnerability buff (eg, those
|
// Potentially choose a new target
|
||||||
// applied in safezones), return to idle
|
if read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS
|
||||||
if should_stop_attacking(target, read_data) {
|
&& !in_aggro_range
|
||||||
if can_speak(agent) {
|
|
||||||
let msg = "npc.speech.villager_enemy_killed".to_string();
|
|
||||||
event_emitter
|
|
||||||
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
|
||||||
}
|
|
||||||
agent.target = None;
|
|
||||||
self.idle(agent, controller, read_data);
|
|
||||||
// Choose a new target every 10 seconds, but only for
|
|
||||||
// enemies
|
|
||||||
// TODO: This should be more principled. Consider factoring
|
|
||||||
// health, combat rating, wielded weapon, etc, into the
|
|
||||||
// decision to change target.
|
|
||||||
} else if read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS
|
|
||||||
&& matches!(self.alignment, Some(Alignment::Enemy))
|
|
||||||
{
|
{
|
||||||
self.choose_target(agent, controller, read_data, event_emitter);
|
self.choose_target(agent, controller, read_data, event_emitter);
|
||||||
} else {
|
}
|
||||||
// TODO Add utility for attacking vs leaving target alone
|
|
||||||
|
if aggro_on {
|
||||||
let target_data = build_target_data(target, tgt_pos, read_data);
|
let target_data = build_target_data(target, tgt_pos, read_data);
|
||||||
self.attack(agent, controller, &target_data, read_data);
|
self.attack(agent, controller, &target_data, read_data);
|
||||||
|
} else {
|
||||||
|
// If we're not yet aggro-ed, strike a menacing pose
|
||||||
|
if thread_rng().gen::<f32>() < read_data.dt.0 * 0.25 {
|
||||||
|
self.chat_general_if_can_speak(
|
||||||
|
agent,
|
||||||
|
"npc.speech.menacing",
|
||||||
|
event_emitter,
|
||||||
|
);
|
||||||
|
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
||||||
|
}
|
||||||
|
self.menacing(agent, controller, read_data, target, tgt_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -969,9 +995,9 @@ impl<'a> AgentData<'a> {
|
|||||||
Some(AgentEvent::Talk(by, subject)) => {
|
Some(AgentEvent::Talk(by, subject)) => {
|
||||||
if can_speak(agent) {
|
if can_speak(agent) {
|
||||||
if let Some(target) = get_entity_by_id(by.id(), read_data) {
|
if let Some(target) = get_entity_by_id(by.id(), read_data) {
|
||||||
agent.target = build_target(target, false, read_data.time.0);
|
agent.target = build_target(target, false, read_data.time.0, false);
|
||||||
|
|
||||||
if self.look_toward(controller, read_data, &target) {
|
if self.look_toward(controller, read_data, target) {
|
||||||
controller.actions.push(ControlAction::Stand);
|
controller.actions.push(ControlAction::Stand);
|
||||||
controller.actions.push(ControlAction::Talk);
|
controller.actions.push(ControlAction::Talk);
|
||||||
controller.push_event(ControlEvent::Utterance(UtteranceKind::Greeting));
|
controller.push_event(ControlEvent::Utterance(UtteranceKind::Greeting));
|
||||||
@ -1017,11 +1043,12 @@ impl<'a> AgentData<'a> {
|
|||||||
};
|
};
|
||||||
self.chat_general(msg, event_emitter);
|
self.chat_general(msg, event_emitter);
|
||||||
} else if agent.behavior.can_trade() {
|
} else if agent.behavior.can_trade() {
|
||||||
let msg = "npc.speech.merchant_advertisement".to_string();
|
self.chat_general(
|
||||||
self.chat_general(msg, event_emitter);
|
"npc.speech.merchant_advertisement",
|
||||||
|
event_emitter,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
let msg = "npc.speech.villager".to_string();
|
self.chat_general("npc.speech.villager", event_emitter);
|
||||||
self.chat_general(msg, event_emitter);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Subject::Trade => {
|
Subject::Trade => {
|
||||||
@ -1031,18 +1058,23 @@ impl<'a> AgentData<'a> {
|
|||||||
by,
|
by,
|
||||||
InviteKind::Trade,
|
InviteKind::Trade,
|
||||||
));
|
));
|
||||||
let msg =
|
self.chat_general(
|
||||||
"npc.speech.merchant_advertisement".to_string();
|
"npc.speech.merchant_advertisement",
|
||||||
self.chat_general(msg, event_emitter);
|
event_emitter,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
let msg = "npc.speech.merchant_busy".to_string();
|
self.chat_general(
|
||||||
self.chat_general(msg, event_emitter);
|
"npc.speech.merchant_busy",
|
||||||
|
event_emitter,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: maybe make some travellers willing to trade with
|
// TODO: maybe make some travellers willing to trade with
|
||||||
// simpler goods like potions
|
// simpler goods like potions
|
||||||
let msg = "npc.speech.villager_decline_trade".to_string();
|
self.chat_general(
|
||||||
self.chat_general(msg, event_emitter);
|
"npc.speech.villager_decline_trade",
|
||||||
|
event_emitter,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Subject::Mood => {
|
Subject::Mood => {
|
||||||
@ -1156,7 +1188,7 @@ impl<'a> AgentData<'a> {
|
|||||||
controller.actions.push(ControlAction::Stand);
|
controller.actions.push(ControlAction::Stand);
|
||||||
controller.actions.push(ControlAction::Talk);
|
controller.actions.push(ControlAction::Talk);
|
||||||
if let Some(target) = get_entity_by_id(with.id(), read_data) {
|
if let Some(target) = get_entity_by_id(with.id(), read_data) {
|
||||||
agent.target = build_target(target, false, read_data.time.0);
|
agent.target = build_target(target, false, read_data.time.0, false);
|
||||||
}
|
}
|
||||||
controller
|
controller
|
||||||
.events
|
.events
|
||||||
@ -1167,22 +1199,28 @@ impl<'a> AgentData<'a> {
|
|||||||
controller
|
controller
|
||||||
.events
|
.events
|
||||||
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
||||||
let msg = "npc.speech.merchant_busy".to_string();
|
self.chat_general_if_can_speak(
|
||||||
self.chat_general_if_can_speak(agent, msg, event_emitter);
|
agent,
|
||||||
|
"npc.speech.merchant_busy",
|
||||||
|
event_emitter,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Provide a hint where to find the closest merchant?
|
// TODO: Provide a hint where to find the closest merchant?
|
||||||
controller
|
controller
|
||||||
.events
|
.events
|
||||||
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
||||||
let msg = "npc.speech.villager_decline_trade".to_string();
|
self.chat_general_if_can_speak(
|
||||||
self.chat_general_if_can_speak(agent, msg, event_emitter);
|
agent,
|
||||||
|
"npc.speech.villager_decline_trade",
|
||||||
|
event_emitter,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(AgentEvent::TradeAccepted(with)) => {
|
Some(AgentEvent::TradeAccepted(with)) => {
|
||||||
if !agent.behavior.is(BehaviorState::TRADING) {
|
if !agent.behavior.is(BehaviorState::TRADING) {
|
||||||
if let Some(target) = get_entity_by_id(with.id(), read_data) {
|
if let Some(target) = get_entity_by_id(with.id(), read_data) {
|
||||||
agent.target = build_target(target, false, read_data.time.0);
|
agent.target = build_target(target, false, read_data.time.0, false);
|
||||||
}
|
}
|
||||||
agent.behavior.set(BehaviorState::TRADING);
|
agent.behavior.set(BehaviorState::TRADING);
|
||||||
agent.behavior.set(BehaviorState::TRADING_ISSUER);
|
agent.behavior.set(BehaviorState::TRADING_ISSUER);
|
||||||
@ -1192,12 +1230,13 @@ impl<'a> AgentData<'a> {
|
|||||||
if agent.behavior.is(BehaviorState::TRADING) {
|
if agent.behavior.is(BehaviorState::TRADING) {
|
||||||
match result {
|
match result {
|
||||||
TradeResult::Completed => {
|
TradeResult::Completed => {
|
||||||
let msg = "npc.speech.merchant_trade_successful".to_string();
|
self.chat_general(
|
||||||
self.chat_general(msg, event_emitter);
|
"npc.speech.merchant_trade_successful",
|
||||||
|
event_emitter,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
let msg = "npc.speech.merchant_trade_declined".to_string();
|
self.chat_general("npc.speech.merchant_trade_declined", event_emitter);
|
||||||
self.chat_general(msg, event_emitter);
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
agent.behavior.unset(BehaviorState::TRADING);
|
agent.behavior.unset(BehaviorState::TRADING);
|
||||||
@ -1268,7 +1307,7 @@ impl<'a> AgentData<'a> {
|
|||||||
// No new events, continue looking towards the last
|
// No new events, continue looking towards the last
|
||||||
// interacting player for some time
|
// interacting player for some time
|
||||||
if let Some(Target { target, .. }) = &agent.target {
|
if let Some(Target { target, .. }) = &agent.target {
|
||||||
self.look_toward(controller, read_data, target);
|
self.look_toward(controller, read_data, *target);
|
||||||
} else {
|
} else {
|
||||||
agent.action_state.timer = 0.0;
|
agent.action_state.timer = 0.0;
|
||||||
}
|
}
|
||||||
@ -1281,14 +1320,11 @@ impl<'a> AgentData<'a> {
|
|||||||
&self,
|
&self,
|
||||||
controller: &mut Controller,
|
controller: &mut Controller,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
target: &EcsEntity,
|
target: EcsEntity,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(tgt_pos) = read_data.positions.get(*target) {
|
if let Some(tgt_pos) = read_data.positions.get(target) {
|
||||||
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
|
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
|
||||||
let tgt_eye_offset = read_data
|
let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| b.eye_height());
|
||||||
.bodies
|
|
||||||
.get(*target)
|
|
||||||
.map_or(0.0, |b| b.eye_height());
|
|
||||||
if let Some(dir) = Dir::from_unnormalized(
|
if let Some(dir) = Dir::from_unnormalized(
|
||||||
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
|
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
|
||||||
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
|
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
|
||||||
@ -1301,6 +1337,24 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn menacing(
|
||||||
|
&self,
|
||||||
|
_agent: &mut Agent,
|
||||||
|
controller: &mut Controller,
|
||||||
|
read_data: &ReadData,
|
||||||
|
target: EcsEntity,
|
||||||
|
_tgt_pos: &Pos,
|
||||||
|
) {
|
||||||
|
self.look_toward(controller, read_data, target);
|
||||||
|
controller.actions.push(ControlAction::Wield);
|
||||||
|
|
||||||
|
let max_move = 0.5;
|
||||||
|
let move_dir_mag = controller.inputs.move_dir.magnitude();
|
||||||
|
if move_dir_mag > max_move {
|
||||||
|
controller.inputs.move_dir = max_move * controller.inputs.move_dir / move_dir_mag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn flee(
|
fn flee(
|
||||||
&self,
|
&self,
|
||||||
agent: &mut Agent,
|
agent: &mut Agent,
|
||||||
@ -1431,34 +1485,41 @@ impl<'a> AgentData<'a> {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let in_search_area = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
|
let max_search_dist = agent.psyche.search_dist();
|
||||||
let mut search_dist = SEARCH_DIST;
|
let max_sight_dist = agent.psyche.sight_dist;
|
||||||
if e_char_state.map_or(false, CharacterState::is_stealthy) {
|
let max_listen_dist = agent.psyche.listen_dist;
|
||||||
// TODO: make sneak more effective based on a stat like e_stats.fitness
|
let in_sight_dist = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
|
||||||
search_dist *= SNEAK_COEFFICIENT;
|
let search_dist = max_sight_dist
|
||||||
};
|
* if e_char_state.map_or(false, CharacterState::is_stealthy) {
|
||||||
|
// TODO: make sneak more effective based on a stat like e_stats.fitness
|
||||||
|
SNEAK_COEFFICIENT
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2)
|
e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
let within_view = |e_pos: &Pos| {
|
let within_fov = |e_pos: &Pos| {
|
||||||
(e_pos.0 - self.pos.0)
|
(e_pos.0 - self.pos.0)
|
||||||
.try_normalized()
|
.try_normalized()
|
||||||
.map_or(true, |v| v.dot(*controller.inputs.look_dir) > 0.15)
|
.map_or(true, |v| v.dot(*controller.inputs.look_dir) > 0.15)
|
||||||
};
|
};
|
||||||
|
|
||||||
let in_listen_area = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
|
let in_listen_dist = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
|
||||||
let mut listen_dist = MAX_LISTEN_DIST;
|
let listen_dist = max_listen_dist
|
||||||
if e_char_state.map_or(false, CharacterState::is_stealthy) {
|
* if e_char_state.map_or(false, CharacterState::is_stealthy) {
|
||||||
// TODO: make sneak more effective based on a stat like e_stats.fitness
|
// TODO: make sneak more effective based on a stat like e_stats.fitness
|
||||||
listen_dist *= SNEAK_COEFFICIENT;
|
SNEAK_COEFFICIENT
|
||||||
}
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
// TODO implement proper sound system for agents
|
// TODO implement proper sound system for agents
|
||||||
e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)
|
e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
let within_reach = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
|
let within_reach = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
|
||||||
(in_search_area(e_pos, e_char_state) && within_view(e_pos))
|
(in_sight_dist(e_pos, e_char_state) && within_fov(e_pos))
|
||||||
|| in_listen_area(e_pos, e_char_state)
|
|| in_listen_dist(e_pos, e_char_state)
|
||||||
};
|
};
|
||||||
|
|
||||||
let owners_hostile = |e_alignment: Option<&Alignment>| {
|
let owners_hostile = |e_alignment: Option<&Alignment>| {
|
||||||
@ -1594,9 +1655,10 @@ impl<'a> AgentData<'a> {
|
|||||||
// TODO choose target by more than just distance
|
// TODO choose target by more than just distance
|
||||||
let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
|
let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
|
||||||
let target = grid
|
let target = grid
|
||||||
.in_circle_aabr(self.pos.0.xy(), SEARCH_DIST)
|
.in_circle_aabr(self.pos.0.xy(), max_search_dist)
|
||||||
.filter_map(worth_choosing)
|
.filter_map(worth_choosing)
|
||||||
.filter_map(possible_target)
|
.filter_map(possible_target)
|
||||||
|
// TODO: This seems expensive. Cache this to avoid recomputing each tick
|
||||||
.filter(|(_, e_pos)| can_see_them(e_pos))
|
.filter(|(_, e_pos)| can_see_them(e_pos))
|
||||||
.min_by_key(|(_, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32)
|
.min_by_key(|(_, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32)
|
||||||
.map(|(e, _)| e);
|
.map(|(e, _)| e);
|
||||||
@ -1609,6 +1671,7 @@ impl<'a> AgentData<'a> {
|
|||||||
target,
|
target,
|
||||||
hostile: true,
|
hostile: true,
|
||||||
selected_at: read_data.time.0,
|
selected_at: read_data.time.0,
|
||||||
|
aggro_on: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4017,11 +4080,13 @@ impl<'a> AgentData<'a> {
|
|||||||
} else if is_village_guard {
|
} else if is_village_guard {
|
||||||
self.follow(agent, controller, &read_data.terrain, &sound_pos);
|
self.follow(agent, controller, &read_data.terrain, &sound_pos);
|
||||||
} else if is_neutral {
|
} else if is_neutral {
|
||||||
let aggro = agent.psyche.aggro;
|
let flee_health = agent.psyche.flee_health;
|
||||||
let close_enough = dist_sqrd < 35.0_f32.powi(2);
|
let close_enough = dist_sqrd < 35.0_f32.powi(2);
|
||||||
let sound_was_loud = sound.vol >= 10.0;
|
let sound_was_loud = sound.vol >= 10.0;
|
||||||
|
|
||||||
if close_enough && (aggro <= 0.5 || (aggro <= 0.7 && sound_was_loud)) {
|
if close_enough
|
||||||
|
&& (flee_health <= 0.7 || (flee_health <= 0.5 && sound_was_loud))
|
||||||
|
{
|
||||||
self.flee(agent, controller, &read_data.terrain, &sound_pos);
|
self.flee(agent, controller, &read_data.terrain, &sound_pos);
|
||||||
} else {
|
} else {
|
||||||
self.idle(agent, controller, read_data);
|
self.idle(agent, controller, read_data);
|
||||||
@ -4048,11 +4113,11 @@ impl<'a> AgentData<'a> {
|
|||||||
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.target = build_target(attacker, true, read_data.time.0);
|
agent.target = build_target(attacker, true, read_data.time.0, true);
|
||||||
|
|
||||||
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
||||||
if should_stop_attacking(attacker, read_data) {
|
if should_stop_attacking(attacker, read_data) {
|
||||||
agent.target = build_target(target, false, read_data.time.0);
|
agent.target = build_target(target, false, read_data.time.0, false);
|
||||||
|
|
||||||
self.idle(agent, controller, read_data);
|
self.idle(agent, controller, read_data);
|
||||||
} else {
|
} else {
|
||||||
@ -4154,11 +4219,11 @@ impl<'a> AgentData<'a> {
|
|||||||
fn chat_general_if_can_speak(
|
fn chat_general_if_can_speak(
|
||||||
&self,
|
&self,
|
||||||
agent: &Agent,
|
agent: &Agent,
|
||||||
msg: String,
|
msg: impl ToString,
|
||||||
event_emitter: &mut Emitter<'_, ServerEvent>,
|
event_emitter: &mut Emitter<'_, ServerEvent>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if can_speak(agent) {
|
if can_speak(agent) {
|
||||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
self.chat_general(msg, event_emitter);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@ -4177,8 +4242,11 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chat_general(&self, msg: String, event_emitter: &mut Emitter<'_, ServerEvent>) {
|
fn chat_general(&self, msg: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) {
|
||||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
||||||
|
*self.uid,
|
||||||
|
msg.to_string(),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_villager_alarm(&self, time: f64, event_emitter: &mut Emitter<'_, ServerEvent>) {
|
fn emit_villager_alarm(&self, time: f64, event_emitter: &mut Emitter<'_, ServerEvent>) {
|
||||||
@ -4304,11 +4372,12 @@ fn entity_was_attacked(entity: EcsEntity, read_data: &ReadData) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_target(target: EcsEntity, is_hostile: bool, time: f64) -> Option<Target> {
|
fn build_target(target: EcsEntity, is_hostile: bool, time: f64, aggro_on: bool) -> Option<Target> {
|
||||||
Some(Target {
|
Some(Target {
|
||||||
target,
|
target,
|
||||||
hostile: is_hostile,
|
hostile: is_hostile,
|
||||||
selected_at: time,
|
selected_at: time,
|
||||||
|
aggro_on,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +265,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
let poise = comp::Poise::new(body);
|
let poise = comp::Poise::new(body);
|
||||||
|
|
||||||
let can_speak = match body {
|
let can_speak = match body {
|
||||||
comp::Body::Humanoid(_) => alignment == comp::Alignment::Npc,
|
comp::Body::Humanoid(_) => true,
|
||||||
comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
|
comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
|
||||||
// Parrots like to have a word in this, too...
|
// Parrots like to have a word in this, too...
|
||||||
bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
|
bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
|
||||||
|
Loading…
Reference in New Issue
Block a user