Chatting now creates speech bubbles

This commit is contained in:
CapsizeGlimmer 2020-05-24 18:18:41 -04:00 committed by Pfauenauge90
parent 73a29b339c
commit c65967ccdb
11 changed files with 184 additions and 97 deletions

View File

@ -1,5 +1,5 @@
use crate::path::Chaser;
use specs::{Component, Entity as EcsEntity};
use crate::{path::Chaser, state::Time};
use specs::{Component, Entity as EcsEntity, FlaggedStorage, HashMapStorage};
use specs_idvs::IDVStorage;
use vek::*;
@ -85,3 +85,17 @@ impl Activity {
impl Default for Activity {
fn default() -> Self { Activity::Idle(Vec2::zero()) }
}
/// Default duration in seconds of chat bubbles
pub const SPEECH_BUBBLE_DURATION: f64 = 5.0;
/// Adds a speech bubble to the entity
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct SpeechBubble {
pub message: String,
pub timeout: Option<Time>,
// TODO add icon enum for player chat type / npc quest+trade
}
impl Component for SpeechBubble {
type Storage = FlaggedStorage<Self, HashMapStorage<Self>>;
}

View File

@ -18,7 +18,7 @@ mod visual;
// Reexports
pub use ability::{CharacterAbility, ItemConfig, Loadout};
pub use admin::Admin;
pub use agent::{Agent, Alignment};
pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION};
pub use body::{
biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, golem,
humanoid, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,

View File

@ -24,6 +24,7 @@ sum_type! {
Sticky(comp::Sticky),
Loadout(comp::Loadout),
CharacterState(comp::CharacterState),
SpeechBubble(comp::SpeechBubble),
Pos(comp::Pos),
Vel(comp::Vel),
Ori(comp::Ori),
@ -50,6 +51,7 @@ sum_type! {
Sticky(PhantomData<comp::Sticky>),
Loadout(PhantomData<comp::Loadout>),
CharacterState(PhantomData<comp::CharacterState>),
SpeechBubble(PhantomData<comp::SpeechBubble>),
Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>),
Ori(PhantomData<comp::Ori>),
@ -76,6 +78,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Loadout(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::SpeechBubble(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world),
@ -100,6 +103,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Loadout(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::SpeechBubble(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world),
@ -128,6 +132,9 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::CharacterState(_) => {
sync::handle_remove::<comp::CharacterState>(entity, world)
},
EcsCompPhantom::SpeechBubble(_) => {
sync::handle_remove::<comp::SpeechBubble>(entity, world)
},
EcsCompPhantom::Pos(_) => sync::handle_remove::<comp::Pos>(entity, world),
EcsCompPhantom::Vel(_) => sync::handle_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_remove::<comp::Ori>(entity, world),

View File

@ -122,6 +122,7 @@ impl State {
ecs.register::<comp::Sticky>();
ecs.register::<comp::Gravity>();
ecs.register::<comp::CharacterState>();
ecs.register::<comp::SpeechBubble>();
// Register components send from clients -> server
ecs.register::<comp::Controller>();

View File

@ -109,6 +109,7 @@ impl Server {
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default());
state.ecs_mut().insert(sys::WaypointTimer::default());
state.ecs_mut().insert(sys::SpeechBubbleTimer::default());
state
.ecs_mut()
.insert(sys::StatsPersistenceTimer::default());

View File

@ -4,7 +4,10 @@ use crate::{
CLIENT_TIMEOUT,
};
use common::{
comp::{Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel},
comp::{
Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, SpeechBubble,
Stats, Vel, SPEECH_BUBBLE_DURATION,
},
event::{EventBus, ServerEvent},
msg::{
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
@ -43,6 +46,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Player>,
WriteStorage<'a, Client>,
WriteStorage<'a, Controller>,
WriteStorage<'a, SpeechBubble>,
);
fn run(
@ -67,6 +71,7 @@ impl<'a> System<'a> for Sys {
mut players,
mut clients,
mut controllers,
mut speech_bubbles,
): Self::SystemData,
) {
timer.start();
@ -394,13 +399,20 @@ impl<'a> System<'a> for Sys {
for (entity, msg) in new_chat_msgs {
match msg {
ServerMsg::ChatMsg { chat_type, message } => {
if let Some(entity) = entity {
let message = if let Some(entity) = entity {
// Handle chat commands.
if message.starts_with("/") && message.len() > 1 {
let argv = String::from(&message[1..]);
server_emitter.emit(ServerEvent::ChatCmd(entity, argv));
continue;
} else {
let message = match players.get(entity) {
let timeout = Some(Time(time + SPEECH_BUBBLE_DURATION));
let bubble = SpeechBubble {
message: message.clone(),
timeout,
};
let _ = speech_bubbles.insert(entity, bubble);
match players.get(entity) {
Some(player) => {
if admins.get(entity).is_some() {
format!("[ADMIN][{}] {}", &player.alias, message)
@ -409,17 +421,14 @@ impl<'a> System<'a> for Sys {
}
},
None => format!("[<Unknown>] {}", message),
};
let msg = ServerMsg::ChatMsg { chat_type, message };
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone());
}
}
} else {
let msg = ServerMsg::ChatMsg { chat_type, message };
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone());
}
message
};
let msg = ServerMsg::ChatMsg { chat_type, message };
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone());
}
},
_ => {

View File

@ -2,6 +2,7 @@ pub mod entity_sync;
pub mod message;
pub mod persistence;
pub mod sentinel;
pub mod speech_bubble;
pub mod subscription;
pub mod terrain;
pub mod terrain_sync;
@ -20,6 +21,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type SpeechBubbleTimer = SysTimer<speech_bubble::Sys>;
pub type StatsPersistenceTimer = SysTimer<persistence::stats::Sys>;
pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
@ -31,11 +33,13 @@ pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
//const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys";
const WAYPOINT_SYS: &str = "waypoint_sys";
const SPEECH_BUBBLE_SYS: &str = "speech_bubble_sys";
const STATS_PERSISTENCE_SYS: &str = "stats_persistence_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
dispatch_builder.add(speech_bubble::Sys, SPEECH_BUBBLE_SYS, &[]);
dispatch_builder.add(persistence::stats::Sys, STATS_PERSISTENCE_SYS, &[]);
}

View File

@ -2,7 +2,7 @@ use super::SysTimer;
use common::{
comp::{
Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, Loadout,
Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel,
Mass, MountState, Mounting, Ori, Player, Pos, Scale, SpeechBubble, Stats, Sticky, Vel,
},
msg::EcsCompPacket,
sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt},
@ -54,6 +54,7 @@ pub struct TrackedComps<'a> {
pub gravity: ReadStorage<'a, Gravity>,
pub loadout: ReadStorage<'a, Loadout>,
pub character_state: ReadStorage<'a, CharacterState>,
pub speech_bubble: ReadStorage<'a, SpeechBubble>,
}
impl<'a> TrackedComps<'a> {
pub fn create_entity_package(
@ -125,6 +126,10 @@ impl<'a> TrackedComps<'a> {
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.speech_bubble
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
// Add untracked comps
pos.map(|c| comps.push(c.into()));
vel.map(|c| comps.push(c.into()));
@ -152,6 +157,7 @@ pub struct ReadTrackers<'a> {
pub gravity: ReadExpect<'a, UpdateTracker<Gravity>>,
pub loadout: ReadExpect<'a, UpdateTracker<Loadout>>,
pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
pub speech_bubble: ReadExpect<'a, UpdateTracker<SpeechBubble>>,
}
impl<'a> ReadTrackers<'a> {
pub fn create_sync_packages(
@ -188,6 +194,12 @@ impl<'a> ReadTrackers<'a> {
&*self.character_state,
&comps.character_state,
filter,
)
.with_component(
&comps.uid,
&*self.speech_bubble,
&comps.speech_bubble,
filter,
);
(entity_sync_package, comp_sync_package)
@ -213,6 +225,7 @@ pub struct WriteTrackers<'a> {
gravity: WriteExpect<'a, UpdateTracker<Gravity>>,
loadout: WriteExpect<'a, UpdateTracker<Loadout>>,
character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
speech_bubble: WriteExpect<'a, UpdateTracker<SpeechBubble>>,
}
fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
@ -236,6 +249,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers
.character_state
.record_changes(&comps.character_state);
trackers.speech_bubble.record_changes(&comps.speech_bubble);
}
pub fn register_trackers(world: &mut World) {
@ -256,6 +270,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<Gravity>();
world.register_tracker::<Loadout>();
world.register_tracker::<CharacterState>();
world.register_tracker::<SpeechBubble>();
}
/// Deleted entities grouped by region

View File

@ -0,0 +1,30 @@
use super::SysTimer;
use common::{comp::SpeechBubble, state::Time};
use specs::{Entities, Join, Read, System, Write, WriteStorage};
/// This system removes timed-out speech bubbles
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
Read<'a, Time>,
WriteStorage<'a, SpeechBubble>,
Write<'a, SysTimer<Self>>,
);
fn run(&mut self, (entities, time, mut speech_bubbles, mut timer): Self::SystemData) {
timer.start();
let expired_ents: Vec<_> = (&entities, &mut speech_bubbles)
.join()
.filter(|(_, speech_bubble)| speech_bubble.timeout.map_or(true, |t| t.0 < time.0))
.map(|(ent, _)| ent)
.collect();
for ent in expired_ents {
println!("Remoaving bobble");
speech_bubbles.remove(ent);
}
timer.end();
}
}

View File

@ -566,6 +566,7 @@ impl Hud {
let stats = ecs.read_storage::<comp::Stats>();
let energy = ecs.read_storage::<comp::Energy>();
let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
let speech_bubbles = ecs.read_storage::<comp::SpeechBubble>();
let interpolated = ecs.read_storage::<vcomp::Interpolated>();
let players = ecs.read_storage::<comp::Player>();
let scales = ecs.read_storage::<comp::Scale>();
@ -890,7 +891,7 @@ impl Hud {
let mut sct_bg_walker = self.ids.sct_bgs.walk();
// Render overhead name tags and health bars
for (pos, name, stats, energy, height_offset, hpfl) in (
for (pos, name, stats, energy, height_offset, hpfl, bubble) in (
&entities,
&pos,
interpolated.maybe(),
@ -900,11 +901,12 @@ impl Hud {
scales.maybe(),
&bodies,
&hp_floater_lists,
speech_bubbles.maybe(),
)
.join()
.filter(|(entity, _, _, stats, _, _, _, _, _)| *entity != me && !stats.is_dead)
.filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead)
// Don't show outside a certain range
.filter(|(_, pos, _, _, _, _, _, _, hpfl)| {
.filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| {
pos.0.distance_squared(player_pos)
< (if hpfl
.time_since_last_dmg_by_me
@ -916,7 +918,7 @@ impl Hud {
})
.powi(2)
})
.map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl)| {
.map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, bubble)| {
// TODO: This is temporary
// If the player used the default character name display their name instead
let name = if stats.name == "Character Name" {
@ -932,6 +934,7 @@ impl Hud {
// TODO: when body.height() is more accurate remove the 2.0
body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
hpfl,
bubble,
)
})
{
@ -944,6 +947,7 @@ impl Hud {
// Chat bubble, name, level, and hp bars
overhead::Overhead::new(
&name,
bubble,
stats,
energy,
own_level,

View File

@ -1,6 +1,6 @@
use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR};
use crate::ui::{fonts::ConrodVoxygenFonts, Ingameable};
use common::comp::{Energy, Stats};
use common::comp::{Energy, SpeechBubble, Stats};
use conrod_core::{
position::Align,
widget::{self, Image, Rectangle, Text},
@ -42,6 +42,7 @@ widget_ids! {
#[derive(WidgetCommon)]
pub struct Overhead<'a> {
name: &'a str,
bubble: Option<&'a SpeechBubble>,
stats: &'a Stats,
energy: &'a Energy,
own_level: u32,
@ -55,6 +56,7 @@ pub struct Overhead<'a> {
impl<'a> Overhead<'a> {
pub fn new(
name: &'a str,
bubble: Option<&'a SpeechBubble>,
stats: &'a Stats,
energy: &'a Energy,
own_level: u32,
@ -64,6 +66,7 @@ impl<'a> Overhead<'a> {
) -> Self {
Self {
name,
bubble,
stats,
energy,
own_level,
@ -84,11 +87,12 @@ impl<'a> Ingameable for Overhead<'a> {
// Number of conrod primitives contained in the overhead display. TODO maybe
// this could be done automatically?
// - 2 Text::new for name
// - 2 Text::new for speech bubble
// - 10 Image::new for speech bubble (9-slice + tail)
// - 1 for level: either Text or Image
// - 4 for HP + mana + fg + bg
19
// If there's a speech bubble
// - 1 Text::new for speech bubble
// - 10 Image::new for speech bubble (9-slice + tail)
7 + if self.bubble.is_some() { 11 } else { 0 }
}
}
@ -126,80 +130,78 @@ impl<'a> Widget for Overhead<'a> {
.x_y(0.0, MANA_BAR_Y + 50.0)
.set(state.ids.name, ui);
// Speech bubble
Text::new("Hello")
.font_id(self.fonts.cyri.conrod_id)
.font_size(15)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.up_from(state.ids.name, 10.0)
.x_align_to(state.ids.name, Align::Middle)
.parent(id)
.set(state.ids.chat_bubble_text, ui);
Image::new(self.imgs.chat_bubble_top_left)
.w_h(10.0, 10.0)
.top_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_top_left, ui);
Image::new(self.imgs.chat_bubble_top)
.h(10.0)
.w_of(state.ids.chat_bubble_text)
.mid_top_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_top, ui);
Image::new(self.imgs.chat_bubble_top_right)
.w_h(10.0, 10.0)
.top_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_top_right, ui);
Image::new(self.imgs.chat_bubble_left)
.w(10.0)
.h_of(state.ids.chat_bubble_text)
.mid_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_left, ui);
Image::new(self.imgs.chat_bubble_mid)
.wh_of(state.ids.chat_bubble_text)
.top_left_of(state.ids.chat_bubble_text)
.parent(id)
.set(state.ids.chat_bubble_mid, ui);
Image::new(self.imgs.chat_bubble_right)
.w(10.0)
.h_of(state.ids.chat_bubble_text)
.mid_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_right, ui);
Image::new(self.imgs.chat_bubble_bottom_left)
.w_h(10.0, 10.0)
.bottom_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_bottom_left, ui);
Image::new(self.imgs.chat_bubble_bottom)
.h(10.0)
.w_of(state.ids.chat_bubble_text)
.mid_bottom_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_bottom, ui);
Image::new(self.imgs.chat_bubble_bottom_right)
.w_h(10.0, 10.0)
.bottom_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_bottom_right, ui);
Image::new(self.imgs.chat_bubble_tail)
.w_h(11.0, 16.0)
.mid_bottom_with_margin_on(state.ids.chat_bubble_text, -16.0)
.parent(id)
.set(state.ids.chat_bubble_tail, ui);
// Why is there a second text widget?: The first is to position the 9-slice
// around and the second is to display text. Changing .depth manually
// causes strange problems in unrelated parts of the ui (the debug
// overlay is offset by a npc's screen position) TODO
Text::new("Hello")
.font_id(self.fonts.cyri.conrod_id)
.font_size(15)
.top_left_of(state.ids.chat_bubble_text)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.parent(id)
.set(state.ids.chat_bubble_text2, ui);
if let Some(bubble) = self.bubble {
// Speech bubble
let mut text = Text::new(&bubble.message)
.font_id(self.fonts.cyri.conrod_id)
.font_size(18)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.up_from(state.ids.name, 10.0)
.x_align_to(state.ids.name, Align::Middle)
.parent(id);
if let Some(w) = text.get_w(ui) {
if w > 250.0 {
text = text.w(250.0);
}
}
Image::new(self.imgs.chat_bubble_top_left)
.w_h(10.0, 10.0)
.top_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_top_left, ui);
Image::new(self.imgs.chat_bubble_top)
.h(10.0)
.w_of(state.ids.chat_bubble_text)
.mid_top_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_top, ui);
Image::new(self.imgs.chat_bubble_top_right)
.w_h(10.0, 10.0)
.top_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_top_right, ui);
Image::new(self.imgs.chat_bubble_left)
.w(10.0)
.h_of(state.ids.chat_bubble_text)
.mid_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_left, ui);
Image::new(self.imgs.chat_bubble_mid)
.wh_of(state.ids.chat_bubble_text)
.top_left_of(state.ids.chat_bubble_text)
.parent(id)
.set(state.ids.chat_bubble_mid, ui);
Image::new(self.imgs.chat_bubble_right)
.w(10.0)
.h_of(state.ids.chat_bubble_text)
.mid_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_right, ui);
Image::new(self.imgs.chat_bubble_bottom_left)
.w_h(10.0, 10.0)
.bottom_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_bottom_left, ui);
Image::new(self.imgs.chat_bubble_bottom)
.h(10.0)
.w_of(state.ids.chat_bubble_text)
.mid_bottom_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_bottom, ui);
Image::new(self.imgs.chat_bubble_bottom_right)
.w_h(10.0, 10.0)
.bottom_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
.parent(id)
.set(state.ids.chat_bubble_bottom_right, ui);
let tail = Image::new(self.imgs.chat_bubble_tail)
.w_h(11.0, 16.0)
.mid_bottom_with_margin_on(state.ids.chat_bubble_text, -16.0)
.parent(id);
// Move text to front (conrod depth is lowest first; not a z-index)
tail.set(state.ids.chat_bubble_tail, ui);
text.depth(tail.get_depth() - 1.0)
.set(state.ids.chat_bubble_text, ui);
}
let hp_percentage =
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;