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_layout = &self.global_state.window.key_layout;
408 let key_desc = key_mouse.display_shortest(key_layout);
409
410 Text::new(&key_desc)
412 .bottom_right_with_margins_on(button_identifier, 0.0, 0.0)
413 .font_size(10)
414 .font_id(self.fonts.cyri.conrod_id)
415 .color(BLACK)
416 .set(text_background, ui);
417
418 Text::new(&key_desc)
420 .bottom_right_with_margins_on(text_background, 1.0, 1.0)
421 .font_size(10)
422 .font_id(self.fonts.cyri.conrod_id)
423 .color(TEXT_COLOR)
424 .set(text, ui);
425 }
426
427 fn show_give_up_message(&self, state: &State, ui: &mut UiCell) {
428 let localized_strings = self.localized_strings;
429 let key_layout = &self.global_state.window.key_layout;
430 let hardcore = self.client.current::<Hardcore>().is_some();
431
432 if let Some(key) = self
433 .global_state
434 .settings
435 .controls
436 .get_binding(GameInput::GiveUp)
437 {
438 let respawn_msg =
439 localized_strings.get_msg_ctx("hud-press_key_to_give_up", &i18n::fluent_args! {
440 "key" => key.display_string(key_layout)
441 });
442 let penalty_msg = if hardcore {
443 self.localized_strings
444 .get_msg("hud-hardcore_will_char_deleted")
445 } else {
446 self.localized_strings.get_msg("hud-items_will_lose_dur")
447 };
448
449 let recieving_help_msg = localized_strings.get_msg("hud-downed_recieving_help");
450 Text::new(&penalty_msg)
451 .mid_bottom_with_margin_on(ui.window, 180.0)
452 .font_size(self.fonts.cyri.scale(30))
453 .font_id(self.fonts.cyri.conrod_id)
454 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
455 .set(state.ids.death_message_3_bg, ui);
456 Text::new(&respawn_msg)
457 .mid_top_with_margin_on(state.ids.death_message_3_bg, -50.0)
458 .font_size(self.fonts.cyri.scale(30))
459 .font_id(self.fonts.cyri.conrod_id)
460 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
461 .set(state.ids.death_message_2_bg, ui);
462 Text::new(&penalty_msg)
463 .bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
464 .font_size(self.fonts.cyri.scale(30))
465 .font_id(self.fonts.cyri.conrod_id)
466 .color(TEXT_COLOR)
467 .set(state.ids.death_message_3, ui);
468 Text::new(&respawn_msg)
469 .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0)
470 .font_size(self.fonts.cyri.scale(30))
471 .font_id(self.fonts.cyri.conrod_id)
472 .color(TEXT_COLOR)
473 .set(state.ids.death_message_2, ui);
474 if self
475 .client
476 .state()
477 .read_storage::<common::interaction::Interactors>()
478 .get(self.client.entity())
479 .is_some_and(|interactors| {
480 interactors.has_interaction(common::interaction::InteractionKind::HelpDowned)
481 })
482 {
483 Text::new(&recieving_help_msg)
484 .mid_top_with_margin_on(state.ids.death_message_2_bg, -50.0)
485 .font_size(self.fonts.cyri.scale(24))
486 .font_id(self.fonts.cyri.conrod_id)
487 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
488 .set(state.ids.death_message_1_bg, ui);
489 Text::new(&recieving_help_msg)
490 .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
491 .font_size(self.fonts.cyri.scale(24))
492 .font_id(self.fonts.cyri.conrod_id)
493 .color(HP_COLOR)
494 .set(state.ids.death_message_1, ui);
495 }
496 }
497 }
498
499 fn show_death_message(&self, state: &State, ui: &mut UiCell) {
500 let localized_strings = self.localized_strings;
501 let key_layout = &self.global_state.window.key_layout;
502 let hardcore = self.client.current::<Hardcore>().is_some();
503
504 if let Some(key) = self
505 .global_state
506 .settings
507 .controls
508 .get_binding(GameInput::Respawn)
509 {
510 Text::new(&self.localized_strings.get_msg("hud-you_died"))
511 .middle_of(ui.window)
512 .font_size(self.fonts.cyri.scale(50))
513 .font_id(self.fonts.cyri.conrod_id)
514 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
515 .set(state.ids.death_message_1_bg, ui);
516 let respawn_msg = if hardcore {
517 localized_strings.get_msg_ctx(
518 "hud-press_key_to_return_to_char_menu",
519 &i18n::fluent_args! {
520 "key" => key.display_string(key_layout)
521 },
522 )
523 } else {
524 localized_strings.get_msg_ctx("hud-press_key_to_respawn", &i18n::fluent_args! {
525 "key" => key.display_string(key_layout)
526 })
527 };
528 let penalty_msg = if hardcore {
529 self.localized_strings.get_msg("hud-hardcore_char_deleted")
530 } else {
531 self.localized_strings.get_msg("hud-items_lost_dur")
532 };
533 Text::new(&respawn_msg)
534 .mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0)
535 .font_size(self.fonts.cyri.scale(30))
536 .font_id(self.fonts.cyri.conrod_id)
537 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
538 .set(state.ids.death_message_2_bg, ui);
539 Text::new(&penalty_msg)
540 .mid_bottom_with_margin_on(state.ids.death_message_2_bg, -50.0)
541 .font_size(self.fonts.cyri.scale(30))
542 .font_id(self.fonts.cyri.conrod_id)
543 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
544 .set(state.ids.death_message_3_bg, ui);
545 Text::new(&self.localized_strings.get_msg("hud-you_died"))
546 .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
547 .font_size(self.fonts.cyri.scale(50))
548 .font_id(self.fonts.cyri.conrod_id)
549 .color(CRITICAL_HP_COLOR)
550 .set(state.ids.death_message_1, ui);
551 Text::new(&respawn_msg)
552 .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0)
553 .font_size(self.fonts.cyri.scale(30))
554 .font_id(self.fonts.cyri.conrod_id)
555 .color(CRITICAL_HP_COLOR)
556 .set(state.ids.death_message_2, ui);
557 Text::new(&penalty_msg)
558 .bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
559 .font_size(self.fonts.cyri.scale(30))
560 .font_id(self.fonts.cyri.conrod_id)
561 .color(CRITICAL_HP_COLOR)
562 .set(state.ids.death_message_3, ui);
563 }
564 }
565
566 fn show_stat_bars(&self, state: &State, ui: &mut UiCell) -> Option<Event> {
567 let (hp_percentage, energy_percentage, poise_percentage): (f64, f64, f64) =
568 if self.health.is_dead {
569 (0.0, 0.0, 0.0)
570 } else {
571 let max_hp = f64::from(self.health.base_max().max(self.health.maximum()));
572 let current_hp = f64::from(self.health.current());
573 (
574 current_hp / max_hp * 100.0,
575 f64::from(self.energy.fraction() * 100.0),
576 f64::from(self.poise.fraction() * 100.0),
577 )
578 };
579
580 let hp_ani = (self.pulse * 4.0).cos() * 0.5 + 0.8;
582 let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
583 let bar_values = self.global_state.settings.interface.bar_numbers;
584 let is_downed = is_downed(Some(self.health), self.char_state);
585 let show_health = self.global_state.settings.interface.always_show_bars
586 || is_downed
587 || (self.health.current() - self.health.maximum()).abs() > Health::HEALTH_EPSILON;
588 let show_energy = self.global_state.settings.interface.always_show_bars
589 || (self.energy.current() - self.energy.maximum()).abs() > Energy::ENERGY_EPSILON;
590 let show_poise = self.global_state.settings.interface.enable_poise_bar
591 && (self.global_state.settings.interface.always_show_bars
592 || (self.poise.current() - self.poise.maximum()).abs() > Poise::POISE_EPSILON);
593 let decayed_health = 1.0 - self.health.maximum() as f64 / self.health.base_max() as f64;
594
595 if show_health && !self.health.is_dead || decayed_health > 0.0 {
596 let offset = 1.0;
597 let hp_percentage = if is_downed {
598 100.0
599 * (1.0 - self.info.key_state.give_up.unwrap_or(0.0) / GIVE_UP_HOLD_TIME)
600 .clamp(0.0, 1.0) as f64
601 } else {
602 hp_percentage
603 };
604
605 Image::new(self.imgs.health_bg)
606 .w_h(484.0, 24.0)
607 .mid_top_with_margin_on(state.ids.frame, -offset)
608 .set(state.ids.bg_health, ui);
609 Rectangle::fill_with([480.0, 18.0], color::TRANSPARENT)
610 .top_left_with_margins_on(state.ids.bg_health, 2.0, 2.0)
611 .set(state.ids.hp_alignment, ui);
612 let health_col = match hp_percentage as u8 {
613 _ if is_downed => crit_hp_color,
614 0..=20 => crit_hp_color,
615 21..=40 => LOW_HP_COLOR,
616 _ => HP_COLOR,
617 };
618 Image::new(self.imgs.bar_content)
619 .w_h(480.0 * hp_percentage / 100.0, 18.0)
620 .color(Some(health_col))
621 .top_left_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
622 .set(state.ids.hp_filling, ui);
623
624 if decayed_health > 0.0 {
625 let decay_bar_len = 480.0 * decayed_health;
626 Image::new(self.imgs.bar_content)
627 .w_h(decay_bar_len, 18.0)
628 .color(Some(QUALITY_EPIC))
629 .top_right_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
630 .crop_kids()
631 .set(state.ids.hp_decayed, ui);
632
633 Image::new(self.imgs.decayed_bg)
634 .w_h(480.0, 18.0)
635 .color(Some(Color::Rgba(0.58, 0.29, 0.93, (hp_ani + 0.6).min(1.0))))
636 .top_left_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
637 .parent(state.ids.hp_decayed)
638 .set(state.ids.decay_overlay, ui);
639 }
640 Image::new(self.imgs.health_frame)
641 .w_h(484.0, 24.0)
642 .color(Some(UI_HIGHLIGHT_0))
643 .middle_of(state.ids.bg_health)
644 .set(state.ids.frame_health, ui);
645 }
646 if show_energy && !self.health.is_dead {
647 let offset = if show_health || decayed_health > 0.0 {
648 34.0
649 } else {
650 1.0
651 };
652 Image::new(self.imgs.energy_bg)
653 .w_h(323.0, 16.0)
654 .mid_top_with_margin_on(state.ids.frame, -offset)
655 .set(state.ids.bg_energy, ui);
656 Rectangle::fill_with([319.0, 10.0], color::TRANSPARENT)
657 .top_left_with_margins_on(state.ids.bg_energy, 2.0, 2.0)
658 .set(state.ids.energy_alignment, ui);
659 Image::new(self.imgs.bar_content)
660 .w_h(319.0 * energy_percentage / 100.0, 10.0)
661 .color(Some(STAMINA_COLOR))
662 .top_left_with_margins_on(state.ids.energy_alignment, 0.0, 0.0)
663 .set(state.ids.energy_filling, ui);
664 Image::new(self.imgs.energy_frame)
665 .w_h(323.0, 16.0)
666 .color(Some(UI_HIGHLIGHT_0))
667 .middle_of(state.ids.bg_energy)
668 .set(state.ids.frame_energy, ui);
669 }
670 if show_poise && !self.health.is_dead {
671 let offset = 17.0;
672
673 let poise_colour = match self.poise.previous_state {
674 self::PoiseState::KnockedDown => BLACK,
675 self::PoiseState::Dazed => Color::Rgba(0.25, 0.0, 0.15, 1.0),
676 self::PoiseState::Stunned => Color::Rgba(0.40, 0.0, 0.30, 1.0),
677 self::PoiseState::Interrupted => Color::Rgba(0.55, 0.0, 0.45, 1.0),
678 _ => POISE_COLOR,
679 };
680
681 Image::new(self.imgs.poise_bg)
682 .w_h(323.0, 14.0)
683 .mid_top_with_margin_on(state.ids.frame, -offset)
684 .set(state.ids.bg_poise, ui);
685 Rectangle::fill_with([319.0, 10.0], color::TRANSPARENT)
686 .top_left_with_margins_on(state.ids.bg_poise, 2.0, 2.0)
687 .set(state.ids.poise_alignment, ui);
688 Image::new(self.imgs.bar_content)
689 .w_h(319.0 * poise_percentage / 100.0, 10.0)
690 .color(Some(poise_colour))
691 .top_left_with_margins_on(state.ids.poise_alignment, 0.0, 0.0)
692 .set(state.ids.poise_filling, ui);
693 for i in 0..state.ids.poise_ticks.len() {
694 Image::new(self.imgs.poise_tick)
695 .w_h(3.0, 10.0)
696 .color(Some(POISEBAR_TICK_COLOR))
697 .top_left_with_margins_on(
698 state.ids.poise_alignment,
699 0.0,
700 319.0f64 * (self::Poise::POISE_THRESHOLDS[i] / self.poise.maximum()) as f64,
701 )
702 .set(state.ids.poise_ticks[i], ui);
703 }
704 Image::new(self.imgs.poise_frame)
705 .w_h(323.0, 16.0)
706 .color(Some(UI_HIGHLIGHT_0))
707 .middle_of(state.ids.bg_poise)
708 .set(state.ids.frame_poise, ui);
709 }
710 Image::new(self.imgs.selected_exp_bg)
712 .w_h(34.0, 38.0)
713 .bottom_right_with_margins_on(state.ids.slot10, 0.0, -37.0)
714 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
715 .set(state.ids.bag_img_frame_bg, ui);
716
717 if Button::image(self.imgs.bag_frame)
718 .w_h(34.0, 38.0)
719 .middle_of(state.ids.bag_img_frame_bg)
720 .set(state.ids.bag_img_frame, ui)
721 .was_clicked()
722 {
723 return Some(Event::OpenBag);
724 }
725 let invs = self.client.inventories();
726 let inventory = invs.get(self.info.viewpoint_entity)?;
727
728 let space_used = inventory.populated_slots();
729 let space_max = inventory.slots().count();
730 let bag_space = format!("{}/{}", space_used, space_max);
731 let bag_space_percentage = space_used as f64 / space_max as f64;
732
733 Image::new(self.imgs.bar_content)
735 .w_h(1.0, 21.0 * bag_space_percentage)
736 .color(if bag_space_percentage < 0.6 {
737 Some(TEXT_VELORITE)
738 } else if bag_space_percentage < 1.0 {
739 Some(LOW_HP_COLOR)
740 } else {
741 Some(CRITICAL_HP_COLOR)
742 })
743 .graphics_for(state.ids.bag_img_frame)
744 .bottom_left_with_margins_on(state.ids.bag_img_frame, 14.0, 2.0)
745 .set(state.ids.bag_filling, ui);
746
747 Rectangle::fill_with([32.0, 11.0], color::TRANSPARENT)
749 .bottom_left_with_margins_on(state.ids.bag_img_frame_bg, 1.0, 2.0)
750 .graphics_for(state.ids.bag_img_frame)
751 .set(state.ids.bag_numbers_alignment, ui);
752 Text::new(&bag_space)
753 .middle_of(state.ids.bag_numbers_alignment)
754 .font_size(if bag_space.len() < 6 { 9 } else { 8 })
755 .font_id(self.fonts.cyri.conrod_id)
756 .color(BLACK)
757 .graphics_for(state.ids.bag_img_frame)
758 .set(state.ids.bag_space_bg, ui);
759 Text::new(&bag_space)
760 .bottom_right_with_margins_on(state.ids.bag_space_bg, 1.0, 1.0)
761 .font_size(if bag_space.len() < 6 { 9 } else { 8 })
762 .font_id(self.fonts.cyri.conrod_id)
763 .color(if bag_space_percentage < 0.6 {
764 TEXT_VELORITE
765 } else if bag_space_percentage < 1.0 {
766 LOW_HP_COLOR
767 } else {
768 CRITICAL_HP_COLOR
769 })
770 .graphics_for(state.ids.bag_img_frame)
771 .set(state.ids.bag_space, ui);
772
773 Image::new(self.imgs.bag_ico)
774 .w_h(24.0, 24.0)
775 .graphics_for(state.ids.bag_img_frame)
776 .mid_bottom_with_margin_on(state.ids.bag_img_frame, 13.0)
777 .set(state.ids.bag_img, ui);
778
779 if let Some(bag) = &self
780 .global_state
781 .settings
782 .controls
783 .get_binding(GameInput::Inventory)
784 {
785 self.create_new_button_with_shadow(
786 ui,
787 bag,
788 state.ids.bag_img,
789 state.ids.bag_text_bg,
790 state.ids.bag_text,
791 );
792 }
793
794 let unspent_sp = self.skillset.has_available_sp();
798 if unspent_sp {
799 let arrow_ani = animation_timer(self.pulse); Image::new(self.imgs.sp_indicator_arrow)
801 .w_h(20.0, 11.0)
802 .graphics_for(state.ids.exp_img_frame)
803 .mid_top_with_margin_on(state.ids.exp_img_frame, -12.0 + arrow_ani as f64)
804 .color(Some(QUALITY_LEGENDARY))
805 .set(state.ids.sp_arrow, ui);
806 Text::new(&self.localized_strings.get_msg("hud-sp_arrow_txt"))
807 .mid_top_with_margin_on(state.ids.sp_arrow, -18.0)
808 .graphics_for(state.ids.exp_img_frame)
809 .font_id(self.fonts.cyri.conrod_id)
810 .font_size(self.fonts.cyri.scale(14))
811 .color(BLACK)
812 .set(state.ids.sp_arrow_txt_bg, ui);
813 Text::new(&self.localized_strings.get_msg("hud-sp_arrow_txt"))
814 .graphics_for(state.ids.exp_img_frame)
815 .bottom_right_with_margins_on(state.ids.sp_arrow_txt_bg, 1.0, 1.0)
816 .font_id(self.fonts.cyri.conrod_id)
817 .font_size(self.fonts.cyri.scale(14))
818 .color(QUALITY_LEGENDARY)
819 .set(state.ids.sp_arrow_txt, ui);
820 }
821
822 if self
823 .global_state
824 .settings
825 .interface
826 .xp_bar_skillgroup
827 .is_some()
828 {
829 let offset = -81.0;
830 let selected_experience = &self
831 .global_state
832 .settings
833 .interface
834 .xp_bar_skillgroup
835 .unwrap_or(SkillGroupKind::General);
836 let current_exp = self.skillset.available_experience(*selected_experience) as f64;
837 let max_exp = self.skillset.skill_point_cost(*selected_experience) as f64;
838 let exp_percentage = current_exp / max_exp.max(1.0);
839 let level = self.skillset.earned_sp(*selected_experience);
840 let level_txt = if level > 0 {
841 self.skillset.earned_sp(*selected_experience).to_string()
842 } else {
843 "".to_string()
844 };
845
846 Image::new(self.imgs.exp_frame_bg)
848 .w_h(594.0, 8.0)
849 .mid_top_with_margin_on(state.ids.frame, -offset)
850 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.9)))
851 .set(state.ids.exp_frame_bg, ui);
852 Image::new(self.imgs.exp_frame)
853 .w_h(594.0, 8.0)
854 .middle_of(state.ids.exp_frame_bg)
855 .set(state.ids.exp_frame, ui);
856
857 Image::new(self.imgs.bar_content)
858 .w_h(590.0 * exp_percentage, 4.0)
859 .color(Some(XP_COLOR))
860 .top_left_with_margins_on(state.ids.exp_frame, 2.0, 2.0)
861 .set(state.ids.exp_filling, ui);
862 Image::new(self.imgs.selected_exp_bg)
864 .w_h(34.0, 38.0)
865 .top_left_with_margins_on(state.ids.exp_frame, -39.0, 3.0)
866 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
867 .set(state.ids.exp_img_frame_bg, ui);
868
869 if Button::image(self.imgs.selected_exp)
870 .w_h(34.0, 38.0)
871 .middle_of(state.ids.exp_img_frame_bg)
872 .set(state.ids.exp_img_frame, ui)
873 .was_clicked()
874 {
875 return Some(Event::OpenDiary(*selected_experience));
876 }
877
878 Text::new(&level_txt)
879 .mid_bottom_with_margin_on(state.ids.exp_img_frame, 2.0)
880 .font_size(11)
881 .font_id(self.fonts.cyri.conrod_id)
882 .color(QUALITY_LEGENDARY)
883 .graphics_for(state.ids.exp_img_frame)
884 .set(state.ids.exp_lvl, ui);
885
886 Image::new(match selected_experience {
887 SkillGroupKind::General => self.imgs.swords_crossed,
888 SkillGroupKind::Weapon(ToolKind::Sword) => self.imgs.sword,
889 SkillGroupKind::Weapon(ToolKind::Hammer) => self.imgs.hammer,
890 SkillGroupKind::Weapon(ToolKind::Axe) => self.imgs.axe,
891 SkillGroupKind::Weapon(ToolKind::Sceptre) => self.imgs.sceptre,
892 SkillGroupKind::Weapon(ToolKind::Bow) => self.imgs.bow,
893 SkillGroupKind::Weapon(ToolKind::Staff) => self.imgs.staff,
894 SkillGroupKind::Weapon(ToolKind::Pick) => self.imgs.mining,
895 _ => self.imgs.nothing,
896 })
897 .w_h(24.0, 24.0)
898 .graphics_for(state.ids.exp_img_frame)
899 .mid_bottom_with_margin_on(state.ids.exp_img_frame, 13.0)
900 .set(state.ids.exp_img, ui);
901
902 if let Some(diary) = &self
904 .global_state
905 .settings
906 .controls
907 .get_binding(GameInput::Diary)
908 {
909 self.create_new_button_with_shadow(
910 ui,
911 diary,
912 state.ids.exp_img,
913 state.ids.diary_txt_bg,
914 state.ids.diary_txt,
915 );
916 }
917 } else {
918 Image::new(self.imgs.selected_exp_bg)
920 .w_h(34.0, 38.0)
921 .bottom_left_with_margins_on(state.ids.slot1, 0.0, -37.0)
922 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
923 .set(state.ids.exp_img_frame_bg, ui);
924
925 if Button::image(self.imgs.selected_exp)
926 .w_h(34.0, 38.0)
927 .middle_of(state.ids.exp_img_frame_bg)
928 .set(state.ids.exp_img_frame, ui)
929 .was_clicked()
930 {
931 return Some(Event::OpenDiary(SkillGroupKind::General));
932 }
933
934 Image::new(self.imgs.spellbook_ico0)
935 .w_h(24.0, 24.0)
936 .graphics_for(state.ids.exp_img_frame)
937 .mid_bottom_with_margin_on(state.ids.exp_img_frame, 13.0)
938 .set(state.ids.exp_img, ui);
939
940 if let Some(diary) = &self
942 .global_state
943 .settings
944 .controls
945 .get_binding(GameInput::Diary)
946 {
947 self.create_new_button_with_shadow(
948 ui,
949 diary,
950 state.ids.exp_img,
951 state.ids.diary_txt_bg,
952 state.ids.diary_txt,
953 );
954 }
955 }
956
957 let bar_text = if self.health.is_dead {
959 Some((
960 self.localized_strings
961 .get_msg("hud-group-dead")
962 .into_owned(),
963 self.localized_strings
964 .get_msg("hud-group-dead")
965 .into_owned(),
966 self.localized_strings
967 .get_msg("hud-group-dead")
968 .into_owned(),
969 ))
970 } else if let BarNumbers::Values = bar_values {
971 Some((
972 format!(
973 "{}/{}",
974 self.health.current().round().max(1.0) as u32, self.health.maximum().round() as u32
977 ),
978 format!(
979 "{}/{}",
980 self.energy.current().round() as u32,
981 self.energy.maximum().round() as u32
982 ),
983 String::new(), ))
985 } else if let BarNumbers::Percent = bar_values {
986 Some((
987 format!("{}%", hp_percentage as u32),
988 format!("{}%", energy_percentage as u32),
989 String::new(), ))
991 } else {
992 None
993 };
994 if let Some((hp_txt, energy_txt, poise_txt)) = bar_text {
995 let hp_txt = if is_downed { String::new() } else { hp_txt };
996
997 Text::new(&hp_txt)
998 .middle_of(state.ids.frame_health)
999 .font_size(self.fonts.cyri.scale(12))
1000 .font_id(self.fonts.cyri.conrod_id)
1001 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
1002 .set(state.ids.hp_txt_bg, ui);
1003 Text::new(&hp_txt)
1004 .bottom_left_with_margins_on(state.ids.hp_txt_bg, 2.0, 2.0)
1005 .font_size(self.fonts.cyri.scale(12))
1006 .font_id(self.fonts.cyri.conrod_id)
1007 .color(TEXT_COLOR)
1008 .set(state.ids.hp_txt, ui);
1009
1010 Text::new(&energy_txt)
1011 .middle_of(state.ids.frame_energy)
1012 .font_size(self.fonts.cyri.scale(12))
1013 .font_id(self.fonts.cyri.conrod_id)
1014 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
1015 .set(state.ids.energy_txt_bg, ui);
1016 Text::new(&energy_txt)
1017 .bottom_left_with_margins_on(state.ids.energy_txt_bg, 2.0, 2.0)
1018 .font_size(self.fonts.cyri.scale(12))
1019 .font_id(self.fonts.cyri.conrod_id)
1020 .color(TEXT_COLOR)
1021 .set(state.ids.energy_txt, ui);
1022
1023 Text::new(&poise_txt)
1024 .middle_of(state.ids.frame_poise)
1025 .font_size(self.fonts.cyri.scale(12))
1026 .font_id(self.fonts.cyri.conrod_id)
1027 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
1028 .set(state.ids.poise_txt_bg, ui);
1029 Text::new(&poise_txt)
1030 .bottom_left_with_margins_on(state.ids.poise_txt_bg, 2.0, 2.0)
1031 .font_size(self.fonts.cyri.scale(12))
1032 .font_id(self.fonts.cyri.conrod_id)
1033 .color(TEXT_COLOR)
1034 .set(state.ids.poise_txt, ui);
1035 }
1036 None
1037 }
1038
1039 fn show_slotbar(&mut self, state: &State, ui: &mut UiCell, slot_offset: f64) {
1040 let shortcuts = self.global_state.settings.interface.shortcut_numbers;
1041 let key_layout = &self.global_state.window.key_layout;
1042
1043 let content_source = (
1045 self.hotbar,
1046 self.inventory,
1047 self.energy,
1048 self.skillset,
1049 self.active_abilities,
1050 self.body,
1051 self.context,
1052 self.combo,
1053 self.char_state,
1054 self.stance,
1055 self.stats,
1056 );
1057
1058 let image_source = (self.item_imgs, self.imgs);
1059 let mut slot_maker = SlotMaker {
1060 empty_slot: self.imgs.skillbar_slot,
1062 filled_slot: self.imgs.skillbar_slot,
1063 selected_slot: self.imgs.inv_slot_sel,
1064 background_color: None,
1065 content_size: ContentSize {
1066 width_height_ratio: 1.0,
1067 max_fraction: 0.9, },
1070 selected_content_scale: 1.0,
1071 amount_font: self.fonts.cyri.conrod_id,
1072 amount_margins: Vec2::new(1.0, 1.0),
1073 amount_font_size: self.fonts.cyri.scale(12),
1074 amount_text_color: TEXT_COLOR,
1075 content_source: &content_source,
1076 image_source: &image_source,
1077 slot_manager: Some(self.slot_manager),
1078 pulse: self.pulse,
1079 };
1080
1081 let tooltip = Tooltip::new({
1083 let edge = &self.rot_imgs.tt_side;
1086 let corner = &self.rot_imgs.tt_corner;
1087 ImageFrame::new(
1088 [edge.cw180, edge.none, edge.cw270, edge.cw90],
1089 [corner.none, corner.cw270, corner.cw90, corner.cw180],
1090 Color::Rgba(0.08, 0.07, 0.04, 1.0),
1091 5.0,
1092 )
1093 })
1094 .title_font_size(self.fonts.cyri.scale(15))
1095 .parent(ui.window)
1096 .desc_font_size(self.fonts.cyri.scale(12))
1097 .font_id(self.fonts.cyri.conrod_id)
1098 .desc_text_color(TEXT_COLOR);
1099
1100 let item_tooltip = ItemTooltip::new(
1101 {
1102 let edge = &self.rot_imgs.tt_side;
1105 let corner = &self.rot_imgs.tt_corner;
1106 ImageFrame::new(
1107 [edge.cw180, edge.none, edge.cw270, edge.cw90],
1108 [corner.none, corner.cw270, corner.cw90, corner.cw180],
1109 Color::Rgba(0.08, 0.07, 0.04, 1.0),
1110 5.0,
1111 )
1112 },
1113 self.client,
1114 self.info,
1115 self.imgs,
1116 self.item_imgs,
1117 self.pulse,
1118 self.msm,
1119 self.rbm,
1120 Some(self.inventory),
1121 self.localized_strings,
1122 self.item_i18n,
1123 )
1124 .title_font_size(self.fonts.cyri.scale(20))
1125 .parent(ui.window)
1126 .desc_font_size(self.fonts.cyri.scale(12))
1127 .font_id(self.fonts.cyri.conrod_id)
1128 .desc_text_color(TEXT_COLOR);
1129
1130 let slot_content = |slot| {
1131 let (hotbar, inventory, ..) = content_source;
1132 hotbar.get(slot).and_then(|content| match content {
1133 hotbar::SlotContents::Inventory(i, _) => inventory.get_by_hash(i),
1134 _ => None,
1135 })
1136 };
1137
1138 let tooltip_text = |slot| {
1140 let (hotbar, inventory, _, skill_set, active_abilities, _, contexts, ..) =
1141 content_source;
1142 hotbar.get(slot).and_then(|content| match content {
1143 hotbar::SlotContents::Inventory(i, _) => inventory.get_by_hash(i).map(|item| {
1144 let (title, desc) =
1145 util::item_text(item, self.localized_strings, self.item_i18n);
1146
1147 (title.into(), desc.into())
1148 }),
1149 hotbar::SlotContents::Ability(i) => active_abilities
1150 .and_then(|a| {
1151 a.auxiliary_set(Some(inventory), Some(skill_set))
1152 .get(i)
1153 .and_then(|a| {
1154 Ability::from(*a).ability_id(
1155 self.char_state,
1156 Some(inventory),
1157 Some(skill_set),
1158 contexts,
1159 )
1160 })
1161 })
1162 .map(|id| util::ability_description(id, self.localized_strings)),
1163 })
1164 };
1165
1166 slot_maker.empty_slot = self.imgs.skillbar_slot;
1167 slot_maker.selected_slot = self.imgs.skillbar_slot;
1168
1169 let slots = slot_entries(state, slot_offset);
1170 for entry in slots {
1171 let slot = slot_maker
1172 .fabricate(entry.slot, [40.0; 2])
1173 .filled_slot(self.imgs.skillbar_slot)
1174 .position(entry.position);
1175 if let Some(item) = slot_content(entry.slot) {
1177 slot.with_item_tooltip(
1178 self.item_tooltip_manager,
1179 core::iter::once(item as &dyn ItemDesc),
1180 &None,
1181 &item_tooltip,
1182 )
1183 .set(entry.widget_id, ui);
1184 } else if let Some((title, desc)) = tooltip_text(entry.slot) {
1186 slot.with_tooltip(self.tooltip_manager, &title, &desc, &tooltip, TEXT_COLOR)
1187 .set(entry.widget_id, ui);
1188 } else {
1190 slot.set(entry.widget_id, ui);
1191 }
1192
1193 if let ShortcutNumbers::On = shortcuts {
1195 if let Some(key) = &self
1196 .global_state
1197 .settings
1198 .controls
1199 .get_binding(entry.game_input)
1200 {
1201 let position = entry.shortcut_position;
1202 let position_bg = entry.shortcut_position_bg;
1203 let (id, id_bg) = entry.shortcut_widget_ids;
1204
1205 let key_desc = key.display_shortest(key_layout);
1206 Text::new(&key_desc)
1208 .position(position)
1209 .font_size(self.fonts.cyri.scale(8))
1210 .font_id(self.fonts.cyri.conrod_id)
1211 .color(TEXT_COLOR)
1212 .set(id, ui);
1213 Text::new(&key_desc)
1215 .position(position_bg)
1216 .font_size(self.fonts.cyri.scale(8))
1217 .font_id(self.fonts.cyri.conrod_id)
1218 .color(BLACK)
1219 .set(id_bg, ui);
1220 }
1221 }
1222 }
1223 Image::new(self.imgs.skillbar_slot)
1225 .w_h(40.0, 40.0)
1226 .right_from(state.ids.slot5, slot_offset)
1227 .set(state.ids.m1_slot_bg, ui);
1228
1229 let primary_ability_id = self.active_abilities.and_then(|a| {
1230 Ability::from(a.primary).ability_id(
1231 self.char_state,
1232 Some(self.inventory),
1233 Some(self.skillset),
1234 self.context,
1235 )
1236 });
1237
1238 let (primary_ability_title, primary_ability_desc) =
1239 util::ability_description(primary_ability_id.unwrap_or(""), self.localized_strings);
1240
1241 Button::image(
1242 primary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
1243 )
1244 .w_h(36.0, 36.0)
1245 .middle_of(state.ids.m1_slot_bg)
1246 .with_tooltip(
1247 self.tooltip_manager,
1248 &primary_ability_title,
1249 &primary_ability_desc,
1250 &tooltip,
1251 TEXT_COLOR,
1252 )
1253 .set(state.ids.m1_content, ui);
1254 Image::new(self.imgs.skillbar_slot)
1256 .w_h(40.0, 40.0)
1257 .right_from(state.ids.m1_slot_bg, slot_offset)
1258 .set(state.ids.m2_slot_bg, ui);
1259
1260 let secondary_ability_id = self.active_abilities.and_then(|a| {
1261 Ability::from(a.secondary).ability_id(
1262 self.char_state,
1263 Some(self.inventory),
1264 Some(self.skillset),
1265 self.context,
1266 )
1267 });
1268
1269 let (secondary_ability_title, secondary_ability_desc) =
1270 util::ability_description(secondary_ability_id.unwrap_or(""), self.localized_strings);
1271
1272 Button::image(
1273 secondary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
1274 )
1275 .w_h(36.0, 36.0)
1276 .middle_of(state.ids.m2_slot_bg)
1277 .image_color(
1278 if self
1279 .active_abilities
1280 .and_then(|a| {
1281 a.activate_ability(
1282 AbilityInput::Secondary,
1283 Some(self.inventory),
1284 self.skillset,
1285 Some(self.body),
1286 self.char_state,
1287 self.context,
1288 self.stats,
1289 )
1290 })
1291 .is_some_and(|(a, _, _)| {
1292 self.energy.current() >= a.energy_cost()
1293 && self.combo.is_some_and(|c| c.counter() >= a.combo_cost())
1294 && a.ability_meta().requirements.requirements_met(self.stance)
1295 })
1296 {
1297 Color::Rgba(1.0, 1.0, 1.0, 1.0)
1298 } else {
1299 Color::Rgba(0.3, 0.3, 0.3, 0.8)
1300 },
1301 )
1302 .with_tooltip(
1303 self.tooltip_manager,
1304 &secondary_ability_title,
1305 &secondary_ability_desc,
1306 &tooltip,
1307 TEXT_COLOR,
1308 )
1309 .set(state.ids.m2_content, ui);
1310
1311 Image::new(self.imgs.m1_ico)
1313 .w_h(16.0, 18.0)
1314 .mid_bottom_with_margin_on(state.ids.m1_content, -11.0)
1315 .set(state.ids.m1_ico, ui);
1316 Image::new(self.imgs.m2_ico)
1317 .w_h(16.0, 18.0)
1318 .mid_bottom_with_margin_on(state.ids.m2_content, -11.0)
1319 .set(state.ids.m2_ico, ui);
1320 }
1321
1322 fn show_combo_counter(&self, combo_floater: ComboFloater, state: &State, ui: &mut UiCell) {
1323 if combo_floater.combo > 0 {
1324 let combo_txt = format!("{} Combo", combo_floater.combo);
1325 let combo_cnt = combo_floater.combo as f32;
1326 let time_since_last_update = comp::combo::COMBO_DECAY_START - combo_floater.timer;
1327 let alpha = (1.0 - time_since_last_update * 0.2).min(1.0) as f32;
1328 let fnt_col = Color::Rgba(
1329 (1.0 - combo_cnt / (combo_cnt + 20.0)).max(0.79),
1331 (1.0 - combo_cnt / (combo_cnt + 80.0)).max(0.19),
1332 (1.0 - combo_cnt / (combo_cnt + 5.0)).max(0.17),
1333 alpha,
1334 );
1335 let fnt_size = ((14.0 + combo_floater.timer as f32 * 0.8).min(30.0)) as u32
1338 + if (time_since_last_update) < 0.1 { 2 } else { 0 };
1339
1340 Rectangle::fill_with([10.0, 10.0], color::TRANSPARENT)
1341 .middle_of(ui.window)
1342 .set(state.ids.combo_align, ui);
1343
1344 Text::new(combo_txt.as_str())
1345 .mid_bottom_with_margin_on(
1346 state.ids.combo_align,
1347 -350.0 + time_since_last_update * -8.0,
1348 )
1349 .font_size(self.fonts.cyri.scale(fnt_size))
1350 .font_id(self.fonts.cyri.conrod_id)
1351 .color(Color::Rgba(0.0, 0.0, 0.0, alpha))
1352 .set(state.ids.combo_bg, ui);
1353 Text::new(combo_txt.as_str())
1354 .bottom_right_with_margins_on(state.ids.combo_bg, 1.0, 1.0)
1355 .font_size(self.fonts.cyri.scale(fnt_size))
1356 .font_id(self.fonts.cyri.conrod_id)
1357 .color(fnt_col)
1358 .set(state.ids.combo, ui);
1359 }
1360 }
1361}
1362
1363pub struct State {
1364 ids: Ids,
1365}
1366
1367impl Widget for Skillbar<'_> {
1368 type Event = Option<Event>;
1369 type State = State;
1370 type Style = ();
1371
1372 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
1373 State {
1374 ids: Ids::new(id_gen),
1375 }
1376 }
1377
1378 fn style(&self) -> Self::Style {}
1379
1380 fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
1381 common_base::prof_span!("Skillbar::update");
1382 let widget::UpdateArgs { state, ui, .. } = args;
1383
1384 let slot_offset = 3.0;
1385
1386 if self.health.is_dead {
1388 self.show_death_message(state, ui);
1389 }
1390 else if comp::is_downed(Some(self.health), self.client.current().as_ref()) {
1392 self.show_give_up_message(state, ui);
1393 }
1394
1395 state.update(|s| {
1399 s.ids.poise_ticks.resize(
1400 self::Poise::POISE_THRESHOLDS.len(),
1401 &mut ui.widget_id_generator(),
1402 )
1403 });
1404
1405 let alignment_size = 40.0 * 12.0 + slot_offset * 11.0;
1407 Rectangle::fill_with([alignment_size, 80.0], color::TRANSPARENT)
1408 .mid_bottom_with_margin_on(ui.window, 10.0)
1409 .set(state.ids.frame, ui);
1410
1411 let event = self.show_stat_bars(state, ui);
1413
1414 self.show_slotbar(state, ui, slot_offset);
1416
1417 if let Some(combo_floater) = self.combo_floater {
1419 self.show_combo_counter(combo_floater, state, ui);
1420 }
1421 event
1422 }
1423}