Added chat bubbles. Refactored bubble+name+hp+energy into new overhead widget

This commit is contained in:
CapsizeGlimmer 2020-05-24 02:37:10 -04:00 committed by Pfauenauge90
parent 3e698d5b32
commit 73a29b339c
13 changed files with 509 additions and 351 deletions

BIN
assets/voxygen/element/frames/bubble/bottom.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/bottom_left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/bottom_right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/mid.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/tail.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/top.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/top_left.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/bubble/top_right.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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",
<BlankGraphic>
nothing: (),
}

View File

@ -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::<comp::Pos>()
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
let mut name_id_walker = self.ids.name_tags.walk();
let mut name_id_bg_walker = self.ids.name_tags_bgs.walk();
let mut level_id_walker = self.ids.levels.walk();
let mut level_skull_id_walker = self.ids.levels_skull.walk();
let mut health_id_walker = self.ids.health_bars.walk();
let mut mana_id_walker = self.ids.mana_bars.walk();
let mut health_back_id_walker = self.ids.health_bar_backs.walk();
let mut health_front_id_walker = self.ids.health_bar_fronts.walk();
let mut sct_bg_id_walker = self.ids.sct_bgs.walk();
let mut sct_id_walker = self.ids.scts.walk();
// Render Health Bars
for (pos, stats, energy, height_offset, hp_floater_list) in (
&entities,
&pos,
interpolated.maybe(),
&stats,
&energy,
scales.maybe(),
&bodies,
&hp_floater_lists,
)
.join()
.filter(|(entity, _, _, stats, _, _, _, _)| {
*entity != me && !stats.is_dead
//&& stats.health.current() != stats.health.maximum()
})
// Don't show outside a certain range
.filter(|(_, pos, _, _, _, _, _, hpfl)| {
pos.0.distance_squared(player_pos)
< (if hpfl
.time_since_last_dmg_by_me
.map_or(false, |t| t < NAMETAG_DMG_TIME)
{
NAMETAG_DMG_RANGE
} else {
NAMETAG_RANGE
})
.powi(2)
})
.map(|(_, pos, interpolated, stats, energy, scale, body, f)| {
(
interpolated.map_or(pos.0, |i| i.pos),
stats,
energy,
// TODO: when body.height() is more accurate remove the 2.0
body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
f,
)
})
{
let back_id = health_back_id_walker.next(
&mut self.ids.health_bar_backs,
&mut ui_widgets.widget_id_generator(),
);
let health_bar_id = health_id_walker.next(
&mut self.ids.health_bars,
&mut ui_widgets.widget_id_generator(),
);
let mana_bar_id = mana_id_walker.next(
&mut self.ids.mana_bars,
&mut ui_widgets.widget_id_generator(),
);
let front_id = health_front_id_walker.next(
&mut self.ids.health_bar_fronts,
&mut ui_widgets.widget_id_generator(),
);
let hp_percentage =
stats.health.current() as f64 / stats.health.maximum() as f64 * 100.0;
let energy_percentage = energy.current() as f64 / energy.maximum() as f64 * 100.0;
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
let ingame_pos = pos + Vec3::unit_z() * height_offset;
// Background
Image::new(self.imgs.enemy_health_bg)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
.position_ingame(ingame_pos)
.set(back_id, ui_widgets);
// % HP Filling
Image::new(self.imgs.enemy_bar)
.w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
.x_y(
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
MANA_BAR_Y + 7.5,
)
.color(Some(if hp_percentage <= 25.0 {
crit_hp_color
} else if hp_percentage <= 50.0 {
LOW_HP_COLOR
} else {
HP_COLOR
}))
.position_ingame(ingame_pos)
.set(health_bar_id, ui_widgets);
// % Mana Filling
Rectangle::fill_with(
[
72.0 * (energy.current() as f64 / energy.maximum() as f64) * BARSIZE,
MANA_BAR_HEIGHT,
],
MANA_COLOR,
)
.x_y(
((3.5 + (energy_percentage / 100.0 * 36.5)) - 36.45) * BARSIZE,
MANA_BAR_Y, //-32.0,
)
.position_ingame(ingame_pos)
.set(mana_bar_id, ui_widgets);
// Foreground
Image::new(self.imgs.enemy_health)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
.position_ingame(ingame_pos)
.set(front_id, ui_widgets);
// Enemy SCT
if let Some(floaters) = Some(hp_floater_list)
.filter(|fl| !fl.floaters.is_empty() && global_state.settings.gameplay.sct)
.map(|l| &l.floaters)
{
// Colors
const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
const DAMAGE_COLORS: [Rgb<f32>; 6] = [
WHITE,
LIGHT_OR,
LIGHT_MED_OR,
MED_OR,
DARK_ORANGE,
RED_ORANGE,
];
// Largest value that select the first color is 40, then it shifts colors
// every 5
let font_col = |font_size: u32| {
DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
};
if global_state.settings.gameplay.sct_damage_batch {
let number_speed = 50.0; // Damage number speed
let sct_bg_id = sct_bg_id_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
let sct_id = sct_id_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
// Calculate total change
// Ignores healing
let hp_damage = floaters.iter().fold(0, |acc, f| {
if f.hp_change < 0 {
acc + f.hp_change
} else {
acc
}
});
let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32;
let timer = floaters
.last()
.expect("There must be at least one floater")
.timer;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if timer < 0.1 {
(FLASH_MAX * (1.0 - timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2;
Text::new(&format!("{}", (hp_damage).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", hp_damage.abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if hp_damage < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, fade)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
} else {
for floater in floaters {
let number_speed = 250.0; // Single Numbers Speed
let sct_bg_id = sct_bg_id_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
let sct_id = sct_id_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
// Calculate total change
let max_hp_frac =
floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if floater.timer < 0.1 {
(FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (floater.timer as f64
/ crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer)
* 0.25)
+ 0.2;
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(if floater.hp_change < 0 {
Color::Rgba(0.0, 0.0, 0.0, fade)
} else {
Color::Rgba(0.0, 0.0, 0.0, 1.0)
})
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if floater.hp_change < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, 1.0)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
}
}
}
}
if global_state.settings.gameplay.sct {
// Render Player SCT numbers
@ -1156,21 +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<f32> = Rgb::new(1.0, 0.9, 0.8);
const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
const DAMAGE_COLORS: [Rgb<f32>; 6] = [
WHITE,
LIGHT_OR,
LIGHT_MED_OR,
MED_OR,
DARK_ORANGE,
RED_ORANGE,
];
// Largest value that select the first color is 40, then it shifts colors
// every 5
let font_col = |font_size: u32| {
DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
};
if global_state.settings.gameplay.sct_damage_batch {
let number_speed = 50.0; // Damage number speed
let sct_id = sct_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
let sct_bg_id = sct_bg_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
// Calculate total change
// Ignores healing
let hp_damage = floaters.iter().fold(0, |acc, f| {
if f.hp_change < 0 {
acc + f.hp_change
} else {
acc
}
});
let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32;
let timer = floaters
.last()
.expect("There must be at least one floater")
.timer;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if timer < 0.1 {
(FLASH_MAX * (1.0 - timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2;
Text::new(&format!("{}", (hp_damage).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", hp_damage.abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if hp_damage < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, fade)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
} else {
15
})
.color(if level_comp > 4 {
HIGH
} else if level_comp < -5 {
LOW
} else {
EQUAL
})
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
.position_ingame(ingame_pos)
.set(level_id, ui_widgets);
if level_comp > 9 {
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
self.imgs.skull_2
} else {
self.imgs.skull
})
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.position_ingame(ingame_pos)
.set(level_skull_id, ui_widgets);
for floater in floaters {
let number_speed = 250.0; // Single Numbers Speed
let sct_id = sct_walker
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
let sct_bg_id = sct_bg_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
// Calculate total change
let max_hp_frac =
floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30
+ (max_hp_frac * 30.0) as u32
+ if floater.timer < 0.1 {
(FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32
} else {
0
};
let font_col = font_col(font_size);
// Timer sets the widget offset
let y = (floater.timer as f64
/ crate::ecs::sys::floater::HP_SHOWTIME as f64
* number_speed)
+ 100.0;
// Timer sets text transparency
let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer)
* 0.25)
+ 0.2;
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.color(if floater.hp_change < 0 {
Color::Rgba(0.0, 0.0, 0.0, fade)
} else {
Color::Rgba(0.0, 0.0, 0.0, 1.0)
})
.x_y(0.0, y - 3.0)
.position_ingame(ingame_pos)
.set(sct_bg_id, ui_widgets);
Text::new(&format!("{}", (floater.hp_change).abs()))
.font_size(font_size)
.font_id(self.fonts.cyri.conrod_id)
.x_y(0.0, y)
.color(if floater.hp_change < 0 {
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
} else {
Color::Rgba(0.1, 1.0, 0.1, 1.0)
})
.position_ingame(ingame_pos)
.set(sct_id, ui_widgets);
}
}
}
}
}

300
voxygen/src/hud/overhead.rs Normal file
View File

@ -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>) -> 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::<f32>() < 0.9 {
self.imgs.skull_2
} else {
self.imgs.skull
})
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.parent(id)
.set(state.ids.level_skull, ui);
} else {
Text::new(&format!("{}", self.stats.level.level()))
.font_id(self.fonts.cyri.conrod_id)
.font_size(if self.stats.level.level() > 9 && level_comp < 10 {
14
} else {
15
})
.color(if level_comp > 4 {
HIGH
} else if level_comp < -5 {
LOW
} else {
EQUAL
})
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
.parent(id)
.set(state.ids.level, ui);
}
}
}