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::KeyboardMouse => 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            } if self.earn_achievement(Achievement::FindCampfire) => {
453                self.show_hint(Hint::Campfire, Duration::from_secs(1));
454            },
455            _ => {},
456        }
457    }
458
459    pub(crate) fn event_notification(&mut self, notif: &UserNotification) {
460        #[allow(clippy::single_match)]
461        match notif {
462            UserNotification::WaypointUpdated => {
463                if self.earn_achievement(Achievement::SetWaypoint) {
464                    self.show_hint(Hint::Waypoint, Duration::from_secs(1));
465                }
466            },
467        }
468    }
469
470    pub(crate) fn event_chat_msg(&mut self, msg: &comp::ChatMsg) {
471        if msg.chat_type.is_player_msg() && self.earn_achievement(Achievement::ReceivedChatMsg) {
472            self.show_hint(Hint::Chat, Duration::from_secs(2));
473        }
474    }
475}
476
477pub struct State {
478    ids: Ids,
479}
480
481widget_ids! {
482    pub struct Ids {
483        bg,
484        text,
485        close_btn,
486        old_frame,
487        old_scrollbar,
488        old_bg[],
489        old_text[],
490        old_icon[],
491    }
492}
493
494#[derive(WidgetCommon)]
495pub struct Tutorial<'a> {
496    _show: &'a Show,
497    client: &'a Client,
498    imgs: &'a Imgs,
499    fonts: &'a Fonts,
500    localized_strings: &'a Localization,
501    global_state: &'a mut GlobalState,
502    rot_imgs: &'a ImgsRot,
503    tooltip_manager: &'a mut TooltipManager,
504    _item_imgs: &'a ItemImgs,
505    pulse: f32,
506    dt: Duration,
507    esc_menu: bool,
508
509    #[conrod(common_builder)]
510    common: widget::CommonBuilder,
511}
512
513const MARGIN: f64 = 16.0;
514
515impl<'a> Tutorial<'a> {
516    pub fn new(
517        _show: &'a Show,
518        client: &'a Client,
519        imgs: &'a Imgs,
520        fonts: &'a Fonts,
521        localized_strings: &'a Localization,
522        global_state: &'a mut GlobalState,
523        rot_imgs: &'a ImgsRot,
524        tooltip_manager: &'a mut TooltipManager,
525        _item_imgs: &'a ItemImgs,
526        pulse: f32,
527        dt: Duration,
528        esc_menu: bool,
529    ) -> Self {
530        Self {
531            _show,
532            client,
533            imgs,
534            rot_imgs,
535            fonts,
536            localized_strings,
537            global_state,
538            tooltip_manager,
539            _item_imgs,
540            pulse,
541            dt,
542            esc_menu,
543            common: widget::CommonBuilder::default(),
544        }
545    }
546}
547
548impl Widget for Tutorial<'_> {
549    type Event = ();
550    type State = State;
551    type Style = ();
552
553    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
554        Self::State {
555            ids: Ids::new(id_gen),
556        }
557    }
558
559    fn style(&self) -> Self::Style {}
560
561    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
562        let widget::UpdateArgs { state, ui, .. } = args;
563
564        // Don't render anything if the tutorial was disabled
565        if self.global_state.profile.tutorial.disabled {
566            return;
567        }
568
569        self.global_state.profile.tutorial.update(self.dt);
570        self.global_state.profile.tutorial.event_tick(self.client);
571
572        let mut old = Vec::new();
573        // TODO: Decide on whether viewing achievements is desirable after play-testing
574        if tweak!(false) && self.esc_menu {
575            old.extend(
576                self.global_state
577                    .profile
578                    .tutorial
579                    .goals
580                    .iter()
581                    .rev()
582                    .map(|n| (*n, false)),
583            );
584            old.extend(
585                self.global_state
586                    .profile
587                    .tutorial
588                    .done
589                    .iter()
590                    .rev()
591                    .map(|n| (*n, true)),
592            );
593        }
594
595        if state.ids.old_bg.len() < old.len() {
596            state.update(|s| {
597                s.ids
598                    .old_bg
599                    .resize(old.len(), &mut ui.widget_id_generator());
600                s.ids
601                    .old_text
602                    .resize(old.len(), &mut ui.widget_id_generator());
603                s.ids
604                    .old_icon
605                    .resize(old.len(), &mut ui.widget_id_generator());
606            })
607        }
608
609        if !old.is_empty() {
610            Rectangle::fill_with([tweak!(130.0), tweak!(100.0)], color::TRANSPARENT)
611                .mid_right_with_margin_on(ui.window, 0.0)
612                .scroll_kids_vertically()
613                .w_h(800.0, 350.0)
614                .set(state.ids.old_frame, ui);
615            Scrollbar::y_axis(state.ids.old_frame)
616                .thickness(5.0)
617                .auto_hide(true)
618                .rgba(1.0, 1.0, 1.0, 0.2)
619                .set(state.ids.old_scrollbar, ui);
620        }
621
622        const BACKGROUND: Color = Color::Rgba(0.13, 0.13, 0.13, 0.85);
623
624        for (i, (node, is_done)) in old.iter().copied().enumerate() {
625            let bg = RoundedRectangle::fill_with([tweak!(230.0), tweak!(100.0)], 20.0, BACKGROUND);
626            let bg = if i == 0 {
627                bg.top_left_with_margins_on(state.ids.old_frame, tweak!(8.0), tweak!(8.0))
628            } else {
629                bg.down_from(state.ids.old_bg[i - 1], 8.0)
630            };
631            bg.w_h(800.0, 52.0)
632                .parent(state.ids.old_frame)
633                .set(state.ids.old_bg[i], ui);
634
635            Image::new(if is_done {
636                self.imgs.check_checked
637            } else {
638                self.imgs.check
639            })
640            .mid_left_with_margin_on(state.ids.old_bg[i], MARGIN)
641            .w_h(24.0, 24.0)
642            .set(state.ids.old_icon[i], ui);
643
644            Text::new(&node.get_msg(
645                &self.global_state.settings,
646                &self.global_state.window.last_input(),
647                self.localized_strings,
648            ))
649            .mid_left_with_margin_on(state.ids.old_bg[i], 24.0 + MARGIN * 2.0)
650            .font_id(self.fonts.cyri.conrod_id)
651            .font_size(self.fonts.cyri.scale(16))
652            .color(TEXT_COLOR)
653            .set(state.ids.old_text[i], ui);
654        }
655
656        if let Some((current, _, anim)) = &mut self.global_state.profile.tutorial.current {
657            *anim += self.dt;
658
659            let anim_alpha = ((Hint::FADE_TIME - (anim.as_secs_f32() - Hint::FADE_TIME).abs())
660                * 3.0)
661                .clamped(0.0, 1.0);
662            let anim_movement = anim_alpha * (1.0 - (self.pulse * 3.0).sin().powi(14) * 0.25);
663
664            let close_tooltip = Tooltip::new({
665                // Edge images [t, b, r, l]
666                // Corner images [tr, tl, br, bl]
667                let edge = &self.rot_imgs.tt_side;
668                let corner = &self.rot_imgs.tt_corner;
669                ImageFrame::new(
670                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
671                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
672                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
673                    5.0,
674                )
675            })
676            .title_font_size(self.fonts.cyri.scale(15))
677            .parent(ui.window)
678            .desc_font_size(self.fonts.cyri.scale(12))
679            .font_id(self.fonts.cyri.conrod_id)
680            .desc_text_color(TEXT_COLOR);
681
682            RoundedRectangle::fill_with(
683                [130.0, 100.0],
684                20.0,
685                BACKGROUND.with_alpha(0.85 * anim_alpha),
686            )
687            .mid_top_with_margin_on(ui.window, 80.0 * anim_movement.sqrt() as f64)
688            .w_h(800.0, 52.0)
689            .set(state.ids.bg, ui);
690
691            if Button::image(self.imgs.disable_bell_btn)
692                .mid_right_with_margin_on(state.ids.bg, MARGIN)
693                .w_h(20.0, 20.0)
694                .image_color_with_feedback(Color::Rgba(1.0, 1.0, 1.0, 0.5 * anim_alpha))
695                .with_tooltip(
696                    self.tooltip_manager,
697                    &self.localized_strings.get_msg("hud-tutorial-disable"),
698                    "",
699                    &close_tooltip,
700                    TEXT_COLOR,
701                )
702                .set(state.ids.close_btn, ui)
703                .was_clicked()
704            {
705                self.global_state.profile.tutorial.disabled = true;
706            }
707
708            RichText::new(
709                &current.get_msg(
710                    &self.global_state.settings,
711                    &self.global_state.window.last_input(),
712                    self.localized_strings,
713                    self.global_state.window.controller_type(),
714                ),
715                self.imgs,
716            )
717            .mid_left_with_margin_on(state.ids.bg, MARGIN)
718            .font_id(self.fonts.cyri.conrod_id)
719            .font_size(self.fonts.cyri.scale(16))
720            .color(TEXT_COLOR.with_alpha(anim_alpha))
721            .set(state.ids.text, ui);
722        }
723    }
724}