mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Make Guards Defend Villagers
This commit is contained in:
parent
35d8306c7a
commit
f316f1da60
@ -301,6 +301,8 @@ pub enum SoundKind {
|
|||||||
Beam,
|
Beam,
|
||||||
Shockwave,
|
Shockwave,
|
||||||
Utterance(UtteranceKind, Body),
|
Utterance(UtteranceKind, Body),
|
||||||
|
// TODO: unify VillagerAlarm with Utterance
|
||||||
|
VillagerAlarm,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -34,6 +34,16 @@ pub enum HealthSource {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HealthSource {
|
||||||
|
pub fn damage_by(&self) -> Option<Uid> {
|
||||||
|
if let HealthSource::Damage { by, .. } = self {
|
||||||
|
*by
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Health {
|
pub struct Health {
|
||||||
current: u32,
|
current: u32,
|
||||||
|
@ -91,6 +91,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
};
|
};
|
||||||
let end_time = creation_time + beam_segment.duration.as_secs_f64();
|
let end_time = creation_time + beam_segment.duration.as_secs_f64();
|
||||||
|
|
||||||
|
let beam_owner = beam_segment
|
||||||
|
.owner
|
||||||
|
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()));
|
||||||
|
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
if rng.gen_bool(0.005) {
|
if rng.gen_bool(0.005) {
|
||||||
server_events.push(ServerEvent::Sound {
|
server_events.push(ServerEvent::Sound {
|
||||||
@ -119,10 +123,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
(beam_segment.speed * (time_since_creation - frame_time)).max(0.0);
|
(beam_segment.speed * (time_since_creation - frame_time)).max(0.0);
|
||||||
let frame_end_dist = (beam_segment.speed * time_since_creation).max(frame_start_dist);
|
let frame_end_dist = (beam_segment.speed * time_since_creation).max(frame_start_dist);
|
||||||
|
|
||||||
let beam_owner = beam_segment
|
|
||||||
.owner
|
|
||||||
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()));
|
|
||||||
|
|
||||||
// Group to ignore collisions with
|
// Group to ignore collisions with
|
||||||
// Might make this more nuanced if beams are used for non damage effects
|
// Might make this more nuanced if beams are used for non damage effects
|
||||||
let group = beam_owner.and_then(|e| read_data.groups.get(e));
|
let group = beam_owner.and_then(|e| read_data.groups.get(e));
|
||||||
|
@ -72,6 +72,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
{
|
{
|
||||||
|
let projectile_owner = projectile
|
||||||
|
.owner
|
||||||
|
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()));
|
||||||
|
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
if physics.on_surface().is_none() && rng.gen_bool(0.05) {
|
if physics.on_surface().is_none() && rng.gen_bool(0.05) {
|
||||||
server_emitter.emit(ServerEvent::Sound {
|
server_emitter.emit(ServerEvent::Sound {
|
||||||
@ -83,12 +87,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
|
|
||||||
// Hit entity
|
// Hit entity
|
||||||
for other in physics.touch_entities.iter().copied() {
|
for other in physics.touch_entities.iter().copied() {
|
||||||
let same_group = projectile
|
let same_group = projectile_owner
|
||||||
.owner
|
|
||||||
// Note: somewhat inefficient since we do the lookup for every touching
|
// Note: somewhat inefficient since we do the lookup for every touching
|
||||||
// entity, but if we pull this out of the loop we would want to do it only
|
// entity, but if we pull this out of the loop we would want to do it only
|
||||||
// if there is at least one touching entity
|
// if there is at least one touching entity
|
||||||
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()))
|
|
||||||
.and_then(|e| read_data.groups.get(e))
|
.and_then(|e| read_data.groups.get(e))
|
||||||
.map_or(false, |owner_group|
|
.map_or(false, |owner_group|
|
||||||
Some(owner_group) == read_data.uid_allocator
|
Some(owner_group) == read_data.uid_allocator
|
||||||
|
@ -85,6 +85,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
|
|
||||||
let end_time = creation_time + shockwave.duration.as_secs_f64();
|
let end_time = creation_time + shockwave.duration.as_secs_f64();
|
||||||
|
|
||||||
|
let shockwave_owner = shockwave
|
||||||
|
.owner
|
||||||
|
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()));
|
||||||
|
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
if rng.gen_bool(0.05) {
|
if rng.gen_bool(0.05) {
|
||||||
server_emitter.emit(ServerEvent::Sound {
|
server_emitter.emit(ServerEvent::Sound {
|
||||||
@ -120,10 +124,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
end: frame_end_dist,
|
end: frame_end_dist,
|
||||||
};
|
};
|
||||||
|
|
||||||
let shockwave_owner = shockwave
|
|
||||||
.owner
|
|
||||||
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()));
|
|
||||||
|
|
||||||
// Group to ignore collisions with
|
// Group to ignore collisions with
|
||||||
// Might make this more nuanced if shockwaves are used for non damage effects
|
// Might make this more nuanced if shockwaves are used for non damage effects
|
||||||
let group = shockwave_owner.and_then(|e| read_data.groups.get(e));
|
let group = shockwave_owner.and_then(|e| read_data.groups.get(e));
|
||||||
|
@ -692,6 +692,10 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
|
|||||||
let ecs = &server.state.ecs();
|
let ecs = &server.state.ecs();
|
||||||
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
|
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
|
||||||
let time = ecs.read_resource::<Time>();
|
let time = ecs.read_resource::<Time>();
|
||||||
|
let owner_entity = owner.and_then(|uid| {
|
||||||
|
ecs.read_resource::<UidAllocator>()
|
||||||
|
.retrieve_entity_internal(uid.into())
|
||||||
|
});
|
||||||
|
|
||||||
let explosion_volume = 2.5 * explosion.radius;
|
let explosion_volume = 2.5 * explosion.radius;
|
||||||
server_eventbus.emit_now(ServerEvent::Sound {
|
server_eventbus.emit_now(ServerEvent::Sound {
|
||||||
@ -712,10 +716,6 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
|
|||||||
.any(|e| matches!(e, RadiusEffect::Attack(_))),
|
.any(|e| matches!(e, RadiusEffect::Attack(_))),
|
||||||
reagent: explosion.reagent,
|
reagent: explosion.reagent,
|
||||||
});
|
});
|
||||||
let owner_entity = owner.and_then(|uid| {
|
|
||||||
ecs.read_resource::<UidAllocator>()
|
|
||||||
.retrieve_entity_internal(uid.into())
|
|
||||||
});
|
|
||||||
let groups = ecs.read_storage::<comp::Group>();
|
let groups = ecs.read_storage::<comp::Group>();
|
||||||
|
|
||||||
// Used to get strength of explosion effects as they falloff over distance
|
// Used to get strength of explosion effects as they falloff over distance
|
||||||
|
@ -5,7 +5,8 @@ use common::{
|
|||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
agent::{
|
agent::{
|
||||||
AgentEvent, Target, DEFAULT_INTERACTION_TIME, MAX_LISTEN_DIST, TRADE_INTERACTION_TIME,
|
AgentEvent, Sound, SoundKind, Target, DEFAULT_INTERACTION_TIME, MAX_LISTEN_DIST,
|
||||||
|
TRADE_INTERACTION_TIME,
|
||||||
},
|
},
|
||||||
buff::{BuffKind, Buffs},
|
buff::{BuffKind, Buffs},
|
||||||
compass::{Direction, Distance},
|
compass::{Direction, Distance},
|
||||||
@ -399,66 +400,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
&read_data.terrain,
|
&read_data.terrain,
|
||||||
tgt_pos,
|
tgt_pos,
|
||||||
);
|
);
|
||||||
// Attack target's attacker
|
} else if entity_was_attacked(target, &read_data) {
|
||||||
} else if tgt_health.last_change.0 < 5.0
|
data.attack_agents_attacker(
|
||||||
&& tgt_health.last_change.1.amount < 0
|
agent, &read_data, controller,
|
||||||
{
|
);
|
||||||
if let comp::HealthSource::Damage {
|
|
||||||
by: Some(by), ..
|
|
||||||
} = tgt_health.last_change.1.cause
|
|
||||||
{
|
|
||||||
if let Some(attacker) = read_data
|
|
||||||
.uid_allocator
|
|
||||||
.retrieve_entity_internal(by.id())
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
if let Some(tgt_pos) =
|
|
||||||
read_data.positions.get(attacker)
|
|
||||||
{
|
|
||||||
if should_stop_attacking(
|
|
||||||
read_data.healths.get(attacker),
|
|
||||||
read_data.buffs.get(attacker),
|
|
||||||
) {
|
|
||||||
agent.target = Some(Target {
|
|
||||||
target,
|
|
||||||
hostile: false,
|
|
||||||
selected_at: read_data.time.0,
|
|
||||||
});
|
|
||||||
data.idle(
|
|
||||||
agent, controller, &read_data,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Follow owner if too far away and not
|
// Follow owner if too far away and not
|
||||||
// fighting
|
// fighting
|
||||||
} else if dist_sqrd > MAX_FOLLOW_DIST.powi(2) {
|
} else if dist_sqrd > MAX_FOLLOW_DIST.powi(2) {
|
||||||
@ -500,19 +445,12 @@ impl<'a> System<'a> for Sys {
|
|||||||
// have a health component
|
// 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 comp::HealthSource::Damage { by: Some(by), .. } =
|
// TODO: Can most of this match be replaced by the function
|
||||||
health.last_change.1.cause
|
// `attack_agents_attacker`?
|
||||||
{
|
if let Some(by) = health.last_change.1.cause.damage_by() {
|
||||||
if let Some(attacker) =
|
if let Some(attacker) = get_entity_by_id(by.id(), &read_data) {
|
||||||
read_data.uid_allocator.retrieve_entity_internal(by.id())
|
|
||||||
{
|
|
||||||
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
||||||
// If the target is dead or in a safezone, remove the
|
if should_stop_attacking(attacker, &read_data) {
|
||||||
// target and idle.
|
|
||||||
if should_stop_attacking(
|
|
||||||
read_data.healths.get(attacker),
|
|
||||||
read_data.buffs.get(attacker),
|
|
||||||
) {
|
|
||||||
agent.target = None;
|
agent.target = None;
|
||||||
data.idle_tree(
|
data.idle_tree(
|
||||||
agent,
|
agent,
|
||||||
@ -527,16 +465,11 @@ impl<'a> System<'a> for Sys {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.target = Some(Target {
|
agent.target =
|
||||||
target: attacker,
|
build_target(attacker, true, read_data.time.0);
|
||||||
hostile: true,
|
let target_data = build_target_data(
|
||||||
selected_at: read_data.time.0,
|
attacker, tgt_pos, &read_data,
|
||||||
});
|
);
|
||||||
let target_data = TargetData {
|
|
||||||
pos: tgt_pos,
|
|
||||||
body: read_data.bodies.get(attacker),
|
|
||||||
scale: read_data.scales.get(attacker),
|
|
||||||
};
|
|
||||||
data.attack(
|
data.attack(
|
||||||
agent,
|
agent,
|
||||||
controller,
|
controller,
|
||||||
@ -548,15 +481,13 @@ impl<'a> System<'a> for Sys {
|
|||||||
.rtsim_entity
|
.rtsim_entity
|
||||||
.and_then(|_| read_data.stats.get(attacker))
|
.and_then(|_| read_data.stats.get(attacker))
|
||||||
{
|
{
|
||||||
agent.rtsim_controller.events.push(
|
if data.rtsim_entity.is_some() {
|
||||||
RtSimEvent::AddMemory(Memory {
|
remember_fight(
|
||||||
item: MemoryItem::CharacterFight {
|
agent,
|
||||||
name: tgt_stats.name.clone(),
|
tgt_stats.name.clone(),
|
||||||
},
|
read_data.time.0,
|
||||||
time_to_forget: read_data.time.0
|
);
|
||||||
+ 300.0,
|
}
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -624,12 +555,8 @@ impl<'a> AgentData<'a> {
|
|||||||
// Set owner if no target
|
// Set owner if no target
|
||||||
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) = read_data.uid_allocator.retrieve_entity_internal(owner.id()) {
|
if let Some(owner) = get_entity_by_id(owner.id(), read_data) {
|
||||||
agent.target = Some(Target {
|
agent.target = build_target(owner, false, read_data.time.0);
|
||||||
target: owner,
|
|
||||||
hostile: false,
|
|
||||||
selected_at: read_data.time.0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,12 +638,10 @@ impl<'a> AgentData<'a> {
|
|||||||
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
|
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
|
||||||
// Should the agent flee?
|
// Should the agent flee?
|
||||||
if 1.0 - agent.psyche.aggro > self.damage && self.flees {
|
if 1.0 - agent.psyche.aggro > self.damage && self.flees {
|
||||||
if agent.action_state.timer == 0.0
|
if agent.action_state.timer == 0.0 && can_speak(agent) {
|
||||||
&& agent.behavior.can(BehaviorCapability::SPEAK)
|
|
||||||
{
|
|
||||||
let msg = "npc.speech.villager_under_attack".to_string();
|
let msg = "npc.speech.villager_under_attack".to_string();
|
||||||
event_emitter
|
self.chat_general(msg, event_emitter);
|
||||||
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
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_sqrd < MAX_FLEE_DIST
|
||||||
{
|
{
|
||||||
@ -732,16 +657,14 @@ impl<'a> AgentData<'a> {
|
|||||||
} else {
|
} else {
|
||||||
// If the hostile entity is dead or has an invulnerability buff (eg, those
|
// If the hostile entity is dead or has an invulnerability buff (eg, those
|
||||||
// applied in safezones), return to idle
|
// applied in safezones), return to idle
|
||||||
if should_stop_attacking(
|
if should_stop_attacking(target, read_data) {
|
||||||
read_data.healths.get(target),
|
|
||||||
read_data.buffs.get(target),
|
|
||||||
) {
|
|
||||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
||||||
let msg = "npc.speech.villager_enemy_killed".to_string();
|
let msg = "npc.speech.villager_enemy_killed".to_string();
|
||||||
event_emitter
|
event_emitter
|
||||||
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
||||||
}
|
}
|
||||||
agent.target = None;
|
agent.target = None;
|
||||||
|
self.idle(agent, controller, read_data);
|
||||||
// Choose a new target every 10 seconds, but only for
|
// Choose a new target every 10 seconds, but only for
|
||||||
// enemies
|
// enemies
|
||||||
// TODO: This should be more principled. Consider factoring
|
// TODO: This should be more principled. Consider factoring
|
||||||
@ -753,11 +676,7 @@ impl<'a> AgentData<'a> {
|
|||||||
self.choose_target(agent, controller, read_data, event_emitter);
|
self.choose_target(agent, controller, read_data, event_emitter);
|
||||||
} else {
|
} else {
|
||||||
// TODO Add utility for attacking vs leaving target alone
|
// TODO Add utility for attacking vs leaving target alone
|
||||||
let target_data = TargetData {
|
let target_data = build_target_data(target, tgt_pos, read_data);
|
||||||
pos: tgt_pos,
|
|
||||||
body: read_data.bodies.get(target),
|
|
||||||
scale: read_data.scales.get(target),
|
|
||||||
};
|
|
||||||
self.attack(agent, controller, &target_data, read_data);
|
self.attack(agent, controller, &target_data, read_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1021,14 +940,9 @@ impl<'a> AgentData<'a> {
|
|||||||
let msg = agent.inbox.pop_front();
|
let msg = agent.inbox.pop_front();
|
||||||
match msg {
|
match msg {
|
||||||
Some(AgentEvent::Talk(by, subject)) => {
|
Some(AgentEvent::Talk(by, subject)) => {
|
||||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
if can_speak(agent) {
|
||||||
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id())
|
if let Some(target) = get_entity_by_id(by.id(), read_data) {
|
||||||
{
|
agent.target = build_target(target, false, read_data.time.0);
|
||||||
agent.target = Some(Target {
|
|
||||||
target,
|
|
||||||
hostile: false,
|
|
||||||
selected_at: read_data.time.0,
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
||||||
@ -1074,19 +988,13 @@ impl<'a> AgentData<'a> {
|
|||||||
destination_name
|
destination_name
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
self.chat_general(msg, event_emitter);
|
||||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
|
||||||
));
|
|
||||||
} else if agent.behavior.can_trade() {
|
} else if agent.behavior.can_trade() {
|
||||||
let msg = "npc.speech.merchant_advertisement".to_string();
|
let msg = "npc.speech.merchant_advertisement".to_string();
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
self.chat_general(msg, event_emitter);
|
||||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
let msg = "npc.speech.villager".to_string();
|
let msg = "npc.speech.villager".to_string();
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
self.chat_general(msg, event_emitter);
|
||||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Subject::Trade => {
|
Subject::Trade => {
|
||||||
@ -1098,26 +1006,16 @@ impl<'a> AgentData<'a> {
|
|||||||
));
|
));
|
||||||
let msg =
|
let msg =
|
||||||
"npc.speech.merchant_advertisement".to_string();
|
"npc.speech.merchant_advertisement".to_string();
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
self.chat_general(msg, event_emitter);
|
||||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
let msg = "npc.speech.merchant_busy".to_string();
|
||||||
UnresolvedChatMsg::npc(
|
self.chat_general(msg, event_emitter);
|
||||||
*self.uid,
|
|
||||||
"npc.speech.merchant_busy".to_string(),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} 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
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
let msg = "npc.speech.villager_decline_trade".to_string();
|
||||||
UnresolvedChatMsg::npc(
|
self.chat_general(msg, event_emitter);
|
||||||
*self.uid,
|
|
||||||
"npc.speech.villager_decline_trade".to_string(),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Subject::Mood => {
|
Subject::Mood => {
|
||||||
@ -1166,33 +1064,21 @@ impl<'a> AgentData<'a> {
|
|||||||
MemoryItem::Mood { state } => state.describe(),
|
MemoryItem::Mood { state } => state.describe(),
|
||||||
_ => "".to_string(),
|
_ => "".to_string(),
|
||||||
};
|
};
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
self.chat_general(msg, event_emitter);
|
||||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Subject::Location(location) => {
|
Subject::Location(location) => {
|
||||||
if let Some(tgt_pos) = read_data.positions.get(target) {
|
if let Some(tgt_pos) = read_data.positions.get(target) {
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
let raw_dir = location.origin.as_::<f32>() - tgt_pos.0.xy();
|
||||||
UnresolvedChatMsg::npc(
|
let dist = Distance::from_dir(raw_dir).name();
|
||||||
*self.uid,
|
let dir = Direction::from_dir(raw_dir).name();
|
||||||
format!(
|
|
||||||
"{} ? I think it's {} {} from here!",
|
let msg = format!(
|
||||||
location.name,
|
"{} ? I think it's {} {} from here!",
|
||||||
Distance::from_dir(
|
location.name, dist, dir
|
||||||
location.origin.as_::<f32>()
|
);
|
||||||
- tgt_pos.0.xy()
|
self.chat_general(msg, event_emitter);
|
||||||
)
|
|
||||||
.name(),
|
|
||||||
Direction::from_dir(
|
|
||||||
location.origin.as_::<f32>()
|
|
||||||
- tgt_pos.0.xy()
|
|
||||||
)
|
|
||||||
.name()
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Subject::Person(person) => {
|
Subject::Person(person) => {
|
||||||
@ -1227,9 +1113,7 @@ impl<'a> AgentData<'a> {
|
|||||||
person.name()
|
person.name()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
self.chat_general(msg, event_emitter);
|
||||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Subject::Work => {},
|
Subject::Work => {},
|
||||||
@ -1244,14 +1128,8 @@ impl<'a> AgentData<'a> {
|
|||||||
// stand still and looking towards the trading player
|
// stand still and looking towards the trading player
|
||||||
controller.actions.push(ControlAction::Stand);
|
controller.actions.push(ControlAction::Stand);
|
||||||
controller.actions.push(ControlAction::Talk);
|
controller.actions.push(ControlAction::Talk);
|
||||||
if let Some(target) =
|
if let Some(target) = get_entity_by_id(with.id(), read_data) {
|
||||||
read_data.uid_allocator.retrieve_entity_internal(with.id())
|
agent.target = build_target(target, false, read_data.time.0);
|
||||||
{
|
|
||||||
agent.target = Some(Target {
|
|
||||||
target,
|
|
||||||
hostile: false,
|
|
||||||
selected_at: read_data.time.0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
controller
|
controller
|
||||||
.events
|
.events
|
||||||
@ -1262,36 +1140,22 @@ impl<'a> AgentData<'a> {
|
|||||||
controller
|
controller
|
||||||
.events
|
.events
|
||||||
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
||||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
let msg = "npc.speech.merchant_busy".to_string();
|
||||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
self.chat_general_if_can_speak(agent, msg, event_emitter);
|
||||||
*self.uid,
|
|
||||||
"npc.speech.merchant_busy".to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} 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));
|
||||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
let msg = "npc.speech.villager_decline_trade".to_string();
|
||||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
self.chat_general_if_can_speak(agent, msg, event_emitter);
|
||||||
*self.uid,
|
|
||||||
"npc.speech.villager_decline_trade".to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(AgentEvent::TradeAccepted(with)) => {
|
Some(AgentEvent::TradeAccepted(with)) => {
|
||||||
if !agent.behavior.is(BehaviorState::TRADING) {
|
if !agent.behavior.is(BehaviorState::TRADING) {
|
||||||
if let Some(target) =
|
if let Some(target) = get_entity_by_id(with.id(), read_data) {
|
||||||
read_data.uid_allocator.retrieve_entity_internal(with.id())
|
agent.target = build_target(target, false, read_data.time.0);
|
||||||
{
|
|
||||||
agent.target = Some(Target {
|
|
||||||
target,
|
|
||||||
hostile: false,
|
|
||||||
selected_at: read_data.time.0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
agent.behavior.set(BehaviorState::TRADING);
|
agent.behavior.set(BehaviorState::TRADING);
|
||||||
agent.behavior.set(BehaviorState::TRADING_ISSUER);
|
agent.behavior.set(BehaviorState::TRADING_ISSUER);
|
||||||
@ -1301,15 +1165,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 => {
|
||||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
let msg = "npc.speech.merchant_trade_successful".to_string();
|
||||||
*self.uid,
|
self.chat_general(msg, event_emitter);
|
||||||
"npc.speech.merchant_trade_successful".to_string(),
|
},
|
||||||
)))
|
_ => {
|
||||||
|
let msg = "npc.speech.merchant_trade_declined".to_string();
|
||||||
|
self.chat_general(msg, event_emitter);
|
||||||
},
|
},
|
||||||
_ => event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
|
||||||
*self.uid,
|
|
||||||
"npc.speech.merchant_trade_declined".to_string(),
|
|
||||||
))),
|
|
||||||
}
|
}
|
||||||
agent.behavior.unset(BehaviorState::TRADING);
|
agent.behavior.unset(BehaviorState::TRADING);
|
||||||
}
|
}
|
||||||
@ -1346,6 +1208,7 @@ impl<'a> AgentData<'a> {
|
|||||||
balance0 / balance1 * 100.0
|
balance0 / balance1 * 100.0
|
||||||
);
|
);
|
||||||
if let Some(tgt_data) = &agent.target {
|
if let Some(tgt_data) = &agent.target {
|
||||||
|
// If talking with someone in particular, "tell" it only to them
|
||||||
if let Some(with) = read_data.uids.get(tgt_data.target) {
|
if let Some(with) = read_data.uids.get(tgt_data.target) {
|
||||||
event_emitter.emit(ServerEvent::Chat(
|
event_emitter.emit(ServerEvent::Chat(
|
||||||
UnresolvedChatMsg::npc_tell(*self.uid, *with, msg),
|
UnresolvedChatMsg::npc_tell(*self.uid, *with, msg),
|
||||||
@ -1374,7 +1237,7 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
if can_speak(agent) {
|
||||||
// No new events, continue looking towards the last interacting player for some
|
// No new events, continue looking towards the last interacting player for some
|
||||||
// time
|
// time
|
||||||
if let Some(Target { target, .. }) = &agent.target {
|
if let Some(Target { target, .. }) = &agent.target {
|
||||||
@ -1509,7 +1372,6 @@ impl<'a> AgentData<'a> {
|
|||||||
) {
|
) {
|
||||||
agent.action_state.timer = 0.0;
|
agent.action_state.timer = 0.0;
|
||||||
|
|
||||||
// Search area
|
|
||||||
let target = self.cached_spatial_grid.0
|
let target = self.cached_spatial_grid.0
|
||||||
.in_circle_aabr(self.pos.0.xy(), SEARCH_DIST)
|
.in_circle_aabr(self.pos.0.xy(), SEARCH_DIST)
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
@ -1522,76 +1384,80 @@ impl<'a> AgentData<'a> {
|
|||||||
(entity, pos, health, stats, inventory, read_data.alignments.get(entity), read_data.char_states.get(entity))
|
(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(|(e, e_pos, e_health, _e_stats, _e_inventory, _e_alignment, char_state)| {
|
||||||
let mut search_dist = SEARCH_DIST;
|
// Filter based on sight and hearing
|
||||||
let mut listen_dist = MAX_LISTEN_DIST;
|
let mut search_dist = SEARCH_DIST;
|
||||||
if char_state.map_or(false, |c_s| c_s.is_stealthy()) {
|
let mut listen_dist = MAX_LISTEN_DIST;
|
||||||
// TODO: make sneak more effective based on a stat like e_stats.fitness
|
if char_state.map_or(false, |c_s| c_s.is_stealthy()) {
|
||||||
search_dist *= SNEAK_COEFFICIENT;
|
// TODO: make sneak more effective based on a stat like e_stats.fitness
|
||||||
listen_dist *= SNEAK_COEFFICIENT;
|
search_dist *= SNEAK_COEFFICIENT;
|
||||||
}
|
listen_dist *= SNEAK_COEFFICIENT;
|
||||||
((e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2) &&
|
}
|
||||||
// Within our view
|
((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_pos.0 - self.pos.0).try_normalized().map(|v| v.dot(*controller.inputs.look_dir) > 0.15).unwrap_or(true))
|
&& e != self.entity
|
||||||
// Within listen distance
|
&& !e_health.is_dead
|
||||||
|| e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)) // TODO implement proper sound system for agents
|
&& !invulnerability_is_in_buffs(read_data.buffs.get(*e))
|
||||||
&& e != self.entity
|
})
|
||||||
&& !e_health.is_dead
|
.filter_map(|(e, e_pos, e_health, e_stats, e_inventory, e_alignment, _char_state)| {
|
||||||
&& !invulnerability_is_in_buffs(read_data.buffs.get(*e))
|
// 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) || (
|
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))
|
||||||
if let Some(rtsim_entity) = &self.rtsim_entity {
|
.or({
|
||||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
// I'm a guard and a villager is in distress
|
||||||
if rtsim_entity.brain.remembers_fight_with_character(&e_stats.name) {
|
let other_is_npc = matches!(e_alignment, Some(Alignment::Npc));
|
||||||
agent.rtsim_controller.events.push(
|
let remembers_damage = e_health.last_change.0 < 5.0;//DAMAGE_MEMORY_DURATION;
|
||||||
RtSimEvent::AddMemory(Memory {
|
read_data.stats.get(*self.entity).map_or(false, |stats| stats.name == "Guard" && other_is_npc && remembers_damage)
|
||||||
item: MemoryItem::CharacterFight { name: e_stats.name.clone() },
|
.then(|| {
|
||||||
time_to_forget: read_data.time.0 + 300.0,
|
e_health.last_change.1.cause.damage_by()
|
||||||
})
|
|
||||||
);
|
|
||||||
let msg = format!("{}! How dare you cross me again!", e_stats.name.clone());
|
|
||||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
self.alignment.map_or(false, |alignment| {
|
|
||||||
if matches!(alignment, Alignment::Npc) && e_inventory.equipped_items().filter(|item| item.tags().contains(&ItemTag::Cultist)).count() > 2 {
|
|
||||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
|
||||||
if self.rtsim_entity.is_some() {
|
|
||||||
agent.rtsim_controller.events.push(
|
|
||||||
RtSimEvent::AddMemory(Memory {
|
|
||||||
item: MemoryItem::CharacterFight { name: e_stats.name.clone() },
|
|
||||||
time_to_forget: read_data.time.0 + 300.0,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let msg = "npc.speech.villager_cultist_alarm".to_string();
|
|
||||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
))
|
.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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
// Can we even see them?
|
// Can we even see them?
|
||||||
.filter(|(_, e_pos, _, _, _, _, _)| read_data.terrain
|
.filter(|(_e, e_pos)| {
|
||||||
|
read_data.terrain
|
||||||
.ray(self.pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z())
|
.ray(self.pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z())
|
||||||
.until(Block::is_opaque)
|
.until(Block::is_opaque)
|
||||||
.cast()
|
.cast()
|
||||||
.0 >= e_pos.0.distance(self.pos.0))
|
.0 >= e_pos.0.distance(self.pos.0)
|
||||||
.min_by_key(|(_, 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);
|
.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);
|
||||||
|
|
||||||
if agent.target.is_none() && target.is_some() {
|
if agent.target.is_none() && target.is_some() {
|
||||||
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
||||||
@ -3937,33 +3803,74 @@ impl<'a> AgentData<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
|
|
||||||
|
|
||||||
if let Some(sound) = agent.sounds_heard.last() {
|
if let Some(sound) = agent.sounds_heard.last() {
|
||||||
let sound_pos = Pos(sound.pos);
|
// FIXME: Perhaps name should be a field of the agent, not stats
|
||||||
let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
|
if let Some(agent_stats) = read_data.stats.get(*self.entity) {
|
||||||
|
let sound_pos = Pos(sound.pos);
|
||||||
|
let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
|
||||||
|
|
||||||
if is_enemy {
|
let is_village_guard = agent_stats.name == *"Guard".to_string();
|
||||||
let far_enough = dist_sqrd > 10.0_f32.powi(2);
|
let is_neutral = self.flees && !is_village_guard;
|
||||||
|
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
|
||||||
|
|
||||||
if far_enough {
|
if is_enemy {
|
||||||
|
let far_enough = dist_sqrd > 10.0_f32.powi(2);
|
||||||
|
|
||||||
|
if far_enough {
|
||||||
|
self.follow(agent, controller, &read_data.terrain, &sound_pos);
|
||||||
|
} else {
|
||||||
|
// TODO: Change this to a search action instead of idle
|
||||||
|
self.idle(agent, controller, read_data);
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
let aggro = agent.psyche.aggro;
|
||||||
|
let close_enough = dist_sqrd < 35.0_f32.powi(2);
|
||||||
|
let sound_was_loud = sound.vol >= 10.0;
|
||||||
|
|
||||||
|
if close_enough && (aggro <= 0.5 || (aggro <= 0.7 && sound_was_loud)) {
|
||||||
|
self.flee(agent, controller, &read_data.terrain, &sound_pos);
|
||||||
|
} else {
|
||||||
|
self.idle(agent, controller, read_data);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Change this to a search action instead of idle
|
// TODO: Change this to a search action instead of idle
|
||||||
self.idle(agent, controller, read_data);
|
self.idle(agent, controller, read_data);
|
||||||
}
|
}
|
||||||
} else if self.flees {
|
}
|
||||||
let aggro = agent.psyche.aggro;
|
}
|
||||||
let close_enough = dist_sqrd < 35.0_f32.powi(2);
|
}
|
||||||
let loud_sound = sound.vol >= 10.0;
|
|
||||||
|
|
||||||
if close_enough && (aggro <= 0.5 || (aggro <= 0.7 && loud_sound)) {
|
fn attack_agents_attacker(
|
||||||
self.flee(agent, controller, &read_data.terrain, &sound_pos);
|
&self,
|
||||||
} else {
|
agent: &mut Agent,
|
||||||
self.idle(agent, controller, read_data);
|
read_data: &ReadData,
|
||||||
|
controller: &mut Controller,
|
||||||
|
) {
|
||||||
|
if let Some(Target { target, .. }) = agent.target {
|
||||||
|
if let Some(tgt_health) = read_data.healths.get(target) {
|
||||||
|
if let Some(by) = tgt_health.last_change.1.cause.damage_by() {
|
||||||
|
if let Some(attacker) = get_entity_by_id(by.id(), read_data) {
|
||||||
|
if agent.target.is_none() {
|
||||||
|
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.target = build_target(attacker, true, read_data.time.0);
|
||||||
|
|
||||||
|
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
||||||
|
if should_stop_attacking(attacker, read_data) {
|
||||||
|
agent.target = build_target(target, false, read_data.time.0);
|
||||||
|
|
||||||
|
self.idle(agent, controller, read_data);
|
||||||
|
} else {
|
||||||
|
let target_data = build_target_data(target, tgt_pos, read_data);
|
||||||
|
|
||||||
|
self.attack(agent, controller, &target_data, read_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.idle(agent, controller, read_data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4013,6 +3920,20 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn chat_general_if_can_speak(
|
||||||
|
&self,
|
||||||
|
agent: &Agent,
|
||||||
|
msg: String,
|
||||||
|
event_emitter: &mut Emitter<'_, ServerEvent>,
|
||||||
|
) -> bool {
|
||||||
|
if can_speak(agent) {
|
||||||
|
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn jump_if(&self, controller: &mut Controller, condition: bool) {
|
fn jump_if(&self, controller: &mut Controller, condition: bool) {
|
||||||
if condition {
|
if condition {
|
||||||
controller
|
controller
|
||||||
@ -4024,6 +3945,27 @@ impl<'a> AgentData<'a> {
|
|||||||
.push(ControlAction::CancelInput(InputKind::Jump))
|
.push(ControlAction::CancelInput(InputKind::Jump))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn chat_general(&self, msg: String, event_emitter: &mut Emitter<'_, ServerEvent>) {
|
||||||
|
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_villager_alarm(&self, time: f64, event_emitter: &mut Emitter<'_, ServerEvent>) {
|
||||||
|
event_emitter.emit(ServerEvent::Sound {
|
||||||
|
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 can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool {
|
fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool {
|
||||||
@ -4037,7 +3979,10 @@ fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If target is dead or has invulnerability buff, returns true
|
// If target is dead or has invulnerability buff, returns true
|
||||||
fn should_stop_attacking(health: Option<&Health>, buffs: Option<&Buffs>) -> bool {
|
fn should_stop_attacking(target: EcsEntity, read_data: &ReadData) -> bool {
|
||||||
|
let health = read_data.healths.get(target);
|
||||||
|
let buffs = read_data.buffs.get(target);
|
||||||
|
|
||||||
health.map_or(true, |a| a.is_dead) || invulnerability_is_in_buffs(buffs)
|
health.map_or(true, |a| a.is_dead) || invulnerability_is_in_buffs(buffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4053,10 +3998,7 @@ fn try_owner_alignment<'a>(
|
|||||||
read_data: &'a ReadData,
|
read_data: &'a ReadData,
|
||||||
) -> Option<&'a Alignment> {
|
) -> Option<&'a Alignment> {
|
||||||
if let Some(Alignment::Owned(owner_uid)) = alignment {
|
if let Some(Alignment::Owned(owner_uid)) = alignment {
|
||||||
if let Some(owner) = read_data
|
if let Some(owner) = get_entity_by_id(owner_uid.id(), read_data) {
|
||||||
.uid_allocator
|
|
||||||
.retrieve_entity_internal(owner_uid.id())
|
|
||||||
{
|
|
||||||
return read_data.alignments.get(owner);
|
return read_data.alignments.get(owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4114,3 +4056,47 @@ fn decrement_awareness(agent: &mut Agent) {
|
|||||||
|
|
||||||
agent.awareness -= decrement;
|
agent.awareness -= decrement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn entity_was_attacked(entity: EcsEntity, read_data: &ReadData) -> bool {
|
||||||
|
if let Some(entity_health) = read_data.healths.get(entity) {
|
||||||
|
entity_health.last_change.0 < 5.0 && entity_health.last_change.1.amount < 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_target(target: EcsEntity, is_hostile: bool, time: f64) -> Option<Target> {
|
||||||
|
Some(Target {
|
||||||
|
target,
|
||||||
|
hostile: is_hostile,
|
||||||
|
selected_at: time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_target_data<'a>(
|
||||||
|
target: EcsEntity,
|
||||||
|
tgt_pos: &'a Pos,
|
||||||
|
read_data: &'a ReadData,
|
||||||
|
) -> TargetData<'a> {
|
||||||
|
TargetData {
|
||||||
|
pos: tgt_pos,
|
||||||
|
body: read_data.bodies.get(target),
|
||||||
|
scale: read_data.scales.get(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user