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