1use crate::{
2 GlobalState,
3 hud::default_water_color,
4 render::UiDrawer,
5 ui::{
6 self, Graphic, GraphicId,
7 fonts::IcedFonts as Fonts,
8 ice::{
9 Element, IcedRenderer, IcedUi as Ui,
10 component::{
11 neat_button,
12 tooltip::{self, WithTooltip},
13 },
14 style,
15 widget::{
16 AspectRatioContainer, BackgroundContainer, Image, MouseDetector, Overlay, Padding,
17 TooltipManager, mouse_detector,
18 },
19 },
20 img_ids::ImageGraphic,
21 },
22 window,
23};
24use client::{Client, ServerInfo};
25use common::{
26 LoadoutBuilder,
27 character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH},
28 comp::{self, Inventory, Item, humanoid, inventory::slot::EquipSlot},
29 resources::Time,
30 terrain::TerrainChunkSize,
31 vol::RectVolSize,
32};
33use common_net::msg::world_msg::{Marker, SiteId};
34use i18n::{Localization, LocalizationHandle};
35use rand::{Rng, thread_rng};
36use crate::settings::Settings;
38use iced::{
41 Align, Button, Checkbox, Color, Column, Container, HorizontalAlignment, Length, Row,
42 Scrollable, Slider, Space, Text, TextInput, button, scrollable, slider, text_input,
43};
44use std::sync::Arc;
45use vek::{Rgba, Vec2};
46
47pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
48pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
49pub const TOOLTIP_BACK_COLOR: Rgba<u8> = Rgba::new(20, 18, 10, 255);
50const FILL_FRAC_ONE: f32 = 0.77;
51const FILL_FRAC_TWO: f32 = 0.53;
52const TOOLTIP_HOVER_DUR: std::time::Duration = std::time::Duration::from_millis(150);
53const TOOLTIP_FADE_DUR: std::time::Duration = std::time::Duration::from_millis(350);
54const BANNER_ALPHA: u8 = 210;
55const SMALL_BUTTON_HEIGHT: u16 = 31;
57
58const STARTER_HAMMER: &str = "common.items.weapons.hammer.starter_hammer";
59const STARTER_BOW: &str = "common.items.weapons.bow.starter";
60const STARTER_AXE: &str = "common.items.weapons.axe.starter_axe";
61const STARTER_STAFF: &str = "common.items.weapons.staff.starter_staff";
62const STARTER_SWORD: &str = "common.items.weapons.sword.starter";
63const STARTER_SWORDS: &str = "common.items.weapons.sword_1h.starter";
64
65const UI_MAIN: Rgba<u8> = Rgba::new(156, 179, 179, 255); image_ids_ice! {
72 struct Imgs {
73 <ImageGraphic>
74 frame_bottom: "voxygen.element.ui.generic.frames.banner_bot",
75
76 slider_range: "voxygen.element.ui.generic.slider.track",
77 slider_indicator: "voxygen.element.ui.generic.slider.indicator",
78
79 char_selection: "voxygen.element.ui.generic.frames.selection",
80 char_selection_hover: "voxygen.element.ui.generic.frames.selection_hover",
81 char_selection_press: "voxygen.element.ui.generic.frames.selection_press",
82
83 delete_button: "voxygen.element.ui.char_select.icons.bin",
84 delete_button_hover: "voxygen.element.ui.char_select.icons.bin_hover",
85 delete_button_press: "voxygen.element.ui.char_select.icons.bin_press",
86
87 edit_button: "voxygen.element.ui.char_select.icons.pen",
88 edit_button_hover: "voxygen.element.ui.char_select.icons.pen_hover",
89 edit_button_press: "voxygen.element.ui.char_select.icons.pen_press",
90
91 name_input: "voxygen.element.ui.generic.textbox",
92
93 swords: "voxygen.element.weapons.swords",
95 sword: "voxygen.element.weapons.sword",
96 axe: "voxygen.element.weapons.axe",
97 hammer: "voxygen.element.weapons.hammer",
98 bow: "voxygen.element.weapons.bow",
99 staff: "voxygen.element.weapons.staff",
100
101 hardcore: "voxygen.element.ui.map.icons.dif_map_icon",
103
104 dice: "voxygen.element.ui.char_select.icons.dice",
106 dice_hover: "voxygen.element.ui.char_select.icons.dice_hover",
107 dice_press: "voxygen.element.ui.char_select.icons.dice_press",
108
109 human_m: "voxygen.element.ui.char_select.portraits.human_m",
111 human_f: "voxygen.element.ui.char_select.portraits.human_f",
112 orc_m: "voxygen.element.ui.char_select.portraits.orc_m",
113 orc_f: "voxygen.element.ui.char_select.portraits.orc_f",
114 dwarf_m: "voxygen.element.ui.char_select.portraits.dwarf_m",
115 dwarf_f: "voxygen.element.ui.char_select.portraits.dwarf_f",
116 draugr_m: "voxygen.element.ui.char_select.portraits.ud_m",
117 draugr_f: "voxygen.element.ui.char_select.portraits.ud_f",
118 elf_m: "voxygen.element.ui.char_select.portraits.elf_m",
119 elf_f: "voxygen.element.ui.char_select.portraits.elf_f",
120 danari_m: "voxygen.element.ui.char_select.portraits.danari_m",
121 danari_f: "voxygen.element.ui.char_select.portraits.danari_f",
122 icon_border: "voxygen.element.ui.generic.buttons.border",
124 icon_border_mo: "voxygen.element.ui.generic.buttons.border_mo",
125 icon_border_press: "voxygen.element.ui.generic.buttons.border_press",
126 icon_border_pressed: "voxygen.element.ui.generic.buttons.border_pressed",
127
128 button: "voxygen.element.ui.generic.buttons.button",
129 button_hover: "voxygen.element.ui.generic.buttons.button_hover",
130 button_press: "voxygen.element.ui.generic.buttons.button_press",
131
132 tt_edge: "voxygen.element.ui.generic.frames.tooltip.edge",
134 tt_corner: "voxygen.element.ui.generic.frames.tooltip.corner",
135
136 town_marker: "voxygen.element.ui.char_select.icons.town_marker",
138 }
139}
140
141pub enum Event {
142 Logout,
143 Play(CharacterId),
144 Spectate,
145 AddCharacter {
146 alias: String,
147 mainhand: Option<String>,
148 offhand: Option<String>,
149 body: comp::Body,
150 hardcore: bool,
151 start_site: Option<SiteId>,
152 },
153 EditCharacter {
154 alias: String,
155 character_id: CharacterId,
156 body: comp::Body,
157 },
158 DeleteCharacter(CharacterId),
159 ClearCharacterListError,
160 SelectCharacter(Option<CharacterId>),
161 ShowRules,
162}
163
164#[expect(clippy::large_enum_variant)]
165enum Mode {
166 Select {
167 info_content: Option<InfoContent>,
168
169 characters_scroll: scrollable::State,
170 character_buttons: Vec<button::State>,
171 new_character_button: button::State,
172 logout_button: button::State,
173 rule_button: button::State,
174 enter_world_button: button::State,
175 spectate_button: button::State,
176 yes_button: button::State,
177 no_button: button::State,
178 },
179 CreateOrEdit {
180 name: String,
181 body: humanoid::Body,
182 inventory: Box<Inventory>,
183 mainhand: Option<&'static str>,
184 offhand: Option<&'static str>,
185
186 body_type_buttons: [button::State; 2],
187 species_buttons: [button::State; 6],
188 tool_buttons: [button::State; 6],
189 sliders: Sliders,
190 hardcore_enabled: bool,
191 left_scroll: scrollable::State,
192 right_scroll: scrollable::State,
193 name_input: text_input::State,
194 back_button: button::State,
195 create_button: button::State,
196 rand_character_button: button::State,
197 rand_name_button: button::State,
198 prev_starting_site_button: button::State,
199 next_starting_site_button: button::State,
200 character_id: Option<CharacterId>,
204 start_site_idx: Option<usize>,
205 },
206}
207
208impl Mode {
209 pub fn select(info_content: Option<InfoContent>) -> Self {
210 Self::Select {
211 info_content,
212 characters_scroll: Default::default(),
213 character_buttons: Vec::new(),
214 new_character_button: Default::default(),
215 logout_button: Default::default(),
216 rule_button: Default::default(),
217 enter_world_button: Default::default(),
218 spectate_button: Default::default(),
219 yes_button: Default::default(),
220 no_button: Default::default(),
221 }
222 }
223
224 pub fn create(name: String) -> Self {
225 let mainhand = Some(STARTER_SWORD);
228 let offhand = None;
229
230 let loadout = LoadoutBuilder::empty()
231 .defaults()
232 .active_mainhand(mainhand.map(Item::new_from_asset_expect))
233 .active_offhand(offhand.map(Item::new_from_asset_expect))
234 .build();
235
236 let inventory = Box::new(Inventory::with_loadout_humanoid(loadout));
237
238 Self::CreateOrEdit {
239 name,
240 body: humanoid::Body::random(),
241 inventory,
242 mainhand,
243 offhand,
244 body_type_buttons: Default::default(),
245 species_buttons: Default::default(),
246 tool_buttons: Default::default(),
247 sliders: Default::default(),
248 hardcore_enabled: false,
249 left_scroll: Default::default(),
250 right_scroll: Default::default(),
251 name_input: Default::default(),
252 back_button: Default::default(),
253 create_button: Default::default(),
254 rand_character_button: Default::default(),
255 rand_name_button: Default::default(),
256 prev_starting_site_button: Default::default(),
257 next_starting_site_button: Default::default(),
258 character_id: None,
259 start_site_idx: None,
260 }
261 }
262
263 pub fn edit(
264 name: String,
265 character_id: CharacterId,
266 body: humanoid::Body,
267 inventory: &Inventory,
268 ) -> Self {
269 Self::CreateOrEdit {
270 name,
271 body,
272 inventory: Box::new(inventory.clone()),
273 mainhand: None,
274 offhand: None,
275 body_type_buttons: Default::default(),
276 species_buttons: Default::default(),
277 tool_buttons: Default::default(),
278 sliders: Default::default(),
279 hardcore_enabled: false,
280 left_scroll: Default::default(),
281 right_scroll: Default::default(),
282 name_input: Default::default(),
283 back_button: Default::default(),
284 create_button: Default::default(),
285 rand_character_button: Default::default(),
286 rand_name_button: Default::default(),
287 prev_starting_site_button: Default::default(),
288 next_starting_site_button: Default::default(),
289 character_id: Some(character_id),
290 start_site_idx: None,
291 }
292 }
293}
294
295#[derive(PartialEq)]
296enum InfoContent {
297 Deletion(usize),
298 LoadingCharacters,
299 CreatingCharacter,
300 EditingCharacter,
301 JoiningCharacter,
302 CharacterError(String),
303}
304
305struct Controls {
306 fonts: Fonts,
307 imgs: Imgs,
308 version: String,
310 alpha: String,
312 server_mismatched_version: Option<String>,
313 tooltip_manager: TooltipManager,
314 mouse_detector: mouse_detector::State,
316 mode: Mode,
317 selected: Option<CharacterId>,
319 default_name: String,
320 map_img: GraphicId,
321 possible_starting_sites: Vec<Marker>,
322 world_sz: Vec2<u32>,
323 has_rules: bool,
324}
325
326#[derive(Clone)]
327enum Message {
328 Back,
329 Logout,
330 ShowRules,
331 EnterWorld,
332 Spectate,
333 Select(CharacterId),
334 Delete(usize),
335 Edit(usize),
336 ConfirmEdit(CharacterId),
337 NewCharacter,
338 CreateCharacter,
339 Name(String),
340 BodyType(humanoid::BodyType),
341 Species(humanoid::Species),
342 Tool((Option<&'static str>, Option<&'static str>)),
343 RandomizeCharacter,
344 HardcoreEnabled(bool),
345 RandomizeName,
346 CancelDeletion,
347 ConfirmDeletion,
348 ClearCharacterListError,
349 HairStyle(u8),
350 HairColor(u8),
351 Skin(u8),
352 Eyes(u8),
353 EyeColor(u8),
354 Accessory(u8),
355 Beard(u8),
356 StartingSite(usize),
357 PrevStartingSite,
358 NextStartingSite,
359 DoNothing,
362}
363
364impl Controls {
365 fn new(
366 fonts: Fonts,
367 imgs: Imgs,
368 selected: Option<CharacterId>,
369 default_name: String,
370 server_info: &ServerInfo,
371 map_img: GraphicId,
372 possible_starting_sites: Vec<Marker>,
373 world_sz: Vec2<u32>,
374 has_rules: bool,
375 ) -> Self {
376 let version = common::util::DISPLAY_VERSION_LONG.clone();
377 let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str());
378 let server_mismatched_version = (common::util::GIT_HASH.to_string()
379 != server_info.git_hash)
380 .then(|| server_info.git_hash.clone());
381
382 Self {
383 fonts,
384 imgs,
385 version,
386 alpha,
387 server_mismatched_version,
388 tooltip_manager: TooltipManager::new(TOOLTIP_HOVER_DUR, TOOLTIP_FADE_DUR),
389 mouse_detector: Default::default(),
390 mode: Mode::select(Some(InfoContent::LoadingCharacters)),
391 selected,
392 default_name,
393 map_img,
394 possible_starting_sites,
395 world_sz,
396 has_rules,
397 }
398 }
399
400 fn view<'a>(
401 &'a mut self,
402 _settings: &Settings,
403 client: &Client,
404 error: &Option<String>,
405 i18n: &'a Localization,
406 ) -> Element<'a, Message> {
407 self.tooltip_manager.maintain();
412
413 let imgs = &self.imgs;
414 let fonts = &self.fonts;
415 let tooltip_manager = &self.tooltip_manager;
416
417 let button_style = style::button::Style::new(imgs.button)
418 .hover_image(imgs.button_hover)
419 .press_image(imgs.button_press)
420 .text_color(TEXT_COLOR)
421 .disabled_text_color(DISABLED_TEXT_COLOR);
422
423 let tooltip_style = tooltip::Style {
424 container: style::container::Style::color_with_image_border(
425 TOOLTIP_BACK_COLOR,
426 imgs.tt_corner,
427 imgs.tt_edge,
428 ),
429 text_color: TEXT_COLOR,
430 text_size: self.fonts.cyri.scale(17),
431 padding: 10,
432 };
433
434 let version = Text::new(&self.version)
435 .size(self.fonts.cyri.scale(15))
436 .width(Length::Fill)
437 .horizontal_alignment(HorizontalAlignment::Right);
438
439 let alpha = Text::new(&self.alpha)
440 .size(self.fonts.cyri.scale(12))
441 .width(Length::Fill)
442 .horizontal_alignment(HorizontalAlignment::Center);
443
444 let top_text = Row::with_children(vec![
445 Space::new(Length::Fill, Length::Shrink).into(),
446 alpha.into(),
447 version.into(),
448 ])
449 .width(Length::Fill);
450
451 let mut warning_container = if let Some(mismatched_version) =
452 &self.server_mismatched_version
453 {
454 let warning = Text::<IcedRenderer>::new(format!(
455 "{}\n{}: {} {}: {}",
456 i18n.get_msg("char_selection-version_mismatch"),
457 i18n.get_msg("main-login-server_version"),
458 mismatched_version,
459 i18n.get_msg("main-login-client_version"),
460 *common::util::GIT_HASH
461 ))
462 .size(self.fonts.cyri.scale(18))
463 .color(iced::Color::from_rgb(1.0, 0.0, 0.0))
464 .width(Length::Fill)
465 .horizontal_alignment(HorizontalAlignment::Center);
466 Some(
467 Container::new(
468 Container::new(Row::with_children(vec![warning.into()]).width(Length::Fill))
469 .style(style::container::Style::color(Rgba::new(0, 0, 0, 217)))
470 .padding(12)
471 .width(Length::Fill)
472 .center_x(),
473 )
474 .padding(16),
475 )
476 } else {
477 None
478 };
479
480 let content = match &mut self.mode {
481 Mode::Select {
482 info_content,
483 characters_scroll,
484 character_buttons,
485 new_character_button,
486 logout_button,
487 rule_button,
488 enter_world_button,
489 spectate_button,
490 yes_button,
491 no_button,
492 } => {
493 match self.selected {
494 Some(character_id) => {
495 if !client
497 .character_list()
498 .characters
499 .iter()
500 .any(|char| char.character.id == Some(character_id))
501 {
502 self.selected = None;
503 }
504 },
505 None => {
506 self.selected = client
509 .character_list()
510 .characters
511 .first()
512 .and_then(|i| i.character.id);
513 },
514 }
515
516 let selected = self.selected.and_then(|id| {
518 client
519 .character_list()
520 .characters
521 .iter()
522 .position(|i| i.character.id == Some(id))
523 });
524
525 if let Some(error) = error {
526 *info_content = Some(InfoContent::CharacterError(format!(
529 "{}: {}",
530 i18n.get_msg("common-error"),
531 error
532 )))
533 } else if let Some(InfoContent::CharacterError(_)) = info_content {
534 *info_content = None;
535 } else if matches!(
536 info_content,
537 Some(InfoContent::LoadingCharacters)
538 | Some(InfoContent::CreatingCharacter)
539 | Some(InfoContent::EditingCharacter)
540 ) && !client.character_list().loading
541 {
542 *info_content = None;
543 }
544
545 #[cfg(feature = "singleplayer")]
546 let server_name =
547 if client.server_info().name == server::settings::SINGLEPLAYER_SERVER_NAME {
548 &i18n.get_msg("common-singleplayer").to_string()
549 } else {
550 &client.server_info().name
551 };
552 #[cfg(not(feature = "singleplayer"))]
553 let server_name = &client.server_info().name;
554
555 let server = Container::new(
556 Column::with_children(vec![
557 Text::new(server_name).size(fonts.cyri.scale(25)).into(),
558 Space::new(Length::Fill, Length::Units(25)).into(),
560 ])
561 .spacing(5)
562 .align_items(Align::Center),
563 )
564 .style(style::container::Style::color(Rgba::new(0, 0, 0, 217)))
565 .padding(12)
566 .center_x()
567 .center_y()
568 .width(Length::Fill);
569
570 let characters = {
571 let characters = &client.character_list().characters;
572 let num = characters.len();
573 const CHAR_BUTTONS: usize = 3;
575 character_buttons.resize_with(num * CHAR_BUTTONS, Default::default);
576
577 let mut characters = characters
579 .iter()
580 .zip(character_buttons.chunks_exact_mut(CHAR_BUTTONS))
581 .filter_map(|(character, buttons)| {
582 let mut buttons = buttons.iter_mut();
583 character.character.id.map(|id| {
585 (
586 id,
587 character,
588 (
589 buttons.next().unwrap(),
590 buttons.next().unwrap(),
591 buttons.next().unwrap(),
592 ),
593 )
594 })
595 })
596 .enumerate()
597 .map(
598 |(
599 i,
600 (
601 character_id,
602 character,
603 (select_button, edit_button, delete_button),
604 ),
605 )| {
606 let select_col = if Some(i) == selected {
607 (255, 208, 69)
608 } else {
609 (255, 255, 255)
610 };
611 Overlay::new(
612 Container::new(Column::with_children({
613 let mut elements = Vec::new();
614 if character.hardcore {
615 elements.push(
616 Image::new(imgs.hardcore)
617 .width(Length::Units(32))
618 .height(Length::Units(32))
619 .into(),
620 );
621 }
622 elements.push(
623 Row::with_children(vec![
624 Button::new(
626 edit_button,
627 Space::new(
628 Length::Units(16),
629 Length::Units(16),
630 ),
631 )
632 .style(
633 style::button::Style::new(imgs.edit_button)
634 .hover_image(imgs.edit_button_hover)
635 .press_image(imgs.edit_button_press),
636 )
637 .on_press(Message::Edit(i))
638 .into(),
639 Button::new(
641 delete_button,
642 Space::new(
643 Length::Units(16),
644 Length::Units(16),
645 ),
646 )
647 .style(
648 style::button::Style::new(imgs.delete_button)
649 .hover_image(imgs.delete_button_hover)
650 .press_image(imgs.delete_button_press),
651 )
652 .on_press(Message::Delete(i))
653 .into(),
654 ])
655 .spacing(5)
656 .into(),
657 );
658
659 elements
660 }))
661 .padding(4),
662 AspectRatioContainer::new(
664 Button::new(
665 select_button,
666 Column::with_children(vec![
667 Text::new(&character.character.alias)
668 .size(fonts.cyri.scale(26))
669 .into(),
670 Text::new(character.location.as_ref().map_or_else(
671 || {
672 i18n.get_msg(
673 "char_selection-uncanny_valley",
674 )
675 .to_string()
676 },
677 |s| s.clone(),
678 ))
679 .into(),
680 ]),
681 )
682 .padding(10)
683 .style(
684 style::button::Style::new(if Some(i) == selected {
685 imgs.char_selection_hover
686 } else {
687 imgs.char_selection
688 })
689 .hover_image(imgs.char_selection_hover)
690 .press_image(imgs.char_selection_press)
691 .image_color(Rgba::new(
692 select_col.0,
693 select_col.1,
694 select_col.2,
695 255,
696 )),
697 )
698 .width(Length::Fill)
699 .height(Length::Fill)
700 .on_press(Message::Select(character_id)),
701 )
702 .ratio_of_image(imgs.char_selection),
703 )
704 .padding(0)
705 .align_x(Align::End)
706 .align_y(Align::End)
707 .into()
708 },
709 )
710 .collect::<Vec<_>>();
711
712 let color = if num >= MAX_CHARACTERS_PER_PLAYER {
714 (97, 97, 25)
715 } else {
716 (97, 255, 18)
717 };
718 characters.push(
719 AspectRatioContainer::new({
720 let button = Button::new(
721 new_character_button,
722 Container::new(Text::new(
723 i18n.get_msg("char_selection-create_new_character"),
724 ))
725 .width(Length::Fill)
726 .height(Length::Fill)
727 .center_x()
728 .center_y(),
729 )
730 .style(
731 style::button::Style::new(imgs.char_selection)
732 .hover_image(imgs.char_selection_hover)
733 .press_image(imgs.char_selection_press)
734 .image_color(Rgba::new(color.0, color.1, color.2, 255))
735 .text_color(iced::Color::from_rgb8(color.0, color.1, color.2))
736 .disabled_text_color(iced::Color::from_rgb8(
737 color.0, color.1, color.2,
738 )),
739 )
740 .width(Length::Fill)
741 .height(Length::Fill);
742 if num < MAX_CHARACTERS_PER_PLAYER {
743 button.on_press(Message::NewCharacter)
744 } else {
745 button
746 }
747 })
748 .ratio_of_image(imgs.char_selection)
749 .into(),
750 );
751 characters
752 };
753
754 let characters = Column::with_children(vec![
757 Container::new(
758 Scrollable::new(characters_scroll)
759 .push(Column::with_children(characters).spacing(4))
760 .padding(6)
761 .scrollbar_width(5)
762 .scroller_width(5)
763 .width(Length::Fill)
764 .style(style::scrollable::Style {
765 track: None,
766 scroller: style::scrollable::Scroller::Color(UI_MAIN),
767 }),
768 )
769 .style(style::container::Style::color(Rgba::from_translucent(
770 0,
771 BANNER_ALPHA,
772 )))
773 .width(Length::Units(322))
774 .height(Length::Fill)
775 .center_x()
776 .into(),
777 Image::new(imgs.frame_bottom)
778 .height(Length::Units(40))
779 .width(Length::Units(322))
780 .color(Rgba::from_translucent(0, BANNER_ALPHA))
781 .into(),
782 ])
783 .height(Length::Fill);
784
785 let mut left_column_children = vec![server.into(), characters.into()];
786
787 if self.has_rules {
788 left_column_children.push(
789 Container::new(neat_button(
790 rule_button,
791 i18n.get_msg("char_selection-rules").into_owned(),
792 FILL_FRAC_ONE,
793 button_style,
794 Some(Message::ShowRules),
795 ))
796 .align_y(Align::End)
797 .width(Length::Fill)
798 .center_x()
799 .height(Length::Units(52))
800 .into(),
801 );
802 }
803 let left_column = Column::with_children(left_column_children)
804 .spacing(10)
805 .width(Length::Units(322)) .height(Length::Fill);
809
810 let top = Row::with_children(vec![
811 left_column.into(),
812 MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill).into(),
813 ])
814 .padding(15)
815 .width(Length::Fill)
816 .height(Length::Fill);
817 let mut bottom_content = vec![
818 Container::new(neat_button(
819 logout_button,
820 i18n.get_msg("char_selection-logout").into_owned(),
821 FILL_FRAC_ONE,
822 button_style,
823 Some(Message::Logout),
824 ))
825 .width(Length::Fill)
826 .height(Length::Units(SMALL_BUTTON_HEIGHT))
827 .into(),
828 ];
829
830 if client.is_moderator() && client.client_type().can_spectate() {
831 bottom_content.push(
832 Container::new(neat_button(
833 spectate_button,
834 i18n.get_msg("char_selection-spectate").into_owned(),
835 FILL_FRAC_TWO,
836 button_style,
837 Some(Message::Spectate),
838 ))
839 .width(Length::Fill)
840 .height(Length::Units(52))
841 .center_x()
842 .into(),
843 );
844 }
845
846 if client.client_type().can_enter_character() {
847 bottom_content.push(
848 Container::new(neat_button(
849 enter_world_button,
850 i18n.get_msg("char_selection-enter_world").into_owned(),
851 FILL_FRAC_TWO,
852 button_style,
853 selected.map(|_| Message::EnterWorld),
854 ))
855 .width(Length::Fill)
856 .height(Length::Units(52))
857 .center_x()
858 .into(),
859 );
860 }
861
862 bottom_content.push(Space::new(Length::Fill, Length::Shrink).into());
863
864 let bottom = Row::with_children(bottom_content).align_items(Align::End);
865
866 let content = Column::with_children(vec![top.into(), bottom.into()])
867 .width(Length::Fill)
868 .padding(5)
869 .height(Length::Fill);
870
871 if let Some(info_content) = info_content {
873 let over_content: Element<_> = match &info_content {
874 InfoContent::Deletion(_) => Column::with_children(vec![
875 Text::new(i18n.get_msg("char_selection-delete_permanently"))
876 .size(fonts.cyri.scale(24))
877 .into(),
878 Row::with_children(vec![
879 neat_button(
880 no_button,
881 i18n.get_msg("common-no").into_owned(),
882 FILL_FRAC_ONE,
883 button_style,
884 Some(Message::CancelDeletion),
885 ),
886 neat_button(
887 yes_button,
888 i18n.get_msg("common-yes").into_owned(),
889 FILL_FRAC_ONE,
890 button_style,
891 Some(Message::ConfirmDeletion),
892 ),
893 ])
894 .height(Length::Units(28))
895 .spacing(30)
896 .into(),
897 ])
898 .align_items(Align::Center)
899 .spacing(10)
900 .into(),
901 InfoContent::LoadingCharacters => {
902 Text::new(i18n.get_msg("char_selection-loading_characters"))
903 .size(fonts.cyri.scale(24))
904 .into()
905 },
906 InfoContent::CreatingCharacter => {
907 Text::new(i18n.get_msg("char_selection-creating_character"))
908 .size(fonts.cyri.scale(24))
909 .into()
910 },
911 InfoContent::EditingCharacter => {
912 Text::new(i18n.get_msg("char_selection-editing_character"))
913 .size(fonts.cyri.scale(24))
914 .into()
915 },
916 InfoContent::JoiningCharacter => {
917 Text::new(i18n.get_msg("char_selection-joining_character"))
918 .size(fonts.cyri.scale(24))
919 .into()
920 },
921 InfoContent::CharacterError(error) => Column::with_children(vec![
922 Text::new(error).size(fonts.cyri.scale(24)).into(),
923 Row::with_children(vec![neat_button(
924 no_button,
925 i18n.get_msg("common-close").into_owned(),
926 FILL_FRAC_ONE,
927 button_style,
928 Some(Message::ClearCharacterListError),
929 )])
930 .height(Length::Units(28))
931 .into(),
932 ])
933 .align_items(Align::Center)
934 .spacing(10)
935 .into(),
936 };
937
938 let over = Container::new(over_content)
939 .style(
940 style::container::Style::color_with_double_cornerless_border(
941 (0, 0, 0, 200).into(),
942 (3, 4, 4, 255).into(),
943 (28, 28, 22, 255).into(),
944 ),
945 )
946 .width(Length::Shrink)
947 .height(Length::Shrink)
948 .max_width(400)
949 .max_height(500)
950 .padding(24)
951 .center_x()
952 .center_y();
953
954 Overlay::new(over, content)
955 .width(Length::Fill)
956 .height(Length::Fill)
957 .center_x()
958 .center_y()
959 .into()
960 } else {
961 content.into()
962 }
963 },
964 Mode::CreateOrEdit {
965 name,
966 body,
967 inventory: _,
968 mainhand,
969 offhand: _,
970 left_scroll,
971 right_scroll,
972 body_type_buttons,
973 species_buttons,
974 tool_buttons,
975 sliders,
976 hardcore_enabled,
977 name_input,
978 back_button,
979 create_button,
980 rand_character_button,
981 rand_name_button,
982 prev_starting_site_button,
983 next_starting_site_button,
984 character_id,
985 start_site_idx,
986 } => {
987 let unselected_style = style::button::Style::new(imgs.icon_border)
988 .hover_image(imgs.icon_border_mo)
989 .press_image(imgs.icon_border_press);
990
991 let selected_style = style::button::Style::new(imgs.icon_border_pressed)
992 .hover_image(imgs.icon_border_mo)
993 .press_image(imgs.icon_border_press);
994
995 let icon_button = |button, selected, msg, img| {
996 Container::new(
997 Button::<_, IcedRenderer>::new(
998 button,
999 Space::new(Length::Units(60), Length::Units(60)),
1000 )
1001 .style(if selected {
1002 selected_style
1003 } else {
1004 unselected_style
1005 })
1006 .on_press(msg),
1007 )
1008 .style(style::container::Style::image(img))
1009 };
1010 let icon_button_tooltip = |button, selected, msg, img, tooltip_i18n_key| {
1011 icon_button(button, selected, msg, img).with_tooltip(
1012 tooltip_manager,
1013 move || {
1014 let tooltip_text = i18n.get_msg(tooltip_i18n_key);
1015 tooltip::text(&tooltip_text, tooltip_style)
1016 },
1017 )
1018 };
1019
1020 let (tool, species, body_type) = if character_id.is_some() {
1022 (Column::new(), Column::new(), Row::new())
1023 } else {
1024 let (body_m_ico, body_f_ico) = match body.species {
1025 humanoid::Species::Human => (imgs.human_m, imgs.human_f),
1026 humanoid::Species::Orc => (imgs.orc_m, imgs.orc_f),
1027 humanoid::Species::Dwarf => (imgs.dwarf_m, imgs.dwarf_f),
1028 humanoid::Species::Elf => (imgs.elf_m, imgs.elf_f),
1029 humanoid::Species::Draugr => (imgs.draugr_m, imgs.draugr_f),
1030 humanoid::Species::Danari => (imgs.danari_m, imgs.danari_f),
1031 };
1032 let [body_m_button, body_f_button] = body_type_buttons;
1033 let body_type = Row::with_children(vec![
1034 icon_button(
1035 body_m_button,
1036 matches!(body.body_type, humanoid::BodyType::Male),
1037 Message::BodyType(humanoid::BodyType::Male),
1038 body_m_ico,
1039 )
1040 .into(),
1041 icon_button(
1042 body_f_button,
1043 matches!(body.body_type, humanoid::BodyType::Female),
1044 Message::BodyType(humanoid::BodyType::Female),
1045 body_f_ico,
1046 )
1047 .into(),
1048 ])
1049 .spacing(1);
1050 let (human_icon, orc_icon, dwarf_icon, elf_icon, draugr_icon, danari_icon) =
1051 match body.body_type {
1052 humanoid::BodyType::Male => (
1053 self.imgs.human_m,
1054 self.imgs.orc_m,
1055 self.imgs.dwarf_m,
1056 self.imgs.elf_m,
1057 self.imgs.draugr_m,
1058 self.imgs.danari_m,
1059 ),
1060 humanoid::BodyType::Female => (
1061 self.imgs.human_f,
1062 self.imgs.orc_f,
1063 self.imgs.dwarf_f,
1064 self.imgs.elf_f,
1065 self.imgs.draugr_f,
1066 self.imgs.danari_f,
1067 ),
1068 };
1069 let [
1070 human_button,
1071 orc_button,
1072 dwarf_button,
1073 elf_button,
1074 draugr_button,
1075 danari_button,
1076 ] = species_buttons;
1077 let species = Column::with_children(vec![
1078 Row::with_children(vec![
1079 icon_button_tooltip(
1080 human_button,
1081 matches!(body.species, humanoid::Species::Human),
1082 Message::Species(humanoid::Species::Human),
1083 human_icon,
1084 "common-species-human",
1085 )
1086 .into(),
1087 icon_button_tooltip(
1088 orc_button,
1089 matches!(body.species, humanoid::Species::Orc),
1090 Message::Species(humanoid::Species::Orc),
1091 orc_icon,
1092 "common-species-orc",
1093 )
1094 .into(),
1095 icon_button_tooltip(
1096 dwarf_button,
1097 matches!(body.species, humanoid::Species::Dwarf),
1098 Message::Species(humanoid::Species::Dwarf),
1099 dwarf_icon,
1100 "common-species-dwarf",
1101 )
1102 .into(),
1103 ])
1104 .spacing(1)
1105 .into(),
1106 Row::with_children(vec![
1107 icon_button_tooltip(
1108 elf_button,
1109 matches!(body.species, humanoid::Species::Elf),
1110 Message::Species(humanoid::Species::Elf),
1111 elf_icon,
1112 "common-species-elf",
1113 )
1114 .into(),
1115 icon_button_tooltip(
1116 draugr_button,
1117 matches!(body.species, humanoid::Species::Draugr),
1118 Message::Species(humanoid::Species::Draugr),
1119 draugr_icon,
1120 "common-species-draugr",
1121 )
1122 .into(),
1123 icon_button_tooltip(
1124 danari_button,
1125 matches!(body.species, humanoid::Species::Danari),
1126 Message::Species(humanoid::Species::Danari),
1127 danari_icon,
1128 "common-species-danari",
1129 )
1130 .into(),
1131 ])
1132 .spacing(1)
1133 .into(),
1134 ])
1135 .spacing(1);
1136 let [
1137 sword_button,
1138 swords_button,
1139 axe_button,
1140 hammer_button,
1141 bow_button,
1142 staff_button,
1143 ] = tool_buttons;
1144 let tool = Column::with_children(vec![
1145 Row::with_children(vec![
1146 icon_button_tooltip(
1147 sword_button,
1148 *mainhand == Some(STARTER_SWORD),
1149 Message::Tool((Some(STARTER_SWORD), None)),
1150 imgs.sword,
1151 "common-weapons-greatsword",
1152 )
1153 .into(),
1154 icon_button_tooltip(
1155 hammer_button,
1156 *mainhand == Some(STARTER_HAMMER),
1157 Message::Tool((Some(STARTER_HAMMER), None)),
1158 imgs.hammer,
1159 "common-weapons-hammer",
1160 )
1161 .into(),
1162 icon_button_tooltip(
1163 axe_button,
1164 *mainhand == Some(STARTER_AXE),
1165 Message::Tool((Some(STARTER_AXE), None)),
1166 imgs.axe,
1167 "common-weapons-axe",
1168 )
1169 .into(),
1170 ])
1171 .spacing(1)
1172 .into(),
1173 Row::with_children(vec![
1174 icon_button_tooltip(
1175 swords_button,
1176 *mainhand == Some(STARTER_SWORDS),
1177 Message::Tool((Some(STARTER_SWORDS), Some(STARTER_SWORDS))),
1178 imgs.swords,
1179 "common-weapons-shortswords",
1180 )
1181 .into(),
1182 icon_button_tooltip(
1183 bow_button,
1184 *mainhand == Some(STARTER_BOW),
1185 Message::Tool((Some(STARTER_BOW), None)),
1186 imgs.bow,
1187 "common-weapons-bow",
1188 )
1189 .into(),
1190 icon_button_tooltip(
1191 staff_button,
1192 *mainhand == Some(STARTER_STAFF),
1193 Message::Tool((Some(STARTER_STAFF), None)),
1194 imgs.staff,
1195 "common-weapons-staff",
1196 )
1197 .into(),
1198 ])
1199 .spacing(1)
1200 .into(),
1201 ])
1202 .spacing(1);
1203
1204 (tool, species, body_type)
1205 };
1206
1207 const SLIDER_TEXT_SIZE: u16 = 20;
1208 const SLIDER_CURSOR_SIZE: (u16, u16) = (9, 21);
1209 const SLIDER_BAR_HEIGHT: u16 = 9;
1210 const SLIDER_BAR_PAD: u16 = 5;
1211 const SLIDER_HEIGHT: u16 = 30;
1213
1214 fn starter_slider<'a>(
1215 text: String,
1216 size: u16,
1217 state: &'a mut slider::State,
1218 max: u32,
1219 selected_val: u32,
1220 on_change: impl 'static + Fn(u32) -> Message,
1221 imgs: &Imgs,
1222 ) -> Element<'a, Message> {
1223 Column::with_children(vec![
1224 Text::new(text).size(size).into(),
1225 Slider::new(state, 0..=max, selected_val, on_change)
1226 .height(SLIDER_HEIGHT)
1227 .style(style::slider::Style::images(
1228 imgs.slider_indicator,
1229 imgs.slider_range,
1230 SLIDER_BAR_PAD,
1231 SLIDER_CURSOR_SIZE,
1232 SLIDER_BAR_HEIGHT,
1233 ))
1234 .into(),
1235 ])
1236 .align_items(Align::Center)
1237 .into()
1238 }
1239 fn char_slider<'a>(
1240 text: String,
1241 state: &'a mut slider::State,
1242 max: u8,
1243 selected_val: u8,
1244 on_change: impl 'static + Fn(u8) -> Message,
1245 (fonts, imgs): (&Fonts, &Imgs),
1246 ) -> Element<'a, Message> {
1247 Column::with_children(vec![
1248 Text::new(text)
1249 .size(fonts.cyri.scale(SLIDER_TEXT_SIZE))
1250 .into(),
1251 Slider::new(state, 0..=max, selected_val, on_change)
1252 .height(SLIDER_HEIGHT)
1253 .style(style::slider::Style::images(
1254 imgs.slider_indicator,
1255 imgs.slider_range,
1256 SLIDER_BAR_PAD,
1257 SLIDER_CURSOR_SIZE,
1258 SLIDER_BAR_HEIGHT,
1259 ))
1260 .into(),
1261 ])
1262 .align_items(Align::Center)
1263 .into()
1264 }
1265 fn char_slider_greyable<'a>(
1266 active: bool,
1267 text: String,
1268 state: &'a mut slider::State,
1269 max: u8,
1270 selected_val: u8,
1271 on_change: impl 'static + Fn(u8) -> Message,
1272 (fonts, imgs): (&Fonts, &Imgs),
1273 ) -> Element<'a, Message> {
1274 if active {
1275 char_slider(text, state, max, selected_val, on_change, (fonts, imgs))
1276 } else {
1277 Column::with_children(vec![
1278 Text::new(text)
1279 .size(fonts.cyri.scale(SLIDER_TEXT_SIZE))
1280 .color(DISABLED_TEXT_COLOR)
1281 .into(),
1282 Slider::new(state, 0..=max.into(), selected_val.into(), |_| {
1285 Message::DoNothing
1286 })
1287 .height(SLIDER_HEIGHT)
1288 .style(style::slider::Style {
1289 cursor: style::slider::Cursor::Color(Rgba::zero()),
1290 bar: style::slider::Bar::Image(
1291 imgs.slider_range,
1292 Rgba::from_translucent(255, 51),
1293 SLIDER_BAR_PAD,
1294 ),
1295 labels: false,
1296 ..Default::default()
1297 })
1298 .into(),
1299 ])
1300 .align_items(Align::Center)
1301 .into()
1302 }
1303 }
1304
1305 let slider_options = Column::with_children(vec![
1306 char_slider(
1307 i18n.get_msg("char_selection-hair_style").into_owned(),
1308 &mut sliders.hair_style,
1309 body.species.num_hair_styles(body.body_type) - 1,
1310 body.hair_style,
1311 Message::HairStyle,
1312 (fonts, imgs),
1313 ),
1314 char_slider(
1315 i18n.get_msg("char_selection-hair_color").into_owned(),
1316 &mut sliders.hair_color,
1317 body.species.num_hair_colors() - 1,
1318 body.hair_color,
1319 Message::HairColor,
1320 (fonts, imgs),
1321 ),
1322 char_slider(
1323 i18n.get_msg("char_selection-skin").into_owned(),
1324 &mut sliders.skin,
1325 body.species.num_skin_colors() - 1,
1326 body.skin,
1327 Message::Skin,
1328 (fonts, imgs),
1329 ),
1330 char_slider(
1331 i18n.get_msg("char_selection-eyeshape").into_owned(),
1332 &mut sliders.eyes,
1333 body.species.num_eyes(body.body_type) - 1,
1334 body.eyes,
1335 Message::Eyes,
1336 (fonts, imgs),
1337 ),
1338 char_slider(
1339 i18n.get_msg("char_selection-eye_color").into_owned(),
1340 &mut sliders.eye_color,
1341 body.species.num_eye_colors() - 1,
1342 body.eye_color,
1343 Message::EyeColor,
1344 (fonts, imgs),
1345 ),
1346 char_slider_greyable(
1347 body.species.num_accessories(body.body_type) > 1,
1348 i18n.get_msg("char_selection-accessories").into_owned(),
1349 &mut sliders.accessory,
1350 body.species.num_accessories(body.body_type) - 1,
1351 body.accessory,
1352 Message::Accessory,
1353 (fonts, imgs),
1354 ),
1355 char_slider_greyable(
1356 body.species.num_beards(body.body_type) > 1,
1357 i18n.get_msg("char_selection-beard").into_owned(),
1358 &mut sliders.beard,
1359 body.species.num_beards(body.body_type) - 1,
1360 body.beard,
1361 Message::Beard,
1362 (fonts, imgs),
1363 ),
1364 ])
1365 .max_width(200)
1366 .padding(5);
1367
1368 let hardcore_checkbox = if character_id.is_some() {
1369 Row::new()
1370 } else {
1371 Row::with_children(vec![
1372 Checkbox::new(
1373 *hardcore_enabled,
1374 i18n.get_msg("char_selection-hardcore"),
1375 Message::HardcoreEnabled,
1376 )
1377 .size(32)
1378 .spacing(8)
1379 .text_size(24)
1380 .style(style::checkbox::Style::new(
1381 imgs.icon_border,
1382 self.imgs.hardcore,
1383 ))
1384 .with_tooltip(tooltip_manager, move || {
1385 let tooltip_text = i18n.get_msg("char_selection-hardcore_tooltip");
1386 tooltip::text(&tooltip_text, tooltip_style)
1387 })
1388 .into(),
1389 ])
1390 };
1391
1392 const CHAR_DICE_SIZE: u16 = 50;
1393 let rand_character = Button::new(
1394 rand_character_button,
1395 Space::new(Length::Units(CHAR_DICE_SIZE), Length::Units(CHAR_DICE_SIZE)),
1396 )
1397 .style(
1398 style::button::Style::new(imgs.dice)
1399 .hover_image(imgs.dice_hover)
1400 .press_image(imgs.dice_press),
1401 )
1402 .on_press(Message::RandomizeCharacter)
1403 .with_tooltip(tooltip_manager, move || {
1404 let tooltip_text = i18n.get_msg("common-rand_appearance");
1405 tooltip::text(&tooltip_text, tooltip_style)
1406 });
1407
1408 let left_column_content = vec![
1409 body_type.into(),
1410 tool.into(),
1411 species.into(),
1412 slider_options.into(),
1413 hardcore_checkbox.into(),
1414 rand_character.into(),
1415 ];
1416
1417 let right_column_content = if character_id.is_none() {
1418 let map_sz = Vec2::new(500, 500);
1419 let map_img = Image::new(self.map_img)
1420 .height(Length::Units(map_sz.x))
1421 .width(Length::Units(map_sz.y));
1422 let map = if let Some(info) = self
1430 .possible_starting_sites
1431 .get(start_site_idx.unwrap_or_default())
1432 {
1433 let site_name = Text::new(
1434 self.possible_starting_sites[start_site_idx.unwrap_or_default()]
1435 .name
1436 .as_ref()
1437 .map(|name| i18n.get_content(name))
1438 .unwrap_or_else(|| "Unknown".to_string()),
1439 )
1440 .horizontal_alignment(HorizontalAlignment::Left)
1441 .color(Color::from_rgb(131.0, 102.0, 0.0));
1442 let pos_frac = info
1443 .wpos
1444 .map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
1445 e as f32 / sz as f32
1446 });
1447 let point = Vec2::new(pos_frac.x, 1.0 - pos_frac.y)
1448 .map2(map_sz, |e, sz| e * sz as f32 - 12.0);
1449 let marker_img = Image::new(imgs.town_marker)
1450 .height(Length::Units(27))
1451 .width(Length::Units(16));
1452 let marker_content: Column<Message, IcedRenderer> = Column::new()
1453 .spacing(2)
1454 .push(site_name)
1455 .push(marker_img)
1456 .align_items(Align::Center);
1457
1458 Overlay::new(
1459 Container::new(marker_content)
1460 .width(Length::Fill)
1461 .height(Length::Fill)
1462 .center_x()
1463 .center_y(),
1464 map_img,
1465 )
1466 .over_position(iced::Point::new(point.x, point.y - 34.0))
1467 .into()
1468 } else {
1469 map_img.into()
1470 };
1471
1472 if self.possible_starting_sites.is_empty() {
1473 vec![map]
1474 } else {
1475 let selected = start_site_idx.get_or_insert_with(|| {
1476 thread_rng().gen_range(0..self.possible_starting_sites.len())
1477 });
1478
1479 let site_slider = starter_slider(
1480 i18n.get_msg("char_selection-starting_site").into_owned(),
1481 30,
1482 &mut sliders.starting_site,
1483 self.possible_starting_sites.len() as u32 - 1,
1484 *selected as u32,
1485 |x| Message::StartingSite(x as usize),
1486 imgs,
1487 );
1488 let site_buttons = Row::with_children(vec![
1489 neat_button(
1490 prev_starting_site_button,
1491 i18n.get_msg("char_selection-starting_site_prev")
1492 .into_owned(),
1493 FILL_FRAC_ONE,
1494 button_style,
1495 Some(Message::PrevStartingSite),
1496 ),
1497 neat_button(
1498 next_starting_site_button,
1499 i18n.get_msg("char_selection-starting_site_next")
1500 .into_owned(),
1501 FILL_FRAC_ONE,
1502 button_style,
1503 Some(Message::NextStartingSite),
1504 ),
1505 ])
1506 .max_height(60)
1507 .padding(15)
1508 .into();
1509 vec![site_slider, map, site_buttons]
1525 }
1526 } else {
1527 Vec::new()
1529 };
1530
1531 let column_left = |column_content, scroll| {
1532 let column = Container::new(
1533 Scrollable::new(scroll)
1534 .push(
1535 Column::with_children(column_content)
1536 .align_items(Align::Center)
1537 .width(Length::Fill)
1538 .spacing(5)
1539 .padding(5),
1540 )
1541 .padding(5)
1542 .width(Length::Fill)
1543 .align_items(Align::Center)
1544 .style(style::scrollable::Style {
1545 track: None,
1546 scroller: style::scrollable::Scroller::Color(UI_MAIN),
1547 }),
1548 )
1549 .width(Length::Units(320)) .height(Length::Fill);
1553
1554 Column::with_children(vec![
1555 Container::new(column)
1556 .style(style::container::Style::color(Rgba::from_translucent(
1557 0,
1558 BANNER_ALPHA,
1559 )))
1560 .width(Length::Units(320))
1561 .center_x()
1562 .into(),
1563 Image::new(imgs.frame_bottom)
1564 .height(Length::Units(40))
1565 .width(Length::Units(320))
1566 .color(Rgba::from_translucent(0, BANNER_ALPHA))
1567 .into(),
1568 ])
1569 .height(Length::Fill)
1570 };
1571 let column_right = |column_content, scroll| {
1572 let column = Container::new(
1573 Scrollable::new(scroll)
1574 .push(
1575 Column::with_children(column_content)
1576 .align_items(Align::Center)
1577 .width(Length::Fill)
1578 .spacing(5)
1579 .padding(5),
1580 )
1581 .padding(5)
1582 .width(Length::Fill)
1583 .align_items(Align::Center)
1584 .style(style::scrollable::Style {
1585 track: None,
1586 scroller: style::scrollable::Scroller::Color(UI_MAIN),
1587 }),
1588 )
1589 .width(Length::Units(520)) .height(Length::Fill);
1593 if character_id.is_none() {
1594 Column::with_children(vec![
1595 Container::new(column)
1596 .style(style::container::Style::color(Rgba::from_translucent(
1597 0,
1598 BANNER_ALPHA,
1599 )))
1600 .width(Length::Units(520))
1601 .center_x()
1602 .into(),
1603 Image::new(imgs.frame_bottom)
1604 .height(Length::Units(40))
1605 .width(Length::Units(520))
1606 .color(Rgba::from_translucent(0, BANNER_ALPHA))
1607 .into(),
1608 ])
1609 .height(Length::Fill)
1610 } else {
1611 Column::with_children(vec![Container::new(column).into()])
1612 }
1613 };
1614
1615 let mouse_area =
1616 MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill);
1617
1618 let top = Row::with_children(vec![
1619 column_left(left_column_content, left_scroll).into(),
1620 Column::with_children(
1621 if let Some(warning_container) = warning_container.take() {
1622 vec![warning_container.into(), mouse_area.into()]
1623 } else {
1624 vec![mouse_area.into()]
1625 },
1626 )
1627 .width(Length::Fill)
1628 .height(Length::Fill)
1629 .into(),
1630 column_right(right_column_content, right_scroll)
1631 .width(Length::Units(520))
1632 .into(),
1633 ])
1634 .padding(10)
1635 .width(Length::Fill)
1636 .height(Length::Fill);
1637
1638 let back = neat_button(
1639 back_button,
1640 i18n.get_msg("common-back").into_owned(),
1641 FILL_FRAC_ONE,
1642 button_style,
1643 Some(Message::Back),
1644 );
1645
1646 const NAME_DICE_SIZE: u16 = 35;
1647 let rand_name = Button::new(
1648 rand_name_button,
1649 Space::new(Length::Units(NAME_DICE_SIZE), Length::Units(NAME_DICE_SIZE)),
1650 )
1651 .style(
1652 style::button::Style::new(imgs.dice)
1653 .hover_image(imgs.dice_hover)
1654 .press_image(imgs.dice_press),
1655 )
1656 .on_press(Message::RandomizeName)
1657 .with_tooltip(tooltip_manager, move || {
1658 let tooltip_text = i18n.get_msg("common-rand_name");
1659 tooltip::text(&tooltip_text, tooltip_style)
1660 });
1661
1662 let confirm_msg = if let Some(character_id) = character_id {
1663 Message::ConfirmEdit(*character_id)
1664 } else {
1665 Message::CreateCharacter
1666 };
1667
1668 let name_input = BackgroundContainer::new(
1669 Image::new(imgs.name_input)
1670 .height(Length::Units(40))
1671 .fix_aspect_ratio(),
1672 TextInput::new(
1673 name_input,
1674 &i18n.get_msg("character_window-character_name"),
1675 name,
1676 Message::Name,
1677 )
1678 .size(25)
1679 .on_submit(confirm_msg.clone()),
1680 )
1681 .padding(Padding::new().horizontal(7).top(5));
1682
1683 let bottom_center = Container::new(
1684 Row::with_children(vec![
1685 rand_name.into(),
1686 name_input.into(),
1687 Space::new(Length::Units(NAME_DICE_SIZE), Length::Units(NAME_DICE_SIZE))
1688 .into(),
1689 ])
1690 .align_items(Align::Center)
1691 .spacing(5)
1692 .padding(16),
1693 )
1694 .style(style::container::Style::color(Rgba::new(0, 0, 0, 100)));
1695
1696 let create = neat_button(
1697 create_button,
1698 i18n.get_msg(if character_id.is_some() {
1699 "common-confirm"
1700 } else {
1701 "common-create"
1702 }),
1703 FILL_FRAC_ONE,
1704 button_style,
1705 (!name.is_empty()).then_some(confirm_msg),
1706 );
1707
1708 let create: Element<Message> = if name.is_empty() {
1709 create
1710 .with_tooltip(tooltip_manager, move || {
1711 let tooltip_text = i18n.get_msg("char_selection-create_info_name");
1712 tooltip::text(&tooltip_text, tooltip_style)
1713 })
1714 .into()
1715 } else {
1716 create
1717 };
1718
1719 let bottom = Row::with_children(vec![
1720 Container::new(back)
1721 .width(Length::Fill)
1722 .height(Length::Units(SMALL_BUTTON_HEIGHT))
1723 .into(),
1724 Container::new(bottom_center)
1725 .width(Length::Fill)
1726 .center_x()
1727 .into(),
1728 Container::new(create)
1729 .width(Length::Fill)
1730 .height(Length::Units(SMALL_BUTTON_HEIGHT))
1731 .align_x(Align::End)
1732 .into(),
1733 ])
1734 .align_items(Align::End);
1735
1736 Column::with_children(vec![top.into(), bottom.into()])
1737 .width(Length::Fill)
1738 .height(Length::Fill)
1739 .padding(5)
1740 .into()
1741 },
1742 };
1743
1744 let children = if let Some(warning_container) = warning_container {
1745 vec![top_text.into(), warning_container.into(), content]
1746 } else {
1747 vec![top_text.into(), content]
1748 };
1749
1750 Container::new(
1751 Column::with_children(children)
1752 .spacing(3)
1753 .width(Length::Fill)
1754 .height(Length::Fill),
1755 )
1756 .padding(3)
1757 .into()
1758 }
1759
1760 fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
1761 match message {
1762 Message::Back => {
1763 if matches!(&self.mode, Mode::CreateOrEdit { .. }) {
1764 self.mode = Mode::select(None);
1765 }
1766 },
1767 Message::Logout => {
1768 events.push(Event::Logout);
1769 },
1770 Message::ShowRules => {
1771 events.push(Event::ShowRules);
1772 },
1773 Message::ConfirmDeletion => {
1774 if let Mode::Select { info_content, .. } = &mut self.mode {
1775 if let Some(InfoContent::Deletion(idx)) = info_content {
1776 if let Some(id) = characters.get(*idx).and_then(|i| i.character.id) {
1777 events.push(Event::DeleteCharacter(id));
1778 if Some(id) == self.selected {
1780 self.selected = None;
1781 events.push(Event::SelectCharacter(None));
1782 }
1783 }
1784 *info_content = None;
1785 }
1786 }
1787 },
1788 Message::CancelDeletion => {
1789 if let Mode::Select { info_content, .. } = &mut self.mode {
1790 if let Some(InfoContent::Deletion(_)) = info_content {
1791 *info_content = None;
1792 }
1793 }
1794 },
1795 Message::ClearCharacterListError => {
1796 events.push(Event::ClearCharacterListError);
1797 },
1798 Message::DoNothing => {},
1799 _ if matches!(self.mode, Mode::Select {
1800 info_content: Some(_),
1801 ..
1802 }) =>
1803 {
1804 },
1811 Message::EnterWorld => {
1812 if let (Mode::Select { info_content, .. }, Some(selected)) =
1813 (&mut self.mode, self.selected)
1814 {
1815 events.push(Event::Play(selected));
1816 *info_content = Some(InfoContent::JoiningCharacter);
1817 }
1818 },
1819 Message::Spectate => {
1820 if matches!(self.mode, Mode::Select { .. }) {
1821 events.push(Event::Spectate);
1822 }
1825 },
1826 Message::Select(id) => {
1827 if let Mode::Select { .. } = &mut self.mode {
1828 self.selected = Some(id);
1829 events.push(Event::SelectCharacter(Some(id)))
1830 }
1831 },
1832 Message::Delete(idx) => {
1833 if let Mode::Select { info_content, .. } = &mut self.mode {
1834 *info_content = Some(InfoContent::Deletion(idx));
1835 }
1836 },
1837 Message::Edit(idx) => {
1838 if matches!(&self.mode, Mode::Select { .. }) {
1839 if let Some(character) = characters.get(idx) {
1840 if let comp::Body::Humanoid(body) = character.body {
1841 if let Some(id) = character.character.id {
1842 self.mode = Mode::edit(
1843 character.character.alias.clone(),
1844 id,
1845 body,
1846 &character.inventory,
1847 );
1848 }
1849 }
1850 }
1851 }
1852 },
1853 Message::NewCharacter => {
1854 if matches!(&self.mode, Mode::Select { .. }) {
1855 self.mode = Mode::create(self.default_name.clone());
1856 }
1857 },
1858 Message::CreateCharacter => {
1859 if let Mode::CreateOrEdit {
1860 name,
1861 body,
1862 hardcore_enabled,
1863 mainhand,
1864 offhand,
1865 start_site_idx,
1866 ..
1867 } = &self.mode
1868 {
1869 events.push(Event::AddCharacter {
1870 alias: name.clone(),
1871 mainhand: mainhand.map(String::from),
1872 offhand: offhand.map(String::from),
1873 body: comp::Body::Humanoid(*body),
1874 hardcore: *hardcore_enabled,
1875 start_site: self
1876 .possible_starting_sites
1877 .get(start_site_idx.unwrap_or_default())
1878 .and_then(|info| info.id),
1879 });
1880 self.mode = Mode::select(Some(InfoContent::CreatingCharacter));
1881 }
1882 },
1883 Message::ConfirmEdit(character_id) => {
1884 if let Mode::CreateOrEdit { name, body, .. } = &self.mode {
1885 events.push(Event::EditCharacter {
1886 alias: name.clone(),
1887 character_id,
1888 body: comp::Body::Humanoid(*body),
1889 });
1890 self.mode = Mode::select(Some(InfoContent::EditingCharacter));
1891 }
1892 },
1893 Message::Name(value) => {
1894 if let Mode::CreateOrEdit { name, .. } = &mut self.mode {
1895 *name = value.chars().take(MAX_NAME_LENGTH).collect();
1896 }
1897 },
1898 Message::BodyType(value) => {
1899 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1900 body.body_type = value;
1901 body.validate();
1902 }
1903 },
1904 Message::Species(value) => {
1905 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1906 body.species = value;
1907 body.validate();
1908 }
1909 },
1910 Message::Tool(value) => {
1911 if let Mode::CreateOrEdit {
1912 mainhand,
1913 offhand,
1914 inventory,
1915 ..
1916 } = &mut self.mode
1917 {
1918 *mainhand = value.0;
1919 *offhand = value.1;
1920 inventory.replace_loadout_item(
1921 EquipSlot::ActiveMainhand,
1922 mainhand.map(Item::new_from_asset_expect),
1923 Time(0.0),
1926 );
1927 inventory.replace_loadout_item(
1928 EquipSlot::ActiveOffhand,
1929 offhand.map(Item::new_from_asset_expect),
1930 Time(0.0),
1933 );
1934 }
1935 },
1936 Message::RandomizeCharacter => {
1938 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1939 let body_type = body.body_type;
1940 let species = body.species;
1941 let mut rng = rand::thread_rng();
1942 body.hair_style = rng.gen_range(0..species.num_hair_styles(body_type));
1943 body.beard = rng.gen_range(0..species.num_beards(body_type));
1944 body.accessory = rng.gen_range(0..species.num_accessories(body_type));
1945 body.hair_color = rng.gen_range(0..species.num_hair_colors());
1946 body.skin = rng.gen_range(0..species.num_skin_colors());
1947 body.eye_color = rng.gen_range(0..species.num_eye_colors());
1948 body.eyes = rng.gen_range(0..species.num_eyes(body_type));
1949 }
1950 },
1951 Message::HardcoreEnabled(checked) => {
1952 if let Mode::CreateOrEdit {
1953 hardcore_enabled: hardcore_checkbox,
1954 ..
1955 } = &mut self.mode
1956 {
1957 *hardcore_checkbox = checked;
1958 }
1959 },
1960 Message::RandomizeName => {
1961 if let Mode::CreateOrEdit { name, body, .. } = &mut self.mode {
1962 use common::npc;
1963 *name = npc::get_npc_name(
1964 npc::NpcKind::Humanoid,
1965 npc::BodyType::from_body(comp::Body::Humanoid(*body)),
1966 );
1967 }
1968 },
1969 Message::HairStyle(value) => {
1970 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1971 body.hair_style = value;
1972 body.validate();
1973 }
1974 },
1975 Message::HairColor(value) => {
1976 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1977 body.hair_color = value;
1978 body.validate();
1979 }
1980 },
1981 Message::Skin(value) => {
1982 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1983 body.skin = value;
1984 body.validate();
1985 }
1986 },
1987 Message::Eyes(value) => {
1988 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1989 body.eyes = value;
1990 body.validate();
1991 }
1992 },
1993 Message::EyeColor(value) => {
1994 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1995 body.eye_color = value;
1996 body.validate();
1997 }
1998 },
1999 Message::Accessory(value) => {
2000 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
2001 body.accessory = value;
2002 body.validate();
2003 }
2004 },
2005 Message::Beard(value) => {
2006 if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
2007 body.beard = value;
2008 body.validate();
2009 }
2010 },
2011 Message::StartingSite(idx) => {
2012 if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2013 *start_site_idx = Some(idx);
2014 }
2015 },
2016 Message::PrevStartingSite => {
2017 if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2018 if !self.possible_starting_sites.is_empty() {
2019 *start_site_idx = Some(
2020 (start_site_idx.unwrap_or_default()
2021 + self.possible_starting_sites.len()
2022 - 1)
2023 % self.possible_starting_sites.len(),
2024 );
2025 }
2026 }
2027 },
2028 Message::NextStartingSite => {
2029 if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2030 if !self.possible_starting_sites.is_empty() {
2031 *start_site_idx = Some(
2032 (start_site_idx.unwrap_or_default()
2033 + self.possible_starting_sites.len()
2034 + 1)
2035 % self.possible_starting_sites.len(),
2036 );
2037 }
2038 }
2039 },
2040 }
2041 }
2042
2043 pub fn display_body_inventory<'a>(
2045 &'a self,
2046 characters: &'a [CharacterItem],
2047 ) -> Option<(comp::Body, &'a Inventory)> {
2048 match &self.mode {
2049 Mode::Select { .. } => self
2050 .selected
2051 .and_then(|id| characters.iter().find(|i| i.character.id == Some(id)))
2052 .map(|i| (i.body, &i.inventory)),
2053 Mode::CreateOrEdit {
2054 inventory, body, ..
2055 } => Some((comp::Body::Humanoid(*body), inventory)),
2056 }
2057 }
2058}
2059
2060pub struct CharSelectionUi {
2061 ui: Ui,
2062 controls: Controls,
2063 enter_pressed: bool,
2064 select_character: Option<CharacterId>,
2065 pub error: Option<String>,
2066}
2067
2068impl CharSelectionUi {
2069 pub fn new(global_state: &mut GlobalState, client: &Client) -> Self {
2070 let server_name = &client.server_info().name;
2072 let selected_character = global_state.profile.get_selected_character(server_name);
2073
2074 let i18n = global_state.i18n.read();
2076
2077 let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
2079
2080 let mut ui = Ui::new(
2081 &mut global_state.window,
2082 font,
2083 global_state.settings.interface.ui_scale,
2084 )
2085 .unwrap();
2086
2087 let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
2088
2089 #[cfg(feature = "singleplayer")]
2090 let default_name = match global_state.singleplayer.is_running() {
2091 true => String::new(),
2092 false => global_state.settings.networking.username.clone(),
2093 };
2094
2095 #[cfg(not(feature = "singleplayer"))]
2096 let default_name = global_state.settings.networking.username.clone();
2097
2098 let controls = Controls::new(
2099 fonts,
2100 Imgs::load(&mut ui).expect("Failed to load images"),
2101 selected_character,
2102 default_name,
2103 client.server_info(),
2104 ui.add_graphic(Graphic::Image(
2105 Arc::clone(client.world_data().topo_map_image()),
2106 Some(default_water_color()),
2107 )),
2108 client
2109 .possible_starting_sites()
2110 .iter()
2111 .filter_map(|site_id| client.sites().get(site_id))
2112 .map(|info| info.marker.clone())
2113 .collect(),
2114 client.world_data().chunk_size().as_(),
2115 client.server_description().rules.is_some(),
2116 );
2117
2118 Self {
2119 ui,
2120 controls,
2121 enter_pressed: false,
2122 select_character: None,
2123 error: None,
2124 }
2125 }
2126
2127 pub fn display_body_inventory<'a>(
2128 &'a self,
2129 characters: &'a [CharacterItem],
2130 ) -> Option<(comp::Body, &'a Inventory)> {
2131 self.controls.display_body_inventory(characters)
2132 }
2133
2134 pub fn handle_event(&mut self, event: window::Event) -> bool {
2135 match event {
2136 window::Event::IcedUi(event) => {
2137 use iced::keyboard;
2139 if let iced::Event::Keyboard(keyboard::Event::KeyPressed {
2140 key_code: keyboard::KeyCode::Enter,
2141 ..
2142 }) = event
2143 {
2144 self.enter_pressed = true;
2145 }
2146
2147 self.ui.handle_event(event);
2148 true
2149 },
2150 window::Event::MouseButton(_, window::PressState::Pressed) => {
2151 !self.controls.mouse_detector.mouse_over()
2152 },
2153 window::Event::ScaleFactorChanged(s) => {
2154 self.ui.scale_factor_changed(s);
2155 false
2156 },
2157 _ => false,
2158 }
2159 }
2160
2161 pub fn update_language(&mut self, i18n: LocalizationHandle) {
2162 let i18n = i18n.read();
2163 let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
2164
2165 self.ui.clear_fonts(font);
2166 self.controls.fonts =
2167 Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
2168 }
2169
2170 pub fn set_scale_mode(&mut self, scale_mode: ui::ScaleMode) {
2171 self.ui.set_scaling_mode(scale_mode);
2172 }
2173
2174 pub fn select_character(&mut self, id: CharacterId) { self.select_character = Some(id); }
2175
2176 pub fn display_error(&mut self, error: String) { self.error = Some(error); }
2177
2178 pub fn maintain(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec<Event> {
2180 let mut events = Vec::new();
2181 let i18n = global_state.i18n.read();
2182
2183 let (mut messages, _) = self.ui.maintain(
2184 self.controls
2185 .view(&global_state.settings, client, &self.error, &i18n),
2186 global_state.window.renderer_mut(),
2187 None,
2188 &mut global_state.clipboard,
2189 );
2190
2191 if self.enter_pressed {
2192 self.enter_pressed = false;
2193 messages.push(match self.controls.mode {
2194 Mode::Select { .. } => Message::EnterWorld,
2195 Mode::CreateOrEdit { .. } => Message::CreateCharacter,
2196 });
2197 }
2198
2199 if let Some(id) = self.select_character.take() {
2200 messages.push(Message::Select(id))
2201 }
2202
2203 messages.into_iter().for_each(|message| {
2204 self.controls
2205 .update(message, &mut events, &client.character_list().characters)
2206 });
2207
2208 events
2209 }
2210
2211 pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) { self.ui.render(drawer); }
2212}
2213
2214#[derive(Default)]
2215struct Sliders {
2216 hair_style: slider::State,
2217 hair_color: slider::State,
2218 skin: slider::State,
2219 eyes: slider::State,
2220 eye_color: slider::State,
2221 accessory: slider::State,
2222 beard: slider::State,
2223 starting_site: slider::State,
2224}