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