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) - 6 different gems. (Topaz, Amethyst, Sapphire, Emerald, Ruby and Diamond)
- Poise system (not currently accessible to players for balancing reasons) - Poise system (not currently accessible to players for balancing reasons)
- Snow particles - Snow particles
- Basic NPC interaction
### Changed ### 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 - 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 - Protection rating was moved to the top left of the loadout view
- Changed camera smoothing to be off by default. - 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 - Footstep SFX is now dependant on distance moved, not time since last play
- Adjusted most NPCs hitboxes to better fit their models. - Adjusted most NPCs hitboxes to better fit their models.
- Changed crafting recipes involving shiny gems to use diamonds instead. - 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 - Fixed a bug where buff/debuff UI elements would flicker when you had more than
one of them active at the same time one of them active at the same time
- Made zooming work on wayland - Made zooming work on wayland
- Fixed AI behavior so only humanoids will attempt to roll
## [0.8.0] - 2020-11-28 ## [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.", "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!", "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": [ "npc.speech.villager_under_attack": [
"Help, I'm under attack!", "Help, I'm 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 player_list(&self) -> &HashMap<Uid, PlayerInfo> { &self.player_list }
pub fn character_list(&self) -> &CharacterList { &self.character_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::{Component, Entity as EcsEntity};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
use std::collections::VecDeque;
use vek::*; 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)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum Alignment { pub enum Alignment {
/// Wild animals and gentle giants /// 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)] #[derive(Clone, Debug, Default)]
pub struct Agent { pub struct Agent {
pub rtsim_controller: RtSimController, pub rtsim_controller: RtSimController,
@ -146,6 +179,7 @@ pub struct Agent {
// TODO move speech patterns into a Behavior component // TODO move speech patterns into a Behavior component
pub can_speak: bool, pub can_speak: bool,
pub psyche: Psyche, pub psyche: Psyche,
pub inbox: VecDeque<AgentEvent>,
} }
impl Agent { impl Agent {
@ -179,6 +213,10 @@ impl Component for Agent {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Activity { pub enum Activity {
Interact {
timer: f32,
interaction: AgentEvent,
},
Idle { Idle {
bearing: Vec2<f32>, bearing: Vec2<f32>,
chaser: Chaser, chaser: Chaser,
@ -194,12 +232,19 @@ pub enum Activity {
been_close: bool, been_close: bool,
powerup: f32, powerup: f32,
}, },
Flee {
target: EcsEntity,
chaser: Chaser,
timer: f32,
},
} }
impl Activity { impl Activity {
pub fn is_follow(&self) -> bool { matches!(self, Activity::Follow { .. }) } pub fn is_follow(&self) -> bool { matches!(self, Activity::Follow { .. }) }
pub fn is_attack(&self) -> bool { matches!(self, Activity::Attack { .. }) } pub fn is_attack(&self) -> bool { matches!(self, Activity::Attack { .. }) }
pub fn is_flee(&self) -> bool { matches!(self, Activity::Flee { .. }) }
} }
impl Default for Activity { impl Default for Activity {

View File

@ -41,6 +41,7 @@ pub enum CharacterState {
Climb, Climb,
Sit, Sit,
Dance, Dance,
Talk,
Sneak, Sneak,
Glide, Glide,
GlideWield, GlideWield,
@ -139,6 +140,7 @@ impl CharacterState {
| CharacterState::BasicBeam(_) | CharacterState::BasicBeam(_)
| CharacterState::Stunned(_) | CharacterState::Stunned(_)
| CharacterState::Wielding | CharacterState::Wielding
| CharacterState::Talk
) )
} }

View File

@ -37,6 +37,7 @@ pub enum ControlEvent {
//ToggleLantern, //ToggleLantern,
EnableLantern, EnableLantern,
DisableLantern, DisableLantern,
Interact(Uid),
Mount(Uid), Mount(Uid),
Unmount, Unmount,
InventoryManip(InventoryManip), InventoryManip(InventoryManip),
@ -55,6 +56,7 @@ pub enum ControlAction {
Dance, Dance,
Sneak, Sneak,
Stand, Stand,
Talk,
} }
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]

View File

@ -78,6 +78,7 @@ pub enum ServerEvent {
}, },
EnableLantern(EcsEntity), EnableLantern(EcsEntity),
DisableLantern(EcsEntity), DisableLantern(EcsEntity),
NpcInteract(EcsEntity, EcsEntity),
Mount(EcsEntity, EcsEntity), Mount(EcsEntity, EcsEntity),
Unmount(EcsEntity), Unmount(EcsEntity),
Possess(Uid, Uid), 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> { impl<T> Path<T> {
pub fn is_empty(&self) -> bool { self.nodes.is_empty() }
pub fn len(&self) -> usize { self.nodes.len() } pub fn len(&self) -> usize { self.nodes.len() }
pub fn iter(&self) -> impl Iterator<Item = &T> { self.nodes.iter() } 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 /// When this field is `Some(..)`, the agent should attempt to make progress
/// toward the given location, accounting for obstacles and other /// toward the given location, accounting for obstacles and other
/// high-priority situations like being attacked. /// 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 /// Proportion of full speed to move
pub speed_factor: f32, pub speed_factor: f32,
} }

View File

@ -24,6 +24,7 @@ pub trait CharacterBehavior {
fn dance(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn dance(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
fn sneak(&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 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 { fn handle_event(&self, data: &JoinData, event: ControlAction) -> StateUpdate {
match event { match event {
ControlAction::SwapLoadout => self.swap_loadout(data), ControlAction::SwapLoadout => self.swap_loadout(data),
@ -34,6 +35,7 @@ pub trait CharacterBehavior {
ControlAction::Dance => self.dance(data), ControlAction::Dance => self.dance(data),
ControlAction::Sneak => self.sneak(data), ControlAction::Sneak => self.sneak(data),
ControlAction::Stand => self.stand(data), ControlAction::Stand => self.stand(data),
ControlAction::Talk => self.talk(data),
} }
} }
// fn init(data: &JoinData) -> CharacterState; // fn init(data: &JoinData) -> CharacterState;

View File

@ -31,6 +31,12 @@ impl CharacterBehavior for Data {
update 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 { fn dance(&self, data: &JoinData) -> StateUpdate {
let mut update = StateUpdate::from(data); let mut update = StateUpdate::from(data);
attempt_dance(data, &mut update); attempt_dance(data, &mut update);

View File

@ -22,5 +22,6 @@ pub mod sit;
pub mod sneak; pub mod sneak;
pub mod spin_melee; pub mod spin_melee;
pub mod stunned; pub mod stunned;
pub mod talk;
pub mod utils; pub mod utils;
pub mod wielding; 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) { pub fn attempt_sneak(data: &JoinData, update: &mut StateUpdate) {
if data.physics.on_ground && data.body.is_humanoid() { if data.physics.on_ground && data.body.is_humanoid() {
update.character = CharacterState::Sneak; 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 j = JoinData::new(&tuple, &updater, &dt);
let mut state_update = match j.character { let mut state_update = match j.character {
CharacterState::Idle => states::idle::Data.handle_event(&j, action), 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::Climb => states::climb::Data.handle_event(&j, action),
CharacterState::Glide => states::glide::Data.handle_event(&j, action), CharacterState::Glide => states::glide::Data.handle_event(&j, action),
CharacterState::GlideWield => { CharacterState::GlideWield => {
@ -274,6 +275,7 @@ impl<'a> System<'a> for Sys {
let mut state_update = match j.character { let mut state_update = match j.character {
CharacterState::Idle => states::idle::Data.behavior(&j), CharacterState::Idle => states::idle::Data.behavior(&j),
CharacterState::Talk => states::talk::Data.behavior(&j),
CharacterState::Climb => states::climb::Data.behavior(&j), CharacterState::Climb => states::climb::Data.behavior(&j),
CharacterState::Glide => states::glide::Data.behavior(&j), CharacterState::Glide => states::glide::Data.behavior(&j),
CharacterState::GlideWield => states::glide_wield::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 => { ControlEvent::DisableLantern => {
server_emitter.emit(ServerEvent::DisableLantern(entity)) 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) => { ControlEvent::InventoryManip(manip) => {
// Unwield if a wielded equipment slot is being modified, to avoid entering // Unwield if a wielded equipment slot is being modified, to avoid entering
// a barehanded wielding state. // a barehanded wielding state.

View File

@ -168,6 +168,7 @@ impl<'a> System<'a> for Sys {
match character_state { match character_state {
// Accelerate recharging energy. // Accelerate recharging energy.
CharacterState::Idle { .. } CharacterState::Idle { .. }
| CharacterState::Talk { .. }
| CharacterState::Sit { .. } | CharacterState::Sit { .. }
| CharacterState::Dance { .. } | CharacterState::Dance { .. }
| CharacterState::Sneak { .. } | CharacterState::Sneak { .. }

View File

@ -2,7 +2,7 @@ use specs::{world::WorldExt, Entity as EcsEntity};
use tracing::error; use tracing::error;
use common::{ 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, consts::MAX_MOUNT_RANGE,
uid::Uid, 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) { pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity) {
let state = server.state_mut(); 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, handle_explosion, handle_knockback, handle_land_on_ground, handle_poise, handle_respawn,
}; };
use group_manip::handle_group; 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 inventory_manip::handle_inventory;
use player::{handle_client_disconnect, handle_exit_ingame}; use player::{handle_client_disconnect, handle_exit_ingame};
use specs::{Entity as EcsEntity, WorldExt}; use specs::{Entity as EcsEntity, WorldExt};
@ -98,6 +100,9 @@ impl Server {
}, },
ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true), ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true),
ServerEvent::DisableLantern(entity) => handle_lantern(self, entity, false), 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::Mount(mounter, mountee) => handle_mount(self, mounter, mountee),
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter), ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
ServerEvent::Possess(possessor_uid, possesse_uid) => { ServerEvent::Possess(possessor_uid, possesse_uid) => {

View File

@ -3,7 +3,7 @@ use common::{comp::inventory::loadout_builder::LoadoutBuilder, store::Id, terrai
use world::{ use world::{
civ::{Site, Track}, civ::{Site, Track},
util::RandomPerm, util::RandomPerm,
World, IndexRef, World,
}; };
pub struct Entity { pub struct Entity {
@ -123,7 +123,7 @@ impl Entity {
.build() .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(|| { let tgt_site = self.brain.tgt.or_else(|| {
world world
.civs() .civs()
@ -146,6 +146,10 @@ impl Entity {
tgt_site.map(|tgt_site| { tgt_site.map(|tgt_site| {
let site = &world.civs().sites[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 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; 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) .map(|e| e as f32)
+ Vec3::new(0.5, 0.5, 0.0); + 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; self.controller.speed_factor = 0.70;
}); });
} }

View File

@ -36,7 +36,7 @@ impl<'a> System<'a> for Sys {
mut rtsim, mut rtsim,
terrain, terrain,
world, world,
_index, index,
positions, positions,
rtsim_entities, rtsim_entities,
mut agents, mut agents,
@ -60,10 +60,10 @@ impl<'a> System<'a> for Sys {
to_reify.push(id); to_reify.push(id);
} else { } else {
// Simulate behaviour // 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 // Move towards target at approximate character speed
entity.pos += Vec3::from( entity.pos += Vec3::from(
(travel_to.xy() - entity.pos.xy()) (travel_to.0.xy() - entity.pos.xy())
.try_normalized() .try_normalized()
.unwrap_or_else(Vec2::zero) .unwrap_or_else(Vec2::zero)
* entity.get_body().max_speed_approx() * entity.get_body().max_speed_approx()
@ -81,7 +81,7 @@ impl<'a> System<'a> for Sys {
// Tick entity AI // Tick entity AI
if entity.last_tick + ENTITY_TICK_PERIOD <= rtsim.tick { 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; entity.last_tick = rtsim.tick;
} }
} }

View File

@ -516,6 +516,8 @@ impl PlayState for SessionState {
.is_some() .is_some()
{ {
client.pick_up(entity); client.pick_up(entity);
} else {
client.npc_interact(entity);
} }
}, },
} }
@ -1495,12 +1497,10 @@ fn select_interactable(
scales.maybe(), scales.maybe(),
colliders.maybe(), colliders.maybe(),
char_states.maybe(), char_states.maybe(),
// Must have this comp to be interactable (for now)
&ecs.read_storage::<comp::Item>(),
) )
.join() .join()
.filter(|(e, _, _, _, _, _)| *e != player_entity) .filter(|(e, _, _, _, _)| *e != player_entity)
.map(|(e, p, s, c, cs, _)| { .map(|(e, p, s, c, cs)| {
let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), cs); let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), cs);
(e, cylinder) (e, cylinder)
}) })