Agent code refactoring

0) extracting code branches to closures
1) make RTSim remember target, even if they can't speak about it
2) make only RTSim Ships constantly flying (and not every flying RTSim)

You can tame RTSim Phoenix, yay
This commit is contained in:
juliancoffee 2021-07-12 18:40:29 +03:00
parent 117035f47b
commit 59e9fc6530
3 changed files with 364 additions and 263 deletions

View File

@ -305,7 +305,7 @@ pub enum SoundKind {
VillagerAlarm,
}
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub struct Target {
pub target: EcsEntity,
pub hostile: bool,

View File

@ -528,10 +528,18 @@ impl Brain {
}
pub fn remembers_character(&self, name_to_remember: &str) -> bool {
self.memories.iter().any(|memory| matches!(&memory.item, MemoryItem::CharacterInteraction { name, .. } if name == name_to_remember))
self.memories.iter().any(|memory| {
matches!(
&memory.item,
MemoryItem::CharacterInteraction { name, .. } if name == name_to_remember)
})
}
pub fn remembers_fight_with_character(&self, name_to_remember: &str) -> bool {
self.memories.iter().any(|memory| matches!(&memory.item, MemoryItem::CharacterFight { name, .. } if name == name_to_remember))
self.memories.iter().any(|memory| {
matches!(
&memory.item,
MemoryItem::CharacterFight { name, .. } if name == name_to_remember)
})
}
}

View File

