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 = match &self.dialogue.kind {
209            rtsim::DialogueKind::Start | rtsim::DialogueKind::End => None,
210            rtsim::DialogueKind::Statement(msg) => Some(self.localized_strings.get_content(msg)),
211            rtsim::DialogueKind::Question { msg, .. } => {
212                Some(self.localized_strings.get_content(msg))
213            },
214            rtsim::DialogueKind::Response { response, .. } => {
215                Some(self.localized_strings.get_content(&response.msg))
216            },
217        };
218
219        if let Some(msg_text) = msg_text {
220            state.update(|s| {
221                self.update_text(s, ui, &msg_text);
222            });
223        }
224
225        if let rtsim::DialogueKind::Question { responses, tag, .. } = &self.dialogue.kind {
226            if state.ids.quest_responses_frames.len() < responses.len() {
227                state.update(|s| {
228                    s.ids
229                        .quest_responses_frames
230                        .resize(responses.len(), &mut ui.widget_id_generator())
231                })
232            };
233            if state.ids.quest_responses_icons.len() < responses.len() {
234                state.update(|s| {
235                    s.ids
236                        .quest_responses_icons
237                        .resize(responses.len(), &mut ui.widget_id_generator())
238                })
239            };
240            if state.ids.quest_responses_amounts.len() < responses.len() {
241                state.update(|s| {
242                    s.ids
243                        .quest_responses_amounts
244                        .resize(responses.len(), &mut ui.widget_id_generator())
245                })
246            };
247            if state.ids.quest_rewards_txts.len() < responses.len() {
248                state.update(|s| {
249                    s.ids
250                        .quest_rewards_txts
251                        .resize(responses.len(), &mut ui.widget_id_generator())
252                })
253            };
254            if state.ids.quest_responses_btn.len() < responses.len() {
255                state.update(|s| {
256                    s.ids
257                        .quest_responses_btn
258                        .resize(responses.len(), &mut ui.widget_id_generator())
259                })
260            };
261
262            for (i, (response_id, response)) in responses.iter().enumerate() {
263                let frame = Button::image(self.imgs.nothing).w_h(186.0, 40.0);
264                let frame = if i == 0 {
265                    frame.top_left_with_margins_on(
266                        state.ids.topics_align,
267                        tweak!(20.0),
268                        tweak!(2.0),
269                    )
270                } else {
271                    frame.down_from(state.ids.quest_responses_frames[i - 1], tweak!(10.0))
272                };
273                frame.set(state.ids.quest_responses_frames[i], ui);
274
275                // Slot BG
276                if Button::image(self.imgs.nothing)
277                    .w_h(120.0, 40.0)
278                    .hover_image(self.imgs.nothing)
279                    .press_image(self.imgs.nothing)
280                    .middle_of(state.ids.quest_responses_frames[i])
281                    .set(state.ids.quest_responses_btn[i], ui)
282                    .was_clicked()
283                {
284                    event = Some(Event::Dialogue(self.sender, rtsim::Dialogue {
285                        id: self.dialogue.id,
286                        kind: rtsim::DialogueKind::Response {
287                            tag: *tag,
288                            response: response.clone(),
289                            response_id: *response_id,
290                        },
291                    }));
292                }
293
294                // Item image
295                if let Some((item, amount)) = &response.given_item {
296                    Image::new(animate_by_pulse(
297                        &self
298                            .item_imgs
299                            .img_ids_or_not_found_img(ItemKey::from(&**item)),
300                        self.pulse,
301                    ))
302                    .middle_of(state.ids.quest_responses_btn[i])
303                    .w_h(20.0, 20.0)
304                    .graphics_for(state.ids.quest_responses_btn[i])
305                    .set(state.ids.quest_responses_icons[i], ui);
306
307                    if *amount > 0 {
308                        Text::new(&format!("x{amount}"))
309                            .mid_bottom_with_margin_on(state.ids.quest_responses_frames[i], 3.0)
310                            .font_id(self.fonts.cyri.conrod_id)
311                            .font_size(self.fonts.cyri.scale(12))
312                            .color(TEXT_COLOR)
313                            .wrap_by_word()
314                            .set(state.ids.quest_responses_amounts[i], ui);
315                    }
316                }
317
318                Text::new(&self.localized_strings.get_content(&response.msg))
319                    .middle_of(state.ids.quest_responses_btn[i])
320                    .graphics_for(state.ids.quest_responses_btn[i])
321                    .font_id(self.fonts.cyri.conrod_id)
322                    .color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
323                    .font_size(self.fonts.cyri.scale(tweak!(14)))
324                    .set(state.ids.quest_rewards_txts[i], ui);
325            }
326        }
327
328        event
329    }
330}