diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2470589d..8072e017dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 6 different gems. (Topaz, Amethyst, Sapphire, Emerald, Ruby and Diamond) - Poise system (not currently accessible to players for balancing reasons) - Snow particles +- Basic NPC interaction ### Changed @@ -37,7 +38,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Default inventory slots reduced to 18 - existing characters given 3x 6-slot bags as compensation - Protection rating was moved to the top left of the loadout view - Changed camera smoothing to be off by default. -- Fixed AI behavior so only humanoids will attempt to roll - Footstep SFX is now dependant on distance moved, not time since last play - Adjusted most NPCs hitboxes to better fit their models. - Changed crafting recipes involving shiny gems to use diamonds instead. @@ -57,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where buff/debuff UI elements would flicker when you had more than one of them active at the same time - Made zooming work on wayland +- Fixed AI behavior so only humanoids will attempt to roll ## [0.8.0] - 2020-11-28 diff --git a/assets/voxygen/i18n/en/_manifest.ron b/assets/voxygen/i18n/en/_manifest.ron index 82ae53de80..8788df0977 100644 --- a/assets/voxygen/i18n/en/_manifest.ron +++ b/assets/voxygen/i18n/en/_manifest.ron @@ -62,6 +62,35 @@ "Sit near a campfire (with the 'K' key) to slowly recover from your injuries.", "Need more bags or better armor to continue your journey? Press 'C' to open the crafting menu!", ], + "npc.speech.villager": [ + "Isn't it such a lovely day?", + "How are you today?", + "Top of the morning to you!", + "I wonder what the Catobelpas thinks when it eats grass.", + "What do you think about this weather?", + "Thinking about those dungeons makes me scared. I hope someone will clear them out.", + "I'd like to go spelunking in a cave when I'm stronger.", + "Have you seen my cat?", + "Have you ever heard of the ferocious Land Sharks? I hear they live in deserts.", + "They say shiny gems of all kinds can be found in caves.", + "I'm just crackers about cheese!", + "Won't you come in? We were just about to have some cheese!", + "They say mushrooms are good for your health. Never eat them myself.", + "Don't forget the crackers!", + "I simply adore dwarven cheese. I wish I could make it.", + "I wonder what is on the other side of the mountains.", + "I hope to make my own glider someday.", + "Would you like to see my garden? Okay, maybe some other time.", + "Lovely day for a stroll in the woods!", + "To be, or not to be? I think I'll be a farmer.", + "Don't you think our village is the best?.", + "What do you suppose makes Glowing Remains glow?.", + "I think it's time for second breakfast!", + "Have you ever caught a firefly?", + "I just can't understand where those Sauroks keep coming from.", + "I wish someone would keep the wolves away from the village.", + "I had a wonderful dream about cheese last night. What does it mean?", + ], "npc.speech.villager_under_attack": [ "Help, I'm under attack!", "Help! I'm under attack!", diff --git a/client/src/lib.rs b/client/src/lib.rs index 09088e2ee7..6d5502add7 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -637,6 +637,23 @@ impl Client { } } + pub fn npc_interact(&mut self, npc_entity: EcsEntity) { + // If we're dead, exit before sending message + if self + .state + .ecs() + .read_storage::() + .get(self.entity) + .map_or(false, |h| h.is_dead) + { + return; + } + + if let Some(uid) = self.state.read_component_copied(npc_entity) { + self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(uid))); + } + } + pub fn player_list(&self) -> &HashMap { &self.player_list } pub fn character_list(&self) -> &CharacterList { &self.character_list } diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 9f12acadc6..43fe21911f 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -6,7 +6,31 @@ use crate::{ }; use specs::{Component, Entity as EcsEntity}; use specs_idvs::IdvStorage; +use std::collections::VecDeque; use vek::*; + +pub const DEFAULT_INTERACTION_TIME: f32 = 3.0; + +#[derive(Eq, PartialEq)] +pub enum Tactic { + Melee, + Axe, + Hammer, + Sword, + Bow, + Staff, + StoneGolemBoss, + CircleCharge { radius: u32, circle_time: u32 }, + QuadLowRanged, + TailSlap, + QuadLowQuick, + QuadLowBasic, + QuadMedJump, + QuadMedBasic, + Lavadrake, + Theropod, +} + #[derive(Copy, Clone, Debug, PartialEq)] pub enum Alignment { /// Wild animals and gentle giants @@ -137,6 +161,15 @@ impl<'a> From<&'a Body> for Psyche { } } +#[derive(Clone, Debug)] +/// Events that affect agent behavior from other entities/players/environment +pub enum AgentEvent { + /// Engage in conversation with entity with Uid + Talk(Uid), + Trade(Uid), + // Add others here +} + #[derive(Clone, Debug, Default)] pub struct Agent { pub rtsim_controller: RtSimController, @@ -146,6 +179,7 @@ pub struct Agent { // TODO move speech patterns into a Behavior component pub can_speak: bool, pub psyche: Psyche, + pub inbox: VecDeque, } impl Agent { @@ -179,6 +213,10 @@ impl Component for Agent { #[derive(Clone, Debug)] pub enum Activity { + Interact { + timer: f32, + interaction: AgentEvent, + }, Idle { bearing: Vec2, chaser: Chaser, @@ -194,12 +232,19 @@ pub enum Activity { been_close: bool, powerup: f32, }, + Flee { + target: EcsEntity, + chaser: Chaser, + timer: f32, + }, } impl Activity { pub fn is_follow(&self) -> bool { matches!(self, Activity::Follow { .. }) } pub fn is_attack(&self) -> bool { matches!(self, Activity::Attack { .. }) } + + pub fn is_flee(&self) -> bool { matches!(self, Activity::Flee { .. }) } } impl Default for Activity { diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 66fad330a1..7f18e3b7d3 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -41,6 +41,7 @@ pub enum CharacterState { Climb, Sit, Dance, + Talk, Sneak, Glide, GlideWield, @@ -139,6 +140,7 @@ impl CharacterState { | CharacterState::BasicBeam(_) | CharacterState::Stunned(_) | CharacterState::Wielding + | CharacterState::Talk ) } diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 9a2a797b0e..a1f461336a 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -37,6 +37,7 @@ pub enum ControlEvent { //ToggleLantern, EnableLantern, DisableLantern, + Interact(Uid), Mount(Uid), Unmount, InventoryManip(InventoryManip), @@ -55,6 +56,7 @@ pub enum ControlAction { Dance, Sneak, Stand, + Talk, } #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/common/src/event.rs b/common/src/event.rs index d9794c803b..68c1d90349 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -78,6 +78,7 @@ pub enum ServerEvent { }, EnableLantern(EcsEntity), DisableLantern(EcsEntity), + NpcInteract(EcsEntity, EcsEntity), Mount(EcsEntity, EcsEntity), Unmount(EcsEntity), Possess(Uid, Uid), diff --git a/common/src/path.rs b/common/src/path.rs index 367f455688..6b27747355 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -32,8 +32,9 @@ impl FromIterator for Path { } } -#[allow(clippy::len_without_is_empty)] // TODO: Pending review in #587 impl Path { + pub fn is_empty(&self) -> bool { self.nodes.is_empty() } + pub fn len(&self) -> usize { self.nodes.len() } pub fn iter(&self) -> impl Iterator { self.nodes.iter() } diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 065f8a343e..e5c1268d2e 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -30,7 +30,7 @@ pub struct RtSimController { /// When this field is `Some(..)`, the agent should attempt to make progress /// toward the given location, accounting for obstacles and other /// high-priority situations like being attacked. - pub travel_to: Option>, + pub travel_to: Option<(Vec3, String)>, /// Proportion of full speed to move pub speed_factor: f32, } diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index f2b6f1ed7c..e1479da487 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -24,6 +24,7 @@ pub trait CharacterBehavior { fn dance(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn sneak(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn stand(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } + fn talk(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn handle_event(&self, data: &JoinData, event: ControlAction) -> StateUpdate { match event { ControlAction::SwapLoadout => self.swap_loadout(data), @@ -34,6 +35,7 @@ pub trait CharacterBehavior { ControlAction::Dance => self.dance(data), ControlAction::Sneak => self.sneak(data), ControlAction::Stand => self.stand(data), + ControlAction::Talk => self.talk(data), } } // fn init(data: &JoinData) -> CharacterState; diff --git a/common/src/states/idle.rs b/common/src/states/idle.rs index c38b68695b..618880ece9 100644 --- a/common/src/states/idle.rs +++ b/common/src/states/idle.rs @@ -31,6 +31,12 @@ impl CharacterBehavior for Data { update } + fn talk(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_talk(data, &mut update); + update + } + fn dance(&self, data: &JoinData) -> StateUpdate { let mut update = StateUpdate::from(data); attempt_dance(data, &mut update); diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 6d8ac0ab11..bd505fbbe0 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -22,5 +22,6 @@ pub mod sit; pub mod sneak; pub mod spin_melee; pub mod stunned; +pub mod talk; pub mod utils; pub mod wielding; diff --git a/common/src/states/talk.rs b/common/src/states/talk.rs new file mode 100644 index 0000000000..fa1566a30e --- /dev/null +++ b/common/src/states/talk.rs @@ -0,0 +1,49 @@ +use super::utils::*; +use crate::{ + comp::{CharacterState, StateUpdate}, + states::behavior::{CharacterBehavior, JoinData}, +}; +use serde::{Deserialize, Serialize}; + +const TURN_RATE: f32 = 40.0; + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] +pub struct Data; + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + + handle_wield(data, &mut update); + handle_orientation(data, &mut update, TURN_RATE); + + update + } + + fn wield(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_wield(data, &mut update); + update + } + + fn sit(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + update.character = CharacterState::Idle; + attempt_sit(data, &mut update); + update + } + + fn dance(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + update.character = CharacterState::Idle; + attempt_dance(data, &mut update); + update + } + + fn stand(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + // Try to Fall/Stand up/Move + update.character = CharacterState::Idle; + update + } +} diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index cc3b18b314..c031e2a2f4 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -322,6 +322,12 @@ pub fn attempt_dance(data: &JoinData, update: &mut StateUpdate) { } } +pub fn attempt_talk(data: &JoinData, update: &mut StateUpdate) { + if data.physics.on_ground { + update.character = CharacterState::Talk; + } +} + pub fn attempt_sneak(data: &JoinData, update: &mut StateUpdate) { if data.physics.on_ground && data.body.is_humanoid() { update.character = CharacterState::Sneak; diff --git a/common/sys/src/agent.rs b/common/sys/src/agent.rs index 7dce4e78ca..cfa1eb389f 100644 --- a/common/sys/src/agent.rs +++ b/common/sys/src/agent.rs @@ -1,7 +1,7 @@ use common::{ comp::{ self, - agent::Activity, + agent::{Activity, AgentEvent, Tactic, DEFAULT_INTERACTION_TIME}, group, group::Invite, inventory::slot::EquipSlot, @@ -34,6 +34,10 @@ use specs::{ use std::f32::consts::PI; use vek::*; +// This is 3.1 to last longer than the last damage timer (3.0 seconds) +const DAMAGE_MEMORY_DURATION: f64 = 3.0; +const FLEE_DURATION: f32 = 3.1; + /// This system will allow NPCs to modify their controller pub struct Sys; impl<'a> System<'a> for Sys { @@ -57,6 +61,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Inventory>, ReadStorage<'a, Stats>, ReadStorage<'a, PhysicsState>, + ReadStorage<'a, CharacterState>, ReadStorage<'a, Uid>, ReadStorage<'a, group::Group>, ReadExpect<'a, TerrainGrid>, @@ -68,7 +73,6 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Invite>, Read<'a, TimeOfDay>, ReadStorage<'a, LightEmitter>, - ReadStorage<'a, CharacterState>, ); #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 @@ -88,6 +92,7 @@ impl<'a> System<'a> for Sys { inventories, stats, physics_states, + char_states, uids, groups, terrain, @@ -99,7 +104,6 @@ impl<'a> System<'a> for Sys { invites, time_of_day, light_emitter, - char_states, ): Self::SystemData, ) { let start_time = std::time::Instant::now(); @@ -204,7 +208,7 @@ impl<'a> System<'a> for Sys { let scale = scales.get(entity).map(|s| s.0).unwrap_or(1.0); - let min_attack_dist = body.map_or(2.0, |b| b.radius() * scale * 1.5); + let min_attack_dist = body.map_or(3.0, |b| b.radius() * scale + 2.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 @@ -224,11 +228,67 @@ impl<'a> System<'a> for Sys { let mut do_idle = false; let mut choose_target = false; + let flees = alignment + .map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_))) + .unwrap_or(true); 'activity: { match &mut agent.activity { + Activity::Interact { interaction, timer } => { + if let AgentEvent::Talk(by) = interaction { + if let Some(target) = uid_allocator.retrieve_entity_internal(by.id()) { + if *timer < DEFAULT_INTERACTION_TIME { + if let Some(tgt_pos) = positions.get(target) { + let eye_offset = body.map_or(0.0, |b| b.eye_height()); + let tgt_eye_offset = bodies.get(target).map_or(0.0, |b| b.eye_height()); + if let Some(dir) = Dir::from_unnormalized( + Vec3::new( + tgt_pos.0.x, + tgt_pos.0.y, + tgt_pos.0.z + tgt_eye_offset, + ) - Vec3::new(pos.0.x, pos.0.y, pos.0.z + eye_offset), + ) { + inputs.look_dir = dir; + } + if *timer == 0.0 { + controller.actions.push(ControlAction::Stand); + controller.actions.push(ControlAction::Talk); + if let Some((_travel_to, destination_name)) = &agent.rtsim_controller.travel_to { + + let msg = format!("I'm heading to {}! Want to come along?", destination_name); + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc(*uid, msg), + )); + } else { + let msg = "npc.speech.villager".to_string(); + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc(*uid, msg), + )); + } + } + } + *timer += dt.0; + } else { + controller.actions.push(ControlAction::Stand); + do_idle = true; + } + } + } + + // Interrupt + if !agent.inbox.is_empty() { + if agent.can_speak { // Remove this if/when we can pet doggos + agent.activity = Activity::Interact { + timer: 0.0, + interaction: agent.inbox.pop_back().unwrap(), // Should not fail as already checked is_empty() + } + } else { + agent.inbox.clear(); + } + } + }, Activity::Idle { bearing, chaser } => { - if let Some(travel_to) = agent.rtsim_controller.travel_to { + if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to { // if it has an rtsim destination and can fly then it should // if it is flying and bumps something above it then it should move down inputs.fly.set_state(traversal_config.can_fly && !terrain @@ -240,7 +300,7 @@ impl<'a> System<'a> for Sys { .1 .map_or(true, |b| b.is_some())); if let Some((bearing, speed)) = - chaser.chase(&*terrain, pos.0, vel.0, travel_to, TraversalConfig { + chaser.chase(&*terrain, pos.0, vel.0, *travel_to, TraversalConfig { min_tgt_dist: 1.25, ..traversal_config }) @@ -327,6 +387,18 @@ impl<'a> System<'a> for Sys { if thread_rng().gen::() < 0.1 { choose_target = true; } + + // Interact + if !agent.inbox.is_empty() { + if flees && agent.can_speak { // Remove this if/when we can pet doggos + agent.activity = Activity::Interact { + timer: 0.0, + interaction: agent.inbox.pop_back().unwrap(), // Should not fail as already checked is_empty() + } + } else { + agent.inbox.clear(); + } + } }, Activity::Follow { target, chaser } => { if let (Some(tgt_pos), _tgt_health) = @@ -358,6 +430,46 @@ impl<'a> System<'a> for Sys { do_idle = true; } }, + Activity::Flee { + target, + chaser, + timer, + } => { + if let Some(body) = body { + if body.can_strafe() { + controller.actions.push(ControlAction::Unwield); + } + } + if let Some(tgt_pos) = positions.get(*target) { + let dist_sqrd = pos.0.distance_squared(tgt_pos.0); + if *timer < FLEE_DURATION || dist_sqrd < MAX_FLEE_DIST.powi(2) { + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + // Away from the target (ironically) + pos.0 + + (pos.0 - tgt_pos.0) + .try_normalized() + .unwrap_or_else(Vec3::unit_y) + * 50.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = + bearing.xy().try_normalized().unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + *timer += dt.0; + } else { + do_idle = true; + } + } + }, Activity::Attack { target, chaser, @@ -365,26 +477,6 @@ impl<'a> System<'a> for Sys { powerup, .. } => { - #[derive(Eq, PartialEq)] - enum Tactic { - Melee, - Axe, - Hammer, - Sword, - Bow, - Staff, - StoneGolemBoss, - CircleCharge { radius: u32, circle_time: u32 }, - QuadLowRanged, - TailSlap, - QuadLowQuick, - QuadLowBasic, - QuadMedJump, - QuadMedBasic, - Lavadrake, - Theropod, - } - let tactic = match inventory.equipped(EquipSlot::Mainhand).as_ref().and_then(|item| { if let ItemKind::Tool(tool) = &item.kind() { Some(&tool.kind) @@ -493,876 +585,531 @@ impl<'a> System<'a> for Sys { let dist_sqrd = pos.0.distance_squared(tgt_pos.0); - let damage = healths - .get(entity) - .map(|h| h.current() as f32 / h.maximum() as f32) - .unwrap_or(0.5); - - // Flee - let flees = alignment - .map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_))) - .unwrap_or(true); - if 1.0 - agent.psyche.aggro > damage && flees { - if let Some(body) = body { - if body.can_strafe() { - controller.actions.push(ControlAction::Unwield); - } - } - if dist_sqrd < MAX_FLEE_DIST.powi(2) { - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - // Away from the target (ironically) - pos.0 - + (pos.0 - tgt_pos.0) + // Match on tactic. Each tactic has different controls + // depending on the distance from the agent to the target + match tactic { + Tactic::Melee => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.primary.set_state(true); + inputs.move_dir = Vec2::zero(); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() .try_normalized() - .unwrap_or_else(Vec3::unit_y) - * 50.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = - bearing.xy().try_normalized().unwrap_or(Vec2::zero()) + .unwrap_or(Vec2::zero()) * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } else { - do_idle = true; - } - } else { - // Match on tactic. Each tactic has different controls - // depending on the distance from the agent to the target - match tactic { - Tactic::Melee => { - if dist_sqrd < (min_attack_dist * scale).powi(2) { - inputs.primary.set_state(true); - inputs.move_dir = Vec2::zero(); - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.02 - { - inputs.roll.set_state(true); - } - } else { - do_idle = true; - } - }, - Tactic::Axe => { - if dist_sqrd < (min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); - if *powerup > 6.0 { - inputs.secondary.set_state(false); - *powerup = 0.0; - } else if *powerup > 4.0 && energy.current() > 10 { - inputs.secondary.set_state(true); - *powerup += dt.0; - } else if stats.skill_set.has_skill(Skill::Axe(AxeSkill::UnlockLeap)) && energy.current() > 800 && thread_rng().gen_bool(0.5) { - inputs.ability3.set_state(true); - *powerup += dt.0; - } else { - inputs.primary.set_state(true); - *powerup += dt.0; - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.02 - { - inputs.roll.set_state(true); - } - } else { - do_idle = true; - } - }, - Tactic::Hammer => { - if dist_sqrd < (min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); - if *powerup > 4.0 { - inputs.secondary.set_state(false); - *powerup = 0.0; - } else if *powerup > 2.0 { - inputs.secondary.set_state(true); - *powerup += dt.0; - } else if stats.skill_set.has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) && energy.current() > 700 - && thread_rng().gen_bool(0.9) { - inputs.ability3.set_state(true); - *powerup += dt.0; - } else { - inputs.primary.set_state(true); - *powerup += dt.0; - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - if stats.skill_set.has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) && *powerup > 5.0 { - inputs.ability3.set_state(true); - *powerup = 0.0; - } else { - *powerup += dt.0; - } - } else { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.02 - { - inputs.roll.set_state(true); - } - } else { - do_idle = true; - } - }, - Tactic::Sword => { - if dist_sqrd < (min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); - if stats.skill_set.has_skill(Skill::Sword(SwordSkill::UnlockSpin)) && *powerup < 2.0 && energy.current() > 600 { - inputs.ability3.set_state(true); - *powerup += dt.0; - } else if *powerup > 2.0 { - *powerup = 0.0; - } else { - inputs.primary.set_state(true); - *powerup += dt.0; - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - if *powerup > 4.0 { - inputs.secondary.set_state(true); - *powerup = 0.0; - } else { - *powerup += dt.0; - } - } else { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.02 - { - inputs.roll.set_state(true); - } - } else { - do_idle = true; - } - }, - Tactic::Bow => { - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (2.0 * min_attack_dist * scale).powi(2) { inputs.roll.set_state(true); - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { - inputs.move_dir = bearing - .xy() - .rotated_z( - thread_rng().gen_range(0.5..1.57), - ) - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - if *powerup > 4.0 { - inputs.secondary.set_state(false); - *powerup = 0.0; - } else if *powerup > 2.0 - && energy.current() > 300 - { - inputs.secondary.set_state(true); - *powerup += dt.0; - } else if stats.skill_set.has_skill(Skill::Bow(BowSkill::UnlockRepeater)) && energy.current() > 400 - && thread_rng().gen_bool(0.8) - { - inputs.secondary.set_state(false); - inputs.ability3.set_state(true); - *powerup += dt.0; - } else { - inputs.secondary.set_state(false); - inputs.primary.set_state(true); - *powerup += dt.0; - } - } else { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.02 - { - inputs.roll.set_state(true); - } - } else { - do_idle = true; } - }, - Tactic::Staff => { - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (min_attack_dist * scale).powi(2) { - inputs.roll.set_state(true); - } else if dist_sqrd - < (5.0 * min_attack_dist * scale).powi(2) - { - if *powerup < 1.5 { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(0.47 * PI) - .try_normalized() - .unwrap_or(Vec2::unit_y()); - *powerup += dt.0; - } else if *powerup < 3.0 { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(-0.47 * PI) - .try_normalized() - .unwrap_or(Vec2::unit_y()); - *powerup += dt.0; - } else { - *powerup = 0.0; - } - if stats.skill_set.has_skill(Skill::Staff(StaffSkill::UnlockShockwave)) && energy.current() > 800 - && thread_rng().gen::() > 0.8 - { - inputs.ability3.set_state(true); - } else if energy.current() > 10 { - inputs.secondary.set_state(true); - } else { - inputs.primary.set_state(true); - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { - inputs.move_dir = bearing - .xy() - .rotated_z( - thread_rng().gen_range(-1.57..-0.5), - ) - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.primary.set_state(true); - } else { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } - if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.02 - { - inputs.roll.set_state(true); - } - } else { - do_idle = true; - } - }, - Tactic::StoneGolemBoss => { - if dist_sqrd < (min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); - inputs.primary.set_state(true); - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if vel.0.is_approx_zero() { - inputs.ability3.set_state(true); - } - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - if *powerup > 5.0 { - inputs.secondary.set_state(true); - *powerup = 0.0; - } else { - *powerup += dt.0; - } - } else { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } - } else { - do_idle = true; - } - }, - Tactic::CircleCharge { - radius, - circle_time, - } => { - if dist_sqrd < (min_attack_dist * scale).powi(2) - && thread_rng().gen_bool(0.5) - { - inputs.move_dir = Vec2::zero(); - inputs.primary.set_state(true); - } else if dist_sqrd - < (radius as f32 * min_attack_dist * scale).powi(2) - { - inputs.move_dir = (pos.0 - tgt_pos.0) - .xy() - .try_normalized() - .unwrap_or(Vec2::unit_y()); - } else if dist_sqrd - < ((radius as f32 + 1.0) * min_attack_dist * scale) - .powi(2) - && dist_sqrd - > (radius as f32 * min_attack_dist * scale).powi(2) - { - if *powerup < circle_time as f32 { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(0.47 * PI) - .try_normalized() - .unwrap_or(Vec2::unit_y()); - *powerup += dt.0; - } else if *powerup < circle_time as f32 + 0.5 { - inputs.secondary.set_state(true); - *powerup += dt.0; - } else if *powerup < 2.0 * circle_time as f32 + 0.5 { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(-0.47 * PI) - .try_normalized() - .unwrap_or(Vec2::unit_y()); - *powerup += dt.0; - } else if *powerup < 2.0 * circle_time as f32 + 1.0 { - inputs.secondary.set_state(true); - *powerup += dt.0; - } else { - *powerup = 0.0; - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } else { - do_idle = true; - } - }, - Tactic::QuadLowRanged => { - if dist_sqrd < (5.0 * min_attack_dist * scale).powi(2) { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .try_normalized() - .unwrap_or(Vec2::unit_y()); - inputs.primary.set_state(true); - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { - if *powerup > 5.0 { - *powerup = 0.0; - } else if *powerup > 2.5 { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(1.75 * PI) - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - *powerup += dt.0; - } else { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(0.25 * PI) - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - *powerup += dt.0; - } - inputs.secondary.set_state(true); - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } else { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } else { - do_idle = true; - } - } else { - do_idle = true; - } - }, - Tactic::TailSlap => { - if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { - if *powerup > 4.0 { - inputs.primary.set_state(false); - *powerup = 0.0; - } else if *powerup > 1.0 { - inputs.primary.set_state(true); - *powerup += dt.0; - } else { - inputs.secondary.set_state(true); - *powerup += dt.0; - } - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .try_normalized() - .unwrap_or(Vec2::unit_y()) - * 0.1; - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } else { - do_idle = true; - } - }, - Tactic::QuadLowQuick => { - if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); + } else { + do_idle = true; + } + }, + Tactic::Axe => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if *powerup > 6.0 { + inputs.secondary.set_state(false); + *powerup = 0.0; + } else if *powerup > 4.0 && energy.current() > 10 { inputs.secondary.set_state(true); - } else if dist_sqrd - < (3.0 * min_attack_dist * scale).powi(2) - && dist_sqrd > (2.0 * min_attack_dist * scale).powi(2) - { + *powerup += dt.0; + } else if stats.skill_set.has_skill(Skill::Axe(AxeSkill::UnlockLeap)) && energy.current() > 800 && thread_rng().gen_bool(0.5) { + inputs.ability3.set_state(true); + *powerup += dt.0; + } else { inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Hammer => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if *powerup > 4.0 { + inputs.secondary.set_state(false); + *powerup = 0.0; + } else if *powerup > 2.0 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if stats.skill_set.has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) && energy.current() > 700 + && thread_rng().gen_bool(0.9) { + inputs.ability3.set_state(true); + *powerup += dt.0; + } else { + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if stats.skill_set.has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) && *powerup > 5.0 { + inputs.ability3.set_state(true); + *powerup = 0.0; + } else { + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Sword => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if stats.skill_set.has_skill(Skill::Sword(SwordSkill::UnlockSpin)) && *powerup < 2.0 && energy.current() > 600 { + inputs.ability3.set_state(true); + *powerup += dt.0; + } else if *powerup > 2.0 { + *powerup = 0.0; + } else { + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if *powerup > 4.0 { + inputs.secondary.set_state(true); + *powerup = 0.0; + } else { + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Bow => { + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (2.0 * min_attack_dist * scale).powi(2) { + inputs.roll.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .rotated_z( + thread_rng().gen_range(0.5..1.57), + ) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if *powerup > 4.0 { + inputs.secondary.set_state(false); + *powerup = 0.0; + } else if *powerup > 2.0 + && energy.current() > 300 + { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if stats.skill_set.has_skill(Skill::Bow(BowSkill::UnlockRepeater)) && energy.current() > 400 + && thread_rng().gen_bool(0.8) + { + inputs.secondary.set_state(false); + inputs.ability3.set_state(true); + *powerup += dt.0; + } else { + inputs.secondary.set_state(false); + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Staff => { + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.roll.set_state(true); + } else if dist_sqrd + < (5.0 * min_attack_dist * scale).powi(2) + { + if *powerup < 1.5 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + *powerup += dt.0; + } else if *powerup < 3.0 { inputs.move_dir = (tgt_pos.0 - pos.0) .xy() .rotated_z(-0.47 * PI) .try_normalized() .unwrap_or(Vec2::unit_y()); - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } + *powerup += dt.0; } else { - do_idle = true; + *powerup = 0.0; } - }, - Tactic::QuadLowBasic => { - if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); - if *powerup > 5.0 { - *powerup = 0.0; - } else if *powerup > 2.0 { - inputs.secondary.set_state(true); - *powerup += dt.0; - } else { - inputs.primary.set_state(true); - *powerup += dt.0; - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } else { - do_idle = true; - } - }, - Tactic::QuadMedJump => { - if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); - inputs.secondary.set_state(true); - } else if dist_sqrd - < (5.0 * min_attack_dist * scale).powi(2) + if stats.skill_set.has_skill(Skill::Staff(StaffSkill::UnlockShockwave)) && energy.current() > 800 + && thread_rng().gen::() > 0.8 { inputs.ability3.set_state(true); - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { - inputs.primary.set_state(true); - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - } else { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } - } else { - do_idle = true; - } - }, - Tactic::QuadMedBasic => { - if dist_sqrd < (min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); - if *powerup < 2.0 { - inputs.secondary.set_state(true); - *powerup += dt.0; - } else if *powerup < 3.0 { - inputs.primary.set_state(true); - *powerup += dt.0; - } else { - *powerup = 0.0; - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } - } else { - do_idle = true; - } - }, - Tactic::Lavadrake => { - if dist_sqrd < (2.5 * min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); + } else if energy.current() > 10 { inputs.secondary.set_state(true); - } else if dist_sqrd - < (7.0 * min_attack_dist * scale).powi(2) - { - if *powerup < 2.0 { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(0.47 * PI) - .try_normalized() - .unwrap_or(Vec2::unit_y()); - inputs.primary.set_state(true); - *powerup += dt.0; - } else if *powerup < 4.0 { - inputs.move_dir = (tgt_pos.0 - pos.0) - .xy() - .rotated_z(-0.47 * PI) - .try_normalized() - .unwrap_or(Vec2::unit_y()); - inputs.primary.set_state(true); - *powerup += dt.0; - } else if *powerup < 6.0 { - inputs.ability3.set_state(true); - *powerup += dt.0; - } else { - *powerup = 0.0; - } - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; - } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { - inputs.move_dir = bearing - .xy() - .try_normalized() - .unwrap_or(Vec2::zero()) - * speed; - inputs.jump.set_state(bearing.z > 1.5); - inputs.move_z = bearing.z; - } } else { - do_idle = true; - } - }, - Tactic::Theropod => { - if dist_sqrd < (2.0 * min_attack_dist * scale).powi(2) { - inputs.move_dir = Vec2::zero(); inputs.primary.set_state(true); - } else if dist_sqrd < MAX_CHASE_DIST.powi(2) - || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) - { - if dist_sqrd < MAX_CHASE_DIST.powi(2) { - *been_close = true; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .rotated_z( + thread_rng().gen_range(-1.57..-0.5), + ) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.primary.set_state(true); + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; } - if let Some((bearing, speed)) = chaser.chase( - &*terrain, - pos.0, - vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: 1.25, - ..traversal_config - }, - ) { + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::StoneGolemBoss => { + if dist_sqrd < (min_attack_dist * scale * 2.0).powi(2) { // 2.0 is temporary correction factor to allow them to melee with their large hitbox + inputs.move_dir = Vec2::zero(); + inputs.primary.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if vel.0.is_approx_zero() { + inputs.ability3.set_state(true); + } + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if *powerup > 5.0 { + inputs.secondary.set_state(true); + *powerup = 0.0; + } else { + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + } else { + do_idle = true; + } + }, + Tactic::CircleCharge { + radius, + circle_time, + } => { + if dist_sqrd < (min_attack_dist * scale).powi(2) + && thread_rng().gen_bool(0.5) + { + inputs.move_dir = Vec2::zero(); + inputs.primary.set_state(true); + } else if dist_sqrd + < (radius as f32 * min_attack_dist * scale).powi(2) + { + inputs.move_dir = (pos.0 - tgt_pos.0) + .xy() + .try_normalized() + .unwrap_or(Vec2::unit_y()); + } else if dist_sqrd + < ((radius as f32 + 1.0) * min_attack_dist * scale) + .powi(2) + && dist_sqrd + > (radius as f32 * min_attack_dist * scale).powi(2) + { + if *powerup < circle_time as f32 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + *powerup += dt.0; + } else if *powerup < circle_time as f32 + 0.5 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if *powerup < 2.0 * circle_time as f32 + 0.5 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(-0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + *powerup += dt.0; + } else if *powerup < 2.0 * circle_time as f32 + 1.0 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else { + *powerup = 0.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::QuadLowRanged => { + if dist_sqrd < (5.0 * min_attack_dist * scale).powi(2) { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .try_normalized() + .unwrap_or(Vec2::unit_y()); + inputs.primary.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + if *powerup > 5.0 { + *powerup = 0.0; + } else if *powerup > 2.5 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(1.75 * PI) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + *powerup += dt.0; + } else { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(0.25 * PI) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + *powerup += dt.0; + } + inputs.secondary.set_state(true); + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } else { inputs.move_dir = bearing .xy() .try_normalized() @@ -1374,8 +1121,311 @@ impl<'a> System<'a> for Sys { } else { do_idle = true; } - }, - } + } else { + do_idle = true; + } + }, + Tactic::TailSlap => { + if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { + if *powerup > 4.0 { + inputs.primary.set_state(false); + *powerup = 0.0; + } else if *powerup > 1.0 { + inputs.primary.set_state(true); + *powerup += dt.0; + } else { + inputs.secondary.set_state(true); + *powerup += dt.0; + } + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .try_normalized() + .unwrap_or(Vec2::unit_y()) + * 0.1; + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::QuadLowQuick => { + if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + inputs.secondary.set_state(true); + } else if dist_sqrd + < (3.0 * min_attack_dist * scale).powi(2) + && dist_sqrd > (2.0 * min_attack_dist * scale).powi(2) + { + inputs.primary.set_state(true); + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(-0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::QuadLowBasic => { + if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if *powerup > 5.0 { + *powerup = 0.0; + } else if *powerup > 2.0 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else { + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::QuadMedJump => { + if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + inputs.secondary.set_state(true); + } else if dist_sqrd + < (5.0 * min_attack_dist * scale).powi(2) + { + inputs.ability3.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.primary.set_state(true); + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + } else { + do_idle = true; + } + }, + Tactic::QuadMedBasic => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if *powerup < 2.0 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if *powerup < 3.0 { + inputs.primary.set_state(true); + *powerup += dt.0; + } else { + *powerup = 0.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::Lavadrake => { + if dist_sqrd < (2.5 * min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + inputs.secondary.set_state(true); + } else if dist_sqrd + < (7.0 * min_attack_dist * scale).powi(2) + { + if *powerup < 2.0 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + inputs.primary.set_state(true); + *powerup += dt.0; + } else if *powerup < 4.0 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(-0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + inputs.primary.set_state(true); + *powerup += dt.0; + } else if *powerup < 6.0 { + inputs.ability3.set_state(true); + *powerup += dt.0; + } else { + *powerup = 0.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::Theropod => { + if dist_sqrd < (2.0 * min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + inputs.primary.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, } } else { do_idle = true; @@ -1393,7 +1443,7 @@ impl<'a> System<'a> for Sys { // Choose a new target to attack: only go out of our way to attack targets we // are hostile toward! - if choose_target { + if !agent.activity.is_flee() && choose_target { // Search for new targets (this looks expensive, but it's only run occasionally) // TODO: Replace this with a better system that doesn't consider *all* entities let closest_entity = (&entities, &positions, &healths, alignments.maybe(), char_states.maybe()) @@ -1440,37 +1490,41 @@ impl<'a> System<'a> for Sys { // --- Activity overrides (in reverse order of priority: most important goes // last!) --- + let damage = healths + .get(entity) + .map(|h| h.current() as f32 / h.maximum() as f32) + .unwrap_or(0.5); + // Attack a target that's attacking us if let Some(my_health) = healths.get(entity) { // Only if the attack was recent - if my_health.last_change.0 < 3.0 { + if !agent.activity.is_flee() && my_health.last_change.0 < DAMAGE_MEMORY_DURATION { if let comp::HealthSource::Damage { by: Some(by), .. } = my_health.last_change.1.cause { - if !agent.activity.is_attack() { - if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) - { - if healths.get(attacker).map_or(false, |a| !a.is_dead) { - match agent.activity { - Activity::Attack { target, .. } if target == attacker => {}, - _ => { - if agent.can_speak { - let msg = - "npc.speech.villager_under_attack".to_string(); - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc(*uid, msg), - )); - } - - agent.activity = Activity::Attack { - target: attacker, - chaser: Chaser::default(), - time: time.0, - been_close: false, - powerup: 0.0, - }; - }, + if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) { + if healths.get(attacker).map_or(false, |a| !a.is_dead) { + if 1.0 - agent.psyche.aggro > damage && flees { + if agent.can_speak { + let msg = + "npc.speech.villager_under_attack".to_string(); + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc(*uid, msg), + )); } + agent.activity = Activity::Flee { + target: attacker, + chaser: Chaser::default(), + timer: 0.0, + }; + } else if !agent.activity.is_attack() { + agent.activity = Activity::Attack { + target: attacker, + chaser: Chaser::default(), + time: time.0, + been_close: false, + powerup: 0.0, + }; } } } diff --git a/common/sys/src/character_behavior.rs b/common/sys/src/character_behavior.rs index 20698f351a..86f760f620 100644 --- a/common/sys/src/character_behavior.rs +++ b/common/sys/src/character_behavior.rs @@ -231,6 +231,7 @@ impl<'a> System<'a> for Sys { let j = JoinData::new(&tuple, &updater, &dt); let mut state_update = match j.character { CharacterState::Idle => states::idle::Data.handle_event(&j, action), + CharacterState::Talk => states::talk::Data.handle_event(&j, action), CharacterState::Climb => states::climb::Data.handle_event(&j, action), CharacterState::Glide => states::glide::Data.handle_event(&j, action), CharacterState::GlideWield => { @@ -274,6 +275,7 @@ impl<'a> System<'a> for Sys { let mut state_update = match j.character { CharacterState::Idle => states::idle::Data.behavior(&j), + CharacterState::Talk => states::talk::Data.behavior(&j), CharacterState::Climb => states::climb::Data.behavior(&j), CharacterState::Glide => states::glide::Data.behavior(&j), CharacterState::GlideWield => states::glide_wield::Data.behavior(&j), diff --git a/common/sys/src/controller.rs b/common/sys/src/controller.rs index 82afd64f4f..cabf205259 100644 --- a/common/sys/src/controller.rs +++ b/common/sys/src/controller.rs @@ -98,6 +98,13 @@ impl<'a> System<'a> for Sys { ControlEvent::DisableLantern => { server_emitter.emit(ServerEvent::DisableLantern(entity)) }, + ControlEvent::Interact(npc_uid) => { + if let Some(npc_entity) = + uid_allocator.retrieve_entity_internal(npc_uid.id()) + { + server_emitter.emit(ServerEvent::NpcInteract(entity, npc_entity)); + } + }, ControlEvent::InventoryManip(manip) => { // Unwield if a wielded equipment slot is being modified, to avoid entering // a barehanded wielding state. diff --git a/common/sys/src/stats.rs b/common/sys/src/stats.rs index 111a93089d..f3d3a74ba0 100644 --- a/common/sys/src/stats.rs +++ b/common/sys/src/stats.rs @@ -168,6 +168,7 @@ impl<'a> System<'a> for Sys { match character_state { // Accelerate recharging energy. CharacterState::Idle { .. } + | CharacterState::Talk { .. } | CharacterState::Sit { .. } | CharacterState::Dance { .. } | CharacterState::Sneak { .. } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 4090e9be0a..bf69e76cd1 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -2,7 +2,7 @@ use specs::{world::WorldExt, Entity as EcsEntity}; use tracing::error; use common::{ - comp::{self, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos}, + comp::{self, agent::AgentEvent, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos}, consts::MAX_MOUNT_RANGE, uid::Uid, }; @@ -55,6 +55,19 @@ pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) { } } +pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_entity: EcsEntity) { + let state = server.state_mut(); + if let Some(agent) = state + .ecs() + .write_storage::() + .get_mut(npc_entity) + { + if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) { + agent.inbox.push_front(AgentEvent::Talk(interactor_uid)); + } + } +} + pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity) { let state = server.state_mut(); diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 5c98f18a25..a66352ee3d 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -12,7 +12,9 @@ use entity_manipulation::{ handle_explosion, handle_knockback, handle_land_on_ground, handle_poise, handle_respawn, }; use group_manip::handle_group; -use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount}; +use interaction::{ + handle_lantern, handle_mount, handle_npc_interaction, handle_possess, handle_unmount, +}; use inventory_manip::handle_inventory; use player::{handle_client_disconnect, handle_exit_ingame}; use specs::{Entity as EcsEntity, WorldExt}; @@ -98,6 +100,9 @@ impl Server { }, ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true), ServerEvent::DisableLantern(entity) => handle_lantern(self, entity, false), + ServerEvent::NpcInteract(interactor, target) => { + handle_npc_interaction(self, interactor, target) + }, ServerEvent::Mount(mounter, mountee) => handle_mount(self, mounter, mountee), ServerEvent::Unmount(mounter) => handle_unmount(self, mounter), ServerEvent::Possess(possessor_uid, possesse_uid) => { diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index dfaaa6babe..3d4e506fc7 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -3,7 +3,7 @@ use common::{comp::inventory::loadout_builder::LoadoutBuilder, store::Id, terrai use world::{ civ::{Site, Track}, util::RandomPerm, - World, + IndexRef, World, }; pub struct Entity { @@ -123,7 +123,7 @@ impl Entity { .build() } - pub fn tick(&mut self, terrain: &TerrainGrid, world: &World) { + pub fn tick(&mut self, terrain: &TerrainGrid, world: &World, index: &IndexRef) { let tgt_site = self.brain.tgt.or_else(|| { world .civs() @@ -146,6 +146,10 @@ impl Entity { tgt_site.map(|tgt_site| { let site = &world.civs().sites[tgt_site]; + let destination_name = site + .site_tmp + .map_or("".to_string(), |id| index.sites[id].name().to_string()); + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; @@ -171,7 +175,7 @@ impl Entity { )) .map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0); - self.controller.travel_to = Some(travel_to); + self.controller.travel_to = Some((travel_to, destination_name)); self.controller.speed_factor = 0.70; }); } diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 9a92926740..002c414f23 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -36,7 +36,7 @@ impl<'a> System<'a> for Sys { mut rtsim, terrain, world, - _index, + index, positions, rtsim_entities, mut agents, @@ -60,10 +60,10 @@ impl<'a> System<'a> for Sys { to_reify.push(id); } else { // Simulate behaviour - if let Some(travel_to) = entity.controller.travel_to { + if let Some(travel_to) = &entity.controller.travel_to { // Move towards target at approximate character speed entity.pos += Vec3::from( - (travel_to.xy() - entity.pos.xy()) + (travel_to.0.xy() - entity.pos.xy()) .try_normalized() .unwrap_or_else(Vec2::zero) * entity.get_body().max_speed_approx() @@ -81,7 +81,7 @@ impl<'a> System<'a> for Sys { // Tick entity AI if entity.last_tick + ENTITY_TICK_PERIOD <= rtsim.tick { - entity.tick(&terrain, &world); + entity.tick(&terrain, &world, &index.as_index_ref()); entity.last_tick = rtsim.tick; } } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index b6c2240686..a0dc128cdb 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -516,6 +516,8 @@ impl PlayState for SessionState { .is_some() { client.pick_up(entity); + } else { + client.npc_interact(entity); } }, } @@ -1495,12 +1497,10 @@ fn select_interactable( scales.maybe(), colliders.maybe(), char_states.maybe(), - // Must have this comp to be interactable (for now) - &ecs.read_storage::(), ) .join() - .filter(|(e, _, _, _, _, _)| *e != player_entity) - .map(|(e, p, s, c, cs, _)| { + .filter(|(e, _, _, _, _)| *e != player_entity) + .map(|(e, p, s, c, cs)| { let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), cs); (e, cylinder) })