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