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