Initial implementation of buffs UI

player buffs animation

more testing debuffs

sorting and display limit fix

overhead buffs

fix

WIP buff removal function

fmt

Update buffs.rs

Now with compiling: WIP group UI buffs

positioning

Update group.rs

Update group.rs

Small optimizations.

Fixed positioning of buffs in group panel. Broke buff tooltips in group panel.

buff frame visuals

added setting for displaying buffs at minimap
This commit is contained in:
Monty Marz 2020-10-16 08:08:45 +02:00 committed by Sam
parent 007d3c09ac
commit 8fa398954d
30 changed files with 824 additions and 178 deletions

View File

@ -1,6 +1,6 @@
ItemDef(
name: "Apple Stick",
description: "Restores 20 Health",
description: "Restores 25 Health",
kind: Consumable(
kind: "AppleStick",
effect: Health((

BIN
assets/voxygen/element/animation/buff_frame/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/3.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/4.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/5.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/6.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/7.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/8.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -291,6 +291,8 @@ magically infused items?"#,
"hud.settings.transparency": "Transparency",
"hud.settings.hotbar": "Hotbar",
"hud.settings.toggle_shortcuts": "Toggle Shortcuts",
"hud.settings.buffs_skillbar": "Buffs at Skillbar",
"hud.settings.buffs_mmap": "Buffs at Minimap",
"hud.settings.toggle_bar_experience": "Toggle Experience Bar",
"hud.settings.scrolling_combat_text": "Scrolling Combat Text",
"hud.settings.single_damage_number": "Single Damage Numbers",
@ -343,9 +345,9 @@ magically infused items?"#,
"hud.settings.refresh_rate": "Refresh Rate",
"hud.settings.save_window_size": "Save window size",
"hud.settings.lighting_rendering_mode": "Lighting Rendering Mode",
"hud.settings.lighting_rendering_mode.ashikhmin": "Type A",
"hud.settings.lighting_rendering_mode.blinnphong": "Type B",
"hud.settings.lighting_rendering_mode.lambertian": "Type L",
"hud.settings.lighting_rendering_mode.ashikhmin": "Type A - High ",
"hud.settings.lighting_rendering_mode.blinnphong": "Type B - Medium",
"hud.settings.lighting_rendering_mode.lambertian": "Type L - Cheap",
"hud.settings.shadow_rendering_mode": "Shadow Rendering Mode",
"hud.settings.shadow_rendering_mode.none": "None",
"hud.settings.shadow_rendering_mode.cheap": "Cheap",
@ -509,6 +511,16 @@ Protection
"esc_menu.quit_game": "Quit Game",
/// End Escape Menu Section
/// Buffs and Debuffs
"buff.remove": "Click to remove",
"buff.title.missing": "Missing Title",
"buff.desc.missing": "Missing Description",
// Buffs
"buff.title.heal_test": "Heal Test",
"buff.desc.heal_test": "This is a test buff to test healing.",
// Debuffs
"debuff.title.bleed_test": "Bleed Test",
"debuff.desc.bleed_test": "This is a test debuff to test bleeding.",
},

View File

@ -37,6 +37,7 @@ use common::{
terrain::{block::Block, neighbors, TerrainChunk, TerrainChunkSize},
vol::RectVolSize,
};
use comp::BuffId;
use futures_executor::block_on;
use futures_timer::Delay;
use futures_util::{select, FutureExt};
@ -631,6 +632,12 @@ impl Client {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern));
}
pub fn remove_buff(&mut self, buff_id: BuffId) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::RemoveBuff(
buff_id,
)));
}
pub fn max_group_size(&self) -> u32 { self.max_group_size }
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> {

View File

@ -151,16 +151,10 @@ impl Buff {
pub fn new(id: BuffId, cat_ids: Vec<BuffCategoryId>, source: BuffSource) -> Self {
let (effects, time) = match id {
BuffId::Bleeding { strength, duration } => (
vec![
BuffEffect::HealthChangeOverTime {
rate: -strength,
accumulated: 0.0,
},
// This effect is for testing purposes
BuffEffect::NameChange {
prefix: String::from("Injured "),
},
],
vec![BuffEffect::HealthChangeOverTime {
rate: -strength,
accumulated: 0.0,
}],
duration,
),
BuffId::Regeneration { strength, duration } => (

View File

@ -1,4 +1,8 @@
use crate::{comp::inventory::slot::Slot, sync::Uid, util::Dir};
use crate::{
comp::{inventory::slot::Slot, BuffId},
sync::Uid,
util::Dir,
};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -37,6 +41,7 @@ pub enum ControlEvent {
Unmount,
InventoryManip(InventoryManip),
GroupManip(GroupManip),
RemoveBuff(BuffId),
Respawn,
}

View File

@ -684,7 +684,7 @@ impl<'a> System<'a> for Sys {
for (_invite, /*alignment,*/ agent, controller) in
(&invites, /*&alignments,*/ &mut agents, &mut controllers).join()
{
let accept = false; // set back to "matches!(alignment, Alignment::Npc)" when we got better NPC recruitment mechanics
let accept = true; // set back to "matches!(alignment, Alignment::Npc)" when we got better NPC recruitment mechanics
if accept {
// Clear agent comp
*agent = Agent::default();

View File

@ -62,7 +62,7 @@ impl<'a> System<'a> for Sys {
BuffEffect::HealthChangeOverTime { rate, accumulated } => {
*accumulated += *rate * buff_delta;
// Apply only 0.5 or higher damage
if accumulated.abs() > 5.0 {
if accumulated.abs() > 50.0 {
let cause = if *accumulated > 0.0 {
HealthSource::Healing { by: buff_owner }
} else {

View File

@ -157,12 +157,31 @@ impl<'a> System<'a> for Sys {
buff_change: buff::BuffChange::Add(buff::Buff::new(
buff::BuffId::Bleeding {
strength: attack.base_damage as f32,
duration: Some(Duration::from_secs(10)),
duration: Some(Duration::from_secs(30)),
},
vec![buff::BuffCategoryId::Physical, buff::BuffCategoryId::Debuff],
buff::BuffSource::Character { by: *uid },
)),
});
server_emitter.emit(ServerEvent::Buff {
uid: *uid_b,
buff_change: buff::BuffChange::Add(buff::Buff::new(
buff::BuffId::Regeneration {
strength: 100.0,
duration: Some(Duration::from_secs(60)),
},
vec![buff::BuffCategoryId::Physical, buff::BuffCategoryId::Buff],
buff::BuffSource::Character { by: *uid },
)),
});
server_emitter.emit(ServerEvent::Buff {
uid: *uid_b,
buff_change: buff::BuffChange::Add(buff::Buff::new(
buff::BuffId::Cursed { duration: None },
vec![buff::BuffCategoryId::Physical, buff::BuffCategoryId::Debuff],
buff::BuffSource::Character { by: *uid },
)),
});
attack.hit_count += 1;
}
if attack.knockback != 0.0 && damage.healthchange != 0.0 {

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
slot::{EquipSlot, Slot},
CharacterState, ControlEvent, Controller, InventoryManip,
BuffChange, CharacterState, ControlEvent, Controller, InventoryManip,
},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
@ -51,7 +51,7 @@ impl<'a> System<'a> for Sys {
span!(_guard, "run", "controller::Sys::run");
let mut server_emitter = server_bus.emitter();
for (entity, _uid, controller, character_state) in
for (entity, uid, controller, character_state) in
(&entities, &uids, &mut controllers, &mut character_states).join()
{
let mut inputs = &mut controller.inputs;
@ -83,6 +83,12 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Mount(entity, mountee_entity));
}
},
ControlEvent::RemoveBuff(buff_id) => {
server_emitter.emit(ServerEvent::Buff {
uid: *uid,
buff_change: BuffChange::RemoveById(buff_id),
});
},
ControlEvent::Unmount => server_emitter.emit(ServerEvent::Unmount(entity)),
ControlEvent::EnableLantern => {
server_emitter.emit(ServerEvent::EnableLantern(entity))

View File

@ -715,6 +715,7 @@ pub fn handle_buff(server: &mut Server, uid: Uid, buff_change: buff::BuffChange)
add_buff_effects(new_buff.clone(), stats.get_mut(entity));
buffs.active_buffs.push(new_buff);
} else {
let mut duplicate_existed = false;
for i in 0..buffs.active_buffs.len() {
let active_buff = &buffs.active_buffs[i];
// Checks if new buff has the same id as an already active buff. If it
@ -724,6 +725,7 @@ pub fn handle_buff(server: &mut Server, uid: Uid, buff_change: buff::BuffChange)
// inactive buffs and add new buff to active
// buffs.
if discriminant(&active_buff.id) == discriminant(&new_buff.id) {
duplicate_existed = true;
if determine_replace_active_buff(
active_buff.clone(),
new_buff.clone(),
@ -731,14 +733,21 @@ pub fn handle_buff(server: &mut Server, uid: Uid, buff_change: buff::BuffChange)
active_buff_indices_for_removal.push(i);
add_buff_effects(new_buff.clone(), stats.get_mut(entity));
buffs.active_buffs.push(new_buff.clone());
} else {
buffs.inactive_buffs.push(new_buff.clone());
} else if let Some(active_dur) = active_buff.time {
if let Some(new_dur) = new_buff.time {
if new_dur > active_dur {
buffs.inactive_buffs.push(new_buff.clone());
}
} else {
buffs.inactive_buffs.push(new_buff.clone());
}
}
} else {
add_buff_effects(new_buff.clone(), stats.get_mut(entity));
buffs.active_buffs.push(new_buff.clone());
}
}
if !duplicate_existed {
add_buff_effects(new_buff.clone(), stats.get_mut(entity));
buffs.active_buffs.push(new_buff.clone());
}
}
},
BuffChange::RemoveByIndex(active_indices, inactive_indices) => {
@ -871,7 +880,7 @@ fn determine_replace_active_buff(active_buff: buff::Buff, new_buff: buff::Buff)
duration: _,
} = active_buff.id
{
new_strength > active_strength
new_strength >= active_strength
} else {
false
}
@ -885,7 +894,7 @@ fn determine_replace_active_buff(active_buff: buff::Buff, new_buff: buff::Buff)
duration: _,
} = active_buff.id
{
new_strength > active_strength
new_strength >= active_strength
} else {
false
}

View File

@ -1,20 +1,23 @@
use super::{
img_ids::{Imgs, ImgsRot},
TEXT_COLOR,
BUFF_COLOR, DEBUFF_COLOR, TEXT_COLOR,
};
use crate::{
hud::{get_buff_info, BuffPosition},
i18n::VoxygenLocalization,
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
GlobalState,
};
use client::Client;
use common::comp::{self, Buffs};
use crate::hud::BuffInfo;
use common::comp::{BuffId, Buffs};
use conrod_core::{
color,
widget::{self, Button, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
widget::{self, Button, Image, Rectangle},
widget_ids, Color, Positionable, Sizeable, Widget, WidgetCommon,
};
use inline_tweak::*;
use std::time::Duration;
widget_ids! {
struct Ids {
align,
@ -22,51 +25,49 @@ widget_ids! {
debuffs_align,
buff_test,
debuff_test,
buffs[],
buff_timers[],
debuffs[],
debuff_timers[],
}
}
pub struct BuffInfo {
id: comp::BuffId,
is_buff: bool,
dur: f32,
}
#[derive(WidgetCommon)]
pub struct BuffsBar<'a> {
client: &'a Client,
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
#[conrod(common_builder)]
common: widget::CommonBuilder,
global_state: &'a GlobalState,
rot_imgs: &'a ImgsRot,
tooltip_manager: &'a mut TooltipManager,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
buffs: &'a Buffs,
pulse: f32,
global_state: &'a GlobalState,
}
impl<'a> BuffsBar<'a> {
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
pub fn new(
client: &'a Client,
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
global_state: &'a GlobalState,
rot_imgs: &'a ImgsRot,
tooltip_manager: &'a mut TooltipManager,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
buffs: &'a Buffs,
pulse: f32,
global_state: &'a GlobalState,
) -> Self {
Self {
client,
imgs,
fonts,
common: widget::CommonBuilder::default(),
global_state,
rot_imgs,
tooltip_manager,
localized_strings,
buffs,
pulse,
global_state,
}
}
}
@ -75,8 +76,12 @@ pub struct State {
ids: Ids,
}
pub enum Event {
RemoveBuff(BuffId),
}
impl<'a> Widget for BuffsBar<'a> {
type Event = ();
type Event = Vec<Event>;
type State = State;
type Style = ();
@ -91,7 +96,11 @@ impl<'a> Widget for BuffsBar<'a> {
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
let mut event = Vec::new();
let localized_strings = self.localized_strings;
let buffs = self.buffs;
let buff_ani = ((self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8) + 0.5; //Animation timer
let buff_position = self.global_state.settings.gameplay.buff_position;
let buffs_tooltip = Tooltip::new({
// Edge images [t, b, r, l]
// Corner images [tr, tl, br, bl]
@ -109,35 +118,230 @@ impl<'a> Widget for BuffsBar<'a> {
.desc_font_size(self.fonts.cyri.scale(12))
.font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR);
// Alignment
Rectangle::fill_with([484.0, 100.0], color::TRANSPARENT)
.mid_bottom_with_margin_on(ui.window, tweak!(92.0))
.set(state.ids.align, ui);
Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
.bottom_left_with_margins_on(state.ids.align, 0.0, 0.0)
.set(state.ids.debuffs_align, ui);
Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
.bottom_right_with_margins_on(state.ids.align, 0.0, 0.0)
.set(state.ids.buffs_align, ui);
// Test Widgets
Image::new(self.imgs.debuff_skull_0)
.w_h(20.0, 20.0)
.bottom_right_with_margins_on(state.ids.debuffs_align, 0.0, 1.0)
.set(state.ids.debuff_test, ui);
Image::new(self.imgs.buff_plus_0)
.w_h(20.0, 20.0)
.bottom_left_with_margins_on(state.ids.buffs_align, 0.0, 1.0)
.set(state.ids.buff_test, ui);
}
}
if let BuffPosition::Bar = buff_position {
// Alignment
Rectangle::fill_with([484.0, 100.0], color::TRANSPARENT)
.mid_bottom_with_margin_on(ui.window, tweak!(92.0))
.set(state.ids.align, ui);
Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
.bottom_left_with_margins_on(state.ids.align, 0.0, 0.0)
.set(state.ids.debuffs_align, ui);
Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
.bottom_right_with_margins_on(state.ids.align, 0.0, 0.0)
.set(state.ids.buffs_align, ui);
fn get_buff_info(buff: comp::Buff) -> BuffInfo {
BuffInfo {
id: buff.id,
is_buff: buff
.cat_ids
.iter()
.any(|cat| *cat == comp::BuffCategoryId::Buff),
dur: buff.time.map(|dur| dur.as_secs_f32()).unwrap_or(100.0),
// Buffs and Debuffs
// Create two vecs to display buffs and debuffs separately
let mut buffs_vec = Vec::<BuffInfo>::new();
let mut debuffs_vec = Vec::<BuffInfo>::new();
for buff in buffs.active_buffs.clone() {
let info = get_buff_info(buff);
if info.is_buff {
buffs_vec.push(info);
} else {
debuffs_vec.push(info);
}
}
if state.ids.buffs.len() < buffs_vec.len() {
state.update(|state| {
state
.ids
.buffs
.resize(buffs_vec.len(), &mut ui.widget_id_generator())
});
};
if state.ids.debuffs.len() < debuffs_vec.len() {
state.update(|state| {
state
.ids
.debuffs
.resize(debuffs_vec.len(), &mut ui.widget_id_generator())
});
};
if state.ids.buff_timers.len() < buffs_vec.len() {
state.update(|state| {
state
.ids
.buff_timers
.resize(buffs_vec.len(), &mut ui.widget_id_generator())
});
};
if state.ids.debuff_timers.len() < debuffs_vec.len() {
state.update(|state| {
state
.ids
.debuff_timers
.resize(debuffs_vec.len(), &mut ui.widget_id_generator())
});
};
let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
// Create Buff Widgets
for (i, buff) in buffs_vec.iter().enumerate() {
if i < 22 {
// Limit displayed buffs
let max_duration = match buff.id {
BuffId::Regeneration { duration, .. } => duration.unwrap().as_secs_f32(),
_ => 10.0,
};
let current_duration = buff.dur;
let duration_percentage = (current_duration / max_duration * 1000.0) as u32; // Percentage to determine which frame of the timer overlay is displayed
let buff_img = match buff.id {
BuffId::Regeneration { .. } => self.imgs.buff_plus_0,
_ => self.imgs.missing_icon,
};
let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
// Sort buffs into rows of 11 slots
let x = i % 11;
let y = i / 11;
let buff_widget = buff_widget.bottom_left_with_margins_on(
state.ids.buffs_align,
0.0 + y as f64 * (21.0),
0.0 + x as f64 * (21.0),
);
buff_widget
.color(if current_duration < 10.0 {
Some(pulsating_col)
} else {
Some(norm_col)
})
.set(state.ids.buffs[i], ui);
// Create Buff tooltip
let title = match buff.id {
BuffId::Regeneration { .. } => {
*&localized_strings.get("buff.title.heal_test")
},
_ => *&localized_strings.get("buff.title.missing"),
};
let remaining_time = if current_duration == 10e6 as f32 {
"Permanent".to_string()
} else {
format!("Remaining: {:.0}s", current_duration)
};
let click_to_remove = format!("<{}>", &localized_strings.get("buff.remove"));
let desc_txt = match buff.id {
BuffId::Regeneration { .. } => {
*&localized_strings.get("buff.desc.heal_test")
},
_ => *&localized_strings.get("buff.desc.missing"),
};
let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove);
// Timer overlay
if Button::image(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, //3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(20.0, 20.0)
.middle_of(state.ids.buffs[i])
.with_tooltip(
self.tooltip_manager,
title,
&desc,
&buffs_tooltip,
BUFF_COLOR,
)
.set(state.ids.buff_timers[i], ui)
.was_clicked()
{
event.push(Event::RemoveBuff(buff.id));
};
};
}
// Create Debuff Widgets
for (i, debuff) in debuffs_vec.iter().enumerate() {
if i < 22 {
// Limit displayed buffs
let max_duration = match debuff.id {
BuffId::Bleeding { duration, .. } => {
duration.unwrap_or(Duration::from_secs(60)).as_secs_f32()
},
BuffId::Cursed { duration, .. } => {
duration.unwrap_or(Duration::from_secs(60)).as_secs_f32()
},
_ => 10.0,
};
let current_duration = debuff.dur;
let duration_percentage = current_duration / max_duration * 1000.0; // Percentage to determine which frame of the timer overlay is displayed
let debuff_img = match debuff.id {
BuffId::Bleeding { .. } => self.imgs.debuff_bleed_0,
BuffId::Cursed { .. } => self.imgs.debuff_skull_0,
_ => self.imgs.missing_icon,
};
let debuff_widget = Image::new(debuff_img).w_h(20.0, 20.0);
// Sort buffs into rows of 11 slots
let x = i % 11;
let y = i / 11;
let debuff_widget = debuff_widget.bottom_right_with_margins_on(
state.ids.debuffs_align,
0.0 + y as f64 * (21.0),
0.0 + x as f64 * (21.0),
);
debuff_widget
.color(if current_duration < 10.0 {
Some(pulsating_col)
} else {
Some(norm_col)
})
.set(state.ids.debuffs[i], ui);
// Create Debuff tooltip
let title = match debuff.id {
BuffId::Bleeding { .. } => {
*&localized_strings.get("debuff.title.bleed_test")
},
_ => *&localized_strings.get("buff.title.missing"),
};
let remaining_time = if current_duration == 10e6 as f32 {
"Permanent".to_string()
} else {
format!("Remaining: {:.0}s", current_duration)
};
let desc_txt = match debuff.id {
BuffId::Bleeding { .. } => {
*&localized_strings.get("debuff.desc.bleed_test")
},
_ => *&localized_strings.get("debuff.desc.missing"),
};
let desc = format!("{}\n\n{}", desc_txt, remaining_time);
Image::new(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, //3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(20.0, 20.0)
.middle_of(state.ids.debuffs[i])
.with_tooltip(
self.tooltip_manager,
title,
&desc,
&buffs_tooltip,
DEBUFF_COLOR,
)
.set(state.ids.debuff_timers[i], ui);
};
}
}
if let BuffPosition::Map = buff_position {
// Alignment
Rectangle::fill_with([tweak!(300.0), tweak!(280.0)], color::RED)
.top_right_with_margins_on(ui.window, tweak!(5.0), tweak!(270.0))
.set(state.ids.align, ui);
}
event
}
}

View File

@ -1,15 +1,20 @@
use super::{
img_ids::Imgs, Show, BLACK, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR,
STAMINA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN,
img_ids::{Imgs, ImgsRot},
Show, BLACK, BUFF_COLOR, DEBUFF_COLOR, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR,
LOW_HP_COLOR, STAMINA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN,
};
use crate::{
i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts,
window::GameInput, GlobalState,
hud::{get_buff_info, BuffInfo},
i18n::VoxygenLocalization,
settings::Settings,
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
window::GameInput,
GlobalState,
};
use client::{self, Client};
use common::{
comp::{group::Role, Stats},
comp::{group::Role, BuffId, Buffs, Stats},
sync::{Uid, WorldSyncExt},
};
use conrod_core::{
@ -18,8 +23,8 @@ use conrod_core::{
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
};
use inline_tweak::*;
use specs::{saveload::MarkerAllocator, WorldExt};
widget_ids! {
pub struct Ids {
group_button,
@ -44,6 +49,8 @@ widget_ids! {
member_panels_txt[],
member_health[],
member_stam[],
buffs[],
buff_timers[],
dead_txt[],
health_txt[],
timeout_bg,
@ -63,10 +70,13 @@ pub struct Group<'a> {
client: &'a Client,
settings: &'a Settings,
imgs: &'a Imgs,
rot_imgs: &'a ImgsRot,
fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
pulse: f32,
global_state: &'a GlobalState,
buffs: &'a Buffs,
tooltip_manager: &'a mut TooltipManager,
#[conrod(common_builder)]
common: widget::CommonBuilder,
@ -79,20 +89,26 @@ impl<'a> Group<'a> {
client: &'a Client,
settings: &'a Settings,
imgs: &'a Imgs,
rot_imgs: &'a ImgsRot,
fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
pulse: f32,
global_state: &'a GlobalState,
buffs: &'a Buffs,
tooltip_manager: &'a mut TooltipManager,
) -> Self {
Self {
show,
client,
settings,
imgs,
rot_imgs,
fonts,
localized_strings,
pulse,
global_state,
buffs,
tooltip_manager,
common: widget::CommonBuilder::default(),
}
}
@ -127,8 +143,27 @@ impl<'a> Widget for Group<'a> {
#[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
let mut events = Vec::new();
let localized_strings = self.localized_strings;
//let buffs = self.buffs;
let buff_ani = ((self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8) + 0.5; //Animation timer
let buffs_tooltip = Tooltip::new({
// Edge images [t, b, r, l]
// Corner images [tr, tl, br, bl]
let edge = &self.rot_imgs.tt_side;
let corner = &self.rot_imgs.tt_corner;
ImageFrame::new(
[edge.cw180, edge.none, edge.cw270, edge.cw90],
[corner.none, corner.cw270, corner.cw90, corner.cw180],
Color::Rgba(0.08, 0.07, 0.04, 1.0),
5.0,
)
})
.title_font_size(self.fonts.cyri.scale(15))
.parent(ui.window)
.desc_font_size(self.fonts.cyri.scale(12))
.font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR);
// Don't show pets
let group_members = self
@ -293,6 +328,7 @@ impl<'a> Widget for Group<'a> {
let client_state = self.client.state();
let stats = client_state.ecs().read_storage::<common::comp::Stats>();
let energy = client_state.ecs().read_storage::<common::comp::Energy>();
let buffs = client_state.ecs().read_storage::<common::comp::Buffs>();
let uid_allocator = client_state
.ecs()
.read_resource::<common::sync::UidAllocator>();
@ -302,6 +338,8 @@ impl<'a> Widget for Group<'a> {
let entity = uid_allocator.retrieve_entity_internal(uid.into());
let stats = entity.and_then(|entity| stats.get(entity));
let energy = entity.and_then(|entity| energy.get(entity));
let buffs = entity.and_then(|entity| buffs.get(entity));
if let Some(stats) = stats {
let char_name = stats.name.to_string();
let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64;
@ -317,7 +355,7 @@ impl<'a> Widget for Group<'a> {
.top_left_with_margins_on(ui.window, offset, 20.0)
} else {
Image::new(self.imgs.member_bg)
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
.down_from(state.ids.member_panels_bg[i - 1], 45.0)
};
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
@ -386,19 +424,19 @@ impl<'a> Widget for Group<'a> {
.set(state.ids.member_panels_frame[i], ui);
// Panel Text
Text::new(&char_name)
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(BLACK)
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt_bg[i], ui);
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(BLACK)
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt_bg[i], ui);
Text::new(&char_name)
.bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(if is_leader { ERROR_COLOR } else { GROUP_COLOR })
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt[i], ui);
.bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(if is_leader { ERROR_COLOR } else { GROUP_COLOR })
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt[i], ui);
if let Some(energy) = energy {
let stam_perc = energy.current() as f64 / energy.maximum() as f64;
// Stamina
@ -408,44 +446,146 @@ impl<'a> Widget for Group<'a> {
.top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
.set(state.ids.member_stam[i], ui);
}
} else {
// Values N.A.
if let Some(stats) = stats {
if let Some(buffs) = buffs {
let mut buffs_vec = Vec::<BuffInfo>::new();
for buff in buffs.active_buffs.clone() {
let info = get_buff_info(buff);
buffs_vec.push(info);
}
state.update(|state| {
state.ids.buffs.resize(
state.ids.buffs.len() + buffs_vec.len(),
&mut ui.widget_id_generator(),
)
});
state.update(|state| {
state.ids.buff_timers.resize(
state.ids.buff_timers.len() + buffs_vec.len(),
&mut ui.widget_id_generator(),
)
});
// Create Buff Widgets
for (x, buff) in buffs_vec.iter().enumerate() {
if x < 11 {
// Limit displayed buffs
let max_duration = match buff.id {
BuffId::Regeneration { duration, .. } => {
duration.unwrap().as_secs_f32()
},
_ => 10.0,
};
let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
let current_duration = buff.dur;
let duration_percentage =
(current_duration / max_duration * 1000.0) as u32; // Percentage to determine which frame of the timer overlay is displayed
let buff_img = match buff.id {
BuffId::Regeneration { .. } => self.imgs.buff_plus_0,
BuffId::Bleeding { .. } => self.imgs.debuff_bleed_0,
BuffId::Cursed { .. } => self.imgs.debuff_skull_0,
};
let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
let buff_widget = if x == 0 {
buff_widget.bottom_left_with_margins_on(
state.ids.member_panels_frame[i],
-21.0,
1.0,
)
} else {
buff_widget.right_from(state.ids.buffs[state.ids.buffs.len() - buffs_vec.len() + x - 1/*x - 1*/], 1.0)
};
buff_widget
.color(if current_duration < 10.0 {
Some(pulsating_col)
} else {
Some(norm_col)
})
.set(state.ids.buffs[state.ids.buffs.len() - buffs_vec.len() + x/*x*/], ui);
// Create Buff tooltip
let title = match buff.id {
BuffId::Regeneration { .. } => {
*&localized_strings.get("buff.title.heal_test")
},
BuffId::Bleeding { .. } => {
*&localized_strings.get("debuff.title.bleed_test")
},
_ => *&localized_strings.get("buff.title.missing"),
};
let remaining_time = if current_duration == 10e6 as f32 {
"Permanent".to_string()
} else {
format!("Remaining: {:.0}s", current_duration)
};
let desc_txt = match buff.id {
BuffId::Regeneration { .. } => {
*&localized_strings.get("buff.desc.heal_test")
},
BuffId::Bleeding { .. } => {
*&localized_strings.get("debuff.desc.bleed_test")
},
_ => *&localized_strings.get("buff.desc.missing"),
};
let desc = format!("{}\n\n{}", desc_txt, remaining_time);
Image::new(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, // 3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(20.0, 20.0)
.middle_of(state.ids.buffs[state.ids.buffs.len() - buffs_vec.len() + x/*x*/])
.with_tooltip(
self.tooltip_manager,
title,
&desc,
&buffs_tooltip,
if buff.is_buff {BUFF_COLOR} else {DEBUFF_COLOR},
)
.set(state.ids.buff_timers[state.ids.buffs.len() - buffs_vec.len() + x/*x*/], ui);
};
}
} else {
// Values N.A.
Text::new(&stats.name.to_string())
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(GROUP_COLOR)
.set(state.ids.member_panels_txt[i], ui);
};
let offset = if self.global_state.settings.gameplay.toggle_debug {
210.0
} else {
110.0
};
let back = if i == 0 {
Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0)
} else {
Image::new(self.imgs.member_bg)
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
};
back.w_h(152.0, 36.0)
.color(Some(TEXT_COLOR))
.set(state.ids.member_panels_bg[i], ui);
// Panel Frame
Image::new(self.imgs.member_frame)
.w_h(152.0, 36.0)
.middle_of(state.ids.member_panels_bg[i])
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.member_panels_frame[i], ui);
// Panel Text
Text::new(&self.localized_strings.get("hud.group.out_of_range"))
.mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0)
.font_size(16)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.dead_txt[i], ui);
let offset = if self.global_state.settings.gameplay.toggle_debug {
210.0
} else {
110.0
};
let back = if i == 0 {
Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0)
} else {
Image::new(self.imgs.member_bg)
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
};
back.w_h(152.0, 36.0)
.color(Some(TEXT_COLOR))
.set(state.ids.member_panels_bg[i], ui);
// Panel Frame
Image::new(self.imgs.member_frame)
.w_h(152.0, 36.0)
.middle_of(state.ids.member_panels_bg[i])
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.member_panels_frame[i], ui);
// Panel Text
Text::new(&self.localized_strings.get("hud.group.out_of_range"))
.mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0)
.font_size(16)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.dead_txt[i], ui);
}
}
}

View File

@ -272,6 +272,7 @@ image_ids! {
hammerleap: "voxygen.element.icons.skill_hammerleap",
skill_axe_leap_slash: "voxygen.element.icons.skill_axe_leap_slash",
skill_bow_jump_burst: "voxygen.element.icons.skill_bow_jump_burst",
missing_icon: "voxygen.element.icons.missing_icon_grey",
// Buttons
button: "voxygen.element.buttons.button",
@ -350,10 +351,22 @@ image_ids! {
chat_world: "voxygen.element.icons.chat.world",
// Buffs
buff_plus_0: "voxygen.element.de_buffs.buff_plus_0",
buff_plus_0: "voxygen.element.icons.de_buffs.buff_plus_0",
// Debuffs
debuff_skull_0: "voxygen.element.de_buffs.debuff_skull_0",
debuff_skull_0: "voxygen.element.icons.de_buffs.debuff_skull_0",
debuff_bleed_0: "voxygen.element.icons.de_buffs.debuff_bleed_0",
// Animation Frames
// Buff Frame
buff_0: "voxygen.element.animation.buff_frame.1",
buff_1: "voxygen.element.animation.buff_frame.2",
buff_2: "voxygen.element.animation.buff_frame.3",
buff_3: "voxygen.element.animation.buff_frame.4",
buff_4: "voxygen.element.animation.buff_frame.5",
buff_5: "voxygen.element.animation.buff_frame.6",
buff_6: "voxygen.element.animation.buff_frame.7",
buff_7: "voxygen.element.animation.buff_frame.8",
<BlankGraphic>
nothing: (),

View File

@ -105,7 +105,7 @@ impl<'a> Widget for MiniMap<'a> {
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
let zoom = state.zoom;
const SCALE: f64 = 1.5;
const SCALE: f64 = 1.5; // TODO Make this a setting
if self.show.mini_map {
Image::new(self.imgs.mmap_frame)
.w_h(174.0 * SCALE, 190.0 * SCALE)

View File

@ -60,7 +60,10 @@ use client::Client;
use common::{
assets::Asset,
comp,
comp::item::{ItemDesc, Quality},
comp::{
item::{ItemDesc, Quality},
BuffId,
},
span,
sync::Uid,
terrain::TerrainChunk,
@ -97,6 +100,8 @@ const STAMINA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
//const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0);
//const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0);
//const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0);
const BUFF_COLOR: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0);
const DEBUFF_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
// Item Quality Colors
const QUALITY_LOW: Color = Color::Rgba(0.41, 0.41, 0.41, 1.0); // Grey - Trash, can be sold to vendors
@ -267,6 +272,13 @@ widget_ids! {
}
}
#[derive(Clone, Copy)]
pub struct BuffInfo {
id: comp::BuffId,
is_buff: bool,
dur: f32,
}
pub struct DebugInfo {
pub tps: f64,
pub frame_time: Duration,
@ -318,6 +330,7 @@ pub enum Event {
ChatTransp(f32),
ChatCharName(bool),
CrosshairType(CrosshairType),
BuffPosition(BuffPosition),
ToggleXpBar(XpBar),
Intro(Intro),
ToggleBarNumbers(BarNumbers),
@ -351,6 +364,7 @@ pub enum Event {
KickMember(common::sync::Uid),
LeaveGroup,
AssignLeader(common::sync::Uid),
RemoveBuff(BuffId),
}
// TODO: Are these the possible layouts we want?
@ -391,6 +405,13 @@ pub enum ShortcutNumbers {
On,
Off,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum BuffPosition {
Bar,
Map,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum PressBehavior {
Toggle = 0,
@ -725,6 +746,7 @@ impl Hud {
let ecs = client.state().ecs();
let pos = ecs.read_storage::<comp::Pos>();
let stats = ecs.read_storage::<comp::Stats>();
let buffs = ecs.read_storage::<comp::Buffs>();
let energy = ecs.read_storage::<comp::Energy>();
let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
let uids = ecs.read_storage::<common::sync::Uid>();
@ -1123,11 +1145,12 @@ impl Hud {
let speech_bubbles = &self.speech_bubbles;
// Render overhead name tags and health bars
for (pos, info, bubble, stats, height_offset, hpfl, in_group) in (
for (pos, info, bubble, stats, buffs, height_offset, hpfl, in_group) in (
&entities,
&pos,
interpolated.maybe(),
&stats,
&buffs,
energy.maybe(),
scales.maybe(),
&bodies,
@ -1141,7 +1164,7 @@ impl Hud {
entity != me && !stats.is_dead
})
.filter_map(
|(entity, pos, interpolated, stats, energy, scale, body, hpfl, uid)| {
|(entity, pos, interpolated, stats, buffs, energy, scale, body, hpfl, uid)| {
// Use interpolated position if available
let pos = interpolated.map_or(pos.0, |i| i.pos);
let in_group = client.group_members().contains_key(uid);
@ -1171,6 +1194,7 @@ impl Hud {
let info = display_overhead_info.then(|| overhead::Info {
name: &stats.name,
stats,
buffs,
energy,
});
let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) {
@ -1185,6 +1209,7 @@ impl Hud {
info,
bubble,
stats,
buffs,
body.height() * scale.map_or(1.0, |s| s.0) + 0.5,
hpfl,
in_group,
@ -1760,22 +1785,48 @@ impl Hud {
// Buffs and Debuffs
if let Some(player_buffs) = buffs.get(client.entity()) {
match BuffsBar::new(
client,
for event in BuffsBar::new(
&self.imgs,
&self.fonts,
global_state,
&self.rot_imgs,
tooltip_manager,
&self.voxygen_i18n,
&player_buffs,
self.pulse,
&global_state,
)
.set(self.ids.buffs, ui_widgets)
{
_ => {},
match event {
buffs::Event::RemoveBuff(buff_id) => events.push(Event::RemoveBuff(buff_id)),
}
}
}
// Group Window
let buffs = buffs.get(client.entity()).unwrap();
for event in Group::new(
&mut self.show,
client,
&global_state.settings,
&self.imgs,
&self.rot_imgs,
&self.fonts,
&self.voxygen_i18n,
self.pulse,
&global_state,
&buffs,
tooltip_manager,
)
.set(self.ids.group_window, ui_widgets)
{
match event {
group::Event::Accept => events.push(Event::AcceptInvite),
group::Event::Decline => events.push(Event::DeclineInvite),
group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
group::Event::LeaveGroup => events.push(Event::LeaveGroup),
group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
}
}
// Popup (waypoint saved and similar notifications)
Popup::new(
&self.voxygen_i18n,
@ -1850,8 +1901,8 @@ impl Hud {
Some(stats),
Some(loadout),
Some(energy),
Some(character_state),
Some(controller),
Some(_character_state),
Some(_controller),
Some(inventory),
) = (
stats.get(entity),
@ -2018,6 +2069,9 @@ impl Hud {
settings_window::Event::ToggleZoomInvert(zoom_inverted) => {
events.push(Event::ToggleZoomInvert(zoom_inverted));
},
settings_window::Event::BuffPosition(buff_position) => {
events.push(Event::BuffPosition(buff_position));
},
settings_window::Event::ToggleMouseYInvert(mouse_y_inverted) => {
events.push(Event::ToggleMouseYInvert(mouse_y_inverted));
},
@ -2142,27 +2196,6 @@ impl Hud {
}
}
}
// Group Window
for event in Group::new(
&mut self.show,
client,
&global_state.settings,
&self.imgs,
&self.fonts,
&self.voxygen_i18n,
self.pulse,
&global_state,
)
.set(self.ids.group_window, ui_widgets)
{
match event {
group::Event::Accept => events.push(Event::AcceptInvite),
group::Event::Decline => events.push(Event::DeclineInvite),
group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
group::Event::LeaveGroup => events.push(Event::LeaveGroup),
group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
}
}
// Spellbook
if self.show.spell {
@ -2694,3 +2727,17 @@ pub fn get_quality_col<I: ItemDesc>(item: &I) -> Color {
Quality::Debug => QUALITY_DEBUG,
}
}
// Get info about applied buffs
fn get_buff_info(buff: comp::Buff) -> BuffInfo {
BuffInfo {
id: buff.id,
is_buff: buff
.cat_ids
.iter()
.any(|cat| *cat == comp::BuffCategoryId::Buff),
dur: buff
.time
.map(|dur| dur.as_secs_f32())
.unwrap_or(10e6 as f32),
}
}

View File

@ -3,16 +3,19 @@ use super::{
REGION_COLOR, SAY_COLOR, STAMINA_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
};
use crate::{
hud::{get_buff_info, BuffInfo},
i18n::VoxygenLocalization,
settings::GameplaySettings,
ui::{fonts::ConrodVoxygenFonts, Ingameable},
};
use common::comp::{Energy, SpeechBubble, SpeechBubbleType, Stats};
use common::comp::{BuffId, Buffs, Energy, SpeechBubble, SpeechBubbleType, Stats};
use conrod_core::{
color,
position::Align,
widget::{self, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
use inline_tweak::*;
const MAX_BUBBLE_WIDTH: f64 = 250.0;
widget_ids! {
@ -44,13 +47,24 @@ widget_ids! {
health_txt,
mana_bar,
health_bar_fg,
// Buffs
buffs_align,
buffs[],
buff_timers[],
}
}
/*pub struct BuffInfo {
id: comp::BuffId,
dur: f32,
}*/
#[derive(Clone, Copy)]
pub struct Info<'a> {
pub name: &'a str,
pub stats: &'a Stats,
pub buffs: &'a Buffs,
pub energy: Option<&'a Energy>,
}
@ -119,17 +133,21 @@ impl<'a> Ingameable for Overhead<'a> {
// - 1 for HP text
// - If there's mana
// - 1 Rect::new for mana
//
// If there are Buffs
// - 1 Alignment Rectangle
// - 10 + 10 Buffs and Timer Overlays
// If there's a speech bubble
// - 2 Text::new for speech bubble
// - 1 Image::new for icon
// - 10 Image::new for speech bubble (9-slice + tail)
self.info.map_or(0, |info| {
2 + if show_healthbar(info.stats) {
5 + if info.energy.is_some() { 1 } else { 0 }
} else {
0
}
2 + 1
+ info.buffs.active_buffs.len().min(10) * 2
+ if show_healthbar(info.stats) {
5 + if info.energy.is_some() { 1 } else { 0 }
} else {
0
}
}) + if self.bubble.is_some() { 13 } else { 0 }
}
}
@ -155,6 +173,7 @@ impl<'a> Widget for Overhead<'a> {
if let Some(Info {
name,
stats,
buffs,
energy,
}) = self.info
{
@ -172,6 +191,11 @@ impl<'a> Widget for Overhead<'a> {
} else {
MANA_BAR_Y + 32.0
};
let mut buffs_vec = Vec::<BuffInfo>::new();
for buff in buffs.active_buffs.clone() {
let info = get_buff_info(buff);
buffs_vec.push(info);
}
let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 };
// Show K for numbers above 10^3 and truncate them
// Show M for numbers above 10^6 and truncate them
@ -185,6 +209,79 @@ impl<'a> Widget for Overhead<'a> {
1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)),
_ => format!("{:.0}M", (health_max as f64 / 1.0e6).max(1.0)),
};
// Buffs
// Alignment
Rectangle::fill_with([tweak!(168.0), tweak!(100.0)], color::TRANSPARENT)
.x_y(-1.0, name_y + tweak!(60.0))
.parent(id)
.set(state.ids.buffs_align, ui);
if state.ids.buffs.len() < buffs_vec.len() {
state.update(|state| {
state
.ids
.buffs
.resize(buffs_vec.len(), &mut ui.widget_id_generator())
});
};
if state.ids.buff_timers.len() < buffs_vec.len() {
state.update(|state| {
state
.ids
.buff_timers
.resize(buffs_vec.len(), &mut ui.widget_id_generator())
});
};
let buff_ani = ((self.pulse * 4.0).cos() * 0.5 + 0.8) + 0.5; //Animation timer
let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
// Create Buff Widgets
for (i, buff) in buffs_vec.iter().enumerate() {
if i < 11 && self.bubble.is_none() {
// Limit displayed buffs
let max_duration = match buff.id {
BuffId::Regeneration { duration, .. } => duration.unwrap().as_secs_f32(),
_ => 10.0,
};
let current_duration = buff.dur;
let duration_percentage = (current_duration / max_duration * 1000.0) as u32; // Percentage to determine which frame of the timer overlay is displayed
let buff_img = match buff.id {
BuffId::Regeneration { .. } => self.imgs.buff_plus_0,
BuffId::Bleeding { .. } => self.imgs.debuff_bleed_0,
BuffId::Cursed { .. } => self.imgs.debuff_skull_0,
};
let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
// Sort buffs into rows of 5 slots
let x = i % 5;
let y = i / 5;
let buff_widget = buff_widget.bottom_left_with_margins_on(
state.ids.buffs_align,
0.0 + y as f64 * (21.0),
0.0 + x as f64 * (21.0),
);
buff_widget
.color(if current_duration < 10.0 {
Some(pulsating_col)
} else {
Some(norm_col)
})
.set(state.ids.buffs[i], ui);
Image::new(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, //3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(20.0, 20.0)
.middle_of(state.ids.buffs[i])
.set(state.ids.buff_timers[i], ui);
};
}
// Name
Text::new(name)
.font_id(self.fonts.cyri.conrod_id)

View File

@ -4,6 +4,7 @@ use super::{
TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
};
use crate::{
hud::BuffPosition,
i18n::{list_localizations, LanguageMetadata, VoxygenLocalization},
render::{AaMode, CloudMode, FluidMode, LightingMode, RenderMode, ShadowMapMode, ShadowMode},
ui::{fonts::ConrodVoxygenFonts, ImageSlider, ScaleMode, ToggleButton},
@ -159,6 +160,7 @@ widget_ids! {
sfx_volume_text,
audio_device_list,
audio_device_text,
//
hotbar_title,
bar_numbers_title,
show_bar_numbers_none_button,
@ -167,18 +169,20 @@ widget_ids! {
show_bar_numbers_values_text,
show_bar_numbers_percentage_button,
show_bar_numbers_percentage_text,
//
show_shortcuts_button,
show_shortcuts_text,
show_xpbar_button,
show_xpbar_text,
show_bars_button,
show_bars_text,
placeholder,
buff_pos_bar_button,
buff_pos_bar_text,
buff_pos_map_button,
buff_pos_map_text,
//
chat_transp_title,
chat_transp_text,
chat_transp_slider,
chat_char_name_text,
chat_char_name_button,
//
sct_title,
sct_show_text,
sct_show_radio,
@ -195,6 +199,7 @@ widget_ids! {
sct_num_dur_text,
sct_num_dur_slider,
sct_num_dur_value,
//
speech_bubble_text,
speech_bubble_dark_mode_text,
speech_bubble_dark_mode_button,
@ -261,6 +266,7 @@ pub enum Event {
ToggleTips(bool),
ToggleBarNumbers(BarNumbers),
ToggleShortcutNumbers(ShortcutNumbers),
BuffPosition(BuffPosition),
ChangeTab(SettingsTab),
Close,
AdjustMousePan(u32),
@ -829,11 +835,61 @@ impl<'a> Widget for SettingsWindow<'a> {
.graphics_for(state.ids.show_shortcuts_button)
.color(TEXT_COLOR)
.set(state.ids.show_shortcuts_text, ui);
Rectangle::fill_with([60.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT)
.down_from(state.ids.show_shortcuts_text, 30.0)
.set(state.ids.placeholder, ui);
// Buff Position
// Buffs above skills
if Button::image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Bar => self.imgs.checkbox_checked,
BuffPosition::Map => self.imgs.checkbox,
})
.w_h(18.0, 18.0)
.hover_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Bar => self.imgs.checkbox_checked_mo,
BuffPosition::Map => self.imgs.checkbox_mo,
})
.press_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Bar => self.imgs.checkbox_checked,
BuffPosition::Map => self.imgs.checkbox_press,
})
.down_from(state.ids.show_shortcuts_button, 8.0)
.set(state.ids.buff_pos_bar_button, ui)
.was_clicked()
{
events.push(Event::BuffPosition(BuffPosition::Bar))
}
Text::new(&self.localized_strings.get("hud.settings.buffs_skillbar"))
.right_from(state.ids.buff_pos_bar_button, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.show_shortcuts_button)
.color(TEXT_COLOR)
.set(state.ids.buff_pos_bar_text, ui);
// Buffs left from minimap
if Button::image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Map => self.imgs.checkbox_checked,
BuffPosition::Bar => self.imgs.checkbox,
})
.w_h(18.0, 18.0)
.hover_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Map => self.imgs.checkbox_checked_mo,
BuffPosition::Bar => self.imgs.checkbox_mo,
})
.press_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Map => self.imgs.checkbox_checked,
BuffPosition::Bar => self.imgs.checkbox_press,
})
.down_from(state.ids.buff_pos_bar_button, 8.0)
.set(state.ids.buff_pos_map_button, ui)
.was_clicked()
{
events.push(Event::BuffPosition(BuffPosition::Map))
}
Text::new(&self.localized_strings.get("hud.settings.buffs_mmap"))
.right_from(state.ids.buff_pos_map_button, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.show_shortcuts_button)
.color(TEXT_COLOR)
.set(state.ids.buff_pos_map_text, ui);
// Content Right Side
/*Scrolling Combat text

View File

@ -894,6 +894,10 @@ impl PlayState for SessionState {
global_state.settings.gameplay.shortcut_numbers = shortcut_numbers;
global_state.settings.save_to_file_warn();
},
HudEvent::BuffPosition(buff_position) => {
global_state.settings.gameplay.buff_position = buff_position;
global_state.settings.save_to_file_warn();
},
HudEvent::UiScale(scale_change) => {
global_state.settings.gameplay.ui_scale =
self.hud.scale_change(scale_change);
@ -921,6 +925,10 @@ impl PlayState for SessionState {
global_state.settings.graphics.max_fps = fps;
global_state.settings.save_to_file_warn();
},
HudEvent::RemoveBuff(buff_id) => {
let mut client = self.client.borrow_mut();
client.remove_buff(buff_id);
},
HudEvent::UseSlot(x) => self.client.borrow_mut().use_slot(x),
HudEvent::SwapSlots(a, b) => self.client.borrow_mut().swap_slots(a, b),
HudEvent::DropSlot(x) => {

View File

@ -1,5 +1,5 @@
use crate::{
hud::{BarNumbers, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar},
hud::{BarNumbers, BuffPosition, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar},
i18n,
render::RenderMode,
ui::ScaleMode,
@ -507,6 +507,7 @@ pub struct GameplaySettings {
pub intro_show: Intro,
pub xp_bar: XpBar,
pub shortcut_numbers: ShortcutNumbers,
pub buff_position: BuffPosition,
pub bar_numbers: BarNumbers,
pub ui_scale: ScaleMode,
pub free_look_behavior: PressBehavior,
@ -537,6 +538,7 @@ impl Default for GameplaySettings {
intro_show: Intro::Show,
xp_bar: XpBar::Always,
shortcut_numbers: ShortcutNumbers::On,
buff_position: BuffPosition::Bar,
bar_numbers: BarNumbers::Values,
ui_scale: ScaleMode::RelativeToWindow([1920.0, 1080.0].into()),
free_look_behavior: PressBehavior::Toggle,