Move message processing and chat bubbles to the client

This commit is contained in:
CapsizeGlimmer 2020-06-04 03:11:35 -04:00 committed by Forest Anderson
parent 0b2a3ebe8b
commit 289ef5d6b2
18 changed files with 193 additions and 101 deletions

View File

@ -802,7 +802,11 @@ impl Client {
};
}
},
ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => {
ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(_uid)) => {
// Don't remove players because we need to remember the
// names of disconnected players in chat.
/*
if self.player_list.remove(&uid).is_none() {
warn!(
"Received msg to remove uid {} from the player list by they \
@ -810,6 +814,7 @@ impl Client {
uid
);
}
*/
},
ServerMsg::PlayerListUpdate(PlayerListUpdate::Alias(uid, new_name)) => {
if let Some(player_info) = self.player_list.get_mut(&uid) {

View File

@ -1,5 +1,5 @@
use crate::{path::Chaser, state::Time};
use specs::{Component, Entity as EcsEntity, FlaggedStorage, HashMapStorage};
use crate::path::Chaser;
use specs::{Component, Entity as EcsEntity};
use specs_idvs::IDVStorage;
use vek::*;

View File

@ -1,6 +1,7 @@
use crate::sync::Uid;
use specs::Component;
use specs_idvs::IDVStorage;
use std::time::{Duration, Instant};
/// A player's current chat mode.
#[derive(Copy, Clone, Debug)]
@ -18,10 +19,26 @@ pub enum ChatMode {
/// Talk to every player on the server
World,
}
impl Component for ChatMode {
type Storage = IDVStorage<Self>;
}
impl ChatMode {
/// Create a message from your current chat mode and uuid.
pub fn msg_from(&self, from: Uid, message: String) -> ChatMsg {
let chat_type = match self {
ChatMode::Tell(to) => ChatType::Tell(from, *to),
ChatMode::Say => ChatType::Say(from),
ChatMode::Region => ChatType::Region(from),
ChatMode::Group => ChatType::Group(from),
ChatMode::Faction => ChatType::Faction(from),
ChatMode::World => ChatType::World(from),
};
ChatMsg { chat_type, message }
}
}
/// List of chat types. Note that this is a superset of `ChatMode`; this is
/// because `ChatType::Kill`, `ChatType::Broadcast`, and `ChatType::Private`
/// cannot be sent by players.
@ -45,6 +62,10 @@ pub enum ChatType {
Region(Uid),
/// World chat
World(Uid),
/// Messages sent from NPCs (Not shown in chat but as speech bubbles)
///
/// The u16 field is a random number for selecting localization variants.
Npc(Uid, u16),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -53,18 +74,32 @@ pub struct ChatMsg {
pub message: String,
}
impl ChatMode {
/// Create a message from your current chat mode and uuid.
pub fn msg_from(&self, from: Uid, message: String) -> ChatMsg {
let chat_type = match self {
ChatMode::Tell(to) => ChatType::Tell(from, *to),
ChatMode::Say => ChatType::Say(from),
ChatMode::Region => ChatType::Region(from),
ChatMode::Group => ChatType::Group(from),
ChatMode::Faction => ChatType::Faction(from),
ChatMode::World => ChatType::World(from),
impl ChatMsg {
pub fn npc(uid: Uid, message: String) -> Self {
let chat_type = ChatType::Npc(uid, rand::random());
Self { chat_type, message }
}
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
let tuple = match self.chat_type {
ChatType::Broadcast => None,
ChatType::Private => None,
ChatType::Kill => None,
ChatType::Tell(u, _) => Some((SpeechBubbleIcon::Tell, u, None)),
ChatType::Say(u) => Some((SpeechBubbleIcon::Say, u, None)),
ChatType::Group(u) => Some((SpeechBubbleIcon::Group, u, None)),
ChatType::Faction(u) => Some((SpeechBubbleIcon::Faction, u, None)),
ChatType::Region(u) => Some((SpeechBubbleIcon::Region, u, None)),
ChatType::World(u) => Some((SpeechBubbleIcon::World, u, None)),
ChatType::Npc(u, r) => Some((SpeechBubbleIcon::None, u, Some(r))),
};
ChatMsg { chat_type, message }
tuple.map(|(icon, from, npc_rand)| {
if let Some(r) = npc_rand {
(SpeechBubble::npc_new(self.message.clone(), r, icon), from)
} else {
(SpeechBubble::player_new(self.message.clone(), icon), from)
}
})
}
}
@ -85,3 +120,68 @@ pub struct Faction(String);
impl Component for Faction {
type Storage = IDVStorage<Self>;
}
/// The contents of a speech bubble
pub enum SpeechBubbleMessage {
/// This message was said by a player and needs no translation
Plain(String),
/// This message was said by an NPC. The fields are a i18n key and a random
/// u16 index
Localized(String, u16),
}
pub enum SpeechBubbleIcon {
// One for each chat mode
Tell,
Say,
Region,
Group,
Faction,
World,
// For NPCs
Quest, // TODO not implemented
Trade, // TODO not implemented
None, // No icon (default for npcs)
}
/// Adds a speech bubble above the character
pub struct SpeechBubble {
pub message: SpeechBubbleMessage,
pub icon: SpeechBubbleIcon,
pub timeout: Instant,
}
impl SpeechBubble {
/// Default duration in seconds of speech bubbles
pub const DEFAULT_DURATION: f64 = 5.0;
pub fn npc_new(i18n_key: String, r: u16, icon: SpeechBubbleIcon) -> Self {
let message = SpeechBubbleMessage::Localized(i18n_key, r);
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
timeout,
icon,
}
}
pub fn player_new(message: String, icon: SpeechBubbleIcon) -> Self {
let message = SpeechBubbleMessage::Plain(message);
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
timeout,
icon,
}
}
pub fn message<F>(&self, i18n_variation: F) -> String
where
F: Fn(String, u16) -> String,
{
match &self.message {
SpeechBubbleMessage::Plain(m) => m.to_string(),
SpeechBubbleMessage::Localized(k, i) => i18n_variation(k.to_string(), *i).to_string(),
}
}
}

View File

@ -19,13 +19,13 @@ mod visual;
// Reexports
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
pub use admin::Admin;
pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION};
pub use agent::{Agent, Alignment};
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,
};
pub use character_state::{Attacking, CharacterState, StateUpdate};
pub use chat::{ChatMode, ChatMsg, ChatType, Faction, Group};
pub use chat::{ChatMode, ChatMsg, ChatType, Faction, Group, SpeechBubble};
pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip,
MountState, Mounting,

View File

@ -119,6 +119,8 @@ pub enum ServerEvent {
ClientDisconnect(EcsEntity),
ChunkRequest(EcsEntity, Vec2<i32>),
ChatCmd(EcsEntity, String),
/// Send a chat message from an npc to the player
Chat(comp::ChatMsg),
}
pub struct EventBus<E> {

View File

@ -25,7 +25,6 @@ sum_type! {
Sticky(comp::Sticky),
Loadout(comp::Loadout),
CharacterState(comp::CharacterState),
SpeechBubble(comp::SpeechBubble),
Pos(comp::Pos),
Vel(comp::Vel),
Ori(comp::Ori),
@ -52,7 +51,6 @@ 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>),
@ -79,7 +77,6 @@ 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),
@ -104,7 +101,6 @@ 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),
@ -133,9 +129,6 @@ 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

