Air-Trap 1.0.0
A multiplayer R-Type clone game engine built with C++23 and ECS architecture
Loading...
Searching...
No Matches
ModMenuScene.cpp
Go to the documentation of this file.
1
11#include <iostream>
12
13namespace rtp::client {
14 namespace scenes {
15
17 // Public API
19
21 Settings& settings,
22 TranslationManager& translationManager,
23 graphics::UiFactory& uiFactory,
24 std::function<void(GameState)> changeState)
25 : _uiRegistry(UiRegistry),
26 _settings(settings),
27 _translationManager(translationManager),
28 _uiFactory(uiFactory),
29 _changeState(changeState),
30 _customSpritesPath("assets/sprites/custom"),
31 _spriteMappingPath("config/client/sprite_mappings.json"),
32 _selectedCategoryIndex(0),
33 _showingCategoryList(true)
34 {
38 }
39
41 {
42 log::info("Entering ModMenuScene");
44 _loadedTextures.clear();
45 loadSpriteMappings(); // Reload mappings in case they changed
46 showCategoryList(); // Display the category list
47 }
48
50 {
51 log::info("Exiting ModMenuScene");
52 _loadedTextures.clear();
53 }
54
55 void ModMenuScene::handleEvent(const sf::Event& e)
56 {
57 if (const auto* kp = e.getIf<sf::Event::KeyPressed>()) {
58 if (kp->code == sf::Keyboard::Key::Escape) {
62 } else {
63 // Return to category list
67 }
68 }
69 }
70 }
71
72 void ModMenuScene::update(float dt)
73 {
74 (void)dt;
75 }
76
78 // Private methods
80
82 {
83 try {
84 if (!std::filesystem::exists(_customSpritesPath)) {
85 std::filesystem::create_directories(_customSpritesPath);
86 log::info("Created custom sprites directory: {}", _customSpritesPath.string());
87 }
88
89 // Create subdirectories for each category
90 std::vector<std::string> subdirs = {
91 "players", "enemies", "projectiles", "effects"
92 };
93
94 for (const auto& subdir : subdirs) {
95 auto path = _customSpritesPath / subdir;
96 if (!std::filesystem::exists(path)) {
97 std::filesystem::create_directories(path);
98 }
99 }
100
101 // Create a README file with instructions
102 auto readmePath = _customSpritesPath / "README.txt";
103 if (!std::filesystem::exists(readmePath)) {
104 std::ofstream readme(readmePath);
105 readme << "Custom Sprites Directory\n";
106 readme << "========================\n\n";
107 readme << "Place your custom sprite files here to replace game sprites.\n\n";
108 readme << "Supported formats: PNG, JPG\n\n";
109 readme << "Categories:\n";
110 readme << " - players/ : Player ship sprites\n";
111 readme << " - enemies/ : Enemy ship sprites\n";
112 readme << " - projectiles/ : Bullet and missile sprites\n";
113 readme << " - effects/ : Explosion and effect sprites\n\n";
114 readme << "Note: Make sure your sprites have transparent backgrounds (PNG recommended)\n";
115 readme.close();
116 }
117 } catch (const std::exception& e) {
118 log::error("Failed to initialize mod directory: {}", e.what());
119 }
120 }
121
123 {
124 // Players category (vaisseaux joueurs)
125 EntityCategory players;
126 players.name = "Players";
127 players.entities = {
128 {"player_ship", "assets/sprites/r-typesheet1.gif", 101, 3, 33, 14} // player_ship - the actual player sprite
129 };
130
131 // Enemies category (ennemis)
132 EntityCategory enemies;
133 enemies.name = "Enemies";
134 enemies.entities = {
135 {"enemy_1", "assets/sprites/r-typesheet2.gif", 159, 35, 24, 16}, // enemy_1
136 {"enemy_2", "assets/sprites/r-typesheet2.gif", 300, 71, 30, 18}, // enemy_2
137 };
138
139 // Projectiles category (projectiles)
140 EntityCategory projectiles;
141 projectiles.name = "Projectiles";
142 projectiles.entities = {
143 {"player_shot_6", "assets/sprites/r-typesheet2.gif", 300, 58, 18, 6}, // player_shot_6 (player bullets)
144 {"enemy_shot_6", "assets/sprites/r-typesheet2.gif", 300, 58, 18, 6}, // enemy_shot_6 (enemy bullets)
145 {"shot_insane", "assets/sprites/r-typesheet1.gif", 134, 18, 33, 32}, // shot_insane
146 {"shot_1", "assets/sprites/r-typesheet1.gif", 215, 85, 18, 12}, // shot_1
147 {"shot_2", "assets/sprites/r-typesheet1.gif", 232, 103, 17, 12}, // shot_2
148 {"shot_3", "assets/sprites/r-typesheet1.gif", 200, 121, 33, 10}, // shot_3
149 {"shot_4", "assets/sprites/r-typesheet1.gif", 168, 137, 49, 12}, // shot_4
150 {"shot_5", "assets/sprites/r-typesheet1.gif", 104, 171, 81, 14}, // shot_5
151 {"shot_7", "assets/sprites/r-typesheet2.gif", 266, 94, 17, 10}, // shot_7
152 };
153
154 // Effects category (explosions/effets)
155 EntityCategory effects;
156 effects.name = "Effects";
157 effects.entities = {
158 {"effect_1", "assets/sprites/r-typesheet1.gif", 2, 51, 33, 32}, // effect_1
159 {"effect_2", "assets/sprites/r-typesheet1.gif", 211, 276, 16, 12}, // effect_2
160 {"effect_3", "assets/sprites/r-typesheet1.gif", 72, 296, 37, 30}, // effect_3
161 {"effect_4", "assets/sprites/r-typesheet2.gif", 101, 118, 17, 14}, // effect_4
162 {"effect_5", "assets/sprites/r-typesheet2.gif", 157, 316, 18, 14}, // effect_5
163 {"power_up", "assets/sprites/r-typesheet3.gif", 0, 0, 16, 16} // power_up (frame 0 of animation)
164 };
165
166 _categories = {players, enemies, projectiles, effects};
167 }
168
170 {
171 // Title
174 {400.0f, 50.0f},
175 "Mod Menu - Entity Categories",
176 "assets/fonts/title.ttf",
177 56,
178 10,
179 {2, 100, 100}
180 );
181
182 // Instructions
185 {100.0f, 150.0f},
186 "Select a category to view and customize entity sprites",
187 "assets/fonts/main.ttf",
188 24,
189 5,
190 {255, 255, 255}
191 );
192
193 // Create category buttons with entity counts
194 float yPos = 220.0f;
195 const float buttonSpacing = 80.0f;
196
197 for (size_t i = 0; i < _categories.size(); ++i) {
198 const auto& category = _categories[i];
199 std::string buttonText = category.name + " (" + std::to_string(category.entities.size()) + ")";
200
203 {100.0f, yPos},
204 {600.0f, 60.0f},
205 buttonText,
206 [this, i]() {
208 _showingCategoryList = false;
211 }
212 );
213 yPos += buttonSpacing;
214 }
215
216 // Back button
219 {490.0f, 650.0f},
220 {300.0f, 60.0f},
221 _translationManager.get("settings.back"),
222 [this]() {
225 }
226 );
227 }
228
229 void ModMenuScene::showEntityCodex(size_t categoryIndex)
230 {
231 if (categoryIndex >= _categories.size()) {
232 log::error("Invalid category index: {}", categoryIndex);
233 return;
234 }
235
236 const auto& category = _categories[categoryIndex];
237
238 // Title
241 {400.0f, 30.0f},
242 category.name + " Codex",
243 "assets/fonts/title.ttf",
244 48,
245 10,
246 {2, 100, 100}
247 );
248
249 // Display entities in a grid
250 const float startX = 100.0f;
251 const float startY = 120.0f;
252 const float spacingX = 200.0f;
253 const float spacingY = 240.0f;
254 const int itemsPerRow = 6;
255
256 for (size_t i = 0; i < category.entities.size(); ++i) {
257 const auto& entity = category.entities[i];
258
259 int row = i / itemsPerRow;
260 int col = i % itemsPerRow;
261
262 float x = startX + col * spacingX;
263 float y = startY + row * spacingY;
264
265 // Check if entity has custom sprite
266 std::string entityKey = getEntityKey(entity);
267 std::string displayTexturePath = entity.texturePath;
268 int displayLeft = entity.rectLeft;
269 int displayTop = entity.rectTop;
270 int displayWidth = entity.rectWidth;
271 int displayHeight = entity.rectHeight;
272
273 // If custom sprite exists, use it instead
274 auto customIt = _customSpriteMappings.find(entityKey);
275 if (customIt != _customSpriteMappings.end()) {
276 displayTexturePath = customIt->second;
277 displayLeft = 0;
278 displayTop = 0;
279 // displayWidth and displayHeight stay the same for scaling
280 }
281
282 // Display sprite preview
285 {x + 50.0f, y + 10.0f},
286 displayTexturePath,
287 displayLeft,
288 displayTop,
289 displayWidth,
290 displayHeight,
291 3.0f, // scale - bigger for better visibility
292 5 // zIndex
293 );
294
295 // Entity name text
296 std::string displayName = entity.name;
297 if (customIt != _customSpriteMappings.end()) {
298 displayName += " *"; // Mark modded sprites with asterisk
299 }
300
303 {x, y + 140.0f},
304 displayName,
305 "assets/fonts/main.ttf",
306 14,
307 5,
308 {255, 255, 255}
309 );
310
311 // Customize button
314 {x + 10.0f, y + 170.0f},
315 {160.0f, 35.0f},
316 customIt != _customSpriteMappings.end() ? "Reset" : "Customize",
317 [this, entity, categoryName = category.name, customIt]() {
318 if (customIt != _customSpriteMappings.end()) {
319 // Reset to default sprite
320 std::string entityKey = getEntityKey(entity);
321 _customSpriteMappings.erase(entityKey);
323 // Reload SpriteCustomizer to update global state
325
326 // Clear texture caches in render systems
328
329 _loadedTextures.clear();
332 log::info("Reset sprite for: {}", entity.name);
333 } else {
334 // Apply custom sprite
335 applyCustomSprite(entity, categoryName);
336 }
337 }
338 );
339 }
340
341 // Back button
344 {490.0f, 650.0f},
345 {300.0f, 60.0f},
346 _translationManager.get("settings.back"),
347 [this]() {
351 }
352 );
353 }
354
355 sf::Texture* ModMenuScene::loadTexture(const std::string& texturePath)
356 {
357 auto it = _loadedTextures.find(texturePath);
358 if (it != _loadedTextures.end()) {
359 return &it->second;
360 }
361
362 sf::Texture texture;
363 if (texture.loadFromFile(texturePath)) {
364 _loadedTextures[texturePath] = std::move(texture);
365 return &_loadedTextures[texturePath];
366 }
367
368 log::error("Failed to load texture: {}", texturePath);
369 return nullptr;
370 }
371
372 std::string ModMenuScene::getEntityKey(const EntitySpriteInfo& entity) const
373 {
374 // Create unique key from entity name (replace spaces with underscores)
375 std::string key = entity.name;
376 std::replace(key.begin(), key.end(), ' ', '_');
377 return key;
378 }
379
381 {
382 try {
384 log::info("Saved sprite mappings to {}", _spriteMappingPath.string());
385 } else {
386 log::error("Failed to save sprite mappings to {}", _spriteMappingPath.string());
387 }
388 } catch (const std::exception& e) {
389 log::error("Failed to save sprite mappings: {}", e.what());
390 }
391 }
392
394 {
395 try {
396 if (!std::filesystem::exists(_spriteMappingPath)) {
397 log::info("No sprite mapping file found, using defaults");
398 return;
399 }
400
402 log::info("Loaded {} custom sprite mappings", _customSpriteMappings.size());
403 } catch (const std::exception& e) {
404 log::error("Failed to load sprite mappings: {}", e.what());
405 }
406 }
407
409 {
410#ifdef _WIN32
411 // Windows: use PowerShell to show file dialog
412 std::string command = R"(powershell -Command "Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.OpenFileDialog; $f.Filter = 'Images|*.png;*.jpg;*.jpeg;*.gif'; $f.Title = 'Select Custom Sprite'; if($f.ShowDialog() -eq 'OK'){$f.FileName}")";
413
414 FILE* pipe = _popen(command.c_str(), "r");
415 if (!pipe) {
416 log::error("Failed to open file dialog");
417 return "";
418 }
419
420 char buffer[512];
421 std::string result;
422 if (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
423 result = buffer;
424 // Remove trailing newline/carriage return
425 while (!result.empty() && (result.back() == '\n' || result.back() == '\r')) {
426 result.pop_back();
427 }
428 }
429 _pclose(pipe);
430
431 return result;
432#else
433 // Linux: use zenity for file dialog
434 std::string command = "zenity --file-selection --title='Select Custom Sprite' --file-filter='Images | *.png *.jpg *.jpeg *.gif'";
435
436 FILE* pipe = popen(command.c_str(), "r");
437 if (!pipe) {
438 log::error("Failed to open file dialog");
439 return "";
440 }
441
442 char buffer[512];
443 std::string result;
444 if (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
445 result = buffer;
446 // Remove trailing newline
447 if (!result.empty() && result.back() == '\n') {
448 result.pop_back();
449 }
450 }
451 pclose(pipe);
452
453 return result;
454#endif
455 }
456
457 sf::Image ModMenuScene::resizeImage(const sf::Image& sourceImage, unsigned int targetWidth, unsigned int targetHeight)
458 {
459 sf::Vector2u sourceSize = sourceImage.getSize();
460 sf::Image resizedImage(sf::Vector2u(targetWidth, targetHeight));
461
462 // Simple nearest-neighbor scaling
463 for (unsigned int y = 0; y < targetHeight; ++y) {
464 for (unsigned int x = 0; x < targetWidth; ++x) {
465 unsigned int srcX = (x * sourceSize.x) / targetWidth;
466 unsigned int srcY = (y * sourceSize.y) / targetHeight;
467 resizedImage.setPixel(sf::Vector2u(x, y), sourceImage.getPixel(sf::Vector2u(srcX, srcY)));
468 }
469 }
470
471 return resizedImage;
472 }
473
474 void ModMenuScene::applyCustomSprite(const EntitySpriteInfo& entity, const std::string& categoryName)
475 {
476 // Open file dialog
477 std::string selectedFile = openFileDialog();
478 if (selectedFile.empty()) {
479 log::info("No file selected");
480 return;
481 }
482
483 log::info("Selected sprite file: {}", selectedFile);
484
485 try {
486 // Load the selected image
487 sf::Image customImage;
488 if (!customImage.loadFromFile(selectedFile)) {
489 log::error("Failed to load custom sprite: {}", selectedFile);
490 return;
491 }
492
493 // Resize to match original sprite dimensions
494 sf::Image resizedImage = resizeImage(customImage, entity.rectWidth, entity.rectHeight);
495
496 // Create category directory if needed
497 std::string categoryLower = categoryName;
498 std::transform(categoryLower.begin(), categoryLower.end(), categoryLower.begin(), ::tolower);
499 auto categoryPath = _customSpritesPath / categoryLower;
500 if (!std::filesystem::exists(categoryPath)) {
501 std::filesystem::create_directories(categoryPath);
502 }
503
504 // Save to custom sprites directory with entity name
505 std::string entityKey = getEntityKey(entity);
506 auto outputPath = categoryPath / (entityKey + ".png");
507
508 if (resizedImage.saveToFile(outputPath.string())) {
509 log::info("Saved custom sprite to: {}", outputPath.string());
510
511 // Update mapping
512 _customSpriteMappings[entityKey] = outputPath.string();
514
515 // Reload SpriteCustomizer to update global state
517
518 // Clear texture caches in render systems
520
521 // Clear texture cache so it reloads
522 _loadedTextures.clear();
523
524 // Refresh the view
527
528 log::info("Custom sprite applied successfully!");
529 } else {
530 log::error("Failed to save resized sprite");
531 }
532 } catch (const std::exception& e) {
533 log::error("Error applying custom sprite: {}", e.what());
534 }
535 }
536
537 } // namespace scenes
538} // namespace rtp::client
Manages game settings and preferences.
Definition Settings.hpp:67
static void clearTextureCaches()
Clear all texture caches in render systems This should be called when sprites are modified to ensure ...
void reloadMappings()
Reload sprite mappings (useful after modifications) This clears the cache and reloads from disk.
static SpriteCustomizer & getInstance()
Get the singleton instance.
Manages game translations for all UI elements.
std::string get(const std::string &key) const
Get translated string for a key.
Factory class for creating UI components in the ECS registry.
Definition UiFactory.hpp:72
static ecs::Entity createSpritePreview(ecs::Registry &registry, const position &position, const std::string &texturePath, int rectLeft, int rectTop, int rectWidth, int rectHeight, float scale=2.0f, int zIndex=0)
Create a sprite preview UI component.
static ecs::Entity createText(ecs::Registry &registry, const position &position, const std::string &content, const std::string &fontPath, unsigned int fontSize, const std::uint8_t zIndex=0, const color &textColor={255, 255, 255})
Definition UiFactory.cpp:43
static ecs::Entity createButton(ecs::Registry &registry, const position &position, const size &size, const std::string &label, std::function< void()> onClick=nullptr)
Create a button UI component.
Definition UiFactory.cpp:17
bool _showingCategoryList
True if showing category list, false if showing entity codex.
std::filesystem::path _customSpritesPath
Path to custom sprites directory.
std::filesystem::path _spriteMappingPath
Path to sprite mapping config file.
std::map< std::string, sf::Texture > _loadedTextures
Cache of loaded textures.
void initializeModDirectory()
Initialize the custom sprites directory.
sf::Image resizeImage(const sf::Image &sourceImage, unsigned int targetWidth, unsigned int targetHeight)
Resize an image to match target dimensions.
size_t _selectedCategoryIndex
Currently selected category index.
ecs::Registry & _uiRegistry
Reference to the ECS registry.
void loadEntityCategories()
Load all entity categories and their sprites.
void applyCustomSprite(const EntitySpriteInfo &entity, const std::string &categoryName)
Apply a custom sprite to an entity.
sf::Texture * loadTexture(const std::string &texturePath)
Load a texture if not already loaded.
TranslationManager & _translationManager
Reference to the translation manager.
void update(float dt) override
Update the scene state.
std::vector< EntityCategory > _categories
Available entity categories with their sprites.
ModMenuScene(ecs::Registry &UiRegistry, Settings &settings, TranslationManager &translationManager, graphics::UiFactory &uiFactory, std::function< void(GameState)> changeState)
Constructor for ModMenuScene.
void handleEvent(const sf::Event &event) override
Handle an incoming event.
std::unordered_map< std::string, std::string > _customSpriteMappings
ChangeStateFn _changeState
Function to change the game state.
graphics::UiFactory & _uiFactory
UI Factory for creating UI components.
void onEnter(void) override
Called when the scene is entered.
void showEntityCodex(size_t categoryIndex)
Show the entity codex for a specific category.
std::string openFileDialog()
Open file dialog to select a custom sprite.
std::string getEntityKey(const EntitySpriteInfo &entity) const
Get the unique key for an entity.
void onExit(void) override
Called when the scene is exited.
void saveSpriteMappings()
Save custom sprite mappings to config file.
void showCategoryList()
Show the main category selection screen.
void loadSpriteMappings()
Load custom sprite mappings from config file.
void clear(void) noexcept
Definition Registry.cpp:102
R-Type client namespace.
@ Menu
Main menu state.
bool saveSpriteMappings(const std::unordered_map< std::string, std::string > &mappings, const std::string &filepath="config/client/sprite_mappings.json")
Save sprite mappings to a JSON file.
std::unordered_map< std::string, std::string > loadSpriteMappings(const std::string &filepath="config/client/sprite_mappings.json")
Load sprite mappings from a JSON file.
void error(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an error message.
void info(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an informational message.
Category of entities with their sprites.
std::vector< EntitySpriteInfo > entities
List of entities in this category.
std::string name
Category name.
Information about an entity's sprite for the mod menu.
int rectHeight
Height of the sprite.
std::string name
Display name of the entity.