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