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