mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added chat bubbles. Refactored bubble+name+hp+energy into new overhead widget
This commit is contained in:
parent
3e698d5b32
commit
73a29b339c
BIN
assets/voxygen/element/frames/bubble/bottom.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/bottom.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/bottom_left.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/bottom_left.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/bottom_right.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/bottom_right.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/left.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/left.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/mid.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/mid.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/right.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/right.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/tail.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/tail.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/top.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/top.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/top_left.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/top_left.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble/top_right.png
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/element/frames/bubble/top_right.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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: (),
|
||||
}
|
||||
|
@ -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
300
voxygen/src/hud/overhead.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user