327 lines
12 KiB
C++
327 lines
12 KiB
C++
#include "SpaceWarp.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
constexpr float MIN_ASPECT = 0.001f;
|
|
}
|
|
|
|
SpaceWarp::SpaceWarp() {
|
|
std::random_device rd;
|
|
rng.seed(rd());
|
|
setFlightMode(SpaceWarpFlightMode::Forward);
|
|
}
|
|
|
|
void SpaceWarp::init(int w, int h, int starCount) {
|
|
resize(w, h);
|
|
stars.resize(std::max(8, starCount));
|
|
for (auto& star : stars) {
|
|
respawn(star, true);
|
|
}
|
|
comets.clear();
|
|
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
|
}
|
|
|
|
void SpaceWarp::resize(int w, int h) {
|
|
width = std::max(1, w);
|
|
height = std::max(1, h);
|
|
centerX = width * 0.5f;
|
|
centerY = height * 0.5f;
|
|
warpFactor = std::max(width, height) * settings.warpFactorScale;
|
|
}
|
|
|
|
void SpaceWarp::setSettings(const SpaceWarpSettings& newSettings) {
|
|
settings = newSettings;
|
|
warpFactor = std::max(width, height) * settings.warpFactorScale;
|
|
cometSpawnTimer = std::clamp(cometSpawnTimer, settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
|
}
|
|
|
|
void SpaceWarp::setFlightMode(SpaceWarpFlightMode mode) {
|
|
flightMode = mode;
|
|
autoPilotEnabled = false;
|
|
switch (mode) {
|
|
case SpaceWarpFlightMode::Forward:
|
|
motion = {1.0f, 0.0f, 0.0f};
|
|
break;
|
|
case SpaceWarpFlightMode::BankLeft:
|
|
motion = {1.05f, -0.85f, 0.0f};
|
|
break;
|
|
case SpaceWarpFlightMode::BankRight:
|
|
motion = {1.05f, 0.85f, 0.0f};
|
|
break;
|
|
case SpaceWarpFlightMode::Reverse:
|
|
motion = {-0.6f, 0.0f, 0.0f};
|
|
break;
|
|
case SpaceWarpFlightMode::Custom:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SpaceWarp::setFlightMotion(const SpaceWarpFlightMotion& newMotion) {
|
|
motion = newMotion;
|
|
flightMode = SpaceWarpFlightMode::Custom;
|
|
autoPilotEnabled = false;
|
|
}
|
|
|
|
void SpaceWarp::setAutoPilotEnabled(bool enabled) {
|
|
autoPilotEnabled = enabled;
|
|
if (enabled) {
|
|
flightMode = SpaceWarpFlightMode::Custom;
|
|
motionTarget = motion;
|
|
autoTimer = 0.0f;
|
|
scheduleNewAutoTarget();
|
|
}
|
|
}
|
|
|
|
void SpaceWarp::scheduleNewAutoTarget() {
|
|
motionTarget.forwardScale = randomRange(0.82f, 1.28f);
|
|
if (randomRange(0.0f, 1.0f) < 0.12f) {
|
|
motionTarget.forwardScale = -randomRange(0.35f, 0.85f);
|
|
}
|
|
motionTarget.lateralSpeed = randomRange(-1.35f, 1.35f);
|
|
motionTarget.verticalSpeed = randomRange(-0.75f, 0.75f);
|
|
autoTimer = randomRange(autoMinInterval, autoMaxInterval);
|
|
}
|
|
|
|
void SpaceWarp::spawnComet() {
|
|
WarpComet comet;
|
|
float aspect = static_cast<float>(width) / static_cast<float>(std::max(1, height));
|
|
float normalizedAspect = std::max(aspect, MIN_ASPECT);
|
|
float xRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? aspect : 1.0f);
|
|
float yRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect));
|
|
comet.x = randomRange(-xRange, xRange);
|
|
comet.y = randomRange(-yRange, yRange);
|
|
comet.z = randomRange(minDepth + 4.0f, maxDepth);
|
|
float baseSpeed = randomRange(settings.minSpeed, settings.maxSpeed);
|
|
float multiplier = randomRange(settings.cometSpeedMultiplierMin, settings.cometSpeedMultiplierMax);
|
|
comet.speed = baseSpeed * multiplier;
|
|
comet.size = randomRange(settings.cometMinSize, settings.cometMaxSize);
|
|
comet.trailLength = randomRange(settings.cometMinTrail, settings.cometMaxTrail);
|
|
comet.life = randomRange(1.8f, 3.4f);
|
|
comet.maxLife = comet.life;
|
|
float shade = randomRange(0.85f, 1.0f);
|
|
Uint8 c = static_cast<Uint8>(std::clamp(220.0f + shade * 35.0f, 0.0f, 255.0f));
|
|
comet.color = SDL_Color{c, Uint8(std::min(255.0f, c * 0.95f)), 255, 255};
|
|
comet.prevScreenX = centerX;
|
|
comet.prevScreenY = centerY;
|
|
comet.screenX = centerX;
|
|
comet.screenY = centerY;
|
|
comets.push_back(comet);
|
|
}
|
|
|
|
float SpaceWarp::randomRange(float min, float max) {
|
|
std::uniform_real_distribution<float> dist(min, max);
|
|
return dist(rng);
|
|
}
|
|
|
|
static int randomIntInclusive(std::mt19937& rng, int min, int max) {
|
|
std::uniform_int_distribution<int> dist(min, max);
|
|
return dist(rng);
|
|
}
|
|
|
|
void SpaceWarp::respawn(WarpStar& star, bool randomDepth) {
|
|
float aspect = static_cast<float>(width) / static_cast<float>(std::max(1, height));
|
|
float normalizedAspect = std::max(aspect, MIN_ASPECT);
|
|
float xRange = settings.baseSpawnRange * (aspect >= 1.0f ? aspect : 1.0f);
|
|
float yRange = settings.baseSpawnRange * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect));
|
|
star.x = randomRange(-xRange, xRange);
|
|
star.y = randomRange(-yRange, yRange);
|
|
star.z = randomDepth ? randomRange(minDepth, maxDepth) : maxDepth;
|
|
star.speed = randomRange(settings.minSpeed, settings.maxSpeed);
|
|
star.shade = randomRange(settings.minShade, settings.maxShade);
|
|
static constexpr Uint8 GRAY_SHADES[] = {160, 180, 200, 220, 240};
|
|
int idx = randomIntInclusive(rng, 0, int(std::size(GRAY_SHADES)) - 1);
|
|
star.baseShade = GRAY_SHADES[idx];
|
|
star.prevScreenX = centerX;
|
|
star.prevScreenY = centerY;
|
|
star.screenX = centerX;
|
|
star.screenY = centerY;
|
|
}
|
|
|
|
bool SpaceWarp::project(const WarpStar& star, float& outX, float& outY) const {
|
|
return projectPoint(star.x, star.y, star.z, outX, outY);
|
|
}
|
|
|
|
bool SpaceWarp::projectPoint(float x, float y, float z, float& outX, float& outY) const {
|
|
if (z <= minDepth) {
|
|
return false;
|
|
}
|
|
float perspective = warpFactor / (z + 0.001f);
|
|
outX = centerX + x * perspective;
|
|
outY = centerY + y * perspective;
|
|
const float margin = settings.spawnMargin;
|
|
return outX >= -margin && outX <= width + margin && outY >= -margin && outY <= height + margin;
|
|
}
|
|
|
|
void SpaceWarp::update(float deltaSeconds) {
|
|
if (stars.empty()) {
|
|
return;
|
|
}
|
|
|
|
if (settings.cometSpawnIntervalMax > 0.0f) {
|
|
cometSpawnTimer -= deltaSeconds;
|
|
if (cometSpawnTimer <= 0.0f) {
|
|
spawnComet();
|
|
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
|
}
|
|
}
|
|
|
|
if (autoPilotEnabled) {
|
|
autoTimer -= deltaSeconds;
|
|
if (autoTimer <= 0.0f) {
|
|
scheduleNewAutoTarget();
|
|
}
|
|
auto follow = std::clamp(deltaSeconds * 0.45f, 0.0f, 1.0f);
|
|
motion.forwardScale = std::lerp(motion.forwardScale, motionTarget.forwardScale, follow);
|
|
motion.lateralSpeed = std::lerp(motion.lateralSpeed, motionTarget.lateralSpeed, follow);
|
|
motion.verticalSpeed = std::lerp(motion.verticalSpeed, motionTarget.verticalSpeed, follow);
|
|
}
|
|
|
|
const float forwardScale = (std::abs(motion.forwardScale) < 0.01f)
|
|
? (motion.forwardScale >= 0.0f ? 0.01f : -0.01f)
|
|
: motion.forwardScale;
|
|
const bool movingBackward = forwardScale < 0.0f;
|
|
const float lateralSpeed = motion.lateralSpeed;
|
|
const float verticalSpeed = motion.verticalSpeed;
|
|
|
|
for (auto& star : stars) {
|
|
star.z -= star.speed * deltaSeconds * forwardScale;
|
|
if (!movingBackward) {
|
|
if (star.z <= minDepth) {
|
|
respawn(star, true);
|
|
continue;
|
|
}
|
|
} else {
|
|
if (star.z >= maxDepth) {
|
|
respawn(star, true);
|
|
star.z = minDepth + randomRange(0.25f, 24.0f);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
float closeness = 1.0f - std::clamp(star.z / maxDepth, 0.0f, 1.0f);
|
|
float driftScale = (0.35f + closeness * 1.25f);
|
|
star.x += lateralSpeed * deltaSeconds * driftScale;
|
|
star.y += verticalSpeed * deltaSeconds * driftScale;
|
|
|
|
float sx = 0.0f;
|
|
float sy = 0.0f;
|
|
if (!project(star, sx, sy)) {
|
|
respawn(star, true);
|
|
continue;
|
|
}
|
|
|
|
star.prevScreenX = star.screenX;
|
|
star.prevScreenY = star.screenY;
|
|
star.screenX = sx;
|
|
star.screenY = sy;
|
|
|
|
float dx = star.screenX - star.prevScreenX;
|
|
float dy = star.screenY - star.prevScreenY;
|
|
float lenSq = dx * dx + dy * dy;
|
|
float maxStreak = std::max(settings.maxTrailLength, 0.0f);
|
|
if (maxStreak > 0.0f && lenSq > maxStreak * maxStreak) {
|
|
float len = std::sqrt(lenSq);
|
|
float scale = maxStreak / len;
|
|
star.prevScreenX = star.screenX - dx * scale;
|
|
star.prevScreenY = star.screenY - dy * scale;
|
|
}
|
|
}
|
|
|
|
for (auto it = comets.begin(); it != comets.end();) {
|
|
auto& comet = *it;
|
|
comet.life -= deltaSeconds;
|
|
comet.z -= comet.speed * deltaSeconds * forwardScale;
|
|
bool expired = comet.life <= 0.0f;
|
|
if (!movingBackward) {
|
|
if (comet.z <= minDepth * 0.35f) expired = true;
|
|
} else {
|
|
if (comet.z >= maxDepth + 40.0f) expired = true;
|
|
}
|
|
|
|
float closeness = 1.0f - std::clamp(comet.z / maxDepth, 0.0f, 1.0f);
|
|
float driftScale = (0.45f + closeness * 1.6f);
|
|
comet.x += lateralSpeed * deltaSeconds * driftScale;
|
|
comet.y += verticalSpeed * deltaSeconds * driftScale;
|
|
|
|
float sx = 0.0f;
|
|
float sy = 0.0f;
|
|
if (!projectPoint(comet.x, comet.y, comet.z, sx, sy)) {
|
|
expired = true;
|
|
} else {
|
|
comet.prevScreenX = comet.screenX;
|
|
comet.prevScreenY = comet.screenY;
|
|
comet.screenX = sx;
|
|
comet.screenY = sy;
|
|
|
|
float dx = comet.screenX - comet.prevScreenX;
|
|
float dy = comet.screenY - comet.prevScreenY;
|
|
float lenSq = dx * dx + dy * dy;
|
|
float maxTrail = std::max(comet.trailLength, 0.0f);
|
|
if (maxTrail > 0.0f && lenSq > maxTrail * maxTrail) {
|
|
float len = std::sqrt(lenSq);
|
|
float scale = maxTrail / len;
|
|
comet.prevScreenX = comet.screenX - dx * scale;
|
|
comet.prevScreenY = comet.screenY - dy * scale;
|
|
}
|
|
}
|
|
|
|
if (expired) {
|
|
it = comets.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) {
|
|
if (stars.empty()) {
|
|
return;
|
|
}
|
|
|
|
SDL_BlendMode previous = SDL_BLENDMODE_NONE;
|
|
SDL_GetRenderDrawBlendMode(renderer, &previous);
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD);
|
|
|
|
for (const auto& star : stars) {
|
|
float depthFactor = 1.0f - std::clamp(star.z / maxDepth, 0.0f, 1.0f);
|
|
float alphaBase = std::clamp(settings.minAlpha + depthFactor * settings.alphaDepthBoost, 0.0f, 255.0f);
|
|
Uint8 alpha = static_cast<Uint8>(std::clamp(alphaBase * alphaScale, 0.0f, 255.0f));
|
|
float colorValue = std::clamp(
|
|
star.baseShade * (settings.baseShadeScale + depthFactor * settings.depthColorScale) * star.shade,
|
|
settings.minColor,
|
|
settings.maxColor);
|
|
Uint8 color = static_cast<Uint8>(colorValue);
|
|
|
|
if (settings.drawTrails) {
|
|
float trailAlphaFloat = alpha * settings.trailAlphaScale;
|
|
Uint8 trailAlpha = static_cast<Uint8>(std::clamp(trailAlphaFloat, 0.0f, 255.0f));
|
|
SDL_SetRenderDrawColor(renderer, color, color, color, trailAlpha);
|
|
SDL_RenderLine(renderer, star.prevScreenX, star.prevScreenY, star.screenX, star.screenY);
|
|
}
|
|
|
|
float dotSize = std::clamp(settings.minDotSize + depthFactor * (settings.maxDotSize - settings.minDotSize),
|
|
settings.minDotSize,
|
|
settings.maxDotSize);
|
|
SDL_FRect dot{star.screenX - dotSize * 0.5f, star.screenY - dotSize * 0.5f, dotSize, dotSize};
|
|
SDL_SetRenderDrawColor(renderer, color, color, color, alpha);
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
}
|
|
|
|
for (const auto& comet : comets) {
|
|
float lifeNorm = std::clamp(comet.life / comet.maxLife, 0.0f, 1.0f);
|
|
Uint8 alpha = static_cast<Uint8>(std::clamp(220.0f * lifeNorm, 0.0f, 255.0f));
|
|
SDL_SetRenderDrawColor(renderer, comet.color.r, comet.color.g, comet.color.b, alpha);
|
|
SDL_RenderLine(renderer, comet.prevScreenX, comet.prevScreenY, comet.screenX, comet.screenY);
|
|
|
|
float size = comet.size * (0.8f + (1.0f - lifeNorm) * 0.6f);
|
|
SDL_FRect head{comet.screenX - size * 0.5f, comet.screenY - size * 0.5f, size, size};
|
|
SDL_RenderFillRect(renderer, &head);
|
|
}
|
|
|
|
SDL_SetRenderDrawBlendMode(renderer, previous);
|
|
}
|