Skip to main content

veloren_voxygen/hud/
quest.rs

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>, // New field to track the last message
35}
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        // Check if we have a new message
113        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; // Start displaying from the first character
120            state.last_displayed_text = Some(msg_text.to_string()); // Store the message
121        }
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        // Window BG
179        // TODO: It would be nice to use `RoundedRectangle` here, but unfortunately it
180        // seems to not propagate scroll events properly!
181        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        // Window frame
186        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        // Content Alignment
194        // Text Left
195        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        // Topics Right
200        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        // Close Button
213        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        // Define type of quest to change introduction text
252        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                // Determine whether all requirements for sending the response are met
302                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                // Response text
344                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                // Item image
353                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                            // Not enough present!
373                            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}