launcher/pages/riders/
mod.rs1use crate::utils::texture_loader::load_texture_robust;
2use crate::widgets::gallery::{GalleryConfig, GalleryState, gallery_grid};
3use crate::widgets::tile::gallery_tile;
4use eframe::egui;
5use std::collections::HashMap;
6use supreme_characters::{
7 CharacterMaterial, get_character_texture_path, get_ghost_character, get_grouped_characters,
8 identify_active_skin, set_character_skin, set_ghost_character,
9};
10
11#[derive(serde::Serialize, serde::Deserialize)]
12#[serde(default)]
13pub struct RidersPageState {
14 pub gallery: GalleryState,
15 pub selected_material: CharacterMaterial,
16 pub selected_skins: HashMap<String, String>, pub ghost_character: Option<String>,
18}
19
20impl Default for RidersPageState {
21 fn default() -> Self {
22 Self {
23 gallery: GalleryState::default(),
24 selected_material: CharacterMaterial::Face,
25 selected_skins: HashMap::new(),
26 ghost_character: None,
27 }
28 }
29}
30
31#[allow(clippy::too_many_lines)]
32pub fn riders_page(
33 ui: &mut egui::Ui,
34 storage: &supreme_fs::GameStorage,
35 state: &mut RidersPageState,
36) {
37 let game_path = storage.game_path();
38 let characters_dir = game_path.join("Data/Characters");
39
40 if !characters_dir.exists() {
41 ui.colored_label(egui::Color32::RED, "Characters directory not found!");
42 return;
43 }
44
45 let characters = get_grouped_characters(&characters_dir);
46 let current_material = state.selected_material;
47
48 if state.ghost_character.is_none() {
49 state.ghost_character = get_ghost_character(&characters_dir);
50 }
51
52 if let Some(ghost) = state.ghost_character.as_ref() {
53 let pretty_name = characters
55 .iter()
56 .find(|c| c.name.to_lowercase() == ghost.to_lowercase())
57 .map(|c| c.name.clone());
58
59 if let Some(pretty) = pretty_name
60 && &pretty != ghost
61 {
62 state.ghost_character = Some(pretty);
63 }
64 }
65
66 gallery_grid(
67 ui,
68 GalleryConfig {
69 id: "riders_grid",
70 title: "Riders",
71 info_text: Some(
72 "To create a new skin: duplicate an original 'bulk' character folder corresponding to the one you want, then edit the texture files. Ensure the .txt files inside are edited to match the new directory name you created.",
73 ),
74 open_folder_path: Some(&characters_dir),
75 },
76 &mut state.gallery,
77 &characters,
78 |ui| {
79 egui::ComboBox::from_id_salt("char_material_select")
80 .selected_text(state.selected_material.to_string())
81 .show_ui(ui, |ui| {
82 for mat in CharacterMaterial::all() {
83 ui.selectable_value(&mut state.selected_material, mat, mat.to_string());
84 }
85 });
86 ui.label("Texture:");
87
88 ui.add_space(8.0);
89
90 let mut selected_ghost = state.ghost_character.clone().unwrap_or_default();
91 let mut ghost_changed = false;
92
93 egui::ComboBox::from_id_salt("ghost_select")
94 .selected_text(&selected_ghost)
95 .show_ui(ui, |ui| {
96 for base_char in &characters {
97 if ui
98 .selectable_value(
99 &mut selected_ghost,
100 base_char.name.clone(),
101 &base_char.name,
102 )
103 .changed()
104 {
105 ghost_changed = true;
106 }
107 }
108 });
109
110 if ghost_changed {
111 if let Err(e) = set_ghost_character(&characters_dir, &selected_ghost) {
112 eprintln!("[Riders] Failed to set ghost character to '{selected_ghost}': {e}");
113 } else {
114 state.ghost_character = Some(selected_ghost);
115 }
116 }
117 ui.label("Ghost:");
118 },
119 |ui, base_char, tile_size, gallery_state| {
120 let base_name = &base_char.name;
121
122 let selected_skin_name = state
124 .selected_skins
125 .entry(base_name.clone())
126 .or_insert_with(|| identify_active_skin(&base_char.base_dir, &base_char.skins));
127
128 let selected_skin = base_char
129 .skins
130 .iter()
131 .find(|s| &s.name == selected_skin_name)
132 .unwrap_or(&base_char.skins[0]);
133
134 let texture_key = format!(
137 "char_{}_{}_{}",
138 base_name,
139 selected_skin.name,
140 current_material.to_string().to_lowercase()
141 );
142
143 if !gallery_state.textures.contains_key(&texture_key)
144 && let Some(texture_path) =
145 get_character_texture_path(&selected_skin.dir, current_material)
146 {
147 let full_texture_path = game_path.join(&texture_path);
148 if let Ok(texture) = load_texture_robust(ui.ctx(), &full_texture_path, &texture_key)
149 {
150 gallery_state.textures.insert(texture_key.clone(), texture);
151 }
152 }
153
154 gallery_tile(
155 ui,
156 tile_size,
157 1.0,
158 base_name,
159 |ui| {
160 if base_char.skins.len() > 1 || selected_skin_name == "(Custom)" {
161 egui::ComboBox::from_id_salt(format!("skin_select_{base_name}"))
162 .selected_text(if selected_skin_name == "(Custom)" {
163 egui::RichText::new("(Custom)").italics()
164 } else {
165 egui::RichText::new(selected_skin_name.clone())
166 })
167 .width(80.0)
168 .show_ui(ui, |ui| {
169 if selected_skin_name == "(Custom)" {
170 ui.selectable_value(
171 selected_skin_name,
172 "(Custom)".to_string(),
173 egui::RichText::new("(Custom)").italics(),
174 );
175 }
176 for skin in &base_char.skins {
177 if ui
178 .selectable_value(
179 selected_skin_name,
180 skin.name.clone(),
181 &skin.name,
182 )
183 .changed()
184 && let Err(e) =
185 set_character_skin(&base_char.base_dir, &skin.dir)
186 {
187 eprintln!("Failed to set character skin: {e}");
188 }
189 }
190 });
191 }
192 },
193 gallery_state.textures.get(&texture_key),
194 );
195 },
196 );
197}