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