veloren_voxygen/menu/main/ui/
world_selector.rs

1use common::resources::MapKind;
2use i18n::Localization;
3use iced::{
4    Align, Button, Column, Container, Length, Row, Scrollable, Slider, Space, Text, TextInput,
5    button, scrollable, slider, text_input,
6};
7use rand::Rng;
8use vek::Rgba;
9
10use crate::{
11    menu::main::ui::{FILL_FRAC_TWO, WorldsChange},
12    ui::{
13        fonts::IcedFonts,
14        ice::{
15            Element,
16            component::neat_button,
17            style,
18            widget::{
19                BackgroundContainer, Image, Overlay, Padding,
20                compound_graphic::{CompoundGraphic, Graphic},
21            },
22        },
23    },
24};
25
26use super::{Imgs, Message};
27
28const INPUT_TEXT_SIZE: u16 = 20;
29
30#[derive(Clone)]
31pub enum Confirmation {
32    Regenerate(usize),
33    Delete(usize),
34}
35
36#[derive(Default)]
37pub struct Screen {
38    back_button: button::State,
39    play_button: button::State,
40    new_button: button::State,
41    yes_button: button::State,
42    no_button: button::State,
43
44    worlds_buttons: Vec<button::State>,
45
46    selection_list: scrollable::State,
47
48    world_name: text_input::State,
49    map_seed: text_input::State,
50    day_length: slider::State,
51    random_seed_button: button::State,
52    world_size_x: slider::State,
53    world_size_y: slider::State,
54
55    map_vertical_scale: slider::State,
56    shape_buttons: enum_map::EnumMap<MapKind, button::State>,
57    map_erosion_quality: slider::State,
58
59    delete_world: button::State,
60    regenerate_map: button::State,
61    generate_map: button::State,
62
63    pub confirmation: Option<Confirmation>,
64}
65
66impl Screen {
67    pub(super) fn view(
68        &mut self,
69        fonts: &IcedFonts,
70        imgs: &Imgs,
71        worlds: &crate::singleplayer::SingleplayerWorlds,
72        i18n: &Localization,
73        button_style: style::button::Style,
74    ) -> Element<Message> {
75        let input_text_size = fonts.cyri.scale(INPUT_TEXT_SIZE);
76
77        let worlds_count = worlds.worlds.len();
78        if self.worlds_buttons.len() != worlds_count {
79            self.worlds_buttons = vec![Default::default(); worlds_count];
80        }
81
82        let title = Text::new(i18n.get_msg("gameinput-map"))
83            .size(fonts.cyri.scale(35))
84            .horizontal_alignment(iced::HorizontalAlignment::Center);
85
86        let mut list = Scrollable::new(&mut self.selection_list)
87            .spacing(8)
88            .height(Length::Fill)
89            .align_items(Align::Start);
90
91        let list_items = self
92            .worlds_buttons
93            .iter_mut()
94            .zip(
95                worlds
96                    .worlds
97                    .iter()
98                    .enumerate()
99                    .map(|(i, w)| (Some(i), &w.name)),
100            )
101            .map(|(state, (i, map))| {
102                let color = if i == worlds.current {
103                    (97, 255, 18)
104                } else {
105                    (97, 97, 25)
106                };
107                let button = Button::new(
108                    state,
109                    Row::with_children(vec![
110                        Space::new(Length::FillPortion(5), Length::Units(0)).into(),
111                        Text::new(map)
112                            .width(Length::FillPortion(95))
113                            .size(fonts.cyri.scale(25))
114                            .vertical_alignment(iced::VerticalAlignment::Center)
115                            .into(),
116                    ]),
117                )
118                .style(
119                    style::button::Style::new(imgs.selection)
120                        .hover_image(imgs.selection_hover)
121                        .press_image(imgs.selection_press)
122                        .image_color(Rgba::new(color.0, color.1, color.2, 192)),
123                )
124                .min_height(56)
125                .on_press(Message::WorldChanged(super::WorldsChange::SetActive(i)));
126                Row::with_children(vec![
127                    Space::new(Length::FillPortion(3), Length::Units(0)).into(),
128                    button.width(Length::FillPortion(92)).into(),
129                    Space::new(Length::FillPortion(5), Length::Units(0)).into(),
130                ])
131            });
132
133        for item in list_items {
134            list = list.push(item);
135        }
136
137        let new_button = Container::new(neat_button(
138            &mut self.new_button,
139            i18n.get_msg("main-singleplayer-new"),
140            FILL_FRAC_TWO,
141            button_style,
142            Some(Message::WorldChanged(super::WorldsChange::AddNew)),
143        ))
144        .center_x()
145        .max_width(200);
146
147        let back_button = Container::new(neat_button(
148            &mut self.back_button,
149            i18n.get_msg("common-back"),
150            FILL_FRAC_TWO,
151            button_style,
152            Some(Message::Back),
153        ))
154        .center_x()
155        .max_width(200);
156
157        let content = Column::with_children(vec![
158            title.into(),
159            list.into(),
160            new_button.into(),
161            back_button.into(),
162        ])
163        .spacing(8)
164        .width(Length::Fill)
165        .height(Length::FillPortion(38))
166        .align_items(Align::Center)
167        .padding(iced::Padding {
168            bottom: 25,
169            ..iced::Padding::new(0)
170        });
171
172        let selection_menu = BackgroundContainer::new(
173            CompoundGraphic::from_graphics(vec![
174                Graphic::image(imgs.banner_top, [138, 17], [0, 0]),
175                Graphic::rect(Rgba::new(0, 0, 0, 230), [130, 300], [4, 17]),
176                // TODO: use non image gradient
177                Graphic::gradient(Rgba::new(0, 0, 0, 230), Rgba::zero(), [130, 50], [4, 182]),
178            ])
179            .fix_aspect_ratio()
180            .height(Length::Fill)
181            .width(Length::Fill),
182            content,
183        )
184        .padding(Padding::new().horizontal(5).top(15));
185        let mut items = vec![selection_menu.into()];
186
187        if let Some(i) = worlds.current {
188            let world = &worlds.worlds[i];
189            let can_edit = !world.is_generated;
190            let message = |m| Message::WorldChanged(super::WorldsChange::CurrentWorldChange(m));
191
192            use super::WorldChange;
193
194            const SLIDER_TEXT_SIZE: u16 = 20;
195            const SLIDER_CURSOR_SIZE: (u16, u16) = (9, 21);
196            const SLIDER_BAR_HEIGHT: u16 = 9;
197            const SLIDER_BAR_PAD: u16 = 0;
198            // Height of interactable area
199            const SLIDER_HEIGHT: u16 = 30;
200            // Day length slider values
201            pub const DAY_LENGTH_MIN: f64 = 10.0;
202            pub const DAY_LENGTH_MAX: f64 = 60.0;
203
204            let mut gen_content = vec![
205                BackgroundContainer::new(
206                    Image::new(imgs.input_bg)
207                        .width(Length::Units(230))
208                        .fix_aspect_ratio(),
209                    Element::from(
210                        TextInput::new(
211                            &mut self.world_name,
212                            &i18n.get_msg("main-singleplayer-world_name"),
213                            &world.name,
214                            move |s| message(WorldChange::Name(s)),
215                        )
216                        .size(input_text_size),
217                    ),
218                )
219                .padding(Padding::new().horizontal(7).top(5))
220                .into(),
221            ];
222
223            let seed = world.seed;
224            let seed_str = i18n.get_msg("main-singleplayer-seed");
225            let mut seed_content = vec![
226                Column::with_children(vec![
227                    Text::new(seed_str.to_string())
228                        .size(SLIDER_TEXT_SIZE)
229                        .horizontal_alignment(iced::HorizontalAlignment::Center)
230                        .into(),
231                ])
232                .padding(iced::Padding::new(5))
233                .into(),
234                BackgroundContainer::new(
235                    Image::new(imgs.input_bg)
236                        .width(Length::Units(190))
237                        .fix_aspect_ratio(),
238                    if can_edit {
239                        Element::from(
240                            TextInput::new(
241                                &mut self.map_seed,
242                                &seed_str,
243                                &seed.to_string(),
244                                move |s| {
245                                    if let Ok(seed) = if s.is_empty() {
246                                        Ok(0)
247                                    } else {
248                                        s.parse::<u32>()
249                                    } {
250                                        message(WorldChange::Seed(seed))
251                                    } else {
252                                        message(WorldChange::Seed(seed))
253                                    }
254                                },
255                            )
256                            .size(input_text_size),
257                        )
258                    } else {
259                        Text::new(world.seed.to_string())
260                            .size(input_text_size)
261                            .width(Length::Fill)
262                            .height(Length::Shrink)
263                            .into()
264                    },
265                )
266                .padding(Padding::new().horizontal(7).top(5))
267                .into(),
268            ];
269
270            if can_edit {
271                seed_content.push(
272                    Container::new(neat_button(
273                        &mut self.random_seed_button,
274                        i18n.get_msg("main-singleplayer-random_seed"),
275                        FILL_FRAC_TWO,
276                        button_style,
277                        Some(message(WorldChange::Seed(rand::thread_rng().gen()))),
278                    ))
279                    .max_width(200)
280                    .into(),
281                )
282            }
283
284            gen_content.push(Row::with_children(seed_content).into());
285
286            if let Some(gen_opts) = world.gen_opts.as_ref() {
287                // Day length setting label
288                gen_content.push(
289                    Text::new(format!(
290                        "{}: {}",
291                        i18n.get_msg("main-singleplayer-day_length"),
292                        world.day_length
293                    ))
294                    .size(SLIDER_TEXT_SIZE)
295                    .horizontal_alignment(iced::HorizontalAlignment::Center)
296                    .into(),
297                );
298
299                // Day length setting slider
300                if can_edit {
301                    gen_content.push(
302                        Row::with_children(vec![
303                            Slider::new(
304                                &mut self.day_length,
305                                DAY_LENGTH_MIN..=DAY_LENGTH_MAX,
306                                world.day_length,
307                                move |d| message(WorldChange::DayLength(d)),
308                            )
309                            .height(SLIDER_HEIGHT)
310                            .style(style::slider::Style::images(
311                                imgs.slider_indicator,
312                                imgs.slider_range,
313                                SLIDER_BAR_PAD,
314                                SLIDER_CURSOR_SIZE,
315                                SLIDER_BAR_HEIGHT,
316                            ))
317                            .into(),
318                        ])
319                        .into(),
320                    )
321                }
322
323                gen_content.push(
324                    Text::new(format!(
325                        "{}: x: {}, y: {}",
326                        i18n.get_msg("main-singleplayer-size_lg"),
327                        gen_opts.x_lg,
328                        gen_opts.y_lg
329                    ))
330                    .size(SLIDER_TEXT_SIZE)
331                    .horizontal_alignment(iced::HorizontalAlignment::Center)
332                    .into(),
333                );
334
335                if can_edit {
336                    gen_content.push(
337                        Row::with_children(vec![
338                            Slider::new(&mut self.world_size_x, 4..=13, gen_opts.x_lg, move |s| {
339                                message(WorldChange::SizeX(s))
340                            })
341                            .height(SLIDER_HEIGHT)
342                            .style(style::slider::Style::images(
343                                imgs.slider_indicator,
344                                imgs.slider_range,
345                                SLIDER_BAR_PAD,
346                                SLIDER_CURSOR_SIZE,
347                                SLIDER_BAR_HEIGHT,
348                            ))
349                            .into(),
350                            Slider::new(&mut self.world_size_y, 4..=13, gen_opts.y_lg, move |s| {
351                                message(WorldChange::SizeY(s))
352                            })
353                            .height(SLIDER_HEIGHT)
354                            .style(style::slider::Style::images(
355                                imgs.slider_indicator,
356                                imgs.slider_range,
357                                SLIDER_BAR_PAD,
358                                SLIDER_CURSOR_SIZE,
359                                SLIDER_BAR_HEIGHT,
360                            ))
361                            .into(),
362                        ])
363                        .into(),
364                    );
365                    let height = Length::Units(86);
366                    if gen_opts.x_lg + gen_opts.y_lg >= 19 {
367                        let mut msg = i18n
368                            .get_msg("main-singleplayer-map_large_warning")
369                            .into_owned();
370                        let default_ops = server::GenOpts::default();
371                        if let Some(s) = (gen_opts.x_lg + gen_opts.y_lg)
372                            .checked_sub(default_ops.x_lg + default_ops.y_lg)
373                        {
374                            // Memory usages would still be more even if `erosion_quality`
375                            // is less than 1.0 so only multiply by that if it's greater
376                            // than 1.
377                            let count = ((1 << s) as f32 * gen_opts.erosion_quality.max(1.0))
378                                .round() as u32;
379                            if count > 1 {
380                                msg.push(' ');
381                                msg.push_str(&i18n.get_msg_ctx(
382                                    "main-singleplayer-map_large_extra_warning",
383                                    &i18n::fluent_args! {
384                                        "count" => count,
385                                    },
386                                ));
387                            }
388                        }
389                        gen_content.push(
390                            Text::new(msg)
391                                .size(SLIDER_TEXT_SIZE)
392                                .height(height)
393                                .color([0.914, 0.835, 0.008])
394                                .horizontal_alignment(iced::HorizontalAlignment::Center)
395                                .into(),
396                        );
397                    } else {
398                        gen_content.push(Space::new(Length::Units(0), height).into());
399                    }
400                }
401
402                gen_content.push(
403                    Text::new(format!(
404                        "{}: {}",
405                        i18n.get_msg("main-singleplayer-map_scale"),
406                        gen_opts.scale
407                    ))
408                    .size(SLIDER_TEXT_SIZE)
409                    .horizontal_alignment(iced::HorizontalAlignment::Center)
410                    .into(),
411                );
412
413                if can_edit {
414                    gen_content.push(
415                        Slider::new(
416                            &mut self.map_vertical_scale,
417                            0.1..=160.0,
418                            gen_opts.scale * 10.0,
419                            move |s| message(WorldChange::Scale(s / 10.0)),
420                        )
421                        .height(SLIDER_HEIGHT)
422                        .style(style::slider::Style::images(
423                            imgs.slider_indicator,
424                            imgs.slider_range,
425                            SLIDER_BAR_PAD,
426                            SLIDER_CURSOR_SIZE,
427                            SLIDER_BAR_HEIGHT,
428                        ))
429                        .into(),
430                    );
431                }
432
433                if can_edit {
434                    gen_content.extend([
435                        Text::new(i18n.get_msg("main-singleplayer-map_shape"))
436                            .size(SLIDER_TEXT_SIZE)
437                            .horizontal_alignment(iced::HorizontalAlignment::Center)
438                            .into(),
439                        Row::with_children(
440                            self.shape_buttons
441                                .iter_mut()
442                                .map(|(shape, state)| {
443                                    let color = if gen_opts.map_kind == shape {
444                                        (97, 255, 18)
445                                    } else {
446                                        (97, 97, 25)
447                                    };
448                                    Button::new(
449                                        state,
450                                        Row::with_children(vec![
451                                            Space::new(Length::FillPortion(5), Length::Units(0))
452                                                .into(),
453                                            Text::new(i18n.get_msg(Self::map_kind_key(shape)))
454                                                .width(Length::FillPortion(95))
455                                                .size(fonts.cyri.scale(14))
456                                                .vertical_alignment(iced::VerticalAlignment::Center)
457                                                .into(),
458                                        ])
459                                        .align_items(Align::Center),
460                                    )
461                                    .style(
462                                        style::button::Style::new(imgs.selection)
463                                            .hover_image(imgs.selection_hover)
464                                            .press_image(imgs.selection_press)
465                                            .image_color(Rgba::new(color.0, color.1, color.2, 192)),
466                                    )
467                                    .width(Length::FillPortion(1))
468                                    .min_height(18)
469                                    .on_press(Message::WorldChanged(
470                                        super::WorldsChange::CurrentWorldChange(
471                                            WorldChange::MapKind(shape),
472                                        ),
473                                    ))
474                                    .into()
475                                })
476                                .collect(),
477                        )
478                        .into(),
479                    ]);
480                } else {
481                    gen_content.push(
482                        Text::new(format!(
483                            "{}: {}",
484                            i18n.get_msg("main-singleplayer-map_shape"),
485                            gen_opts.map_kind,
486                        ))
487                        .size(SLIDER_TEXT_SIZE)
488                        .horizontal_alignment(iced::HorizontalAlignment::Center)
489                        .into(),
490                    );
491                }
492
493                gen_content.push(
494                    Text::new(format!(
495                        "{}: {}",
496                        i18n.get_msg("main-singleplayer-map_erosion_quality"),
497                        gen_opts.erosion_quality
498                    ))
499                    .size(SLIDER_TEXT_SIZE)
500                    .horizontal_alignment(iced::HorizontalAlignment::Center)
501                    .into(),
502                );
503
504                if can_edit {
505                    gen_content.push(
506                        Slider::new(
507                            &mut self.map_erosion_quality,
508                            0.0..=20.0,
509                            gen_opts.erosion_quality * 10.0,
510                            move |s| message(WorldChange::ErosionQuality(s / 10.0)),
511                        )
512                        .height(SLIDER_HEIGHT)
513                        .style(style::slider::Style::images(
514                            imgs.slider_indicator,
515                            imgs.slider_range,
516                            SLIDER_BAR_PAD,
517                            SLIDER_CURSOR_SIZE,
518                            SLIDER_BAR_HEIGHT,
519                        ))
520                        .into(),
521                    );
522                }
523            }
524
525            let mut world_buttons = vec![];
526
527            if world.gen_opts.is_none() && can_edit {
528                let create_custom = Container::new(neat_button(
529                    &mut self.regenerate_map,
530                    i18n.get_msg("main-singleplayer-create_custom"),
531                    FILL_FRAC_TWO,
532                    button_style,
533                    Some(Message::WorldChanged(
534                        super::WorldsChange::CurrentWorldChange(WorldChange::DefaultGenOps),
535                    )),
536                ))
537                .center_x()
538                .width(Length::FillPortion(1))
539                .max_width(200);
540                world_buttons.push(create_custom.into());
541            }
542
543            if world.is_generated {
544                let regenerate = Container::new(neat_button(
545                    &mut self.generate_map,
546                    i18n.get_msg("main-singleplayer-regenerate"),
547                    FILL_FRAC_TWO,
548                    button_style,
549                    Some(Message::WorldConfirmation(Confirmation::Regenerate(i))),
550                ))
551                .center_x()
552                .width(Length::FillPortion(1))
553                .max_width(200);
554                world_buttons.push(regenerate.into())
555            }
556            let delete = Container::new(neat_button(
557                &mut self.delete_world,
558                i18n.get_msg("main-singleplayer-delete"),
559                FILL_FRAC_TWO,
560                button_style,
561                Some(Message::WorldConfirmation(Confirmation::Delete(i))),
562            ))
563            .center_x()
564            .width(Length::FillPortion(1))
565            .max_width(200);
566
567            world_buttons.push(delete.into());
568
569            gen_content.push(Row::with_children(world_buttons).into());
570
571            let play_button = Container::new(neat_button(
572                &mut self.play_button,
573                i18n.get_msg(if world.is_generated || world.gen_opts.is_none() {
574                    "main-singleplayer-play"
575                } else {
576                    "main-singleplayer-generate_and_play"
577                }),
578                FILL_FRAC_TWO,
579                button_style,
580                Some(Message::SingleplayerPlay),
581            ))
582            .center_x()
583            .max_width(200);
584
585            gen_content.push(play_button.into());
586
587            let gen_opts = Column::with_children(gen_content).align_items(Align::Center);
588
589            let opts_menu = BackgroundContainer::new(
590                CompoundGraphic::from_graphics(vec![
591                    Graphic::image(imgs.banner_top, [138, 17], [0, 0]),
592                    Graphic::rect(Rgba::new(0, 0, 0, 230), [130, 300], [4, 17]),
593                    // TODO: use non image gradient
594                    Graphic::gradient(Rgba::new(0, 0, 0, 230), Rgba::zero(), [130, 50], [4, 182]),
595                ])
596                .fix_aspect_ratio()
597                .height(Length::Fill)
598                .width(Length::Fill),
599                gen_opts,
600            )
601            .padding(Padding::new().horizontal(5).top(15));
602
603            items.push(opts_menu.into());
604        }
605
606        let all = Row::with_children(items)
607            .height(Length::Fill)
608            .width(Length::Fill);
609
610        if let Some(confirmation) = self.confirmation.as_ref() {
611            const FILL_FRAC_ONE: f32 = 0.77;
612
613            let (text, yes_msg, index) = match confirmation {
614                Confirmation::Regenerate(i) => (
615                    "menu-singleplayer-confirm_regenerate",
616                    Message::WorldChanged(WorldsChange::Regenerate(*i)),
617                    i,
618                ),
619                Confirmation::Delete(i) => (
620                    "menu-singleplayer-confirm_delete",
621                    Message::WorldChanged(WorldsChange::Delete(*i)),
622                    i,
623                ),
624            };
625
626            if let Some(name) = worlds.worlds.get(*index).map(|world| &world.name) {
627                let over_content = Column::with_children(vec![
628                    Text::new(i18n.get_msg_ctx(text, &i18n::fluent_args! { "world_name" => name }))
629                        .size(fonts.cyri.scale(24))
630                        .into(),
631                    Row::with_children(vec![
632                        neat_button(
633                            &mut self.no_button,
634                            i18n.get_msg("common-no").into_owned(),
635                            FILL_FRAC_ONE,
636                            button_style,
637                            Some(Message::WorldCancelConfirmation),
638                        ),
639                        neat_button(
640                            &mut self.yes_button,
641                            i18n.get_msg("common-yes").into_owned(),
642                            FILL_FRAC_ONE,
643                            button_style,
644                            Some(yes_msg),
645                        ),
646                    ])
647                    .height(Length::Units(28))
648                    .spacing(30)
649                    .into(),
650                ])
651                .align_items(Align::Center)
652                .spacing(10);
653
654                let over = Container::new(over_content)
655                    .style(
656                        style::container::Style::color_with_double_cornerless_border(
657                            (0, 0, 0, 200).into(),
658                            (3, 4, 4, 255).into(),
659                            (28, 28, 22, 255).into(),
660                        ),
661                    )
662                    .width(Length::Shrink)
663                    .height(Length::Shrink)
664                    .max_width(400)
665                    .max_height(500)
666                    .padding(24)
667                    .center_x()
668                    .center_y();
669
670                Overlay::new(over, all)
671                    .width(Length::Fill)
672                    .height(Length::Fill)
673                    .center_x()
674                    .center_y()
675                    .into()
676            } else {
677                self.confirmation = None;
678                all.into()
679            }
680        } else {
681            all.into()
682        }
683    }
684
685    fn map_kind_key(map_kind: MapKind) -> &'static str {
686        match map_kind {
687            MapKind::Circle => "main-singleplayer-map_shape-circle",
688            MapKind::Square => "main-singleplayer-map_shape-square",
689        }
690    }
691}