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