@ -71,7 +71,7 @@ pub enum ServerMsg {
Ping,
Pong,
/// A message to go into the client chat box. The client is responsible for
/// formatting the message.
/// formatting the message and turning it into a speech bubble.
ChatMsg(comp::ChatMsg),
SetPlayerEntity(Uid),
TimeOfDay(state::TimeOfDay),

View File

@ -122,7 +122,6 @@ 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

@ -3,12 +3,13 @@ use crate::{
self,
agent::Activity,
item::{tool::ToolKind, ItemKind},
Agent, Alignment, CharacterState, ControlAction, Controller, Loadout, MountState, Ori, Pos,
Scale, SpeechBubble, Stats,
Agent, Alignment, CharacterState, ChatMsg, ControlAction, Controller, Loadout, MountState,
Ori, Pos, Scale, Stats,
},
event::{EventBus, ServerEvent},
path::Chaser,
state::{DeltaTime, Time},
sync::UidAllocator,
sync::{Uid, UidAllocator},
terrain::TerrainGrid,
util::Dir,
vol::ReadVol,
@ -16,7 +17,7 @@ use crate::{
use rand::{thread_rng, Rng};
use specs::{
saveload::{Marker, MarkerAllocator},
Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage,
};
use vek::*;
@ -28,6 +29,7 @@ impl<'a> System<'a> for Sys {
Read<'a, UidAllocator>,
Read<'a, Time>,
Read<'a, DeltaTime>,
Write<'a, EventBus<ServerEvent>>,
Entities<'a>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Ori>,
@ -35,11 +37,11 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Stats>,
ReadStorage<'a, Loadout>,
ReadStorage<'a, CharacterState>,
ReadStorage<'a, Uid>,
ReadExpect<'a, TerrainGrid>,
ReadStorage<'a, Alignment>,
WriteStorage<'a, Agent>,
WriteStorage<'a, Controller>,
WriteStorage<'a, SpeechBubble>,
ReadStorage<'a, MountState>,
);
@ -50,6 +52,7 @@ impl<'a> System<'a> for Sys {
uid_allocator,
time,
dt,
event_bus,
entities,
positions,
orientations,
@ -57,11 +60,11 @@ impl<'a> System<'a> for Sys {
stats,
loadouts,
character_states,
uids,
terrain,
alignments,
mut agents,
mut controllers,
mut speech_bubbles,
mount_states,
): Self::SystemData,
) {
@ -72,6 +75,7 @@ impl<'a> System<'a> for Sys {
alignment,
loadout,
character_state,
uid,
agent,
controller,
mount_state,
@ -82,6 +86,7 @@ impl<'a> System<'a> for Sys {
alignments.maybe(),
&loadouts,
&character_states,
&uids,
&mut agents,
&mut controllers,
mount_states.maybe(),
@ -386,10 +391,9 @@ impl<'a> System<'a> for Sys {
{
if stats.get(attacker).map_or(false, |a| !a.is_dead) {
if agent.can_speak {
let message =
"npc.speech.villager_under_attack".to_string();
let bubble = SpeechBubble::npc_new(message, *time);
let _ = speech_bubbles.insert(entity, bubble);
let msg = "npc.speech.villager_under_attack".to_string();
event_bus
.emit_now(ServerEvent::Chat(ChatMsg::npc(*uid, msg)));
}
agent.activity = Activity::Attack {

View File

@ -1,5 +1,8 @@
use crate::Server;
use common::event::{EventBus, ServerEvent};
use crate::{state_ext::StateExt, Server};
use common::{
event::{EventBus, ServerEvent},
msg::ServerMsg,
};
use entity_creation::{
handle_create_npc, handle_create_waypoint, handle_initialize_character,
handle_loaded_character_data, handle_shoot,
@ -103,6 +106,10 @@ impl Server {
ServerEvent::ChatCmd(entity, cmd) => {
chat_commands.push((entity, cmd));
},
ServerEvent::Chat(msg) => {
self.state
.notify_registered_clients(ServerMsg::ChatMsg(msg));
},
}
}

View File

@ -114,8 +114,8 @@ 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::PersistenceTimer::default());
//state.ecs_mut().insert(sys::StatsPersistenceTimer::default());
// System schedulers to control execution of systems
state

View File

@ -464,7 +464,8 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::ChatCmd(entity, argv));
}
} else {
// TODO FIXME speech bubbles and prefixes are handled by the client now
// Send speech bubble and chat message
// TODO filter group, faction, say, and bubble distance.
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(ServerMsg::ChatMsg(msg.clone()));
}

View File

@ -2,7 +2,6 @@ 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;
@ -21,9 +20,10 @@ 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 PersistenceTimer = SysTimer<persistence::Sys>;
pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
//pub type StatsPersistenceTimer = SysTimer<persistence::stats::Sys>;
//pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
// System names
// Note: commented names may be useful in the future
@ -33,14 +33,14 @@ pub type PersistenceScheduler = SysScheduler<persistence::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 PERSISTENCE_SYS: &str = "persistence_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::Sys, PERSISTENCE_SYS, &[]);
//dispatch_builder.add(persistence::stats::Sys, STATS_PERSISTENCE_SYS, &[]);
}
pub fn run_sync_systems(ecs: &mut specs::World) {

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, SpeechBubble, Stats, Sticky, Vel,
Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel,
},
msg::EcsCompPacket,
sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt},
@ -54,7 +54,6 @@ 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(
@ -126,10 +125,6 @@ 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()));
@ -157,7 +152,6 @@ 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(
@ -194,12 +188,6 @@ 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)
@ -225,7 +213,6 @@ 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) {
@ -249,7 +236,6 @@ 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) {
@ -270,7 +256,6 @@ 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

@ -1,29 +0,0 @@
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 {
speech_bubbles.remove(ent);
}
timer.end();
}
}

