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