From 3ee599caeb459a604a97f65948b061d81dfb7dbf Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Sun, 15 Mar 2026 22:05:41 -0700 Subject: [PATCH] Add trails and projections --- as6/components/PhysicsComponent.cpp | 126 +++++++++++++++++++++++-- as6/components/PhysicsComponent.hpp | 32 +++++++ as6/components/ProbeStateComponent.cpp | 3 +- as6/components/ProjectionComponent.cpp | 75 ++++++++++++++- as6/components/ProjectionComponent.hpp | 16 +++- as6/components/TrailComponent.cpp | 95 ++++++++++++++++++- as6/components/TrailComponent.hpp | 28 +++++- 7 files changed, 358 insertions(+), 17 deletions(-) diff --git a/as6/components/PhysicsComponent.cpp b/as6/components/PhysicsComponent.cpp index 0e0e2c1..4293e83 100644 --- a/as6/components/PhysicsComponent.cpp +++ b/as6/components/PhysicsComponent.cpp @@ -1,27 +1,137 @@ #include "components/PhysicsComponent.hpp" #include "Entity.hpp" +#include "components/GravityWellComponent.hpp" +#include "components/NullZoneComponent.hpp" #include "components/TransformComponent.hpp" +#include #include void PhysicsComponent::Setup() {} +void PhysicsComponent::ClampVelocity(float &vxRef, float &vyRef) const { + const float speed = std::sqrt(vxRef * vxRef + vyRef * vyRef); + if (speed > speedCap && speed > 0.0f) { + const float scale = speedCap / speed; + vxRef *= scale; + vyRef *= scale; + } +} + +void PhysicsComponent::ClampVelocity(double &vxRef, double &vyRef) const { + const double speed = std::sqrt(vxRef * vxRef + vyRef * vyRef); + if (speed > static_cast(speedCap) && speed > 0.0) { + const double scale = static_cast(speedCap) / speed; + vxRef *= scale; + vyRef *= scale; + } +} + +void PhysicsComponent::IntegratePosition(float &xRef, float &yRef, float vxValue, float vyValue, + float dt) { + xRef += vxValue * dt; + yRef += vyValue * dt; +} + +void PhysicsComponent::IntegratePosition(double &xRef, double &yRef, double vxValue, double vyValue, + double dt) { + xRef += vxValue * dt; + yRef += vyValue * dt; +} + +bool PhysicsComponent::IsInsideNullZone(double xPos) const { + if (!context || !context->entities) { + return false; + } + + for (auto &other : *context->entities) { + if (!other || other.get() == entity) { + continue; + } + + auto zone = other->GetComponent(); + auto zoneTransform = other->GetComponent(); + if (!zone || !zoneTransform) { + continue; + } + + const double left = static_cast(zoneTransform->get().x); + const double right = left + static_cast(zone->get().width); + if (xPos >= left && xPos <= right) { + return true; + } + } + + return false; +} + +bool PhysicsComponent::ComputeWellDeltaVelocity(double px, double py, double dt, double &dvx, + double &dvy, bool ignoreWellActive) const { + dvx = 0.0; + dvy = 0.0; + + if (!context || !context->wellEntity) { + return false; + } + + auto wellTransform = context->wellEntity->GetComponent(); + auto wellGravity = context->wellEntity->GetComponent(); + if (!wellTransform || !wellGravity) { + return false; + } + + if (!ignoreWellActive && !wellGravity->get().active) { + return false; + } + + if (IsInsideNullZone(px)) { + return false; + } + + const double dx = static_cast(wellTransform->get().x) - px; + const double dy = static_cast(wellTransform->get().y) - py; + const double dist = std::sqrt(dx * dx + dy * dy); + if (dist <= 0.0001) { + return false; + } + + const double clampedDist = std::max(dist, static_cast(wellGravity->get().minDist)); + const double force = static_cast(wellGravity->get().mass) / (clampedDist * clampedDist); + dvx = (dx / dist) * force * dt; + dvy = (dy / dist) * force * dt; + return true; +} + +void PhysicsComponent::SimulateStep(double &xRef, double &yRef, double &vxRef, double &vyRef, + double dt, bool ignoreWellActive) const { + double dvx = 0.0; + double dvy = 0.0; + ComputeWellDeltaVelocity(xRef, yRef, dt, dvx, dvy, ignoreWellActive); + vxRef += dvx; + vyRef += dvy; + + ClampVelocity(vxRef, vyRef); + IntegratePosition(xRef, yRef, vxRef, vyRef, dt); +} + 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; - } + double x = static_cast(transform->get().x); + double y = static_cast(transform->get().y); + double vxLocal = static_cast(vx); + double vyLocal = static_cast(vy); - transform->get().x += vx * dt; - transform->get().y += vy * dt; + SimulateStep(x, y, vxLocal, vyLocal, static_cast(dt), false); + + transform->get().x = static_cast(x); + transform->get().y = static_cast(y); + vx = static_cast(vxLocal); + vy = static_cast(vyLocal); } void PhysicsComponent::Cleanup() {} diff --git a/as6/components/PhysicsComponent.hpp b/as6/components/PhysicsComponent.hpp index a4348ee..b8bd2cf 100644 --- a/as6/components/PhysicsComponent.hpp +++ b/as6/components/PhysicsComponent.hpp @@ -10,6 +10,38 @@ struct PhysicsComponent : public Component { float vy = 0.0f; float speedCap = 400.0f; + /** + * Clamps the provided velocity vector to this component's speed cap. + */ + void ClampVelocity(float &vxRef, float &vyRef) const; + void ClampVelocity(double &vxRef, double &vyRef) const; + + /** + * Integrates a position by velocity over dt. + */ + static void IntegratePosition(float &xRef, float &yRef, float vxValue, float vyValue, float dt); + static void IntegratePosition(double &xRef, double &yRef, double vxValue, double vyValue, + double dt); + + /** + * Returns true when the given X position is inside a null zone. + */ + bool IsInsideNullZone(double xPos) const; + + /** + * Computes velocity delta from well gravity for a position. + * Returns false when gravity should not apply. + */ + bool ComputeWellDeltaVelocity(double px, double py, double dt, double &dvx, double &dvy, + bool ignoreWellActive = false) const; + + /** + * Performs one projected simulation step using well/null-zone rules, + * velocity clamp, and position integration. + */ + void SimulateStep(double &xRef, double &yRef, double &vxRef, double &vyRef, double dt, + bool ignoreWellActive = false) const; + void Setup() override; void Update(float dt) override; void Cleanup() override; diff --git a/as6/components/ProbeStateComponent.cpp b/as6/components/ProbeStateComponent.cpp index 562cbb9..b9b9639 100644 --- a/as6/components/ProbeStateComponent.cpp +++ b/as6/components/ProbeStateComponent.cpp @@ -19,7 +19,8 @@ void ProbeStateComponent::Update(float) { if (transform->get().y < -20.0f || transform->get().y > static_cast(GetScreenHeight() + 20) || - transform->get().x < -20.0f) { + transform->get().x < -20.0f || + transform->get().x > static_cast(GetScreenWidth() + 20)) { if (context->onPlayerDeath) { context->onPlayerDeath(); } diff --git a/as6/components/ProjectionComponent.cpp b/as6/components/ProjectionComponent.cpp index 6fd21b8..6c25250 100644 --- a/as6/components/ProjectionComponent.cpp +++ b/as6/components/ProjectionComponent.cpp @@ -1,5 +1,74 @@ #include "components/ProjectionComponent.hpp" -void ProjectionComponent::Setup() {} -void ProjectionComponent::Update(float) {} -void ProjectionComponent::Cleanup() {} +#include "Entity.hpp" +#include "components/GravityReceiverComponent.hpp" +#include "components/GravityWellComponent.hpp" +#include "components/PhysicsComponent.hpp" +#include "components/TransformComponent.hpp" + +void ProjectionComponent::Setup() { points.clear(); } + +void ProjectionComponent::Update(float) { + points.clear(); + highlightActive = false; + + if (!context || !context->probeEntity || entity != context->probeEntity) { + return; + } + + auto transform = entity->GetComponent(); + auto physics = entity->GetComponent(); + auto receiver = entity->GetComponent(); + if (!transform || !physics || !receiver) { + return; + } + + Entity *wellEntity = receiver->get().well ? receiver->get().well : context->wellEntity; + if (!wellEntity) { + return; + } + + auto wellTransform = wellEntity->GetComponent(); + auto wellGravity = wellEntity->GetComponent(); + if (!wellTransform || !wellGravity) { + return; + } + + // 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; + + double px = static_cast(transform->get().x); + double py = static_cast(transform->get().y); + double vx = static_cast(physics->get().vx); + double vy = static_cast(physics->get().vy); + + points.reserve(static_cast(steps)); + for (int i = 0; i < steps; ++i) { + physics->get().SimulateStep(px, py, vx, vy, static_cast(stepDt), true); + points.push_back({static_cast(px), static_cast(py)}); + } + + Draw(); +} + +void ProjectionComponent::Cleanup() { points.clear(); } + +void ProjectionComponent::Draw() const { + if (points.empty()) { + return; + } + + const Color base = highlightActive ? activeColor : inactiveColor; + for (size_t i = 0; i < points.size(); ++i) { + // t goes from 0 to 1 across the points, used for fading effect + const float t = (points.size() <= 1) ? 0.0f : (float)i / (points.size() - 1); + Color c = base; + float alphaScale = 1.0f - t; + + // multiply alpha to make it fade towards the end + c.a = (unsigned char)((float)(base.a) * alphaScale); + + DrawCircleV(points[i], pointRadius, c); + } +} diff --git a/as6/components/ProjectionComponent.hpp b/as6/components/ProjectionComponent.hpp index 0a885e7..9feca51 100644 --- a/as6/components/ProjectionComponent.hpp +++ b/as6/components/ProjectionComponent.hpp @@ -2,11 +2,25 @@ #include "Component.hpp" +#include "raylib.h" + +#include + /** - * Placeholder for future trajectory projection behavior. + * Predicts and renders the probe's near-future trajectory. */ struct ProjectionComponent : public Component { + std::vector points; + int steps = 30; + float stepDt = 1.0f / 60.0f; + float pointRadius = 2.4f; + bool highlightActive = false; + Color inactiveColor = Color{125, 146, 178, 120}; + Color activeColor = Color{160, 206, 255, 220}; + void Setup() override; void Update(float dt) override; void Cleanup() override; + + void Draw() const; }; diff --git a/as6/components/TrailComponent.cpp b/as6/components/TrailComponent.cpp index a2b7d9e..535b765 100644 --- a/as6/components/TrailComponent.cpp +++ b/as6/components/TrailComponent.cpp @@ -1,5 +1,94 @@ #include "components/TrailComponent.hpp" -void TrailComponent::Setup() {} -void TrailComponent::Update(float) {} -void TrailComponent::Cleanup() {} +#include "Entity.hpp" +#include "components/TransformComponent.hpp" + +#include +#include + +void TrailComponent::Setup() { + points.clear(); + elapsed = 0.0f; + lastSampleAt = 0.0f; + + auto transform = entity->GetComponent(); + if (!transform) { + return; + } + + points.push_back({transform->get().x, transform->get().y, elapsed}); +} + +void TrailComponent::Update(float dt) { + elapsed += dt; + + auto transform = entity->GetComponent(); + if (!transform) { + return; + } + + while (!points.empty() && (elapsed - points.front().createdAt) > pointLifetime) { + points.pop_front(); + } + + const float x = transform->get().x; + const float y = transform->get().y; + if (points.empty()) { + points.push_back({x, y, elapsed}); + lastSampleAt = elapsed; + return; + } + + const Point &last = points.back(); + const float dx = x - last.x; + const float dy = y - last.y; + + // check if we've moved far enough from the last point or if enough time has passed to warrant a + // new sample, for smoother trails when the probe is moving slowly + const bool movedEnough = (dx * dx + dy * dy) >= (sampleDistance * sampleDistance); + const bool waitedEnough = (elapsed - lastSampleAt) >= sampleInterval; + if (movedEnough || waitedEnough) { + points.push_back({x, y, elapsed}); + lastSampleAt = elapsed; + } else { + points.back().x = x; + points.back().y = y; + points.back().createdAt = elapsed; + } + + if (points.size() > maxPoints) { + while (points.size() > maxPoints) { + points.pop_front(); + } + } + + Draw(); +} + +void TrailComponent::Cleanup() { + points.clear(); + elapsed = 0.0f; + lastSampleAt = 0.0f; +} + +void TrailComponent::Draw() const { + if (points.size() < 2 || pointLifetime <= 0.0f) { + return; + } + + for (size_t i = 1; i < points.size(); ++i) { + const Point &a = points[i - 1]; + const Point &b = points[i]; + + const float ageA = elapsed - a.createdAt; + const float ageB = elapsed - b.createdAt; + const float lifeNormA = std::clamp(1.0f - (ageA / pointLifetime), 0.0f, 1.0f); + const float lifeNormB = std::clamp(1.0f - (ageB / pointLifetime), 0.0f, 1.0f); + const float lifeNorm = 0.5f * (lifeNormA + lifeNormB); + + Color c = color; + c.a = (unsigned char)((float)(color.a) * lifeNorm); + + DrawLineEx({a.x, a.y}, {b.x, b.y}, lineWidth, c); + } +} diff --git a/as6/components/TrailComponent.hpp b/as6/components/TrailComponent.hpp index ca699b7..ce793d5 100644 --- a/as6/components/TrailComponent.hpp +++ b/as6/components/TrailComponent.hpp @@ -2,11 +2,37 @@ #include "Component.hpp" +#include "raylib.h" + +#include +#include + /** - * Placeholder for future probe trail visualization. + * Stores and renders a fading positional trail for moving entities. + * + * Points are timestamped and retired from the front of a queue once they + * exceed `pointLifetime`, so updates avoid touching every point each frame. */ struct TrailComponent : public Component { + struct Point { + float x = 0.0f; + float y = 0.0f; + float createdAt = 0.0f; + }; + + std::deque points; + float elapsed = 0.0f; + float lastSampleAt = 0.0f; + float sampleDistance = 5.0f; + float sampleInterval = 1.0f / 30.0f; + float pointLifetime = 0.85f; + float lineWidth = 2.0f; + std::size_t maxPoints = 96; + Color color = Color{120, 210, 255, 180}; + void Setup() override; void Update(float dt) override; void Cleanup() override; + + void Draw() const; };