1use super::img_ids::Imgs;
2
3use crate::{
4 GlobalState,
5 game_input::GameInput,
6 hud::{
7 CollectFailedData, HudCollectFailedReason, HudLootOwner, IconHandler,
8 controller_icons::LayerIconIds,
9 },
10 ui::{Ingameable, fonts::Fonts},
11 window::LastInput,
12};
13use conrod_core::{
14 Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
15 widget::{self, RoundedRectangle, Text},
16 widget_ids,
17};
18use i18n::Localization;
19use std::borrow::Cow;
20
21pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0);
22pub const NEGATIVE_TEXT_COLOR: Color = Color::Rgba(0.91, 0.15, 0.17, 1.0);
23pub const PICKUP_FAILED_FADE_OUT_TIME: f32 = 1.5;
24
25widget_ids! {
26 struct Ids {
27 name_bg,
29 name,
30 btn_bg,
32 btns[],
33 icns[], inv_full_bg,
36 inv_full,
37 }
38}
39
40#[derive(WidgetCommon)]
43pub struct Overitem<'a> {
44 name: Cow<'a, str>,
45 quality: Color,
46 distance_from_player_sqr: f32,
47 fonts: &'a Fonts,
48 localized_strings: &'a Localization,
49 #[conrod(common_builder)]
50 common: widget::CommonBuilder,
51 properties: OveritemProperties,
52 pulse: f32,
53 interaction_options: Vec<(Option<GameInput>, String, Color)>,
55 imgs: &'a Imgs,
56 global_state: &'a GlobalState,
57}
58
59impl<'a> Overitem<'a> {
60 pub fn new(
61 name: Cow<'a, str>,
62 quality: Color,
63 distance_from_player_sqr: f32,
64 fonts: &'a Fonts,
65 localized_strings: &'a Localization,
66 properties: OveritemProperties,
67 pulse: f32,
68 interaction_options: Vec<(Option<GameInput>, String, Color)>,
69 imgs: &'a Imgs,
70 global_state: &'a GlobalState,
71 ) -> Self {
72 Self {
73 name,
74 quality,
75 distance_from_player_sqr,
76 fonts,
77 localized_strings,
78 common: widget::CommonBuilder::default(),
79 properties,
80 pulse,
81 interaction_options,
82 imgs,
83 global_state,
84 }
85 }
86}
87
88pub struct OveritemProperties {
89 pub active: bool,
90 pub pickup_failed_pulse: Option<CollectFailedData>,
91}
92
93pub struct State {
94 ids: Ids,
95}
96
97impl Ingameable for Overitem<'_> {
98 fn prim_count(&self) -> usize {
99 let base = 2;
104
105 let interaction_ids = match self.global_state.window.last_input() {
107 LastInput::KeyboardMouse => self
108 .global_state
109 .settings
110 .controls
111 .get_binding(GameInput::Interact)
112 .filter(|_| self.properties.active)
113 .map_or(0, |_| 2),
114 LastInput::Controller => {
115 self.global_state
117 .settings
118 .controller
119 .get_game_button_binding(GameInput::Interact)
120 .filter(|_| self.properties.active)
121 .map_or(3, |_| 5)
122 },
123 };
124
125 let pulse = if self.properties.pickup_failed_pulse.is_some() {
127 2
128 } else {
129 0
130 };
131
132 base + interaction_ids + pulse
133 }
134}
135
136impl Widget for Overitem<'_> {
137 type Event = ();
138 type State = State;
139 type Style = ();
140
141 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
142 State {
143 ids: Ids::new(id_gen),
144 }
145 }
146
147 fn style(&self) -> Self::Style {}
148
149 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
150 let widget::UpdateArgs { id, state, ui, .. } = args;
151
152 let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
153
154 let scale = 30.0;
168
169 let text_font_size = scale * 1.0;
170 let text_pos_y = scale * 1.2;
171
172 let btn_rect_size = scale * 0.8;
173 let btn_font_size = scale * 0.6;
174 let btn_rect_pos_y = 0.0;
175 let btn_text_pos_y = btn_rect_pos_y + ((btn_rect_size - btn_font_size) * 0.5);
176 let btn_radius = btn_rect_size / 5.0;
177
178 let inv_full_font_size = scale * 1.0;
179 let inv_full_pos_y = scale * 2.4;
180
181 Text::new(&self.name)
183 .font_id(self.fonts.cyri.conrod_id)
184 .font_size(text_font_size as u32)
185 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
186 .x_y(-1.0, text_pos_y - 2.0)
187 .parent(id)
188 .depth(self.distance_from_player_sqr + 4.0)
189 .set(state.ids.name_bg, ui);
190 Text::new(&self.name)
191 .font_id(self.fonts.cyri.conrod_id)
192 .font_size(text_font_size as u32)
193 .color(self.quality)
194 .x_y(0.0, text_pos_y)
195 .depth(self.distance_from_player_sqr + 3.0)
196 .parent(id)
197 .set(state.ids.name, ui);
198
199 if !self.interaction_options.is_empty() && self.properties.active {
201 let mut max_w = btn_rect_size;
202 let mut max_h = 0.0;
203 let mut box_offset = 0.0;
204
205 match self.global_state.window.last_input() {
206 LastInput::KeyboardMouse => {
207 let texts = self
208 .interaction_options
209 .iter()
210 .filter_map(|(input, action, color)| {
211 let binding = if let Some(input) = input {
212 Some(self.global_state.settings.controls.get_binding(*input)?)
213 } else {
214 None
215 };
216 Some((binding, action, color))
217 })
218 .map(|(input, action, color)| {
219 if let Some(input) = input {
220 let input = input.display_string();
221 (format!("{} {action}", input.as_str()), color)
222 } else {
223 (action.to_string(), color)
224 }
225 })
226 .collect::<Vec<_>>();
227 if state.ids.btns.len() < texts.len() {
228 state.update(|state| {
229 state
230 .ids
231 .btns
232 .resize(texts.len(), &mut ui.widget_id_generator());
233 })
234 }
235
236 for (idx, (text, color)) in texts.iter().enumerate() {
237 let hints_text = Text::new(text)
238 .font_id(self.fonts.cyri.conrod_id)
239 .font_size(btn_font_size as u32)
240 .color(**color)
241 .x_y(0.0, btn_text_pos_y + max_h)
242 .depth(self.distance_from_player_sqr + 1.0)
243 .parent(id);
244 let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
245 max_w = max_w.max(w);
246 max_h += h;
247 hints_text.set(state.ids.btns[idx], ui);
248 }
249
250 max_h = max_h.max(btn_rect_size);
251 },
252 LastInput::Controller => {
253 let controller_texts = self.interaction_options.iter().collect::<Vec<_>>(); if state.ids.btns.len() < controller_texts.len() {
258 state.update(|state| {
259 state
260 .ids
261 .btns
262 .resize(controller_texts.len(), &mut ui.widget_id_generator());
263 })
264 }
265 let icns_size = controller_texts.len() * 3; if state.ids.icns.len() < icns_size {
267 state.update(|state| {
268 state
269 .ids
270 .icns
271 .resize(icns_size, &mut ui.widget_id_generator());
272 })
273 }
274
275 let icon_handler = IconHandler::new(self.global_state, self.imgs);
276 let mut icons_w: u8 = 0;
277
278 for (idx, (inputs, action, color)) in controller_texts.iter().enumerate() {
280 let text_widget_id = state.ids.btns[idx];
282 let hints_text = Text::new(action)
283 .font_id(self.fonts.cyri.conrod_id)
284 .font_size(btn_font_size as u32)
285 .color(*color)
286 .x_y(0.0, btn_text_pos_y + max_h)
287 .depth(self.distance_from_player_sqr + 1.0)
288 .parent(id);
289 let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
290 max_w = max_w.max(w);
291 max_h += h;
292 hints_text.set(text_widget_id, ui);
293
294 let idx_icns = idx * 3;
296 let icon_ids = LayerIconIds {
297 main: state.ids.icns[idx_icns],
298 modifier1: state.ids.icns[idx_icns + 1],
299 modifier2: state.ids.icns[idx_icns + 2],
300 };
301 if let Some(input) = inputs {
302 let count = icon_handler.set_controller_icons_left(
303 *input,
304 17.0,
305 text_widget_id,
306 &icon_ids,
307 ui,
308 );
309 icons_w = icons_w.max(count);
310 } else {
311 icon_handler.set_controller_icons_left_none(
313 17.0,
314 text_widget_id,
315 &icon_ids,
316 ui,
317 );
318 }
319 }
320
321 let icon_largest_width = icons_w as f64 * 21.0;
322 box_offset = icon_largest_width / 2.0;
323 max_w += icon_largest_width;
324 max_h = max_h.max(btn_rect_size);
325 },
326 }
327
328 RoundedRectangle::fill_with(
329 [max_w + btn_radius * 2.0, max_h + btn_radius * 2.0],
330 btn_radius,
331 btn_color,
332 )
333 .x_y(0.0 - box_offset, btn_rect_pos_y)
334 .depth(self.distance_from_player_sqr + 2.0)
335 .parent(id)
336 .set(state.ids.btn_bg, ui);
337 }
338 if let Some(collect_failed_data) = self.properties.pickup_failed_pulse {
339 let age = ((self.pulse - collect_failed_data.pulse) / PICKUP_FAILED_FADE_OUT_TIME)
341 .clamp(0.0, 1.0);
342
343 let alpha = 1.0 - age.powi(4);
344 let brightness = 1.0 / (age / 0.07 - 1.0).abs().clamp(0.01, 1.0);
345 let shade_color = |color: Color| {
346 let color::Hsla(hue, sat, lum, alp) = color.to_hsl();
347 color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha)
348 };
349
350 let text = match collect_failed_data.reason {
351 HudCollectFailedReason::InventoryFull => {
352 self.localized_strings.get_msg("hud-inventory_full")
353 },
354 HudCollectFailedReason::LootOwned { owner, expiry_secs } => {
355 let owner_name = match owner {
356 HudLootOwner::Name(name) => {
357 Cow::Owned(self.localized_strings.get_content(&name))
358 },
359 HudLootOwner::Group => self.localized_strings.get_msg("hud-another_group"),
360 HudLootOwner::Unknown => self.localized_strings.get_msg("hud-someone_else"),
361 };
362 self.localized_strings.get_msg_ctx(
363 "hud-owned_by_for_secs",
364 &i18n::fluent_args! {
365 "name" => owner_name,
366 "secs" => expiry_secs,
367 },
368 )
369 },
370 };
371
372 Text::new(&text)
373 .font_id(self.fonts.cyri.conrod_id)
374 .font_size(inv_full_font_size as u32)
375 .color(shade_color(Color::Rgba(0.0, 0.0, 0.0, 1.0)))
376 .x_y(-1.0, inv_full_pos_y - 2.0)
377 .parent(id)
378 .depth(self.distance_from_player_sqr + 6.0)
379 .set(state.ids.inv_full_bg, ui);
380
381 Text::new(&text)
382 .font_id(self.fonts.cyri.conrod_id)
383 .font_size(inv_full_font_size as u32)
384 .color(shade_color(Color::Rgba(1.0, 0.0, 0.0, 1.0)))
385 .x_y(0.0, inv_full_pos_y)
386 .parent(id)
387 .depth(self.distance_from_player_sqr + 5.0)
388 .set(state.ids.inv_full, ui);
389 }
390 }
391}