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 if can_edit {
210 Element::from(
211 TextInput::new(
212 &mut self.world_name,
213 &i18n.get_msg("main-singleplayer-world_name"),
214 &world.name,
215 move |s| message(WorldChange::Name(s)),
216 )
217 .size(input_text_size),
218 )
219 } else {
220 Text::new(&world.name)
221 .size(input_text_size)
222 .width(Length::Fill)
223 .height(Length::Shrink)
224 .into()
225 },
226 )
227 .padding(Padding::new().horizontal(7).top(5))
228 .into(),
229 ];
230
231 let seed = world.seed;
232 let seed_str = i18n.get_msg("main-singleplayer-seed");
233 let mut seed_content = vec![
234 Column::with_children(vec![
235 Text::new(seed_str.to_string())
236 .size(SLIDER_TEXT_SIZE)
237 .horizontal_alignment(iced::HorizontalAlignment::Center)
238 .into(),
239 ])
240 .padding(iced::Padding::new(5))
241 .into(),
242 BackgroundContainer::new(
243 Image::new(imgs.input_bg)
244 .width(Length::Units(190))
245 .fix_aspect_ratio(),
246 if can_edit {
247 Element::from(
248 TextInput::new(
249 &mut self.map_seed,
250 &seed_str,
251 &seed.to_string(),
252 move |s| {
253 if let Ok(seed) = if s.is_empty() {
254 Ok(0)
255 } else {
256 s.parse::<u32>()
257 } {
258 message(WorldChange::Seed(seed))
259 } else {
260 message(WorldChange::Seed(seed))
261 }
262 },
263 )
264 .size(input_text_size),
265 )
266 } else {
267 Text::new(world.seed.to_string())
268 .size(input_text_size)
269 .width(Length::Fill)
270 .height(Length::Shrink)
271 .into()
272 },
273 )
274 .padding(Padding::new().horizontal(7).top(5))
275 .into(),
276 ];
277
278 if can_edit {
279 seed_content.push(
280 Container::new(neat_button(
281 &mut self.random_seed_button,
282 i18n.get_msg("main-singleplayer-random_seed"),
283 FILL_FRAC_TWO,
284 button_style,
285 Some(message(WorldChange::Seed(rand::thread_rng().gen()))),
286 ))
287 .max_width(200)
288 .into(),
289 )
290 }
291
292 gen_content.push(Row::with_children(seed_content).into());
293
294 if let Some(gen_opts) = world.gen_opts.as_ref() {
295 gen_content.push(
297 Text::new(format!(
298 "{}: {}",
299 i18n.get_msg("main-singleplayer-day_length"),
300 world.day_length
301 ))
302 .size(SLIDER_TEXT_SIZE)
303 .horizontal_alignment(iced::HorizontalAlignment::Center)
304 .into(),
305 );
306
307 if can_edit {
309 gen_content.push(
310 Row::with_children(vec![
311 Slider::new(
312 &mut self.day_length,
313 DAY_LENGTH_MIN..=DAY_LENGTH_MAX,
314 world.day_length,
315 move |d| message(WorldChange::DayLength(d)),
316 )
317 .height(SLIDER_HEIGHT)
318 .style(style::slider::Style::images(
319 imgs.slider_indicator,
320 imgs.slider_range,
321 SLIDER_BAR_PAD,
322 SLIDER_CURSOR_SIZE,
323 SLIDER_BAR_HEIGHT,
324 ))
325 .into(),
326 ])
327 .into(),
328 )
329 }
330
331 gen_content.push(
332 Text::new(format!(
333 "{}: x: {}, y: {}",
334 i18n.get_msg("main-singleplayer-size_lg"),
335 gen_opts.x_lg,
336 gen_opts.y_lg
337 ))
338 .size(SLIDER_TEXT_SIZE)
339 .horizontal_alignment(iced::HorizontalAlignment::Center)
340 .into(),
341 );
342
343 if can_edit {
344 gen_content.push(
345 Row::with_children(vec![
346 Slider::new(&mut self.world_size_x, 4..=13, gen_opts.x_lg, move |s| {
347 message(WorldChange::SizeX(s))
348 })
349 .height(SLIDER_HEIGHT)
350 .style(style::slider::Style::images(
351 imgs.slider_indicator,
352 imgs.slider_range,
353 SLIDER_BAR_PAD,
354 SLIDER_CURSOR_SIZE,
355 SLIDER_BAR_HEIGHT,
356 ))
357 .into(),
358 Slider::new(&mut self.world_size_y, 4..=13, gen_opts.y_lg, move |s| {
359 message(WorldChange::SizeY(s))
360 })
361 .height(SLIDER_HEIGHT)
362 .style(style::slider::Style::images(
363 imgs.slider_indicator,
364 imgs.slider_range,
365 SLIDER_BAR_PAD,
366 SLIDER_CURSOR_SIZE,
367 SLIDER_BAR_HEIGHT,
368 ))
369 .into(),
370 ])
371 .into(),
372 );
373 let height = Length::Units(56);
374 if gen_opts.x_lg + gen_opts.y_lg >= 19 {
375 gen_content.push(
376 Text::new(i18n.get_msg("main-singleplayer-map_large_warning"))
377 .size(SLIDER_TEXT_SIZE)
378 .height(height)
379 .color([0.914, 0.835, 0.008])
380 .horizontal_alignment(iced::HorizontalAlignment::Center)
381 .into(),
382 );
383 } else {
384 gen_content.push(Space::new(Length::Units(0), height).into());
385 }
386 }
387
388 gen_content.push(
389 Text::new(format!(
390 "{}: {}",
391 i18n.get_msg("main-singleplayer-map_scale"),
392 gen_opts.scale
393 ))
394 .size(SLIDER_TEXT_SIZE)
395 .horizontal_alignment(iced::HorizontalAlignment::Center)
396 .into(),
397 );
398
399 if can_edit {
400 gen_content.push(
401 Slider::new(
402 &mut self.map_vertical_scale,
403 0.1..=160.0,
404 gen_opts.scale * 10.0,
405 move |s| message(WorldChange::Scale(s / 10.0)),
406 )
407 .height(SLIDER_HEIGHT)
408 .style(style::slider::Style::images(
409 imgs.slider_indicator,
410 imgs.slider_range,
411 SLIDER_BAR_PAD,
412 SLIDER_CURSOR_SIZE,
413 SLIDER_BAR_HEIGHT,
414 ))
415 .into(),
416 );
417 }
418
419 if can_edit {
420 gen_content.extend([
421 Text::new(i18n.get_msg("main-singleplayer-map_shape"))
422 .size(SLIDER_TEXT_SIZE)
423 .horizontal_alignment(iced::HorizontalAlignment::Center)
424 .into(),
425 Row::with_children(
426 self.shape_buttons
427 .iter_mut()
428 .map(|(shape, state)| {
429 let color = if gen_opts.map_kind == shape {
430 (97, 255, 18)
431 } else {
432 (97, 97, 25)
433 };
434 Button::new(
435 state,
436 Row::with_children(vec![
437 Space::new(Length::FillPortion(5), Length::Units(0))
438 .into(),
439 Text::new(i18n.get_msg(Self::map_kind_key(shape)))
440 .width(Length::FillPortion(95))
441 .size(fonts.cyri.scale(14))
442 .vertical_alignment(iced::VerticalAlignment::Center)
443 .into(),
444 ])
445 .align_items(Align::Center),
446 )
447 .style(
448 style::button::Style::new(imgs.selection)
449 .hover_image(imgs.selection_hover)
450 .press_image(imgs.selection_press)
451 .image_color(Rgba::new(color.0, color.1, color.2, 192)),
452 )
453 .width(Length::FillPortion(1))
454 .min_height(18)
455 .on_press(Message::WorldChanged(
456 super::WorldsChange::CurrentWorldChange(
457 WorldChange::MapKind(shape),
458 ),
459 ))
460 .into()
461 })
462 .collect(),
463 )
464 .into(),
465 ]);
466 } else {
467 gen_content.push(
468 Text::new(format!(
469 "{}: {}",
470 i18n.get_msg("main-singleplayer-map_shape"),
471 gen_opts.map_kind,
472 ))
473 .size(SLIDER_TEXT_SIZE)
474 .horizontal_alignment(iced::HorizontalAlignment::Center)
475 .into(),
476 );
477 }
478
479 gen_content.push(
480 Text::new(format!(
481 "{}: {}",
482 i18n.get_msg("main-singleplayer-map_erosion_quality"),
483 gen_opts.erosion_quality
484 ))
485 .size(SLIDER_TEXT_SIZE)
486 .horizontal_alignment(iced::HorizontalAlignment::Center)
487 .into(),
488 );
489
490 if can_edit {
491 gen_content.push(
492 Slider::new(
493 &mut self.map_erosion_quality,
494 0.0..=20.0,
495 gen_opts.erosion_quality * 10.0,
496 move |s| message(WorldChange::ErosionQuality(s / 10.0)),
497 )
498 .height(SLIDER_HEIGHT)
499 .style(style::slider::Style::images(
500 imgs.slider_indicator,
501 imgs.slider_range,
502 SLIDER_BAR_PAD,
503 SLIDER_CURSOR_SIZE,
504 SLIDER_BAR_HEIGHT,
505 ))
506 .into(),
507 );
508 }
509 }
510
511 let mut world_buttons = vec![];
512
513 if world.gen_opts.is_none() && can_edit {
514 let create_custom = Container::new(neat_button(
515 &mut self.regenerate_map,
516 i18n.get_msg("main-singleplayer-create_custom"),
517 FILL_FRAC_TWO,
518 button_style,
519 Some(Message::WorldChanged(
520 super::WorldsChange::CurrentWorldChange(WorldChange::DefaultGenOps),
521 )),
522 ))
523 .center_x()
524 .width(Length::FillPortion(1))
525 .max_width(200);
526 world_buttons.push(create_custom.into());
527 }
528
529 if world.is_generated {
530 let regenerate = Container::new(neat_button(
531 &mut self.generate_map,
532 i18n.get_msg("main-singleplayer-regenerate"),
533 FILL_FRAC_TWO,
534 button_style,
535 Some(Message::WorldConfirmation(Confirmation::Regenerate(i))),
536 ))
537 .center_x()
538 .width(Length::FillPortion(1))
539 .max_width(200);
540 world_buttons.push(regenerate.into())
541 }
542 let delete = Container::new(neat_button(
543 &mut self.delete_world,
544 i18n.get_msg("main-singleplayer-delete"),
545 FILL_FRAC_TWO,
546 button_style,
547 Some(Message::WorldConfirmation(Confirmation::Delete(i))),
548 ))
549 .center_x()
550 .width(Length::FillPortion(1))
551 .max_width(200);
552
553 world_buttons.push(delete.into());
554
555 gen_content.push(Row::with_children(world_buttons).into());
556
557 let play_button = Container::new(neat_button(
558 &mut self.play_button,
559 i18n.get_msg(if world.is_generated || world.gen_opts.is_none() {
560 "main-singleplayer-play"
561 } else {
562 "main-singleplayer-generate_and_play"
563 }),
564 FILL_FRAC_TWO,
565 button_style,
566 Some(Message::SingleplayerPlay),
567 ))
568 .center_x()
569 .max_width(200);
570
571 gen_content.push(play_button.into());
572
573 let gen_opts = Column::with_children(gen_content).align_items(Align::Center);
574
575 let opts_menu = BackgroundContainer::new(
576 CompoundGraphic::from_graphics(vec![
577 Graphic::image(imgs.banner_top, [138, 17], [0, 0]),
578 Graphic::rect(Rgba::new(0, 0, 0, 230), [130, 300], [4, 17]),
579 Graphic::gradient(Rgba::new(0, 0, 0, 230), Rgba::zero(), [130, 50], [4, 182]),
581 ])
582 .fix_aspect_ratio()
583 .height(Length::Fill)
584 .width(Length::Fill),
585 gen_opts,
586 )
587 .padding(Padding::new().horizontal(5).top(15));
588
589 items.push(opts_menu.into());
590 }
591
592 let all = Row::with_children(items)
593 .height(Length::Fill)
594 .width(Length::Fill);
595
596 if let Some(confirmation) = self.confirmation.as_ref() {
597 const FILL_FRAC_ONE: f32 = 0.77;
598
599 let (text, yes_msg, index) = match confirmation {
600 Confirmation::Regenerate(i) => (
601 "menu-singleplayer-confirm_regenerate",
602 Message::WorldChanged(WorldsChange::Regenerate(*i)),
603 i,
604 ),
605 Confirmation::Delete(i) => (
606 "menu-singleplayer-confirm_delete",
607 Message::WorldChanged(WorldsChange::Delete(*i)),
608 i,
609 ),
610 };
611
612 if let Some(name) = worlds.worlds.get(*index).map(|world| &world.name) {
613 let over_content = Column::with_children(vec![
614 Text::new(i18n.get_msg_ctx(text, &i18n::fluent_args! { "world_name" => name }))
615 .size(fonts.cyri.scale(24))
616 .into(),
617 Row::with_children(vec![
618 neat_button(
619 &mut self.no_button,
620 i18n.get_msg("common-no").into_owned(),
621 FILL_FRAC_ONE,
622 button_style,
623 Some(Message::WorldCancelConfirmation),
624 ),
625 neat_button(
626 &mut self.yes_button,
627 i18n.get_msg("common-yes").into_owned(),
628 FILL_FRAC_ONE,
629 button_style,
630 Some(yes_msg),
631 ),
632 ])
633 .height(Length::Units(28))
634 .spacing(30)
635 .into(),
636 ])
637 .align_items(Align::Center)
638 .spacing(10);
639
640 let over = Container::new(over_content)
641 .style(
642 style::container::Style::color_with_double_cornerless_border(
643 (0, 0, 0, 200).into(),
644 (3, 4, 4, 255).into(),
645 (28, 28, 22, 255).into(),
646 ),
647 )
648 .width(Length::Shrink)
649 .height(Length::Shrink)
650 .max_width(400)
651 .max_height(500)
652 .padding(24)
653 .center_x()
654 .center_y();
655
656 Overlay::new(over, all)
657 .width(Length::Fill)
658 .height(Length::Fill)
659 .center_x()
660 .center_y()
661 .into()
662 } else {
663 self.confirmation = None;
664 all.into()
665 }
666 } else {
667 all.into()
668 }
669 }
670
671 fn map_kind_key(map_kind: MapKind) -> &'static str {
672 match map_kind {
673 MapKind::Circle => "main-singleplayer-map_shape-circle",
674 MapKind::Square => "main-singleplayer-map_shape-square",
675 }
676 }
677}