mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Add basic NPC interaction and fix NPC chat spamming
This commit is contained in:
parent
7553983110
commit
23b1df3cdd
@ -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
|
||||
|
||||
|
@ -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!",
|
||||
|
@ -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 }
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)]
|
||||
|
@ -78,6 +78,7 @@ pub enum ServerEvent {
|
||||
},
|
||||
EnableLantern(EcsEntity),
|
||||
DisableLantern(EcsEntity),
|
||||
NpcInteract(EcsEntity, EcsEntity),
|
||||
Mount(EcsEntity, EcsEntity),
|
||||
Unmount(EcsEntity),
|
||||
Possess(Uid, Uid),
|
||||
|
@ -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() }
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
49
common/src/states/talk.rs
Normal file
49
common/src/states/talk.rs
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,47 +585,6 @@ 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)
|
||||
.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 {
|
||||
@ -891,7 +942,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::StoneGolemBoss => {
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
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)
|
||||
@ -1376,7 +1427,6 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
} 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,20 +1490,21 @@ 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 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 1.0 - agent.psyche.aggro > damage && flees {
|
||||
if agent.can_speak {
|
||||
let msg =
|
||||
"npc.speech.villager_under_attack".to_string();
|
||||
@ -1461,7 +1512,12 @@ impl<'a> System<'a> for Sys {
|
||||
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(),
|
||||
@ -1469,8 +1525,6 @@ impl<'a> System<'a> for Sys {
|
||||
been_close: false,
|
||||
powerup: 0.0,
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
|
@ -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 { .. }
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user