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
|
- 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
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.logout": "Ausloggen",
|
||||||
"esc_menu.quit_game": "Desktop",
|
"esc_menu.quit_game": "Desktop",
|
||||||
/// End Escape Menu Section
|
/// 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.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.",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -323,5 +323,8 @@ Force
|
|||||||
Dexterité
|
Dexterité
|
||||||
|
|
||||||
Intelligence"#,
|
Intelligence"#,
|
||||||
|
},
|
||||||
|
|
||||||
|
vector_map: {
|
||||||
}
|
}
|
||||||
)
|
)
|
@ -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: {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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: {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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: {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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: {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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>();
|
||||||
|
@ -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,16 +378,24 @@ 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())
|
||||||
{
|
{
|
||||||
|
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 {
|
agent.activity = Activity::Attack {
|
||||||
target: attacker,
|
target: attacker,
|
||||||
chaser: Chaser::default(),
|
chaser: Chaser::default(),
|
||||||
@ -398,6 +408,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Follow owner if we're too far, or if they're under attack
|
// Follow owner if we're too far, or if they're under attack
|
||||||
if let Some(Alignment::Owned(owner)) = alignment.copied() {
|
if let Some(Alignment::Owned(owner)) = alignment.copied() {
|
||||||
|
@ -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());
|
||||||
|
@ -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,18 +415,15 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => format!("[<Unknown>] {}", message),
|
None => format!("[<Unknown>] {}", message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message
|
||||||
};
|
};
|
||||||
let msg = ServerMsg::ChatMsg { chat_type, message };
|
let msg = ServerMsg::ChatMsg { chat_type, message };
|
||||||
for client in (&mut clients).join().filter(|c| c.is_registered()) {
|
for client in (&mut clients).join().filter(|c| c.is_registered()) {
|
||||||
client.notify(msg.clone());
|
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
panic!("Invalid message type.");
|
panic!("Invalid message type.");
|
||||||
|
@ -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, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
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
|
.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,
|
||||||
})
|
})
|
||||||
|
@ -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: (),
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
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)
|
.position_ingame(ingame_pos)
|
||||||
.set(name_bg_id, ui_widgets);
|
.set(overhead_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);
|
|
||||||
|
|
||||||
// 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,
|
||||||
|
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)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(if op_level > 9 && level_comp < 10 {
|
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
|
||||||
14
|
.x_y(0.0, y - 3.0)
|
||||||
} 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)
|
.position_ingame(ingame_pos)
|
||||||
.set(level_id, ui_widgets);
|
.set(sct_bg_id, ui_widgets);
|
||||||
if level_comp > 9 {
|
Text::new(&format!("{}", hp_damage.abs()))
|
||||||
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
|
.font_size(font_size)
|
||||||
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
self.imgs.skull_2
|
.x_y(0.0, y)
|
||||||
|
.color(if hp_damage < 0 {
|
||||||
|
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
|
||||||
} else {
|
} else {
|
||||||
self.imgs.skull
|
Color::Rgba(0.1, 1.0, 0.1, fade)
|
||||||
})
|
})
|
||||||
.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)
|
.position_ingame(ingame_pos)
|
||||||
.set(level_skull_id, ui_widgets);
|
.set(sct_id, ui_widgets);
|
||||||
|
} else {
|
||||||
|
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)
|
.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
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_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,9 +946,12 @@ impl<'a> Widget for SettingsWindow<'a> {
|
|||||||
.set(state.ids.sct_batch_inc_text, ui);
|
.set(state.ids.sct_batch_inc_text, ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Energybars Numbers
|
// Speech bubble dark mode
|
||||||
// Hotbar text
|
let speech_bubble_dark_mode = ToggleButton::new(
|
||||||
Text::new(&self.localized_strings.get("hud.settings.energybar_numbers"))
|
self.global_state.settings.gameplay.speech_bubble_dark_mode,
|
||||||
|
self.imgs.checkbox,
|
||||||
|
self.imgs.checkbox_checked,
|
||||||
|
)
|
||||||
.down_from(
|
.down_from(
|
||||||
if self.global_state.settings.gameplay.sct {
|
if self.global_state.settings.gameplay.sct {
|
||||||
state.ids.sct_batch_inc_radio
|
state.ids.sct_batch_inc_radio
|
||||||
@ -954,6 +960,31 @@ impl<'a> Widget for SettingsWindow<'a> {
|
|||||||
},
|
},
|
||||||
20.0,
|
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(state.ids.speech_bubble_dark_mode_button, 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)
|
||||||
|
@ -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(¤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
|
/// 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);
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user