Merge branch 'CapsizeGlimmer/chat_bubbles' into 'master'

Capsize glimmer/chat bubbles

See merge request veloren/veloren!1017
This commit is contained in:
Monty Marz 2020-05-27 11:52:53 +00:00
commit 2ad8172d6f
47 changed files with 971 additions and 413 deletions

View File

@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added context-sensitive crosshair
- Announce alias changes to all clients.
- Dance animation
- Speech bubbles appear when nearby players talk
- NPCs call for help when attacked
### Changed

BIN
assets/voxygen/element/frames/bubble/bottom.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/bottom_left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/bottom_right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/mid.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/tail.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/top.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/top_left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/top_right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/bottom.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/bottom_left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/bottom_right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/mid.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/tail.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/top.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/top_left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble_dark/top_right.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -383,5 +383,8 @@ Willenskraft
"esc_menu.logout": "Ausloggen",
"esc_menu.quit_game": "Desktop",
/// End Escape Menu Section
},
vector_map: {
}
)

View File

@ -238,6 +238,7 @@ Enjoy your stay in the World of Veloren."#,
"hud.settings.cumulated_damage": "Cumulated Damage",
"hud.settings.incoming_damage": "Incoming Damage",
"hud.settings.cumulated_incoming_damage": "Cumulated Incoming Damage",
"hud.settings.speech_bubble_dark_mode": "Speech Bubble Dark Mode",
"hud.settings.energybar_numbers": "Energybar Numbers",
"hud.settings.values": "Values",
"hud.settings.percentages": "Percentages",
@ -375,14 +376,52 @@ Fitness
Willpower
"#,
/// Start character window section
/// End character window section
/// Start Escape Menu Section
"esc_menu.logout": "Logout",
"esc_menu.quit_game": "Quit Game",
/// End Escape Menu Section
},
vector_map: {
"npc.speech.villager_under_attack": [
"Help, I'm under attack!",
"Help! I'm under attack!",
"Ouch! I'm under attack!",
"Ouch! I'm under attack! Help!",
"Help me! I'm under attack!",
"I'm under attack! Help!",
"I'm under attack! Help me!",
"Help!",
"Help! Help!",
"Help! Help! Help!",
"I'm under attack!",
"AAAHHH! I'm under attack!",
"AAAHHH! I'm under attack! Help!",
"Help! We're under attack!",
"Help! Murderer!",
"Help! There's a murderer on the loose!",
"Help! They're trying to kill me!",
"Guards, I'm under attack!",
"Guards! I'm under attack!",
"I'm under attack! Guards!",
"Help! Guards! I'm under attack!",
"Guards! Come quick!",
"Guards! Guards!",
"Guards! There's a villain attacking me!",
"Guards, slay this foul villain!",
"Guards! There's a murderer!",
"Guards! Help me!",
"You won't get away with this! Guards!",
"You fiend!",
"Help me!",
"Help! Please!",
"Ouch! Guards! Help!",
"They're coming for me!",
"Help! Help! I'm being repressed",
"Ah, now we see the violence inherent in the system.",
],
}
)

View File

@ -323,5 +323,8 @@ Force
Dexterité
Intelligence"#,
},
vector_map: {
}
)

View File

@ -524,5 +524,8 @@ Volontà
"esc_menu.logout": "Disconnettiti",
"esc_menu.quit_game": "Esci dal Gioco",
/// End Escape Menu Section
}
},
vector_map: {
}
)

View File

@ -368,5 +368,8 @@ Força de vontade
"esc_menu.logout": "Desconectar",
"esc_menu.quit_game": "Sair do jogo",
/// End Escape Menu Section
},
vector_map: {
}
)

View File

@ -365,5 +365,8 @@ https://account.veloren.net."#,
"esc_menu.logout": "Выйти в меню",
"esc_menu.quit_game": "Выйти из игры",
/// End Escape Menu Section
},
vector_map: {
}
)

View File

@ -397,5 +397,8 @@ Hareket gücü
"esc_menu.logout": "Çıkış yap",
"esc_menu.quit_game": "Oyundan çık",
/// End Escape Menu Section
},
vector_map: {
}
)

View File

