diff --git a/assets/voxygen/element/frames/bubble/bottom.png b/assets/voxygen/element/frames/bubble/bottom.png new file mode 100644 index 0000000000..a133651d2a --- /dev/null +++ b/assets/voxygen/element/frames/bubble/bottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b2389dc62c0765c9ed56e53afab639c7fcb90656d83a051e8cf513cda926804 +size 136 diff --git a/assets/voxygen/element/frames/bubble/bottom_left.png b/assets/voxygen/element/frames/bubble/bottom_left.png new file mode 100644 index 0000000000..514e7f025d --- /dev/null +++ b/assets/voxygen/element/frames/bubble/bottom_left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2983324f979fe0ee212e7b4527d4d2ae042ec7da418cee01cdd0637d9410b042 +size 3485 diff --git a/assets/voxygen/element/frames/bubble/bottom_right.png b/assets/voxygen/element/frames/bubble/bottom_right.png new file mode 100644 index 0000000000..418b586b89 --- /dev/null +++ b/assets/voxygen/element/frames/bubble/bottom_right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7681e4b43db2d68f8576b26e91c8395f292b9aab76b0fac27ef2f5239e438766 +size 3639 diff --git a/assets/voxygen/element/frames/bubble/left.png b/assets/voxygen/element/frames/bubble/left.png new file mode 100644 index 0000000000..533725330d --- /dev/null +++ b/assets/voxygen/element/frames/bubble/left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6da4e97492532b7c8b83119bd419c5689c00149e217c26dcff6e9628e0eef6de +size 124 diff --git a/assets/voxygen/element/frames/bubble/mid.png b/assets/voxygen/element/frames/bubble/mid.png new file mode 100644 index 0000000000..f00c3ea319 --- /dev/null +++ b/assets/voxygen/element/frames/bubble/mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4adcd985ae2c5fb0f3f9c32995c1da886f91503ca238052e4eec87fd51831efe +size 109 diff --git a/assets/voxygen/element/frames/bubble/right.png b/assets/voxygen/element/frames/bubble/right.png new file mode 100644 index 0000000000..210af16f17 --- /dev/null +++ b/assets/voxygen/element/frames/bubble/right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b17fa4cacc5617de542e8ea054113dba1e8aa19272567f4a8cf026d1e849d0d0 +size 125 diff --git a/assets/voxygen/element/frames/bubble/tail.png b/assets/voxygen/element/frames/bubble/tail.png new file mode 100644 index 0000000000..19889b332f --- /dev/null +++ b/assets/voxygen/element/frames/bubble/tail.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16f8d6165a5071be33830a33215bbcd5fc4a7783a932bff396df98167e5241bb +size 227 diff --git a/assets/voxygen/element/frames/bubble/top.png b/assets/voxygen/element/frames/bubble/top.png new file mode 100644 index 0000000000..0c88a60f54 --- /dev/null +++ b/assets/voxygen/element/frames/bubble/top.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14518edba8bc71829422acd9c3e0c76dc9c78678ed1acc87e81257667eb46a79 +size 136 diff --git a/assets/voxygen/element/frames/bubble/top_left.png b/assets/voxygen/element/frames/bubble/top_left.png new file mode 100644 index 0000000000..e1b770cec8 --- /dev/null +++ b/assets/voxygen/element/frames/bubble/top_left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4fd9c69f48c863371cef8ed8ea332710b975583b6ba7f0fa91359042a471d32 +size 184 diff --git a/assets/voxygen/element/frames/bubble/top_right.png b/assets/voxygen/element/frames/bubble/top_right.png new file mode 100644 index 0000000000..d994a4198c --- /dev/null +++ b/assets/voxygen/element/frames/bubble/top_right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8127cb08af479b2e08f3a4decfd5640c97095d7333bf7cd40a4a3e61c8cf09ae +size 219 diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 11f8fcc76b..bec188275e 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -272,6 +272,18 @@ image_ids! { progress_frame: "voxygen.element.frames.progress_bar", progress: "voxygen.element.misc_bg.progress", + // Chat bubbles + chat_bubble_top_left: "voxygen.element.frames.bubble.top_left", + chat_bubble_top: "voxygen.element.frames.bubble.top", + chat_bubble_top_right: "voxygen.element.frames.bubble.top_right", + chat_bubble_left: "voxygen.element.frames.bubble.left", + chat_bubble_mid: "voxygen.element.frames.bubble.mid", + chat_bubble_right: "voxygen.element.frames.bubble.right", + chat_bubble_bottom_left: "voxygen.element.frames.bubble.bottom_left", + chat_bubble_bottom: "voxygen.element.frames.bubble.bottom", + chat_bubble_bottom_right: "voxygen.element.frames.bubble.bottom_right", + chat_bubble_tail: "voxygen.element.frames.bubble.tail", + nothing: (), } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index c5a1e2878b..f4743ec34f 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -7,6 +7,7 @@ mod img_ids; mod item_imgs; mod map; mod minimap; +mod overhead; mod popup; mod settings_window; mod skillbar; @@ -102,17 +103,6 @@ widget_ids! { crosshair_inner, crosshair_outer, - // Character Names - name_tags[], - name_tags_bgs[], - levels[], - levels_skull[], - // Health Bars - health_bars[], - mana_bars[], - health_bar_fronts[], - health_bar_backs[], - // SCT player_scts[], player_sct_bgs[], @@ -125,6 +115,8 @@ widget_ids! { sct_bgs[], scts[], + overheads[], + // Intro Text intro_bg, intro_text, @@ -643,13 +635,9 @@ impl Hud { } } - // Nametags and healthbars - // Max amount the sct font size increases when "flashing" const FLASH_MAX: f32 = 25.0; - const BARSIZE: f64 = 2.0; - const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5; - const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0; + // Get player position. let player_pos = client .state() @@ -657,265 +645,6 @@ impl Hud { .read_storage::() .get(client.entity()) .map_or(Vec3::zero(), |pos| pos.0); - let mut name_id_walker = self.ids.name_tags.walk(); - let mut name_id_bg_walker = self.ids.name_tags_bgs.walk(); - let mut level_id_walker = self.ids.levels.walk(); - let mut level_skull_id_walker = self.ids.levels_skull.walk(); - let mut health_id_walker = self.ids.health_bars.walk(); - let mut mana_id_walker = self.ids.mana_bars.walk(); - let mut health_back_id_walker = self.ids.health_bar_backs.walk(); - let mut health_front_id_walker = self.ids.health_bar_fronts.walk(); - let mut sct_bg_id_walker = self.ids.sct_bgs.walk(); - let mut sct_id_walker = self.ids.scts.walk(); - - // Render Health Bars - for (pos, stats, energy, height_offset, hp_floater_list) in ( - &entities, - &pos, - interpolated.maybe(), - &stats, - &energy, - scales.maybe(), - &bodies, - &hp_floater_lists, - ) - .join() - .filter(|(entity, _, _, stats, _, _, _, _)| { - *entity != me && !stats.is_dead - //&& stats.health.current() != stats.health.maximum() - }) - // Don't show outside a certain range - .filter(|(_, pos, _, _, _, _, _, hpfl)| { - pos.0.distance_squared(player_pos) - < (if hpfl - .time_since_last_dmg_by_me - .map_or(false, |t| t < NAMETAG_DMG_TIME) - { - NAMETAG_DMG_RANGE - } else { - NAMETAG_RANGE - }) - .powi(2) - }) - .map(|(_, pos, interpolated, stats, energy, scale, body, f)| { - ( - interpolated.map_or(pos.0, |i| i.pos), - stats, - energy, - // TODO: when body.height() is more accurate remove the 2.0 - body.height() * 2.0 * scale.map_or(1.0, |s| s.0), - f, - ) - }) - { - let back_id = health_back_id_walker.next( - &mut self.ids.health_bar_backs, - &mut ui_widgets.widget_id_generator(), - ); - let health_bar_id = health_id_walker.next( - &mut self.ids.health_bars, - &mut ui_widgets.widget_id_generator(), - ); - let mana_bar_id = mana_id_walker.next( - &mut self.ids.mana_bars, - &mut ui_widgets.widget_id_generator(), - ); - let front_id = health_front_id_walker.next( - &mut self.ids.health_bar_fronts, - &mut ui_widgets.widget_id_generator(), - ); - let hp_percentage = - stats.health.current() as f64 / stats.health.maximum() as f64 * 100.0; - let energy_percentage = energy.current() as f64 / energy.maximum() as f64 * 100.0; - let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer - let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); - - let ingame_pos = pos + Vec3::unit_z() * height_offset; - - // Background - Image::new(self.imgs.enemy_health_bg) - .w_h(84.0 * BARSIZE, 10.0 * BARSIZE) - .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5) - .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8))) - .position_ingame(ingame_pos) - .set(back_id, ui_widgets); - - // % HP Filling - Image::new(self.imgs.enemy_bar) - .w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE) - .x_y( - (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE, - MANA_BAR_Y + 7.5, - ) - .color(Some(if hp_percentage <= 25.0 { - crit_hp_color - } else if hp_percentage <= 50.0 { - LOW_HP_COLOR - } else { - HP_COLOR - })) - .position_ingame(ingame_pos) - .set(health_bar_id, ui_widgets); - // % Mana Filling - Rectangle::fill_with( - [ - 72.0 * (energy.current() as f64 / energy.maximum() as f64) * BARSIZE, - MANA_BAR_HEIGHT, - ], - MANA_COLOR, - ) - .x_y( - ((3.5 + (energy_percentage / 100.0 * 36.5)) - 36.45) * BARSIZE, - MANA_BAR_Y, //-32.0, - ) - .position_ingame(ingame_pos) - .set(mana_bar_id, ui_widgets); - - // Foreground - Image::new(self.imgs.enemy_health) - .w_h(84.0 * BARSIZE, 10.0 * BARSIZE) - .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99))) - .position_ingame(ingame_pos) - .set(front_id, ui_widgets); - - // Enemy SCT - if let Some(floaters) = Some(hp_floater_list) - .filter(|fl| !fl.floaters.is_empty() && global_state.settings.gameplay.sct) - .map(|l| &l.floaters) - { - // Colors - const WHITE: Rgb = Rgb::new(1.0, 0.9, 0.8); - const LIGHT_OR: Rgb = Rgb::new(1.0, 0.925, 0.749); - const LIGHT_MED_OR: Rgb = Rgb::new(1.0, 0.85, 0.498); - const MED_OR: Rgb = Rgb::new(1.0, 0.776, 0.247); - const DARK_ORANGE: Rgb = Rgb::new(1.0, 0.7, 0.0); - const RED_ORANGE: Rgb = Rgb::new(1.0, 0.349, 0.0); - const DAMAGE_COLORS: [Rgb; 6] = [ - WHITE, - LIGHT_OR, - LIGHT_MED_OR, - MED_OR, - DARK_ORANGE, - RED_ORANGE, - ]; - // Largest value that select the first color is 40, then it shifts colors - // every 5 - let font_col = |font_size: u32| { - DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize] - }; - - if global_state.settings.gameplay.sct_damage_batch { - let number_speed = 50.0; // Damage number speed - let sct_bg_id = sct_bg_id_walker - .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator()); - let sct_id = sct_id_walker - .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator()); - // Calculate total change - // Ignores healing - let hp_damage = floaters.iter().fold(0, |acc, f| { - if f.hp_change < 0 { - acc + f.hp_change - } else { - acc - } - }); - let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32; - let timer = floaters - .last() - .expect("There must be at least one floater") - .timer; - // Increase font size based on fraction of maximum health - // "flashes" by having a larger size in the first 100ms - let font_size = 30 - + (max_hp_frac * 30.0) as u32 - + if timer < 0.1 { - (FLASH_MAX * (1.0 - timer / 0.1)) as u32 - } else { - 0 - }; - let font_col = font_col(font_size); - // Timer sets the widget offset - let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64 - * number_speed) - + 100.0; - // Timer sets text transparency - let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2; - - Text::new(&format!("{}", (hp_damage).abs())) - .font_size(font_size) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(0.0, 0.0, 0.0, fade)) - .x_y(0.0, y - 3.0) - .position_ingame(ingame_pos) - .set(sct_bg_id, ui_widgets); - Text::new(&format!("{}", hp_damage.abs())) - .font_size(font_size) - .font_id(self.fonts.cyri.conrod_id) - .x_y(0.0, y) - .color(if hp_damage < 0 { - Color::Rgba(font_col.r, font_col.g, font_col.b, fade) - } else { - Color::Rgba(0.1, 1.0, 0.1, fade) - }) - .position_ingame(ingame_pos) - .set(sct_id, ui_widgets); - } else { - for floater in floaters { - let number_speed = 250.0; // Single Numbers Speed - let sct_bg_id = sct_bg_id_walker - .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator()); - let sct_id = sct_id_walker - .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator()); - // Calculate total change - let max_hp_frac = - floater.hp_change.abs() as f32 / stats.health.maximum() as f32; - // Increase font size based on fraction of maximum health - // "flashes" by having a larger size in the first 100ms - let font_size = 30 - + (max_hp_frac * 30.0) as u32 - + if floater.timer < 0.1 { - (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32 - } else { - 0 - }; - let font_col = font_col(font_size); - // Timer sets the widget offset - let y = (floater.timer as f64 - / crate::ecs::sys::floater::HP_SHOWTIME as f64 - * number_speed) - + 100.0; - // Timer sets text transparency - let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer) - * 0.25) - + 0.2; - - Text::new(&format!("{}", (floater.hp_change).abs())) - .font_size(font_size) - .font_id(self.fonts.cyri.conrod_id) - .color(if floater.hp_change < 0 { - Color::Rgba(0.0, 0.0, 0.0, fade) - } else { - Color::Rgba(0.0, 0.0, 0.0, 1.0) - }) - .x_y(0.0, y - 3.0) - .position_ingame(ingame_pos) - .set(sct_bg_id, ui_widgets); - Text::new(&format!("{}", (floater.hp_change).abs())) - .font_size(font_size) - .font_id(self.fonts.cyri.conrod_id) - .x_y(0.0, y) - .color(if floater.hp_change < 0 { - Color::Rgba(font_col.r, font_col.g, font_col.b, fade) - } else { - Color::Rgba(0.1, 1.0, 0.1, 1.0) - }) - .position_ingame(ingame_pos) - .set(sct_id, ui_widgets); - } - } - } - } if global_state.settings.gameplay.sct { // Render Player SCT numbers @@ -1156,21 +885,26 @@ impl Hud { } } - // Render Name Tags - for (pos, name, level, height_offset) in ( + let mut overhead_walker = self.ids.overheads.walk(); + let mut sct_walker = self.ids.scts.walk(); + let mut sct_bg_walker = self.ids.sct_bgs.walk(); + + // Render overhead name tags and health bars + for (pos, name, stats, energy, height_offset, hpfl) in ( &entities, &pos, interpolated.maybe(), &stats, + &energy, players.maybe(), scales.maybe(), &bodies, &hp_floater_lists, ) .join() - .filter(|(entity, _, _, stats, _, _, _, _)| *entity != me && !stats.is_dead) + .filter(|(entity, _, _, stats, _, _, _, _, _)| *entity != me && !stats.is_dead) // Don't show outside a certain range - .filter(|(_, pos, _, _, _, _, _, hpfl)| { + .filter(|(_, pos, _, _, _, _, _, _, hpfl)| { pos.0.distance_squared(player_pos) < (if hpfl .time_since_last_dmg_by_me @@ -1182,7 +916,7 @@ impl Hud { }) .powi(2) }) - .map(|(_, pos, interpolated, stats, player, scale, body, _)| { + .map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl)| { // TODO: This is temporary // If the player used the default character name display their name instead let name = if stats.name == "Character Name" { @@ -1192,87 +926,169 @@ impl Hud { }; ( interpolated.map_or(pos.0, |i| i.pos), - format!("{}", name), - stats.level, + name, + stats, + energy, + // TODO: when body.height() is more accurate remove the 2.0 body.height() * 2.0 * scale.map_or(1.0, |s| s.0), + hpfl, ) }) { - let name_id = name_id_walker.next( - &mut self.ids.name_tags, + let overhead_id = overhead_walker.next( + &mut self.ids.overheads, &mut ui_widgets.widget_id_generator(), ); - let name_bg_id = name_id_bg_walker.next( - &mut self.ids.name_tags_bgs, - &mut ui_widgets.widget_id_generator(), - ); - let level_id = level_id_walker - .next(&mut self.ids.levels, &mut ui_widgets.widget_id_generator()); - let level_skull_id = level_skull_id_walker.next( - &mut self.ids.levels_skull, - &mut ui_widgets.widget_id_generator(), - ); - let ingame_pos = pos + Vec3::unit_z() * height_offset; - // Name - Text::new(&name) - .font_id(self.fonts.cyri.conrod_id) - .font_size(30) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .x_y(-1.0, MANA_BAR_Y + 48.0) - .position_ingame(ingame_pos) - .set(name_bg_id, ui_widgets); - Text::new(&name) - .font_id(self.fonts.cyri.conrod_id) - .font_size(30) - .color(Color::Rgba(0.61, 0.61, 0.89, 1.0)) - .x_y(0.0, MANA_BAR_Y + 50.0) - .position_ingame(ingame_pos) - .set(name_id, ui_widgets); + // Chat bubble, name, level, and hp bars + overhead::Overhead::new( + &name, + stats, + energy, + own_level, + self.pulse, + &self.imgs, + &self.fonts, + ) + .x_y(0.0, 100.0) + .position_ingame(ingame_pos) + .set(overhead_id, ui_widgets); - // Level - const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4); - const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0); - const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); - let op_level = level.level(); - let level_str = format!("{}", op_level); - // Change visuals of the level display depending on the player level/opponent - // level - let level_comp = op_level as i64 - own_level as i64; - // + 10 level above player -> skull - // + 5-10 levels above player -> high - // -5 - +5 levels around player level -> equal - // - 5 levels below player -> low - Text::new(if level_comp < 10 { &level_str } else { "?" }) - .font_id(self.fonts.cyri.conrod_id) - .font_size(if op_level > 9 && level_comp < 10 { - 14 + // Enemy SCT + if global_state.settings.gameplay.sct && !hpfl.floaters.is_empty() { + let floaters = &hpfl.floaters; + + // Colors + const WHITE: Rgb = Rgb::new(1.0, 0.9, 0.8); + const LIGHT_OR: Rgb = Rgb::new(1.0, 0.925, 0.749); + const LIGHT_MED_OR: Rgb = Rgb::new(1.0, 0.85, 0.498); + const MED_OR: Rgb = Rgb::new(1.0, 0.776, 0.247); + const DARK_ORANGE: Rgb = Rgb::new(1.0, 0.7, 0.0); + const RED_ORANGE: Rgb = Rgb::new(1.0, 0.349, 0.0); + const DAMAGE_COLORS: [Rgb; 6] = [ + WHITE, + LIGHT_OR, + LIGHT_MED_OR, + MED_OR, + DARK_ORANGE, + RED_ORANGE, + ]; + // Largest value that select the first color is 40, then it shifts colors + // every 5 + let font_col = |font_size: u32| { + DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize] + }; + + if global_state.settings.gameplay.sct_damage_batch { + let number_speed = 50.0; // Damage number speed + let sct_id = sct_walker + .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator()); + let sct_bg_id = sct_bg_walker + .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator()); + // Calculate total change + // Ignores healing + let hp_damage = floaters.iter().fold(0, |acc, f| { + if f.hp_change < 0 { + acc + f.hp_change + } else { + acc + } + }); + let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32; + let timer = floaters + .last() + .expect("There must be at least one floater") + .timer; + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size = 30 + + (max_hp_frac * 30.0) as u32 + + if timer < 0.1 { + (FLASH_MAX * (1.0 - timer / 0.1)) as u32 + } else { + 0 + }; + let font_col = font_col(font_size); + // Timer sets the widget offset + let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64 + * number_speed) + + 100.0; + // Timer sets text transparency + let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2; + + Text::new(&format!("{}", (hp_damage).abs())) + .font_size(font_size) + .font_id(self.fonts.cyri.conrod_id) + .color(Color::Rgba(0.0, 0.0, 0.0, fade)) + .x_y(0.0, y - 3.0) + .position_ingame(ingame_pos) + .set(sct_bg_id, ui_widgets); + Text::new(&format!("{}", hp_damage.abs())) + .font_size(font_size) + .font_id(self.fonts.cyri.conrod_id) + .x_y(0.0, y) + .color(if hp_damage < 0 { + Color::Rgba(font_col.r, font_col.g, font_col.b, fade) + } else { + Color::Rgba(0.1, 1.0, 0.1, fade) + }) + .position_ingame(ingame_pos) + .set(sct_id, ui_widgets); } else { - 15 - }) - .color(if level_comp > 4 { - HIGH - } else if level_comp < -5 { - LOW - } else { - EQUAL - }) - .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0) - .position_ingame(ingame_pos) - .set(level_id, ui_widgets); - if level_comp > 9 { - let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer - Image::new(if skull_ani as i32 == 1 && rand::random::() < 0.9 { - self.imgs.skull_2 - } else { - self.imgs.skull - }) - .w_h(18.0 * BARSIZE, 18.0 * BARSIZE) - .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))) - .position_ingame(ingame_pos) - .set(level_skull_id, ui_widgets); + for floater in floaters { + let number_speed = 250.0; // Single Numbers Speed + let sct_id = sct_walker + .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator()); + let sct_bg_id = sct_bg_walker + .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator()); + // Calculate total change + let max_hp_frac = + floater.hp_change.abs() as f32 / stats.health.maximum() as f32; + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size = 30 + + (max_hp_frac * 30.0) as u32 + + if floater.timer < 0.1 { + (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32 + } else { + 0 + }; + let font_col = font_col(font_size); + // Timer sets the widget offset + let y = (floater.timer as f64 + / crate::ecs::sys::floater::HP_SHOWTIME as f64 + * number_speed) + + 100.0; + // Timer sets text transparency + let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer) + * 0.25) + + 0.2; + + Text::new(&format!("{}", (floater.hp_change).abs())) + .font_size(font_size) + .font_id(self.fonts.cyri.conrod_id) + .color(if floater.hp_change < 0 { + Color::Rgba(0.0, 0.0, 0.0, fade) + } else { + Color::Rgba(0.0, 0.0, 0.0, 1.0) + }) + .x_y(0.0, y - 3.0) + .position_ingame(ingame_pos) + .set(sct_bg_id, ui_widgets); + Text::new(&format!("{}", (floater.hp_change).abs())) + .font_size(font_size) + .font_id(self.fonts.cyri.conrod_id) + .x_y(0.0, y) + .color(if floater.hp_change < 0 { + Color::Rgba(font_col.r, font_col.g, font_col.b, fade) + } else { + Color::Rgba(0.1, 1.0, 0.1, 1.0) + }) + .position_ingame(ingame_pos) + .set(sct_id, ui_widgets); + } + } } } } diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs new file mode 100644 index 0000000000..2f66aa5493 --- /dev/null +++ b/voxygen/src/hud/overhead.rs @@ -0,0 +1,300 @@ +use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR}; +use crate::ui::{fonts::ConrodVoxygenFonts, Ingameable}; +use common::comp::{Energy, Stats}; +use conrod_core::{ + position::Align, + widget::{self, Image, Rectangle, Text}, + widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, +}; + +widget_ids! { + struct Ids { + // Chat bubble + chat_bubble_text, + chat_bubble_text2, + chat_bubble_top_left, + chat_bubble_top, + chat_bubble_top_right, + chat_bubble_left, + chat_bubble_mid, + chat_bubble_right, + chat_bubble_bottom_left, + chat_bubble_bottom, + chat_bubble_bottom_right, + chat_bubble_tail, + + // Name + name_bg, + name, + + // HP + level, + level_skull, + health_bar, + health_bar_bg, + mana_bar, + health_bar_fg, + } +} + +/// ui widget containing everything that goes over a character's head +/// (Speech bubble, Name, Level, HP/energy bars, etc.) +#[derive(WidgetCommon)] +pub struct Overhead<'a> { + name: &'a str, + stats: &'a Stats, + energy: &'a Energy, + own_level: u32, + pulse: f32, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + #[conrod(common_builder)] + common: widget::CommonBuilder, +} + +impl<'a> Overhead<'a> { + pub fn new( + name: &'a str, + stats: &'a Stats, + energy: &'a Energy, + own_level: u32, + pulse: f32, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + ) -> Self { + Self { + name, + stats, + energy, + own_level, + pulse, + imgs, + fonts, + common: widget::CommonBuilder::default(), + } + } +} + +pub struct State { + ids: Ids, +} + +impl<'a> Ingameable for Overhead<'a> { + fn prim_count(&self) -> usize { + // Number of conrod primitives contained in the overhead display. TODO maybe + // this could be done automatically? + // - 2 Text::new for name + // - 2 Text::new for speech bubble + // - 10 Image::new for speech bubble (9-slice + tail) + // - 1 for level: either Text or Image + // - 4 for HP + mana + fg + bg + 19 + } +} + +impl<'a> Widget for Overhead<'a> { + type Event = (); + type State = State; + type Style = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + fn style(&self) -> Self::Style { () } + + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { id, state, ui, .. } = args; + + const BARSIZE: f64 = 2.0; + const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5; + const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0; + + // Name + Text::new(&self.name) + .font_id(self.fonts.cyri.conrod_id) + .font_size(30) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .x_y(-1.0, MANA_BAR_Y + 48.0) + .set(state.ids.name_bg, ui); + Text::new(&self.name) + .font_id(self.fonts.cyri.conrod_id) + .font_size(30) + .color(Color::Rgba(0.61, 0.61, 0.89, 1.0)) + .x_y(0.0, MANA_BAR_Y + 50.0) + .set(state.ids.name, ui); + + // Speech bubble + Text::new("Hello") + .font_id(self.fonts.cyri.conrod_id) + .font_size(15) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .up_from(state.ids.name, 10.0) + .x_align_to(state.ids.name, Align::Middle) + .parent(id) + .set(state.ids.chat_bubble_text, ui); + Image::new(self.imgs.chat_bubble_top_left) + .w_h(10.0, 10.0) + .top_left_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_top_left, ui); + Image::new(self.imgs.chat_bubble_top) + .h(10.0) + .w_of(state.ids.chat_bubble_text) + .mid_top_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_top, ui); + Image::new(self.imgs.chat_bubble_top_right) + .w_h(10.0, 10.0) + .top_right_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_top_right, ui); + Image::new(self.imgs.chat_bubble_left) + .w(10.0) + .h_of(state.ids.chat_bubble_text) + .mid_left_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_left, ui); + Image::new(self.imgs.chat_bubble_mid) + .wh_of(state.ids.chat_bubble_text) + .top_left_of(state.ids.chat_bubble_text) + .parent(id) + .set(state.ids.chat_bubble_mid, ui); + Image::new(self.imgs.chat_bubble_right) + .w(10.0) + .h_of(state.ids.chat_bubble_text) + .mid_right_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_right, ui); + Image::new(self.imgs.chat_bubble_bottom_left) + .w_h(10.0, 10.0) + .bottom_left_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_bottom_left, ui); + Image::new(self.imgs.chat_bubble_bottom) + .h(10.0) + .w_of(state.ids.chat_bubble_text) + .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_bottom, ui); + Image::new(self.imgs.chat_bubble_bottom_right) + .w_h(10.0, 10.0) + .bottom_right_with_margin_on(state.ids.chat_bubble_text, -10.0) + .parent(id) + .set(state.ids.chat_bubble_bottom_right, ui); + Image::new(self.imgs.chat_bubble_tail) + .w_h(11.0, 16.0) + .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -16.0) + .parent(id) + .set(state.ids.chat_bubble_tail, ui); + // Why is there a second text widget?: The first is to position the 9-slice + // around and the second is to display text. Changing .depth manually + // causes strange problems in unrelated parts of the ui (the debug + // overlay is offset by a npc's screen position) TODO + Text::new("Hello") + .font_id(self.fonts.cyri.conrod_id) + .font_size(15) + .top_left_of(state.ids.chat_bubble_text) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .parent(id) + .set(state.ids.chat_bubble_text2, ui); + + let hp_percentage = + self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0; + let energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0; + let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer + let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); + + // Background + Image::new(self.imgs.enemy_health_bg) + .w_h(84.0 * BARSIZE, 10.0 * BARSIZE) + .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5) + .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8))) + .parent(id) + .set(state.ids.health_bar_bg, ui); + + // % HP Filling + Image::new(self.imgs.enemy_bar) + .w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE) + .x_y( + (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE, + MANA_BAR_Y + 7.5, + ) + .color(Some(if hp_percentage <= 25.0 { + crit_hp_color + } else if hp_percentage <= 50.0 { + LOW_HP_COLOR + } else { + HP_COLOR + })) + .parent(id) + .set(state.ids.health_bar, ui); + // % Mana Filling + Rectangle::fill_with( + [ + 72.0 * (self.energy.current() as f64 / self.energy.maximum() as f64) * BARSIZE, + MANA_BAR_HEIGHT, + ], + MANA_COLOR, + ) + .x_y( + ((3.5 + (energy_percentage / 100.0 * 36.5)) - 36.45) * BARSIZE, + MANA_BAR_Y, //-32.0, + ) + .parent(id) + .set(state.ids.mana_bar, ui); + + // Foreground + Image::new(self.imgs.enemy_health) + .w_h(84.0 * BARSIZE, 10.0 * BARSIZE) + .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99))) + .parent(id) + .set(state.ids.health_bar_fg, ui); + + // Level + const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4); + const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0); + const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); + // Change visuals of the level display depending on the player level/opponent + // level + let level_comp = self.stats.level.level() as i64 - self.own_level as i64; + // + 10 level above player -> skull + // + 5-10 levels above player -> high + // -5 - +5 levels around player level -> equal + // - 5 levels below player -> low + if level_comp > 9 { + let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer + Image::new(if skull_ani as i32 == 1 && rand::random::() < 0.9 { + self.imgs.skull_2 + } else { + self.imgs.skull + }) + .w_h(18.0 * BARSIZE, 18.0 * BARSIZE) + .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))) + .parent(id) + .set(state.ids.level_skull, ui); + } else { + Text::new(&format!("{}", self.stats.level.level())) + .font_id(self.fonts.cyri.conrod_id) + .font_size(if self.stats.level.level() > 9 && level_comp < 10 { + 14 + } else { + 15 + }) + .color(if level_comp > 4 { + HIGH + } else if level_comp < -5 { + LOW + } else { + EQUAL + }) + .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0) + .parent(id) + .set(state.ids.level, ui); + } + } +}