Re-allow and improve fleeing.

This commit is contained in:
holychowders 2021-10-22 17:11:09 -05:00
parent de5ca67615
commit b40d94dd53
8 changed files with 266 additions and 191 deletions

View File

@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Trading over long distances using ghost characters or client-side exploits is no longer possible
- Merchant cost percentages displayed as floored, whole numbers
- Bodies of water no longer contain black chunks on the voxel minimap.
- Agents can flee once again, and more appropriately
## [0.11.0] - 2021-09-11

View File

@ -186,5 +186,13 @@
"Turn around if you want to live!",
"You're not welcome here!",
],
"npc.speech.cultist_low_health_fleeing": [
"Retreat for the cause!",
"Retreat!",
"Curse you!",
"I will curse you in the afterlife!",
"I must rest!",
"They're too strong!",
]
}
)

View File

@ -1,5 +1,8 @@
use crate::{
comp::{humanoid, quadruped_low, quadruped_medium, quadruped_small, ship, Body, UtteranceKind},
comp::{
biped_small, bird_medium, humanoid, quadruped_low, quadruped_medium, quadruped_small, ship,
Body, UtteranceKind,
},
path::Chaser,
rtsim::RtSimController,
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
@ -189,72 +192,82 @@ impl<'a> From<&'a Body> for Psyche {
Self {
flee_health: match body {
Body::Humanoid(humanoid) => match humanoid.species {
humanoid::Species::Danari => 0.1,
humanoid::Species::Dwarf => 0.2,
humanoid::Species::Elf => 0.3,
humanoid::Species::Danari => 0.4,
humanoid::Species::Dwarf => 0.3,
humanoid::Species::Elf => 0.4,
humanoid::Species::Human => 0.4,
humanoid::Species::Orc => 0.1,
humanoid::Species::Undead => 0.1,
humanoid::Species::Orc => 0.3,
humanoid::Species::Undead => 0.3,
},
Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
quadruped_small::Species::Pig => 0.5,
quadruped_small::Species::Fox => 0.7,
quadruped_small::Species::Sheep => 0.5,
quadruped_small::Species::Boar => 0.2,
quadruped_small::Species::Jackalope => 0.6,
quadruped_small::Species::Sheep => 0.6,
quadruped_small::Species::Boar => 0.1,
quadruped_small::Species::Jackalope => 0.0,
quadruped_small::Species::Skunk => 0.4,
quadruped_small::Species::Cat => 0.8,
quadruped_small::Species::Batfox => 0.4,
quadruped_small::Species::Cat => 0.9,
quadruped_small::Species::Batfox => 0.1,
quadruped_small::Species::Raccoon => 0.6,
quadruped_small::Species::Quokka => 0.6,
quadruped_small::Species::Dodarock => 0.1,
quadruped_small::Species::Dodarock => 0.0,
quadruped_small::Species::Holladon => 0.0,
quadruped_small::Species::Hyena => 0.6,
quadruped_small::Species::Rabbit => 0.9,
quadruped_small::Species::Hyena => 0.2,
quadruped_small::Species::Dog => 0.8,
quadruped_small::Species::Rabbit => 0.7,
quadruped_small::Species::Truffler => 0.2,
quadruped_small::Species::Frog => 0.6,
quadruped_small::Species::Hare => 0.8,
quadruped_small::Species::Hare => 0.3,
quadruped_small::Species::Goat => 0.5,
quadruped_small::Species::Porcupine => 0.7,
quadruped_small::Species::Turtle => 0.7,
// FIXME: This is to balance for enemy rats in dunegeons
// Normal rats should probably always flee.
quadruped_small::Species::Rat => 0.0,
quadruped_small::Species::Beaver => 0.7,
_ => 1.0,
},
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Tuskram => 0.3,
quadruped_medium::Species::Frostfang => 0.1,
quadruped_medium::Species::Mouflon => 0.3,
quadruped_medium::Species::Catoblepas => 0.2,
quadruped_medium::Species::Deer => 0.4,
quadruped_medium::Species::Hirdrasil => 0.3,
quadruped_medium::Species::Donkey => 0.3,
quadruped_medium::Species::Camel => 0.3,
quadruped_medium::Species::Zebra => 0.3,
quadruped_medium::Species::Antelope => 0.4,
quadruped_medium::Species::Horse => 0.3,
quadruped_medium::Species::Cattle => 0.3,
quadruped_medium::Species::Darkhound => 0.1,
quadruped_medium::Species::Dreadhorn => 0.2,
quadruped_medium::Species::Snowleopard => 0.3,
quadruped_medium::Species::Llama => 0.4,
quadruped_medium::Species::Alpaca => 0.4,
_ => 0.5,
quadruped_medium::Species::Bonerattler => 0.0,
quadruped_medium::Species::Tiger => 0.1,
_ => 0.3,
},
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
quadruped_low::Species::Salamander => 0.3,
quadruped_low::Species::Salamander => 0.2,
quadruped_low::Species::Monitor => 0.3,
quadruped_low::Species::Asp => 0.1,
quadruped_low::Species::Pangolin => 0.6,
_ => 0.4,
quadruped_low::Species::Tortoise => 0.2,
quadruped_low::Species::Rocksnapper => 0.05,
quadruped_low::Species::Asp => 0.05,
_ => 0.0,
},
Body::BipedSmall(biped_small) => match biped_small.species {
biped_small::Species::Gnarling => 0.2,
biped_small::Species::Adlet => 0.2,
biped_small::Species::Haniwa => 0.1,
biped_small::Species::Sahagin => 0.1,
biped_small::Species::Myrmidon => 0.0,
biped_small::Species::Husk => 0.0,
_ => 0.5,
},
Body::BirdMedium(bird_medium) => match bird_medium.species {
bird_medium::Species::Goose => 0.4,
bird_medium::Species::Peacock => 0.4,
bird_medium::Species::Eagle => 0.3,
bird_medium::Species::Parrot => 0.8,
_ => 0.5,
},
Body::BipedSmall(_) => 0.5,
Body::BirdMedium(_) => 0.5,
Body::BirdLarge(_) => 0.1,
Body::FishMedium(_) => 0.85,
Body::FishSmall(_) => 1.0,
Body::FishMedium(_) => 0.75,
Body::BipedLarge(_) => 0.0,
Body::Object(_) => 0.0,
Body::Golem(_) => 0.0,
Body::Theropod(_) => 0.0,
Body::Dragon(_) => 0.0,
Body::Ship(_) => 0.0,
Body::Dragon(_) => 0.0,
},
sight_dist: 40.0,
listen_dist: 30.0,
@ -477,16 +490,14 @@ impl Agent {
self
}
pub fn with_no_flee(mut self, no_flee: bool) -> Self {
if no_flee {
self.set_no_flee();
pub fn with_no_flee_if(mut self, condition: bool) -> Self {
if condition {
self.psyche.flee_health = 0.0;
}
self
}
pub fn set_no_flee(&mut self) { self.psyche.flee_health = 0.0; }
// TODO: Get rid of this method, it does weird things
// FIXME: Only one of *three* things in this method sets a location.
pub fn with_destination(mut self, pos: Vec3<f32>) -> Self {
self.psyche.flee_health = 0.0;
self.rtsim_controller = RtSimController::with_destination(pos);

View File

@ -511,7 +511,24 @@ impl Body {
quadruped_small::Species::Holladon => 80,
quadruped_small::Species::Hyena => 45,
quadruped_small::Species::Truffler => 45,
_ => 40,
quadruped_small::Species::Fox => 15,
quadruped_small::Species::Cat => 25,
quadruped_small::Species::Quokka => 10,
// FIXME: I would have set rats to 5, but that makes enemy ones in dungeons too
// easy. Put this back when the two types of rats are distinguishable.
quadruped_small::Species::Rat => 20,
quadruped_small::Species::Jackalope => 30,
quadruped_small::Species::Hare => 15,
quadruped_small::Species::Rabbit => 10,
quadruped_small::Species::Frog => 5,
quadruped_small::Species::Axolotl => 5,
quadruped_small::Species::Gecko => 5,
quadruped_small::Species::Squirrel => 10,
quadruped_small::Species::Porcupine => 15,
quadruped_small::Species::Beaver => 15,
quadruped_small::Species::Dog => 30,
quadruped_small::Species::Sheep => 30,
_ => 20,
},
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Grolgar => 90,
@ -519,9 +536,9 @@ impl Body {
quadruped_medium::Species::Tiger => 70,
quadruped_medium::Species::Lion => 90,
quadruped_medium::Species::Tarasque => 150,
quadruped_medium::Species::Wolf => 55,
quadruped_medium::Species::Wolf => 45,
quadruped_medium::Species::Frostfang => 40,
quadruped_medium::Species::Mouflon => 50,
quadruped_medium::Species::Mouflon => 40,
quadruped_medium::Species::Catoblepas => 100,
quadruped_medium::Species::Bonerattler => 50,
quadruped_medium::Species::Deer => 50,
@ -545,21 +562,20 @@ impl Body {
_ => 70,
},
Body::BirdMedium(bird_medium) => match bird_medium.species {
bird_medium::Species::Chicken => 30,
bird_medium::Species::Duck => 30,
bird_medium::Species::Goose => 30,
bird_medium::Species::Parrot => 25,
bird_medium::Species::Peacock => 35,
bird_medium::Species::Eagle => 45,
_ => 25,
bird_medium::Species::Owl => 45,
bird_medium::Species::Duck => 10,
_ => 15,
},
Body::FishMedium(_) => 25,
Body::FishMedium(_) => 15,
Body::Dragon(_) => 500,
Body::BirdLarge(bird_large) => match bird_large.species {
bird_large::Species::Roc => 280,
_ => 300,
},
Body::FishSmall(_) => 2,
Body::FishSmall(_) => 3,
Body::BipedLarge(biped_large) => match biped_large.species {
biped_large::Species::Ogre => 320,
biped_large::Species::Cyclops => 320,

View File

@ -180,7 +180,7 @@ impl CharacterBehavior for Data {
agent: Some(
comp::Agent::from_body(&body)
.with_behavior(Behavior::from(BehaviorCapability::SPEAK))
.with_no_flee(true),
.with_no_flee_if(true),
),
alignment: comp::Alignment::Owned(*data.uid),
scale: self

View File

@ -38,10 +38,17 @@ impl Body {
quadruped_small::Species::Axolotl => 70.0,
quadruped_small::Species::Pig => 70.0,
quadruped_small::Species::Sheep => 70.0,
quadruped_small::Species::Cat => 70.0,
quadruped_small::Species::Truffler => 70.0,
quadruped_small::Species::Fungome => 70.0,
quadruped_small::Species::Goat => 80.0,
quadruped_small::Species::Raccoon => 100.0,
quadruped_small::Species::Dodarock => 80.0,
quadruped_small::Species::Frog => 150.0,
quadruped_small::Species::Porcupine => 100.0,
quadruped_small::Species::Beaver => 100.0,
quadruped_small::Species::Rabbit => 110.0,
quadruped_small::Species::Cat => 150.0,
quadruped_small::Species::Quokka => 100.0,
_ => 125.0,
},
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
@ -111,7 +118,7 @@ impl Body {
quadruped_low::Species::Asp => 110.0,
quadruped_low::Species::Tortoise => 60.0,
quadruped_low::Species::Rocksnapper => 70.0,
quadruped_low::Species::Pangolin => 120.0,
quadruped_low::Species::Pangolin => 90.0,
quadruped_low::Species::Maneater => 80.0,
quadruped_low::Species::Sandshark => 160.0,
quadruped_low::Species::Hakulaq => 140.0,

View File

@ -67,7 +67,6 @@ struct AgentData<'a> {
alignment: Option<&'a Alignment>,
traversal_config: TraversalConfig,
scale: f32,
flees: bool,
damage: f32,
light_emitter: Option<&'a LightEmitter>,
glider_equipped: bool,
@ -319,10 +318,6 @@ impl<'a> System<'a> for Sys {
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_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
@ -356,7 +351,6 @@ impl<'a> System<'a> for Sys {
alignment: alignment.as_ref(),
traversal_config,
scale,
flees,
damage: health_fraction,
light_emitter,
glider_equipped,
@ -398,19 +392,15 @@ impl<'a> System<'a> for Sys {
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();
let too_far_away = dist_sqrd > (MAX_FOLLOW_DIST).powi(2);
// If too far away, then follow
if too_far_away {
data.follow(agent, controller, &read_data.terrain, tgt_pos);
// Attack target's attacker
// Else, attack target's attacker (if there is one)
} 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
// Otherwise, just idle
} else {
idle(agent, controller, event_emitter);
}
@ -424,7 +414,7 @@ impl<'a> System<'a> for Sys {
controller: &mut Controller,
event_emitter| {
if let Some(tgt_health) = read_data.healths.get(target) {
// If target is dead, leave it
// If target is dead, forget them
if tgt_health.is_dead {
if let Some(tgt_stats) =
data.rtsim_entity.and(read_data.stats.get(target))
@ -432,13 +422,10 @@ impl<'a> System<'a> for Sys {
rtsim_forget_enemy(&tgt_stats.name, agent);
}
agent.target = None;
// If the target is hostile
// (either based on alignment or if
// the target just attacked)
// Else, if target is hostile, hostile tree
} else if hostile {
data.hostile_tree(agent, controller, &read_data, event_emitter);
// Target is something worth following
// methinks
// Else, if owned, act as pet to them
} else if let Some(Alignment::Owned(uid)) = data.alignment {
if read_data.uids.get(target) == Some(uid) {
react_as_pet(agent, target, controller, event_emitter);
@ -481,9 +468,9 @@ impl<'a> System<'a> for Sys {
if let Some(attacker) =
read_data.uid_allocator.retrieve_entity_internal(by.uid().0)
{
// If the target is dead or in a safezone, remove the
// target and idle.
if should_stop_attacking(attacker, &read_data) {
// If target is dead or invulnerable (for now, this only
// means safezone), untarget them and idle.
if is_dead_or_invulnerable(attacker, &read_data) {
agent.target = None;
} else if let Some(tgt_pos) =
read_data.positions.get(attacker)
@ -495,9 +482,8 @@ impl<'a> System<'a> for Sys {
}
// 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)
// 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) =
@ -606,8 +592,9 @@ impl<'a> AgentData<'a> {
decrement_awareness(agent);
forget_old_sounds(agent, read_data);
let small_chance = thread_rng().gen_bool(0.1);
// Set owner if no target
if agent.target.is_none() && thread_rng().gen_bool(0.1) {
if agent.target.is_none() && small_chance {
if let Some(Alignment::Owned(owner)) = self.alignment {
if let Some(owner) = get_entity_by_id(owner.id(), read_data) {
agent.target = build_target(owner, false, read_data.time.0, false);
@ -641,7 +628,9 @@ impl<'a> AgentData<'a> {
}
// If we receive a new interaction, start the interaction timer
if can_speak(agent) && self.recv_interaction(agent, controller, read_data, event_emitter) {
if allowed_to_speak(agent)
&& self.recv_interaction(agent, controller, read_data, event_emitter)
{
agent.timer.start(read_data.time.0, TimerAction::Interact);
}
@ -714,22 +703,23 @@ impl<'a> AgentData<'a> {
.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) {
self.chat_general("npc.speech.villager_under_attack", event_emitter);
self.emit_scream(read_data.time.0, event_emitter);
let should_flee = self.damage.min(1.0) < agent.psyche.flee_health;
if should_flee {
let has_opportunity_to_flee = agent.action_state.timer < FLEE_DURATION;
let within_flee_distance = dist_sq < MAX_FLEE_DIST.powi(2);
// FIXME: Using the action state timer to see if an agent is allowed to speak is
// a hack.
if agent.action_state.timer == 0.0 {
self.cry_out(agent, read_data.time.0, event_emitter);
agent.action_state.timer = 0.01;
} else if agent.action_state.timer < FLEE_DURATION
|| dist_sq < MAX_FLEE_DIST.powi(2)
{
} else if within_flee_distance && has_opportunity_to_flee {
self.flee(agent, controller, &read_data.terrain, tgt_pos);
agent.action_state.timer += read_data.dt.0;
} else {
@ -737,36 +727,30 @@ impl<'a> AgentData<'a> {
agent.target = None;
self.idle(agent, controller, read_data);
}
} else if should_stop_attacking(target, read_data) {
if can_speak(agent) {
let msg = "npc.speech.villager_enemy_killed".to_string();
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
}
} else if is_dead(target, read_data) {
self.exclaim_relief_about_enemy_dead(agent, event_emitter);
agent.target = None;
self.idle(agent, controller, read_data);
} else if is_invulnerable(target, read_data) {
agent.target = None;
self.idle(agent, controller, read_data);
} else {
// Potentially choose a new target
if read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS
&& !in_aggro_range
{
let is_time_to_retarget =
read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
if !in_aggro_range && is_time_to_retarget {
self.choose_target(agent, controller, read_data, event_emitter);
}
// FIXME: This check being a pre-requisite to attack() prevents agents from
// supporting their buddies when necessary. For example, village guards will
// literally watch villagers and other guards be slaughtered until these victims
// get within aggro range (which is what aggro_on checks) or get too far.
if aggro_on {
let target_data = build_target_data(target, tgt_pos, 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);
self.menacing(agent, target, controller, read_data, event_emitter);
}
}
}
@ -1040,7 +1024,7 @@ impl<'a> AgentData<'a> {
let msg = agent.inbox.pop_front();
match msg {
Some(AgentEvent::Talk(by, subject)) => {
if can_speak(agent) {
if allowed_to_speak(agent) {
if let Some(target) = get_entity_by_id(by.id(), read_data) {
agent.target = build_target(target, false, read_data.time.0, false);
@ -1088,25 +1072,25 @@ impl<'a> AgentData<'a> {
destination_name
)
};
self.chat_general(msg, event_emitter);
self.chat_npc(msg, event_emitter);
} else if agent.behavior.can_trade() {
if !agent.behavior.is(BehaviorState::TRADING) {
controller.events.push(ControlEvent::InitiateInvite(
by,
InviteKind::Trade,
));
self.chat_general(
self.chat_npc(
"npc.speech.merchant_advertisement",
event_emitter,
);
} else {
self.chat_general(
self.chat_npc(
"npc.speech.merchant_busy",
event_emitter,
);
}
} else {
self.chat_general("npc.speech.villager", event_emitter);
self.chat_npc("npc.speech.villager", event_emitter);
}
},
Subject::Trade => {
@ -1116,12 +1100,12 @@ impl<'a> AgentData<'a> {
by,
InviteKind::Trade,
));
self.chat_general(
self.chat_npc(
"npc.speech.merchant_advertisement",
event_emitter,
);
} else {
self.chat_general(
self.chat_npc(
"npc.speech.merchant_busy",
event_emitter,
);
@ -1129,7 +1113,7 @@ impl<'a> AgentData<'a> {
} else {
// TODO: maybe make some travellers willing to trade with
// simpler goods like potions
self.chat_general(
self.chat_npc(
"npc.speech.villager_decline_trade",
event_emitter,
);
@ -1181,7 +1165,7 @@ impl<'a> AgentData<'a> {
MemoryItem::Mood { state } => state.describe(),
_ => "".to_string(),
};
self.chat_general(msg, event_emitter);
self.chat_npc(msg, event_emitter);
}
}
},
@ -1195,7 +1179,7 @@ impl<'a> AgentData<'a> {
"{} ? I think it's {} {} from here!",
location.name, dist, dir
);
self.chat_general(msg, event_emitter);
self.chat_npc(msg, event_emitter);
}
},
Subject::Person(person) => {
@ -1230,7 +1214,7 @@ impl<'a> AgentData<'a> {
person.name()
)
};
self.chat_general(msg, event_emitter);
self.chat_npc(msg, event_emitter);
}
},
Subject::Work => {},
@ -1257,9 +1241,9 @@ impl<'a> AgentData<'a> {
controller
.events
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
self.chat_general_if_can_speak(
agent,
self.chat_npc_if_allowed_to_speak(
"npc.speech.merchant_busy",
agent,
event_emitter,
);
}
@ -1268,9 +1252,9 @@ impl<'a> AgentData<'a> {
controller
.events
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
self.chat_general_if_can_speak(
agent,
self.chat_npc_if_allowed_to_speak(
"npc.speech.villager_decline_trade",
agent,
event_emitter,
);
}
@ -1288,13 +1272,10 @@ impl<'a> AgentData<'a> {
if agent.behavior.is(BehaviorState::TRADING) {
match result {
TradeResult::Completed => {
self.chat_general(
"npc.speech.merchant_trade_successful",
event_emitter,
);
self.chat_npc("npc.speech.merchant_trade_successful", event_emitter);
},
_ => {
self.chat_general("npc.speech.merchant_trade_declined", event_emitter);
self.chat_npc("npc.speech.merchant_trade_declined", event_emitter);
},
}
agent.behavior.unset(BehaviorState::TRADING);
@ -1390,19 +1371,27 @@ impl<'a> AgentData<'a> {
fn menacing(
&self,
_agent: &mut Agent,
agent: &Agent,
target: EcsEntity,
controller: &mut Controller,
read_data: &ReadData,
target: EcsEntity,
_tgt_pos: &Pos,
event_emitter: &mut Emitter<ServerEvent>,
) {
let max_move = 0.5;
let move_dir = controller.inputs.move_dir;
let move_dir_mag = move_dir.magnitude();
let small_chance = thread_rng().gen::<f32>() < read_data.dt.0 * 0.25;
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;
controller.inputs.move_dir = max_move * move_dir / move_dir_mag;
}
if small_chance {
self.chat_npc_if_allowed_to_speak("npc.speech.menacing", agent, event_emitter);
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
}
}
@ -1573,7 +1562,7 @@ impl<'a> AgentData<'a> {
|| in_listen_dist(e_pos, e_char_state, e_inventory)
};
let owners_hostile = |e_alignment: Option<&Alignment>| {
let is_owner_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)
@ -1581,17 +1570,18 @@ impl<'a> AgentData<'a> {
})
};
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 = read_data.time.0 - e_health.last_change.time.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 guard_defending_villager = |e_health: &Health, e_alignment: Option<&Alignment>| {
let i_am_a_guard = read_data
.stats
.get(*self.entity)
.map_or(false, |stats| stats.name == "Guard");
let other_is_a_villager = matches!(e_alignment, Some(Alignment::Npc));
let villager_has_taken_damage = e_health.last_change.time.0 < 5.0;
let attacker_of = |health: &Health| health.last_change.damage_by();
need_help
let i_should_defend = i_am_a_guard && other_is_a_villager && villager_has_taken_damage;
i_should_defend
.then(|| {
attacker_of(e_health)
.and_then(|damage_contributor| {
@ -1617,15 +1607,14 @@ impl<'a> AgentData<'a> {
.remembers_fight_with_character(&target_stats.name)
{
rtsim_new_enemy(&target_stats.name, agent, read_data);
if can_speak(agent) {
let message = format!(
self.chat_npc_if_allowed_to_speak(
format!(
"{}! How dare you cross me again!",
target_stats.name.clone()
),
agent,
event_emitter,
);
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, message,
)));
}
true
} else {
false
@ -1651,12 +1640,13 @@ impl<'a> AgentData<'a> {
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,
)));
}
self.chat_npc_if_allowed_to_speak(
"npc.speech.villager_cultist_alarm",
agent,
event_emitter,
);
true
} else {
false
@ -1677,13 +1667,13 @@ impl<'a> AgentData<'a> {
let can_target = within_reach(e_pos, e_char_state, e_inventory)
&& entity != *self.entity
&& !e_health.is_dead
&& !invulnerability_is_in_buffs(read_data.buffs.get(entity));
&& !is_invulnerable(entity, read_data);
if !can_target {
None
} else if owners_hostile(e_alignment) {
} else if is_owner_hostile(e_alignment) {
Some((entity, *e_pos))
} else if let Some(villain_info) = guard_duty(e_health, e_alignment) {
} else if let Some(villain_info) = guard_defending_villager(e_health, e_alignment) {
Some(villain_info)
} else if rtsim_remember(e_stats, agent, event_emitter)
|| npc_sees_cultist(e_stats, e_inventory, agent, event_emitter)
@ -3453,8 +3443,9 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
) {
if attack_data.dist_sqrd > 30.0_f32.powi(2) {
// If random chance and can see target
if thread_rng().gen_bool(0.05)
let small_chance = thread_rng().gen_bool(0.05);
if small_chance
&& can_see_tgt(
&*read_data.terrain,
self.pos,
@ -4166,20 +4157,20 @@ impl<'a> AgentData<'a> {
controller: &mut Controller,
read_data: &ReadData,
) {
// Currently this means that we are in a safezone
if invulnerability_is_in_buffs(read_data.buffs.get(*self.entity)) {
if is_invulnerable(*self.entity, read_data) {
self.idle(agent, controller, read_data);
return;
}
if let Some(sound) = agent.sounds_heard.last() {
// FIXME: Perhaps name should be a field of the agent, not stats
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);
// FIXME: We need to be able to change the name of a guard without breaking this
// logic The `Mark` enum from common::agent could be used to
// match with `agent::Mark::Guard`
let is_village_guard = agent_stats.name == *"Guard".to_string();
let is_neutral = self.flees && !is_village_guard;
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
if is_enemy {
@ -4193,7 +4184,7 @@ impl<'a> AgentData<'a> {
}
} else if is_village_guard {
self.follow(agent, controller, &read_data.terrain, &sound_pos);
} else if is_neutral {
} else if !is_village_guard {
let flee_health = agent.psyche.flee_health;
let close_enough = dist_sqrd < 35.0_f32.powi(2);
let sound_was_loud = sound.vol >= 10.0;
@ -4230,7 +4221,7 @@ impl<'a> AgentData<'a> {
agent.target = build_target(attacker, true, read_data.time.0, true);
if let Some(tgt_pos) = read_data.positions.get(attacker) {
if should_stop_attacking(attacker, read_data) {
if is_dead_or_invulnerable(attacker, read_data) {
agent.target = build_target(target, false, read_data.time.0, false);
self.idle(agent, controller, read_data);
@ -4330,14 +4321,14 @@ impl<'a> AgentData<'a> {
}
}
fn chat_general_if_can_speak(
fn chat_npc_if_allowed_to_speak(
&self,
agent: &Agent,
msg: impl ToString,
agent: &Agent,
event_emitter: &mut Emitter<'_, ServerEvent>,
) -> bool {
if can_speak(agent) {
self.chat_general(msg, event_emitter);
if allowed_to_speak(agent) {
self.chat_npc(msg, event_emitter);
true
} else {
false
@ -4356,7 +4347,7 @@ impl<'a> AgentData<'a> {
}
}
fn chat_general(&self, msg: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) {
fn chat_npc(&self, msg: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
msg.to_string(),
@ -4375,6 +4366,43 @@ impl<'a> AgentData<'a> {
});
}
}
fn cry_out(&self, agent: &Agent, time: f64, event_emitter: &mut Emitter<'_, ServerEvent>) {
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 {
self.chat_npc_if_allowed_to_speak(
"npc.speech.cultist_low_health_fleeing",
agent,
event_emitter,
);
} else if is_villager {
self.chat_npc_if_allowed_to_speak(
"npc.speech.villager_under_attack",
agent,
event_emitter,
);
self.emit_scream(time, event_emitter);
}
}
fn exclaim_relief_about_enemy_dead(
&self,
agent: &Agent,
event_emitter: &mut Emitter<'_, ServerEvent>,
) {
let is_villager = matches!(self.alignment, Some(Alignment::Npc));
if is_villager {
self.chat_npc_if_allowed_to_speak(
"npc.speech.villager_enemy_killed",
agent,
event_emitter,
);
}
}
}
fn rtsim_new_enemy(target_name: &str, agent: &mut Agent, read_data: &ReadData) {
@ -4406,17 +4434,21 @@ fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32)
>= dist_sqrd
}
// If target is dead or has invulnerability buff, returns true
fn should_stop_attacking(target: EcsEntity, read_data: &ReadData) -> bool {
let health = read_data.healths.get(target);
let buffs = read_data.buffs.get(target);
fn is_dead_or_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
is_dead(entity, read_data) || is_invulnerable(entity, read_data)
}
health.map_or(true, |a| a.is_dead) || invulnerability_is_in_buffs(buffs)
fn is_dead(entity: EcsEntity, read_data: &ReadData) -> bool {
let health = read_data.healths.get(entity);
health.map_or(false, |a| a.is_dead)
}
// FIXME: The logic that is used in this function and throughout the code
// shouldn't be used to mean that a character is in a safezone.
fn invulnerability_is_in_buffs(buffs: Option<&Buffs>) -> bool {
fn is_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
let buffs = read_data.buffs.get(entity);
buffs.map_or(false, |b| b.kinds.contains_key(&BuffKind::Invulnerability))
}
@ -4515,7 +4547,7 @@ fn build_target_data<'a>(
}
}
fn can_speak(agent: &Agent) -> bool { agent.behavior.can(BehaviorCapability::SPEAK) }
fn allowed_to_speak(agent: &Agent) -> bool { agent.behavior.can(BehaviorCapability::SPEAK) }
fn get_entity_by_id(id: u64, read_data: &ReadData) -> Option<EcsEntity> {
read_data.uid_allocator.retrieve_entity_internal(id)

View File

@ -471,10 +471,10 @@ impl NpcData {
let health = Some(comp::Health::new(body, health_scaling.unwrap_or(0)));
let poise = comp::Poise::new(body);
// Allow Humanoid, BirdMedium, and Parrot to speak
let can_speak = match body {
comp::Body::Humanoid(_) => true,
comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
// Parrots like to have a word in this, too...
bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
_ => false,
},
@ -495,13 +495,13 @@ impl NpcData {
.with_trade_site(trade_for_site),
)
.with_patrol_origin(pos)
.with_no_flee(!matches!(agent_mark, Some(agent::Mark::Guard)))
.with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)))
});
let agent = if matches!(alignment, comp::Alignment::Enemy)
&& matches!(body, comp::Body::Humanoid(_))
{
agent.map(|a| a.with_aggro_no_warn())
agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
} else {
agent
};