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