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 - Added context-sensitive crosshair
- Announce alias changes to all clients. - Announce alias changes to all clients.
- Dance animation - Dance animation
- Speech bubbles appear when nearby players talk
- NPCs call for help when attacked
### Changed ### 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.logout": "Ausloggen",
"esc_menu.quit_game": "Desktop", "esc_menu.quit_game": "Desktop",
/// End Escape Menu Section /// 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.cumulated_damage": "Cumulated Damage",
"hud.settings.incoming_damage": "Incoming Damage", "hud.settings.incoming_damage": "Incoming Damage",
"hud.settings.cumulated_incoming_damage": "Cumulated 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.energybar_numbers": "Energybar Numbers",
"hud.settings.values": "Values", "hud.settings.values": "Values",
"hud.settings.percentages": "Percentages", "hud.settings.percentages": "Percentages",
@ -375,14 +376,52 @@ Fitness
Willpower Willpower
"#, "#,
/// End character window section
/// Start character window section
/// Start Escape Menu Section /// Start Escape Menu Section
"esc_menu.logout": "Logout", "esc_menu.logout": "Logout",
"esc_menu.quit_game": "Quit Game", "esc_menu.quit_game": "Quit Game",
/// End Escape Menu Section /// 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é Dexterité
Intelligence"#, Intelligence"#,
},
vector_map: {
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,19 @@
use crate::path::Chaser; use crate::{path::Chaser, state::Time};
use specs::{Component, Entity as EcsEntity}; use specs::{Component, Entity as EcsEntity, FlaggedStorage, HashMapStorage};
use specs_idvs::IDVStorage; use specs_idvs::IDVStorage;
use vek::*; use vek::*;
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum Alignment { pub enum Alignment {
/// Wild animals and gentle giants
Wild, Wild,
/// Dungeon cultists and bandits
Enemy, Enemy,
/// Friendly folk in villages
Npc, Npc,
/// Farm animals and pets of villagers
Tame,
/// Pets you've tamed with a collar
Owned(EcsEntity), Owned(EcsEntity),
} }
@ -27,6 +33,10 @@ impl Alignment {
match (self, other) { match (self, other) {
(Alignment::Enemy, Alignment::Enemy) => true, (Alignment::Enemy, Alignment::Enemy) => true,
(Alignment::Owned(a), Alignment::Owned(b)) if a == b => 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, _ => false,
} }
} }
@ -40,6 +50,9 @@ impl Component for Alignment {
pub struct Agent { pub struct Agent {
pub patrol_origin: Option<Vec3<f32>>, pub patrol_origin: Option<Vec3<f32>>,
pub activity: Activity, 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 { impl Agent {
@ -47,6 +60,15 @@ impl Agent {
self.patrol_origin = Some(origin); self.patrol_origin = Some(origin);
self 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 { impl Component for Agent {
@ -85,3 +107,50 @@ impl Activity {
impl Default for Activity { impl Default for Activity {
fn default() -> Self { Activity::Idle(Vec2::zero()) } 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 // Reexports
pub use ability::{CharacterAbility, ItemConfig, Loadout}; pub use ability::{CharacterAbility, ItemConfig, Loadout};
pub use admin::Admin; pub use admin::Admin;
pub use agent::{Agent, Alignment}; pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION};
pub use body::{ pub use body::{
biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, golem, biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, golem,
humanoid, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData, humanoid, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,

View File

@ -24,6 +24,7 @@ sum_type! {
Sticky(comp::Sticky), Sticky(comp::Sticky),
Loadout(comp::Loadout), Loadout(comp::Loadout),
CharacterState(comp::CharacterState), CharacterState(comp::CharacterState),
SpeechBubble(comp::SpeechBubble),
Pos(comp::Pos), Pos(comp::Pos),
Vel(comp::Vel), Vel(comp::Vel),
Ori(comp::Ori), Ori(comp::Ori),
@ -50,6 +51,7 @@ sum_type! {
Sticky(PhantomData<comp::Sticky>), Sticky(PhantomData<comp::Sticky>),
Loadout(PhantomData<comp::Loadout>), Loadout(PhantomData<comp::Loadout>),
CharacterState(PhantomData<comp::CharacterState>), CharacterState(PhantomData<comp::CharacterState>),
SpeechBubble(PhantomData<comp::SpeechBubble>),
Pos(PhantomData<comp::Pos>), Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>), Vel(PhantomData<comp::Vel>),
Ori(PhantomData<comp::Ori>), Ori(PhantomData<comp::Ori>),
@ -76,6 +78,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Loadout(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::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::Pos(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Vel(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), 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::Sticky(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Loadout(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::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::Pos(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Vel(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), EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world),
@ -128,6 +132,9 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::CharacterState(_) => { EcsCompPhantom::CharacterState(_) => {
sync::handle_remove::<comp::CharacterState>(entity, world) 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::Pos(_) => sync::handle_remove::<comp::Pos>(entity, world),
EcsCompPhantom::Vel(_) => sync::handle_remove::<comp::Vel>(entity, world), EcsCompPhantom::Vel(_) => sync::handle_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_remove::<comp::Ori>(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::Sticky>();
ecs.register::<comp::Gravity>(); ecs.register::<comp::Gravity>();
ecs.register::<comp::CharacterState>(); ecs.register::<comp::CharacterState>();
ecs.register::<comp::SpeechBubble>();
// Register components send from clients -> server // Register components send from clients -> server
ecs.register::<comp::Controller>(); ecs.register::<comp::Controller>();

View File

@ -4,7 +4,7 @@ use crate::{
agent::Activity, agent::Activity,
item::{tool::ToolKind, ItemKind}, item::{tool::ToolKind, ItemKind},
Agent, Alignment, CharacterState, ControlAction, Controller, Loadout, MountState, Ori, Pos, Agent, Alignment, CharacterState, ControlAction, Controller, Loadout, MountState, Ori, Pos,
Scale, Stats, Scale, SpeechBubble, Stats,
}, },
path::Chaser, path::Chaser,
state::{DeltaTime, Time}, state::{DeltaTime, Time},
@ -38,6 +38,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Alignment>, ReadStorage<'a, Alignment>,
WriteStorage<'a, Agent>, WriteStorage<'a, Agent>,
WriteStorage<'a, Controller>, WriteStorage<'a, Controller>,
WriteStorage<'a, SpeechBubble>,
ReadStorage<'a, MountState>, ReadStorage<'a, MountState>,
); );
@ -58,6 +59,7 @@ impl<'a> System<'a> for Sys {
alignments, alignments,
mut agents, mut agents,
mut controllers, mut controllers,
mut speech_bubbles,
mount_states, mount_states,
): Self::SystemData, ): Self::SystemData,
) { ) {
@ -376,23 +378,32 @@ impl<'a> System<'a> for Sys {
// last!) --- // last!) ---
// Attack a target that's attacking us // 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 // 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 } if let comp::HealthSource::Attack { by }
| comp::HealthSource::Projectile { owner: Some(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 !agent.activity.is_attack() {
if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
{ {
agent.activity = Activity::Attack { if stats.get(attacker).map_or(false, |a| !a.is_dead) {
target: attacker, if agent.can_speak {
chaser: Chaser::default(), let message =
time: time.0, "npc.speech.villager_under_attack".to_string();
been_close: false, let bubble = SpeechBubble::npc_new(message, *time);
powerup: 0.0, 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::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default()); state.ecs_mut().insert(sys::TerrainTimer::default());
state.ecs_mut().insert(sys::WaypointTimer::default()); state.ecs_mut().insert(sys::WaypointTimer::default());
state.ecs_mut().insert(sys::SpeechBubbleTimer::default());
state state
.ecs_mut() .ecs_mut()
.insert(sys::StatsPersistenceTimer::default()); .insert(sys::StatsPersistenceTimer::default());

View File

@ -4,7 +4,10 @@ use crate::{
CLIENT_TIMEOUT, CLIENT_TIMEOUT,
}; };
use common::{ 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}, event::{EventBus, ServerEvent},
msg::{ msg::{
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate, validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
@ -43,6 +46,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Player>, WriteStorage<'a, Player>,
WriteStorage<'a, Client>, WriteStorage<'a, Client>,
WriteStorage<'a, Controller>, WriteStorage<'a, Controller>,
WriteStorage<'a, SpeechBubble>,
); );
fn run( fn run(
@ -67,12 +71,11 @@ impl<'a> System<'a> for Sys {
mut players, mut players,
mut clients, mut clients,
mut controllers, mut controllers,
mut speech_bubbles,
): Self::SystemData, ): Self::SystemData,
) { ) {
timer.start(); timer.start();
let time = time.0;
let persistence_db_dir = &persistence_db_dir.0; let persistence_db_dir = &persistence_db_dir.0;
let mut server_emitter = server_event_bus.emitter(); let mut server_emitter = server_event_bus.emitter();
@ -92,13 +95,13 @@ impl<'a> System<'a> for Sys {
// Update client ping. // Update client ping.
if new_msgs.len() > 0 { if new_msgs.len() > 0 {
client.last_ping = time client.last_ping = time.0
} else if time - client.last_ping > CLIENT_TIMEOUT // Timeout } else if time.0 - client.last_ping > CLIENT_TIMEOUT // Timeout
|| client.postbox.error().is_some() || client.postbox.error().is_some()
// Postbox error // Postbox error
{ {
server_emitter.emit(ServerEvent::ClientDisconnect(entity)); 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. // Try pinging the client if the timeout is nearing.
client.postbox.send_message(ServerMsg::Ping); client.postbox.send_message(ServerMsg::Ping);
} }
@ -394,13 +397,16 @@ impl<'a> System<'a> for Sys {
for (entity, msg) in new_chat_msgs { for (entity, msg) in new_chat_msgs {
match msg { match msg {
ServerMsg::ChatMsg { chat_type, message } => { ServerMsg::ChatMsg { chat_type, message } => {
if let Some(entity) = entity { let message = if let Some(entity) = entity {
// Handle chat commands. // Handle chat commands.
if message.starts_with("/") && message.len() > 1 { if message.starts_with("/") && message.len() > 1 {
let argv = String::from(&message[1..]); let argv = String::from(&message[1..]);
server_emitter.emit(ServerEvent::ChatCmd(entity, argv)); server_emitter.emit(ServerEvent::ChatCmd(entity, argv));
continue;
} else { } 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) => { Some(player) => {
if admins.get(entity).is_some() { if admins.get(entity).is_some() {
format!("[ADMIN][{}] {}", &player.alias, message) format!("[ADMIN][{}] {}", &player.alias, message)
@ -409,17 +415,14 @@ impl<'a> System<'a> for Sys {
} }
}, },
None => format!("[<Unknown>] {}", message), 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 { } else {
let msg = ServerMsg::ChatMsg { chat_type, message }; message
for client in (&mut clients).join().filter(|c| c.is_registered()) { };
client.notify(msg.clone()); 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 message;
pub mod persistence; pub mod persistence;
pub mod sentinel; pub mod sentinel;
pub mod speech_bubble;
pub mod subscription; pub mod subscription;
pub mod terrain; pub mod terrain;
pub mod terrain_sync; pub mod terrain_sync;
@ -20,6 +21,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>; pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>; pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>; pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type SpeechBubbleTimer = SysTimer<speech_bubble::Sys>;
pub type StatsPersistenceTimer = SysTimer<persistence::stats::Sys>; pub type StatsPersistenceTimer = SysTimer<persistence::stats::Sys>;
pub type StatsPersistenceScheduler = SysScheduler<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_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys"; const TERRAIN_SYS: &str = "server_terrain_sys";
const WAYPOINT_SYS: &str = "waypoint_sys"; const WAYPOINT_SYS: &str = "waypoint_sys";
const SPEECH_BUBBLE_SYS: &str = "speech_bubble_sys";
const STATS_PERSISTENCE_SYS: &str = "stats_persistence_sys"; const STATS_PERSISTENCE_SYS: &str = "stats_persistence_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]); dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(waypoint::Sys, WAYPOINT_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, &[]); dispatch_builder.add(persistence::stats::Sys, STATS_PERSISTENCE_SYS, &[]);
} }

View File

@ -2,7 +2,7 @@ use super::SysTimer;
use common::{ use common::{
comp::{ comp::{
Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, Loadout, 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, msg::EcsCompPacket,
sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt}, sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt},
@ -54,6 +54,7 @@ pub struct TrackedComps<'a> {
pub gravity: ReadStorage<'a, Gravity>, pub gravity: ReadStorage<'a, Gravity>,
pub loadout: ReadStorage<'a, Loadout>, pub loadout: ReadStorage<'a, Loadout>,
pub character_state: ReadStorage<'a, CharacterState>, pub character_state: ReadStorage<'a, CharacterState>,
pub speech_bubble: ReadStorage<'a, SpeechBubble>,
} }
impl<'a> TrackedComps<'a> { impl<'a> TrackedComps<'a> {
pub fn create_entity_package( pub fn create_entity_package(
@ -125,6 +126,10 @@ impl<'a> TrackedComps<'a> {
.get(entity) .get(entity)
.cloned() .cloned()
.map(|c| comps.push(c.into())); .map(|c| comps.push(c.into()));
self.speech_bubble
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
// Add untracked comps // Add untracked comps
pos.map(|c| comps.push(c.into())); pos.map(|c| comps.push(c.into()));
vel.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 gravity: ReadExpect<'a, UpdateTracker<Gravity>>,
pub loadout: ReadExpect<'a, UpdateTracker<Loadout>>, pub loadout: ReadExpect<'a, UpdateTracker<Loadout>>,
pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>, pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
pub speech_bubble: ReadExpect<'a, UpdateTracker<SpeechBubble>>,
} }
impl<'a> ReadTrackers<'a> { impl<'a> ReadTrackers<'a> {
pub fn create_sync_packages( pub fn create_sync_packages(
@ -188,6 +194,12 @@ impl<'a> ReadTrackers<'a> {
&*self.character_state, &*self.character_state,
&comps.character_state, &comps.character_state,
filter, filter,
)
.with_component(
&comps.uid,
&*self.speech_bubble,
&comps.speech_bubble,
filter,
); );
(entity_sync_package, comp_sync_package) (entity_sync_package, comp_sync_package)
@ -213,6 +225,7 @@ pub struct WriteTrackers<'a> {
gravity: WriteExpect<'a, UpdateTracker<Gravity>>, gravity: WriteExpect<'a, UpdateTracker<Gravity>>,
loadout: WriteExpect<'a, UpdateTracker<Loadout>>, loadout: WriteExpect<'a, UpdateTracker<Loadout>>,
character_state: WriteExpect<'a, UpdateTracker<CharacterState>>, character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
speech_bubble: WriteExpect<'a, UpdateTracker<SpeechBubble>>,
} }
fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
@ -236,6 +249,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers trackers
.character_state .character_state
.record_changes(&comps.character_state); .record_changes(&comps.character_state);
trackers.speech_bubble.record_changes(&comps.speech_bubble);
} }
pub fn register_trackers(world: &mut World) { 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::<Gravity>();
world.register_tracker::<Loadout>(); world.register_tracker::<Loadout>();
world.register_tracker::<CharacterState>(); world.register_tracker::<CharacterState>();
world.register_tracker::<SpeechBubble>();
} }
/// Deleted entities grouped by region /// 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 .health
.set_to(stats.health.maximum(), comp::HealthSource::Revive); .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 // TODO: This code sets an appropriate base_damage for the enemy. This doesn't
// work because the damage is now saved in an ability // work because the damage is now saved in an ability
/* /*
@ -344,7 +346,7 @@ impl<'a> System<'a> for Sys {
loadout, loadout,
body, body,
alignment, alignment,
agent: comp::Agent::default().with_patrol_origin(entity.pos), agent: comp::Agent::new(entity.pos, can_speak),
scale: comp::Scale(scale), scale: comp::Scale(scale),
drop_item: entity.loot_drop, drop_item: entity.loot_drop,
}) })

View File

@ -272,6 +272,29 @@ image_ids! {
progress_frame: "voxygen.element.frames.progress_bar", progress_frame: "voxygen.element.frames.progress_bar",
progress: "voxygen.element.misc_bg.progress", 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> <BlankGraphic>
nothing: (), nothing: (),
} }

View File

@ -7,6 +7,7 @@ mod img_ids;
mod item_imgs; mod item_imgs;
mod map; mod map;
mod minimap; mod minimap;
mod overhead;
mod popup; mod popup;
mod settings_window; mod settings_window;
mod skillbar; mod skillbar;
@ -102,17 +103,6 @@ widget_ids! {
crosshair_inner, crosshair_inner,
crosshair_outer, 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 // SCT
player_scts[], player_scts[],
player_sct_bgs[], player_sct_bgs[],
@ -125,6 +115,8 @@ widget_ids! {
sct_bgs[], sct_bgs[],
scts[], scts[],
overheads[],
// Intro Text // Intro Text
intro_bg, intro_bg,
intro_text, intro_text,
@ -243,6 +235,7 @@ pub enum Event {
Sct(bool), Sct(bool),
SctPlayerBatch(bool), SctPlayerBatch(bool),
SctDamageBatch(bool), SctDamageBatch(bool),
SpeechBubbleDarkMode(bool),
ToggleDebug(bool), ToggleDebug(bool),
UiScale(ScaleChange), UiScale(ScaleChange),
CharacterSelection, CharacterSelection,
@ -574,6 +567,7 @@ impl Hud {
let stats = ecs.read_storage::<comp::Stats>(); let stats = ecs.read_storage::<comp::Stats>();
let energy = ecs.read_storage::<comp::Energy>(); let energy = ecs.read_storage::<comp::Energy>();
let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>(); 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 interpolated = ecs.read_storage::<vcomp::Interpolated>();
let players = ecs.read_storage::<comp::Player>(); let players = ecs.read_storage::<comp::Player>();
let scales = ecs.read_storage::<comp::Scale>(); 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" // Max amount the sct font size increases when "flashing"
const FLASH_MAX: f32 = 25.0; 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. // Get player position.
let player_pos = client let player_pos = client
.state() .state()
@ -657,265 +647,6 @@ impl Hud {
.read_storage::<comp::Pos>() .read_storage::<comp::Pos>()
.get(client.entity()) .get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0); .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 { if global_state.settings.gameplay.sct {
// Render Player SCT numbers // Render Player SCT numbers
@ -1156,21 +887,27 @@ impl Hud {
} }
} }
// Render Name Tags let mut overhead_walker = self.ids.overheads.walk();
for (pos, name, level, height_offset) in ( 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, &entities,
&pos, &pos,
interpolated.maybe(), interpolated.maybe(),
&stats, &stats,
&energy,
players.maybe(), players.maybe(),
scales.maybe(), scales.maybe(),
&bodies, &bodies,
&hp_floater_lists, &hp_floater_lists,
speech_bubbles.maybe(),
) )
.join() .join()
.filter(|(entity, _, _, stats, _, _, _, _)| *entity != me && !stats.is_dead) .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead)
// Don't show outside a certain range // Don't show outside a certain range
.filter(|(_, pos, _, _, _, _, _, hpfl)| { .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| {
pos.0.distance_squared(player_pos) pos.0.distance_squared(player_pos)
< (if hpfl < (if hpfl
.time_since_last_dmg_by_me .time_since_last_dmg_by_me
@ -1182,7 +919,7 @@ impl Hud {
}) })
.powi(2) .powi(2)
}) })
.map(|(_, pos, interpolated, stats, player, scale, body, _)| { .map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, bubble)| {
// TODO: This is temporary // TODO: This is temporary
// If the player used the default character name display their name instead // If the player used the default character name display their name instead
let name = if stats.name == "Character Name" { let name = if stats.name == "Character Name" {
@ -1192,87 +929,173 @@ impl Hud {
}; };
( (
interpolated.map_or(pos.0, |i| i.pos), interpolated.map_or(pos.0, |i| i.pos),
format!("{}", name), name,
stats.level, 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), body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
hpfl,
bubble,
) )
}) })
{ {
let name_id = name_id_walker.next( let overhead_id = overhead_walker.next(
&mut self.ids.name_tags, &mut self.ids.overheads,
&mut ui_widgets.widget_id_generator(), &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; let ingame_pos = pos + Vec3::unit_z() * height_offset;
// Name // Speech bubble, name, level, and hp bars
Text::new(&name) overhead::Overhead::new(
.font_id(self.fonts.cyri.conrod_id) &name,
.font_size(30) bubble,
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) stats,
.x_y(-1.0, MANA_BAR_Y + 48.0) energy,
.position_ingame(ingame_pos) own_level,
.set(name_bg_id, ui_widgets); &global_state.settings.gameplay,
Text::new(&name) self.pulse,
.font_id(self.fonts.cyri.conrod_id) &self.voxygen_i18n,
.font_size(30) &self.imgs,
.color(Color::Rgba(0.61, 0.61, 0.89, 1.0)) &self.fonts,
.x_y(0.0, MANA_BAR_Y + 50.0) )
.position_ingame(ingame_pos) .x_y(0.0, 100.0)
.set(name_id, ui_widgets); .position_ingame(ingame_pos)
.set(overhead_id, ui_widgets);
// Level // Enemy SCT
const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4); if global_state.settings.gameplay.sct && !hpfl.floaters.is_empty() {
const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0); let floaters = &hpfl.floaters;
const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
let op_level = level.level(); // Colors
let level_str = format!("{}", op_level); const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
// Change visuals of the level display depending on the player level/opponent const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
// level const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
let level_comp = op_level as i64 - own_level as i64; const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
// + 10 level above player -> skull const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
// + 5-10 levels above player -> high const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
// -5 - +5 levels around player level -> equal const DAMAGE_COLORS: [Rgb<f32>; 6] = [
// - 5 levels below player -> low WHITE,
Text::new(if level_comp < 10 { &level_str } else { "?" }) LIGHT_OR,
.font_id(self.fonts.cyri.conrod_id) LIGHT_MED_OR,
.font_size(if op_level > 9 && level_comp < 10 { MED_OR,
14 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 { } else {
15 for floater in floaters {
}) let number_speed = 250.0; // Single Numbers Speed
.color(if level_comp > 4 { let sct_id = sct_walker
HIGH .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
} else if level_comp < -5 { let sct_bg_id = sct_bg_walker
LOW .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
} else { // Calculate total change
EQUAL let max_hp_frac =
}) floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0) // Increase font size based on fraction of maximum health
.position_ingame(ingame_pos) // "flashes" by having a larger size in the first 100ms
.set(level_id, ui_widgets); let font_size = 30
if level_comp > 9 { + (max_hp_frac * 30.0) as u32
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer + if floater.timer < 0.1 {
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 { (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32
self.imgs.skull_2 } else {
} else { 0
self.imgs.skull };
}) let font_col = font_col(font_size);
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE) // Timer sets the widget offset
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0) let y = (floater.timer as f64
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))) / crate::ecs::sys::floater::HP_SHOWTIME as f64
.position_ingame(ingame_pos) * number_speed)
.set(level_skull_id, ui_widgets); + 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) .set(self.ids.settings_window, ui_widgets)
{ {
match event { match event {
settings_window::Event::SpeechBubbleDarkMode(sbdm) => {
events.push(Event::SpeechBubbleDarkMode(sbdm));
},
settings_window::Event::Sct(sct) => { settings_window::Event::Sct(sct) => {
events.push(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_text,
sct_num_dur_slider, sct_num_dur_slider,
sct_num_dur_value, sct_num_dur_value,
speech_bubble_dark_mode_text,
speech_bubble_dark_mode_button,
free_look_behavior_text, free_look_behavior_text,
free_look_behavior_list free_look_behavior_list
} }
@ -235,6 +237,7 @@ pub enum Event {
Sct(bool), Sct(bool),
SctPlayerBatch(bool), SctPlayerBatch(bool),
SctDamageBatch(bool), SctDamageBatch(bool),
SpeechBubbleDarkMode(bool),
ChangeLanguage(LanguageMetadata), ChangeLanguage(LanguageMetadata),
ChangeBinding(GameInput), ChangeBinding(GameInput),
ChangeFreeLookBehavior(PressBehavior), ChangeFreeLookBehavior(PressBehavior),
@ -943,17 +946,45 @@ impl<'a> Widget for SettingsWindow<'a> {
.set(state.ids.sct_batch_inc_text, ui); .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 // Energybars Numbers
// Hotbar text // Hotbar text
Text::new(&self.localized_strings.get("hud.settings.energybar_numbers")) Text::new(&self.localized_strings.get("hud.settings.energybar_numbers"))
.down_from( .down_from(state.ids.speech_bubble_dark_mode_button, 20.0)
if self.global_state.settings.gameplay.sct {
state.ids.sct_batch_inc_radio
} else {
state.ids.sct_show_radio
},
20.0,
)
.font_size(self.fonts.cyri.scale(18)) .font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR) .color(TEXT_COLOR)

View File

@ -53,10 +53,15 @@ pub type VoxygenFonts = HashMap<String, Font>;
pub struct VoxygenLocalization { pub struct VoxygenLocalization {
/// A map storing the localized texts /// 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>, 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. /// into a ASCII version by using the `deunicode` crate.
pub convert_utf8_to_ascii: bool, pub convert_utf8_to_ascii: bool,
@ -78,23 +83,56 @@ impl VoxygenLocalization {
} }
} }
/// Return the missing keys compared to the reference language and return /// Get a variation of localized text from the given key
/// them ///
pub fn list_missing_entries(&self) -> HashSet<String> { /// `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 = let reference_localization =
load_expect::<VoxygenLocalization>(i18n_asset_key(REFERENCE_LANG).as_ref()); 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 /// Log missing entries (compared to the reference language) as warnings
pub fn log_missing_entries(&self) { 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!( 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, self.metadata.language_identifier,
missing_key missing_key
); );
@ -116,6 +154,10 @@ impl Asset for VoxygenLocalization {
for value in asked_localization.string_map.values_mut() { for value in asked_localization.string_map.values_mut() {
*value = deunicode(value); *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 = asked_localization.metadata.language_name =
deunicode(&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.gameplay.sct_damage_batch = sct_damage_batch;
global_state.settings.save_to_file_warn(); 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) => { HudEvent::ToggleDebug(toggle_debug) => {
global_state.settings.gameplay.toggle_debug = toggle_debug; global_state.settings.gameplay.toggle_debug = toggle_debug;
global_state.settings.save_to_file_warn(); global_state.settings.save_to_file_warn();

View File

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

View File

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