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        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        // Check if we have a new message
123        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; // Start displaying from the first character
130            state.last_displayed_text = Some(msg_text.to_string()); // Store the message
131        }
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        // Window BG
189        // TODO: It would be nice to use `RoundedRectangle` here, but unfortunately it
190        // seems to not propagate scroll events properly!
191        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        // Window frame
196        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        // // X-Button
202        // if Button::image(self.imgs.close_button)
203        //     .w_h(24.0, 25.0)
204        //     .hover_image(self.imgs.close_button_hover)
205        //     .press_image(self.imgs.close_button_press)
206        //     .top_right_with_margins_on(state.ids.frame, 0.0, 0.0)
207        //     .set(state.ids.close, ui)
208        //     .was_clicked()
209        // {
210        //     event = Some(Event::Close);
211        // }
212
213        const BACKGROUND: Color = Color::Rgba(0.0, 0.0, 0.0, 0.85);
214
215        // Content Alignment
216        // Text Left
217        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        // Topics Right
222        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        // Define type of quest to change introduction text
260        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                // Determine whether all requirements for sending the response are met
310                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                // Response text
352                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                // Item image
361                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                            // Not enough present!
381                            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}