mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Fix villagers seeing cultists and familiar enemies through objects.
This commit is contained in:
parent
a22f035638
commit
914b44c714
@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Harvester boss arenas should be more accessible and easier to exit
|
- Harvester boss arenas should be more accessible and easier to exit
|
||||||
- Fix agents not idling
|
- Fix agents not idling
|
||||||
- Fixed an error where '{amount} Exp' floater did not use existing localizations
|
- Fixed an error where '{amount} Exp' floater did not use existing localizations
|
||||||
|
- Fix villagers seeing cultists and familiar enemies through objects.
|
||||||
|
|
||||||
## [0.12.0] - 2022-02-19
|
## [0.12.0] - 2022-02-19
|
||||||
|
|
||||||
|
@ -597,7 +597,7 @@ impl Agent {
|
|||||||
.push(RtSimEvent::ForgetEnemy(target_name.to_owned()));
|
.push(RtSimEvent::ForgetEnemy(target_name.to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_enemy(&mut self, target_name: &str, time: f64) {
|
pub fn add_fight_to_memory(&mut self, target_name: &str, time: f64) {
|
||||||
self.rtsim_controller
|
self.rtsim_controller
|
||||||
.events
|
.events
|
||||||
.push(RtSimEvent::AddMemory(Memory {
|
.push(RtSimEvent::AddMemory(Memory {
|
||||||
|
@ -14,7 +14,7 @@ use crate::{
|
|||||||
data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData},
|
data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData},
|
||||||
util::{
|
util::{
|
||||||
aim_projectile, can_see_tgt, get_entity_by_id, is_dead, is_dead_or_invulnerable,
|
aim_projectile, can_see_tgt, get_entity_by_id, is_dead, is_dead_or_invulnerable,
|
||||||
is_invulnerable, stop_pursuing, try_owner_alignment,
|
is_invulnerable, is_villager, stop_pursuing, try_owner_alignment,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -37,7 +37,7 @@ use common::{
|
|||||||
},
|
},
|
||||||
projectile::ProjectileConstructor,
|
projectile::ProjectileConstructor,
|
||||||
Agent, Alignment, BehaviorState, Body, CharacterState, ControlAction, ControlEvent,
|
Agent, Alignment, BehaviorState, Body, CharacterState, ControlAction, ControlEvent,
|
||||||
Controller, Health, HealthChange, InputKind, Inventory, InventoryAction, Pos, Scale, Stats,
|
Controller, Health, HealthChange, InputKind, Inventory, InventoryAction, Pos, Scale,
|
||||||
UnresolvedChatMsg, UtteranceKind,
|
UnresolvedChatMsg, UtteranceKind,
|
||||||
},
|
},
|
||||||
effect::{BuffEffect, Effect},
|
effect::{BuffEffect, Effect},
|
||||||
@ -327,7 +327,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
if let Some(attacker_stats) =
|
if let Some(attacker_stats) =
|
||||||
data.rtsim_entity.and(read_data.stats.get(attacker))
|
data.rtsim_entity.and(read_data.stats.get(attacker))
|
||||||
{
|
{
|
||||||
agent.add_enemy(
|
agent.add_fight_to_memory(
|
||||||
&attacker_stats.name,
|
&attacker_stats.name,
|
||||||
read_data.time.0,
|
read_data.time.0,
|
||||||
);
|
);
|
||||||
@ -537,7 +537,7 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rng.gen::<f32>() < 0.1 {
|
if rng.gen::<f32>() < 0.1 {
|
||||||
self.choose_target(agent, controller, read_data, event_emitter);
|
self.choose_target(agent, controller, read_data);
|
||||||
} else {
|
} else {
|
||||||
self.handle_sounds_heard(agent, controller, read_data, rng);
|
self.handle_sounds_heard(agent, controller, read_data, rng);
|
||||||
}
|
}
|
||||||
@ -608,7 +608,7 @@ impl<'a> AgentData<'a> {
|
|||||||
// FIXME: Using the action state timer to see if an agent is allowed to speak is
|
// FIXME: Using the action state timer to see if an agent is allowed to speak is
|
||||||
// a hack.
|
// a hack.
|
||||||
if agent.action_state.timer == 0.0 {
|
if agent.action_state.timer == 0.0 {
|
||||||
self.cry_out(agent, read_data.time.0, event_emitter);
|
self.cry_out(agent, event_emitter, read_data);
|
||||||
agent.action_state.timer = 0.01;
|
agent.action_state.timer = 0.01;
|
||||||
} else if within_flee_distance && has_opportunity_to_flee {
|
} else if within_flee_distance && has_opportunity_to_flee {
|
||||||
self.flee(agent, controller, &read_data.terrain, tgt_pos);
|
self.flee(agent, controller, &read_data.terrain, tgt_pos);
|
||||||
@ -639,7 +639,7 @@ impl<'a> AgentData<'a> {
|
|||||||
read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
|
read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
|
||||||
|
|
||||||
if !in_aggro_range && is_time_to_retarget {
|
if !in_aggro_range && is_time_to_retarget {
|
||||||
self.choose_target(agent, controller, read_data, event_emitter);
|
self.choose_target(agent, controller, read_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if aggro_on {
|
if aggro_on {
|
||||||
@ -648,9 +648,13 @@ impl<'a> AgentData<'a> {
|
|||||||
read_data.bodies.get(target),
|
read_data.bodies.get(target),
|
||||||
read_data.scales.get(target),
|
read_data.scales.get(target),
|
||||||
);
|
);
|
||||||
|
let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
|
||||||
|
|
||||||
|
tgt_name
|
||||||
|
.map(|tgt_name| agent.add_fight_to_memory(&tgt_name, read_data.time.0));
|
||||||
self.attack(agent, controller, &target_data, read_data, rng);
|
self.attack(agent, controller, &target_data, read_data, rng);
|
||||||
} else {
|
} else {
|
||||||
self.menacing(agent, target, controller, read_data, event_emitter, rng);
|
self.menacing(agent, controller, target, read_data, event_emitter, rng);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1395,9 +1399,9 @@ impl<'a> AgentData<'a> {
|
|||||||
|
|
||||||
fn menacing(
|
fn menacing(
|
||||||
&self,
|
&self,
|
||||||
agent: &Agent,
|
agent: &mut Agent,
|
||||||
target: EcsEntity,
|
|
||||||
controller: &mut Controller,
|
controller: &mut Controller,
|
||||||
|
target: EcsEntity,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
event_emitter: &mut Emitter<ServerEvent>,
|
event_emitter: &mut Emitter<ServerEvent>,
|
||||||
rng: &mut impl Rng,
|
rng: &mut impl Rng,
|
||||||
@ -1406,6 +1410,18 @@ impl<'a> AgentData<'a> {
|
|||||||
let move_dir = controller.inputs.move_dir;
|
let move_dir = controller.inputs.move_dir;
|
||||||
let move_dir_mag = move_dir.magnitude();
|
let move_dir_mag = move_dir.magnitude();
|
||||||
let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25;
|
let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25;
|
||||||
|
let mut chat = |msg: &str| {
|
||||||
|
self.chat_npc_if_allowed_to_speak(msg.to_string(), agent, event_emitter);
|
||||||
|
};
|
||||||
|
let mut chat_villager_remembers_fighting = || {
|
||||||
|
let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
|
||||||
|
|
||||||
|
if let Some(tgt_name) = tgt_name {
|
||||||
|
chat(format!("{}! How dare you cross me again!", &tgt_name).as_str());
|
||||||
|
} else {
|
||||||
|
chat("You! How dare you cross me again!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.look_toward(controller, read_data, target);
|
self.look_toward(controller, read_data, target);
|
||||||
controller.push_action(ControlAction::Wield);
|
controller.push_action(ControlAction::Wield);
|
||||||
@ -1415,8 +1431,19 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if small_chance {
|
if small_chance {
|
||||||
self.chat_npc_if_allowed_to_speak("npc.speech.menacing", agent, event_emitter);
|
|
||||||
controller.push_utterance(UtteranceKind::Angry);
|
controller.push_utterance(UtteranceKind::Angry);
|
||||||
|
|
||||||
|
if is_villager(self.alignment) {
|
||||||
|
if self.remembers_fight_with(target, read_data) {
|
||||||
|
chat_villager_remembers_fighting();
|
||||||
|
} else if self.entity_looks_like_cultist(target, read_data) {
|
||||||
|
chat("npc.speech.villager_cultist_alarm");
|
||||||
|
} else {
|
||||||
|
chat("npc.speech.menacing");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chat("npc.speech.menacing");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1512,13 +1539,7 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn choose_target(
|
fn choose_target(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) {
|
||||||
&self,
|
|
||||||
agent: &mut Agent,
|
|
||||||
controller: &mut Controller,
|
|
||||||
read_data: &ReadData,
|
|
||||||
event_emitter: &mut Emitter<'_, ServerEvent>,
|
|
||||||
) {
|
|
||||||
agent.action_state.timer = 0.0;
|
agent.action_state.timer = 0.0;
|
||||||
let mut aggro_on = false;
|
let mut aggro_on = false;
|
||||||
|
|
||||||
@ -1528,7 +1549,6 @@ impl<'a> AgentData<'a> {
|
|||||||
entity,
|
entity,
|
||||||
pos,
|
pos,
|
||||||
read_data.healths.get(entity)?,
|
read_data.healths.get(entity)?,
|
||||||
read_data.stats.get(entity)?,
|
|
||||||
read_data.inventories.get(entity)?,
|
read_data.inventories.get(entity)?,
|
||||||
read_data.alignments.get(entity),
|
read_data.alignments.get(entity),
|
||||||
read_data.char_states.get(entity),
|
read_data.char_states.get(entity),
|
||||||
@ -1589,7 +1609,6 @@ impl<'a> AgentData<'a> {
|
|||||||
.stats
|
.stats
|
||||||
.get(*self.entity)
|
.get(*self.entity)
|
||||||
.map_or(false, |stats| stats.name == "Guard");
|
.map_or(false, |stats| stats.name == "Guard");
|
||||||
let other_is_a_villager = matches!(e_alignment, Some(Alignment::Npc));
|
|
||||||
let we_are_friendly: bool = self.alignment.map_or(false, |ma| {
|
let we_are_friendly: bool = self.alignment.map_or(false, |ma| {
|
||||||
e_alignment.map_or(false, |ea| !ea.hostile_towards(*ma))
|
e_alignment.map_or(false, |ea| !ea.hostile_towards(*ma))
|
||||||
});
|
});
|
||||||
@ -1605,7 +1624,7 @@ impl<'a> AgentData<'a> {
|
|||||||
|
|
||||||
let i_should_defend = other_has_taken_damage
|
let i_should_defend = other_has_taken_damage
|
||||||
&& ((we_are_friendly && we_share_species)
|
&& ((we_are_friendly && we_share_species)
|
||||||
|| (i_am_a_guard && other_is_a_villager)
|
|| (i_am_a_guard && is_villager(e_alignment))
|
||||||
|| i_own_other);
|
|| i_own_other);
|
||||||
|
|
||||||
i_should_defend
|
i_should_defend
|
||||||
@ -1634,101 +1653,38 @@ impl<'a> AgentData<'a> {
|
|||||||
.flatten()
|
.flatten()
|
||||||
};
|
};
|
||||||
|
|
||||||
let rtsim_remember =
|
let possible_target =
|
||||||
|target_stats: &Stats,
|
|(entity, e_pos, e_health, e_inventory, e_alignment, e_char_state, e_body): (
|
||||||
agent: &mut Agent,
|
EcsEntity,
|
||||||
event_emitter: &mut Emitter<'_, ServerEvent>| {
|
&Pos,
|
||||||
self.rtsim_entity.map_or(false, |rtsim_entity| {
|
&Health,
|
||||||
if rtsim_entity
|
&Inventory,
|
||||||
.brain
|
Option<&Alignment>,
|
||||||
.remembers_fight_with_character(&target_stats.name)
|
Option<&CharacterState>,
|
||||||
{
|
Option<&Body>,
|
||||||
agent.add_enemy(&target_stats.name, read_data.time.0);
|
)| {
|
||||||
self.chat_npc_if_allowed_to_speak(
|
let can_target = within_reach(e_pos, e_char_state, e_inventory)
|
||||||
format!(
|
&& entity != *self.entity
|
||||||
"{}! How dare you cross me again!",
|
&& !e_health.is_dead
|
||||||
target_stats.name.clone()
|
&& !is_invulnerable(entity, read_data);
|
||||||
),
|
|
||||||
agent,
|
if !can_target {
|
||||||
event_emitter,
|
None
|
||||||
);
|
} else if is_owner_hostile(e_alignment) {
|
||||||
true
|
Some((entity, *e_pos))
|
||||||
} else {
|
} else if let Some(villain_info) = guard_other(e_health, e_body, e_alignment) {
|
||||||
false
|
aggro_on = true;
|
||||||
}
|
Some(villain_info)
|
||||||
})
|
} else if self.remembers_fight_with(entity, read_data)
|
||||||
|
|| is_villager(self.alignment)
|
||||||
|
&& self.entity_looks_like_cultist(entity, read_data)
|
||||||
|
{
|
||||||
|
Some((entity, *e_pos))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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() {
|
|
||||||
agent.add_enemy(&target_stats.name, read_data.time.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.chat_npc_if_allowed_to_speak(
|
|
||||||
"npc.speech.villager_cultist_alarm",
|
|
||||||
agent,
|
|
||||||
event_emitter,
|
|
||||||
);
|
|
||||||
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let possible_target = |(
|
|
||||||
entity,
|
|
||||||
e_pos,
|
|
||||||
e_health,
|
|
||||||
e_stats,
|
|
||||||
e_inventory,
|
|
||||||
e_alignment,
|
|
||||||
e_char_state,
|
|
||||||
e_body,
|
|
||||||
): (
|
|
||||||
EcsEntity,
|
|
||||||
&Pos,
|
|
||||||
&Health,
|
|
||||||
&Stats,
|
|
||||||
&Inventory,
|
|
||||||
Option<&Alignment>,
|
|
||||||
Option<&CharacterState>,
|
|
||||||
Option<&Body>,
|
|
||||||
)| {
|
|
||||||
let can_target = within_reach(e_pos, e_char_state, e_inventory)
|
|
||||||
&& entity != *self.entity
|
|
||||||
&& !e_health.is_dead
|
|
||||||
&& !is_invulnerable(entity, read_data);
|
|
||||||
|
|
||||||
if !can_target {
|
|
||||||
None
|
|
||||||
} else if is_owner_hostile(e_alignment) {
|
|
||||||
Some((entity, *e_pos))
|
|
||||||
} else if let Some(villain_info) = guard_other(e_health, e_body, e_alignment) {
|
|
||||||
aggro_on = true;
|
|
||||||
Some(villain_info)
|
|
||||||
} else if rtsim_remember(e_stats, agent, event_emitter)
|
|
||||||
|| npc_sees_cultist(e_stats, e_inventory, agent, event_emitter)
|
|
||||||
{
|
|
||||||
Some((entity, *e_pos))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Search area
|
// Search area
|
||||||
// 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;
|
||||||
@ -2300,6 +2256,8 @@ impl<'a> AgentData<'a> {
|
|||||||
|
|
||||||
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
||||||
if is_dead_or_invulnerable(attacker, read_data) {
|
if is_dead_or_invulnerable(attacker, read_data) {
|
||||||
|
// FIXME?: Shouldn't target be set to `None`?
|
||||||
|
// If is dead, then probably. If invulnerable, maybe not.
|
||||||
agent.target =
|
agent.target =
|
||||||
Some(Target::new(target, false, read_data.time.0, false));
|
Some(Target::new(target, false, read_data.time.0, false));
|
||||||
|
|
||||||
@ -2310,7 +2268,11 @@ impl<'a> AgentData<'a> {
|
|||||||
read_data.bodies.get(target),
|
read_data.bodies.get(target),
|
||||||
read_data.scales.get(target),
|
read_data.scales.get(target),
|
||||||
);
|
);
|
||||||
|
if let Some(tgt_name) =
|
||||||
|
read_data.stats.get(target).map(|stats| stats.name.clone())
|
||||||
|
{
|
||||||
|
agent.add_fight_to_memory(&tgt_name, read_data.time.0)
|
||||||
|
}
|
||||||
self.attack(agent, controller, &target_data, read_data, rng);
|
self.attack(agent, controller, &target_data, read_data, rng);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2439,10 +2401,13 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cry_out(&self, agent: &Agent, time: f64, event_emitter: &mut Emitter<'_, ServerEvent>) {
|
fn cry_out(
|
||||||
|
&self,
|
||||||
|
agent: &Agent,
|
||||||
|
event_emitter: &mut Emitter<'_, ServerEvent>,
|
||||||
|
read_data: &ReadData,
|
||||||
|
) {
|
||||||
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
|
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
|
||||||
// FIXME: This is not necessarily a "villager"
|
|
||||||
let is_villager = matches!(self.alignment, Some(Alignment::Npc));
|
|
||||||
|
|
||||||
if is_enemy {
|
if is_enemy {
|
||||||
self.chat_npc_if_allowed_to_speak(
|
self.chat_npc_if_allowed_to_speak(
|
||||||
@ -2450,13 +2415,13 @@ impl<'a> AgentData<'a> {
|
|||||||
agent,
|
agent,
|
||||||
event_emitter,
|
event_emitter,
|
||||||
);
|
);
|
||||||
} else if is_villager {
|
} else if is_villager(self.alignment) {
|
||||||
self.chat_npc_if_allowed_to_speak(
|
self.chat_npc_if_allowed_to_speak(
|
||||||
"npc.speech.villager_under_attack",
|
"npc.speech.villager_under_attack",
|
||||||
agent,
|
agent,
|
||||||
event_emitter,
|
event_emitter,
|
||||||
);
|
);
|
||||||
self.emit_scream(time, event_emitter);
|
self.emit_scream(read_data.time.0, event_emitter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2465,9 +2430,7 @@ impl<'a> AgentData<'a> {
|
|||||||
agent: &Agent,
|
agent: &Agent,
|
||||||
event_emitter: &mut Emitter<'_, ServerEvent>,
|
event_emitter: &mut Emitter<'_, ServerEvent>,
|
||||||
) {
|
) {
|
||||||
let is_villager = matches!(self.alignment, Some(Alignment::Npc));
|
if is_villager(self.alignment) {
|
||||||
|
|
||||||
if is_villager {
|
|
||||||
self.chat_npc_if_allowed_to_speak(
|
self.chat_npc_if_allowed_to_speak(
|
||||||
"npc.speech.villager_enemy_killed",
|
"npc.speech.villager_enemy_killed",
|
||||||
agent,
|
agent,
|
||||||
@ -2511,4 +2474,24 @@ impl<'a> AgentData<'a> {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn entity_looks_like_cultist(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
|
||||||
|
let number_of_cultist_items_equipped = read_data.inventories.get(entity).map_or(0, |inv| {
|
||||||
|
inv.equipped_items()
|
||||||
|
.filter(|item| item.tags().contains(&ItemTag::Cultist))
|
||||||
|
.count()
|
||||||
|
});
|
||||||
|
|
||||||
|
number_of_cultist_items_equipped > 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remembers_fight_with(&self, other: EcsEntity, read_data: &ReadData) -> bool {
|
||||||
|
let name = || read_data.stats.get(other).map(|stats| stats.name.clone());
|
||||||
|
|
||||||
|
self.rtsim_entity.map_or(false, |rtsim_entity| {
|
||||||
|
name().map_or(false, |name| {
|
||||||
|
rtsim_entity.brain.remembers_fight_with_character(&name)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,3 +121,8 @@ fn should_let_target_escape(
|
|||||||
) -> f32 {
|
) -> f32 {
|
||||||
(dist_to_home_sqrd / own_health_fraction) * dur_since_last_attacked as f32 * 0.005
|
(dist_to_home_sqrd / own_health_fraction) * dur_since_last_attacked as f32 * 0.005
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: `Alignment::Npc` doesn't necessarily mean villager.
|
||||||
|
pub fn is_villager(alignment: Option<&Alignment>) -> bool {
|
||||||
|
alignment.map_or(false, |alignment| matches!(alignment, Alignment::Npc))
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user