Add basic NPC interaction and fix NPC chat spamming

This commit is contained in:
James Melkonian 2021-01-31 20:29:50 +00:00 committed by Joshua Barretto
parent 7553983110
commit 23b1df3cdd
23 changed files with 1166 additions and 918 deletions

View File

@ -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

View File

@ -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!",

View File

@ -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 }

View File

@ -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 {

View File

@ -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
)
}

View File

@ -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)]

View File

@ -78,6 +78,7 @@ pub enum ServerEvent {
},
EnableLantern(EcsEntity),
DisableLantern(EcsEntity),
NpcInteract(EcsEntity, EcsEntity),
Mount(EcsEntity, EcsEntity),
Unmount(EcsEntity),
Possess(Uid, Uid),

View File

@ -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() }

View File

@ -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,
}

View File

@ -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;

View File

@ -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);

View File

@ -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
View 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
}
}

View File

@ -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;

View File

@ -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,
};
},
}
}
}
}

View File

@ -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),

View File

@ -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.

View File

@ -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 { .. }

View File

@ -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();

View File

@ -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) => {

View File

@ -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;
});
}

View File

@ -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;
}
}

View File

@ -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)
})