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 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 const SLIDER_HEIGHT: u16 = 30;
200 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 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 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 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 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}