@ -1,5 +1,3 @@
#![allow(clippy::branches_sharing_code)] // TODO: evaluate
use crate::rtsim::{Entity as RtSimData, RtSim};
use common::{
comp::{
@ -194,13 +192,13 @@ impl<'a> System<'a> for Sys {
const ORIGIN: Origin = Origin::Server;
const PHASE: Phase = Phase::Create;
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
fn run(
job: &mut Job<Self>,
(read_data, event_bus, mut agents, mut controllers, mut rtsim): Self::SystemData,
) {
let rtsim = &mut *rtsim;
job.cpu_stats.measure(ParMode::Rayon);
(
&read_data.entities,
(&read_data.energies, read_data.healths.maybe()),
@ -246,33 +244,36 @@ impl<'a> System<'a> for Sys {
agent,
controller,
light_emitter,
groups,
group,
_,
char_state,
)| {
//// Hack, replace with better system when groups are more sophisticated
//// Override alignment if in a group unless entity is owned already
let alignment = if !matches!(
// Hack, replace with better system when groups are more sophisticated
// Override alignment if in a group unless entity is owned already
let alignment = if matches!(
&read_data.alignments.get(entity),
&Some(Alignment::Owned(_))
) {
groups
read_data.alignments.get(entity).copied()
} else {
group
.and_then(|g| read_data.group_manager.group_info(*g))
.and_then(|info| read_data.uids.get(info.leader))
.copied()
.map(Alignment::Owned)
.or(read_data.alignments.get(entity).copied())
} else {
read_data.alignments.get(entity).copied()
.map_or_else(
|| read_data.alignments.get(entity).copied(),
|uid| Some(Alignment::Owned(uid)),
)
};
controller.reset();
let mut event_emitter = event_bus.emitter();
let event_emitter = event_bus.emitter();
// Default to looking in orientation direction (can be overridden below)
// Default to looking in orientation direction
// (can be overridden below)
controller.inputs.look_dir = ori.look_dir();
let scale = read_data.scales.get(entity).map(|s| s.0).unwrap_or(1.0);
let scale = read_data.scales.get(entity).map_or(1.0, |Scale(s)| *s);
let glider_equipped = inventory
.equipped(EquipSlot::Glider)
@ -280,43 +281,49 @@ impl<'a> System<'a> for Sys {
.map_or(false, |item| {
matches!(item.kind(), comp::item::ItemKind::Glider(_))
});
let is_gliding = matches!(
read_data.char_states.get(entity),
Some(CharacterState::GlideWield) | Some(CharacterState::Glide(_))
Some(CharacterState::GlideWield | CharacterState::Glide(_))
) && physics_state.on_ground.is_none();
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.add_measurement(read_data.time.0, pos.0);
}
// This controls how picky NPCs are about their pathfinding. Giants are larger
// and so can afford to be less precise when trying to move around
// the world (especially since they would otherwise get stuck on
// This controls how picky NPCs are about their pathfinding.
// Giants are larger and so can afford to be less precise
// when trying to move around the world
// (especially since they would otherwise get stuck on
// obstacles that smaller entities would not).
let node_tolerance = scale * 1.5;
let slow_factor = body.map(|b| b.base_accel() / 250.0).unwrap_or(0.0).min(1.0);
let slow_factor = body.map_or(0.0, |b| b.base_accel() / 250.0).min(1.0);
let traversal_config = TraversalConfig {
node_tolerance,
slow_factor,
on_ground: physics_state.on_ground.is_some(),
in_liquid: physics_state.in_liquid().is_some(),
min_tgt_dist: 1.0,
can_climb: body.map(|b| b.can_climb()).unwrap_or(false),
can_fly: body.map(|b| b.fly_thrust().is_some()).unwrap_or(false),
can_climb: body.map_or(false, Body::can_climb),
can_fly: body.map_or(false, |b| b.fly_thrust().is_some()),
};
let flees = alignment
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
.unwrap_or(true);
let damage = health.map_or(1.0, |h| h.current() as f32 / h.maximum() as f32);
let flees = alignment.map_or(true, |a| {
!matches!(a, Alignment::Enemy | Alignment::Owned(_))
});
let health_fraction = health.map_or(1.0, Health::fraction);
let rtsim_entity = read_data
.rtsim_entities
.get(entity)
.and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0));
if traversal_config.can_fly && rtsim_entity.is_some() {
// hack (kinda): Never turn off flight for rtsim entities that can fly
// at all, since it results in stuttering and falling back to the ground.
if traversal_config.can_fly
&& rtsim_entity.is_some()
&& matches!(body, Some(Body::Ship(_)))
{
// hack (kinda): Never turn off flight for rtsim entities
// that can fly at all,
// since it results in stuttering and falling back to the ground.
controller
.actions
.push(ControlAction::basic_input(InputKind::Fly));
@ -339,7 +346,7 @@ impl<'a> System<'a> for Sys {
traversal_config,
scale,
flees,
damage,
damage: health_fraction,
light_emitter,
glider_equipped,
is_gliding,
@ -347,7 +354,6 @@ impl<'a> System<'a> for Sys {
char_state,
cached_spatial_grid: &read_data.cached_spatial_grid,
};
///////////////////////////////////////////////////////////
// Behavior tree
///////////////////////////////////////////////////////////
@ -367,151 +373,133 @@ impl<'a> System<'a> for Sys {
// are the only parts of this tree that should provide
// inputs.
// If falling fast and can glide, save yourself!
if data.glider_equipped && data.physics_state.on_ground.is_none() {
// toggle glider when vertical velocity is above some threshold (here ~
// glider fall vertical speed)
data.glider_fall(agent, controller, &read_data);
} else if let Some(Target {
target, hostile, ..
}) = agent.target
{
if let Some(tgt_health) = read_data.healths.get(target) {
// If the target is hostile (either based on alignment or if
// the target just attacked
if !tgt_health.is_dead {
if hostile {
let idle = |agent: &mut Agent, controller, mut event_emitter| {
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
};
let relax = |agent: &mut Agent, controller, event_emitter| {
agent.target = None;
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_to_target =
|agent: &mut Agent,
target: EcsEntity,
hostile: bool,
controller,
mut event_emitter| {
if let Some(tgt_health) = read_data.healths.get(target) {
// If target is dead, leave it
if tgt_health.is_dead {
relax(agent, controller, event_emitter);
// If the target is hostile
// (either based on alignment or if
// the target just attacked)
} else if hostile {
data.hostile_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
// Target is something worth following methinks
// Target is something worth following
// methinks
} else if let Some(Alignment::Owned(_)) = data.alignment {
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,
);
} else if entity_was_attacked(target, &read_data) {
data.attack_agents_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 {
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
}
}
react_as_pet(agent, target, controller, event_emitter);
} else {
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
idle(agent, controller, event_emitter);
}
} else {
agent.target = None;
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
relax(agent, controller, event_emitter);
}
} else {
agent.target = None;
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
}
} else {
// Target an entity that's attacking us if the attack was recent and we
// have a health component
match health {
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => {
// TODO: Can most of this match be replaced by the function
// `attack_agents_attacker`?
if let Some(by) = health.last_change.1.cause.damage_by() {
if let Some(attacker) = get_entity_by_id(by.id(), &read_data) {
if let Some(tgt_pos) = read_data.positions.get(attacker) {
if should_stop_attacking(attacker, &read_data) {
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
} else {
if agent.target.is_none() {
controller.push_event(ControlEvent::Utterance(
UtteranceKind::Angry,
));
}
};
agent.target =
build_target(attacker, true, read_data.time.0);
let target_data = build_target_data(
attacker, tgt_pos, &read_data,
);
data.attack(
agent,
controller,
&target_data,
&read_data,
);
// Remember this encounter if an RtSim entity
if let Some(tgt_stats) = data
.rtsim_entity
.and_then(|_| read_data.stats.get(attacker))
{
if data.rtsim_entity.is_some() {
remember_fight(
agent,
tgt_stats.name.clone(),
read_data.time.0,
);
}
}
}
} else {
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut 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_then(|_| read_data.stats.get(attacker))
{
rtsim_new_enemy(&tgt_stats.name, agent, &read_data);
}
} else {
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
relax(agent, controller, event_emitter);
}
}
};
// If falling fast and can glide, save yourself!
if data.glider_equipped && data.physics_state.on_ground.is_none() {
// toggle glider when vertical velocity is above some
// threshold (here ~ glider fall vertical speed)
data.glider_fall(agent, controller, &read_data);
} else if let Some(target_info) = agent.target {
let Target {
target, hostile, ..
} = target_info;
react_to_target(agent, target, hostile, controller, event_emitter);
} else {
// Target an entity that's attacking us if the attack
// was recent and we have a health component
match health {
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => {
if let Some(by) = health.last_change.1.cause.damage_by() {
react_to_damage(agent, by, controller, event_emitter);
} else {
relax(agent, controller, event_emitter);
}
},
_ => {
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
idle(agent, controller, event_emitter);
},
}
}
@ -526,12 +514,12 @@ impl<'a> System<'a> for Sys {
for event in core::mem::take(&mut agent.rtsim_controller.events) {
match event {
RtSimEvent::AddMemory(memory) => {
rtsim.insert_entity_memory(rtsim_entity.0, memory.clone())
rtsim.insert_entity_memory(rtsim_entity.0, memory.clone());
},
RtSimEvent::SetMood(memory) => {
rtsim.set_entity_mood(rtsim_entity.0, memory.clone())
rtsim.set_entity_mood(rtsim_entity.0, memory.clone());
},
_ => {},
RtSimEvent::PrintMemories => {},
}
}
}
@ -658,7 +646,7 @@ impl<'a> AgentData<'a> {
// If the hostile entity is dead or has an invulnerability buff (eg, those
// applied in safezones), return to idle
if should_stop_attacking(target, read_data) {
if agent.behavior.can(BehaviorCapability::SPEAK) {
if can_speak(agent) {
let msg = "npc.speech.villager_enemy_killed".to_string();
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
@ -1372,92 +1360,205 @@ impl<'a> AgentData<'a> {
) {
agent.action_state.timer = 0.0;
let target = self.cached_spatial_grid.0
.in_circle_aabr(self.pos.0.xy(), SEARCH_DIST)
.filter_map(|entity| {
read_data.positions
.get(entity)
.and_then(|l| read_data.healths.get(entity).map(|r| (l, r)))
.and_then(|l| read_data.stats.get(entity).map(|r| (l, r)))
.and_then(|l| read_data.inventories.get(entity).map(|r| (l, r)))
.map(|(((pos, health), stats), inventory)| {
(entity, pos, health, stats, inventory, read_data.alignments.get(entity), read_data.char_states.get(entity))
})
})
.filter(|(e, e_pos, e_health, _e_stats, _e_inventory, _e_alignment, char_state)| {
// Filter based on sight and hearing
let worth_choosing = |entity| {
read_data
.positions
.get(entity)
.and_then(|pos| read_data.healths.get(entity).map(|h| (pos, h)))
.and_then(|(pos, health)| {
read_data
.stats
.get(entity)
.map(|stats| (pos, health, stats))
})
.and_then(|(pos, health, stats)| {
read_data
.inventories
.get(entity)
.map(|inventory| (pos, health, stats, inventory))
})
.map(|(pos, health, stats, inventory)| {
(
entity,
pos,
health,
stats,
inventory,
read_data.alignments.get(entity),
read_data.char_states.get(entity),
)
})
};
let in_search_area = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
let mut search_dist = SEARCH_DIST;
let mut listen_dist = MAX_LISTEN_DIST;
if char_state.map_or(false, |c_s| c_s.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
search_dist *= SNEAK_COEFFICIENT;
};
e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2)
};
let within_view = |e_pos: &Pos| {
(e_pos.0 - self.pos.0)
.try_normalized()
.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 mut listen_dist = MAX_LISTEN_DIST;
if e_char_state.map_or(false, CharacterState::is_stealthy) {
// TODO: make sneak more effective based on a stat like e_stats.fitness
listen_dist *= SNEAK_COEFFICIENT;
}
((self.within_range_of(search_dist, e_pos.0) && self.within_view_angle(e_pos.0, controller)) || self.within_range_of(listen_dist, e_pos.0)) // TODO implement proper sound system for agents
&& e != self.entity
&& !e_health.is_dead
&& !invulnerability_is_in_buffs(read_data.buffs.get(*e))
})
.filter_map(|(e, e_pos, e_health, e_stats, e_inventory, e_alignment, _char_state)| {
// Hostile entities
try_owner_alignment(self.alignment, read_data).and_then(|a| try_owner_alignment(e_alignment, read_data).map(|b| a.hostile_towards(*b))).unwrap_or(false).then(|| (e, e_pos))
.or({
// I'm a guard and a villager is in distress
let other_is_npc = matches!(e_alignment, Some(Alignment::Npc));
let remembers_damage = e_health.last_change.0 < 5.0;//DAMAGE_MEMORY_DURATION;
read_data.stats.get(*self.entity).map_or(false, |stats| stats.name == "Guard" && other_is_npc && remembers_damage)
.then(|| {
e_health.last_change.1.cause.damage_by()
})
.flatten()
.and_then(|attacker_uid| {
get_entity_by_id(attacker_uid.id(), read_data)
})
.and_then(|attacker| {
read_data
.positions
.get(attacker)
.map(|a_pos| (attacker, a_pos))
})
}).or({
// I remember fighting this entity
self.rtsim_entity.and_then(|rtsim_entity| {
rtsim_entity.brain.remembers_fight_with_character(&e_stats.name).then(|| {
if can_speak(agent) {
remember_fight(agent, e_stats.name.clone(), read_data.time.0);
let msg = format!("{}! How dare you cross me again!", e_stats.name.clone());
self.chat_general(msg, event_emitter);
}
(e, e_pos)
})
})
}).or({
// Cultists!
self.alignment.and_then(|alignment| {
(matches!(alignment, Alignment::Npc) && e_inventory.equipped_items().filter(|item| item.tags().contains(&ItemTag::Cultist)).count() > 2).then(|| {
if can_speak(agent) {
if self.rtsim_entity.is_some() {
remember_fight(agent, e_stats.name.clone(), read_data.time.0);
}
let msg = "npc.speech.villager_cultist_alarm".to_string();
self.chat_general(msg, event_emitter);
self.emit_villager_alarm(read_data.time.0, event_emitter);
}
(e, e_pos)
})
})
// TODO implement proper sound system for agents
e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)
};
let within_reach = |e_pos: &Pos, e_char_state: Option<&CharacterState>| {
(in_search_area(e_pos, e_char_state) && within_view(e_pos))
|| in_listen_area(e_pos, e_char_state)
};
let owners_hostile = |e_alignment: Option<&Alignment>| {
try_owner_alignment(self.alignment, read_data).map_or(false, |owner_alignment| {
try_owner_alignment(e_alignment, read_data).map_or(false, |e_owner_alignment| {
owner_alignment.hostile_towards(*e_owner_alignment)
})
})
// Can we even see them?
.filter(|(_e, e_pos)| {
read_data.terrain
};
let guard_duty = |e_health: &Health, e_alignment: Option<&Alignment>| {
// I'm a guard and a villager is in distress
let other_is_npc = matches!(e_alignment, Some(Alignment::Npc));
let remembers_damage = e_health.last_change.0 < 5.0;
let need_help = read_data.stats.get(*self.entity).map_or(false, |stats| {
stats.name == "Guard" && other_is_npc && remembers_damage
});
let attacker_of = |health: &Health| health.last_change.1.cause.damage_by();
need_help
.then(|| {
attacker_of(e_health)
.and_then(|attacker_uid| get_entity_by_id(attacker_uid.id(), read_data))
.and_then(|attacker| {
read_data
.positions
.get(attacker)
.map(|a_pos| (attacker, *a_pos))
})
})
.flatten()
};
let rtsim_remember =
|target_stats: &Stats,
agent: &mut Agent,
event_emitter: &mut Emitter<'_, ServerEvent>| {
if let Some(rtsim_entity) = &self.rtsim_entity {
if rtsim_entity
.brain
.remembers_fight_with_character(&target_stats.name)
{
rtsim_new_enemy(&target_stats.name, agent, read_data);
if can_speak(agent) {
let message = format!(
"{}! How dare you cross me again!",
target_stats.name.clone()
);
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, message,
)));
}
true
} else {
false
}
} else {
false
}
};
let npc_sees_cultist =
|target_stats: &Stats,
target_inventory: &Inventory,
agent: &mut Agent,
event_emitter: &mut Emitter<'_, ServerEvent>| {
self.alignment.map_or(false, |alignment| {
if matches!(alignment, Alignment::Npc)
&& target_inventory
.equipped_items()
.filter(|item| item.tags().contains(&ItemTag::Cultist))
.count()
> 2
{
if self.rtsim_entity.is_some() {
rtsim_new_enemy(&target_stats.name, agent, read_data);
}
if can_speak(agent) {
let message = "npc.speech.villager_cultist_alarm".to_string();
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, message,
)));
}
true
} else {
false
}
})
};
let possible_target =
|(entity, e_pos, e_health, e_stats, e_inventory, e_alignment, e_char_state): (
EcsEntity,
&Pos,
&Health,
&Stats,
&Inventory,
Option<&Alignment>,
Option<&CharacterState>,
)| {
let can_target = within_reach(e_pos, e_char_state)
&& entity != *self.entity
&& !e_health.is_dead
&& !invulnerability_is_in_buffs(read_data.buffs.get(entity));
if !can_target {
None
} else if owners_hostile(e_alignment) {
Some((entity, *e_pos))
} else if let Some(villain_info) = guard_duty(e_health, e_alignment) {
Some(villain_info)
} else if rtsim_remember(e_stats, agent, event_emitter) {
Some((entity, *e_pos))
} else if npc_sees_cultist(e_stats, e_inventory, agent, event_emitter) {
Some((entity, *e_pos))
} else {
None
}
};
let can_see_them = |e_pos: &Pos| {
read_data
.terrain
.ray(self.pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z())
.until(Block::is_opaque)
.cast()
.0 >= e_pos.0.distance(self.pos.0)
})
.min_by_key(|(_e, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32) // TODO choose target by more than just distance
.map(|(e, _e_pos)| e);
.0
>= e_pos.0.distance(self.pos.0)
};
// Search area
// TODO choose target by more than just distance
let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
let target = grid
.in_circle_aabr(self.pos.0.xy(), SEARCH_DIST)
.filter_map(worth_choosing)
.filter_map(possible_target)
.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)
.map(|(e, _)| e);
if agent.target.is_none() && target.is_some() {
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
@ -2548,6 +2649,7 @@ impl<'a> AgentData<'a> {
}
}
#[allow(clippy::branches_sharing_code)] //TODO: evaluate
fn handle_quadlow_ranged_attack(
&self,
agent: &mut Agent,
@ -3842,7 +3944,7 @@ impl<'a> AgentData<'a> {
}
}
fn attack_agents_attacker(
fn attack_target_attacker(
&self,
agent: &mut Agent,
read_data: &ReadData,
@ -3955,17 +4057,18 @@ impl<'a> AgentData<'a> {
sound: Sound::new(SoundKind::VillagerAlarm, self.pos.0, 100.0, time),
});
}
}
fn within_view_angle(&self, tgt_pos: Vec3<f32>, controller: &Controller) -> bool {
(tgt_pos - self.pos.0)
.try_normalized()
.map(|v| v.dot(*controller.inputs.look_dir) > 0.15)
.unwrap_or(true)
}
fn within_range_of(&self, range: f32, their_pos: Vec3<f32>) -> bool {
their_pos.distance_squared(self.pos.0) < range.powi(2)
}
fn rtsim_new_enemy(target_name: &str, agent: &mut Agent, read_data: &ReadData) {
agent
.rtsim_controller
.events
.push(RtSimEvent::AddMemory(Memory {
item: MemoryItem::CharacterFight {
name: target_name.to_owned(),
},
time_to_forget: read_data.time.0 + 300.0,
}));
}
fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool {
@ -4087,16 +4190,6 @@ fn build_target_data<'a>(
fn can_speak(agent: &Agent) -> bool { agent.behavior.can(BehaviorCapability::SPEAK) }
fn remember_fight(agent: &mut Agent, name: String, time: f64) {
agent
.rtsim_controller
.events
.push(RtSimEvent::AddMemory(Memory {
item: MemoryItem::CharacterFight { name },
time_to_forget: time + 300.0,
}));
}
fn get_entity_by_id(id: u64, read_data: &ReadData) -> Option<EcsEntity> {
read_data.uid_allocator.retrieve_entity_internal(id)
}