diff --git a/assets/common/items/food/apple_stick.ron b/assets/common/items/food/apple_stick.ron index 5bf9454978..7d5071e5f7 100644 --- a/assets/common/items/food/apple_stick.ron +++ b/assets/common/items/food/apple_stick.ron @@ -1,6 +1,6 @@ ItemDef( name: "Apple Stick", - description: "Restores 20 Health", + description: "Restores 25 Health", kind: Consumable( kind: "AppleStick", effect: Health(( diff --git a/assets/voxygen/element/animation/buff_frame/1.png b/assets/voxygen/element/animation/buff_frame/1.png new file mode 100644 index 0000000000..e05166cd46 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/1.png differ diff --git a/assets/voxygen/element/animation/buff_frame/2.png b/assets/voxygen/element/animation/buff_frame/2.png new file mode 100644 index 0000000000..54c5183ec1 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/2.png differ diff --git a/assets/voxygen/element/animation/buff_frame/3.png b/assets/voxygen/element/animation/buff_frame/3.png new file mode 100644 index 0000000000..b0a196aeac Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/3.png differ diff --git a/assets/voxygen/element/animation/buff_frame/4.png b/assets/voxygen/element/animation/buff_frame/4.png new file mode 100644 index 0000000000..beceaf397b Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/4.png differ diff --git a/assets/voxygen/element/animation/buff_frame/5.png b/assets/voxygen/element/animation/buff_frame/5.png new file mode 100644 index 0000000000..4cb8dc04c1 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/5.png differ diff --git a/assets/voxygen/element/animation/buff_frame/6.png b/assets/voxygen/element/animation/buff_frame/6.png new file mode 100644 index 0000000000..4bc7af2a36 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/6.png differ diff --git a/assets/voxygen/element/animation/buff_frame/7.png b/assets/voxygen/element/animation/buff_frame/7.png new file mode 100644 index 0000000000..80dcb85b44 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/7.png differ diff --git a/assets/voxygen/element/animation/buff_frame/8.png b/assets/voxygen/element/animation/buff_frame/8.png new file mode 100644 index 0000000000..e158dfbb97 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/8.png differ diff --git a/assets/voxygen/element/de_buffs/buff_plus_0.png b/assets/voxygen/element/icons/de_buffs/buff_plus_0.png similarity index 100% rename from assets/voxygen/element/de_buffs/buff_plus_0.png rename to assets/voxygen/element/icons/de_buffs/buff_plus_0.png diff --git a/assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png b/assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png new file mode 100644 index 0000000000..4638eeb618 Binary files /dev/null and b/assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png differ diff --git a/assets/voxygen/element/de_buffs/debuff_skull_0.png b/assets/voxygen/element/icons/de_buffs/debuff_skull_0.png similarity index 100% rename from assets/voxygen/element/de_buffs/debuff_skull_0.png rename to assets/voxygen/element/icons/de_buffs/debuff_skull_0.png diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 55355503b8..c85ffe8d27 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -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.", }, diff --git a/client/src/lib.rs b/client/src/lib.rs index ad47263340..9342123523 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -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)> { diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs index ca1d690002..828cf39d96 100644 --- a/common/src/comp/buff.rs +++ b/common/src/comp/buff.rs @@ -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 } => ( diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index b9c7e1cb4c..64cdf93505 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -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, } diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 06d39cb1c8..63f263a765 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -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(); diff --git a/common/src/sys/buff.rs b/common/src/sys/buff.rs index e509d5114b..a959ef3729 100644 --- a/common/src/sys/buff.rs +++ b/common/src/sys/buff.rs @@ -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 { diff --git a/common/src/sys/combat.rs b/common/src/sys/combat.rs index 6e46d94d6d..7eb6cd53a7 100644 --- a/common/src/sys/combat.rs +++ b/common/src/sys/combat.rs @@ -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 { diff --git a/common/src/sys/controller.rs b/common/src/sys/controller.rs index 380b176adf..03fbf46c6e 100644 --- a/common/src/sys/controller.rs +++ b/common/src/sys/controller.rs @@ -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)) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d0ba6ee4d6..8d7e1b106e 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -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 } diff --git a/voxygen/src/hud/buffs.rs b/voxygen/src/hud/buffs.rs index ad32915443..15a6552399 100644 --- a/voxygen/src/hud/buffs.rs +++ b/voxygen/src/hud/buffs.rs @@ -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 } } diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 26b853cd08..06503bc9d8 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -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); + } } } diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 3aced27a57..fbb48033c0 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -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: (), diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 1bdeef1395..ca993285cb 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -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) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index bd3d0575c4..1576fb3ff1 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -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), + } +} diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index e24e4eb853..8ee435ea1f 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -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) diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 9bf18b9473..b2975c7d52 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -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 diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 9d752aeed3..d0b3d3a8c9 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -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) => { diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index be4eed60db..8b4396a094 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -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,