veloren_voxygen/hud/
quest.rs

1use client::{Client, EcsEntity};
2use common::{comp::ItemKey, rtsim};
3use conrod_core::{
4    Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
5    widget::{self, Button, Image, Rectangle, Text},
6    widget_ids,
7};
8use i18n::Localization;
9use std::time::{Duration, Instant};
10
11use crate::ui::{TooltipManager, fonts::Fonts};
12use inline_tweak::*;
13
14use super::{
15    Show, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, animate_by_pulse,
16    img_ids::{Imgs, ImgsRot},
17    item_imgs::ItemImgs,
18};
19
20pub struct State {
21    ids: Ids,
22    text_timer: Option<Instant>,
23    text_position: usize,
24    last_displayed_text: Option<String>, // New field to track the last message
25}
26
27widget_ids! {
28    pub struct Ids {
29        quest_close,
30        bg,
31        frame,
32        icon,
33        close,
34        title_align,
35        title,
36        text_align,
37        topics_align,
38        scrollbar,
39        intro_txt,
40        desc_txt_0,
41        quest_objectives[],
42        quest_response_txt,
43        objective_text,
44        quest_responses_frames[],
45        quest_responses_btn[],
46        quest_responses_icons[],
47        quest_responses_amounts[],
48        quest_rewards_txts[],
49        accept_btn,
50        decline_btn,
51    }
52}
53
54#[derive(WidgetCommon)]
55pub struct Quest<'a> {
56    _show: &'a Show,
57    _client: &'a Client,
58    imgs: &'a Imgs,
59    fonts: &'a Fonts,
60    localized_strings: &'a Localization,
61    _rot_imgs: &'a ImgsRot,
62    _tooltip_manager: &'a mut TooltipManager,
63    item_imgs: &'a ItemImgs,
64    sender: EcsEntity,
65    dialogue: &'a rtsim::Dialogue<true>,
66    pulse: f32,
67
68    #[conrod(common_builder)]
69    common: widget::CommonBuilder,
70}
71
72impl<'a> Quest<'a> {
73    pub fn new(
74        _show: &'a Show,
75        _client: &'a Client,
76        imgs: &'a Imgs,
77        fonts: &'a Fonts,
78        localized_strings: &'a Localization,
79        _rot_imgs: &'a ImgsRot,
80        _tooltip_manager: &'a mut TooltipManager,
81        item_imgs: &'a ItemImgs,
82        sender: EcsEntity,
83        dialogue: &'a rtsim::Dialogue<true>,
84        pulse: f32,
85    ) -> Self {
86        Self {
87            _show,
88            _client,
89            imgs,
90            _rot_imgs,
91            fonts,
92            localized_strings,
93            _tooltip_manager,
94            item_imgs,
95            sender,
96            dialogue,
97            pulse,
98            common: widget::CommonBuilder::default(),
99        }
100    }
101
102    fn update_text(&self, state: &mut State, ui: &mut UiCell, msg_text: &str) {
103        let now = Instant::now();
104
105        // Check if we have a new message
106        let is_new_message = state.text_position == 0
107            || state.text_position > msg_text.chars().count()
108            || state.last_displayed_text.as_deref() != Some(msg_text);
109
110        if is_new_message {
111            state.text_timer = Some(now);
112            state.text_position = 1; // Start displaying from the first character
113            state.last_displayed_text = Some(msg_text.to_string()); // Store the message
114        }
115
116        if state.text_timer.is_none() {
117            state.text_timer = Some(now);
118        }
119
120        if let Some(start_time) = state.text_timer {
121            if now.duration_since(start_time) >= Duration::from_millis(10)
122                && state.text_position < msg_text.chars().count()
123            {
124                state.text_position += 1;
125                state.text_timer = Some(now);
126            }
127        }
128
129        let display_text: String = msg_text
130            .chars()
131            .take(state.text_position.min(msg_text.chars().count()))
132            .collect();
133
134        Text::new(&display_text)
135            .top_left_with_margins_on(state.ids.text_align, 8.0, 8.0)
136            .w(500.0)
137            .font_id(self.fonts.cyri.conrod_id)
138            .font_size(self.fonts.cyri.scale(20))
139            .color(TEXT_COLOR)
140            .set(state.ids.desc_txt_0, ui);
141    }
142}
143
144pub enum Event {
145    Dialogue(EcsEntity, rtsim::Dialogue),
146    #[allow(dead_code)]
147    Close,
148}
149
150impl Widget for Quest<'_> {
151    type Event = Option<Event>;
152    type State = State;
153    type Style = ();
154
155    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
156        Self::State {
157            ids: Ids::new(id_gen),
158            text_timer: None,
159            text_position: 0,
160            last_displayed_text: None,
161        }
162    }
163
164    fn style(&self) -> Self::Style {}
165
166    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
167        let widget::UpdateArgs { state, ui, .. } = args;
168        let mut event = None;
169
170        // Window BG
171        Image::new(self.imgs.dialogue_bg)
172            .mid_bottom_with_margin_on(ui.window, 80.0)
173            .color(Some(UI_MAIN))
174            .w_h(720.0, 234.0)
175            .set(state.ids.bg, ui);
176        // Window frame
177        Image::new(self.imgs.dialogue_frame)
178            .middle_of(state.ids.bg)
179            .color(Some(UI_HIGHLIGHT_0))
180            .w_h(720.0, 234.0)
181            .set(state.ids.frame, ui);
182
183        // // X-Button
184        // if Button::image(self.imgs.close_button)
185        //     .w_h(24.0, 25.0)
186        //     .hover_image(self.imgs.close_button_hover)
187        //     .press_image(self.imgs.close_button_press)
188        //     .top_right_with_margins_on(state.ids.frame, 0.0, 0.0)
189        //     .set(state.ids.close, ui)
190        //     .was_clicked()
191        // {
192        //     event = Some(Event::Close);
193        // }
194
195        // Content Alignment
196        // Text Left
197        Rectangle::fill_with([tweak!(529.0), tweak!(230.0)], color::TRANSPARENT)
198            .top_left_with_margins_on(state.ids.frame, tweak!(2.0), tweak!(2.0))
199            .scroll_kids_vertically()
200            .set(state.ids.text_align, ui);
201        // Topics Right
202        Rectangle::fill_with([tweak!(186.0), tweak!(230.0)], color::TRANSPARENT)
203            .top_right_with_margins_on(state.ids.frame, tweak!(2.0), tweak!(2.0))
204            .scroll_kids_vertically()
205            .set(state.ids.topics_align, ui);
206
207        // Define type of quest to change introduction text
208        let msg_text = self
209            .dialogue
210            .message()
211            .map(|msg| self.localized_strings.get_content(msg));
212
213        if let Some(msg_text) = msg_text {
214            state.update(|s| {
215                self.update_text(s, ui, &msg_text);
216            });
217        }
218
219        if let rtsim::DialogueKind::Question { responses, tag, .. } = &self.dialogue.kind {
220            if state.ids.quest_responses_frames.len() < responses.len() {
221                state.update(|s| {
222                    s.ids
223                        .quest_responses_frames
224                        .resize(responses.len(), &mut ui.widget_id_generator())
225                })
226            };
227            if state.ids.quest_responses_icons.len() < responses.len() {
228                state.update(|s| {
229                    s.ids
230                        .quest_responses_icons
231                        .resize(responses.len(), &mut ui.widget_id_generator())
232                })
233            };
234            if state.ids.quest_responses_amounts.len() < responses.len() {
235                state.update(|s| {
236                    s.ids
237                        .quest_responses_amounts
238                        .resize(responses.len(), &mut ui.widget_id_generator())
239                })
240            };
241            if state.ids.quest_rewards_txts.len() < responses.len() {
242                state.update(|s| {
243                    s.ids
244                        .quest_rewards_txts
245                        .resize(responses.len(), &mut ui.widget_id_generator())
246                })
247            };
248            if state.ids.quest_responses_btn.len() < responses.len() {
249                state.update(|s| {
250                    s.ids
251                        .quest_responses_btn
252                        .resize(responses.len(), &mut ui.widget_id_generator())
253                })
254            };
255
256            for (i, (response_id, response)) in responses.iter().enumerate() {
257                let frame = Button::image(self.imgs.nothing).w_h(186.0, 30.0);
258                let frame = if i == 0 {
259                    frame.top_left_with_margins_on(
260                        state.ids.topics_align,
261                        tweak!(20.0),
262                        tweak!(2.0),
263                    )
264                } else {
265                    frame.down_from(state.ids.quest_responses_frames[i - 1], 0.0)
266                };
267                frame.set(state.ids.quest_responses_frames[i], ui);
268
269                // Slot BG
270                if Button::image(self.imgs.nothing)
271                    .w_h(120.0, 40.0)
272                    .hover_image(self.imgs.nothing)
273                    .press_image(self.imgs.nothing)
274                    .middle_of(state.ids.quest_responses_frames[i])
275                    .set(state.ids.quest_responses_btn[i], ui)
276                    .was_clicked()
277                {
278                    event = Some(Event::Dialogue(self.sender, rtsim::Dialogue {
279                        id: self.dialogue.id,
280                        kind: rtsim::DialogueKind::Response {
281                            tag: *tag,
282                            response: response.clone(),
283                            response_id: *response_id,
284                        },
285                    }));
286                }
287
288                // Item image
289                if let Some((item, amount)) = &response.given_item {
290                    Image::new(animate_by_pulse(
291                        &self
292                            .item_imgs
293                            .img_ids_or_not_found_img(ItemKey::from(&**item)),
294                        self.pulse,
295                    ))
296                    .middle_of(state.ids.quest_responses_btn[i])
297                    .w_h(20.0, 20.0)
298                    .graphics_for(state.ids.quest_responses_btn[i])
299                    .set(state.ids.quest_responses_icons[i], ui);
300
301                    if *amount > 0 {
302                        Text::new(&format!("x{amount}"))
303                            .mid_bottom_with_margin_on(state.ids.quest_responses_frames[i], 3.0)
304                            .font_id(self.fonts.cyri.conrod_id)
305                            .font_size(self.fonts.cyri.scale(12))
306                            .color(TEXT_COLOR)
307                            .wrap_by_word()
308                            .set(state.ids.quest_responses_amounts[i], ui);
309                    }
310                }
311
312                Text::new(&self.localized_strings.get_content(&response.msg))
313                    .middle_of(state.ids.quest_responses_btn[i])
314                    .graphics_for(state.ids.quest_responses_btn[i])
315                    .font_id(self.fonts.cyri.conrod_id)
316                    .color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
317                    .font_size(self.fonts.cyri.scale(tweak!(14)))
318                    .set(state.ids.quest_rewards_txts[i], ui);
319            }
320        }
321
322        event
323    }
324}