1use super::{
2 HudInfo, Show, TEXT_COLOR, Windows, animate_by_pulse, get_quality_col,
3 img_ids::{Imgs, ImgsRot},
4 item_imgs::ItemImgs,
5 util,
6};
7use crate::ui::{ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, fonts::Fonts};
8use client::Client;
9use common::{
10 comp::{
11 FrontendItem, Inventory,
12 inventory::item::{ItemDesc, ItemI18n, MaterialStatManifest, Quality},
13 },
14 recipe::RecipeBookManifest,
15 uid::Uid,
16};
17use common_net::sync::WorldSyncExt;
18use conrod_core::{
19 Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
20 position::Dimension,
21 widget::{self, Image, List, Rectangle, Scrollbar, Text},
22 widget_ids,
23};
24use i18n::Localization;
25use std::{collections::VecDeque, num::NonZeroU32};
26
27widget_ids! {
28 struct Ids{
29 frame,
30 message_box,
31 scrollbar,
32 message_icons[],
33 message_icon_bgs[],
34 message_icon_frames[],
35 message_texts[],
36 message_text_shadows[],
37 }
38}
39
40const MAX_MESSAGES: usize = 50;
41
42const BOX_WIDTH: f64 = 300.0;
43const BOX_HEIGHT: f64 = 350.0;
44
45const ICON_BG_SIZE: f64 = 33.0;
46const ICON_SIZE: f64 = 30.0;
47const ICON_LABEL_SPACER: f64 = 7.0;
48
49const MESSAGE_VERTICAL_PADDING: f64 = 1.0;
50
51const HOVER_FADE_OUT_TIME: f32 = 2.0;
52const MESSAGE_FADE_OUT_TIME: f32 = 4.5;
53const AUTO_SHOW_FADE_OUT_TIME: f32 = 1.0;
54
55const MAX_MERGE_TIME: f32 = MESSAGE_FADE_OUT_TIME;
56
57#[derive(WidgetCommon)]
58pub struct LootScroller<'a> {
59 new_messages: &'a mut VecDeque<LootMessage>,
60
61 client: &'a Client,
62 info: &'a HudInfo,
63 show: &'a Show,
64 imgs: &'a Imgs,
65 item_imgs: &'a ItemImgs,
66 rot_imgs: &'a ImgsRot,
67 fonts: &'a Fonts,
68 localized_strings: &'a Localization,
69 item_i18n: &'a ItemI18n,
70 msm: &'a MaterialStatManifest,
71 rbm: &'a RecipeBookManifest,
72 inventory: Option<&'a Inventory>,
73 item_tooltip_manager: &'a mut ItemTooltipManager,
74 pulse: f32,
75
76 #[conrod(common_builder)]
77 common: widget::CommonBuilder,
78}
79impl<'a> LootScroller<'a> {
80 pub fn new(
81 new_messages: &'a mut VecDeque<LootMessage>,
82 client: &'a Client,
83 info: &'a HudInfo,
84 show: &'a Show,
85 imgs: &'a Imgs,
86 item_imgs: &'a ItemImgs,
87 rot_imgs: &'a ImgsRot,
88 fonts: &'a Fonts,
89 localized_strings: &'a Localization,
90 item_i18n: &'a ItemI18n,
91 msm: &'a MaterialStatManifest,
92 rbm: &'a RecipeBookManifest,
93 inventory: Option<&'a Inventory>,
94 item_tooltip_manager: &'a mut ItemTooltipManager,
95 pulse: f32,
96 ) -> Self {
97 Self {
98 new_messages,
99 client,
100 info,
101 show,
102 imgs,
103 item_imgs,
104 rot_imgs,
105 fonts,
106 localized_strings,
107 item_i18n,
108 msm,
109 rbm,
110 inventory,
111 item_tooltip_manager,
112 pulse,
113 common: widget::CommonBuilder::default(),
114 }
115 }
116}
117
118#[derive(Debug, PartialEq)]
119pub struct LootMessage {
120 pub item: FrontendItem,
121 pub amount: NonZeroU32,
122 pub taken_by: Uid,
123}
124
125pub struct State {
126 ids: Ids,
127 messages: VecDeque<(LootMessage, f32)>, last_hover_pulse: Option<f32>,
130 last_auto_show_pulse: Option<f32>, }
132
133impl Widget for LootScroller<'_> {
134 type Event = ();
135 type State = State;
136 type Style = ();
137
138 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
139 State {
140 ids: Ids::new(id_gen),
141 messages: VecDeque::new(),
142 last_hover_pulse: None,
143 last_auto_show_pulse: None,
144 }
145 }
146
147 fn style(&self) -> Self::Style {}
148
149 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
150 let widget::UpdateArgs { state, ui, .. } = args;
151
152 let item_tooltip = ItemTooltip::new(
154 {
155 let edge = &self.rot_imgs.tt_side;
158 let corner = &self.rot_imgs.tt_corner;
159 ImageFrame::new(
160 [edge.cw180, edge.none, edge.cw270, edge.cw90],
161 [corner.none, corner.cw270, corner.cw90, corner.cw180],
162 Color::Rgba(0.08, 0.07, 0.04, 1.0),
163 5.0,
164 )
165 },
166 self.client,
167 self.info,
168 self.imgs,
169 self.item_imgs,
170 self.pulse,
171 self.msm,
172 self.rbm,
173 self.inventory,
174 self.localized_strings,
175 self.item_i18n,
176 )
177 .title_font_size(self.fonts.cyri.scale(20))
178 .parent(ui.window)
179 .desc_font_size(self.fonts.cyri.scale(12))
180 .font_id(self.fonts.cyri.conrod_id)
181 .desc_text_color(TEXT_COLOR);
182
183 if !self.new_messages.is_empty() {
184 let pulse = self.pulse;
185 let oldest_merge_pulse = pulse - MAX_MERGE_TIME;
186
187 state.update(|s| {
188 s.messages.retain(|(message, t)| {
189 if *t >= oldest_merge_pulse {
190 if let Some(i) = self.new_messages.iter().position(|m| {
191 m.item.item_definition_id() == message.item.item_definition_id()
192 && m.taken_by == message.taken_by
193 }) {
194 self.new_messages[i].amount = self.new_messages[i]
195 .amount
196 .saturating_add(message.amount.get());
197 false
198 } else {
199 true
200 }
201 } else {
202 true
203 }
204 });
205 s.messages
206 .extend(self.new_messages.drain(..).map(|message| (message, pulse)));
207 while s.messages.len() > MAX_MESSAGES {
208 s.messages.pop_front();
209 }
210 });
211 ui.scroll_widget(state.ids.message_box, [0.0, f64::MAX]);
212 }
213
214 if self.show.diary
216 || self.show.map
217 || self.show.open_windows != Windows::None
218 || self.show.social
219 || self.show.trade
220 {
221 if state.last_hover_pulse.is_some() || state.last_auto_show_pulse.is_some() {
222 state.update(|s| {
223 s.last_hover_pulse = None;
224 s.last_auto_show_pulse = None;
225 });
226 }
227 } else {
228 if ui
230 .rect_of(state.ids.message_box)
231 .map(|r| r.pad_left(-6.0))
232 .is_some_and(|r| r.is_over(ui.global_input().current.mouse.xy))
233 {
234 state.update(|s| s.last_hover_pulse = Some(self.pulse));
235 }
236
237 if state.ids.message_icons.len() < state.messages.len() {
238 state.update(|s| {
239 s.ids
240 .message_icons
241 .resize(s.messages.len(), &mut ui.widget_id_generator())
242 });
243 }
244 if state.ids.message_icon_bgs.len() < state.messages.len() {
245 state.update(|s| {
246 s.ids
247 .message_icon_bgs
248 .resize(s.messages.len(), &mut ui.widget_id_generator())
249 });
250 }
251 if state.ids.message_icon_frames.len() < state.messages.len() {
252 state.update(|s| {
253 s.ids
254 .message_icon_frames
255 .resize(s.messages.len(), &mut ui.widget_id_generator())
256 });
257 }
258 if state.ids.message_texts.len() < state.messages.len() {
259 state.update(|s| {
260 s.ids
261 .message_texts
262 .resize(s.messages.len(), &mut ui.widget_id_generator())
263 });
264 }
265 if state.ids.message_text_shadows.len() < state.messages.len() {
266 state.update(|s| {
267 s.ids
268 .message_text_shadows
269 .resize(s.messages.len(), &mut ui.widget_id_generator())
270 });
271 }
272
273 let hover_age = state
274 .last_hover_pulse
275 .map_or(1.0, |t| (self.pulse - t) / HOVER_FADE_OUT_TIME);
276 let auto_show_age = state
277 .last_auto_show_pulse
278 .map_or(1.0, |t| (self.pulse - t) / AUTO_SHOW_FADE_OUT_TIME);
279
280 let show_all_age = hover_age.min(auto_show_age);
281
282 let messages_to_display = state
283 .messages
284 .iter()
285 .rev()
286 .map(|(message, t)| {
287 let age = (self.pulse - t) / MESSAGE_FADE_OUT_TIME;
288 (message, age)
289 })
290 .filter(|(_, age)| age.min(show_all_age) < 1.0)
291 .collect::<Vec<_>>();
292
293 let (mut list_messages, _) = List::flow_up(messages_to_display.len())
294 .w_h(BOX_WIDTH, BOX_HEIGHT)
295 .scroll_kids_vertically()
296 .bottom_left_with_margins_on(ui.window, 308.0, 20.0)
297 .set(state.ids.message_box, ui);
298
299 if show_all_age < 1.0
301 && ui
302 .widget_graph()
303 .widget(state.ids.message_box)
304 .and_then(|w| w.maybe_y_scroll_state)
305 .is_some_and(|s| s.scrollable_range_len > BOX_HEIGHT)
306 {
307 Scrollbar::y_axis(state.ids.message_box)
308 .thickness(5.0)
309 .rgba(0.33, 0.33, 0.33, 1.0 - show_all_age.powi(4))
310 .left_from(state.ids.message_box, 1.0)
311 .set(state.ids.scrollbar, ui);
312 }
313
314 let stats = self.client.state().read_storage::<common::comp::Stats>();
315
316 while let Some(list_message) = list_messages.next(ui) {
317 let i = list_message.i;
318
319 let (message, age) = messages_to_display[i];
320 let LootMessage {
321 item,
322 amount,
323 taken_by,
324 } = message;
325
326 let alpha = 1.0 - age.min(show_all_age).powi(4);
327
328 let brightness = 1.0 / (age / 0.05 - 1.0).abs().clamp(0.01, 1.0);
329
330 let shade_color = |color: Color| {
331 let color::Hsla(hue, sat, lum, alp) = color.to_hsl();
332 color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha)
333 };
334
335 let quality_col_image = match item.quality() {
336 Quality::Low => self.imgs.inv_slot_grey,
337 Quality::Common => self.imgs.inv_slot_common,
338 Quality::Moderate => self.imgs.inv_slot_green,
339 Quality::High => self.imgs.inv_slot_blue,
340 Quality::Epic => self.imgs.inv_slot_purple,
341 Quality::Legendary => self.imgs.inv_slot_gold,
342 Quality::Artifact => self.imgs.inv_slot_orange,
343 _ => self.imgs.inv_slot_red,
344 };
345 let quality_col = get_quality_col(item.quality());
346
347 Image::new(self.imgs.pixel)
348 .color(Some(shade_color(quality_col.alpha(0.7))))
349 .w_h(ICON_BG_SIZE, ICON_BG_SIZE)
350 .top_left_with_margins_on(list_message.widget_id, MESSAGE_VERTICAL_PADDING, 0.0)
351 .set(state.ids.message_icon_bgs[i], ui);
352
353 Image::new(quality_col_image)
354 .color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0))))
355 .wh_of(state.ids.message_icon_bgs[i])
356 .middle_of(state.ids.message_icon_bgs[i])
357 .set(state.ids.message_icon_frames[i], ui);
358
359 Image::new(animate_by_pulse(
360 &self.item_imgs.img_ids_or_not_found_img(item.into()),
361 self.pulse,
362 ))
363 .color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0))))
364 .w_h(ICON_SIZE, ICON_SIZE)
365 .middle_of(state.ids.message_icon_bgs[i])
366 .with_item_tooltip(
367 self.item_tooltip_manager,
368 core::iter::once(item as &dyn ItemDesc),
369 &None,
370 &item_tooltip,
371 )
372 .set(state.ids.message_icons[i], ui);
373
374 let target_name = self
375 .client
376 .player_list()
377 .get(taken_by)
378 .map_or_else(
379 || {
380 self.client
381 .state()
382 .ecs()
383 .entity_from_uid(*taken_by)
384 .and_then(|entity| stats.get(entity).map(|e| e.name.clone()))
385 },
386 |info| Some(info.player_alias.clone()),
387 )
388 .unwrap_or_else(|| format!("<uid {}>", *taken_by));
389
390 let (user_gender, is_you) = match self.client.player_list().get(taken_by) {
391 Some(player_info) => match player_info.character.as_ref() {
392 Some(character_info) => (
393 match character_info.gender {
394 Some(common::comp::Gender::Feminine) => "she".to_string(),
395 Some(common::comp::Gender::Masculine) => "he".to_string(),
396 None => "??".to_string(),
397 },
398 self.client.uid().expect("Client doesn't have a Uid!!!") == *taken_by,
399 ),
400 None => ("??".to_string(), false),
401 },
402 None => ("??".to_string(), false),
403 };
404
405 let label = if is_you {
406 self.localized_strings.get_msg_ctx(
407 "hud-loot-pickup-msg-you",
408 &i18n::fluent_args! {
409 "gender" => user_gender,
410 "amount" => amount.get(),
411 "item" => {
412 let (name, _) =
413 util::item_text(&item, self.localized_strings, self.item_i18n);
414 name
415 },
416 },
417 )
418 } else {
419 self.localized_strings.get_msg_ctx(
420 "hud-loot-pickup-msg",
421 &i18n::fluent_args! {
422 "gender" => user_gender,
423 "actor" => target_name,
424 "amount" => amount.get(),
425 "item" => {
426 let (name, _) =
427 util::item_text(&item, self.localized_strings, self.item_i18n);
428 name
429 },
430 },
431 )
432 };
433
434 let label_font_size = 20;
435
436 Text::new(&label)
437 .top_left_with_margins_on(
438 list_message.widget_id,
439 MESSAGE_VERTICAL_PADDING + 1.0,
440 ICON_BG_SIZE + ICON_LABEL_SPACER,
441 )
442 .font_id(self.fonts.cyri.conrod_id)
443 .font_size(self.fonts.cyri.scale(label_font_size))
444 .color(shade_color(quality_col))
445 .graphics_for(state.ids.message_icons[i])
446 .and(|text| {
447 let text_width = match text.get_x_dimension(ui) {
448 Dimension::Absolute(x) => x,
449 _ => f64::MAX,
450 }
451 .min(BOX_WIDTH - (ICON_BG_SIZE + ICON_LABEL_SPACER));
452 text.w(text_width)
453 })
454 .set(state.ids.message_texts[i], ui);
455 Text::new(&label)
456 .depth(1.0)
457 .parent(list_message.widget_id)
458 .x_y_relative_to(state.ids.message_texts[i], -1.0, -1.0)
459 .wh_of(state.ids.message_texts[i])
460 .font_id(self.fonts.cyri.conrod_id)
461 .font_size(self.fonts.cyri.scale(label_font_size))
462 .color(shade_color(color::rgba(0.0, 0.0, 0.0, 1.0)))
463 .set(state.ids.message_text_shadows[i], ui);
464
465 let height = 2.0 * MESSAGE_VERTICAL_PADDING
466 + ICON_BG_SIZE.max(
467 1.0 + ui
468 .rect_of(state.ids.message_texts[i])
469 .map_or(0.0, |r| r.h() + label_font_size as f64 / 3.0),
470 );
473
474 let rect = Rectangle::fill_with([BOX_WIDTH, height], color::TRANSPARENT);
475
476 list_message.set(rect, ui);
477 }
478 }
479 }
480}