Air-Trap 1.0.0
A multiplayer R-Type clone game engine built with C++23 and ECS architecture
Loading...
Searching...
No Matches
MenuScene.cpp
Go to the documentation of this file.
1
13#include <algorithm>
14#include <vector>
15#include <cmath>
17
18namespace rtp::client {
19 namespace scenes {
20
22 // Public API
24
26 ecs::Registry& worldRegistry,
27 Settings& settings,
28 TranslationManager& translationManager,
29 NetworkSyncSystem& network,
30 graphics::UiFactory& uiFactory,
31 EntityBuilder& worldEntityBuilder,
32 std::function<void(GameState)> changeState)
33 : _uiRegistry(UiRegistry),
34 _worldRegistry(worldRegistry),
35 _settings(settings),
36 _translationManager(translationManager),
37 _network(network),
38 _uiFactory(uiFactory),
39 _worldEntityBuilder(worldEntityBuilder),
40 _changeState(changeState),
41 _menuMusicEntity()
42 {
43 }
44
46 {
47 log::info("Entering MenuScene");
48 _menuTime = 0.0f;
49 _menuWorldEntities.clear();
50 _menuEnemies.clear();
51
52 // Ensure we load the latest weapon configurations so UI shows correct details
54
56 bool musicAlreadyPlaying = false;
57
58 if (audioSources) {
59 auto& sources = audioSources.value().get();
60 for (const auto& entity : sources.entities()) {
61 auto& source = sources[entity];
62 if (source.audioPath == "assets/musics/menu.mp3") {
63 musicAlreadyPlaying = true;
64 _menuMusicEntity = entity;
65 log::info("Menu music already playing, reusing it");
66 break;
67 }
68 }
69 }
70
71 if (!musicAlreadyPlaying) {
72 auto musicEntity = _uiRegistry.spawn();
73 if (musicEntity) {
74 _menuMusicEntity = musicEntity.value();
76 menuMusic.audioPath = "assets/musics/menu.mp3";
77 menuMusic.volume = 80.0f;
78 menuMusic.loop = true;
79 menuMusic.isPlaying = true;
80 menuMusic.dirty = true;
81
83 log::info("Playing menu music");
84 }
85 }
86
87 auto styleButton = [this](ecs::Entity entity,
88 const ecs::components::ui::Button &style) {
89 auto buttonsOpt = _uiRegistry.get<ecs::components::ui::Button>();
90 if (!buttonsOpt) {
91 return;
92 }
93 auto &buttons = buttonsOpt.value().get();
94 if (!buttons.has(entity)) {
95 return;
96 }
97 auto &button = buttons[entity];
98 std::copy(std::begin(style.idleColor), std::end(style.idleColor),
99 std::begin(button.idleColor));
100 std::copy(std::begin(style.hoverColor), std::end(style.hoverColor),
101 std::begin(button.hoverColor));
102 std::copy(std::begin(style.pressedColor), std::end(style.pressedColor),
103 std::begin(button.pressedColor));
104 };
105
107 panelStyle.idleColor[0] = 24;
108 panelStyle.idleColor[1] = 28;
109 panelStyle.idleColor[2] = 36;
110 std::copy(std::begin(panelStyle.idleColor), std::end(panelStyle.idleColor),
111 std::begin(panelStyle.hoverColor));
112 std::copy(std::begin(panelStyle.idleColor), std::end(panelStyle.idleColor),
113 std::begin(panelStyle.pressedColor));
114
115 ecs::components::ui::Button primaryStyle;
116 primaryStyle.idleColor[0] = 32;
117 primaryStyle.idleColor[1] = 140;
118 primaryStyle.idleColor[2] = 140;
119 primaryStyle.hoverColor[0] = 48;
120 primaryStyle.hoverColor[1] = 170;
121 primaryStyle.hoverColor[2] = 170;
122 primaryStyle.pressedColor[0] = 20;
123 primaryStyle.pressedColor[1] = 110;
124 primaryStyle.pressedColor[2] = 110;
125
126 ecs::components::ui::Button secondaryStyle;
127 secondaryStyle.idleColor[0] = 70;
128 secondaryStyle.idleColor[1] = 74;
129 secondaryStyle.idleColor[2] = 84;
130 secondaryStyle.hoverColor[0] = 95;
131 secondaryStyle.hoverColor[1] = 100;
132 secondaryStyle.hoverColor[2] = 112;
133 secondaryStyle.pressedColor[0] = 55;
134 secondaryStyle.pressedColor[1] = 60;
135 secondaryStyle.pressedColor[2] = 70;
136
137 auto spawnParallaxLayer = [this](const EntityTemplate& base) {
138 EntityTemplate first = base;
139 first.position = {0.0f, 0.0f};
140 if (auto e = _worldEntityBuilder.spawn(first)) {
141 _menuWorldEntities.push_back(e.value());
142 }
143
144 EntityTemplate second = base;
145 float width = base.parallax.textureWidth * base.scale.x;
146 second.position = {width, 0.0f};
147 if (auto e = _worldEntityBuilder.spawn(second)) {
148 _menuWorldEntities.push_back(e.value());
149 }
150 };
151
152 spawnParallaxLayer(EntityTemplate::createParrallaxLvl1_1());
153 spawnParallaxLayer(EntityTemplate::createParrallaxLvl1_2());
154
155 struct EnemySpec {
156 EntityTemplate (*create)(const Vec2f&);
157 float startX;
158 float baseY;
159 float speed;
160 float amplitude;
161 float frequency;
162 float phase;
163 float scale;
164 };
165
166 const EnemySpec enemySpecs[] = {
167 {EntityTemplate::enemy_1, 1350.0f, 180.0f, 32.0f, 10.0f, 1.2f, 0.0f, 1.2f},
168 {EntityTemplate::enemy_2, 1550.0f, 260.0f, 46.0f, 14.0f, 1.4f, 1.2f, 1.1f},
169 {EntityTemplate::enemy_1, 1750.0f, 380.0f, 38.0f, 12.0f, 1.1f, 2.1f, 1.0f},
170 {EntityTemplate::enemy_2, 1950.0f, 520.0f, 54.0f, 16.0f, 1.0f, 3.0f, 1.25f}
171 };
172
173 for (const auto& spec : enemySpecs) {
174 EntityTemplate t = spec.create({spec.startX, spec.baseY});
175 t.scale = {spec.scale, spec.scale};
176 if (auto e = _worldEntityBuilder.spawn(t)) {
177 ecs::Entity entity = e.value();
178 _menuWorldEntities.push_back(entity);
179 _menuEnemies.push_back(MenuEnemy{
180 entity,
181 spec.speed,
182 spec.baseY,
183 spec.amplitude,
184 spec.frequency,
185 spec.phase,
186 spec.startX
187 });
188 }
189 }
190
193 {404.0f, 74.0f},
194 _translationManager.get("menu.title"),
195 "assets/fonts/title.ttf",
196 72,
197 9,
198 {12, 14, 18}
199 );
200
203 {400.0f, 70.0f},
204 _translationManager.get("menu.title"),
205 "assets/fonts/title.ttf",
206 72,
207 10,
208 {48, 170, 170}
209 );
210
213 {470.0f, 150.0f},
214 "Ready for launch",
215 "assets/fonts/main.ttf",
216 20,
217 10,
218 {170, 180, 190}
219 );
220
221 const float weaponPanelX = 80.0f;
222 const float weaponPanelY = 200.0f;
223 const float weaponPanelW = 300.0f;
224 const float weaponPanelH = 320.0f;
225
226 // Weapon selection list (explicit available kinds)
227 const std::vector<ecs::components::WeaponKind> availableKinds = {
232 };
233
234 // Left arrow button
235 auto weaponPanel = _uiFactory.createButton(
237 {weaponPanelX, weaponPanelY},
238 {weaponPanelW, weaponPanelH},
239 "",
240 nullptr
241 );
242 styleButton(weaponPanel, panelStyle);
243
246 {weaponPanelX + 20.0f, weaponPanelY + 18.0f},
247 "LOADOUT",
248 "assets/fonts/main.ttf",
249 18,
250 5,
251 {170, 180, 190}
252 );
253
256 {weaponPanelX + 20.0f, weaponPanelY + 50.0f},
257 "WEAPON",
258 "assets/fonts/main.ttf",
259 16,
260 5,
261 {130, 140, 150}
262 );
263
264 auto leftArrow = _uiFactory.createButton(
266 {weaponPanelX + 20.0f, weaponPanelY + 85.0f},
267 {44.0f, 44.0f},
268 "<",
269 [this, availableKinds]() {
270 auto currentKind = _settings.getSelectedWeapon();
271 size_t idx = 0;
272 for (size_t i = 0; i < availableKinds.size(); ++i) {
273 if (availableKinds[i] == currentKind) { idx = i; break; }
274 }
275 if (idx == 0) idx = availableKinds.size() - 1;
276 else --idx;
277 _settings.setSelectedWeapon(availableKinds[idx]);
278 _settings.save("config/settings.cfg");
281 }
282 );
283 styleButton(leftArrow, secondaryStyle);
284
285 std::string initialWeaponName = _settings.getWeaponName(_settings.getSelectedWeapon());
288 if (!dn.empty()) initialWeaponName = dn;
289 }
290
293 {weaponPanelX + 80.0f, weaponPanelY + 93.0f},
294 initialWeaponName,
295 "assets/fonts/main.ttf",
296 20,
297 5,
298 {255, 214, 120}
299 );
300
301 auto rightArrow = _uiFactory.createButton(
303 {weaponPanelX + weaponPanelW - 64.0f, weaponPanelY + 85.0f},
304 {44.0f, 44.0f},
305 ">",
306 [this, availableKinds]() {
307 auto currentKind = _settings.getSelectedWeapon();
308 size_t idx = 0;
309 for (size_t i = 0; i < availableKinds.size(); ++i) {
310 if (availableKinds[i] == currentKind) { idx = i; break; }
311 }
312 idx = (idx + 1) % availableKinds.size();
313 _settings.setSelectedWeapon(availableKinds[idx]);
314 _settings.save("config/settings.cfg");
317 }
318 );
319 styleButton(rightArrow, secondaryStyle);
320
323 {weaponPanelX + 20.0f, weaponPanelY + 150.0f},
324 "",
325 "assets/fonts/main.ttf",
326 14,
327 5,
328 {180, 190, 200}
329 );
331
332 struct BtnDef { std::string key; std::function<void()> cb; };
333 std::vector<BtnDef> buttons{
334 {"menu.play", [this]() { _network.requestListRooms(); _changeState(GameState::Lobby); }},
335 {"menu.singleplayer", [this]() { _network.tryStartSolo(); _changeState(GameState::RoomWaiting); }},
336 {"menu.settings", [this]() { _changeState(GameState::Settings); }},
337 {"menu.mods", [this]() { _changeState(GameState::ModMenu); }},
338 {"menu.exit", [this]() { std::exit(0); }}
339 };
340
341 const float panelW = 420.0f;
342 const float panelH = 380.0f;
343 const float panelX = (1280.0f - panelW) / 2.0f;
344 const float panelY = 210.0f;
345
346 auto menuPanel = _uiFactory.createButton(
348 {panelX, panelY},
349 {panelW, panelH},
350 "",
351 nullptr
352 );
353 styleButton(menuPanel, panelStyle);
354
357 {panelX + 40.0f, panelY + 20.0f},
358 "SELECT MODE",
359 "assets/fonts/main.ttf",
360 18,
361 10,
362 {170, 180, 190}
363 );
364
365 const float startX = panelX + 40.0f;
366 const float startY = panelY + 70.0f;
367 const float spacingY = 68.0f;
368 const graphics::size btnSize{panelW - 80.0f, 60.0f};
369
370 for (size_t i = 0; i < buttons.size(); ++i) {
371 float y = startY + static_cast<float>(i) * spacingY;
374 {startX, y},
375 btnSize,
376 _translationManager.get(buttons[i].key),
377 buttons[i].cb
378 );
379 if (i == 0) {
380 styleButton(button, primaryStyle);
381 } else {
382 styleButton(button, secondaryStyle);
383 }
384 }
385
388 {40.0f, 680.0f},
389 "Tip: Customize sprites in Mods",
390 "assets/fonts/main.ttf",
391 16,
392 10,
393 {120, 130, 140}
394 );
395 }
396
398 {
399 log::info("Exiting MenuScene");
400 // La musique sera arrêtée par Application::stopAllUiSounds()
401 for (ecs::Entity entity : _menuWorldEntities) {
403 }
404 _menuWorldEntities.clear();
405 _menuEnemies.clear();
406 }
407
408 void MenuScene::handleEvent(const sf::Event& e)
409 {
410 if (const auto* kp = e.getIf<sf::Event::KeyPressed>()) {
411 if (kp->code == sf::Keyboard::Key::Escape) {
412 std::exit(0);
413 }
414 }
415 }
416
417 void MenuScene::update(float dt)
418 {
419 _menuTime += dt;
420
421 auto transformsOpt = _worldRegistry.get<ecs::components::Transform>();
422 if (!transformsOpt) {
423 return;
424 }
425
426 auto& transforms = transformsOpt.value().get();
427 for (auto& enemy : _menuEnemies) {
428 if (!transforms.has(enemy.entity)) {
429 continue;
430 }
431 auto& transform = transforms[enemy.entity];
432 transform.position.x -= enemy.speed * dt;
433 if (transform.position.x < -120.0f) {
434 transform.position.x = enemy.resetX;
435 }
436 transform.position.y = enemy.baseY +
437 std::sin(_menuTime * enemy.frequency + enemy.phase) * enemy.amplitude;
438 }
439 }
440
442 {
443 auto weapon = _settings.getSelectedWeapon();
444
446 if (texts && texts.value().get().has(_weaponNameText)) {
447 auto& text = texts.value().get()[_weaponNameText];
448 // Prefer configured display name if available
450 // WeaponConfig provides getWeaponDisplayName()
451 auto dn = rtp::config::getWeaponDisplayName(weapon);
452 if (!dn.empty()) text.content = dn;
453 else text.content = _settings.getWeaponName(weapon);
454 } else {
455 text.content = _settings.getWeaponName(weapon);
456 }
457 }
458
459 std::string stats;
461 auto def = rtp::config::getWeaponDef(weapon);
462 stats += "Damage: " + std::to_string(def.damage) + "\n";
463 if (def.maxAmmo < 0 || def.ammo < 0) {
464 stats += "Ammo: Infinite\n";
465 } else {
466 stats += "Ammo: " + std::to_string(def.maxAmmo) + "\n";
467 }
468 if (def.fireRate == 0.0f && def.beamDuration > 0.0f) {
469 stats += "Fire Rate: Continuous\n";
470 } else if (def.fireRate >= 5.0f) {
471 stats += "Fire Rate: High\n";
472 } else if (def.fireRate >= 2.0f) {
473 stats += "Fire Rate: Medium\n";
474 } else {
475 stats += "Fire Rate: Slow\n";
476 }
477 std::string special;
478 if (def.beamDuration > 0.0f) {
479 special += "Beam: " + std::to_string(def.beamDuration) + "s active / " + std::to_string(def.beamCooldown) + "s cooldown";
480 }
481 // Reflect feature removed
482 if (def.homing) {
483 if (!special.empty()) special += " / ";
484 special += "Auto-homing shots";
485 }
486 if (def.isBoomerang) {
487 if (!special.empty()) special += " / ";
488 special += "Returns to player";
489 }
490 if (special.empty()) special = "Standard shots";
491 stats += "Special: " + special + "\n";
492 stats += "Difficulty: " + std::to_string(def.difficulty) + "/5";
493 } else {
494 log::warning("No weapon configurations found; using default stats display");
495 }
496
497 if (texts && texts.value().get().has(_weaponStatsText)) {
498 auto& text = texts.value().get()[_weaponStatsText];
499 text.content = stats;
500 }
501 }
502
503 } // namespace scenes
504} // namespace rtp::client
void kill(ecs::Entity entity)
auto spawn(const EntityTemplate &t) -> std::expected< ecs::Entity, Error >
System to handle network-related operations on the client side.
void tryStartSolo(void)
Start a solo game by creating a private room and auto-joining/readying.
void requestListRooms(void)
Send a request to have the list of available rooms from the server.
void sendSelectedWeapon(uint8_t weaponKind) const
Send currently selected weapon to server to apply immediately.
Manages game settings and preferences.
Definition Settings.hpp:67
std::string getWeaponName(ecs::components::WeaponKind weapon) const
Definition Settings.cpp:316
ecs::components::WeaponKind getSelectedWeapon() const
Definition Settings.hpp:162
void setSelectedWeapon(ecs::components::WeaponKind weapon)
Definition Settings.hpp:167
bool save(const std::string &filename="config/settings.cfg")
Definition Settings.cpp:339
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 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
ecs::Entity _weaponNameText
Entity for weapon name display.
std::vector< MenuEnemy > _menuEnemies
Moving enemies for menu decor.
float _menuTime
Menu animation timer.
NetworkSyncSystem & _network
Reference to the client network.
Definition MenuScene.hpp:96
MenuScene(ecs::Registry &UiRegistry, ecs::Registry &worldRegistry, Settings &settings, TranslationManager &translationManager, NetworkSyncSystem &network, graphics::UiFactory &uiFactory, EntityBuilder &worldEntityBuilder, std::function< void(GameState)> changeState)
Constructor for MenuScene.
Definition MenuScene.cpp:25
ChangeStateFn _changeState
Function to change the game state.
Definition MenuScene.hpp:99
TranslationManager & _translationManager
Reference to the translation manager.
Definition MenuScene.hpp:95
EntityBuilder & _worldEntityBuilder
World entity builder for menu visuals.
Definition MenuScene.hpp:98
graphics::UiFactory & _uiFactory
UI Factory for creating UI components.
Definition MenuScene.hpp:97
Settings & _settings
Reference to the application settings.
Definition MenuScene.hpp:94
ecs::Registry & _worldRegistry
Reference to the world ECS registry.
Definition MenuScene.hpp:93
std::vector< ecs::Entity > _menuWorldEntities
Menu background entities.
void onEnter(void) override
Called when the scene is entered.
Definition MenuScene.cpp:45
void handleEvent(const sf::Event &event) override
Handle an incoming event.
ecs::Entity _weaponStatsText
Entity for weapon stats display.
void onExit(void) override
Called when the scene is exited.
ecs::Registry & _uiRegistry
Reference to the ECS registry.
Definition MenuScene.hpp:92
ecs::Entity _menuMusicEntity
Entity for menu background music.
void updateWeaponDisplay()
Update weapon name and stats texts.
void update(float dt) override
Update the scene state.
Represents an entity in the ECS (Entity-Component-System) architecture.
Definition Entity.hpp:63
auto spawn(void) -> std::expected< Entity, rtp::Error >
Definition Registry.cpp:51
auto add(Entity entity, Args &&...args) -> std::expected< std::reference_wrapper< T >, rtp::Error >
auto get(this const Self &self) -> std::expected< std::reference_wrapper< ConstLike< Self, SparseArray< T > > >, rtp::Error >
R-Type client namespace.
@ RoomWaiting
Room waiting state.
@ ModMenu
Mod menu state.
@ Settings
Settings menu state.
std::string getWeaponDisplayName(rtp::ecs::components::WeaponKind kind)
bool hasWeaponConfigs()
const rtp::ecs::components::SimpleWeapon & getWeaponDef(rtp::ecs::components::WeaponKind kind)
void reloadWeaponConfigs()
@ Tracker
Weak auto-homing shots.
@ Boomerang
Single projectile that returns.
@ Classic
Default spam/charge laser.
@ Beam
Continuous beam 5s active, 5s cooldown.
void warning(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log a warning message.
void info(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an informational message.
Vec2f position
Spawn position.
static EntityTemplate createParrallaxLvl1_2()
static EntityTemplate createParrallaxLvl1_1()
static EntityTemplate enemy_2(const Vec2f &initialPos)
static EntityTemplate enemy_1(const Vec2f &initialPos)
ecs::components::ParallaxLayer parallax
Parallax layer data.
Represents the size of a UI component.
Definition UiFactory.hpp:55
float textureWidth
Width of the texture used for parallax scrolling.
Component representing position, rotation, and scale of an entity.
Definition Transform.hpp:23
Vec2f position
X and Y coordinates.
Definition Transform.hpp:24
Component for entities that emit continuous sound (music, loops, ambient)
std::string audioPath
Path to the audio file.
bool isPlaying
Current playback state.
bool dirty
Flag to indicate changes need to be applied.
float volume
Volume level (0.0 - 1.0)
bool loop
Whether the audio should loop.
Component representing a clickable button.
Definition Button.hpp:30
uint8_t idleColor[3]
RGB color when idle.
Definition Button.hpp:38
uint8_t pressedColor[3]
RGB color when pressed.
Definition Button.hpp:40
uint8_t hoverColor[3]
RGB color when hovered.
Definition Button.hpp:39
Component representing text to render.
Definition Text.hpp:19