From bd622d18730fcaca53aab05ffcbcb8da87a37286 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Mon, 16 Mar 2026 13:03:53 -0700 Subject: [PATCH] Add black hole and stuff --- as6/Components.hpp | 1 + as6/EnergyBarRaygui.hpp | 16 +++-- as6/Entities.cpp | 71 ++++++++++++++++----- as6/Entities.hpp | 2 + as6/components/GravityReceiverComponent.cpp | 63 ++++++++---------- as6/components/GravityReceiverComponent.hpp | 8 +-- as6/components/GravityWellComponent.cpp | 54 ++++++++++++---- as6/components/GravityWellComponent.hpp | 16 +++-- as6/components/PhysicsComponent.cpp | 2 +- as6/components/PlayerBlackHoleComponent.hpp | 13 ++++ as6/components/ProjectionComponent.cpp | 64 +++++++++++++++---- as6/components/SpawnComponent.cpp | 4 +- as6/components/StatsComponent.hpp | 4 +- as6/scene/DeathScene.cpp | 4 ++ as6/scene/DeathScene.hpp | 5 ++ as6/scene/GameplayScene.cpp | 36 ++++++----- as6/scene/GameplayScene.hpp | 2 +- 17 files changed, 244 insertions(+), 121 deletions(-) create mode 100644 as6/components/PlayerBlackHoleComponent.hpp diff --git a/as6/Components.hpp b/as6/Components.hpp index a991d16..bf03144 100644 --- a/as6/Components.hpp +++ b/as6/Components.hpp @@ -8,6 +8,7 @@ #include "components/LifetimeComponent.hpp" #include "components/NullZoneComponent.hpp" #include "components/PhysicsComponent.hpp" +#include "components/PlayerBlackHoleComponent.hpp" #include "components/ProbeStateComponent.hpp" #include "components/ProjectionComponent.hpp" #include "components/RenderComponent.hpp" diff --git a/as6/EnergyBarRaygui.hpp b/as6/EnergyBarRaygui.hpp index c269e17..4946b70 100644 --- a/as6/EnergyBarRaygui.hpp +++ b/as6/EnergyBarRaygui.hpp @@ -34,13 +34,21 @@ inline void DrawMainMenu() { "GRAVITY SURFING"); GuiLabel((Rectangle){WINDOW_WIDTH / 2 - 200, WINDOW_HEIGHT / 2, 400, 60}, - "LEFT CLICK to turn on gravity well and attract probe\n" - "Avoid hazards and collect stars to keep your meter from draining\n" - "If your meter runs out, gravity well turns off and you lose control\n" - "until you collect another star\n" + "LEFT CLICK to place a black hole that lasts 1 second\n" + "Each black hole costs 10 meter and does not drain over time\n" + "Avoid hazards and collect stars to refill meter\n" + "Place as many black holes as your meter allows\n" "Press SPACE or LEFT CLICK to start"); } +inline void DrawDeathStats(int stars) { + GuiLabel((Rectangle){WINDOW_WIDTH / 2 - 150, WINDOW_HEIGHT / 2 - 40, 300, 32}, "YOU DIED"); + GuiLabel((Rectangle){WINDOW_WIDTH / 2 - 150, WINDOW_HEIGHT / 2 - 4, 300, 24}, + TextFormat("Stars Collected: %d", stars)); + GuiLabel((Rectangle){WINDOW_WIDTH / 2 - 150, WINDOW_HEIGHT / 2 + 28, 300, 20}, + "Press ENTER or LEFT CLICK to retry"); +} + //------------------------------------------------------------------------------------ // Controls Functions Definitions (local) //------------------------------------------------------------------------------------ diff --git a/as6/Entities.cpp b/as6/Entities.cpp index 07fc992..699af40 100644 --- a/as6/Entities.cpp +++ b/as6/Entities.cpp @@ -3,6 +3,7 @@ #include "Components.hpp" #include "EnergyBarRaygui.hpp" +#include "raylib.h" #include std::shared_ptr CreateProbe() { @@ -54,19 +55,8 @@ std::shared_ptr CreateGravityWell() { transform.y = 230.0f; auto &well = e->AddComponent(); - well.mass = static_cast(1 << 22); + well.mass = 0.0f; 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; } @@ -85,6 +75,8 @@ std::shared_ptr CreateBlackHole(float x, float y) { well.controlledByMouse = false; well.alwaysActive = true; + // shader removed: previously used for black hole lensing + auto &collider = e->AddComponent(); collider.radius = 18.0f; @@ -107,6 +99,52 @@ std::shared_ptr CreateBlackHole(float x, float y) { return e; } +std::shared_ptr CreatePlayerBlackHole(float worldX, float y, float scrollX, float ttl) { + auto e = std::make_shared(); + auto &transform = e->AddComponent(); + transform.x = worldX - scrollX; + transform.y = y; + + auto &scrollable = e->AddComponent(); + scrollable.worldX = worldX; + + auto &well = e->AddComponent(); + well.mass = static_cast(1 << 22); + well.minDist = 28.0f; + well.controlledByMouse = false; + well.alwaysActive = true; + + // shader removed: previously used for black hole lensing + + auto &lifetime = e->AddComponent(); + lifetime.remaining = ttl; + + // Mark this as a player-created black hole so it can be removed via right-click + e->AddComponent(); + + auto &render = e->AddComponent(); + render.draw = [e]() { + auto transform = e->GetComponent(); + if (!transform) { + return; + } + + auto lifetime = e->GetComponent(); + if (!lifetime) { + return; + } + + const int cx = static_cast(transform->get().x); + const int cy = static_cast(transform->get().y); + float t = 360 * lifetime->get().remaining / 1; + DrawCircleSector({(float)cx, (float)cy}, 28.0f, 0.0f, t, 32, Color{70, 95, 170, 25}); + DrawCircleLines(cx, cy, 18.0f, Color{70, 95, 170, 240}); + DrawCircleLines(cx, cy, 24.0f, Color{95, 120, 200, 120}); + }; + + return e; +} + std::shared_ptr CreateStar(float x, float y) { auto e = std::make_shared(); auto &t = e->AddComponent(); @@ -115,7 +153,7 @@ std::shared_ptr CreateStar(float x, float y) { auto &scrollable = e->AddComponent(); scrollable.worldX = x; auto &collider = e->AddComponent(); - collider.radius = 12.0f; + collider.radius = 20.0f; e->AddComponent(); auto &render = e->AddComponent(); @@ -126,9 +164,10 @@ std::shared_ptr CreateStar(float x, float y) { } Vector2 center = {transform->get().x, transform->get().y}; Color STAR_COLOR = Color{255, 223, 86, 255}; - Color GLOW_COLOR = Color{255, 223, 86, 120}; - DrawCircleV(center, 12.0f, GLOW_COLOR); - DrawPoly(center, 3, 4.0f, transform->get().x, STAR_COLOR); + Color GLOW_COLOR = Color{255, 223, 86, 80}; + DrawCircleV(center, 20.0f, GLOW_COLOR); + DrawPoly(center, 3, 10.0f, transform->get().x, STAR_COLOR); + DrawPoly(center, 3, 10.0f, transform->get().x + 180, STAR_COLOR); }; return e; } diff --git a/as6/Entities.hpp b/as6/Entities.hpp index dfa6a6b..3b35c59 100644 --- a/as6/Entities.hpp +++ b/as6/Entities.hpp @@ -7,6 +7,8 @@ std::shared_ptr CreateProbe(); std::shared_ptr CreateGravityWell(); std::shared_ptr CreateBlackHole(float x, float y); +std::shared_ptr CreatePlayerBlackHole(float worldX, float y, float scrollX, + float ttl = 1.0f); 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); diff --git a/as6/components/GravityReceiverComponent.cpp b/as6/components/GravityReceiverComponent.cpp index 9be390a..935179c 100644 --- a/as6/components/GravityReceiverComponent.cpp +++ b/as6/components/GravityReceiverComponent.cpp @@ -4,7 +4,6 @@ #include "components/GravityWellComponent.hpp" #include "components/NullZoneComponent.hpp" #include "components/PhysicsComponent.hpp" -#include "components/StatsComponent.hpp" #include "components/TransformComponent.hpp" #include @@ -17,33 +16,9 @@ void GravityReceiverComponent::Update(float dt) { 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; - } - - StatsComponent *statsPtr = nullptr; - if (context->statsEntity) { - auto stats = context->statsEntity->GetComponent(); - if (stats) { - statsPtr = &stats->get(); - } - } - - if (!wellGravity->get().active) { - return; - } - if (statsPtr && statsPtr->value <= 0.0f) { + if (!myTransform || !physics) { return; } @@ -72,23 +47,35 @@ void GravityReceiverComponent::Update(float dt) { 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) { + if (!context->entities) { 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; + for (auto &other : *context->entities) { + if (!other || other.get() == entity) { + continue; + } - physics->get().vx += nx * force * dt; - physics->get().vy += ny * force * dt; + auto wellTransform = other->GetComponent(); + auto wellGravity = other->GetComponent(); + if (!wellTransform || !wellGravity || !wellGravity->get().active) { + continue; + } - if (statsPtr) { - statsPtr->Drain(statsPtr->drainRate * dt); + 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) { + continue; + } + + 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; } } diff --git a/as6/components/GravityReceiverComponent.hpp b/as6/components/GravityReceiverComponent.hpp index 07d26b2..ee9381b 100644 --- a/as6/components/GravityReceiverComponent.hpp +++ b/as6/components/GravityReceiverComponent.hpp @@ -3,15 +3,9 @@ #include "Component.hpp" /** - * Applies gravity well force to the probe and drains meter while active. + * Applies gravity from active wells to the probe. */ 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. */ diff --git a/as6/components/GravityWellComponent.cpp b/as6/components/GravityWellComponent.cpp index ff77da1..443a1c5 100644 --- a/as6/components/GravityWellComponent.cpp +++ b/as6/components/GravityWellComponent.cpp @@ -1,32 +1,58 @@ #include "components/GravityWellComponent.hpp" +#include "Entities.hpp" #include "Entity.hpp" -#include "components/TransformComponent.hpp" +#include "components/PlayerBlackHoleComponent.hpp" +#include "components/StatsComponent.hpp" #include "raylib.h" -#include - void GravityWellComponent::Setup() {} -void GravityWellComponent::Update(float dt) { - auto transform = entity->GetComponent(); - if (!transform) { - return; - } - +void GravityWellComponent::Update(float) { if (!controlledByMouse) { active = alwaysActive; return; } - active = IsMouseButtonDown(MOUSE_BUTTON_LEFT); + active = false; + + if (!context || !context->entities) { + return; + } + + // Right click removes all player-created black holes + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { + for (auto &ent : *context->entities) { + if (!ent || ent->queuedForFree) { + continue; + } + + auto marker = ent->GetComponent(); + if (marker) { + ent->QueueFree(); + } + } + // Do not early return; allow left-click placement to still happen in the same frame + } + + if (!IsMouseButtonPressed(MOUSE_BUTTON_LEFT) || !context->statsEntity) { + return; + } + + auto stats = context->statsEntity->GetComponent(); + if (!stats || stats->get().value < placementCost) { + return; + } + + stats->get().Drain(placementCost); 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; + const float worldX = context->scrollX + mouse.x; + + auto blackHole = CreatePlayerBlackHole(worldX, mouse.y, context->scrollX, placementTtl); + blackHole->SetContext(context); + context->entities->push_back(blackHole); } void GravityWellComponent::Cleanup() {} diff --git a/as6/components/GravityWellComponent.hpp b/as6/components/GravityWellComponent.hpp index 8199496..578350b 100644 --- a/as6/components/GravityWellComponent.hpp +++ b/as6/components/GravityWellComponent.hpp @@ -3,7 +3,7 @@ #include "Component.hpp" /** - * Represents player-controlled gravity source that follows mouse input. + * Handles gravity well behavior for both hazards and player-spawned black holes. */ struct GravityWellComponent : public Component { /** @@ -18,13 +18,12 @@ struct GravityWellComponent : public Component { 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). + * Whether the gravity source is currently active and should apply gravitational force. */ bool active = false; /** - * If true, active state is driven by left mouse button and this well follows the cursor. + * If true, this component is used as the player input controller. */ bool controlledByMouse = true; @@ -34,9 +33,14 @@ struct GravityWellComponent : public Component { bool alwaysActive = false; /** - * Lerp factor for how quickly the gravity well follows the mouse position. + * Meter cost paid to place one player black hole. */ - float followLerp = 12.0f; + float placementCost = 10.0f; + + /** + * Lifetime in seconds for each player-placed black hole. + */ + float placementTtl = 1.0f; void Setup() override; void Update(float dt) override; diff --git a/as6/components/PhysicsComponent.cpp b/as6/components/PhysicsComponent.cpp index b6eeaa6..26be919 100644 --- a/as6/components/PhysicsComponent.cpp +++ b/as6/components/PhysicsComponent.cpp @@ -100,7 +100,7 @@ bool PhysicsComponent::ComputeGravityDeltaVelocity(double px, double py, double continue; } - if (probeMeterDepleted && wellGravity->get().controlledByMouse) { + if (probeMeterDepleted && !wellGravity->get().alwaysActive) { continue; } diff --git a/as6/components/PlayerBlackHoleComponent.hpp b/as6/components/PlayerBlackHoleComponent.hpp new file mode 100644 index 0000000..e2bf3d2 --- /dev/null +++ b/as6/components/PlayerBlackHoleComponent.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "Component.hpp" + +/** + * Marker component to identify player-created black holes. It carries no + * behavior and is only used for runtime identification and removal. + */ +struct PlayerBlackHoleComponent : public Component { + void Setup() override {} + void Update(float) override {} + void Cleanup() override {} +}; diff --git a/as6/components/ProjectionComponent.cpp b/as6/components/ProjectionComponent.cpp index 6c25250..3dd3732 100644 --- a/as6/components/ProjectionComponent.cpp +++ b/as6/components/ProjectionComponent.cpp @@ -1,11 +1,14 @@ #include "components/ProjectionComponent.hpp" #include "Entity.hpp" -#include "components/GravityReceiverComponent.hpp" #include "components/GravityWellComponent.hpp" #include "components/PhysicsComponent.hpp" +#include "components/StatsComponent.hpp" #include "components/TransformComponent.hpp" +#include +#include + void ProjectionComponent::Setup() { points.clear(); } void ProjectionComponent::Update(float) { @@ -18,25 +21,45 @@ void ProjectionComponent::Update(float) { auto transform = entity->GetComponent(); auto physics = entity->GetComponent(); - auto receiver = entity->GetComponent(); - if (!transform || !physics || !receiver) { + if (!transform || !physics || !context->entities) { return; } - Entity *wellEntity = receiver->get().well ? receiver->get().well : context->wellEntity; - if (!wellEntity) { - return; + float placementCost = 10.0f; + float previewMass = static_cast(1 << 22); + float previewMinDist = 28.0f; + if (context->wellEntity) { + auto controllerWell = context->wellEntity->GetComponent(); + if (controllerWell) { + placementCost = controllerWell->get().placementCost; + if (controllerWell->get().mass > 0.0f) { + previewMass = controllerWell->get().mass; + } + previewMinDist = controllerWell->get().minDist; + } } - auto wellTransform = wellEntity->GetComponent(); - auto wellGravity = wellEntity->GetComponent(); - if (!wellTransform || !wellGravity) { - return; + bool canPreviewPlacement = false; + if (context->statsEntity) { + auto stats = context->statsEntity->GetComponent(); + canPreviewPlacement = stats && stats->get().value >= placementCost; } - // only highlight if the well is active, inactive will be a lighter color to show the potential - // projection if the player were to activate it - highlightActive = wellGravity->get().active; + const Vector2 mouse = GetMousePosition(); + const double previewX = static_cast(mouse.x); + const double previewY = static_cast(mouse.y); + + for (auto &other : *context->entities) { + if (!other || other.get() == entity) { + continue; + } + + auto wellGravity = other->GetComponent(); + if (wellGravity && wellGravity->get().active) { + highlightActive = true; + break; + } + } double px = static_cast(transform->get().x); double py = static_cast(transform->get().y); @@ -46,6 +69,21 @@ void ProjectionComponent::Update(float) { points.reserve(static_cast(steps)); for (int i = 0; i < steps; ++i) { physics->get().SimulateStep(px, py, vx, vy, static_cast(stepDt), true); + + if (canPreviewPlacement && !physics->get().IsInsideNullZone(px)) { + const double dx = previewX - px; + const double dy = previewY - py; + const double dist = std::sqrt(dx * dx + dy * dy); + if (dist > 0.0001) { + const double clampedDist = std::max(dist, static_cast(previewMinDist)); + const double force = static_cast(previewMass) / (clampedDist * clampedDist); + const double dt = static_cast(stepDt); + vx += (dx / dist) * force * dt; + vy += (dy / dist) * force * dt; + physics->get().ClampVelocity(vx, vy); + } + } + points.push_back({static_cast(px), static_cast(py)}); } diff --git a/as6/components/SpawnComponent.cpp b/as6/components/SpawnComponent.cpp index 12e425f..9e3f1eb 100644 --- a/as6/components/SpawnComponent.cpp +++ b/as6/components/SpawnComponent.cpp @@ -33,12 +33,12 @@ void SpawnComponent::Update(float) { while (cursorWX < spawnLimit) { const float r = static_cast(GetRandomValue(0, 99)); - if (r < 50.0f) { + if (r < 70.0f) { const float y = static_cast(GetRandomValue(48, GetScreenHeight() - 48)); auto star = CreateStar(cursorWX, y); star->SetContext(context); context->entities->push_back(star); - } else if (r < 78.0f) { + } else if (r < 90.0f) { const float y = static_cast(GetRandomValue(42, GetScreenHeight() - 42)); auto asteroid = CreateAsteroid(cursorWX, y); asteroid->SetContext(context); diff --git a/as6/components/StatsComponent.hpp b/as6/components/StatsComponent.hpp index 2ef38d6..90b2e3f 100644 --- a/as6/components/StatsComponent.hpp +++ b/as6/components/StatsComponent.hpp @@ -10,8 +10,8 @@ struct StatsComponent : public Component { float value = 60.0f; float maxValue = 100.0f; - float drainRate = 14.0f; - float gainPerStar = 28.0f; + float drainRate = 10.0f; + float gainPerStar = 50.0f; int stars = 0; void Setup() override; diff --git a/as6/scene/DeathScene.cpp b/as6/scene/DeathScene.cpp index 25d0a40..a1d40d6 100644 --- a/as6/scene/DeathScene.cpp +++ b/as6/scene/DeathScene.cpp @@ -1,5 +1,6 @@ #include "scene/DeathScene.hpp" +#include "EnergyBarRaygui.hpp" #include "scene/GameplayScene.hpp" #include "scene/SceneManager.hpp" #include "scene/StartMenuScene.hpp" @@ -17,4 +18,7 @@ void DeathScene::Update(float) { void DeathScene::Draw() { Scene::Draw(); + // Draw centered death stats using the HUD helper + // collectedStars is a private member; access via this-> to satisfy older compilers / linters + DrawDeathStats(this->collectedStars); } diff --git a/as6/scene/DeathScene.hpp b/as6/scene/DeathScene.hpp index 27c6089..a2bb548 100644 --- a/as6/scene/DeathScene.hpp +++ b/as6/scene/DeathScene.hpp @@ -14,6 +14,11 @@ class StartMenuScene; class DeathScene : public Scene { public: explicit DeathScene(SceneManager &owner) : Scene(owner) {} + // Alternate constructor that accepts the star count to display. + explicit DeathScene(SceneManager &owner, int stars) : Scene(owner), collectedStars(stars) {} void Update(float dt) override; void Draw() override; + + private: + int collectedStars = 0; }; diff --git a/as6/scene/GameplayScene.cpp b/as6/scene/GameplayScene.cpp index e1d0c83..29df318 100644 --- a/as6/scene/GameplayScene.cpp +++ b/as6/scene/GameplayScene.cpp @@ -4,17 +4,23 @@ #include "scene/DeathScene.hpp" #include "scene/SceneManager.hpp" +#include + void GameplayScene::Enter() { entities.clear(); entities.reserve(20); context = {}; - wantsDeathScene = false; collectedCount = 0; meterValue = 60.0f; - context.onPlayerDeath = [this]() { wantsDeathScene = true; }; + context.onPlayerDeath = [this]() { + // Inject the collected star count into the queued DeathScene by passing it + // as a constructor argument. SceneManager::QueueSceneChange supports + // forwarding constructor args. + manager.QueueSceneChange(collectedCount); + }; context.AddCollectiblePickedListener([this](Entity &collectible) { - ++collectedCount; + collectedCount++; auto transform = collectible.GetComponent(); if (!transform || !context.entities) { @@ -56,23 +62,19 @@ void GameplayScene::Enter() { } } -void GameplayScene::Update(float dt) { - UpdateAllSystems(entities, dt); +void GameplayScene::Exit() { entities.clear(); } - if (wantsDeathScene) { - manager.QueueSceneChange(); - } -} +void GameplayScene::Update(float dt) { UpdateAllSystems(entities, dt); } void GameplayScene::Draw() { - std::optional> worldScroll; - if (context.worldEntity) { - worldScroll = context.worldEntity->GetComponent(); - } - - std::optional> probeTransform; - if (context.probeEntity) { - probeTransform = context.probeEntity->GetComponent(); + for (auto &entity : entities) { + if (!entity || entity->queuedForFree) { + continue; + } + auto render = entity->GetComponent(); + if (render) { + render->get().Draw(); + } } Scene::Draw(); diff --git a/as6/scene/GameplayScene.hpp b/as6/scene/GameplayScene.hpp index 92ff2bc..876a070 100644 --- a/as6/scene/GameplayScene.hpp +++ b/as6/scene/GameplayScene.hpp @@ -21,12 +21,12 @@ class GameplayScene : public Scene { public: explicit GameplayScene(SceneManager &owner) : Scene(owner) {} void Enter() override; + void Exit() override; void Update(float dt) override; void Draw() override; private: GameContext context; - bool wantsDeathScene = false; int collectedCount = 0; float meterValue = 60.0f; };