1use super::{
2 BLACK, BarNumbers, CRITICAL_HP_COLOR, HP_COLOR, HudInfo, LOW_HP_COLOR, POISE_COLOR,
3 POISEBAR_TICK_COLOR, QUALITY_EPIC, QUALITY_LEGENDARY, STAMINA_COLOR, ShortcutNumbers,
4 TEXT_COLOR, TEXT_VELORITE, UI_HIGHLIGHT_0, XP_COLOR, hotbar,
5 img_ids::{Imgs, ImgsRot},
6 item_imgs::ItemImgs,
7 slots, util,
8};
9use crate::{
10 GlobalState,
11 game_input::GameInput,
12 hud::{
13 ComboFloater, Position, PositionSpecifier, animation::animation_timer,
14 controller_icons as icon_utils,
15 },
16 key_state::GIVE_UP_HOLD_TIME,
17 ui::{
18 ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, Tooltip, TooltipManager,
19 Tooltipable,
20 fonts::Fonts,
21 slot::{ContentSize, SlotMaker},
22 },
23 window::{KeyMouse, LastInput},
24};
25use i18n::Localization;
26
27use client::{self, Client};
28use common::{
29 comp::{
30 self, Ability, ActiveAbilities, Body, Buffs, CharacterState, Combo, Energy, Hardcore,
31 Health, Inventory, Poise, PoiseState, SkillSet, Stats,
32 ability::{AbilityInput, Stance},
33 is_downed,
34 item::{ItemDesc, ItemI18n, MaterialStatManifest, tool::ToolKind},
35 skillset::SkillGroupKind,
36 },
37 recipe::RecipeBookManifest,
38};
39use conrod_core::{
40 Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
41 widget::{self, Button, Image, Rectangle, Text},
42 widget_ids,
43};
44use vek::*;
45
46widget_ids! {
47 struct Ids {
48 death_message_1,
50 death_message_2,
51 death_message_1_bg,
52 death_message_2_bg,
53 death_message_3,
54 death_message_3_bg,
55 frame,
57 bg_health,
58 frame_health,
59 bg_energy,
60 frame_energy,
61 bg_poise,
62 frame_poise,
63 m1_ico,
64 m2_ico,
65 hp_alignment,
67 hp_filling,
68 hp_decayed,
69 hp_txt_bg,
70 hp_txt,
71 decay_overlay,
72 energy_alignment,
74 energy_filling,
75 energy_txt_bg,
76 energy_txt,
77 poise_alignment,
79 poise_filling,
80 poise_ticks[],
81 poise_txt_bg,
82 poise_txt,
83 exp_frame_bg,
85 exp_frame,
86 exp_filling,
87 exp_img_frame_bg,
88 exp_img_frame,
89 exp_img,
90 exp_lvl,
91 diary_txt_bg,
92 diary_txt,
93 sp_arrow,
94 sp_arrow_txt_bg,
95 sp_arrow_txt,
96 bag_frame,
98 bag_filling,
99 bag_img_frame_bg,
100 bag_img_frame,
101 bag_img,
102 bag_space_bg,
103 bag_space,
104 bag_numbers_alignment,
105 bag_text_bg,
106 bag_text,
107 combo_align,
109 combo_bg,
110 combo,
111 m1_slot_bg,
113 m1_content,
114 m2_slot_bg,
115 m2_content,
116 slot1,
117 slot1_text,
118 slot1_text_bg,
119 slot2,
120 slot2_text,
121 slot2_text_bg,
122 slot3,
123 slot3_text,
124 slot3_text_bg,
125 slot4,
126 slot4_text,
127 slot4_text_bg,
128 slot5,
129 slot5_text,
130 slot5_text_bg,
131 slot6,
132 slot6_text,
133 slot6_text_bg,
134 slot7,
135 slot7_text,
136 slot7_text_bg,
137 slot8,
138 slot8_text,
139 slot8_text_bg,
140 slot9,
141 slot9_text,
142 slot9_text_bg,
143 slot10,
144 slot10_text,
145 slot10_text_bg,
146 slot_highlight,
147 }
148}
149
150#[derive(Clone, Copy)]
151struct SlotEntry {
152 slot: hotbar::Slot,
153 widget_id: widget::Id,
154 position: PositionSpecifier,
155 game_input: GameInput,
156 shortcut_position: PositionSpecifier,
157 shortcut_position_bg: PositionSpecifier,
158 shortcut_widget_ids: (widget::Id, widget::Id),
159}
160
161fn slot_entries(state: &State, slot_offset: f64) -> [SlotEntry; 10] {
162 use PositionSpecifier::*;
163
164 [
165 SlotEntry {
167 slot: hotbar::Slot::One,
168 widget_id: state.ids.slot1,
169 position: BottomLeftWithMarginsOn(state.ids.frame, 0.0, 0.0),
170 game_input: GameInput::Slot1,
171 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot1_text_bg, 1.0, 1.0),
172 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot1, 3.0, 5.0),
173 shortcut_widget_ids: (state.ids.slot1_text, state.ids.slot1_text_bg),
174 },
175 SlotEntry {
176 slot: hotbar::Slot::Two,
177 widget_id: state.ids.slot2,
178 position: RightFrom(state.ids.slot1, slot_offset),
179 game_input: GameInput::Slot2,
180 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot2_text_bg, 1.0, 1.0),
181 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot2, 3.0, 5.0),
182 shortcut_widget_ids: (state.ids.slot2_text, state.ids.slot2_text_bg),
183 },
184 SlotEntry {
185 slot: hotbar::Slot::Three,
186 widget_id: state.ids.slot3,
187 position: RightFrom(state.ids.slot2, slot_offset),
188 game_input: GameInput::Slot3,
189 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot3_text_bg, 1.0, 1.0),
190 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot3, 3.0, 5.0),
191 shortcut_widget_ids: (state.ids.slot3_text, state.ids.slot3_text_bg),
192 },
193 SlotEntry {
194 slot: hotbar::Slot::Four,
195 widget_id: state.ids.slot4,
196 position: RightFrom(state.ids.slot3, slot_offset),
197 game_input: GameInput::Slot4,
198 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot4_text_bg, 1.0, 1.0),
199 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot4, 3.0, 5.0),
200 shortcut_widget_ids: (state.ids.slot4_text, state.ids.slot4_text_bg),
201 },
202 SlotEntry {
203 slot: hotbar::Slot::Five,
204 widget_id: state.ids.slot5,
205 position: RightFrom(state.ids.slot4, slot_offset),
206 game_input: GameInput::Slot5,
207 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot5_text_bg, 1.0, 1.0),
208 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot5, 3.0, 5.0),
209 shortcut_widget_ids: (state.ids.slot5_text, state.ids.slot5_text_bg),
210 },
211 SlotEntry {
213 slot: hotbar::Slot::Six,
214 widget_id: state.ids.slot6,
215 position: RightFrom(state.ids.m2_slot_bg, slot_offset),
216 game_input: GameInput::Slot6,
217 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot6_text_bg, 1.0, 1.0),
218 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot6, 3.0, 5.0),
219 shortcut_widget_ids: (state.ids.slot6_text, state.ids.slot6_text_bg),
220 },
221 SlotEntry {
222 slot: hotbar::Slot::Seven,
223 widget_id: state.ids.slot7,
224 position: RightFrom(state.ids.slot6, slot_offset),
225 game_input: GameInput::Slot7,
226 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot7_text_bg, 1.0, 1.0),
227 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot7, 3.0, 5.0),
228 shortcut_widget_ids: (state.ids.slot7_text, state.ids.slot7_text_bg),
229 },
230 SlotEntry {
231 slot: hotbar::Slot::Eight,
232 widget_id: state.ids.slot8,
233 position: RightFrom(state.ids.slot7, slot_offset),
234 game_input: GameInput::Slot8,
235 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot8_text_bg, 1.0, 1.0),
236 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot8, 3.0, 5.0),
237 shortcut_widget_ids: (state.ids.slot8_text, state.ids.slot8_text_bg),
238 },
239 SlotEntry {
240 slot: hotbar::Slot::Nine,
241 widget_id: state.ids.slot9,
242 position: RightFrom(state.ids.slot8, slot_offset),
243 game_input: GameInput::Slot9,
244 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot9_text_bg, 1.0, 1.0),
245 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot9, 3.0, 5.0),
246 shortcut_widget_ids: (state.ids.slot9_text, state.ids.slot9_text_bg),
247 },
248 SlotEntry {
249 slot: hotbar::Slot::Ten,
250 widget_id: state.ids.slot10,
251 position: RightFrom(state.ids.slot9, slot_offset),
252 game_input: GameInput::Slot10,
253 shortcut_position: BottomLeftWithMarginsOn(state.ids.slot10_text_bg, 1.0, 1.0),
254 shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot10, 3.0, 5.0),
255 shortcut_widget_ids: (state.ids.slot10_text, state.ids.slot10_text_bg),
256 },
257 ]
258}
259
260pub enum Event {
261 OpenDiary(SkillGroupKind),
262 OpenBag,
263}
264
265#[derive(WidgetCommon)]
266pub struct Skillbar<'a> {
267 client: &'a Client,
268 info: &'a HudInfo<'a>,
269 global_state: &'a GlobalState,
270 imgs: &'a Imgs,
271 item_imgs: &'a ItemImgs,
272 fonts: &'a Fonts,
273 rot_imgs: &'a ImgsRot,
274 health: &'a Health,
275 inventory: &'a Inventory,
276 energy: &'a Energy,
277 poise: &'a Poise,
278 skillset: &'a SkillSet,
279 active_abilities: Option<&'a ActiveAbilities>,
280 body: &'a Body,
281 hotbar: &'a hotbar::State,
282 tooltip_manager: &'a mut TooltipManager,
283 item_tooltip_manager: &'a mut ItemTooltipManager,
284 slot_manager: &'a mut slots::SlotManager,
285 localized_strings: &'a Localization,
286 item_i18n: &'a ItemI18n,
287 pulse: f32,
288 #[conrod(common_builder)]
289 common: widget::CommonBuilder,
290 msm: &'a MaterialStatManifest,
291 rbm: &'a RecipeBookManifest,
292 combo_floater: Option<ComboFloater>,
293 combo: Option<&'a Combo>,
294 char_state: Option<&'a CharacterState>,
295 stance: Option<&'a Stance>,
296 stats: Option<&'a Stats>,
297 buffs: Option<&'a Buffs>,
298}
299
300impl<'a> Skillbar<'a> {
301 #[expect(clippy::too_many_arguments)]
302 pub fn new(
303 client: &'a Client,
304 info: &'a HudInfo,
305 global_state: &'a GlobalState,
306 imgs: &'a Imgs,
307 item_imgs: &'a ItemImgs,
308 fonts: &'a Fonts,
309 rot_imgs: &'a ImgsRot,
310 health: &'a Health,
311 inventory: &'a Inventory,
312 energy: &'a Energy,
313 poise: &'a Poise,
314 skillset: &'a SkillSet,
315 active_abilities: Option<&'a ActiveAbilities>,
316 body: &'a Body,
317 pulse: f32,
318 hotbar: &'a hotbar::State,
319 tooltip_manager: &'a mut TooltipManager,
320 item_tooltip_manager: &'a mut ItemTooltipManager,
321 slot_manager: &'a mut slots::SlotManager,
322 localized_strings: &'a Localization,
323 item_i18n: &'a ItemI18n,
324 msm: &'a MaterialStatManifest,
325 rbm: &'a RecipeBookManifest,
326 combo_floater: Option<ComboFloater>,
327 combo: Option<&'a Combo>,
328 char_state: Option<&'a CharacterState>,
329 stance: Option<&'a Stance>,
330 stats: Option<&'a Stats>,
331 buffs: Option<&'a Buffs>,
332 ) -> Self {
333 Self {
334 client,
335 info,
336 global_state,
337 imgs,
338 item_imgs,
339 fonts,
340 rot_imgs,
341 health,
342 inventory,
343 energy,
344 poise,
345 skillset,
346 active_abilities,
347 body,
348 common: widget::CommonBuilder::default(),
349 pulse,
350 hotbar,
351 tooltip_manager,
352 item_tooltip_manager,
353 slot_manager,
354 localized_strings,
355 item_i18n,
356 msm,
357 rbm,
358 combo_floater,
359 combo,
360 char_state,
361 stance,
362 stats,
363 buffs,
364 }
365 }
366
367 fn create_new_button_with_shadow(
368 &self,
369 ui: &mut UiCell,
370 key_mouse: &KeyMouse,
371 button_identifier: widget::Id,
372 text_background: widget::Id,
373 text: widget::Id,
374 ) {
375 let key_desc = key_mouse.display_shortest();
376
377 Text::new(&key_desc)
379 .bottom_right_with_margins_on(button_identifier, 0.0, 0.0)
380 .font_size(10)
381 .font_id(self.fonts.cyri.conrod_id)
382 .color(BLACK)
383 .set(text_background, ui);
384
385 Text::new(&key_desc)
387 .bottom_right_with_margins_on(text_background, 1.0, 1.0)
388 .font_size(10)
389 .font_id(self.fonts.cyri.conrod_id)
390 .color(TEXT_COLOR)
391 .set(text, ui);
392 }
393
394 fn show_give_up_message(&self, state: &State, ui: &mut UiCell) {
395 let localized_strings = self.localized_strings;
396 let hardcore = self.client.current::<Hardcore>().is_some();
397
398 if let Some(key) = self
399 .global_state
400 .settings
401 .controls
402 .get_binding(GameInput::GiveUp)
403 {
404 let respawn_msg =
405 localized_strings.get_msg_ctx("hud-press_key_to_give_up", &i18n::fluent_args! {
406 "key" => key.display_string()
407 });
408 let penalty_msg = if hardcore {
409 self.localized_strings
410 .get_msg("hud-hardcore_will_char_deleted")
411 } else {
412 self.localized_strings.get_msg("hud-items_will_lose_dur")
413 };
414
415 let recieving_help_msg = localized_strings.get_msg("hud-downed_recieving_help");
416 Text::new(&penalty_msg)
417 .mid_bottom_with_margin_on(ui.window, 180.0)
418 .font_size(self.fonts.cyri.scale(30))
419 .font_id(self.fonts.cyri.conrod_id)
420 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
421 .set(state.ids.death_message_3_bg, ui);
422 Text::new(&respawn_msg)
423 .mid_top_with_margin_on(state.ids.death_message_3_bg, -50.0)
424 .font_size(self.fonts.cyri.scale(30))
425 .font_id(self.fonts.cyri.conrod_id)
426 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
427 .set(state.ids.death_message_2_bg, ui);
428 Text::new(&penalty_msg)
429 .bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
430 .font_size(self.fonts.cyri.scale(30))
431 .font_id(self.fonts.cyri.conrod_id)
432 .color(TEXT_COLOR)
433 .set(state.ids.death_message_3, ui);
434 Text::new(&respawn_msg)
435 .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0)
436 .font_size(self.fonts.cyri.scale(30))
437 .font_id(self.fonts.cyri.conrod_id)
438 .color(TEXT_COLOR)
439 .set(state.ids.death_message_2, ui);
440 if self
441 .client
442 .state()
443 .read_storage::<common::interaction::Interactors>()
444 .get(self.client.entity())
445 .is_some_and(|interactors| {
446 interactors.has_interaction(common::interaction::InteractionKind::HelpDowned)
447 })
448 {
449 Text::new(&recieving_help_msg)
450 .mid_top_with_margin_on(state.ids.death_message_2_bg, -50.0)
451 .font_size(self.fonts.cyri.scale(24))
452 .font_id(self.fonts.cyri.conrod_id)
453 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
454 .set(state.ids.death_message_1_bg, ui);
455 Text::new(&recieving_help_msg)
456 .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
457 .font_size(self.fonts.cyri.scale(24))
458 .font_id(self.fonts.cyri.conrod_id)
459 .color(HP_COLOR)
460 .set(state.ids.death_message_1, ui);
461 }
462 }
463 }
464
465 fn show_death_message(&self, state: &State, ui: &mut UiCell) {
466 let localized_strings = self.localized_strings;
467 let hardcore = self.client.current::<Hardcore>().is_some();
468
469 if let Some(key) = self
470 .global_state
471 .settings
472 .controls
473 .get_binding(GameInput::Respawn)
474 {
475 Text::new(&self.localized_strings.get_msg("hud-you_died"))
476 .middle_of(ui.window)
477 .font_size(self.fonts.cyri.scale(50))
478 .font_id(self.fonts.cyri.conrod_id)
479 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
480 .set(state.ids.death_message_1_bg, ui);
481 let respawn_msg = if hardcore {
482 localized_strings.get_msg_ctx(
483 "hud-press_key_to_return_to_char_menu",
484 &i18n::fluent_args! {
485 "key" => key.display_string()
486 },
487 )
488 } else {
489 localized_strings.get_msg_ctx("hud-press_key_to_respawn", &i18n::fluent_args! {
490 "key" => key.display_string()
491 })
492 };
493 let penalty_msg = if hardcore {
494 self.localized_strings.get_msg("hud-hardcore_char_deleted")
495 } else {
496 self.localized_strings.get_msg("hud-items_lost_dur")
497 };
498 Text::new(&respawn_msg)
499 .mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0)
500 .font_size(self.fonts.cyri.scale(30))
501 .font_id(self.fonts.cyri.conrod_id)
502 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
503 .set(state.ids.death_message_2_bg, ui);
504 Text::new(&penalty_msg)
505 .mid_bottom_with_margin_on(state.ids.death_message_2_bg, -50.0)
506 .font_size(self.fonts.cyri.scale(30))
507 .font_id(self.fonts.cyri.conrod_id)
508 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
509 .set(state.ids.death_message_3_bg, ui);
510 Text::new(&self.localized_strings.get_msg("hud-you_died"))
511 .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
512 .font_size(self.fonts.cyri.scale(50))
513 .font_id(self.fonts.cyri.conrod_id)
514 .color(CRITICAL_HP_COLOR)
515 .set(state.ids.death_message_1, ui);
516 Text::new(&respawn_msg)
517 .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0)
518 .font_size(self.fonts.cyri.scale(30))
519 .font_id(self.fonts.cyri.conrod_id)
520 .color(CRITICAL_HP_COLOR)
521 .set(state.ids.death_message_2, ui);
522 Text::new(&penalty_msg)
523 .bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
524 .font_size(self.fonts.cyri.scale(30))
525 .font_id(self.fonts.cyri.conrod_id)
526 .color(CRITICAL_HP_COLOR)
527 .set(state.ids.death_message_3, ui);
528 }
529 }
530
531 fn show_stat_bars(&self, state: &State, ui: &mut UiCell, events: &mut Vec<Event>) {
532 let (hp_percentage, energy_percentage, poise_percentage): (f64, f64, f64) =
533 if self.health.is_dead {
534 (0.0, 0.0, 0.0)
535 } else {
536 let max_hp = f64::from(self.health.base_max().max(self.health.maximum()));
537 let current_hp = f64::from(self.health.current());
538 (
539 current_hp / max_hp * 100.0,
540 f64::from(self.energy.fraction() * 100.0),
541 f64::from(self.poise.fraction() * 100.0),
542 )
543 };
544
545 let hp_ani = (self.pulse * 4.0).cos() * 0.5 + 0.8;
547 let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
548 let bar_values = self.global_state.settings.interface.bar_numbers;
549 let is_downed = is_downed(Some(self.health), self.char_state);
550 let show_health = self.global_state.settings.interface.always_show_bars
551 || is_downed
552 || (self.health.current() - self.health.maximum()).abs() > Health::HEALTH_EPSILON;
553 let show_energy = self.global_state.settings.interface.always_show_bars
554 || (self.energy.current() - self.energy.maximum()).abs() > Energy::ENERGY_EPSILON;
555 let show_poise = self.global_state.settings.interface.enable_poise_bar
556 && (self.global_state.settings.interface.always_show_bars
557 || (self.poise.current() - self.poise.maximum()).abs() > Poise::POISE_EPSILON);
558 let decayed_health = 1.0 - self.health.maximum() as f64 / self.health.base_max() as f64;
559
560 if show_health && !self.health.is_dead || decayed_health > 0.0 {
561 let offset = 1.0;
562 let hp_percentage = if is_downed {
563 100.0
564 * (1.0 - self.info.key_state.give_up.unwrap_or(0.0) / GIVE_UP_HOLD_TIME)
565 .clamp(0.0, 1.0) as f64
566 } else {
567 hp_percentage
568 };
569
570 Image::new(self.imgs.health_bg)
571 .w_h(484.0, 24.0)
572 .mid_top_with_margin_on(state.ids.frame, -offset)
573 .set(state.ids.bg_health, ui);
574 Rectangle::fill_with([480.0, 18.0], color::TRANSPARENT)
575 .top_left_with_margins_on(state.ids.bg_health, 2.0, 2.0)
576 .set(state.ids.hp_alignment, ui);
577 let health_col = match hp_percentage as u8 {
578 _ if is_downed => crit_hp_color,
579 0..=20 => crit_hp_color,
580 21..=40 => LOW_HP_COLOR,
581 _ => HP_COLOR,
582 };
583 Image::new(self.imgs.bar_content)
584 .w_h(480.0 * hp_percentage / 100.0, 18.0)
585 .color(Some(health_col))
586 .top_left_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
587 .set(state.ids.hp_filling, ui);
588
589 if decayed_health > 0.0 {
590 let decay_bar_len = 480.0 * decayed_health;
591 Image::new(self.imgs.bar_content)
592 .w_h(decay_bar_len, 18.0)
593 .color(Some(QUALITY_EPIC))
594 .top_right_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
595 .crop_kids()
596 .set(state.ids.hp_decayed, ui);
597
598 Image::new(self.imgs.decayed_bg)
599 .w_h(480.0, 18.0)
600 .color(Some(Color::Rgba(0.58, 0.29, 0.93, (hp_ani + 0.6).min(1.0))))
601 .top_left_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
602 .parent(state.ids.hp_decayed)
603 .set(state.ids.decay_overlay, ui);
604 }
605 Image::new(self.imgs.health_frame)
606 .w_h(484.0, 24.0)
607 .color(Some(UI_HIGHLIGHT_0))
608 .middle_of(state.ids.bg_health)
609 .set(state.ids.frame_health, ui);
610 }
611 if show_energy && !self.health.is_dead {
612 let offset = if show_health || decayed_health > 0.0 {
613 33.0
614 } else {
615 1.0
616 };
617 Image::new(self.imgs.energy_bg)
618 .w_h(323.0, 16.0)
619 .mid_top_with_margin_on(state.ids.frame, -offset)
620 .set(state.ids.bg_energy, ui);
621 Rectangle::fill_with([319.0, 10.0], color::TRANSPARENT)
622 .top_left_with_margins_on(state.ids.bg_energy, 2.0, 2.0)
623 .set(state.ids.energy_alignment, ui);
624 Image::new(self.imgs.bar_content)
625 .w_h(319.0 * energy_percentage / 100.0, 10.0)
626 .color(Some(STAMINA_COLOR))
627 .top_left_with_margins_on(state.ids.energy_alignment, 0.0, 0.0)
628 .set(state.ids.energy_filling, ui);
629 Image::new(self.imgs.energy_frame)
630 .w_h(323.0, 16.0)
631 .color(Some(UI_HIGHLIGHT_0))
632 .middle_of(state.ids.bg_energy)
633 .set(state.ids.frame_energy, ui);
634 }
635 if show_poise && !self.health.is_dead {
636 let offset = 16.0;
637
638 let poise_colour = match self.poise.previous_state {
639 self::PoiseState::KnockedDown => BLACK,
640 self::PoiseState::Dazed => Color::Rgba(0.25, 0.0, 0.15, 1.0),
641 self::PoiseState::Stunned => Color::Rgba(0.40, 0.0, 0.30, 1.0),
642 self::PoiseState::Interrupted => Color::Rgba(0.55, 0.0, 0.45, 1.0),
643 _ => POISE_COLOR,
644 };
645
646 Image::new(self.imgs.poise_bg)
647 .w_h(323.0, 14.0)
648 .mid_top_with_margin_on(state.ids.frame, -offset)
649 .set(state.ids.bg_poise, ui);
650 Rectangle::fill_with([319.0, 10.0], color::TRANSPARENT)
651 .top_left_with_margins_on(state.ids.bg_poise, 2.0, 2.0)
652 .set(state.ids.poise_alignment, ui);
653 Image::new(self.imgs.bar_content)
654 .w_h(319.0 * poise_percentage / 100.0, 10.0)
655 .color(Some(poise_colour))
656 .top_left_with_margins_on(state.ids.poise_alignment, 0.0, 0.0)
657 .set(state.ids.poise_filling, ui);
658 for i in 0..state.ids.poise_ticks.len() {
659 Image::new(self.imgs.poise_tick)
660 .w_h(3.0, 10.0)
661 .color(Some(POISEBAR_TICK_COLOR))
662 .top_left_with_margins_on(
663 state.ids.poise_alignment,
664 0.0,
665 319.0f64 * (self::Poise::POISE_THRESHOLDS[i] / self.poise.maximum()) as f64,
666 )
667 .set(state.ids.poise_ticks[i], ui);
668 }
669 Image::new(self.imgs.poise_frame)
670 .w_h(323.0, 16.0)
671 .color(Some(UI_HIGHLIGHT_0))
672 .middle_of(state.ids.bg_poise)
673 .set(state.ids.frame_poise, ui);
674 }
675 Image::new(self.imgs.selected_exp_bg)
677 .w_h(34.0, 38.0)
678 .bottom_right_with_margins_on(state.ids.slot10, 0.0, -37.0)
679 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
680 .set(state.ids.bag_img_frame_bg, ui);
681
682 if Button::image(self.imgs.bag_frame)
683 .w_h(34.0, 38.0)
684 .middle_of(state.ids.bag_img_frame_bg)
685 .set(state.ids.bag_img_frame, ui)
686 .was_clicked()
687 {
688 events.push(Event::OpenBag);
689 }
690 let inventory = self.inventory;
691
692 let space_used = inventory.populated_slots();
693 let space_max = inventory.slots().count();
694 let bag_space = format!("{}/{}", space_used, space_max);
695 let bag_space_percentage = space_used as f64 / space_max as f64;
696
697 Image::new(self.imgs.bar_content)
699 .w_h(1.0, 21.0 * bag_space_percentage)
700 .color(if bag_space_percentage < 0.6 {
701 Some(TEXT_VELORITE)
702 } else if bag_space_percentage < 1.0 {
703 Some(LOW_HP_COLOR)
704 } else {
705 Some(CRITICAL_HP_COLOR)
706 })
707 .graphics_for(state.ids.bag_img_frame)
708 .bottom_left_with_margins_on(state.ids.bag_img_frame, 14.0, 2.0)
709 .set(state.ids.bag_filling, ui);
710
711 Rectangle::fill_with([32.0, 11.0], color::TRANSPARENT)
713 .bottom_left_with_margins_on(state.ids.bag_img_frame_bg, 1.0, 2.0)
714 .graphics_for(state.ids.bag_img_frame)
715 .set(state.ids.bag_numbers_alignment, ui);
716 Text::new(&bag_space)
717 .middle_of(state.ids.bag_numbers_alignment)
718 .font_size(if bag_space.len() < 6 { 9 } else { 8 })
719 .font_id(self.fonts.cyri.conrod_id)
720 .color(BLACK)
721 .graphics_for(state.ids.bag_img_frame)
722 .set(state.ids.bag_space_bg, ui);
723 Text::new(&bag_space)
724 .bottom_right_with_margins_on(state.ids.bag_space_bg, 1.0, 1.0)
725 .font_size(if bag_space.len() < 6 { 9 } else { 8 })
726 .font_id(self.fonts.cyri.conrod_id)
727 .color(if bag_space_percentage < 0.6 {
728 TEXT_VELORITE
729 } else if bag_space_percentage < 1.0 {
730 LOW_HP_COLOR
731 } else {
732 CRITICAL_HP_COLOR
733 })
734 .graphics_for(state.ids.bag_img_frame)
735 .set(state.ids.bag_space, ui);
736
737 Image::new(self.imgs.bag_ico)
738 .w_h(24.0, 24.0)
739 .graphics_for(state.ids.bag_img_frame)
740 .mid_bottom_with_margin_on(state.ids.bag_img_frame, 13.0)
741 .set(state.ids.bag_img, ui);
742
743 if let Some(bag) = &self
744 .global_state
745 .settings
746 .controls
747 .get_binding(GameInput::Inventory)
748 {
749 self.create_new_button_with_shadow(
750 ui,
751 bag,
752 state.ids.bag_img,
753 state.ids.bag_text_bg,
754 state.ids.bag_text,
755 );
756 }
757
758 let unspent_sp = self.skillset.can_unlock_any_skill();
762 if unspent_sp {
763 let arrow_ani = animation_timer(self.pulse); Image::new(self.imgs.sp_indicator_arrow)
765 .w_h(20.0, 11.0)
766 .graphics_for(state.ids.exp_img_frame)
767 .mid_top_with_margin_on(state.ids.exp_img_frame, -12.0 + arrow_ani as f64)
768 .color(Some(QUALITY_LEGENDARY))
769 .set(state.ids.sp_arrow, ui);
770 Text::new(&self.localized_strings.get_msg("hud-sp_arrow_txt"))
771 .mid_top_with_margin_on(state.ids.sp_arrow, -18.0)
772 .graphics_for(state.ids.exp_img_frame)
773 .font_id(self.fonts.cyri.conrod_id)
774 .font_size(self.fonts.cyri.scale(14))
775 .color(BLACK)
776 .set(state.ids.sp_arrow_txt_bg, ui);
777 Text::new(&self.localized_strings.get_msg("hud-sp_arrow_txt"))
778 .graphics_for(state.ids.exp_img_frame)
779 .bottom_right_with_margins_on(state.ids.sp_arrow_txt_bg, 1.0, 1.0)
780 .font_id(self.fonts.cyri.conrod_id)
781 .font_size(self.fonts.cyri.scale(14))
782 .color(QUALITY_LEGENDARY)
783 .set(state.ids.sp_arrow_txt, ui);
784 }
785
786 if self
787 .global_state
788 .settings
789 .interface
790 .xp_bar_skillgroup
791 .is_some()
792 {
793 let offset = -81.0;
794 let selected_experience = &self
795 .global_state
796 .settings
797 .interface
798 .xp_bar_skillgroup
799 .unwrap_or(SkillGroupKind::General);
800 let current_exp = self.skillset.available_experience(*selected_experience) as f64;
801 let max_exp = self.skillset.skill_point_cost(*selected_experience) as f64;
802 let exp_percentage = current_exp / max_exp.max(1.0);
803 let level = self.skillset.earned_sp(*selected_experience);
804 let level_txt = if level > 0 {
805 self.skillset.earned_sp(*selected_experience).to_string()
806 } else {
807 "".to_string()
808 };
809
810 Image::new(self.imgs.exp_frame_bg)
812 .w_h(594.0, 8.0)
813 .mid_top_with_margin_on(state.ids.frame, -offset)
814 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.9)))
815 .set(state.ids.exp_frame_bg, ui);
816 Image::new(self.imgs.exp_frame)
817 .w_h(594.0, 8.0)
818 .middle_of(state.ids.exp_frame_bg)
819 .set(state.ids.exp_frame, ui);
820
821 Image::new(self.imgs.bar_content)
822 .w_h(590.0 * exp_percentage, 4.0)
823 .color(Some(XP_COLOR))
824 .top_left_with_margins_on(state.ids.exp_frame, 2.0, 2.0)
825 .set(state.ids.exp_filling, ui);
826 Image::new(self.imgs.selected_exp_bg)
828 .w_h(34.0, 38.0)
829 .top_left_with_margins_on(state.ids.exp_frame, -39.0, 3.0)
830 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
831 .set(state.ids.exp_img_frame_bg, ui);
832
833 if Button::image(self.imgs.selected_exp)
834 .w_h(34.0, 38.0)
835 .middle_of(state.ids.exp_img_frame_bg)
836 .set(state.ids.exp_img_frame, ui)
837 .was_clicked()
838 {
839 events.push(Event::OpenDiary(*selected_experience));
840 }
841
842 Text::new(&level_txt)
843 .mid_bottom_with_margin_on(state.ids.exp_img_frame, 2.0)
844 .font_size(11)
845 .font_id(self.fonts.cyri.conrod_id)
846 .color(QUALITY_LEGENDARY)
847 .graphics_for(state.ids.exp_img_frame)
848 .set(state.ids.exp_lvl, ui);
849
850 Image::new(match selected_experience {
851 SkillGroupKind::General => self.imgs.swords_crossed,
852 SkillGroupKind::Weapon(ToolKind::Sword) => self.imgs.sword,
853 SkillGroupKind::Weapon(ToolKind::Hammer) => self.imgs.hammer,
854 SkillGroupKind::Weapon(ToolKind::Axe) => self.imgs.axe,
855 SkillGroupKind::Weapon(ToolKind::Sceptre) => self.imgs.sceptre,
856 SkillGroupKind::Weapon(ToolKind::Bow) => self.imgs.bow,
857 SkillGroupKind::Weapon(ToolKind::Staff) => self.imgs.staff,
858 SkillGroupKind::Weapon(ToolKind::Pick) => self.imgs.mining,
859 _ => self.imgs.nothing,
860 })
861 .w_h(24.0, 24.0)
862 .graphics_for(state.ids.exp_img_frame)
863 .mid_bottom_with_margin_on(state.ids.exp_img_frame, 13.0)
864 .set(state.ids.exp_img, ui);
865
866 if let Some(diary) = &self
868 .global_state
869 .settings
870 .controls
871 .get_binding(GameInput::Diary)
872 {
873 self.create_new_button_with_shadow(
874 ui,
875 diary,
876 state.ids.exp_img,
877 state.ids.diary_txt_bg,
878 state.ids.diary_txt,
879 );
880 }
881 } else {
882 Image::new(self.imgs.selected_exp_bg)
884 .w_h(34.0, 38.0)
885 .bottom_left_with_margins_on(state.ids.slot1, 0.0, -37.0)
886 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
887 .set(state.ids.exp_img_frame_bg, ui);
888
889 if Button::image(self.imgs.selected_exp)
890 .w_h(34.0, 38.0)
891 .middle_of(state.ids.exp_img_frame_bg)
892 .set(state.ids.exp_img_frame, ui)
893 .was_clicked()
894 {
895 events.push(Event::OpenDiary(SkillGroupKind::General));
896 }
897
898 Image::new(self.imgs.spellbook_ico0)
899 .w_h(24.0, 24.0)
900 .graphics_for(state.ids.exp_img_frame)
901 .mid_bottom_with_margin_on(state.ids.exp_img_frame, 13.0)
902 .set(state.ids.exp_img, ui);
903
904 if let Some(diary) = &self
906 .global_state
907 .settings
908 .controls
909 .get_binding(GameInput::Diary)
910 {
911 self.create_new_button_with_shadow(
912 ui,
913 diary,
914 state.ids.exp_img,
915 state.ids.diary_txt_bg,
916 state.ids.diary_txt,
917 );
918 }
919 }
920
921 let bar_text = if self.health.is_dead {
923 Some((
924 self.localized_strings
925 .get_msg("hud-group-dead")
926 .into_owned(),
927 self.localized_strings
928 .get_msg("hud-group-dead")
929 .into_owned(),
930 self.localized_strings
931 .get_msg("hud-group-dead")
932 .into_owned(),
933 ))
934 } else if let BarNumbers::Values = bar_values {
935 Some((
936 format!(
937 "{}/{}",
938 self.health.current().round().max(1.0) as u32, self.health.maximum().round() as u32
941 ),
942 format!(
943 "{}/{}",
944 self.energy.current().round() as u32,
945 self.energy.maximum().round() as u32
946 ),
947 String::new(), ))
949 } else if let BarNumbers::Percent = bar_values {
950 Some((
951 format!("{}%", hp_percentage as u32),
952 format!("{}%", energy_percentage as u32),
953 String::new(), ))
955 } else {
956 None
957 };
958 if let Some((hp_txt, energy_txt, poise_txt)) = bar_text {
959 let hp_txt = if is_downed { String::new() } else { hp_txt };
960
961 Text::new(&hp_txt)
962 .middle_of(state.ids.frame_health)
963 .font_size(self.fonts.cyri.scale(12))
964 .font_id(self.fonts.cyri.conrod_id)
965 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
966 .set(state.ids.hp_txt_bg, ui);
967 Text::new(&hp_txt)
968 .bottom_left_with_margins_on(state.ids.hp_txt_bg, 2.0, 2.0)
969 .font_size(self.fonts.cyri.scale(12))
970 .font_id(self.fonts.cyri.conrod_id)
971 .color(TEXT_COLOR)
972 .set(state.ids.hp_txt, ui);
973
974 Text::new(&energy_txt)
975 .middle_of(state.ids.frame_energy)
976 .font_size(self.fonts.cyri.scale(12))
977 .font_id(self.fonts.cyri.conrod_id)
978 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
979 .set(state.ids.energy_txt_bg, ui);
980 Text::new(&energy_txt)
981 .bottom_left_with_margins_on(state.ids.energy_txt_bg, 2.0, 2.0)
982 .font_size(self.fonts.cyri.scale(12))
983 .font_id(self.fonts.cyri.conrod_id)
984 .color(TEXT_COLOR)
985 .set(state.ids.energy_txt, ui);
986
987 Text::new(&poise_txt)
988 .middle_of(state.ids.frame_poise)
989 .font_size(self.fonts.cyri.scale(12))
990 .font_id(self.fonts.cyri.conrod_id)
991 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
992 .set(state.ids.poise_txt_bg, ui);
993 Text::new(&poise_txt)
994 .bottom_left_with_margins_on(state.ids.poise_txt_bg, 2.0, 2.0)
995 .font_size(self.fonts.cyri.scale(12))
996 .font_id(self.fonts.cyri.conrod_id)
997 .color(TEXT_COLOR)
998 .set(state.ids.poise_txt, ui);
999 }
1000 }
1001
1002 fn show_slotbar(&mut self, state: &State, ui: &mut UiCell, slot_offset: f64) {
1003 let shortcuts = self.global_state.settings.interface.shortcut_numbers;
1004
1005 let content_source = (
1007 self.hotbar,
1008 self.inventory,
1009 self.energy,
1010 self.skillset,
1011 self.active_abilities,
1012 self.body,
1013 self.combo,
1014 self.char_state,
1015 self.stance,
1016 self.stats,
1017 self.buffs,
1018 );
1019
1020 let image_source = (self.item_imgs, self.imgs);
1021 let mut slot_maker = SlotMaker {
1022 empty_slot: self.imgs.skillbar_slot,
1024 hovered_slot: self.imgs.skillbar_index,
1025 filled_slot: self.imgs.skillbar_slot,
1026 selected_slot: self.imgs.inv_slot_sel,
1027 background_color: None,
1028 content_size: ContentSize {
1029 width_height_ratio: 1.0,
1030 max_fraction: 0.9, },
1033 selected_content_scale: 1.0,
1034 amount_font: self.fonts.cyri.conrod_id,
1035 amount_margins: Vec2::new(1.0, 1.0),
1036 amount_font_size: self.fonts.cyri.scale(12),
1037 amount_text_color: TEXT_COLOR,
1038 content_source: &content_source,
1039 image_source: &image_source,
1040 slot_manager: Some(self.slot_manager),
1041 last_input: &self.global_state.window.last_input(),
1042 pulse: self.pulse,
1043 };
1044
1045 let tooltip = Tooltip::new({
1047 let edge = &self.rot_imgs.tt_side;
1050 let corner = &self.rot_imgs.tt_corner;
1051 ImageFrame::new(
1052 [edge.cw180, edge.none, edge.cw270, edge.cw90],
1053 [corner.none, corner.cw270, corner.cw90, corner.cw180],
1054 Color::Rgba(0.08, 0.07, 0.04, 1.0),
1055 5.0,
1056 )
1057 })
1058 .title_font_size(self.fonts.cyri.scale(15))
1059 .parent(ui.window)
1060 .desc_font_size(self.fonts.cyri.scale(12))
1061 .font_id(self.fonts.cyri.conrod_id)
1062 .desc_text_color(TEXT_COLOR);
1063
1064 let item_tooltip = ItemTooltip::new(
1065 {
1066 let edge = &self.rot_imgs.tt_side;
1069 let corner = &self.rot_imgs.tt_corner;
1070 ImageFrame::new(
1071 [edge.cw180, edge.none, edge.cw270, edge.cw90],
1072 [corner.none, corner.cw270, corner.cw90, corner.cw180],
1073 Color::Rgba(0.08, 0.07, 0.04, 1.0),
1074 5.0,
1075 )
1076 },
1077 self.client,
1078 self.info,
1079 self.imgs,
1080 self.item_imgs,
1081 self.pulse,
1082 self.msm,
1083 self.rbm,
1084 Some(self.inventory),
1085 self.localized_strings,
1086 self.item_i18n,
1087 )
1088 .title_font_size(self.fonts.cyri.scale(20))
1089 .parent(ui.window)
1090 .desc_font_size(self.fonts.cyri.scale(12))
1091 .font_id(self.fonts.cyri.conrod_id)
1092 .desc_text_color(TEXT_COLOR);
1093
1094 let slot_content = |slot| {
1095 let (hotbar, inventory, ..) = content_source;
1096 hotbar.get(slot).and_then(|content| match content {
1097 hotbar::SlotContents::Inventory(i, _) => inventory.get_by_hash(i),
1098 _ => None,
1099 })
1100 };
1101
1102 let tooltip_text = |slot| {
1104 let (hotbar, inventory, _, skill_set, active_abilities, _, combo, _, stance, _, buffs) =
1105 content_source;
1106 hotbar.get(slot).and_then(|content| match content {
1107 hotbar::SlotContents::Inventory(i, _) => inventory.get_by_hash(i).map(|item| {
1108 let (title, desc) =
1109 util::item_text(item, self.localized_strings, self.item_i18n);
1110
1111 (title.into(), desc.into())
1112 }),
1113 hotbar::SlotContents::Ability(i) => active_abilities
1114 .and_then(|a| {
1115 a.auxiliary_set(Some(inventory), Some(skill_set))
1116 .get(i)
1117 .and_then(|a| {
1118 Ability::from(*a).ability_id(
1119 self.char_state,
1120 Some(inventory),
1121 Some(skill_set),
1122 stance,
1123 combo,
1124 buffs,
1125 )
1126 })
1127 })
1128 .map(|id| util::ability_description(id, self.localized_strings)),
1129 })
1130 };
1131
1132 slot_maker.empty_slot = self.imgs.skillbar_slot;
1133 slot_maker.selected_slot = self.imgs.skillbar_slot;
1134
1135 let slots = slot_entries(state, slot_offset);
1136 for entry in slots {
1137 let slot = slot_maker
1138 .fabricate(entry.slot, [40.0; 2], false, false)
1139 .filled_slot(self.imgs.skillbar_slot)
1140 .position(entry.position);
1141 if let Some(item) = slot_content(entry.slot) {
1143 slot.with_item_tooltip(
1144 self.item_tooltip_manager,
1145 core::iter::once(item as &dyn ItemDesc),
1146 &None,
1147 &item_tooltip,
1148 )
1149 .set(entry.widget_id, ui);
1150 } else if let Some((title, desc)) = tooltip_text(entry.slot) {
1152 slot.with_tooltip(self.tooltip_manager, &title, &desc, &tooltip, TEXT_COLOR)
1153 .set(entry.widget_id, ui);
1154 } else {
1156 slot.set(entry.widget_id, ui);
1157 }
1158
1159 match self.global_state.window.last_input() {
1161 LastInput::Controller => {
1162 if self
1164 .global_state
1165 .settings
1166 .controller
1167 .get_game_button_binding(GameInput::CurrentSlot)
1168 .is_some()
1169 || self
1170 .global_state
1171 .settings
1172 .controller
1173 .get_layer_button_binding(GameInput::CurrentSlot)
1174 .is_some()
1175 {
1176 let current_hotbar_selection =
1177 self.hotbar.currently_selected_slot == entry.slot;
1178 if current_hotbar_selection {
1179 let selection_image = self.imgs.skillbar_index;
1180
1181 Image::new(selection_image)
1182 .w_h(42.0, 42.0)
1183 .middle_of(entry.widget_id)
1184 .graphics_for(entry.widget_id)
1185 .set(state.ids.slot_highlight, ui);
1186 }
1187 }
1188 },
1189 LastInput::Keyboard | LastInput::Mouse => {
1190 if self
1192 .global_state
1193 .settings
1194 .controls
1195 .get_binding(GameInput::CurrentSlot)
1196 .is_some()
1197 {
1198 let current_hotbar_selection =
1199 self.hotbar.currently_selected_slot == entry.slot;
1200 if current_hotbar_selection {
1201 let selection_image = self.imgs.skillbar_index;
1202
1203 Image::new(selection_image)
1204 .w_h(42.0, 42.0)
1205 .middle_of(entry.widget_id)
1206 .graphics_for(entry.widget_id)
1207 .set(state.ids.slot_highlight, ui);
1208 }
1209 }
1210 },
1211 }
1212
1213 if let ShortcutNumbers::On = shortcuts
1215 && let Some(key) = &self
1216 .global_state
1217 .settings
1218 .controls
1219 .get_binding(entry.game_input)
1220 {
1221 let position = entry.shortcut_position;
1222 let position_bg = entry.shortcut_position_bg;
1223 let (id, id_bg) = entry.shortcut_widget_ids;
1224
1225 let key_desc = key.display_shortest();
1226 Text::new(&key_desc)
1228 .position(position)
1229 .font_size(self.fonts.cyri.scale(8))
1230 .font_id(self.fonts.cyri.conrod_id)
1231 .color(TEXT_COLOR)
1232 .set(id, ui);
1233 Text::new(&key_desc)
1235 .position(position_bg)
1236 .font_size(self.fonts.cyri.scale(8))
1237 .font_id(self.fonts.cyri.conrod_id)
1238 .color(BLACK)
1239 .set(id_bg, ui);
1240 }
1241 }
1242 let (primary_id, primary_bg, secondary_id, secondary_bg) =
1244 match self.global_state.window.last_input() {
1245 LastInput::Keyboard | LastInput::Mouse => (
1246 state.ids.m1_content,
1247 state.ids.m1_slot_bg,
1248 state.ids.m2_content,
1249 state.ids.m2_slot_bg,
1250 ),
1251 LastInput::Controller => (
1252 state.ids.m2_content,
1253 state.ids.m2_slot_bg,
1254 state.ids.m1_content,
1255 state.ids.m1_slot_bg,
1256 ),
1257 };
1258
1259 Image::new(self.imgs.skillbar_slot)
1261 .w_h(40.0, 40.0)
1262 .right_from(state.ids.slot5, slot_offset)
1263 .set(state.ids.m1_slot_bg, ui);
1264
1265 let primary_ability_id = self.active_abilities.and_then(|a| {
1266 Ability::from(a.primary).ability_id(
1267 self.char_state,
1268 Some(self.inventory),
1269 Some(self.skillset),
1270 self.stance,
1271 self.combo,
1272 self.buffs,
1273 )
1274 });
1275
1276 let (primary_ability_title, primary_ability_desc) =
1277 util::ability_description(primary_ability_id.unwrap_or(""), self.localized_strings);
1278
1279 Button::image(
1280 primary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
1281 )
1282 .w_h(36.0, 36.0)
1283 .middle_of(primary_bg)
1284 .with_tooltip(
1285 self.tooltip_manager,
1286 &primary_ability_title,
1287 &primary_ability_desc,
1288 &tooltip,
1289 TEXT_COLOR,
1290 )
1291 .set(primary_id, ui);
1292 Image::new(self.imgs.skillbar_slot)
1294 .w_h(40.0, 40.0)
1295 .right_from(state.ids.m1_slot_bg, slot_offset)
1296 .set(state.ids.m2_slot_bg, ui);
1297
1298 let secondary_ability_id = self.active_abilities.and_then(|a| {
1299 Ability::from(a.secondary).ability_id(
1300 self.char_state,
1301 Some(self.inventory),
1302 Some(self.skillset),
1303 self.stance,
1304 self.combo,
1305 self.buffs,
1306 )
1307 });
1308
1309 let (secondary_ability_title, secondary_ability_desc) =
1310 util::ability_description(secondary_ability_id.unwrap_or(""), self.localized_strings);
1311
1312 Button::image(
1313 secondary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
1314 )
1315 .w_h(36.0, 36.0)
1316 .middle_of(secondary_bg)
1317 .image_color(
1318 if self
1319 .active_abilities
1320 .and_then(|a| {
1321 a.activate_ability(
1322 AbilityInput::Secondary,
1323 Some(self.inventory),
1324 self.skillset,
1325 Some(self.body),
1326 self.char_state,
1327 self.stance,
1328 self.combo,
1329 self.stats,
1330 self.buffs,
1331 )
1332 })
1333 .is_some_and(|(a, _, _)| {
1334 self.energy.current() >= a.energy_cost()
1335 && self.combo.is_some_and(|c| c.counter() >= a.combo_cost())
1336 && a.ability_meta()
1337 .requirements
1338 .requirements_met(self.stance, Some(self.inventory))
1339 })
1340 {
1341 Color::Rgba(1.0, 1.0, 1.0, 1.0)
1342 } else {
1343 Color::Rgba(0.3, 0.3, 0.3, 0.8)
1344 },
1345 )
1346 .with_tooltip(
1347 self.tooltip_manager,
1348 &secondary_ability_title,
1349 &secondary_ability_desc,
1350 &tooltip,
1351 TEXT_COLOR,
1352 )
1353 .set(secondary_id, ui);
1354
1355 match self.global_state.window.last_input() {
1357 LastInput::Keyboard | LastInput::Mouse => {
1358 Image::new(self.imgs.m1_ico)
1359 .w_h(16.0, 18.0)
1360 .mid_bottom_with_margin_on(state.ids.m1_content, -11.0)
1361 .set(state.ids.m1_ico, ui);
1362 Image::new(self.imgs.m2_ico)
1363 .w_h(16.0, 18.0)
1364 .mid_bottom_with_margin_on(state.ids.m2_content, -11.0)
1365 .set(state.ids.m2_ico, ui);
1366 },
1367 LastInput::Controller => {
1368 Image::new(icon_utils::fetch_skillbar_gamepad_left(
1369 self.global_state.window.controller_type(),
1370 self.imgs,
1371 ))
1372 .w_h(18.0, 18.0)
1373 .mid_bottom_with_margin_on(state.ids.m1_content, -11.0)
1374 .set(state.ids.m1_ico, ui);
1375 Image::new(icon_utils::fetch_skillbar_gamepad_right(
1376 self.global_state.window.controller_type(),
1377 self.imgs,
1378 ))
1379 .w_h(18.0, 18.0)
1380 .mid_bottom_with_margin_on(state.ids.m2_content, -11.0)
1381 .set(state.ids.m2_ico, ui);
1382 },
1383 }
1384 }
1385
1386 fn show_combo_counter(&self, combo_floater: ComboFloater, state: &State, ui: &mut UiCell) {
1387 if combo_floater.combo > 0 {
1388 let combo_txt = format!("{} Combo", combo_floater.combo);
1389 let combo_cnt = combo_floater.combo as f32;
1390 let time_since_last_update = comp::combo::COMBO_DECAY_START - combo_floater.timer;
1391 let alpha = (1.0 - time_since_last_update * 0.2).min(1.0) as f32;
1392 let fnt_col = Color::Rgba(
1393 (1.0 - combo_cnt / (combo_cnt + 20.0)).max(0.79),
1395 (1.0 - combo_cnt / (combo_cnt + 80.0)).max(0.19),
1396 (1.0 - combo_cnt / (combo_cnt + 5.0)).max(0.17),
1397 alpha,
1398 );
1399 let fnt_size = ((14.0 + combo_floater.timer as f32 * 0.8).min(30.0)) as u32
1402 + if (time_since_last_update) < 0.1 { 2 } else { 0 };
1403
1404 Rectangle::fill_with([10.0, 10.0], color::TRANSPARENT)
1405 .middle_of(ui.window)
1406 .set(state.ids.combo_align, ui);
1407
1408 Text::new(combo_txt.as_str())
1409 .mid_bottom_with_margin_on(
1410 state.ids.combo_align,
1411 -350.0 + time_since_last_update * -8.0,
1412 )
1413 .font_size(self.fonts.cyri.scale(fnt_size))
1414 .font_id(self.fonts.cyri.conrod_id)
1415 .color(Color::Rgba(0.0, 0.0, 0.0, alpha))
1416 .set(state.ids.combo_bg, ui);
1417 Text::new(combo_txt.as_str())
1418 .bottom_right_with_margins_on(state.ids.combo_bg, 1.0, 1.0)
1419 .font_size(self.fonts.cyri.scale(fnt_size))
1420 .font_id(self.fonts.cyri.conrod_id)
1421 .color(fnt_col)
1422 .set(state.ids.combo, ui);
1423 }
1424 }
1425}
1426
1427pub struct State {
1428 ids: Ids,
1429}
1430
1431impl Widget for Skillbar<'_> {
1432 type Event = Vec<Event>;
1433 type State = State;
1434 type Style = ();
1435
1436 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
1437 State {
1438 ids: Ids::new(id_gen),
1439 }
1440 }
1441
1442 fn style(&self) -> Self::Style {}
1443
1444 fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
1445 common_base::prof_span!("Skillbar::update");
1446 let widget::UpdateArgs { state, ui, .. } = args;
1447
1448 let mut events = Vec::new();
1449
1450 let slot_offset = 3.0;
1451
1452 if self.health.is_dead {
1454 self.show_death_message(state, ui);
1455 }
1456 else if comp::is_downed(Some(self.health), self.client.current().as_ref()) {
1458 self.show_give_up_message(state, ui);
1459 }
1460
1461 state.update(|s| {
1465 s.ids.poise_ticks.resize(
1466 self::Poise::POISE_THRESHOLDS.len(),
1467 &mut ui.widget_id_generator(),
1468 )
1469 });
1470
1471 let alignment_size = 40.0 * 12.0 + slot_offset * 11.0;
1473 Rectangle::fill_with([alignment_size, 80.0], color::TRANSPARENT)
1474 .mid_bottom_with_margin_on(ui.window, 10.0)
1475 .set(state.ids.frame, ui);
1476
1477 self.show_stat_bars(state, ui, &mut events);
1479
1480 self.show_slotbar(state, ui, slot_offset);
1482
1483 if let Some(combo_floater) = self.combo_floater {
1485 self.show_combo_counter(combo_floater, state, ui);
1486 }
1487 events
1488 }
1489}