#include "SpaceWarp.h" #include #include #include 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(width) / static_cast(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(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 dist(min, max); return dist(rng); } static int randomIntInclusive(std::mt19937& rng, int min, int max) { std::uniform_int_distribution dist(min, max); return dist(rng); } void SpaceWarp::respawn(WarpStar& star, bool randomDepth) { float aspect = static_cast(width) / static_cast(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(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(colorValue); if (settings.drawTrails) { float trailAlphaFloat = alpha * settings.trailAlphaScale; Uint8 trailAlpha = static_cast(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(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); }