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