1use client::{Client, EcsEntity};
2use common::{
3 comp::{self, ItemKey},
4 rtsim,
5};
6use conrod_core::{
7 Borderable, Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
8 widget::{self, Button, Image, Rectangle, Scrollbar, Text},
9 widget_ids,
10};
11use i18n::Localization;
12use specs::WorldExt;
13use std::{
14 borrow::Cow,
15 time::{Duration, Instant},
16};
17
18use crate::{
19 GlobalState,
20 ui::{TooltipManager, fonts::Fonts},
21};
22use inline_tweak::*;
23
24use super::{
25 GameInput, Show, TEXT_COLOR, animate_by_pulse,
26 img_ids::{Imgs, ImgsRot},
27 item_imgs::ItemImgs,
28};
29
30pub struct State {
31 ids: Ids,
32 text_timer: Option<Instant>,
33 text_position: usize,
34 last_displayed_text: Option<String>, }
36
37widget_ids! {
38 pub struct Ids {
39 quest_close,
40 bg,
41 frame,
42 close,
43 title_align,
44 title,
45 text_align,
46 topics_align,
47 scrollbar,
48 intro_txt,
49 desc_txt_0,
50 ack_prompt,
51 quest_response_txt,
52 objective_text,
53 quest_responses_frames[],
54 quest_responses_btn[],
55 quest_responses_icons[],
56 quest_responses_amounts[],
57 quest_rewards_txts[],
58 }
59}
60
61#[derive(WidgetCommon)]
62pub struct Quest<'a> {
63 _show: &'a Show,
64 client: &'a Client,
65 _imgs: &'a Imgs,
66 fonts: &'a Fonts,
67 localized_strings: &'a Localization,
68 global_state: &'a GlobalState,
69 _rot_imgs: &'a ImgsRot,
70 _tooltip_manager: &'a mut TooltipManager,
71 item_imgs: &'a ItemImgs,
72 sender: EcsEntity,
73 dialogue: &'a rtsim::Dialogue<true>,
74 recv_time: Instant,
75 pulse: f32,
76
77 #[conrod(common_builder)]
78 common: widget::CommonBuilder,
79}
80
81impl<'a> Quest<'a> {
82 pub fn new(
83 _show: &'a Show,
84 client: &'a Client,
85 _imgs: &'a Imgs,
86 fonts: &'a Fonts,
87 localized_strings: &'a Localization,
88 global_state: &'a GlobalState,
89 _rot_imgs: &'a ImgsRot,
90 _tooltip_manager: &'a mut TooltipManager,
91 item_imgs: &'a ItemImgs,
92 sender: EcsEntity,
93 dialogue: &'a rtsim::Dialogue<true>,
94 recv_time: Instant,
95 pulse: f32,
96 ) -> Self {
97 Self {
98 _show,
99 client,
100 _imgs,
101 _rot_imgs,
102 fonts,
103 localized_strings,
104 global_state,
105 _tooltip_manager,
106 item_imgs,
107 sender,
108 dialogue,
109 recv_time,
110 pulse,
111 common: widget::CommonBuilder::default(),
112 }
113 }
114
115 fn update_text(&self, state: &mut State, ui: &mut UiCell, msg_text: &str) {
116 let now = Instant::now();
117
118 let is_new_message = state.text_position == 0
120 || state.text_position > msg_text.chars().count()
121 || state.last_displayed_text.as_deref() != Some(msg_text);
122
123 if is_new_message {
124 state.text_timer = Some(now);
125 state.text_position = 1; state.last_displayed_text = Some(msg_text.to_string()); }
128
129 if state.text_timer.is_none() {
130 state.text_timer = Some(now);
131 }
132
133 if let Some(start_time) = state.text_timer
134 && now.duration_since(start_time) >= Duration::from_millis(10)
135 && state.text_position < msg_text.chars().count()
136 {
137 state.text_position += 1;
138 state.text_timer = Some(now);
139 }
140
141 let display_text: String = msg_text
142 .chars()
143 .take(state.text_position.min(msg_text.chars().count()))
144 .collect();
145
146 const MARGIN: f64 = 16.0;
147 Text::new(&display_text)
148 .top_left_with_margins_on(state.ids.text_align, MARGIN, MARGIN)
149 .w(429.0 - MARGIN * 2.0)
150 .h(200.0 - MARGIN * 2.0)
151 .font_id(self.fonts.cyri.conrod_id)
152 .font_size(self.fonts.cyri.scale(16))
153 .color(TEXT_COLOR)
154 .set(state.ids.desc_txt_0, ui);
155 }
156}
157
158pub enum Event {
159 Dialogue(EcsEntity, rtsim::Dialogue),
160 #[allow(dead_code)]
161 Close,
162}
163
164impl Widget for Quest<'_> {
165 type Event = Option<Event>;
166 type State = State;
167 type Style = ();
168
169 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
170 Self::State {
171 ids: Ids::new(id_gen),
172 text_timer: None,
173 text_position: 0,
174 last_displayed_text: None,
175 }
176 }
177
178 fn style(&self) -> Self::Style {}
179
180 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
181 let widget::UpdateArgs { state, ui, .. } = args;
182 let mut event = None;
183
184 Rectangle::fill_with([tweak!(130.0), tweak!(100.0)], color::TRANSPARENT)
188 .mid_bottom_with_margin_on(ui.window, 80.0)
189 .w_h(749.0, 234.0)
190 .set(state.ids.bg, ui);
191 Rectangle::fill_with([tweak!(130.0), tweak!(100.0)], color::TRANSPARENT)
193 .middle_of(state.ids.bg)
194 .w_h(749.0, tweak!(234.0))
195 .set(state.ids.frame, ui);
196
197 const BACKGROUND: Color = Color::Rgba(0.0, 0.0, 0.0, 0.85);
198
199 Rectangle::fill_with([tweak!(429.0), tweak!(200.0)], BACKGROUND)
202 .top_left_with_margins_on(state.ids.frame, tweak!(0.0), tweak!(0.0))
203 .scroll_kids_vertically()
204 .set(state.ids.text_align, ui);
205 Rectangle::fill_with([tweak!(315.0), tweak!(200.0)], BACKGROUND)
207 .top_right_with_margins_on(state.ids.frame, tweak!(0.0), tweak!(2.0))
208 .scroll_kids_vertically()
209 .set(state.ids.topics_align, ui);
210 Scrollbar::y_axis(state.ids.topics_align)
211 .h(tweak!(169.0))
212 .top_right_with_margins(29.0, tweak!(4.0))
213 .thickness(tweak!(23.0))
214 .auto_hide(true)
215 .rgba(1.0, 1.0, 1.0, 0.2)
216 .set(state.ids.scrollbar, ui);
217
218 if Button::image(self._imgs.close_btn)
220 .w_h(24.0, 25.0)
221 .hover_image(self._imgs.close_btn_hover)
222 .press_image(self._imgs.close_btn_press)
223 .top_right_with_margins_on(state.ids.frame, 2.0, 4.0)
224 .set(state.ids.quest_close, ui)
225 .was_clicked()
226 {
227 event = Some(Event::Close);
228 }
229
230 if let rtsim::DialogueKind::Statement { .. } = &self.dialogue.kind {
231 let recv_time = self.recv_time.elapsed().as_secs_f32();
232 Text::new(&if let Some(key) = self
233 .global_state
234 .settings
235 .controls
236 .get_binding(GameInput::Interact)
237 {
238 self.localized_strings.get_msg_ctx(
239 "hud-dialogue-ack",
240 &i18n::fluent_args! { "key" => key.display_string() },
241 )
242 } else {
243 Cow::Borrowed("")
244 })
245 .bottom_right_with_margins_on(state.ids.text_align, 12.0, 12.0)
246 .font_id(self.fonts.cyri.conrod_id)
247 .font_size(self.fonts.cyri.scale(12))
248 .color(Color::Rgba(
249 1.0,
250 1.0,
251 1.0,
252 (0.6 + (recv_time * tweak!(5.0)).sin() * 0.4) * (recv_time - 1.0).clamp(0.0, 1.0),
253 ))
254 .set(state.ids.ack_prompt, ui);
255 }
256
257 let msg_text = self
259 .dialogue
260 .message()
261 .map(|msg| self.localized_strings.get_content(msg));
262
263 if let Some(msg_text) = msg_text {
264 state.update(|s| {
265 self.update_text(s, ui, &msg_text);
266 });
267 }
268
269 if let rtsim::DialogueKind::Question { responses, tag, .. } = &self.dialogue.kind {
270 if state.ids.quest_responses_frames.len() < responses.len() {
271 state.update(|s| {
272 s.ids
273 .quest_responses_frames
274 .resize(responses.len(), &mut ui.widget_id_generator())
275 })
276 };
277 if state.ids.quest_responses_icons.len() < responses.len() {
278 state.update(|s| {
279 s.ids
280 .quest_responses_icons
281 .resize(responses.len(), &mut ui.widget_id_generator())
282 })
283 };
284 if state.ids.quest_responses_amounts.len() < responses.len() {
285 state.update(|s| {
286 s.ids
287 .quest_responses_amounts
288 .resize(responses.len(), &mut ui.widget_id_generator())
289 })
290 };
291 if state.ids.quest_rewards_txts.len() < responses.len() {
292 state.update(|s| {
293 s.ids
294 .quest_rewards_txts
295 .resize(responses.len(), &mut ui.widget_id_generator())
296 })
297 };
298 if state.ids.quest_responses_btn.len() < responses.len() {
299 state.update(|s| {
300 s.ids
301 .quest_responses_btn
302 .resize(responses.len(), &mut ui.widget_id_generator())
303 })
304 };
305
306 for (i, (response_id, response)) in responses.iter().enumerate() {
307 let is_valid = if let Some((item, amount)) = &response.given_item {
309 self.client
310 .state()
311 .ecs()
312 .read_storage::<comp::Inventory>()
313 .get(self.client.entity())
314 .is_some_and(|inv| inv.item_count(item) >= *amount as u64)
315 } else {
316 true
317 };
318
319 let frame = Button::new()
320 .border_color(color::TRANSPARENT)
321 .color(Color::Rgba(1.0, 1.0, 1.0, 0.0))
322 .hover_color(if is_valid {
323 Color::Rgba(1.0, 1.0, 1.0, 0.05)
324 } else {
325 Color::Rgba(1.0, 0.5, 0.5, 0.05)
326 })
327 .press_color(Color::Rgba(1.0, 1.0, 1.0, 0.1))
328 .parent(state.ids.topics_align)
329 .w_h(286.0, 30.0);
330 let frame = if i == 0 {
331 frame.top_left_with_margins_on(state.ids.topics_align, tweak!(0.0), tweak!(0.0))
332 } else {
333 frame.down_from(state.ids.quest_responses_frames[i - 1], 0.0)
334 };
335 if frame
336 .set(state.ids.quest_responses_frames[i], ui)
337 .was_clicked()
338 {
339 event = Some(Event::Dialogue(self.sender, rtsim::Dialogue {
340 id: self.dialogue.id,
341 kind: rtsim::DialogueKind::Response {
342 tag: *tag,
343 response: response.clone(),
344 response_id: *response_id,
345 },
346 }));
347 }
348
349 Text::new(&self.localized_strings.get_content(&response.msg))
351 .middle_of(state.ids.quest_responses_frames[i])
352 .graphics_for(state.ids.quest_responses_frames[i])
353 .font_id(self.fonts.cyri.conrod_id)
354 .color(Color::Rgba(1.0, 1.0, 1.0, if is_valid { 1.0 } else { 0.3 }))
355 .font_size(self.fonts.cyri.scale(tweak!(14)))
356 .set(state.ids.quest_rewards_txts[i], ui);
357
358 if let Some((item, amount)) = &response.given_item {
360 Image::new(animate_by_pulse(
361 &self
362 .item_imgs
363 .img_ids_or_not_found_img(ItemKey::from(&**item)),
364 self.pulse,
365 ))
366 .mid_left_with_margin_on(state.ids.quest_responses_frames[i], 8.0)
367 .w_h(20.0, 20.0)
368 .graphics_for(state.ids.quest_responses_frames[i])
369 .set(state.ids.quest_responses_icons[i], ui);
370
371 Text::new(&format!("x{amount}"))
372 .mid_left_with_margin_on(state.ids.quest_responses_icons[i], tweak!(24.0))
373 .font_id(self.fonts.cyri.conrod_id)
374 .font_size(self.fonts.cyri.scale(12))
375 .color(if is_valid {
376 TEXT_COLOR
377 } else {
378 Color::Rgba(1.0, 0.2, 0.2, 0.6 + (self.pulse * 8.0).sin() * 0.4)
380 })
381 .wrap_by_word()
382 .set(state.ids.quest_responses_amounts[i], ui);
383 }
384 }
385 }
386
387 event
388 }
389}