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;

File diff suppressed because it is too large Load Diff

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