diff --git a/assets/common/abilities/unique/turret/basic.ron b/assets/common/abilities/unique/turret/basic.ron new file mode 100644 index 0000000000..c1a324eb49 --- /dev/null +++ b/assets/common/abilities/unique/turret/basic.ron @@ -0,0 +1,15 @@ +BasicRanged( + energy_cost: 0, + buildup_duration: 500, + recover_duration: 300, + projectile: Arrow( + damage: 200.0, + knockback: 5.0, + energy_regen: 100, + ), + projectile_body: Object(ArrowTurret), + projectile_light: None, + projectile_gravity: Some(Gravity(0.1)), + projectile_speed: 100.0, + can_continue: true, +) \ No newline at end of file diff --git a/assets/common/abilities/weapon_ability_manifest.ron b/assets/common/abilities/weapon_ability_manifest.ron index 465d1a4040..9682d4a883 100644 --- a/assets/common/abilities/weapon_ability_manifest.ron +++ b/assets/common/abilities/weapon_ability_manifest.ron @@ -132,6 +132,11 @@ secondary: "common.abilities.unique.theropodbird.triplestrike", abilities: [], ), + Unique(Turret): ( + primary: "common.abilities.unique.turret.basic", + secondary: "common.abilities.unique.turret.basic", + skills: [], + ), Debug: ( primary: "common.abilities.debug.forwardboost", secondary: "common.abilities.debug.upboost", diff --git a/assets/common/items/npc_weapons/unique/turret.ron b/assets/common/items/npc_weapons/unique/turret.ron new file mode 100644 index 0000000000..736cf71775 --- /dev/null +++ b/assets/common/items/npc_weapons/unique/turret.ron @@ -0,0 +1,15 @@ +ItemDef( + name: "Turret", + description: "Turret weapon", + kind: Tool( + ( + kind: Unique(Turret), + stats: ( + equip_time_millis: 10, + power: 1.00, + speed: 1.00, + ), + ) + ), + quality: Low, +) diff --git a/assets/voxygen/voxel/object/crossbow.vox b/assets/voxygen/voxel/object/crossbow.vox new file mode 100644 index 0000000000..bbf5a5fc0c --- /dev/null +++ b/assets/voxygen/voxel/object/crossbow.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f4824ad33b17103b0701734007a231d353cdce3950dbe1f4f61ccd25bb1f480 +size 5784 diff --git a/assets/voxygen/voxel/object_manifest.ron b/assets/voxygen/voxel/object_manifest.ron index d379e65561..377d477acf 100644 --- a/assets/voxygen/voxel/object_manifest.ron +++ b/assets/voxygen/voxel/object_manifest.ron @@ -377,4 +377,16 @@ central: ("object.steak"), ) ), + Crossbow: ( + bone0: ( + offset: (-18.0, -15.5, 0.0), + central: ("object.crossbow"), + ) + ), + ArrowTurret: ( + bone0: ( + offset: (-1.5, -6.5, -1.5), + central: ("weapon.projectile.turret-arrow"), + ) + ), }) \ No newline at end of file diff --git a/assets/voxygen/voxel/weapon/projectile/turret-arrow.vox b/assets/voxygen/voxel/weapon/projectile/turret-arrow.vox new file mode 100644 index 0000000000..0cae2698f1 --- /dev/null +++ b/assets/voxygen/voxel/weapon/projectile/turret-arrow.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2f2517cf5bba387a4c56a4c872ccc8bc41956dd255e32bcd52cfd1d8e69b5a5 +size 1184 diff --git a/common/src/comp/body/object.rs b/common/src/comp/body/object.rs index f0bcdeacac..23b971c08f 100644 --- a/common/src/comp/body/object.rs +++ b/common/src/comp/body/object.rs @@ -70,6 +70,8 @@ make_case_elim!( BoltNature = 60, MeatDrop = 61, Steak = 62, + Crossbow = 63, + ArrowTurret = 64, } ); @@ -80,7 +82,7 @@ impl Body { } } -pub const ALL_OBJECTS: [Body; 63] = [ +pub const ALL_OBJECTS: [Body; 65] = [ Body::Arrow, Body::Bomb, Body::Scarecrow, @@ -144,6 +146,8 @@ pub const ALL_OBJECTS: [Body; 63] = [ Body::BoltNature, Body::MeatDrop, Body::Steak, + Body::Crossbow, + Body::ArrowTurret, ]; impl From for super::Body { @@ -216,6 +220,8 @@ impl Body { Body::BoltNature => "bolt_nature", Body::MeatDrop => "meat_drop", Body::Steak => "steak", + Body::Crossbow => "crossbow", + Body::ArrowTurret => "arrow_turret", } } } diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 2d94ab70d5..4f95fabb40 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -295,4 +295,5 @@ pub enum UniqueKind { QuadSmallBasic, TheropodBasic, TheropodBird, + Turret, } diff --git a/common/src/comp/inventory/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs index ff81dcf0df..ece989aca2 100644 --- a/common/src/comp/inventory/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -5,7 +5,7 @@ use crate::comp::{ slot::{ArmorSlot, EquipSlot}, }, item::{Item, ItemKind}, - quadruped_low, quadruped_medium, theropod, Body, + object, quadruped_low, quadruped_medium, theropod, Body, }; use rand::Rng; @@ -227,6 +227,14 @@ impl LoadoutBuilder { )); }, }, + Body::Object(object) => match object { + object::Body::Crossbow => { + main_tool = Some(Item::new_from_asset_expect( + "common.items.npc_weapons.unique.turret", + )); + }, + _ => {}, + }, _ => {}, }; } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 9abcfb8184..0374e32df0 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -66,7 +66,7 @@ impl Body { Body::BirdSmall(_) => 75.0, Body::FishSmall(_) => 60.0, Body::BipedLarge(_) => 75.0, - Body::Object(_) => 40.0, + Body::Object(_) => 0.0, Body::Golem(_) => 60.0, Body::Theropod(_) => 135.0, Body::QuadrupedLow(quadruped_low) => match quadruped_low.species { @@ -116,7 +116,7 @@ impl Body { Body::BirdSmall(_) => 35.0, Body::FishSmall(_) => 10.0, Body::BipedLarge(_) => 12.0, - Body::Object(_) => 5.0, + Body::Object(_) => 0.0, Body::Golem(_) => 8.0, Body::Theropod(theropod) => match theropod.species { theropod::Species::Archaeos => 2.5, diff --git a/common/sys/src/agent.rs b/common/sys/src/agent.rs new file mode 100644 index 0000000000..9684b580f2 --- /dev/null +++ b/common/sys/src/agent.rs @@ -0,0 +1,1566 @@ +use common::{ + comp::{ + self, + agent::Activity, + group, + group::Invite, + inventory::slot::EquipSlot, + item::{ + tool::{ToolKind, UniqueKind}, + ItemKind, + }, + skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, + Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, + GroupManip, Health, Inventory, LightEmitter, MountState, Ori, PhysicsState, Pos, Scale, + Stats, UnresolvedChatMsg, Vel, + }, + event::{EventBus, ServerEvent}, + metrics::SysMetrics, + path::{Chaser, TraversalConfig}, + resources::{DeltaTime, Time, TimeOfDay}, + span, + terrain::{Block, TerrainGrid}, + time::DayPeriod, + uid::{Uid, UidAllocator}, + util::Dir, + vol::ReadVol, +}; +use rand::{thread_rng, Rng}; +use rayon::iter::ParallelIterator; +use specs::{ + saveload::{Marker, MarkerAllocator}, + Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, System, Write, WriteStorage, +}; +use std::f32::consts::PI; +use vek::*; + +/// This system will allow NPCs to modify their controller +pub struct Sys; +impl<'a> System<'a> for Sys { + #[allow(clippy::type_complexity)] + type SystemData = ( + ( + Read<'a, UidAllocator>, + Read<'a, Time>, + Read<'a, DeltaTime>, + Read<'a, group::GroupManager>, + ), + ReadExpect<'a, SysMetrics>, + Write<'a, EventBus>, + Entities<'a>, + ReadStorage<'a, Energy>, + ReadStorage<'a, Pos>, + ReadStorage<'a, Vel>, + ReadStorage<'a, Ori>, + ReadStorage<'a, Scale>, + ReadStorage<'a, Health>, + ReadStorage<'a, Inventory>, + ReadStorage<'a, Stats>, + ReadStorage<'a, PhysicsState>, + ReadStorage<'a, Uid>, + ReadStorage<'a, group::Group>, + ReadExpect<'a, TerrainGrid>, + ReadStorage<'a, Alignment>, + ReadStorage<'a, Body>, + WriteStorage<'a, Agent>, + WriteStorage<'a, Controller>, + ReadStorage<'a, MountState>, + ReadStorage<'a, Invite>, + Read<'a, TimeOfDay>, + ReadStorage<'a, LightEmitter>, + ReadStorage<'a, CharacterState>, + ); + + #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 + fn run( + &mut self, + ( + (uid_allocator, time, dt, group_manager), + sys_metrics, + event_bus, + entities, + energies, + positions, + velocities, + orientations, + scales, + healths, + inventories, + stats, + physics_states, + uids, + groups, + terrain, + alignments, + bodies, + mut agents, + mut controllers, + mount_states, + invites, + time_of_day, + light_emitter, + char_states, + ): Self::SystemData, + ) { + let start_time = std::time::Instant::now(); + span!(_guard, "run", "agent::Sys::run"); + + ( + &entities, + &energies, + &positions, + &velocities, + &orientations, + alignments.maybe(), + &inventories, + &stats, + &physics_states, + bodies.maybe(), + &uids, + &mut agents, + &mut controllers, + mount_states.maybe(), + groups.maybe(), + light_emitter.maybe(), + ) + .par_join() + .filter(|(_, _, _, _, _, _, _, _, _, _, _, _, _, mount_state, _, _)| { + // Skip mounted entities + mount_state.map(|ms| *ms == MountState::Unmounted).unwrap_or(true) + }) + .for_each(|( + entity, + energy, + pos, + vel, + ori, + alignment, + inventory, + stats, + physics_state, + body, + uid, + agent, + controller, + _, + group, + light_emitter, + )| { + // Hack, replace with better system when groups are more sophisticated + // Override alignment if in a group unless entity is owned already + let alignment = if !matches!(alignment, Some(Alignment::Owned(_))) { + group + .and_then(|g| group_manager.group_info(*g)) + .and_then(|info| uids.get(info.leader)) + .copied() + .map(Alignment::Owned) + .or(alignment.copied()) + } else { + alignment.copied() + }; + + controller.reset(); + let mut event_emitter = event_bus.emitter(); + // Light lanterns at night + // TODO Add a method to turn on NPC lanterns underground + let lantern_equipped = inventory.equipped(EquipSlot::Lantern).as_ref().map_or(false, |item| { + matches!(item.kind(), comp::item::ItemKind::Lantern(_)) + }); + let lantern_turned_on = light_emitter.is_some(); + let day_period = DayPeriod::from(time_of_day.0); + // Only emit event for agents that have a lantern equipped + if lantern_equipped { + let mut rng = thread_rng(); + if day_period.is_dark() && !lantern_turned_on { + // Agents with turned off lanterns turn them on randomly once it's nighttime and + // keep them on + // Only emit event for agents that sill need to + // turn on their lantern + if let 0 = rng.gen_range(0..1000) { + controller.events.push(ControlEvent::EnableLantern) + } + } else if lantern_turned_on && day_period.is_light() { + // agents with turned on lanterns turn them off randomly once it's daytime and + // keep them off + if let 0 = rng.gen_range(0..2000) { + controller.events.push(ControlEvent::DisableLantern) + } + } + }; + + let mut inputs = &mut controller.inputs; + + // Default to looking in orientation direction (can be overridden below) + //inputs.look_dir = ori.0; + + const AVG_FOLLOW_DIST: f32 = 6.0; + const MAX_FOLLOW_DIST: f32 = 12.0; + const MAX_CHASE_DIST: f32 = 18.0; + const LISTEN_DIST: f32 = 16.0; + const SEARCH_DIST: f32 = 48.0; + const SIGHT_DIST: f32 = 80.0; + const MAX_FLEE_DIST: f32 = 20.0; + const SNEAK_COEFFICIENT: f32 = 0.25; + + 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); + + // 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 + // the world (especially since they would otherwise get stuck on + // obstacles that smaller entities would not). + let node_tolerance = scale * 1.5; + let slow_factor = body.map(|b| b.base_accel() / 250.0).unwrap_or(0.0).min(1.0); + let traversal_config = TraversalConfig { + node_tolerance, + slow_factor, + on_ground: physics_state.on_ground, + in_liquid: physics_state.in_liquid.is_some(), + min_tgt_dist: 1.0, + can_climb: body.map(|b| b.can_climb()).unwrap_or(false), + can_fly: body.map(|b| b.can_fly()).unwrap_or(false), + }; + + let mut do_idle = false; + let mut choose_target = false; + + 'activity: { + match &mut agent.activity { + Activity::Idle { bearing, chaser } => { + if let Some(travel_to) = 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 + .ray( + pos.0, + pos.0 + (Vec3::unit_z() * 3.0)) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_some())); + if let Some((bearing, speed)) = + chaser.chase(&*terrain, pos.0, vel.0, travel_to, TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }) + { + inputs.move_dir = + bearing.xy().try_normalized().unwrap_or(Vec2::zero()) + * speed.min(agent.rtsim_controller.speed_factor); + inputs.jump.set_state(bearing.z > 1.5 || traversal_config.can_fly && traversal_config.on_ground); + inputs.climb = Some(comp::Climb::Up); + //.filter(|_| bearing.z > 0.1 || physics_state.in_liquid.is_some()); + + inputs.move_z = bearing.z + if traversal_config.can_fly { + if terrain + .ray( + pos.0 + Vec3::unit_z(), + pos.0 + + bearing + .try_normalized() + .unwrap_or(Vec3::unit_y()) + * 60.0 + + Vec3::unit_z(), + ) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_some()) + { + 1.0 //fly up when approaching obstacles + } else { -0.1 } //flying things should slowly come down from the stratosphere + } else { + 0.05 //normal land traveller offset + }; + } + } else { + *bearing += Vec2::new( + thread_rng().gen::() - 0.5, + thread_rng().gen::() - 0.5, + ) * 0.1 + - *bearing * 0.003 + - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { + (pos.0 - patrol_origin).xy() * 0.0002 + }); + + // Stop if we're too close to a wall + *bearing *= 0.1 + + if terrain + .ray( + pos.0 + Vec3::unit_z(), + pos.0 + + Vec3::from(*bearing) + .try_normalized() + .unwrap_or(Vec3::unit_y()) + * 5.0 + + Vec3::unit_z(), + ) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_none()) + { + 0.9 + } else { + 0.0 + }; + + if bearing.magnitude_squared() > 0.5f32.powi(2) { + inputs.move_dir = *bearing * 0.65; + } + + // Put away weapon + if thread_rng().gen::() < 0.005 { + controller.actions.push(ControlAction::Unwield); + } + + // Sit + if thread_rng().gen::() < 0.0035 { + controller.actions.push(ControlAction::Sit); + } + } + + controller.actions.push(ControlAction::Unwield); + + // Sometimes try searching for new targets + if thread_rng().gen::() < 0.1 { + choose_target = true; + } + }, + Activity::Follow { target, chaser } => { + if let (Some(tgt_pos), _tgt_health) = + (positions.get(*target), healths.get(*target)) + { + let dist = pos.0.distance(tgt_pos.0); + // Follow, or return to idle + if dist > AVG_FOLLOW_DIST { + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: AVG_FOLLOW_DIST, + ..traversal_config + }, + ) { + inputs.move_dir = + bearing.xy().try_normalized().unwrap_or(Vec2::zero()) + * speed.min(0.2 + (dist - AVG_FOLLOW_DIST) / 8.0); + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + } else { + do_idle = true; + } + }, + Activity::Attack { + target, + chaser, + been_close, + 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, + Turret, + } + + let tactic = match inventory.equipped(EquipSlot::Mainhand).as_ref().and_then(|item| { + if let ItemKind::Tool(tool) = &item.kind() { + Some(&tool.kind) + } else { + None + } + }) { + Some(ToolKind::Bow) => Tactic::Bow, + Some(ToolKind::Staff) => Tactic::Staff, + Some(ToolKind::Hammer) => Tactic::Hammer, + Some(ToolKind::Sword) => Tactic::Sword, + Some(ToolKind::Axe) => Tactic::Axe, + Some(ToolKind::Unique(UniqueKind::StoneGolemFist)) => { + Tactic::StoneGolemBoss + }, + Some(ToolKind::Unique(UniqueKind::QuadMedQuick)) => { + Tactic::CircleCharge { + radius: 3, + circle_time: 2, + } + }, + Some(ToolKind::Unique(UniqueKind::QuadMedCharge)) => { + Tactic::CircleCharge { + radius: 15, + circle_time: 1, + } + }, + + Some(ToolKind::Unique(UniqueKind::QuadMedJump)) => Tactic::QuadMedJump, + Some(ToolKind::Unique(UniqueKind::QuadMedBasic)) => { + Tactic::QuadMedBasic + }, + Some(ToolKind::Unique(UniqueKind::QuadLowRanged)) => { + Tactic::QuadLowRanged + }, + Some(ToolKind::Unique(UniqueKind::QuadLowTail)) => Tactic::TailSlap, + Some(ToolKind::Unique(UniqueKind::QuadLowQuick)) => { + Tactic::QuadLowQuick + }, + Some(ToolKind::Unique(UniqueKind::QuadLowBasic)) => { + Tactic::QuadLowBasic + }, + Some(ToolKind::Unique(UniqueKind::QuadLowBreathe)) => Tactic::Lavadrake, + Some(ToolKind::Unique(UniqueKind::TheropodBasic)) => Tactic::Theropod, + Some(ToolKind::Unique(UniqueKind::TheropodBird)) => Tactic::Theropod, + Some(ToolKind::Unique(UniqueKind::Turret)) => Tactic::Turret, + _ => Tactic::Melee, + }; + + if let (Some(tgt_pos), Some(tgt_health), tgt_alignment) = ( + positions.get(*target), + healths.get(*target), + alignments.get(*target).copied().unwrap_or( + uids.get(*target) + .copied() + .map(Alignment::Owned) + .unwrap_or(Alignment::Wild), + ), + ) { + // Wield the weapon as running towards the target + controller.actions.push(ControlAction::Wield); + + 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()) + + // Special case for jumping attacks to jump at the body + // of the target and not the ground around the target + // For the ranged it is to shoot at the feet and not + // the head to get splash damage + if tactic == Tactic::QuadMedJump { + 1.0 + } else if matches!(tactic, Tactic::QuadLowRanged) { + -1.0 + } else { + 0.0 + }; + + // Hacky distance offset for ranged weapons + let distance_offset = match tactic { + Tactic::Bow => 0.0004 /* Yay magic numbers */ * pos.0.distance_squared(tgt_pos.0), + Tactic::Staff => 0.0015 /* Yay magic numbers */ * pos.0.distance_squared(tgt_pos.0), + Tactic::QuadLowRanged => 0.03 /* Yay magic numbers */ * pos.0.distance_squared(tgt_pos.0), + _ => 0.0, + }; + + // Apply the distance and eye offsets to make the + // look_dir the vector from projectile launch to + // target point + if let Some(dir) = Dir::from_unnormalized( + Vec3::new( + tgt_pos.0.x, + tgt_pos.0.y, + tgt_pos.0.z + tgt_eye_offset + distance_offset, + ) - Vec3::new(pos.0.x, pos.0.y, pos.0.z + eye_offset), + ) { + inputs.look_dir = dir; + } + + // Don't attack entities we are passive towards + // TODO: This is here, it's a bit of a hack + if let Some(alignment) = alignment { + if alignment.passive_towards(tgt_alignment) || tgt_health.is_dead { + do_idle = true; + break 'activity; + } + } + + 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) + .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; + } + } 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; + } + + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Axe => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if *powerup > 6.0 { + inputs.secondary.set_state(false); + *powerup = 0.0; + } else if *powerup > 4.0 && energy.current() > 10 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if stats.skill_set.has_skill(Skill::Axe(AxeSkill::UnlockLeap)) && energy.current() > 800 && thread_rng().gen_bool(0.5) { + inputs.ability3.set_state(true); + *powerup += dt.0; + } else { + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Hammer => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if *powerup > 4.0 { + inputs.secondary.set_state(false); + *powerup = 0.0; + } else if *powerup > 2.0 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if stats.skill_set.has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) && energy.current() > 700 + && thread_rng().gen_bool(0.9) { + inputs.ability3.set_state(true); + *powerup += dt.0; + } else { + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if stats.skill_set.has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) && *powerup > 5.0 { + inputs.ability3.set_state(true); + *powerup = 0.0; + } else { + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Sword => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + if stats.skill_set.has_skill(Skill::Sword(SwordSkill::UnlockSpin)) && *powerup < 2.0 && energy.current() > 600 { + inputs.ability3.set_state(true); + *powerup += dt.0; + } else if *powerup > 2.0 { + *powerup = 0.0; + } else { + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if *powerup > 4.0 { + inputs.secondary.set_state(true); + *powerup = 0.0; + } else { + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Bow => { + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (2.0 * min_attack_dist * scale).powi(2) { + inputs.roll.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .rotated_z( + thread_rng().gen_range(0.5..1.57), + ) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if *powerup > 4.0 { + inputs.secondary.set_state(false); + *powerup = 0.0; + } else if *powerup > 2.0 + && energy.current() > 300 + { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if stats.skill_set.has_skill(Skill::Bow(BowSkill::UnlockRepeater)) && energy.current() > 400 + && thread_rng().gen_bool(0.8) + { + inputs.secondary.set_state(false); + inputs.ability3.set_state(true); + *powerup += dt.0; + } else { + inputs.secondary.set_state(false); + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::Staff => { + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.roll.set_state(true); + } else if dist_sqrd + < (5.0 * min_attack_dist * scale).powi(2) + { + if *powerup < 1.5 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + *powerup += dt.0; + } else if *powerup < 3.0 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(-0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + *powerup += dt.0; + } else { + *powerup = 0.0; + } + if stats.skill_set.has_skill(Skill::Staff(StaffSkill::UnlockShockwave)) && energy.current() > 800 + && thread_rng().gen::() > 0.8 + { + inputs.ability3.set_state(true); + } else if energy.current() > 10 { + inputs.secondary.set_state(true); + } else { + inputs.primary.set_state(true); + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .rotated_z( + thread_rng().gen_range(-1.57..-0.5), + ) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.primary.set_state(true); + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < 16.0f32.powi(2) + && thread_rng().gen::() < 0.02 + { + inputs.roll.set_state(true); + } + } else { + do_idle = true; + } + }, + Tactic::StoneGolemBoss => { + if dist_sqrd < (min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + inputs.primary.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if vel.0.is_approx_zero() { + inputs.ability3.set_state(true); + } + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + if *powerup > 5.0 { + inputs.secondary.set_state(true); + *powerup = 0.0; + } else { + *powerup += dt.0; + } + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } + } else { + do_idle = true; + } + }, + Tactic::CircleCharge { + radius, + circle_time, + } => { + if dist_sqrd < (min_attack_dist * scale).powi(2) + && thread_rng().gen_bool(0.5) + { + inputs.move_dir = Vec2::zero(); + inputs.primary.set_state(true); + } else if dist_sqrd + < (radius as f32 * min_attack_dist * scale).powi(2) + { + inputs.move_dir = (pos.0 - tgt_pos.0) + .xy() + .try_normalized() + .unwrap_or(Vec2::unit_y()); + } else if dist_sqrd + < ((radius as f32 + 1.0) * min_attack_dist * scale) + .powi(2) + && dist_sqrd + > (radius as f32 * min_attack_dist * scale).powi(2) + { + if *powerup < circle_time as f32 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + *powerup += dt.0; + } else if *powerup < circle_time as f32 + 0.5 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else if *powerup < 2.0 * circle_time as f32 + 0.5 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(-0.47 * PI) + .try_normalized() + .unwrap_or(Vec2::unit_y()); + *powerup += dt.0; + } else if *powerup < 2.0 * circle_time as f32 + 1.0 { + inputs.secondary.set_state(true); + *powerup += dt.0; + } else { + *powerup = 0.0; + } + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::QuadLowRanged => { + if dist_sqrd < (5.0 * min_attack_dist * scale).powi(2) { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .try_normalized() + .unwrap_or(Vec2::unit_y()); + inputs.primary.set_state(true); + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) { + if *powerup > 5.0 { + *powerup = 0.0; + } else if *powerup > 2.5 { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(1.75 * PI) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + *powerup += dt.0; + } else { + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .rotated_z(0.25 * PI) + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + *powerup += dt.0; + } + inputs.secondary.set_state(true); + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } else { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + } else { + do_idle = true; + } + }, + Tactic::TailSlap => { + if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { + if *powerup > 4.0 { + inputs.primary.set_state(false); + *powerup = 0.0; + } else if *powerup > 1.0 { + inputs.primary.set_state(true); + *powerup += dt.0; + } else { + inputs.secondary.set_state(true); + *powerup += dt.0; + } + inputs.move_dir = (tgt_pos.0 - pos.0) + .xy() + .try_normalized() + .unwrap_or(Vec2::unit_y()) + * 0.1; + } else if dist_sqrd < MAX_CHASE_DIST.powi(2) + || (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close) + { + if dist_sqrd < MAX_CHASE_DIST.powi(2) { + *been_close = true; + } + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..traversal_config + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed; + inputs.jump.set_state(bearing.z > 1.5); + inputs.move_z = bearing.z; + } + } else { + do_idle = true; + } + }, + Tactic::QuadLowQuick => { + if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) { + inputs.move_dir = Vec2::zero(); + 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; + } + }, + Tactic::Turret => { + inputs.look_dir = ori.0; + if can_see_tgt(&*terrain, pos, tgt_pos, dist_sqrd) + { + inputs.primary.set_state(true); + } else { + do_idle = true; + } + }, + } + } + } else { + do_idle = true; + } + }, + } + } + + if do_idle { + agent.activity = Activity::Idle { + bearing: Vec2::zero(), + chaser: Chaser::default(), + }; + } + + // Choose a new target to attack: only go out of our way to attack targets we + // are hostile toward! + if 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()) + .join() + .filter(|(e, e_pos, e_health, e_alignment, char_state)| { + let mut search_dist = SEARCH_DIST; + let mut listen_dist = LISTEN_DIST; + if char_state.map_or(false, |c_s| c_s.is_stealthy()) { + // TODO: make sneak more effective based on a stat like e_stats.fitness + search_dist *= SNEAK_COEFFICIENT; + listen_dist *= SNEAK_COEFFICIENT; + } + ((e_pos.0.distance_squared(pos.0) < search_dist.powi(2) && + // Within our view + (e_pos.0 - pos.0).try_normalized().map(|v| v.dot(*inputs.look_dir) > 0.15).unwrap_or(true)) + // Within listen distance + || e_pos.0.distance_squared(pos.0) < listen_dist.powi(2)) + && *e != entity + && !e_health.is_dead + && alignment + .and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))) + .unwrap_or(false) + }) + // Can we even see them? + .filter(|(_, e_pos, _, _, _)| terrain + .ray(pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z()) + .until(Block::is_opaque) + .cast() + .0 >= e_pos.0.distance(pos.0)) + .min_by_key(|(_, e_pos, _, _, _)| (e_pos.0.distance_squared(pos.0) * 100.0) as i32) + .map(|(e, _, _, _, _)| e); + + if let Some(target) = closest_entity { + agent.activity = Activity::Attack { + target, + chaser: Chaser::default(), + time: time.0, + been_close: false, + powerup: 0.0, + }; + } + } + + // --- Activity overrides (in reverse order of priority: most important goes + // last!) --- + + // 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 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, + }; + }, + } + } + } + } + } + } + } + + // Follow owner if we're too far, or if they're under attack + if let Some(Alignment::Owned(owner)) = alignment { + (|| { + let owner = uid_allocator.retrieve_entity_internal(owner.id())?; + + let owner_pos = positions.get(owner)?; + let dist_sqrd = pos.0.distance_squared(owner_pos.0); + if dist_sqrd > MAX_FOLLOW_DIST.powi(2) && !agent.activity.is_follow() { + agent.activity = Activity::Follow { + target: owner, + chaser: Chaser::default(), + }; + } + + // Attack owner's attacker + let owner_health = healths.get(owner)?; + if owner_health.last_change.0 < 5.0 && owner_health.last_change.1.amount < 0 { + if let comp::HealthSource::Damage { by: Some(by), .. } = + owner_health.last_change.1.cause + { + if !agent.activity.is_attack() { + let attacker = uid_allocator.retrieve_entity_internal(by.id())?; + + agent.activity = Activity::Attack { + target: attacker, + chaser: Chaser::default(), + time: time.0, + been_close: false, + powerup: 0.0, + }; + } + } + } + + Some(()) + })(); + } + + debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); + debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); + }); + + // Process group invites + for (_invite, /*alignment,*/ agent, controller) in + (&invites, /*&alignments,*/ &mut agents, &mut controllers).join() + { + let accept = false; // set back to "matches!(alignment, Alignment::Npc)" when we got better NPC recruitment mechanics + if accept { + // Clear agent comp + *agent = Agent::default(); + controller + .events + .push(ControlEvent::GroupManip(GroupManip::Accept)); + } else { + controller + .events + .push(ControlEvent::GroupManip(GroupManip::Decline)); + } + } + sys_metrics.agent_ns.store( + start_time.elapsed().as_nanos() as u64, + std::sync::atomic::Ordering::Relaxed, + ); + } +} + +fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool { + terrain + .ray(pos.0 + Vec3::unit_z(), tgt_pos.0 + Vec3::unit_z()) + .until(Block::is_opaque) + .cast() + .0 + .powi(2) + >= dist_sqrd +} diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 35fdeea176..d8ccd32c97 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -884,7 +884,7 @@ impl Settlement { .with_body(match dynamic_rng.gen_range(0..5) { _ if is_dummy => { is_human = false; - object::Body::TrainingDummy.into() + object::Body::Crossbow.into() }, 0 => { let species = match dynamic_rng.gen_range(0..3) { @@ -916,9 +916,9 @@ impl Settlement { comp::Body::Humanoid(humanoid::Body::random()) }, }) - .with_agency(!is_dummy) + .with_agency(true) // TEMPORARY .with_alignment(if is_dummy { - comp::Alignment::Passive + comp::Alignment::Enemy // TEMPORARY } else if is_human { comp::Alignment::Npc } else {