@ -1,13 +1,19 @@
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::*;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Alignment {
/// Wild animals and gentle giants
Wild,
/// Dungeon cultists and bandits
Enemy,
/// Friendly folk in villages
Npc,
/// Farm animals and pets of villagers
Tame,
/// Pets you've tamed with a collar
Owned(EcsEntity),
}
@ -27,6 +33,10 @@ impl Alignment {
match (self, other) {
(Alignment::Enemy, Alignment::Enemy) => true,
(Alignment::Owned(a), Alignment::Owned(b)) if a == b => true,
(Alignment::Npc, Alignment::Npc) => true,
(Alignment::Npc, Alignment::Tame) => true,
(Alignment::Tame, Alignment::Npc) => true,
(Alignment::Tame, Alignment::Tame) => true,
_ => false,
}
}
@ -40,6 +50,9 @@ impl Component for Alignment {
pub struct Agent {
pub patrol_origin: Option<Vec3<f32>>,
pub activity: Activity,
/// Does the agent talk when e.g. hit by the player
// TODO move speech patterns into a Behavior component
pub can_speak: bool,
}
impl Agent {
@ -47,6 +60,15 @@ impl Agent {
self.patrol_origin = Some(origin);
self
}
pub fn new(origin: Vec3<f32>, can_speak: bool) -> Self {
let patrol_origin = Some(origin);
Agent {
patrol_origin,
can_speak,
..Default::default()
}
}
}
impl Component for Agent {
@ -85,3 +107,50 @@ impl Activity {
impl Default for Activity {
fn default() -> Self { Activity::Idle(Vec2::zero()) }
}
/// Default duration in seconds of speech bubbles
pub const SPEECH_BUBBLE_DURATION: f64 = 5.0;
/// The contents of a speech bubble
#[derive(Clone, Debug, Serialize, Deserialize)]
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),
}
/// Adds a speech bubble to the entity
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SpeechBubble {
pub message: SpeechBubbleMessage,
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>>;
}
impl SpeechBubble {
pub fn npc_new(i18n_key: String, now: Time) -> Self {
let message = SpeechBubbleMessage::Localized(i18n_key, rand::random());
let timeout = Some(Time(now.0 + SPEECH_BUBBLE_DURATION));
Self { message, timeout }
}
pub fn player_new(message: String, now: Time) -> Self {
let message = SpeechBubbleMessage::Plain(message);
let timeout = Some(Time(now.0 + SPEECH_BUBBLE_DURATION));
Self { message, timeout }
}
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

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

@ -4,7 +4,7 @@ use crate::{
agent::Activity,
item::{tool::ToolKind, ItemKind},
Agent, Alignment, CharacterState, ControlAction, Controller, Loadout, MountState, Ori, Pos,
Scale, Stats,
Scale, SpeechBubble, Stats,
},
path::Chaser,
state::{DeltaTime, Time},
@ -38,6 +38,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Alignment>,
WriteStorage<'a, Agent>,
WriteStorage<'a, Controller>,
WriteStorage<'a, SpeechBubble>,
ReadStorage<'a, MountState>,
);
@ -58,6 +59,7 @@ impl<'a> System<'a> for Sys {
alignments,
mut agents,
mut controllers,
mut speech_bubbles,
mount_states,
): Self::SystemData,
) {
@ -376,23 +378,32 @@ impl<'a> System<'a> for Sys {
// last!) ---
// Attack a target that's attacking us
if let Some(stats) = stats.get(entity) {
if let Some(my_stats) = stats.get(entity) {
// Only if the attack was recent
if stats.health.last_change.0 < 5.0 {
if my_stats.health.last_change.0 < 5.0 {
if let comp::HealthSource::Attack { by }
| comp::HealthSource::Projectile { owner: Some(by) } =
stats.health.last_change.1.cause
my_stats.health.last_change.1.cause
{
if !agent.activity.is_attack() {
if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
{
agent.activity = Activity::Attack {
target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
powerup: 0.0,
};
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);
}
agent.activity = Activity::Attack {
target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
powerup: 0.0,
};
}
}
}
}

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,
},
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,12 +71,11 @@ impl<'a> System<'a> for Sys {
mut players,
mut clients,
mut controllers,
mut speech_bubbles,
): Self::SystemData,
) {
timer.start();
let time = time.0;
let persistence_db_dir = &persistence_db_dir.0;
let mut server_emitter = server_event_bus.emitter();
@ -92,13 +95,13 @@ impl<'a> System<'a> for Sys {
// Update client ping.
if new_msgs.len() > 0 {
client.last_ping = time
} else if time - client.last_ping > CLIENT_TIMEOUT // Timeout
client.last_ping = time.0
} else if time.0 - client.last_ping > CLIENT_TIMEOUT // Timeout
|| client.postbox.error().is_some()
// Postbox error
{
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
} else if time - client.last_ping > CLIENT_TIMEOUT * 0.5 {
} else if time.0 - client.last_ping > CLIENT_TIMEOUT * 0.5 {
// Try pinging the client if the timeout is nearing.
client.postbox.send_message(ServerMsg::Ping);
}
@ -394,13 +397,16 @@ 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 bubble = SpeechBubble::player_new(message.clone(), *time);
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 +415,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,29 @@
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

@ -329,6 +329,8 @@ impl<'a> System<'a> for Sys {
.health
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
let can_speak = alignment == comp::Alignment::Npc;
// TODO: This code sets an appropriate base_damage for the enemy. This doesn't
// work because the damage is now saved in an ability
/*
@ -344,7 +346,7 @@ impl<'a> System<'a> for Sys {
loadout,
body,
alignment,
agent: comp::Agent::default().with_patrol_origin(entity.pos),
agent: comp::Agent::new(entity.pos, can_speak),
scale: comp::Scale(scale),
drop_item: entity.loot_drop,
})

View File

@ -272,6 +272,29 @@ image_ids! {
progress_frame: "voxygen.element.frames.progress_bar",
progress: "voxygen.element.misc_bg.progress",
// Speech bubbles
speech_bubble_top_left: "voxygen.element.frames.bubble.top_left",
speech_bubble_top: "voxygen.element.frames.bubble.top",
speech_bubble_top_right: "voxygen.element.frames.bubble.top_right",
speech_bubble_left: "voxygen.element.frames.bubble.left",
speech_bubble_mid: "voxygen.element.frames.bubble.mid",
speech_bubble_right: "voxygen.element.frames.bubble.right",
speech_bubble_bottom_left: "voxygen.element.frames.bubble.bottom_left",
speech_bubble_bottom: "voxygen.element.frames.bubble.bottom",
speech_bubble_bottom_right: "voxygen.element.frames.bubble.bottom_right",
speech_bubble_tail: "voxygen.element.frames.bubble.tail",
dark_bubble_top_left: "voxygen.element.frames.bubble_dark.top_left",
dark_bubble_top: "voxygen.element.frames.bubble_dark.top",
dark_bubble_top_right: "voxygen.element.frames.bubble_dark.top_right",
dark_bubble_left: "voxygen.element.frames.bubble_dark.left",
dark_bubble_mid: "voxygen.element.frames.bubble_dark.mid",
dark_bubble_right: "voxygen.element.frames.bubble_dark.right",
dark_bubble_bottom_left: "voxygen.element.frames.bubble_dark.bottom_left",
dark_bubble_bottom: "voxygen.element.frames.bubble_dark.bottom",
dark_bubble_bottom_right: "voxygen.element.frames.bubble_dark.bottom_right",
dark_bubble_tail: "voxygen.element.frames.bubble_dark.tail",
<BlankGraphic>
nothing: (),
}

View File

@ -7,6 +7,7 @@ mod img_ids;
mod item_imgs;
mod map;
mod minimap;
mod overhead;
mod popup;
mod settings_window;
mod skillbar;
@ -102,17 +103,6 @@ widget_ids! {
crosshair_inner,
crosshair_outer,
// Character Names
name_tags[],
name_tags_bgs[],
levels[],
levels_skull[],
// Health Bars
health_bars[],
mana_bars[],
health_bar_fronts[],
health_bar_backs[],
// SCT
player_scts[],
player_sct_bgs[],
@ -125,6 +115,8 @@ widget_ids! {
sct_bgs[],
scts[],
overheads[],
// Intro Text
intro_bg,
intro_text,
@ -243,6 +235,7 @@ pub enum Event {
Sct(bool),
SctPlayerBatch(bool),
SctDamageBatch(bool),
SpeechBubbleDarkMode(bool),
ToggleDebug(bool),
UiScale(ScaleChange),
CharacterSelection,
@ -574,6 +567,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>();
@ -643,13 +637,9 @@ impl Hud {
}
}
// Nametags and healthbars
// Max amount the sct font size increases when "flashing"
const FLASH_MAX: f32 = 25.0;
const BARSIZE: f64 = 2.0;
const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
// Get player position.
let player_pos = client
.state()
@ -657,265 +647,6 @@ impl Hud {
.read_storage::<comp::Pos>()
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
let mut name_id_walker = self.ids.name_tags.walk();
let mut name_id_bg_walker = self.ids.name_tags_bgs.walk();
let mut level_id_walker = self.ids.levels.walk();
let mut level_skull_id_walker = self.ids.levels_skull.walk();
let mut health_id_walker = self.ids.health_bars.walk();
let mut mana_id_walker = self.ids.mana_bars.walk();
let mut health_back_id_walker = self.ids.health_bar_backs.walk();
let mut health_front_id_walker = self.ids.health_bar_fronts.walk();
let mut sct_bg_id_walker = self.ids.sct_bgs.walk();
let mut sct_id_walker = self.ids.scts.walk();
// Render Health Bars
for (pos, stats, energy, height_offset, hp_floater_list) in (
&entities,
&pos,
interpolated.maybe(),
&stats,
&energy,
scales.maybe(),
&bodies,
&hp_floater_lists,
)
.join()
.filter(|(entity, _, _, stats, _, _, _, _)| {
*entity != me && !stats.is_dead
//&& stats.health.current() != stats.health.maximum()
})
// Don't show outside a certain range
.filter(|(_, pos, _, _, _, _, _, hpfl)| {
pos.0.distance_squared(player_pos)
< (if hpfl
.time_since_last_dmg_by_me
.map_or(false, |t| t < NAMETAG_DMG_TIME)
{
NAMETAG_DMG_RANGE
} else {
NAMETAG_RANGE
})
.powi(2)
})
.map(|(_, pos, interpolated, stats, energy, scale, body, f)| {
(
interpolated.map_or(pos.0, |i| i.pos),
stats,
energy,
// TODO: when body.height() is more accurate remove the 2.0
body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
f,
)
})
{
let back_id = health_back_id_walker.next(
&mut self.ids.health_bar_backs,
&mut ui_widgets.widget_id_generator(),
);
let health_bar_id = health_id_walker.next(
&mut self.ids.health_bars,
&mut ui_widgets.widget_id_generator(),
);
let mana_bar_id = mana_id_walker.next(
&mut self.ids.mana_bars,
&mut ui_widgets.widget_id_generator(),
);
let front_id = health_front_id_walker.next(
&mut self.ids.health_bar_fronts,
&mut ui_widgets.widget_id_generator(),
);
let hp_percentage =
stats.health.current() as f64 / stats.health.maximum() as f64 * 100.0;
let energy_percentage = energy.current() as f64 / energy.maximum() as f64 * 100.0;
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
let ingame_pos = pos + Vec3::unit_z() * height_offset;
// Background
Image::new(self.imgs.enemy_health_bg)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
.position_ingame(ingame_pos)
.set(back_id, ui_widgets);
// % HP Filling
Image::new(self.imgs.enemy_bar)
.w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
.x_y(
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
MANA_BAR_Y + 7.5,
)
.color(Some(if hp_percentage <= 25.0 {
crit_hp_color
} else if hp_percentage <= 50.0 {
LOW_HP_COLOR
} else {
HP_COLOR
}))
.position_ingame(ingame_pos)
.set(health_bar_id, ui_widgets);
// % Mana Filling
Rectangle::fill_with(
[
72.0 * (energy.current() as f64 / energy.maximum() as f64) * BARSIZE,
MANA_BAR_HEIGHT,
],
MANA_COLOR,
)
.x_y(
((3.5 + (energy_percentage / 100.0 * 36.5)) - 36.45) * BARSIZE,
MANA_BAR_Y, //-32.0,
)
.position_ingame(ingame_pos)
.set(mana_bar_id, ui_widgets);
// Foreground
Image::new(self.imgs.enemy_health)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
.position_ingame(ingame_pos)
.set(front_id, ui_widgets);
// Enemy SCT
if let Some(floaters) = Some(hp_floater_list)
.filter(|fl| !fl.floaters.is_empty() && global_state.settings.gameplay.sct)
.map(|l| &l.floaters)
{
// Colors
const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
const DAMAGE_COLORS: [Rgb<f32>; 6] = [
WHITE,
LIGHT_OR,
LIGHT_MED_OR,
MED_OR,
DARK_ORANGE,
RED_ORANGE,
];
// Largest value that select the first color is 40, then it shifts colors
// every 5
let font_col = |font_size: u32| {
DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
};
if global_state.settings.gameplay.sct_damage_batch {
let number_speed = 50.0; // Damage number speed
let sct_bg_id = sct_bg_id_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
let sct_id = sct_id_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
// Calculate total change
// Ignores healing
let hp_damage = floaters.iter().fold(0, |acc, f| {
if f.hp_change < 0 {
acc + f.hp_change
} else {
acc
}
});
let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32;
let timer = floaters
.last()
.expect("There must be at least one floater")
.timer;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if timer < 0.1 {
(FLASH_MAX * (1.0 - timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2;
Text::new(&format!("{}", (hp_damage).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", hp_damage.abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if hp_damage < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, fade)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
} else {
for floater in floaters {
let number_speed = 250.0; // Single Numbers Speed
let sct_bg_id = sct_bg_id_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
let sct_id = sct_id_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
// Calculate total change
let max_hp_frac =
floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if floater.timer < 0.1 {
(FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (floater.timer as f64
/ crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer)
* 0.25)
+ 0.2;
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(if floater.hp_change < 0 {
Color::Rgba(0.0, 0.0, 0.0, fade)
} else {
Color::Rgba(0.0, 0.0, 0.0, 1.0)
})
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if floater.hp_change < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, 1.0)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
}
}
}
}
if global_state.settings.gameplay.sct {
// Render Player SCT numbers
@ -1156,21 +887,27 @@ impl Hud {
}
}
// Render Name Tags
for (pos, name, level, height_offset) in (
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 (
&entities,
&pos,
interpolated.maybe(),
&stats,
&energy,
players.maybe(),
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
@ -1182,7 +919,7 @@ impl Hud {
})
.powi(2)
})
.map(|(_, pos, interpolated, stats, player, scale, body, _)| {
.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" {
@ -1192,87 +929,173 @@ impl Hud {
};
(
interpolated.map_or(pos.0, |i| i.pos),
format!("{}", name),
stats.level,
name,
stats,
energy,
// 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,
)
})
{
let name_id = name_id_walker.next(
&mut self.ids.name_tags,
let overhead_id = overhead_walker.next(
&mut self.ids.overheads,
&mut ui_widgets.widget_id_generator(),
);
let name_bg_id = name_id_bg_walker.next(
&mut self.ids.name_tags_bgs,
&mut ui_widgets.widget_id_generator(),
);
let level_id = level_id_walker
.next(&mut self.ids.levels, &mut ui_widgets.widget_id_generator());
let level_skull_id = level_skull_id_walker.next(
&mut self.ids.levels_skull,
&mut ui_widgets.widget_id_generator(),
);
let ingame_pos = pos + Vec3::unit_z() * height_offset;
// Name
Text::new(&name)
.font_id(self.fonts.cyri.conrod_id)
.font_size(30)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.x_y(-1.0, MANA_BAR_Y + 48.0)
.position_ingame(ingame_pos)
.set(name_bg_id, ui_widgets);
Text::new(&name)
.font_id(self.fonts.cyri.conrod_id)
.font_size(30)
.color(Color::Rgba(0.61, 0.61, 0.89, 1.0))
.x_y(0.0, MANA_BAR_Y + 50.0)
.position_ingame(ingame_pos)
.set(name_id, ui_widgets);
// Speech bubble, name, level, and hp bars
overhead::Overhead::new(
&name,
bubble,
stats,
energy,
own_level,
&global_state.settings.gameplay,
self.pulse,
&self.voxygen_i18n,
&self.imgs,
&self.fonts,
)
.x_y(0.0, 100.0)
.position_ingame(ingame_pos)
.set(overhead_id, ui_widgets);
// Level
const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
let op_level = level.level();
let level_str = format!("{}", op_level);
// Change visuals of the level display depending on the player level/opponent
// level
let level_comp = op_level as i64 - own_level as i64;
// + 10 level above player -> skull
// + 5-10 levels above player -> high
// -5 - +5 levels around player level -> equal
// - 5 levels below player -> low
Text::new(if level_comp < 10 { &level_str } else { "?" })
.font_id(self.fonts.cyri.conrod_id)
.font_size(if op_level > 9 && level_comp < 10 {
14
// Enemy SCT
if global_state.settings.gameplay.sct && !hpfl.floaters.is_empty() {
let floaters = &hpfl.floaters;
// Colors
const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
const DAMAGE_COLORS: [Rgb<f32>; 6] = [
WHITE,
LIGHT_OR,
LIGHT_MED_OR,
MED_OR,
DARK_ORANGE,
RED_ORANGE,
];
// Largest value that select the first color is 40, then it shifts colors
// every 5
let font_col = |font_size: u32| {
DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
};
if global_state.settings.gameplay.sct_damage_batch {
let number_speed = 50.0; // Damage number speed
let sct_id = sct_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
let sct_bg_id = sct_bg_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
// Calculate total change
// Ignores healing
let hp_damage = floaters.iter().fold(0, |acc, f| {
if f.hp_change < 0 {
acc + f.hp_change
} else {
acc
}
});
let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32;
let timer = floaters
.last()
.expect("There must be at least one floater")
.timer;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if timer < 0.1 {
(FLASH_MAX * (1.0 - timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2;
Text::new(&format!("{}", (hp_damage).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", hp_damage.abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if hp_damage < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, fade)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
} else {
15
})
.color(if level_comp > 4 {
HIGH
} else if level_comp < -5 {
LOW
} else {
EQUAL
})
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
.position_ingame(ingame_pos)
.set(level_id, ui_widgets);
if level_comp > 9 {
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
self.imgs.skull_2
} else {
self.imgs.skull
})
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.position_ingame(ingame_pos)
.set(level_skull_id, ui_widgets);
for floater in floaters {
let number_speed = 250.0; // Single Numbers Speed
let sct_id = sct_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
let sct_bg_id = sct_bg_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
// Calculate total change
let max_hp_frac =
floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if floater.timer < 0.1 {
(FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (floater.timer as f64
/ crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer)
* 0.25)
+ 0.2;
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(if floater.hp_change < 0 {
Color::Rgba(0.0, 0.0, 0.0, fade)
} else {
Color::Rgba(0.0, 0.0, 0.0, 1.0)
})
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if floater.hp_change < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, 1.0)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
}
}
}
}
}
@ -1806,6 +1629,9 @@ impl Hud {
.set(self.ids.settings_window, ui_widgets)
{
match event {
settings_window::Event::SpeechBubbleDarkMode(sbdm) => {
events.push(Event::SpeechBubbleDarkMode(sbdm));
},
settings_window::Event::Sct(sct) => {
events.push(Event::Sct(sct));
},

361
voxygen/src/hud/overhead.rs Normal file
View File

@ -0,0 +1,361 @@
use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR};
use crate::{
i18n::VoxygenLocalization,
settings::GameplaySettings,
ui::{fonts::ConrodVoxygenFonts, Ingameable},
};
use common::comp::{Energy, SpeechBubble, Stats};
use conrod_core::{
position::Align,
widget::{self, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
widget_ids! {
struct Ids {
// Speech bubble
speech_bubble_text,
speech_bubble_text2,
speech_bubble_top_left,
speech_bubble_top,
speech_bubble_top_right,
speech_bubble_left,
speech_bubble_mid,
speech_bubble_right,
speech_bubble_bottom_left,
speech_bubble_bottom,
speech_bubble_bottom_right,
speech_bubble_tail,
// Name
name_bg,
name,
// HP
level,
level_skull,
health_bar,
health_bar_bg,
mana_bar,
health_bar_fg,
}
}
/// ui widget containing everything that goes over a character's head
/// (Speech bubble, Name, Level, HP/energy bars, etc.)
#[derive(WidgetCommon)]
pub struct Overhead<'a> {
name: &'a str,
bubble: Option<&'a SpeechBubble>,
stats: &'a Stats,
energy: &'a Energy,
own_level: u32,
settings: &'a GameplaySettings,
pulse: f32,
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
#[conrod(common_builder)]
common: widget::CommonBuilder,
}
impl<'a> Overhead<'a> {
pub fn new(
name: &'a str,
bubble: Option<&'a SpeechBubble>,
stats: &'a Stats,
energy: &'a Energy,
own_level: u32,
settings: &'a GameplaySettings,
pulse: f32,
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
) -> Self {
Self {
name,
bubble,
stats,
energy,
own_level,
settings,
pulse,
voxygen_i18n,
imgs,
fonts,
common: widget::CommonBuilder::default(),
}
}
}
pub struct State {
ids: Ids,
}
impl<'a> Ingameable for Overhead<'a> {
fn prim_count(&self) -> usize {
// Number of conrod primitives contained in the overhead display. TODO maybe
// this could be done automatically?
// - 2 Text::new for name
// - 1 for level: either Text or Image
// - 4 for HP + mana + fg + bg
// 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 }
}
}
impl<'a> Widget for Overhead<'a> {
type Event = ();
type State = State;
type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State {
ids: Ids::new(id_gen),
}
}
fn style(&self) -> Self::Style { () }
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { id, state, ui, .. } = args;
const BARSIZE: f64 = 2.0;
const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
// Name
Text::new(&self.name)
.font_id(self.fonts.cyri.conrod_id)
.font_size(30)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.x_y(-1.0, MANA_BAR_Y + 48.0)
.set(state.ids.name_bg, ui);
Text::new(&self.name)
.font_id(self.fonts.cyri.conrod_id)
.font_size(30)
.color(Color::Rgba(0.61, 0.61, 0.89, 1.0))
.x_y(0.0, MANA_BAR_Y + 50.0)
.set(state.ids.name, ui);
// Speech bubble
if let Some(bubble) = self.bubble {
let dark_mode = self.settings.speech_bubble_dark_mode;
let localizer =
|s: String, i| -> String { self.voxygen_i18n.get_variation(&s, i).to_string() };
let bubble_contents: String = bubble.message(localizer);
let mut text = Text::new(&bubble_contents)
.font_id(self.fonts.cyri.conrod_id)
.font_size(18)
.up_from(state.ids.name, 20.0)
.x_align_to(state.ids.name, Align::Middle)
.parent(id);
text = if dark_mode {
text.color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
} else {
text.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
};
if let Some(w) = text.get_w(ui) {
if w > 250.0 {
text = text.w(250.0);
}
}
Image::new(if dark_mode {
self.imgs.dark_bubble_top_left
} else {
self.imgs.speech_bubble_top_left
})
.w_h(16.0, 16.0)
.top_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_top_left, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_top
} else {
self.imgs.speech_bubble_top
})
.h(16.0)
.padded_w_of(state.ids.speech_bubble_text, -4.0)
.mid_top_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_top, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_top_right
} else {
self.imgs.speech_bubble_top_right
})
.w_h(16.0, 16.0)
.top_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_top_right, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_left
} else {
self.imgs.speech_bubble_left
})
.w(16.0)
.padded_h_of(state.ids.speech_bubble_text, -4.0)
.mid_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_left, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_mid
} else {
self.imgs.speech_bubble_mid
})
.padded_wh_of(state.ids.speech_bubble_text, -4.0)
.top_left_with_margin_on(state.ids.speech_bubble_text, -4.0)
.parent(id)
.set(state.ids.speech_bubble_mid, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_right
} else {
self.imgs.speech_bubble_right
})
.w(16.0)
.padded_h_of(state.ids.speech_bubble_text, -4.0)
.mid_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_right, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_bottom_left
} else {
self.imgs.speech_bubble_bottom_left
})
.w_h(16.0, 16.0)
.bottom_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_bottom_left, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_bottom
} else {
self.imgs.speech_bubble_bottom
})
.h(16.0)
.padded_w_of(state.ids.speech_bubble_text, -4.0)
.mid_bottom_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_bottom, ui);
Image::new(if dark_mode {
self.imgs.dark_bubble_bottom_right
} else {
self.imgs.speech_bubble_bottom_right
})
.w_h(16.0, 16.0)
.bottom_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
.parent(id)
.set(state.ids.speech_bubble_bottom_right, ui);
let tail = Image::new(if dark_mode {
self.imgs.dark_bubble_tail
} else {
self.imgs.speech_bubble_tail
})
.w_h(22.0, 28.0)
.mid_bottom_with_margin_on(state.ids.speech_bubble_text, -32.0)
.parent(id);
// Move text to front (conrod depth is lowest first; not a z-index)
tail.set(state.ids.speech_bubble_tail, ui);
text.depth(tail.get_depth() - 1.0)
.set(state.ids.speech_bubble_text, ui);
}
let hp_percentage =
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
let energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
// Background
Image::new(self.imgs.enemy_health_bg)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
.parent(id)
.set(state.ids.health_bar_bg, ui);
// % HP Filling
Image::new(self.imgs.enemy_bar)
.w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
.x_y(
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
MANA_BAR_Y + 7.5,
)
.color(Some(if hp_percentage <= 25.0 {
crit_hp_color
} else if hp_percentage <= 50.0 {
LOW_HP_COLOR
} else {
HP_COLOR
}))
.parent(id)
.set(state.ids.health_bar, ui);
// % Mana Filling
Rectangle::fill_with(
[
72.0 * (self.energy.current() as f64 / self.energy.maximum() as f64) * BARSIZE,
MANA_BAR_HEIGHT,
],
MANA_COLOR,
)
.x_y(
((3.5 + (energy_percentage / 100.0 * 36.5)) - 36.45) * BARSIZE,
MANA_BAR_Y, //-32.0,
)
.parent(id)
.set(state.ids.mana_bar, ui);
// Foreground
Image::new(self.imgs.enemy_health)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
.parent(id)
.set(state.ids.health_bar_fg, ui);
// Level
const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
// Change visuals of the level display depending on the player level/opponent
// level
let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
// + 10 level above player -> skull
// + 5-10 levels above player -> high
// -5 - +5 levels around player level -> equal
// - 5 levels below player -> low
if level_comp > 9 {
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
self.imgs.skull_2
} else {
self.imgs.skull
})
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.parent(id)
.set(state.ids.level_skull, ui);
} else {
Text::new(&format!("{}", self.stats.level.level()))
.font_id(self.fonts.cyri.conrod_id)
.font_size(if self.stats.level.level() > 9 && level_comp < 10 {
14
} else {
15
})
.color(if level_comp > 4 {
HIGH
} else if level_comp < -5 {
LOW
} else {
EQUAL
})
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
.parent(id)
.set(state.ids.level, ui);
}
}
}

View File

@ -153,6 +153,8 @@ widget_ids! {
sct_num_dur_text,
sct_num_dur_slider,
sct_num_dur_value,
speech_bubble_dark_mode_text,
speech_bubble_dark_mode_button,
free_look_behavior_text,
free_look_behavior_list
}
@ -235,6 +237,7 @@ pub enum Event {
Sct(bool),
SctPlayerBatch(bool),
SctDamageBatch(bool),
SpeechBubbleDarkMode(bool),
ChangeLanguage(LanguageMetadata),
ChangeBinding(GameInput),
ChangeFreeLookBehavior(PressBehavior),
@ -943,17 +946,45 @@ impl<'a> Widget for SettingsWindow<'a> {
.set(state.ids.sct_batch_inc_text, ui);
}
// Speech bubble dark mode
let speech_bubble_dark_mode = ToggleButton::new(
self.global_state.settings.gameplay.speech_bubble_dark_mode,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.down_from(
if self.global_state.settings.gameplay.sct {
state.ids.sct_batch_inc_radio
} else {
state.ids.sct_show_radio
},
20.0,
)
.x(0.0)
.w_h(18.0, 18.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.speech_bubble_dark_mode_button, ui);
if self.global_state.settings.gameplay.speech_bubble_dark_mode
!= speech_bubble_dark_mode
{
events.push(Event::SpeechBubbleDarkMode(speech_bubble_dark_mode));
}
Text::new(
&self
.localized_strings
.get("hud.settings.speech_bubble_dark_mode"),
)
.right_from(state.ids.speech_bubble_dark_mode_button, 10.0)
.font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.speech_bubble_dark_mode_text, ui);
// Energybars Numbers
// Hotbar text
Text::new(&self.localized_strings.get("hud.settings.energybar_numbers"))
.down_from(
if self.global_state.settings.gameplay.sct {
state.ids.sct_batch_inc_radio
} else {
state.ids.sct_show_radio
},
20.0,
)
.down_from(state.ids.speech_bubble_dark_mode_button, 20.0)
.font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)

View File

@ -53,10 +53,15 @@ pub type VoxygenFonts = HashMap<String, Font>;
pub struct VoxygenLocalization {
/// A map storing the localized texts
///
/// Localized content can be access using a String key
/// Localized content can be accessed using a String key.
pub string_map: HashMap<String, String>,
/// Either to convert the input text encoded in UTF-8
/// A map for storing variations of localized texts, for example multiple
/// ways of saying "Help, I'm under attack". Used primarily for npc
/// dialogue.
pub vector_map: HashMap<String, Vec<String>>,
/// Whether to convert the input text encoded in UTF-8
/// into a ASCII version by using the `deunicode` crate.
pub convert_utf8_to_ascii: bool,
@ -78,23 +83,56 @@ impl VoxygenLocalization {
}
}
/// Return the missing keys compared to the reference language and return
/// them
pub fn list_missing_entries(&self) -> HashSet<String> {
/// Get a variation of localized text from the given key
///
/// `index` should be a random number from `0` to `u16::max()`
///
/// If the key is not present in the localization object
/// then the key is returned.
pub fn get_variation<'a>(&'a self, key: &'a str, index: u16) -> &str {
match self.vector_map.get(key) {
Some(v) if !v.is_empty() => &v[index as usize % v.len()],
_ => key,
}
}
/// Return the missing keys compared to the reference language
pub fn list_missing_entries(&self) -> (HashSet<String>, HashSet<String>) {
let reference_localization =
load_expect::<VoxygenLocalization>(i18n_asset_key(REFERENCE_LANG).as_ref());
let reference_keys: HashSet<_> =
reference_localization.string_map.keys().cloned().collect();
let current_keys: HashSet<_> = self.string_map.keys().cloned().collect();
reference_keys.difference(&current_keys).cloned().collect()
let reference_string_keys: HashSet<_> =
reference_localization.string_map.keys().cloned().collect();
let string_keys: HashSet<_> = self.string_map.keys().cloned().collect();
let strings = reference_string_keys
.difference(&string_keys)
.cloned()
.collect();
let reference_vector_keys: HashSet<_> =
reference_localization.vector_map.keys().cloned().collect();
let vector_keys: HashSet<_> = self.vector_map.keys().cloned().collect();
let vectors = reference_vector_keys
.difference(&vector_keys)
.cloned()
.collect();
(strings, vectors)
}
/// Log missing entries (compared to the reference language) as warnings
pub fn log_missing_entries(&self) {
for missing_key in self.list_missing_entries() {
let (missing_strings, missing_vectors) = self.list_missing_entries();
for missing_key in missing_strings {
log::warn!(
"[{:?}] Missing key {:?}",
"[{:?}] Missing string key {:?}",
self.metadata.language_identifier,
missing_key
);
}
for missing_key in missing_vectors {
log::warn!(
"[{:?}] Missing vector key {:?}",
self.metadata.language_identifier,
missing_key
);
@ -116,6 +154,10 @@ impl Asset for VoxygenLocalization {
for value in asked_localization.string_map.values_mut() {
*value = deunicode(value);
}
for value in asked_localization.vector_map.values_mut() {
*value = value.into_iter().map(|s| deunicode(s)).collect();
}
}
asked_localization.metadata.language_name =
deunicode(&asked_localization.metadata.language_name);

View File

@ -594,6 +594,10 @@ impl PlayState for SessionState {
global_state.settings.gameplay.sct_damage_batch = sct_damage_batch;
global_state.settings.save_to_file_warn();
},
HudEvent::SpeechBubbleDarkMode(sbdm) => {
global_state.settings.gameplay.speech_bubble_dark_mode = sbdm;
global_state.settings.save_to_file_warn();
},
HudEvent::ToggleDebug(toggle_debug) => {
global_state.settings.gameplay.toggle_debug = toggle_debug;
global_state.settings.save_to_file_warn();

View File

@ -455,6 +455,7 @@ pub struct GameplaySettings {
pub sct: bool,
pub sct_player_batch: bool,
pub sct_damage_batch: bool,
pub speech_bubble_dark_mode: bool,
pub mouse_y_inversion: bool,
pub smooth_pan_enable: bool,
pub crosshair_transp: f32,
@ -480,6 +481,7 @@ impl Default for GameplaySettings {
sct: true,
sct_player_batch: true,
sct_damage_batch: false,
speech_bubble_dark_mode: false,
crosshair_transp: 0.6,
chat_transp: 0.4,
crosshair_type: CrosshairType::Round,

View File

@ -784,8 +784,8 @@ impl Settlement {
if matches!(sample.plot, Some(Plot::Town))
&& RandomField::new(self.seed).chance(Vec3::from(wpos2d), 1.0 / (50.0 * 50.0))
{
let is_human: bool;
let entity = EntityInfo::at(entity_wpos)
.with_alignment(comp::Alignment::Npc)
.with_body(match rng.gen_range(0, 4) {
0 => {
let species = match rng.gen_range(0, 3) {
@ -793,7 +793,7 @@ impl Settlement {
1 => quadruped_small::Species::Sheep,
_ => quadruped_small::Species::Cat,
};
is_human = false;
comp::Body::QuadrupedSmall(quadruped_small::Body::random_with(
rng, &species,
))
@ -805,14 +805,22 @@ impl Settlement {
2 => bird_medium::Species::Goose,
_ => bird_medium::Species::Peacock,
};
is_human = false;
comp::Body::BirdMedium(bird_medium::Body::random_with(
rng, &species,
))
},
_ => comp::Body::Humanoid(humanoid::Body::random()),
_ => {
is_human = true;
comp::Body::Humanoid(humanoid::Body::random())
},
})
.do_if(rng.gen(), |entity| {
.with_alignment(if is_human {
comp::Alignment::Npc
} else {
comp::Alignment::Tame
})
.do_if(is_human && rng.gen(), |entity| {
entity.with_main_tool(assets::load_expect_cloned(
match rng.gen_range(0, 7) {
0 => "common.items.weapons.tool.broom",