View File

@ -367,6 +367,7 @@ impl<'a> Widget for Chat<'a> {
ChatType::Faction(uid) => (FACTION_COLOR, message_format(uid, message)),
ChatType::Region(uid) => (REGION_COLOR, message_format(uid, message)),
ChatType::World(uid) => (WORLD_COLOR, message_format(uid, message)),
ChatType::Npc(_uid, _r) => continue, // Should be filtered by hud/mod.rs
};
let text = Text::new(&msg)
.font_size(self.fonts.opensans.scale(15))

View File

@ -47,14 +47,17 @@ use crate::{
GlobalState,
};
use client::Client;
use common::{assets::load_expect, comp, terrain::TerrainChunk, vol::RectRasterableVol};
use common::{assets::load_expect, comp, sync::Uid, terrain::TerrainChunk, vol::RectRasterableVol};
use conrod_core::{
text::cursor::Index,
widget::{self, Button, Image, Text},
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
};
use specs::{Join, WorldExt};
use std::collections::VecDeque;
use std::{
collections::{HashMap, VecDeque},
time::Instant,
};
use vek::*;
const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0);
@ -465,6 +468,7 @@ pub struct Hud {
rot_imgs: ImgsRot,
new_messages: VecDeque<comp::ChatMsg>,
new_notifications: VecDeque<common::msg::Notification>,
speech_bubbles: HashMap<Uid, comp::SpeechBubble>,
show: Show,
//never_show: bool,
//intro: bool,
@ -530,9 +534,10 @@ impl Hud {
fonts,
ids,
new_messages: VecDeque::new(),
new_notifications: VecDeque::new(),
speech_bubbles: HashMap::new(),
//intro: false,
//intro_2: false,
new_notifications: VecDeque::new(),
show: Show {
help: false,
intro: true,
@ -606,7 +611,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 uids = ecs.read_storage::<common::sync::Uid>();
let interpolated = ecs.read_storage::<vcomp::Interpolated>();
let players = ecs.read_storage::<comp::Player>();
let scales = ecs.read_storage::<comp::Scale>();
@ -934,12 +939,23 @@ impl Hud {
}
}
// Pop speech bubbles
self.speech_bubbles
.retain(|_uid, bubble| bubble.timeout > Instant::now());
// Push speech bubbles
for msg in self.new_messages.iter() {
if let Some((bubble, uid)) = msg.to_bubble() {
self.speech_bubbles.insert(uid, bubble);
}
}
let mut overhead_walker = self.ids.overheads.walk();
let mut sct_walker = self.ids.scts.walk();
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, bubble) in (
for (pos, name, stats, energy, height_offset, hpfl, uid) in (
&entities,
&pos,
interpolated.maybe(),
@ -949,7 +965,7 @@ impl Hud {
scales.maybe(),
&bodies,
&hp_floater_lists,
speech_bubbles.maybe(),
&uids,
)
.join()
.filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead)
@ -966,7 +982,7 @@ impl Hud {
})
.powi(2)
})
.map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, bubble)| {
.map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, uid)| {
// TODO: This is temporary
// If the player used the default character name display their name instead
let name = if stats.name == "Character Name" {
@ -982,10 +998,12 @@ 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,
uid,
)
})
{
let bubble = self.speech_bubbles.get(uid);
let overhead_id = overhead_walker.next(
&mut self.ids.overheads,
&mut ui_widgets.widget_id_generator(),
@ -1551,6 +1569,15 @@ impl Hud {
.set(self.ids.skillbar, ui_widgets);
}
// Don't put NPC messages in chat box.
self.new_messages.retain(|m| {
if let comp::ChatType::Npc(_, _) = m.chat_type {
false
} else {
true
}
});
// Chat box
match Chat::new(
&mut self.new_messages,

View File

@ -77,11 +77,8 @@ impl SessionState {
fn tick(&mut self, dt: Duration, global_state: &mut GlobalState) -> Result<TickAction, Error> {
self.inputs.tick(dt);
for event in self.client.borrow_mut().tick(
self.inputs.clone(),
dt,
crate::ecs::sys::add_local_systems,
)? {
let mut client = self.client.borrow_mut();
for event in client.tick(self.inputs.clone(), dt, crate::ecs::sys::add_local_systems)? {
match event {
client::Event::Chat(m) => {
self.hud.new_message(m);