mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'CapsizeGlimmer/chat_bubbles' into 'master'
Capsize glimmer/chat bubbles See merge request veloren/veloren!1017
This commit is contained in:
commit
2ad8172d6f
@ -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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
BIN
assets/voxygen/element/frames/bubble_dark/top_right.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -383,5 +383,8 @@ Willenskraft
|
||||
"esc_menu.logout": "Ausloggen",
|
||||
"esc_menu.quit_game": "Desktop",
|
||||
/// End Escape Menu Section
|
||||
},
|
||||
|
||||
vector_map: {
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -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.",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
@ -323,5 +323,8 @@ Force
|
||||
Dexterité
|
||||
|
||||
Intelligence"#,
|
||||
},
|
||||
|
||||
vector_map: {
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -524,5 +524,8 @@ Volontà
|
||||
"esc_menu.logout": "Disconnettiti",
|
||||
"esc_menu.quit_game": "Esci dal Gioco",
|
||||
/// End Escape Menu Section
|
||||
}
|
||||
},
|
||||
|
||||
vector_map: {
|
||||
}
|
||||
)
|
||||
|
@ -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: {
|
||||
}
|
||||
)
|
||||
|
@ -365,5 +365,8 @@ https://account.veloren.net."#,
|
||||
"esc_menu.logout": "Выйти в меню",
|
||||
"esc_menu.quit_game": "Выйти из игры",
|
||||
/// End Escape Menu Section
|
||||
},
|
||||
|
||||
vector_map: {
|
||||
}
|
||||
)
|
||||
|
@ -397,5 +397,8 @@ Hareket gücü
|
||||
"esc_menu.logout": "Çıkış yap",
|
||||
"esc_menu.quit_game": "Oyundan çık",
|
||||
/// End Escape Menu Section
|
||||
},
|
||||
|
||||
vector_map: {
|
||||
}
|
||||
)
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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>();
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
|
@ -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, &[]);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
29
server/src/sys/speech_bubble.rs
Normal file
29
server/src/sys/speech_bubble.rs
Normal 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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
||||
|
@ -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: (),
|
||||
}
|
||||
|
@ -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
361
voxygen/src/hud/overhead.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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(¤t_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);
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user