From 23b1df3cdd9b8bf1803fc2e3649749124c5b170e Mon Sep 17 00:00:00 2001
From: James Melkonian <jemelkonian@gmail.com>
Date: Sun, 31 Jan 2021 20:29:50 +0000
Subject: [PATCH] Add basic NPC interaction and fix NPC chat spamming

---
 CHANGELOG.md                         |    3 +-
 assets/voxygen/i18n/en/_manifest.ron |   29 +
 client/src/lib.rs                    |   17 +
 common/src/comp/agent.rs             |   45 +
 common/src/comp/character_state.rs   |    2 +
 common/src/comp/controller.rs        |    2 +
 common/src/event.rs                  |    1 +
 common/src/path.rs                   |    3 +-
 common/src/rtsim.rs                  |    2 +-
 common/src/states/behavior.rs        |    2 +
 common/src/states/idle.rs            |    6 +
 common/src/states/mod.rs             |    1 +
 common/src/states/talk.rs            |   49 +
 common/src/states/utils.rs           |    6 +
 common/sys/src/agent.rs              | 1858 +++++++++++++-------------
 common/sys/src/character_behavior.rs |    2 +
 common/sys/src/controller.rs         |    7 +
 common/sys/src/stats.rs              |    1 +
 server/src/events/interaction.rs     |   15 +-
 server/src/events/mod.rs             |    7 +-
 server/src/rtsim/entity.rs           |   10 +-
 server/src/rtsim/tick.rs             |    8 +-
 voxygen/src/session.rs               |    8 +-
 23 files changed, 1166 insertions(+), 918 deletions(-)
 create mode 100644 common/src/states/talk.rs

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::<comp::Health>()
+            .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<Uid, PlayerInfo> { &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<AgentEvent>,
 }
 
 impl Agent {
@@ -179,6 +213,10 @@ impl Component for Agent {
 
 #[derive(Clone, Debug)]
 pub enum Activity {
+    Interact {
+        timer: f32,
+        interaction: AgentEvent,
+    },
     Idle {
         bearing: Vec2<f32>,
         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<T> FromIterator<T> for Path<T> {
     }
 }
 
-#[allow(clippy::len_without_is_empty)] // TODO: Pending review in #587
 impl<T> Path<T> {
+    pub fn is_empty(&self) -> bool { self.nodes.is_empty() }
+
     pub fn len(&self) -> usize { self.nodes.len() }
 
     pub fn iter(&self) -> impl Iterator<Item = &T> { 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<Vec3<f32>>,
+    pub travel_to: Option<(Vec3<f32>, 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() > 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() < 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::<f32>() > 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::<f32>() < 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::<comp::Agent>()
+        .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::<comp::Item>(),
             )
                 .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)
                 })