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