Skip to main content

launcher/pages/riders/
mod.rs

1use 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>, // Base Name -> Skin Name
17    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        // Ensure the case matches the disk directory (pretty name)
54        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            // Get selected skin for this base character
123            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            // Resolve texture based on selected skin
135            // TODO: Investigate the use better typing to avoid string-based lookup.
136            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}