From 4911bfe8d5ee35133bfb33083a4cd1a6275d6f99 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Sun, 15 Mar 2026 21:26:32 -0700 Subject: [PATCH] Refactor components and scenes into header/impl files --- as6/CMakeLists.txt | 28 +- as6/Component.hpp | 23 ++ as6/Components.hpp | 417 +------------------- as6/Draw.cpp | 5 + as6/Draw.hpp | 5 +- as6/Entities.cpp | 187 +++++++++ as6/Entities.hpp | 191 +-------- as6/Entity.hpp | 22 +- as6/SceneManager.hpp | 267 +------------ as6/Systems.cpp | 26 ++ as6/Systems.hpp | 25 +- as6/components/CollectibleComponent.cpp | 39 ++ as6/components/CollectibleComponent.hpp | 12 + as6/components/ColliderComponent.cpp | 48 +++ as6/components/ColliderComponent.hpp | 54 +++ as6/components/GravityReceiverComponent.cpp | 95 +++++ as6/components/GravityReceiverComponent.hpp | 23 ++ as6/components/GravityWellComponent.cpp | 27 ++ as6/components/GravityWellComponent.hpp | 34 ++ as6/components/HazardComponent.cpp | 27 ++ as6/components/HazardComponent.hpp | 12 + as6/components/HudComponent.cpp | 5 + as6/components/HudComponent.hpp | 12 + as6/components/LifetimeComponent.cpp | 14 + as6/components/LifetimeComponent.hpp | 17 + as6/components/MeterComponent.cpp | 21 + as6/components/MeterComponent.hpp | 22 ++ as6/components/NullZoneComponent.cpp | 5 + as6/components/NullZoneComponent.hpp | 14 + as6/components/PhysicsComponent.cpp | 27 ++ as6/components/PhysicsComponent.hpp | 16 + as6/components/ProbeStateComponent.cpp | 29 ++ as6/components/ProbeStateComponent.hpp | 17 + as6/components/ProjectionComponent.cpp | 5 + as6/components/ProjectionComponent.hpp | 12 + as6/components/RenderComponent.cpp | 11 + as6/components/RenderComponent.hpp | 22 ++ as6/components/ScrollComponent.cpp | 13 + as6/components/ScrollComponent.hpp | 16 + as6/components/ScrollableComponent.cpp | 21 + as6/components/ScrollableComponent.hpp | 14 + as6/components/SpawnComponent.cpp | 5 + as6/components/SpawnComponent.hpp | 14 + as6/components/TrailComponent.cpp | 5 + as6/components/TrailComponent.hpp | 12 + as6/components/TransformComponent.cpp | 5 + as6/components/TransformComponent.hpp | 15 + as6/scene/DeathScene.cpp | 23 ++ as6/scene/DeathScene.hpp | 19 + as6/scene/GameplayScene.cpp | 100 +++++ as6/scene/GameplayScene.hpp | 33 ++ as6/scene/Scene.hpp | 20 + as6/scene/SceneManager.hpp | 64 +++ as6/scene/StartMenuScene.cpp | 18 + as6/scene/StartMenuScene.hpp | 18 + 55 files changed, 1338 insertions(+), 893 deletions(-) create mode 100644 as6/Component.hpp create mode 100644 as6/Draw.cpp create mode 100644 as6/Entities.cpp create mode 100644 as6/Systems.cpp create mode 100644 as6/components/CollectibleComponent.cpp create mode 100644 as6/components/CollectibleComponent.hpp create mode 100644 as6/components/ColliderComponent.cpp create mode 100644 as6/components/ColliderComponent.hpp create mode 100644 as6/components/GravityReceiverComponent.cpp create mode 100644 as6/components/GravityReceiverComponent.hpp create mode 100644 as6/components/GravityWellComponent.cpp create mode 100644 as6/components/GravityWellComponent.hpp create mode 100644 as6/components/HazardComponent.cpp create mode 100644 as6/components/HazardComponent.hpp create mode 100644 as6/components/HudComponent.cpp create mode 100644 as6/components/HudComponent.hpp create mode 100644 as6/components/LifetimeComponent.cpp create mode 100644 as6/components/LifetimeComponent.hpp create mode 100644 as6/components/MeterComponent.cpp create mode 100644 as6/components/MeterComponent.hpp create mode 100644 as6/components/NullZoneComponent.cpp create mode 100644 as6/components/NullZoneComponent.hpp create mode 100644 as6/components/PhysicsComponent.cpp create mode 100644 as6/components/PhysicsComponent.hpp create mode 100644 as6/components/ProbeStateComponent.cpp create mode 100644 as6/components/ProbeStateComponent.hpp create mode 100644 as6/components/ProjectionComponent.cpp create mode 100644 as6/components/ProjectionComponent.hpp create mode 100644 as6/components/RenderComponent.cpp create mode 100644 as6/components/RenderComponent.hpp create mode 100644 as6/components/ScrollComponent.cpp create mode 100644 as6/components/ScrollComponent.hpp create mode 100644 as6/components/ScrollableComponent.cpp create mode 100644 as6/components/ScrollableComponent.hpp create mode 100644 as6/components/SpawnComponent.cpp create mode 100644 as6/components/SpawnComponent.hpp create mode 100644 as6/components/TrailComponent.cpp create mode 100644 as6/components/TrailComponent.hpp create mode 100644 as6/components/TransformComponent.cpp create mode 100644 as6/components/TransformComponent.hpp create mode 100644 as6/scene/DeathScene.cpp create mode 100644 as6/scene/DeathScene.hpp create mode 100644 as6/scene/GameplayScene.cpp create mode 100644 as6/scene/GameplayScene.hpp create mode 100644 as6/scene/Scene.hpp create mode 100644 as6/scene/SceneManager.hpp create mode 100644 as6/scene/StartMenuScene.cpp create mode 100644 as6/scene/StartMenuScene.hpp diff --git a/as6/CMakeLists.txt b/as6/CMakeLists.txt index 1192d68..9b0e997 100644 --- a/as6/CMakeLists.txt +++ b/as6/CMakeLists.txt @@ -12,7 +12,33 @@ add_subdirectory(../raylib-cpp raylib) include(../assets/includeable.cmake) -add_executable(as6-gravity-surfing main.cpp) +add_executable(as6-gravity-surfing + main.cpp + Draw.cpp + Entities.cpp + Systems.cpp + components/TransformComponent.cpp + components/PhysicsComponent.cpp + components/GravityWellComponent.cpp + components/ScrollComponent.cpp + components/MeterComponent.cpp + components/NullZoneComponent.cpp + components/ColliderComponent.cpp + components/ScrollableComponent.cpp + components/SpawnComponent.cpp + components/TrailComponent.cpp + components/ProjectionComponent.cpp + components/LifetimeComponent.cpp + components/CollectibleComponent.cpp + components/HazardComponent.cpp + components/GravityReceiverComponent.cpp + components/ProbeStateComponent.cpp + components/HudComponent.cpp + components/RenderComponent.cpp + scene/StartMenuScene.cpp + scene/GameplayScene.cpp + scene/DeathScene.cpp +) target_link_libraries(as6-gravity-surfing PUBLIC raylib raylib_cpp buffered-raylib) target_include_directories(as6-gravity-surfing PRIVATE diff --git a/as6/Component.hpp b/as6/Component.hpp new file mode 100644 index 0000000..bac925f --- /dev/null +++ b/as6/Component.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "GameContext.hpp" + +struct Entity; + +/** + * Base interface that every concrete gameplay component derives from. + * + * Each component is attached to one entity and receives the shared scene + * `GameContext` pointer so it can read global gameplay state. + */ +class Component { + public: + Entity *entity = nullptr; + GameContext *context = nullptr; + + virtual void Setup() = 0; + virtual void Update(float dt) = 0; + virtual void Cleanup() = 0; + + virtual ~Component() = default; +}; diff --git a/as6/Components.hpp b/as6/Components.hpp index 7d06bec..1232e6a 100644 --- a/as6/Components.hpp +++ b/as6/Components.hpp @@ -1,401 +1,20 @@ #pragma once -#include "Entity.hpp" -#include "GameContext.hpp" - -#include "raylib.h" - -#include -#include -#include -#include - -struct TransformComponent : public Component { - float x = 0.0f; - float y = 0.0f; - void Setup() override {} - void Update(float) override {} - void Cleanup() override {} -}; - -struct PhysicsComponent : public Component { - float vx = 0.0f; - float vy = 0.0f; - float speedCap = 400.0f; - void Setup() override {} - void Update(float dt) override { - auto transform = entity->GetComponent(); - if (!transform) { - return; - } - - const float speed = std::sqrt(vx * vx + vy * vy); - if (speed > speedCap && speed > 0.0f) { - const float scale = speedCap / speed; - vx *= scale; - vy *= scale; - } - - transform->get().x += vx * dt; - transform->get().y += vy * dt; - } - void Cleanup() override {} -}; - -struct GravityWellComponent : public Component { - float mass = 150000.0f; - float minDist = 30.0f; - bool active = false; - float followLerp = 12.0f; - void Setup() override {} - void Update(float dt) override { - auto transform = entity->GetComponent(); - if (!transform) { - return; - } - - active = IsMouseButtonDown(MOUSE_BUTTON_LEFT); - - const Vector2 mouse = GetMousePosition(); - auto &t = transform->get(); - const float blend = std::clamp(followLerp * dt, 0.0f, 1.0f); - t.x += (mouse.x - t.x) * blend; - t.y += (mouse.y - t.y) * blend; - } - void Cleanup() override {} -}; - -struct ScrollComponent : public Component { - float scrollX = 0.0f; - float speed = 2.0f; - float accel = 0.018f; - void Setup() override {} - void Update(float dt) override { - speed += accel * dt; - scrollX += speed * dt * 60.0f; - if (context) { - context->scrollX = scrollX; - } - } - void Cleanup() override {} -}; - -struct MeterComponent : public Component { - float value = 60.0f; - float maxValue = 100.0f; - float drainRate = 14.0f; - float gainPerStar = 28.0f; - void Setup() override {} - - void SetValue(float newValue) { - const float oldValue = value; - value = newValue; - if (context && oldValue != value) { - context->EmitMeterChanged(oldValue, value); - } - } - - void AddValue(float delta) { SetValue(std::clamp(value + delta, 0.0f, maxValue)); } - - void Drain(float amount) { AddValue(-amount); } - - void Update(float) override {} - void Cleanup() override {} -}; - -struct NullZoneComponent : public Component { - float width = 70.0f; - void Setup() override {} - void Update(float) override {} - void Cleanup() override {} -}; - -struct ColliderComponent : public Component { - float radius = 8.0f; - bool monitoring = false; - bool monitorable = true; - std::vector> onCollision; - void Setup() override {} - - void EmitCollision(Entity &other) { - for (auto &callback : onCollision) { - callback(other); - } - } - - void AddCollisionListener(std::function callback) { - onCollision.push_back(std::move(callback)); - } - - void Update(float) override { - if (!monitoring || !context || !context->entities || entity->queuedForFree) { - return; - } - - auto selfTransform = entity->GetComponent(); - if (!selfTransform) { - return; - } - - for (auto &other : *context->entities) { - if (!other || other.get() == entity || other->queuedForFree) { - continue; - } - - auto otherCollider = other->GetComponent(); - auto otherTransform = other->GetComponent(); - if (!otherCollider || !otherTransform || !otherCollider->get().monitorable) { - continue; - } - - const float dx = selfTransform->get().x - otherTransform->get().x; - const float dy = selfTransform->get().y - otherTransform->get().y; - const float r = radius + otherCollider->get().radius; - if ((dx * dx + dy * dy) <= (r * r)) { - EmitCollision(*other); - } - } - } - void Cleanup() override {} -}; - -struct ScrollableComponent : public Component { - float worldX = 0.0f; - void Setup() override {} - void Update(float) override { - if (!context) { - return; - } - - auto transform = entity->GetComponent(); - if (!transform) { - return; - } - - transform->get().x = worldX - context->scrollX; - } - void Cleanup() override {} -}; - -struct SpawnComponent : public Component { - float cursorWX = 0.0f; - void Setup() override {} - void Update(float) override {} - void Cleanup() override {} -}; - -struct TrailComponent : public Component { - void Setup() override {} - void Update(float) override {} - void Cleanup() override {} -}; - -struct ProjectionComponent : public Component { - void Setup() override {} - void Update(float) override {} - void Cleanup() override {} -}; - -struct LifetimeComponent : public Component { - float remaining = 0.0f; - void Setup() override {} - void Update(float dt) override { - remaining -= dt; - if (remaining <= 0.0f) { - entity->QueueFree(); - } - } - void Cleanup() override {} -}; - -struct CollectibleComponent : public Component { - void Setup() override { - auto selfCollider = entity->GetComponent(); - if (!selfCollider) { - return; - } - - selfCollider->get().monitoring = true; - - selfCollider->get().AddCollisionListener([this](Entity &other) { - if (entity->queuedForFree || !context || !context->probeEntity || - &other != context->probeEntity) { - return; - } - - context->EmitCollectiblePicked(*entity); - entity->QueueFree(); - - if (!context->hudEntity) { - return; - } - - auto meter = context->hudEntity->GetComponent(); - if (!meter) { - return; - } - - meter->get().AddValue(meter->get().gainPerStar); - }); - } - - void Update(float) override {} - - void Cleanup() override {} -}; - -struct HazardComponent : public Component { - void Setup() override { - auto selfCollider = entity->GetComponent(); - if (!selfCollider) { - return; - } - - selfCollider->get().monitoring = true; - - selfCollider->get().AddCollisionListener([this](Entity &other) { - if (!context || !context->probeEntity || &other != context->probeEntity) { - return; - } - - if (context->onPlayerDeath) { - context->onPlayerDeath(); - } - }); - } - void Update(float) override {} - void Cleanup() override {} -}; - -struct GravityReceiverComponent : public Component { - Entity *well = nullptr; - bool inVoid = false; - void Setup() override {} - void Update(float dt) override { - if (!context || !context->probeEntity || entity != context->probeEntity) { - return; - } - - if (!well && context->wellEntity) { - well = context->wellEntity; - } - if (!well) { - return; - } - - auto myTransform = entity->GetComponent(); - auto physics = entity->GetComponent(); - auto wellTransform = well->GetComponent(); - auto wellGravity = well->GetComponent(); - if (!myTransform || !physics || !wellTransform || !wellGravity) { - return; - } - - MeterComponent *meterPtr = nullptr; - if (context->hudEntity) { - auto meter = context->hudEntity->GetComponent(); - if (meter) { - meterPtr = &meter->get(); - } - } - - if (!wellGravity->get().active) { - return; - } - if (meterPtr && meterPtr->value <= 0.0f) { - return; - } - - inVoid = false; - if (context->entities) { - for (auto &other : *context->entities) { - if (!other || other.get() == entity) { - continue; - } - - auto zone = other->GetComponent(); - auto zoneTransform = other->GetComponent(); - if (!zone || !zoneTransform) { - continue; - } - - const float left = zoneTransform->get().x; - const float right = left + zone->get().width; - if (myTransform->get().x >= left && myTransform->get().x <= right) { - inVoid = true; - break; - } - } - } - if (inVoid) { - return; - } - - const float dx = wellTransform->get().x - myTransform->get().x; - const float dy = wellTransform->get().y - myTransform->get().y; - const float dist = std::sqrt(dx * dx + dy * dy); - if (dist <= 0.0001f) { - return; - } - - const float clampedDist = std::max(dist, wellGravity->get().minDist); - const float force = wellGravity->get().mass / (clampedDist * clampedDist); - const float nx = dx / dist; - const float ny = dy / dist; - - physics->get().vx += nx * force * dt; - physics->get().vy += ny * force * dt; - - if (meterPtr) { - meterPtr->Drain(meterPtr->drainRate * dt); - } - } - void Cleanup() override {} -}; - -struct ProbeStateComponent : public Component { - float spawnX = 96.0f; - float spawnY = 230.0f; - float spawnVx = 165.0f; - float spawnVy = 0.0f; - void Setup() override {} - void Update(float) override { - if (!context || !context->probeEntity || entity != context->probeEntity) { - return; - } - - auto transform = entity->GetComponent(); - if (!transform) { - return; - } - - if (transform->get().y < -20.0f || - transform->get().y > static_cast(GetScreenHeight() + 20) || - transform->get().x < -20.0f) { - if (context->onPlayerDeath) { - context->onPlayerDeath(); - } - } - } - void Cleanup() override {} -}; - -struct HudComponent : public Component { - void Setup() override {} - void Update(float) override {} - void Cleanup() override {} -}; - -struct RenderComponent : public Component { - std::function draw; - - virtual void Draw() { - if (draw) { - draw(); - } - } - - void Setup() override {} - void Update(float) override {} - void Cleanup() override {} -}; +#include "components/CollectibleComponent.hpp" +#include "components/ColliderComponent.hpp" +#include "components/GravityReceiverComponent.hpp" +#include "components/GravityWellComponent.hpp" +#include "components/HazardComponent.hpp" +#include "components/HudComponent.hpp" +#include "components/LifetimeComponent.hpp" +#include "components/MeterComponent.hpp" +#include "components/NullZoneComponent.hpp" +#include "components/PhysicsComponent.hpp" +#include "components/ProbeStateComponent.hpp" +#include "components/ProjectionComponent.hpp" +#include "components/RenderComponent.hpp" +#include "components/ScrollComponent.hpp" +#include "components/ScrollableComponent.hpp" +#include "components/SpawnComponent.hpp" +#include "components/TrailComponent.hpp" +#include "components/TransformComponent.hpp" diff --git a/as6/Draw.cpp b/as6/Draw.cpp new file mode 100644 index 0000000..4294562 --- /dev/null +++ b/as6/Draw.cpp @@ -0,0 +1,5 @@ +#include "Draw.hpp" + +void DrawSceneOutline() { + DrawRectangleLines(0, 0, GetScreenWidth(), GetScreenHeight(), raylib::Color::DarkGray()); +} diff --git a/as6/Draw.hpp b/as6/Draw.hpp index bc24d9d..85d4707 100644 --- a/as6/Draw.hpp +++ b/as6/Draw.hpp @@ -2,7 +2,4 @@ #include "raylib-cpp.hpp" -// placeholder for draw helpers -void DrawSceneOutline() { - DrawRectangleLines(0, 0, GetScreenWidth(), GetScreenHeight(), raylib::Color::DarkGray()); -} +void DrawSceneOutline(); diff --git a/as6/Entities.cpp b/as6/Entities.cpp new file mode 100644 index 0000000..fbfecc3 --- /dev/null +++ b/as6/Entities.cpp @@ -0,0 +1,187 @@ +#include "Entities.hpp" + +#include "Components.hpp" + +#include "raylib.h" + +std::shared_ptr CreateProbe() { + auto e = std::make_shared(); + auto &transform = e->AddComponent(); + transform.x = 96.0f; + transform.y = 230.0f; + + e->AddComponent(); + + auto &physics = e->AddComponent(); + physics.vx = 108.0f; + physics.vy = 0.0f; + physics.speedCap = 8192.0f; + + auto &collider = e->AddComponent(); + collider.radius = 7.0f; + collider.monitorable = true; + + auto &probeState = e->AddComponent(); + probeState.spawnX = 96.0f; + probeState.spawnY = 230.0f; + probeState.spawnVx = 165.0f; + probeState.spawnVy = 0.0f; + + e->AddComponent(); + e->AddComponent(); + auto &render = e->AddComponent(); + render.draw = [e]() { + auto transform = e->GetComponent(); + if (!transform) { + return; + } + DrawCircleV({transform->get().x, transform->get().y}, 7.0f, Color{250, 244, 227, 255}); + }; + return e; +} + +std::shared_ptr CreateGravityWell() { + auto e = std::make_shared(); + auto &transform = e->AddComponent(); + transform.x = 96.0f; + transform.y = 230.0f; + + auto &well = e->AddComponent(); + well.mass = static_cast(1 << 22); + well.minDist = 28.0f; + well.followLerp = 12.0f; + + auto &render = e->AddComponent(); + render.draw = [e]() { + auto transform = e->GetComponent(); + if (!transform) { + return; + } + DrawCircleLines(static_cast(transform->get().x), static_cast(transform->get().y), + 18.0f, Color{86, 197, 255, 255}); + }; + return e; +} + +std::shared_ptr CreateStar(float x, float y) { + auto e = std::make_shared(); + auto &t = e->AddComponent(); + t.x = x; + t.y = y; + auto &scrollable = e->AddComponent(); + scrollable.worldX = x; + auto &collider = e->AddComponent(); + collider.radius = 6.0f; + + e->AddComponent(); + auto &render = e->AddComponent(); + render.draw = [e]() { + auto transform = e->GetComponent(); + if (!transform) { + return; + } + DrawCircleV({transform->get().x, transform->get().y}, 6.0f, Color{255, 223, 86, 255}); + }; + return e; +} + +std::shared_ptr CreateAsteroid(float x, float y) { + auto e = std::make_shared(); + auto &t = e->AddComponent(); + t.x = x; + t.y = y; + auto &scrollable = e->AddComponent(); + scrollable.worldX = x; + auto &collider = e->AddComponent(); + collider.radius = 13.0f; + + e->AddComponent(); + + auto &render = e->AddComponent(); + render.draw = [e]() { + auto transform = e->GetComponent(); + if (!transform) { + return; + } + DrawCircleV({transform->get().x, transform->get().y}, 13.0f, Color{116, 126, 142, 255}); + }; + return e; +} + +std::shared_ptr CreateDebris(float x, float y, float vx, float vy, float ttl) { + auto e = std::make_shared(); + auto &t = e->AddComponent(); + t.x = x; + t.y = y; + + auto &physics = e->AddComponent(); + physics.vx = vx; + physics.vy = vy; + physics.speedCap = 260.0f; + + auto &lifetime = e->AddComponent(); + lifetime.remaining = ttl; + + auto &render = e->AddComponent(); + render.draw = [e]() { + auto transform = e->GetComponent(); + if (!transform) { + return; + } + DrawCircleV({transform->get().x, transform->get().y}, 3.0f, Color{245, 196, 104, 220}); + }; + + return e; +} + +std::shared_ptr CreateNullZone(float x, float width) { + auto e = std::make_shared(); + auto &t = e->AddComponent(); + t.x = x; + auto &scrollable = e->AddComponent(); + scrollable.worldX = x; + + auto &nullZone = e->AddComponent(); + nullZone.width = width; + auto &render = e->AddComponent(); + render.draw = [e]() { + auto transform = e->GetComponent(); + auto zone = e->GetComponent(); + if (!transform || !zone) { + return; + } + + DrawRectangle(static_cast(transform->get().x), 0, static_cast(zone->get().width), + GetScreenHeight(), Color{96, 64, 146, 80}); + }; + return e; +} + +std::shared_ptr CreateWorld() { + auto e = std::make_shared(); + e->AddComponent(); + e->AddComponent(); + return e; +} + +std::shared_ptr CreateHUD() { + auto e = std::make_shared(); + e->AddComponent(); + e->AddComponent(); + auto &render = e->AddComponent(); + render.draw = [e]() { + auto meter = e->GetComponent(); + if (!meter) { + return; + } + + const float t = std::clamp(meter->get().value / 100.0f, 0.0f, 1.0f); + const int x = 14; + const int y = 86; + const int w = 240; + const int h = 14; + DrawRectangleLines(x, y, w, h, Color{125, 140, 165, 255}); + DrawRectangle(x + 1, y + 1, static_cast((w - 2) * t), h - 2, Color{60, 199, 178, 255}); + }; + return e; +} diff --git a/as6/Entities.hpp b/as6/Entities.hpp index 2916d38..ddb3f2b 100644 --- a/as6/Entities.hpp +++ b/as6/Entities.hpp @@ -1,187 +1,14 @@ #pragma once -#include "Components.hpp" #include "Entity.hpp" + #include -std::shared_ptr CreateProbe() { - auto e = std::make_shared(); - auto &transform = e->AddComponent(); - transform.x = 96.0f; - transform.y = 230.0f; - - e->AddComponent(); - - auto &physics = e->AddComponent(); - physics.vx = 108.0f; - physics.vy = 0.0f; - physics.speedCap = 8192.0f; - - auto &collider = e->AddComponent(); - collider.radius = 7.0f; - collider.monitorable = true; - - auto &probeState = e->AddComponent(); - probeState.spawnX = 96.0f; - probeState.spawnY = 230.0f; - probeState.spawnVx = 165.0f; - probeState.spawnVy = 0.0f; - - e->AddComponent(); - e->AddComponent(); - auto &render = e->AddComponent(); - render.draw = [e]() { - auto transform = e->GetComponent(); - if (!transform) { - return; - } - DrawCircleV({transform->get().x, transform->get().y}, 7.0f, Color{250, 244, 227, 255}); - }; - return e; -} - -std::shared_ptr CreateGravityWell() { - auto e = std::make_shared(); - auto &transform = e->AddComponent(); - transform.x = 96.0f; - transform.y = 230.0f; - - auto &well = e->AddComponent(); - well.mass = (float)(1 << 22); - well.minDist = 28.0f; - well.followLerp = 12.0f; - - auto &render = e->AddComponent(); - render.draw = [e]() { - auto transform = e->GetComponent(); - if (!transform) { - return; - } - DrawCircleLines(static_cast(transform->get().x), static_cast(transform->get().y), - 18.0f, Color{86, 197, 255, 255}); - }; - return e; -} - -std::shared_ptr CreateStar(float x, float y) { - auto e = std::make_shared(); - auto &t = e->AddComponent(); - t.x = x; - t.y = y; - auto &scrollable = e->AddComponent(); - scrollable.worldX = x; - auto &collider = e->AddComponent(); - collider.radius = 6.0f; - - e->AddComponent(); - auto &render = e->AddComponent(); - render.draw = [e]() { - auto transform = e->GetComponent(); - if (!transform) { - return; - } - DrawCircleV({transform->get().x, transform->get().y}, 6.0f, Color{255, 223, 86, 255}); - }; - return e; -} - -std::shared_ptr CreateAsteroid(float x, float y) { - auto e = std::make_shared(); - auto &t = e->AddComponent(); - t.x = x; - t.y = y; - auto &scrollable = e->AddComponent(); - scrollable.worldX = x; - auto &collider = e->AddComponent(); - collider.radius = 13.0f; - - e->AddComponent(); - - auto &render = e->AddComponent(); - render.draw = [e]() { - auto transform = e->GetComponent(); - if (!transform) { - return; - } - DrawCircleV({transform->get().x, transform->get().y}, 13.0f, Color{116, 126, 142, 255}); - }; - return e; -} - -std::shared_ptr CreateDebris(float x, float y, float vx, float vy, float ttl = 1.25f) { - auto e = std::make_shared(); - auto &t = e->AddComponent(); - t.x = x; - t.y = y; - - auto &physics = e->AddComponent(); - physics.vx = vx; - physics.vy = vy; - physics.speedCap = 260.0f; - - auto &lifetime = e->AddComponent(); - lifetime.remaining = ttl; - - auto &render = e->AddComponent(); - render.draw = [e]() { - auto transform = e->GetComponent(); - if (!transform) { - return; - } - DrawCircleV({transform->get().x, transform->get().y}, 3.0f, Color{245, 196, 104, 220}); - }; - - return e; -} - -std::shared_ptr CreateNullZone(float x, float width) { - auto e = std::make_shared(); - auto &t = e->AddComponent(); - t.x = x; - auto &scrollable = e->AddComponent(); - scrollable.worldX = x; - - auto &nullZone = e->AddComponent(); - nullZone.width = width; - auto &render = e->AddComponent(); - render.draw = [e]() { - auto transform = e->GetComponent(); - auto zone = e->GetComponent(); - if (!transform || !zone) { - return; - } - - DrawRectangle(static_cast(transform->get().x), 0, static_cast(zone->get().width), - GetScreenHeight(), Color{96, 64, 146, 80}); - }; - return e; -} - -std::shared_ptr CreateWorld() { - auto e = std::make_shared(); - e->AddComponent(); - e->AddComponent(); - return e; -} - -std::shared_ptr CreateHUD() { - auto e = std::make_shared(); - e->AddComponent(); - e->AddComponent(); - auto &render = e->AddComponent(); - render.draw = [e]() { - auto meter = e->GetComponent(); - if (!meter) { - return; - } - - const float t = std::clamp(meter->get().value / 100.0f, 0.0f, 1.0f); - const int x = 14; - const int y = 86; - const int w = 240; - const int h = 14; - DrawRectangleLines(x, y, w, h, Color{125, 140, 165, 255}); - DrawRectangle(x + 1, y + 1, static_cast((w - 2) * t), h - 2, Color{60, 199, 178, 255}); - }; - return e; -} +std::shared_ptr CreateProbe(); +std::shared_ptr CreateGravityWell(); +std::shared_ptr CreateStar(float x, float y); +std::shared_ptr CreateAsteroid(float x, float y); +std::shared_ptr CreateDebris(float x, float y, float vx, float vy, float ttl = 1.25f); +std::shared_ptr CreateNullZone(float x, float width); +std::shared_ptr CreateWorld(); +std::shared_ptr CreateHUD(); diff --git a/as6/Entity.hpp b/as6/Entity.hpp index 5aca883..76fd4f1 100644 --- a/as6/Entity.hpp +++ b/as6/Entity.hpp @@ -1,6 +1,6 @@ #pragma once -#include "GameContext.hpp" +#include "Component.hpp" #include #include @@ -9,26 +9,6 @@ #include struct Entity; -class Component; - -/** - * Base interface that every component derives from. Each component keeps a - * backref to its owning entity plus the shared game context so it can read - * global state (scroll offset, meter, entity pointers, reset flags, etc.). The - * lifecycle follows a `Setup` / `Update` / `Cleanup` pattern invoked by the - * owning entity. - */ -class Component { - public: - Entity *entity = nullptr; - GameContext *context = nullptr; - - virtual void Setup() = 0; - virtual void Update(float dt) = 0; - virtual void Cleanup() = 0; - - virtual ~Component() = default; -}; struct Entity { std::vector> components; diff --git a/as6/SceneManager.hpp b/as6/SceneManager.hpp index a683c1c..f1693f6 100644 --- a/as6/SceneManager.hpp +++ b/as6/SceneManager.hpp @@ -1,264 +1,7 @@ #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); - template void QueueSceneChange(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 QueueSceneChange(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; - std::unique_ptr queued; -}; - -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::QueueSceneChange(std::unique_ptr nextScene) { - queued = std::move(nextScene); -} - -inline void SceneManager::Update(float dt) { - if (queued) { - ChangeScene(std::move(queued)); - } - - if (current) { - current->Update(dt); - } - - if (queued) { - ChangeScene(std::move(queued)); - } -} - -inline void SceneManager::Draw() { - if (current) { - current->Draw(); - } -} - -inline void StartMenuScene::Update(float) { - if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { - manager.QueueSceneChange(); - } -} - -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 &collectible) { - ++collectedCount; - - auto transform = collectible.GetComponent(); - if (!transform || !context.entities) { - return; - } - - context.entities->push_back( - CreateDebris(transform->get().x - 4.0f, transform->get().y, -22.0f, -36.0f)); - context.entities->push_back( - CreateDebris(transform->get().x + 4.0f, transform->get().y, 24.0f, -28.0f)); - }); - 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; - } - - context.entities = &entities; - context.worldEntity = (entities.size() > 0 && entities[0]) ? entities[0].get() : nullptr; - context.wellEntity = (entities.size() > 1 && entities[1]) ? entities[1].get() : nullptr; - context.probeEntity = (entities.size() > 2 && entities[2]) ? entities[2].get() : nullptr; - context.hudEntity = (!entities.empty() && entities.back()) ? entities.back().get() : nullptr; - - for (auto &entity : entities) { - if (!entity) { - continue; - } - entity->SetContext(&context); - } -} - -inline void GameplayScene::Update(float dt) { - UpdateAllSystems(entities, dt); - - context.worldEntity = (entities.size() > 0 && entities[0]) ? entities[0].get() : nullptr; - context.wellEntity = (entities.size() > 1 && entities[1]) ? entities[1].get() : nullptr; - context.probeEntity = (entities.size() > 2 && entities[2]) ? entities[2].get() : nullptr; - context.hudEntity = (!entities.empty() && entities.back()) ? entities.back().get() : nullptr; - - if (wantsDeathScene) { - manager.QueueSceneChange(); - } -} - -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 || entity->queuedForFree) { - continue; - } - - auto render = entity->GetComponent(); - if (render) { - render->get().Draw(); - } - } -} - -inline void DeathScene::Update(float) { - if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { - manager.QueueSceneChange(); - return; - } - - if (IsKeyPressed(KEY_ESCAPE)) { - manager.QueueSceneChange(); - } -} - -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)...)); -} - -template inline void SceneManager::QueueSceneChange(Args &&...args) { - QueueSceneChange(std::make_unique(*this, std::forward(args)...)); -} +#include "scene/DeathScene.hpp" +#include "scene/GameplayScene.hpp" +#include "scene/Scene.hpp" +#include "scene/SceneManager.hpp" +#include "scene/StartMenuScene.hpp" diff --git a/as6/Systems.cpp b/as6/Systems.cpp new file mode 100644 index 0000000..920b76c --- /dev/null +++ b/as6/Systems.cpp @@ -0,0 +1,26 @@ +#include "Systems.hpp" + +void UpdateAllSystems(std::vector> &entities, float deltaTime) { + for (auto &entity : entities) { + if (!entity) { + continue; + } + + entity->Update(deltaTime); + } + + auto remover = [](const std::shared_ptr &entity) { + if (!entity) { + return true; + } + + if (entity->queuedForFree) { + entity->Cleanup(); + return true; + } + + return false; + }; + + entities.erase(std::remove_if(entities.begin(), entities.end(), remover), entities.end()); +} diff --git a/as6/Systems.hpp b/as6/Systems.hpp index 93e2145..96bdaec 100644 --- a/as6/Systems.hpp +++ b/as6/Systems.hpp @@ -6,27 +6,4 @@ #include #include -void UpdateAllSystems(std::vector> &entities, float deltaTime) { - for (auto &entity : entities) { - if (!entity) { - continue; - } - - entity->Update(deltaTime); - } - - auto remover = [](const std::shared_ptr &entity) { - if (!entity) { - return true; - } - - if (entity->queuedForFree) { - entity->Cleanup(); - return true; - } - - return false; - }; - - entities.erase(std::remove_if(entities.begin(), entities.end(), remover), entities.end()); -} +void UpdateAllSystems(std::vector> &entities, float deltaTime); diff --git a/as6/components/CollectibleComponent.cpp b/as6/components/CollectibleComponent.cpp new file mode 100644 index 0000000..5a9f4a8 --- /dev/null +++ b/as6/components/CollectibleComponent.cpp @@ -0,0 +1,39 @@ +#include "components/CollectibleComponent.hpp" + +#include "Entity.hpp" +#include "components/ColliderComponent.hpp" +#include "components/MeterComponent.hpp" + +void CollectibleComponent::Setup() { + auto selfCollider = entity->GetComponent(); + if (!selfCollider) { + return; + } + + selfCollider->get().monitoring = true; + + selfCollider->get().AddCollisionListener([this](Entity &other) { + if (entity->queuedForFree || !context || !context->probeEntity || + &other != context->probeEntity) { + return; + } + + context->EmitCollectiblePicked(*entity); + entity->QueueFree(); + + if (!context->hudEntity) { + return; + } + + auto meter = context->hudEntity->GetComponent(); + if (!meter) { + return; + } + + meter->get().AddValue(meter->get().gainPerStar); + }); +} + +void CollectibleComponent::Update(float) {} + +void CollectibleComponent::Cleanup() {} diff --git a/as6/components/CollectibleComponent.hpp b/as6/components/CollectibleComponent.hpp new file mode 100644 index 0000000..849fa06 --- /dev/null +++ b/as6/components/CollectibleComponent.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Component.hpp" + +/** + * Handles collectible pickup by probe and applies meter gain. + */ +struct CollectibleComponent : public Component { + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/ColliderComponent.cpp b/as6/components/ColliderComponent.cpp new file mode 100644 index 0000000..223be14 --- /dev/null +++ b/as6/components/ColliderComponent.cpp @@ -0,0 +1,48 @@ +#include "components/ColliderComponent.hpp" + +#include "Entity.hpp" +#include "components/TransformComponent.hpp" + +void ColliderComponent::Setup() {} + +void ColliderComponent::EmitCollision(Entity &other) { + for (auto &callback : onCollision) { + callback(other); + } +} + +void ColliderComponent::AddCollisionListener(std::function callback) { + onCollision.push_back(std::move(callback)); +} + +void ColliderComponent::Update(float) { + if (!monitoring || !context || !context->entities || entity->queuedForFree) { + return; + } + + auto selfTransform = entity->GetComponent(); + if (!selfTransform) { + return; + } + + for (auto &other : *context->entities) { + if (!other || other.get() == entity || other->queuedForFree) { + continue; + } + + auto otherCollider = other->GetComponent(); + auto otherTransform = other->GetComponent(); + if (!otherCollider || !otherTransform || !otherCollider->get().monitorable) { + continue; + } + + const float dx = selfTransform->get().x - otherTransform->get().x; + const float dy = selfTransform->get().y - otherTransform->get().y; + const float r = radius + otherCollider->get().radius; + if ((dx * dx + dy * dy) <= (r * r)) { + EmitCollision(*other); + } + } +} + +void ColliderComponent::Cleanup() {} diff --git a/as6/components/ColliderComponent.hpp b/as6/components/ColliderComponent.hpp new file mode 100644 index 0000000..789dd2d --- /dev/null +++ b/as6/components/ColliderComponent.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "Component.hpp" + +#include +#include + +/** + * Distance-based circle collider with event callbacks. + * + * `monitoring` enables active overlap checks, similar to Godot's Area2D monitoring. + * `monitorable` lets this collider be seen by monitors. + */ +struct ColliderComponent : public Component { + /** + * Radius of the circular collider, used for distance checks against other colliders. + */ + float radius = 8.0f; + + /** + * Whether this collider actively checks for overlaps and emits collision events. + */ + bool monitoring = false; + + /** + * Whether this collider can be detected by other colliders that have monitoring enabled. + */ + bool monitorable = true; + + /** + * Callbacks to invoke when a collision with another entity is detected. The callback + * receives the other entity involved in the collision. + */ + std::vector> onCollision; + + void Setup() override; + + /** + * Emits a collision event to all registered listeners with the given other entity. + * + * @param other The other entity that this collider has collided with. + */ + void EmitCollision(Entity &other); + + /** + * Registers a collision listener callback to be invoked when a collision is detected. + * + * @param callback The callback function that takes the other entity as an argument. + */ + void AddCollisionListener(std::function callback); + + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/GravityReceiverComponent.cpp b/as6/components/GravityReceiverComponent.cpp new file mode 100644 index 0000000..b7b751c --- /dev/null +++ b/as6/components/GravityReceiverComponent.cpp @@ -0,0 +1,95 @@ +#include "components/GravityReceiverComponent.hpp" + +#include "Entity.hpp" +#include "components/GravityWellComponent.hpp" +#include "components/MeterComponent.hpp" +#include "components/NullZoneComponent.hpp" +#include "components/PhysicsComponent.hpp" +#include "components/TransformComponent.hpp" + +#include +#include + +void GravityReceiverComponent::Setup() {} + +void GravityReceiverComponent::Update(float dt) { + if (!context || !context->probeEntity || entity != context->probeEntity) { + return; + } + + if (!well && context->wellEntity) { + well = context->wellEntity; + } + if (!well) { + return; + } + + auto myTransform = entity->GetComponent(); + auto physics = entity->GetComponent(); + auto wellTransform = well->GetComponent(); + auto wellGravity = well->GetComponent(); + if (!myTransform || !physics || !wellTransform || !wellGravity) { + return; + } + + MeterComponent *meterPtr = nullptr; + if (context->hudEntity) { + auto meter = context->hudEntity->GetComponent(); + if (meter) { + meterPtr = &meter->get(); + } + } + + if (!wellGravity->get().active) { + return; + } + if (meterPtr && meterPtr->value <= 0.0f) { + return; + } + + inVoid = false; + if (context->entities) { + for (auto &other : *context->entities) { + if (!other || other.get() == entity) { + continue; + } + + auto zone = other->GetComponent(); + auto zoneTransform = other->GetComponent(); + if (!zone || !zoneTransform) { + continue; + } + + const float left = zoneTransform->get().x; + const float right = left + zone->get().width; + if (myTransform->get().x >= left && myTransform->get().x <= right) { + inVoid = true; + break; + } + } + } + if (inVoid) { + return; + } + + const float dx = wellTransform->get().x - myTransform->get().x; + const float dy = wellTransform->get().y - myTransform->get().y; + const float dist = std::sqrt(dx * dx + dy * dy); + if (dist <= 0.0001f) { + return; + } + + const float clampedDist = std::max(dist, wellGravity->get().minDist); + const float force = wellGravity->get().mass / (clampedDist * clampedDist); + const float nx = dx / dist; + const float ny = dy / dist; + + physics->get().vx += nx * force * dt; + physics->get().vy += ny * force * dt; + + if (meterPtr) { + meterPtr->Drain(meterPtr->drainRate * dt); + } +} + +void GravityReceiverComponent::Cleanup() {} diff --git a/as6/components/GravityReceiverComponent.hpp b/as6/components/GravityReceiverComponent.hpp new file mode 100644 index 0000000..07d26b2 --- /dev/null +++ b/as6/components/GravityReceiverComponent.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "Component.hpp" + +/** + * Applies gravity well force to the probe and drains meter while active. + */ +struct GravityReceiverComponent : public Component { + /** + * The gravity well entity that this receiver is affected by. If null, the receiver will not + * apply any forces. + */ + Entity *well = nullptr; + + /** + * Whether the probe is inside a void zone, which disables the well's gravity. + */ + bool inVoid = false; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/GravityWellComponent.cpp b/as6/components/GravityWellComponent.cpp new file mode 100644 index 0000000..f7b2691 --- /dev/null +++ b/as6/components/GravityWellComponent.cpp @@ -0,0 +1,27 @@ +#include "components/GravityWellComponent.hpp" + +#include "Entity.hpp" +#include "components/TransformComponent.hpp" + +#include "raylib.h" + +#include + +void GravityWellComponent::Setup() {} + +void GravityWellComponent::Update(float dt) { + auto transform = entity->GetComponent(); + if (!transform) { + return; + } + + active = IsMouseButtonDown(MOUSE_BUTTON_LEFT); + + const Vector2 mouse = GetMousePosition(); + auto &t = transform->get(); + const float blend = std::clamp(followLerp * dt, 0.0f, 1.0f); + t.x += (mouse.x - t.x) * blend; + t.y += (mouse.y - t.y) * blend; +} + +void GravityWellComponent::Cleanup() {} diff --git a/as6/components/GravityWellComponent.hpp b/as6/components/GravityWellComponent.hpp new file mode 100644 index 0000000..c1746b1 --- /dev/null +++ b/as6/components/GravityWellComponent.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "Component.hpp" + +/** + * Represents player-controlled gravity source that follows mouse input. + */ +struct GravityWellComponent : public Component { + /** + * Mass of the gravity well, affecting the strength of the gravitational pull on receivers. + */ + float mass = 150000.0f; + + /** + * Minimum distance for gravity pull calculations to avoid singularities and excessive forces + * when very close to receivers. + */ + float minDist = 30.0f; + + /** + * Whether the gravity well is currently active and should apply gravitational forces to + * receivers (e.g. tied to mouse button state). + */ + bool active = false; + + /** + * Lerp factor for how quickly the gravity well follows the mouse position. + */ + float followLerp = 12.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/HazardComponent.cpp b/as6/components/HazardComponent.cpp new file mode 100644 index 0000000..7fa0314 --- /dev/null +++ b/as6/components/HazardComponent.cpp @@ -0,0 +1,27 @@ +#include "components/HazardComponent.hpp" + +#include "Entity.hpp" +#include "components/ColliderComponent.hpp" + +void HazardComponent::Setup() { + auto selfCollider = entity->GetComponent(); + if (!selfCollider) { + return; + } + + selfCollider->get().monitoring = true; + + selfCollider->get().AddCollisionListener([this](Entity &other) { + if (!context || !context->probeEntity || &other != context->probeEntity) { + return; + } + + if (context->onPlayerDeath) { + context->onPlayerDeath(); + } + }); +} + +void HazardComponent::Update(float) {} + +void HazardComponent::Cleanup() {} diff --git a/as6/components/HazardComponent.hpp b/as6/components/HazardComponent.hpp new file mode 100644 index 0000000..1cdbb62 --- /dev/null +++ b/as6/components/HazardComponent.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Component.hpp" + +/** + * Emits player death event when the probe collides with this hazard. + */ +struct HazardComponent : public Component { + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/HudComponent.cpp b/as6/components/HudComponent.cpp new file mode 100644 index 0000000..1fd2c93 --- /dev/null +++ b/as6/components/HudComponent.cpp @@ -0,0 +1,5 @@ +#include "components/HudComponent.hpp" + +void HudComponent::Setup() {} +void HudComponent::Update(float) {} +void HudComponent::Cleanup() {} diff --git a/as6/components/HudComponent.hpp b/as6/components/HudComponent.hpp new file mode 100644 index 0000000..75ff8b1 --- /dev/null +++ b/as6/components/HudComponent.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Component.hpp" + +/** + * Marker component for HUD entities. + */ +struct HudComponent : public Component { + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/LifetimeComponent.cpp b/as6/components/LifetimeComponent.cpp new file mode 100644 index 0000000..cfd1a59 --- /dev/null +++ b/as6/components/LifetimeComponent.cpp @@ -0,0 +1,14 @@ +#include "components/LifetimeComponent.hpp" + +#include "Entity.hpp" + +void LifetimeComponent::Setup() {} + +void LifetimeComponent::Update(float dt) { + remaining -= dt; + if (remaining <= 0.0f) { + entity->QueueFree(); + } +} + +void LifetimeComponent::Cleanup() {} diff --git a/as6/components/LifetimeComponent.hpp b/as6/components/LifetimeComponent.hpp new file mode 100644 index 0000000..5d7c4b4 --- /dev/null +++ b/as6/components/LifetimeComponent.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "Component.hpp" + +/** + * Frees an entity after a fixed amount of remaining time. + */ +struct LifetimeComponent : public Component { + /** + * Time in seconds until the entity is freed. + */ + float remaining = 0.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/MeterComponent.cpp b/as6/components/MeterComponent.cpp new file mode 100644 index 0000000..73259e1 --- /dev/null +++ b/as6/components/MeterComponent.cpp @@ -0,0 +1,21 @@ +#include "components/MeterComponent.hpp" + +#include + +void MeterComponent::Setup() {} + +void MeterComponent::SetValue(float newValue) { + const float oldValue = value; + value = newValue; + if (context && oldValue != value) { + context->EmitMeterChanged(oldValue, value); + } +} + +void MeterComponent::AddValue(float delta) { SetValue(std::clamp(value + delta, 0.0f, maxValue)); } + +void MeterComponent::Drain(float amount) { AddValue(-amount); } + +void MeterComponent::Update(float) {} + +void MeterComponent::Cleanup() {} diff --git a/as6/components/MeterComponent.hpp b/as6/components/MeterComponent.hpp new file mode 100644 index 0000000..f0b46e5 --- /dev/null +++ b/as6/components/MeterComponent.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Component.hpp" + +#include + +/** + * Stores and mutates the player's gravity meter with change events. + */ +struct MeterComponent : public Component { + float value = 60.0f; + float maxValue = 100.0f; + float drainRate = 14.0f; + float gainPerStar = 28.0f; + + void Setup() override; + void SetValue(float newValue); + void AddValue(float delta); + void Drain(float amount); + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/NullZoneComponent.cpp b/as6/components/NullZoneComponent.cpp new file mode 100644 index 0000000..699071e --- /dev/null +++ b/as6/components/NullZoneComponent.cpp @@ -0,0 +1,5 @@ +#include "components/NullZoneComponent.hpp" + +void NullZoneComponent::Setup() {} +void NullZoneComponent::Update(float) {} +void NullZoneComponent::Cleanup() {} diff --git a/as6/components/NullZoneComponent.hpp b/as6/components/NullZoneComponent.hpp new file mode 100644 index 0000000..6512025 --- /dev/null +++ b/as6/components/NullZoneComponent.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "Component.hpp" + +/** + * Defines a vertical strip where gravity pull is disabled. + */ +struct NullZoneComponent : public Component { + float width = 70.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/PhysicsComponent.cpp b/as6/components/PhysicsComponent.cpp new file mode 100644 index 0000000..0e0e2c1 --- /dev/null +++ b/as6/components/PhysicsComponent.cpp @@ -0,0 +1,27 @@ +#include "components/PhysicsComponent.hpp" + +#include "Entity.hpp" +#include "components/TransformComponent.hpp" + +#include + +void PhysicsComponent::Setup() {} + +void PhysicsComponent::Update(float dt) { + auto transform = entity->GetComponent(); + if (!transform) { + return; + } + + const float speed = std::sqrt(vx * vx + vy * vy); + if (speed > speedCap && speed > 0.0f) { + const float scale = speedCap / speed; + vx *= scale; + vy *= scale; + } + + transform->get().x += vx * dt; + transform->get().y += vy * dt; +} + +void PhysicsComponent::Cleanup() {} diff --git a/as6/components/PhysicsComponent.hpp b/as6/components/PhysicsComponent.hpp new file mode 100644 index 0000000..a4348ee --- /dev/null +++ b/as6/components/PhysicsComponent.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Component.hpp" + +/** + * Integrates velocity into transform each frame with a speed cap. + */ +struct PhysicsComponent : public Component { + float vx = 0.0f; + float vy = 0.0f; + float speedCap = 400.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/ProbeStateComponent.cpp b/as6/components/ProbeStateComponent.cpp new file mode 100644 index 0000000..562cbb9 --- /dev/null +++ b/as6/components/ProbeStateComponent.cpp @@ -0,0 +1,29 @@ +#include "components/ProbeStateComponent.hpp" + +#include "Entity.hpp" +#include "components/TransformComponent.hpp" + +#include "raylib.h" + +void ProbeStateComponent::Setup() {} + +void ProbeStateComponent::Update(float) { + if (!context || !context->probeEntity || entity != context->probeEntity) { + return; + } + + auto transform = entity->GetComponent(); + if (!transform) { + return; + } + + if (transform->get().y < -20.0f || + transform->get().y > static_cast(GetScreenHeight() + 20) || + transform->get().x < -20.0f) { + if (context->onPlayerDeath) { + context->onPlayerDeath(); + } + } +} + +void ProbeStateComponent::Cleanup() {} diff --git a/as6/components/ProbeStateComponent.hpp b/as6/components/ProbeStateComponent.hpp new file mode 100644 index 0000000..f00bbd3 --- /dev/null +++ b/as6/components/ProbeStateComponent.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "Component.hpp" + +/** + * Monitors probe bounds and emits death when probe exits playable area. + */ +struct ProbeStateComponent : public Component { + float spawnX = 96.0f; + float spawnY = 230.0f; + float spawnVx = 165.0f; + float spawnVy = 0.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/ProjectionComponent.cpp b/as6/components/ProjectionComponent.cpp new file mode 100644 index 0000000..6fd21b8 --- /dev/null +++ b/as6/components/ProjectionComponent.cpp @@ -0,0 +1,5 @@ +#include "components/ProjectionComponent.hpp" + +void ProjectionComponent::Setup() {} +void ProjectionComponent::Update(float) {} +void ProjectionComponent::Cleanup() {} diff --git a/as6/components/ProjectionComponent.hpp b/as6/components/ProjectionComponent.hpp new file mode 100644 index 0000000..0a885e7 --- /dev/null +++ b/as6/components/ProjectionComponent.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Component.hpp" + +/** + * Placeholder for future trajectory projection behavior. + */ +struct ProjectionComponent : public Component { + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/RenderComponent.cpp b/as6/components/RenderComponent.cpp new file mode 100644 index 0000000..eb7d80c --- /dev/null +++ b/as6/components/RenderComponent.cpp @@ -0,0 +1,11 @@ +#include "components/RenderComponent.hpp" + +void RenderComponent::Draw() { + if (draw) { + draw(); + } +} + +void RenderComponent::Setup() {} +void RenderComponent::Update(float) {} +void RenderComponent::Cleanup() {} diff --git a/as6/components/RenderComponent.hpp b/as6/components/RenderComponent.hpp new file mode 100644 index 0000000..8f01945 --- /dev/null +++ b/as6/components/RenderComponent.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Component.hpp" + +#include + +/** + * Stores draw callback used by scene render loop. + */ +struct RenderComponent : public Component { + /** + * Callback that performs drawing for this entity. This is invoked by the + * scene's render loop after all updates have been processed, so it can be + * used to draw the entity based on its final state for the frame. + */ + std::function draw; + + void Draw(); + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/ScrollComponent.cpp b/as6/components/ScrollComponent.cpp new file mode 100644 index 0000000..d51c027 --- /dev/null +++ b/as6/components/ScrollComponent.cpp @@ -0,0 +1,13 @@ +#include "components/ScrollComponent.hpp" + +void ScrollComponent::Setup() {} + +void ScrollComponent::Update(float dt) { + speed += accel * dt; + scrollX += speed * dt * 60.0f; + if (context) { + context->scrollX = scrollX; + } +} + +void ScrollComponent::Cleanup() {} diff --git a/as6/components/ScrollComponent.hpp b/as6/components/ScrollComponent.hpp new file mode 100644 index 0000000..92e1b78 --- /dev/null +++ b/as6/components/ScrollComponent.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Component.hpp" + +/** + * Advances the gameplay scroll value over time. + */ +struct ScrollComponent : public Component { + float scrollX = 0.0f; + float speed = 2.0f; + float accel = 0.018f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/ScrollableComponent.cpp b/as6/components/ScrollableComponent.cpp new file mode 100644 index 0000000..6009513 --- /dev/null +++ b/as6/components/ScrollableComponent.cpp @@ -0,0 +1,21 @@ +#include "components/ScrollableComponent.hpp" + +#include "Entity.hpp" +#include "components/TransformComponent.hpp" + +void ScrollableComponent::Setup() {} + +void ScrollableComponent::Update(float) { + if (!context) { + return; + } + + auto transform = entity->GetComponent(); + if (!transform) { + return; + } + + transform->get().x = worldX - context->scrollX; +} + +void ScrollableComponent::Cleanup() {} diff --git a/as6/components/ScrollableComponent.hpp b/as6/components/ScrollableComponent.hpp new file mode 100644 index 0000000..e746000 --- /dev/null +++ b/as6/components/ScrollableComponent.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "Component.hpp" + +/** + * Converts a stable world X position into camera-relative screen X. + */ +struct ScrollableComponent : public Component { + float worldX = 0.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/SpawnComponent.cpp b/as6/components/SpawnComponent.cpp new file mode 100644 index 0000000..d520d96 --- /dev/null +++ b/as6/components/SpawnComponent.cpp @@ -0,0 +1,5 @@ +#include "components/SpawnComponent.hpp" + +void SpawnComponent::Setup() {} +void SpawnComponent::Update(float) {} +void SpawnComponent::Cleanup() {} diff --git a/as6/components/SpawnComponent.hpp b/as6/components/SpawnComponent.hpp new file mode 100644 index 0000000..7a33239 --- /dev/null +++ b/as6/components/SpawnComponent.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "Component.hpp" + +/** + * Placeholder for spawn cursor/state while spawning system evolves. + */ +struct SpawnComponent : public Component { + float cursorWX = 0.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/TrailComponent.cpp b/as6/components/TrailComponent.cpp new file mode 100644 index 0000000..a2b7d9e --- /dev/null +++ b/as6/components/TrailComponent.cpp @@ -0,0 +1,5 @@ +#include "components/TrailComponent.hpp" + +void TrailComponent::Setup() {} +void TrailComponent::Update(float) {} +void TrailComponent::Cleanup() {} diff --git a/as6/components/TrailComponent.hpp b/as6/components/TrailComponent.hpp new file mode 100644 index 0000000..ca699b7 --- /dev/null +++ b/as6/components/TrailComponent.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Component.hpp" + +/** + * Placeholder for future probe trail visualization. + */ +struct TrailComponent : public Component { + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/components/TransformComponent.cpp b/as6/components/TransformComponent.cpp new file mode 100644 index 0000000..f9afc74 --- /dev/null +++ b/as6/components/TransformComponent.cpp @@ -0,0 +1,5 @@ +#include "components/TransformComponent.hpp" + +void TransformComponent::Setup() {} +void TransformComponent::Update(float) {} +void TransformComponent::Cleanup() {} diff --git a/as6/components/TransformComponent.hpp b/as6/components/TransformComponent.hpp new file mode 100644 index 0000000..ec918fd --- /dev/null +++ b/as6/components/TransformComponent.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "Component.hpp" + +/** + * Stores world-space position for an entity. + */ +struct TransformComponent : public Component { + float x = 0.0f; + float y = 0.0f; + + void Setup() override; + void Update(float dt) override; + void Cleanup() override; +}; diff --git a/as6/scene/DeathScene.cpp b/as6/scene/DeathScene.cpp new file mode 100644 index 0000000..c5ec1cc --- /dev/null +++ b/as6/scene/DeathScene.cpp @@ -0,0 +1,23 @@ +#include "scene/DeathScene.hpp" + +#include "scene/GameplayScene.hpp" +#include "scene/SceneManager.hpp" +#include "scene/StartMenuScene.hpp" + +void DeathScene::Update(float) { + if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + manager.QueueSceneChange(); + return; + } + + if (IsKeyPressed(KEY_ESCAPE)) { + manager.QueueSceneChange(); + } +} + +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}); +} diff --git a/as6/scene/DeathScene.hpp b/as6/scene/DeathScene.hpp new file mode 100644 index 0000000..27c6089 --- /dev/null +++ b/as6/scene/DeathScene.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "Draw.hpp" +#include "scene/Scene.hpp" + +#include "raylib.h" + +class GameplayScene; +class StartMenuScene; + +/** + * Post-death scene that offers restart or return to menu. + */ +class DeathScene : public Scene { + public: + explicit DeathScene(SceneManager &owner) : Scene(owner) {} + void Update(float dt) override; + void Draw() override; +}; diff --git a/as6/scene/GameplayScene.cpp b/as6/scene/GameplayScene.cpp new file mode 100644 index 0000000..747f608 --- /dev/null +++ b/as6/scene/GameplayScene.cpp @@ -0,0 +1,100 @@ +#include "scene/GameplayScene.hpp" + +#include "scene/DeathScene.hpp" +#include "scene/SceneManager.hpp" + +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 &collectible) { + ++collectedCount; + + auto transform = collectible.GetComponent(); + if (!transform || !context.entities) { + return; + } + + context.entities->push_back( + CreateDebris(transform->get().x - 4.0f, transform->get().y, -22.0f, -36.0f)); + context.entities->push_back( + CreateDebris(transform->get().x + 4.0f, transform->get().y, 24.0f, -28.0f)); + }); + 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; + } + + context.entities = &entities; + context.worldEntity = (entities.size() > 0 && entities[0]) ? entities[0].get() : nullptr; + context.wellEntity = (entities.size() > 1 && entities[1]) ? entities[1].get() : nullptr; + context.probeEntity = (entities.size() > 2 && entities[2]) ? entities[2].get() : nullptr; + context.hudEntity = (!entities.empty() && entities.back()) ? entities.back().get() : nullptr; + + for (auto &entity : entities) { + if (!entity) { + continue; + } + entity->SetContext(&context); + } +} + +void GameplayScene::Update(float dt) { + UpdateAllSystems(entities, dt); + + context.worldEntity = (entities.size() > 0 && entities[0]) ? entities[0].get() : nullptr; + context.wellEntity = (entities.size() > 1 && entities[1]) ? entities[1].get() : nullptr; + context.probeEntity = (entities.size() > 2 && entities[2]) ? entities[2].get() : nullptr; + context.hudEntity = (!entities.empty() && entities.back()) ? entities.back().get() : nullptr; + + if (wantsDeathScene) { + manager.QueueSceneChange(); + } +} + +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 || entity->queuedForFree) { + continue; + } + + auto render = entity->GetComponent(); + if (render) { + render->get().Draw(); + } + } +} diff --git a/as6/scene/GameplayScene.hpp b/as6/scene/GameplayScene.hpp new file mode 100644 index 0000000..65f55e8 --- /dev/null +++ b/as6/scene/GameplayScene.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "Components.hpp" +#include "Draw.hpp" +#include "Entities.hpp" +#include "GameContext.hpp" +#include "Systems.hpp" +#include "scene/Scene.hpp" + +#include "raylib.h" + +#include +#include + +class DeathScene; + +/** + * Main gameplay state containing active entity list and gameplay context. + */ +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; +}; diff --git a/as6/scene/Scene.hpp b/as6/scene/Scene.hpp new file mode 100644 index 0000000..4c2f0f1 --- /dev/null +++ b/as6/scene/Scene.hpp @@ -0,0 +1,20 @@ +#pragma once + +class SceneManager; + +/** + * Base state interface for scene manager states. + */ +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; +}; diff --git a/as6/scene/SceneManager.hpp b/as6/scene/SceneManager.hpp new file mode 100644 index 0000000..5ddc5f4 --- /dev/null +++ b/as6/scene/SceneManager.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "scene/Scene.hpp" + +#include +#include + +/** + * Owns current scene and supports immediate or queued transitions. + */ +class SceneManager { + public: + template void ChangeScene(Args &&...args); + template void QueueSceneChange(Args &&...args); + + void ChangeScene(std::unique_ptr nextScene); + void QueueSceneChange(std::unique_ptr nextScene); + void Update(float dt); + void Draw(); + + private: + std::unique_ptr current; + std::unique_ptr queued; +}; + +inline void SceneManager::ChangeScene(std::unique_ptr nextScene) { + if (current) { + current->Exit(); + } + current = std::move(nextScene); + if (current) { + current->Enter(); + } +} + +inline void SceneManager::QueueSceneChange(std::unique_ptr nextScene) { + queued = std::move(nextScene); +} + +inline void SceneManager::Update(float dt) { + if (queued) { + ChangeScene(std::move(queued)); + } + if (current) { + current->Update(dt); + } + if (queued) { + ChangeScene(std::move(queued)); + } +} + +inline void SceneManager::Draw() { + if (current) { + current->Draw(); + } +} + +template inline void SceneManager::ChangeScene(Args &&...args) { + ChangeScene(std::make_unique(*this, std::forward(args)...)); +} + +template inline void SceneManager::QueueSceneChange(Args &&...args) { + QueueSceneChange(std::make_unique(*this, std::forward(args)...)); +} diff --git a/as6/scene/StartMenuScene.cpp b/as6/scene/StartMenuScene.cpp new file mode 100644 index 0000000..9405aba --- /dev/null +++ b/as6/scene/StartMenuScene.cpp @@ -0,0 +1,18 @@ +#include "scene/StartMenuScene.hpp" + +#include "scene/GameplayScene.hpp" +#include "scene/SceneManager.hpp" + +void StartMenuScene::Update(float) { + if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + manager.QueueSceneChange(); + } +} + +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}); +} diff --git a/as6/scene/StartMenuScene.hpp b/as6/scene/StartMenuScene.hpp new file mode 100644 index 0000000..96bfc37 --- /dev/null +++ b/as6/scene/StartMenuScene.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "Draw.hpp" +#include "scene/Scene.hpp" + +#include "raylib.h" + +class GameplayScene; + +/** + * Landing scene shown before gameplay starts. + */ +class StartMenuScene : public Scene { + public: + explicit StartMenuScene(SceneManager &owner) : Scene(owner) {} + void Update(float dt) override; + void Draw() override; +};