#pragma once #include "Draw.hpp" #include "Entities.hpp" #include "Systems.hpp" #include "raylib.h" #include #include #include class SceneManager; class Scene { public: explicit Scene(SceneManager &owner) : manager(owner) {} virtual ~Scene() = default; virtual void Enter() {} virtual void Exit() {} virtual void Update(float dt) = 0; virtual void Draw() = 0; protected: SceneManager &manager; }; /** * Manages the current active scene, using an object oriented state machine approach. */ class SceneManager { public: /** * @brief Changes the current scene, invoking `Exit` on the old scene and `Enter` * on the new scene. The new scene is constructed in-place with the provided arguments. * * @tparam T The type of the new scene, must derive from `Scene`. * @param args Arguments forwarded to the constructor of the new scene. */ template void ChangeScene(Args &&...args); /** * @brief Changes to the provided next scene, invoking `Exit` on the old scene and * `Enter` on the new scene. * * @param nextScene The new scene to switch to. */ void ChangeScene(std::unique_ptr nextScene); void Update(float dt); void Draw(); private: // use unique_ptr to enforce single ownership and ensure proper cleanup on // scene change std::unique_ptr current; }; class StartMenuScene; class GameplayScene; class DeathScene; class StartMenuScene : public Scene { public: explicit StartMenuScene(SceneManager &owner) : Scene(owner) {} void Update(float dt) override; void Draw() override; }; class GameplayScene : public Scene { public: explicit GameplayScene(SceneManager &owner) : Scene(owner) {} void Enter() override; void Update(float dt) override; void Draw() override; private: std::vector> entities; GameContext context; bool wantsDeathScene = false; int collectedCount = 0; float meterValue = 60.0f; }; class DeathScene : public Scene { public: explicit DeathScene(SceneManager &owner) : Scene(owner) {} void Update(float dt) override; void Draw() override; }; inline void SceneManager::ChangeScene(std::unique_ptr nextScene) { if (current) { current->Exit(); } current = std::move(nextScene); if (current) { current->Enter(); } } inline void SceneManager::Update(float dt) { if (current) { current->Update(dt); } } inline void SceneManager::Draw() { if (current) { current->Draw(); } } inline void StartMenuScene::Update(float) { if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { manager.ChangeScene(); } } inline void StartMenuScene::Draw() { DrawSceneOutline(); DrawText("Gravity Surfing", 24, 28, 36, Color{230, 238, 255, 255}); DrawText("Click or press Enter to start", 24, 78, 22, Color{140, 198, 220, 255}); DrawText("Left mouse controls gravity well", 24, 130, 18, Color{170, 185, 205, 255}); DrawText("Collect stars and dodge hazards", 24, 156, 18, Color{170, 185, 205, 255}); } inline void GameplayScene::Enter() { entities.clear(); entities.reserve(20); context = {}; wantsDeathScene = false; collectedCount = 0; meterValue = 60.0f; context.onPlayerDeath = [this]() { wantsDeathScene = true; }; context.AddCollectiblePickedListener([this](Entity &) { ++collectedCount; }); context.AddMeterChangedListener([this](float, float newValue) { meterValue = newValue; }); entities.push_back(CreateWorld()); entities.push_back(CreateGravityWell()); entities.push_back(CreateProbe()); entities.push_back(CreateStar(900.0f, 120.0f)); entities.push_back(CreateAsteroid(1100.0f, 330.0f)); entities.push_back(CreateNullZone(1280.0f, 70.0f)); entities.push_back(CreateHUD()); if (auto meter = entities.back()->GetComponent()) { meterValue = meter->get().value; } } inline void GameplayScene::Update(float dt) { UpdateAllSystems(entities, context, dt); if (wantsDeathScene) { manager.ChangeScene(); } } inline void GameplayScene::Draw() { DrawSceneOutline(); DrawText("Gravity Surfing", 14, 12, 20, Color{230, 238, 255, 255}); auto worldScroll = entities[0]->GetComponent(); auto probeTransform = entities[2]->GetComponent(); if (worldScroll) { DrawText(TextFormat("scrollX: %.2f", worldScroll->get().scrollX), 14, 40, 16, Color{125, 225, 205, 255}); } if (probeTransform) { const auto &probe = probeTransform->get(); DrawText(TextFormat("probe: (%.1f, %.1f)", probe.x, probe.y), 14, 60, 16, Color{235, 215, 125, 255}); } DrawText(TextFormat("meter: %.1f", meterValue), 14, 80, 16, Color{120, 210, 190, 255}); DrawText(TextFormat("stars: %i", collectedCount), 14, 100, 16, Color{255, 223, 86, 255}); for (auto &entity : entities) { if (!entity) { continue; } auto collectible = entity->GetComponent(); if (collectible && collectible->get().collected) { continue; } auto render = entity->GetComponent(); if (render) { render->get().Draw(); } } } inline void DeathScene::Update(float) { if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { manager.ChangeScene(); return; } if (IsKeyPressed(KEY_ESCAPE)) { manager.ChangeScene(); } } inline void DeathScene::Draw() { DrawSceneOutline(); DrawText("PROBE LOST", 24, 28, 36, Color{255, 208, 208, 255}); DrawText("Press Enter or click to restart", 24, 84, 22, Color{255, 170, 170, 255}); DrawText("Press Esc for menu", 24, 116, 20, Color{200, 188, 188, 255}); } template inline void SceneManager::ChangeScene(Args &&...args) { ChangeScene(std::make_unique(*this, std::forward(args)...)); }