Skip to main content

veloren_voxygen/hud/
tutorial.rs

1use crate::{
2    GlobalState, Settings,
3    hud::controller_icons as icon_utils,
4    session::interactable::{EntityInteraction, Interactable},
5    ui::{ImageFrame, RichText, Tooltip, TooltipManager, Tooltipable, fonts::Fonts},
6    window::{ControllerType, LastInput},
7};
8use client::Client;
9use common::{
10    DamageSource,
11    comp::{self, Vel},
12    resources::TimeOfDay,
13    terrain::SiteKindMeta,
14};
15use conrod_core::{
16    Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
17    widget::{self, Button, Image, Rectangle, RoundedRectangle, Scrollbar, Text},
18    widget_ids,
19};
20use i18n::Localization;
21use inline_tweak::*;
22use serde::{Deserialize, Serialize};
23use specs::WorldExt;
24use std::{borrow::Cow, time::Duration};
25use vek::*;
26
27use super::{
28    GameInput, Outcome, Show, TEXT_COLOR, UserNotification,
29    img_ids::{Imgs, ImgsRot},
30    item_imgs::ItemImgs,
31};
32
33#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub enum Hint {
35    Move,
36    Jump,
37    OpenInventory,
38    FallDamage,
39    OpenGlider,
40    Glider,
41    StallGlider,
42    Roll,
43    Attacked,
44    Unwield,
45    Campfire,
46    Waypoint,
47    OpenDiary,
48    FullInventory,
49    RespawnDurability,
50    RecipeAvailable,
51    EnergyLow,
52    Chat,
53    Sneak,
54    Lantern,
55    Zoom,
56    FirstPerson,
57    Swim,
58    OpenMap,
59    UseItem,
60    Crafting,
61}
62
63#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
64pub enum Achievement {
65    Moved,
66    Jumped,
67    OpenInventory,
68    OpenGlider,
69    StallGlider,
70    Rolled,
71    Wield,
72    Unwield,
73    FindCampfire,
74    SetWaypoint,
75    OpenDiary,
76    FullInventory,
77    Respawned,
78    RecipeAvailable,
79    OpenCrafting,
80    EnergyLow,
81    ReceivedChatMsg,
82    NearEnemies,
83    InDark,
84    UsedLantern,
85    Swim,
86    Zoom,
87    OpenMap,
88}
89
90impl Hint {
91    const FADE_TIME: f32 = 5.0;
92
93    fn get_msg<'a>(
94        &self,
95        settings: &Settings,
96        last_input: &LastInput,
97        i18n: &'a Localization,
98        ctrl: ControllerType,
99    ) -> Cow<'a, str> {
100        let get_key = |key| match last_input {
101            LastInput::Controller => icon_utils::get_controller_input_string(key, settings, ctrl)
102                .unwrap_or_else(|| icon_utils::UNBOUND_KEY.to_string()),
103            LastInput::Keyboard | LastInput::Mouse => settings
104                .controls
105                .get_binding(key)
106                .map(|key| key.display_string())
107                .unwrap_or_else(|| icon_utils::UNBOUND_KEY.to_string()),
108        };
109        let key = &format!("tutorial-{self:?}");
110        match self {
111            Self::Move => i18n.get_msg_ctx(key, &i18n::fluent_args! {
112                "w" => get_key(GameInput::MoveForward),
113                "a" => get_key(GameInput::MoveLeft),
114                "s" => get_key(GameInput::MoveBack),
115                "d" => get_key(GameInput::MoveRight),
116            }),
117            Self::Jump => i18n.get_msg_ctx(key, &i18n::fluent_args! {
118                "key" => get_key(GameInput::Jump),
119            }),
120            Self::OpenInventory => i18n.get_msg_ctx(key, &i18n::fluent_args! {
121                "key" => get_key(GameInput::Inventory),
122            }),
123            Self::FallDamage | Self::OpenGlider => i18n.get_msg_ctx(key, &i18n::fluent_args! {
124                "key" => get_key(GameInput::Glide),
125            }),
126            Self::Roll => i18n.get_msg_ctx(key, &i18n::fluent_args! {
127                "key" => get_key(GameInput::Roll),
128            }),
129            Self::Attacked => i18n.get_msg_ctx(key, &i18n::fluent_args! {
130                "key" => get_key(GameInput::Primary),
131            }),
132            Self::Unwield => i18n.get_msg_ctx(key, &i18n::fluent_args! {
133                "key" => get_key(GameInput::ToggleWield),
134            }),
135            Self::Campfire => i18n.get_msg_ctx(key, &i18n::fluent_args! {
136                "key" => get_key(GameInput::Sit),
137            }),
138            Self::OpenDiary => i18n.get_msg_ctx(key, &i18n::fluent_args! {
139                "key" => get_key(GameInput::Diary),
140            }),
141            Self::RecipeAvailable => i18n.get_msg_ctx(key, &i18n::fluent_args! {
142                "key" => get_key(GameInput::Crafting),
143            }),
144            Self::Chat => i18n.get_msg_ctx(key, &i18n::fluent_args! {
145                "key" => get_key(GameInput::Chat),
146            }),
147            Self::Sneak => i18n.get_msg_ctx(key, &i18n::fluent_args! {
148                "key" => get_key(GameInput::Sneak),
149            }),
150            Self::Lantern => i18n.get_msg_ctx(key, &i18n::fluent_args! {
151                "key" => get_key(GameInput::ToggleLantern),
152            }),
153            Self::Zoom => i18n.get_msg_ctx(key, &i18n::fluent_args! {
154                "in" => get_key(GameInput::ZoomIn),
155                "out" => get_key(GameInput::ZoomOut),
156            }),
157            Self::Swim => i18n.get_msg_ctx(key, &i18n::fluent_args! {
158                "down" => get_key(GameInput::SwimDown),
159                "up" => get_key(GameInput::SwimUp),
160            }),
161            Self::OpenMap => i18n.get_msg_ctx(key, &i18n::fluent_args! {
162                "key" => get_key(GameInput::Map),
163            }),
164            _ => i18n.get_msg(key),
165        }
166    }
167}
168
169impl Achievement {
170    fn get_msg<'a>(
171        &self,
172        _settings: &Settings,
173        _last_input: &LastInput,
174        i18n: &'a Localization,
175    ) -> Cow<'a, str> {
176        i18n.get_msg(&format!("achievement-{self:?}"))
177    }
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
181pub struct TutorialState {
182    // (_, time_since_active)
183    current: Option<(Hint, Option<Achievement>, Duration)>,
184    // (_, cancel if achieved, time until display)
185    pending: Vec<(Hint, Option<Achievement>, Duration)>,
186
187    time_ingame: Duration,
188    goals: Vec<Achievement>,
189    done: Vec<Achievement>,
190    disabled: bool,
191}
192
193impl Default for TutorialState {
194    fn default() -> Self {
195        let mut this = Self {
196            current: None,
197            pending: Vec::new(),
198            goals: Vec::new(),
199            done: Default::default(),
200            time_ingame: Duration::ZERO,
201            disabled: false,
202        };
203        this.add_hinted_goal(Hint::Move, Achievement::Moved, Duration::from_secs(10));
204        this.show_hint(Hint::Zoom, Duration::from_mins(3));
205        this
206    }
207}
208
209impl TutorialState {
210    fn update(&mut self, dt: Duration) {
211        self.time_ingame += dt;
212        self.current.take_if(|(_, a, dur)| {
213            if a.map_or(false, |a| self.done.contains(&a)) {
214                *dur = (*dur).max(Duration::from_secs_f32(Hint::FADE_TIME * 2.0 - 1.0));
215            }
216            *dur > Duration::from_secs(12)
217        });
218        for (_, _, dur) in &mut self.pending {
219            *dur = dur.saturating_sub(dt);
220        }
221        self.pending.retain(|(hint, achievement, dur)| {
222            if dur.is_zero() && self.current.is_none() {
223                self.current = Some((*hint, *achievement, Duration::ZERO));
224                false
225            } else if let Some(a) = achievement
226                && self.done.contains(a)
227            {
228                false
229            } else {
230                true
231            }
232        });
233    }
234
235    fn done(&self, achievement: Achievement) -> bool { self.done.contains(&achievement) }
236
237    fn earn_achievement(&mut self, achievement: Achievement) -> bool {
238        if !self.done.contains(&achievement) {
239            self.done.push(achievement);
240            self.goals.retain(|a| a != &achievement);
241            self.pending.retain(|(_, a, _)| a != &Some(achievement));
242            true
243        } else {
244            false
245        }
246    }
247
248    fn add_goal(&mut self, achievement: Achievement) {
249        if !self.done.contains(&achievement) && !self.goals.contains(&achievement) {
250            self.goals.push(achievement);
251        }
252    }
253
254    fn add_hinted_goal(&mut self, hint: Hint, achievement: Achievement, timeout: Duration) {
255        if self.pending.iter().all(|(h, _, _)| h != &hint) && !self.done(achievement) {
256            self.add_goal(achievement);
257            self.pending.push((hint, Some(achievement), timeout));
258        }
259    }
260
261    fn show_hint(&mut self, hint: Hint, timeout: Duration) {
262        self.pending.push((hint, None, timeout));
263    }
264
265    pub(crate) fn event_tick(&mut self, client: &Client) {
266        if let Some(comp::CharacterState::Glide(glide)) = client.current::<comp::CharacterState>()
267            && glide.ori.look_dir().z > 0.4
268            && let Some(vel) = client.current::<Vel>()
269            && vel.0.z < -vel.0.xy().magnitude()
270            && self.earn_achievement(Achievement::StallGlider)
271        {
272            self.show_hint(Hint::StallGlider, Duration::ZERO);
273        }
274
275        if let Some(cs) = client.current::<comp::CharacterState>() {
276            if cs.is_wield() && self.earn_achievement(Achievement::Wield) {
277                self.add_hinted_goal(Hint::Unwield, Achievement::Unwield, Duration::from_mins(2));
278            }
279
280            if !cs.is_wield() && self.done(Achievement::Wield) {
281                self.earn_achievement(Achievement::Unwield);
282            }
283        }
284
285        if let Some(ps) = client.current::<comp::PhysicsState>()
286            && ps.in_liquid().is_some()
287            && self.earn_achievement(Achievement::Swim)
288        {
289            self.show_hint(Hint::Swim, Duration::from_secs(3));
290        }
291
292        if let Some(inv) = client.current::<comp::Inventory>()
293            && inv.free_slots() == 0
294            && self.earn_achievement(Achievement::FullInventory)
295        {
296            self.show_hint(Hint::FullInventory, Duration::from_secs(2));
297        }
298
299        if let Some(energy) = client.current::<comp::Energy>()
300            && energy.fraction() < 0.25
301            && self.earn_achievement(Achievement::EnergyLow)
302        {
303            self.show_hint(Hint::EnergyLow, Duration::ZERO);
304        }
305
306        if !self.done(Achievement::RecipeAvailable) && !client.available_recipes().is_empty() {
307            self.earn_achievement(Achievement::RecipeAvailable);
308            self.add_hinted_goal(
309                Hint::RecipeAvailable,
310                Achievement::OpenCrafting,
311                Duration::from_secs(1),
312            );
313        }
314
315        if self.time_ingame > Duration::from_mins(10)
316            && let Some(chunk) = client.current_chunk()
317            && let Some(pos) = client.current::<comp::Pos>()
318            && let in_cave = pos.0.z < chunk.meta().alt() - 20.0
319            && let near_enemies = matches!(
320                chunk.meta().site(),
321                Some(SiteKindMeta::Dungeon(_) | SiteKindMeta::Cave)
322            )
323            && (in_cave || near_enemies)
324            && self.earn_achievement(Achievement::NearEnemies)
325        {
326            self.show_hint(Hint::Sneak, Duration::ZERO);
327        }
328
329        if self.time_ingame > Duration::from_mins(3)
330            && client
331                .state()
332                .ecs()
333                .read_resource::<TimeOfDay>()
334                .day_period()
335                .is_dark()
336            && self.earn_achievement(Achievement::InDark)
337        {
338            self.add_hinted_goal(
339                Hint::Lantern,
340                Achievement::UsedLantern,
341                Duration::from_secs(10),
342            );
343        }
344    }
345
346    pub(crate) fn event_move(&mut self) {
347        self.earn_achievement(Achievement::Moved);
348        self.add_hinted_goal(Hint::Jump, Achievement::Jumped, Duration::from_secs(15));
349    }
350
351    pub(crate) fn event_jump(&mut self) {
352        self.earn_achievement(Achievement::Jumped);
353        self.add_hinted_goal(Hint::Roll, Achievement::Rolled, Duration::from_secs(30));
354    }
355
356    pub(crate) fn event_roll(&mut self) {
357        self.earn_achievement(Achievement::Rolled);
358        self.add_hinted_goal(
359            Hint::OpenGlider,
360            Achievement::OpenGlider,
361            Duration::from_mins(2),
362        );
363    }
364
365    pub(crate) fn event_collect(&mut self) {
366        self.add_hinted_goal(
367            Hint::OpenInventory,
368            Achievement::OpenInventory,
369            Duration::from_secs(1),
370        );
371    }
372
373    pub(crate) fn event_respawn(&mut self) {
374        if self.earn_achievement(Achievement::Respawned) {
375            self.show_hint(Hint::RespawnDurability, Duration::from_secs(5));
376        }
377    }
378
379    pub(crate) fn event_open_inventory(&mut self) {
380        if self.earn_achievement(Achievement::OpenInventory) {
381            self.show_hint(Hint::UseItem, Duration::from_secs(1));
382        }
383    }
384
385    pub(crate) fn event_open_diary(&mut self) { self.earn_achievement(Achievement::OpenDiary); }
386
387    pub(crate) fn event_open_crafting(&mut self) {
388        if self.earn_achievement(Achievement::OpenCrafting) {
389            self.show_hint(Hint::Crafting, Duration::from_secs(1));
390        }
391    }
392
393    pub(crate) fn event_open_map(&mut self) { self.earn_achievement(Achievement::OpenMap); }
394
395    pub(crate) fn event_map_marker(&mut self) {
396        self.add_hinted_goal(Hint::OpenMap, Achievement::OpenMap, Duration::from_secs(1));
397    }
398
399    pub(crate) fn event_lantern(&mut self) { self.earn_achievement(Achievement::UsedLantern); }
400
401    pub(crate) fn event_zoom(&mut self, delta: f32) {
402        if delta < 0.0
403            && self.time_ingame > Duration::from_mins(15)
404            && self.earn_achievement(Achievement::Zoom)
405        {
406            self.show_hint(Hint::FirstPerson, Duration::from_secs(2));
407        }
408    }
409
410    pub(crate) fn event_outcome(&mut self, client: &Client, outcome: &Outcome) {
411        match outcome {
412            Outcome::HealthChange { info, .. }
413                if Some(info.target) == client.uid()
414                    && info.cause == Some(DamageSource::Falling) =>
415            {
416                self.add_hinted_goal(
417                    Hint::FallDamage,
418                    Achievement::OpenGlider,
419                    Duration::from_secs(1),
420                );
421            },
422            Outcome::HealthChange { info, .. }
423                if Some(info.target) == client.uid()
424                    && !matches!(info.cause, Some(DamageSource::Falling))
425                    && info.amount < 0.0 =>
426            {
427                self.add_hinted_goal(Hint::Attacked, Achievement::Wield, Duration::ZERO);
428            },
429            Outcome::SkillPointGain { uid, .. } if Some(*uid) == client.uid() => {
430                self.add_hinted_goal(
431                    Hint::OpenDiary,
432                    Achievement::OpenDiary,
433                    Duration::from_secs(3),
434                );
435            },
436            _ => {},
437        }
438    }
439
440    pub(crate) fn event_open_glider(&mut self) {
441        if self.earn_achievement(Achievement::OpenGlider) {
442            self.show_hint(Hint::Glider, Duration::from_secs(1));
443        }
444    }
445
446    pub(crate) fn event_find_interactable(&mut self, inter: &Interactable) {
447        #[allow(clippy::single_match)]
448        match inter {
449            Interactable::Entity {
450                interaction: EntityInteraction::CampfireSit,
451                ..
452            } => {
453                if self.earn_achievement(Achievement::FindCampfire) {
454                    self.show_hint(Hint::Campfire, Duration::from_secs(1));
455                }
456            },
457            _ => {},
458        }
459    }
460
461    pub(crate) fn event_notification(&mut self, notif: &UserNotification) {
462        #[allow(clippy::single_match)]
463        match notif {
464            UserNotification::WaypointUpdated => {
465                if self.earn_achievement(Achievement::SetWaypoint) {
466                    self.show_hint(Hint::Waypoint, Duration::from_secs(1));
467                }
468            },
469        }
470    }
471
472    pub(crate) fn event_chat_msg(&mut self, msg: &comp::ChatMsg) {
473        if msg.chat_type.is_player_msg() && self.earn_achievement(Achievement::ReceivedChatMsg) {
474            self.show_hint(Hint::Chat, Duration::from_secs(2));
475        }
476    }
477}
478
479pub struct State {
480    ids: Ids,
481}
482
483widget_ids! {
484    pub struct Ids {
485        bg,
486        text,
487        close_btn,
488        old_frame,
489        old_scrollbar,
490        old_bg[],
491        old_text[],
492        old_icon[],
493    }
494}
495
496#[derive(WidgetCommon)]
497pub struct Tutorial<'a> {
498    _show: &'a Show,
499    client: &'a Client,
500    imgs: &'a Imgs,
501    fonts: &'a Fonts,
502    localized_strings: &'a Localization,
503    global_state: &'a mut GlobalState,
504    rot_imgs: &'a ImgsRot,
505    tooltip_manager: &'a mut TooltipManager,
506    _item_imgs: &'a ItemImgs,
507    pulse: f32,
508    dt: Duration,
509    esc_menu: bool,
510
511    #[conrod(common_builder)]
512    common: widget::CommonBuilder,
513}
514
515const MARGIN: f64 = 16.0;
516
517impl<'a> Tutorial<'a> {
518    pub fn new(
519        _show: &'a Show,
520        client: &'a Client,
521        imgs: &'a Imgs,
522        fonts: &'a Fonts,
523        localized_strings: &'a Localization,
524        global_state: &'a mut GlobalState,
525        rot_imgs: &'a ImgsRot,
526        tooltip_manager: &'a mut TooltipManager,
527        _item_imgs: &'a ItemImgs,
528        pulse: f32,
529        dt: Duration,
530        esc_menu: bool,
531    ) -> Self {
532        Self {
533            _show,
534            client,
535            imgs,
536            rot_imgs,
537            fonts,
538            localized_strings,
539            global_state,
540            tooltip_manager,
541            _item_imgs,
542            pulse,
543            dt,
544            esc_menu,
545            common: widget::CommonBuilder::default(),
546        }
547    }
548}
549
550impl Widget for Tutorial<'_> {
551    type Event = ();
552    type State = State;
553    type Style = ();
554
555    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
556        Self::State {
557            ids: Ids::new(id_gen),
558        }
559    }
560
561    fn style(&self) -> Self::Style {}
562
563    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
564        let widget::UpdateArgs { state, ui, .. } = args;
565
566        // Don't render anything if the tutorial was disabled
567        if self.global_state.profile.tutorial.disabled {
568            return;
569        }
570
571        self.global_state.profile.tutorial.update(self.dt);
572        self.global_state.profile.tutorial.event_tick(self.client);
573
574        let mut old = Vec::new();
575        // TODO: Decide on whether viewing achievements is desirable after play-testing
576        if tweak!(false) && self.esc_menu {
577            old.extend(
578                self.global_state
579                    .profile
580                    .tutorial
581                    .goals
582                    .iter()
583                    .rev()
584                    .map(|n| (*n, false)),
585            );
586            old.extend(
587                self.global_state
588                    .profile
589                    .tutorial
590                    .done
591                    .iter()
592                    .rev()
593                    .map(|n| (*n, true)),
594            );
595        }
596
597        if state.ids.old_bg.len() < old.len() {
598            state.update(|s| {
599                s.ids
600                    .old_bg
601                    .resize(old.len(), &mut ui.widget_id_generator());
602                s.ids
603                    .old_text
604                    .resize(old.len(), &mut ui.widget_id_generator());
605                s.ids
606                    .old_icon
607                    .resize(old.len(), &mut ui.widget_id_generator());
608            })
609        }
610
611        if !old.is_empty() {
612            Rectangle::fill_with([tweak!(130.0), tweak!(100.0)], color::TRANSPARENT)
613                .mid_right_with_margin_on(ui.window, 0.0)
614                .scroll_kids_vertically()
615                .w_h(800.0, 350.0)
616                .set(state.ids.old_frame, ui);
617            Scrollbar::y_axis(state.ids.old_frame)
618                .thickness(5.0)
619                .auto_hide(true)
620                .rgba(1.0, 1.0, 1.0, 0.2)
621                .set(state.ids.old_scrollbar, ui);
622        }
623
624        const BACKGROUND: Color = Color::Rgba(0.13, 0.13, 0.13, 0.85);
625
626        for (i, (node, is_done)) in old.iter().copied().enumerate() {
627            let bg = RoundedRectangle::fill_with([tweak!(230.0), tweak!(100.0)], 20.0, BACKGROUND);
628            let bg = if i == 0 {
629                bg.top_left_with_margins_on(state.ids.old_frame, tweak!(8.0), tweak!(8.0))
630            } else {
631                bg.down_from(state.ids.old_bg[i - 1], 8.0)
632            };
633            bg.w_h(800.0, 52.0)
634                .parent(state.ids.old_frame)
635                .set(state.ids.old_bg[i], ui);
636
637            Image::new(if is_done {
638                self.imgs.check_checked
639            } else {
640                self.imgs.check
641            })
642            .mid_left_with_margin_on(state.ids.old_bg[i], MARGIN)
643            .w_h(24.0, 24.0)
644            .set(state.ids.old_icon[i], ui);
645
646            Text::new(&node.get_msg(
647                &self.global_state.settings,
648                &self.global_state.window.last_input(),
649                self.localized_strings,
650            ))
651            .mid_left_with_margin_on(state.ids.old_bg[i], 24.0 + MARGIN * 2.0)
652            .font_id(self.fonts.cyri.conrod_id)
653            .font_size(self.fonts.cyri.scale(16))
654            .color(TEXT_COLOR)
655            .set(state.ids.old_text[i], ui);
656        }
657
658        if let Some((current, _, anim)) = &mut self.global_state.profile.tutorial.current {
659            *anim += self.dt;
660
661            let anim_alpha = ((Hint::FADE_TIME - (anim.as_secs_f32() - Hint::FADE_TIME).abs())
662                * 3.0)
663                .clamped(0.0, 1.0);
664            let anim_movement = anim_alpha * (1.0 - (self.pulse * 3.0).sin().powi(14) * 0.25);
665
666            let close_tooltip = Tooltip::new({
667                // Edge images [t, b, r, l]
668                // Corner images [tr, tl, br, bl]
669                let edge = &self.rot_imgs.tt_side;
670                let corner = &self.rot_imgs.tt_corner;
671                ImageFrame::new(
672                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
673                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
674                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
675                    5.0,
676                )
677            })
678            .title_font_size(self.fonts.cyri.scale(15))
679            .parent(ui.window)
680            .desc_font_size(self.fonts.cyri.scale(12))
681            .font_id(self.fonts.cyri.conrod_id)
682            .desc_text_color(TEXT_COLOR);
683
684            RoundedRectangle::fill_with(
685                [130.0, 100.0],
686                20.0,
687                BACKGROUND.with_alpha(0.85 * anim_alpha),
688            )
689            .mid_top_with_margin_on(ui.window, 80.0 * anim_movement.sqrt() as f64)
690            .w_h(800.0, 52.0)
691            .set(state.ids.bg, ui);
692
693            if Button::image(self.imgs.disable_bell_btn)
694                .mid_right_with_margin_on(state.ids.bg, MARGIN)
695                .w_h(20.0, 20.0)
696                .image_color_with_feedback(Color::Rgba(1.0, 1.0, 1.0, 0.5 * anim_alpha))
697                .with_tooltip(
698                    self.tooltip_manager,
699                    &self.localized_strings.get_msg("hud-tutorial-disable"),
700                    "",
701                    &close_tooltip,
702                    TEXT_COLOR,
703                )
704                .set(state.ids.close_btn, ui)
705                .was_clicked()
706            {
707                self.global_state.profile.tutorial.disabled = true;
708            }
709
710            RichText::new(
711                &current.get_msg(
712                    &self.global_state.settings,
713                    &self.global_state.window.last_input(),
714                    self.localized_strings,
715                    self.global_state.window.controller_type(),
716                ),
717                self.imgs,
718            )
719            .mid_left_with_margin_on(state.ids.bg, MARGIN)
720            .font_id(self.fonts.cyri.conrod_id)
721            .font_size(self.fonts.cyri.scale(16))
722            .color(TEXT_COLOR.with_alpha(anim_alpha))
723            .set(state.ids.text, ui);
724        }
725    }
726}