#include "vk.hpp" #include "SDL.h" #include <random> #include <cmath> #include <format> #include <ctime> #include <climits> #include <cstring> #include "argz.hpp" #include "input.hpp" #ifndef M_PI #define M_PI 3.14159265358979323846 #endif constexpr int GAME_W = 640; constexpr int GAME_H = 360; constexpr int STAR_COUNT = 800; constexpr int MAX_PROJECTILES = 50; constexpr int MAX_ASTEROIDS = 30; constexpr int MAX_PARTICLES = 40; constexpr int ASTEROID_VERTICES = 8; constexpr float PROJECTILE_SPEED = 5.0f; constexpr int PROJECTILE_LIFETIME = 60; constexpr int FIRE_COOLDOWN = 5; constexpr int SHOTS_PER_BURST = 5; constexpr int FIRE_DELAY = 3; constexpr int EXPLOSION_DURATION = 90; constexpr float MIN_ASTEROID_RADIUS = 8.0f; constexpr int LARGE_ASTEROID_POINTS = 20; constexpr int MEDIUM_ASTEROID_POINTS = 50; constexpr int SMALL_ASTEROID_POINTS = 100; struct Ship { float x, y; float vx, vy; float angle; int lives; int fire_cooldown; int burst_count; bool exploding; int explosion_timer; int score; int continuous_fire_timer; bool overheated; int overheat_cooldown; }; struct Projectile { float x, y; float vx, vy; float lifetime; bool active; }; struct Asteroid { float x, y; float vx, vy; float radius; bool active; float vertices[ASTEROID_VERTICES][2]; float rotation_angle; float rotation_speed; }; struct Particle { float x, y; float vx, vy; int lifetime; bool active; }; struct Star { float x, y, speed; float size; float brightness; int layer; float r, g, b; float twinklePhase; float twinkleSpeed; }; enum GameState { STATE_COUNTDOWN, STATE_LAUNCH, STATE_PLAYING, STATE_GAMEOVER }; template<typename Func> void bresenhamLine(int x0, int y0, int x1, int y1, Func plot) { int dx = abs(x1 - x0); int dy = abs(y1 - y0); int sx = (x0 < x1) ? 1 : -1; int sy = (y0 < y1) ? 1 : -1; int err = dx - dy; while (true) { plot(x0, y0); if (x0 == x1 && y0 == y1) break; int e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } class SpaceRoxWindow : public mx::VKWindow { public: SpaceRoxWindow(const std::string &path, int wx, int wy, bool full) : mx::VKWindow("-[ SpaceRox - Vulkan ]-", wx, wy, full) { setPath(path); srand(static_cast<unsigned>(time(nullptr))); } virtual ~SpaceRoxWindow() {} void initVulkan() override { mx::VKWindow::initVulkan(); if (!enableControllerInput()) { SDL_Log("Controller input unavailable; continuing with keyboard input."); } SDL_Surface *starSurf = SDL_CreateRGBSurfaceWithFormat(0, 4, 4, 32, SDL_PIXELFORMAT_RGBA32); Uint32 *pixels = (Uint32*)starSurf->pixels; for (int y = 0; y < 4; y++) { for (int x = 0; x < 4; x++) { float dx = (x - 1.5f) / 1.5f; float dy = (y - 1.5f) / 1.5f; float dist = sqrtf(dx*dx + dy*dy); Uint8 alpha = (dist < 1.0f) ? (Uint8)(255 * (1.0f - dist)) : 0; pixels[y * 4 + x] = SDL_MapRGBA(starSurf->format, 255, 255, 255, alpha); } } starSprite = createSprite(starSurf, util.path + "/data/sprite_vert.spv", util.path + "/data/sprite_frag.spv"); SDL_FreeSurface(starSurf); SDL_Surface *whiteSurf = SDL_CreateRGBSurfaceWithFormat(0, 2, 2, 32, SDL_PIXELFORMAT_RGBA32); SDL_FillRect(whiteSurf, nullptr, SDL_MapRGBA(whiteSurf->format, 255, 255, 255, 255)); pixel = createSprite(whiteSurf, util.path + "/data/sprite_vert.spv", util.path + "/data/solid_frag.spv"); SDL_FreeSurface(whiteSurf); initGame(); } void onResize() override { vkDeviceWaitIdle(device); clearTextQueue(); lastFontSize = 0; updateFontSize(); } void proc() override { Uint32 currentTime = SDL_GetTicks(); Uint32 frameTime = currentTime - lastFrameTime; if (frameTime < targetFrameTime) { SDL_Delay(targetFrameTime - frameTime); currentTime = SDL_GetTicks(); frameTime = currentTime - lastFrameTime; } if (lastFrameTime > 0) { float deltaTime = frameTime / 1000.0f; globalTime += deltaTime; } lastFrameTime = currentTime; updateFontSize(); pollController(); const Uint8 *keys = SDL_GetKeyboardState(nullptr); switch (state) { case STATE_COUNTDOWN: updateCountdown(); drawCountdown(); break; case STATE_LAUNCH: updateLaunch(); drawLaunch(); break; case STATE_PLAYING: if ((keys[SDL_SCANCODE_SPACE] || joyFire) && canFire()) { fireProjectile(ship.x, ship.y, ship.angle); } updateShip(); updateFireTimer(keys); updateProjectiles(); updateAsteroids(); updateExplosion(); checkAsteroidCollisions(); checkShipAsteroidCollision(); checkProjectileAsteroidCollisions(); checkAndSpawnAsteroids(); handleDeathSequence(); drawGame(); break; case STATE_GAMEOVER: drawGameOver(); break; } } void event(SDL_Event &e) override { if (controller.connectEvent(e)) { mx::system_out << "Controller connected: " << controller.name() << " (index " << controller.controllerIndex() << ")\n"; joyRestLX = 0; joyRestLY = 0; } if (e.type == SDL_CONTROLLERDEVICEREMOVED) { mx::system_out << "Controller disconnected.\n"; joyRestLX = 0; joyRestLY = 0; } switch (e.type) { case SDL_QUIT: quit(); break; case SDL_KEYDOWN: if (e.key.keysym.sym == SDLK_ESCAPE) { quit(); } else if (e.key.keysym.sym == SDLK_ESCAPE) quit(); if (e.key.keysym.sym == SDLK_SPACE && state == STATE_GAMEOVER) { restartGame(); } if (state == STATE_PLAYING) { if (e.key.keysym.sym == SDLK_LEFT) keyLeft = true; if (e.key.keysym.sym == SDLK_RIGHT) keyRight = true; if (e.key.keysym.sym == SDLK_UP) keyThrust = true; } break; case SDL_KEYUP: if (state == STATE_PLAYING) { if (e.key.keysym.sym == SDLK_LEFT) keyLeft = false; if (e.key.keysym.sym == SDLK_RIGHT) keyRight = false; if (e.key.keysym.sym == SDLK_UP) keyThrust = false; } break; case SDL_CONTROLLERBUTTONDOWN: handleControllerButton(e.cbutton.button); break; case SDL_CONTROLLERBUTTONUP: handleControllerButtonUp(e.cbutton.button); break; } } void handleControllerButton(Uint8 button) { switch (state) { case STATE_GAMEOVER: if (button == SDL_CONTROLLER_BUTTON_A || button == SDL_CONTROLLER_BUTTON_START) { restartGame(); } break; case STATE_PLAYING: if (button == SDL_CONTROLLER_BUTTON_A) { joyFire = true; } else if (button == SDL_CONTROLLER_BUTTON_B || button == SDL_CONTROLLER_BUTTON_BACK) { } else if (button == SDL_CONTROLLER_BUTTON_START) { } break; default: break; } } void handleControllerButtonUp(Uint8 button) { if (state == STATE_PLAYING) { if (button == SDL_CONTROLLER_BUTTON_A) { joyFire = false; } } } void pollController() { if (!controller.active()) { joyLeft = joyRight = joyThrust = joyFire = false; joyCenterStable = 0; return; } if(controller.getButton(mx::Input_Button::BTN_START)) { quit(); return; } if (state != STATE_PLAYING || ship.exploding) { joyLeft = joyRight = joyThrust = joyFire = false; return; } Sint16 rawLX = controller.getAxis(SDL_CONTROLLER_AXIS_LEFTX); Sint16 rawLY = controller.getAxis(SDL_CONTROLLER_AXIS_LEFTY); if (abs(rawLX) < JOY_RECENTER_ZONE && abs(rawLY) < JOY_RECENTER_ZONE) { if (joyCenterStable < JOY_RECENTER_FRAMES) { joyCenterStable++; } if (joyCenterStable >= JOY_RECENTER_FRAMES) { joyRestLX = rawLX; joyRestLY = rawLY; } } else { joyCenterStable = 0; } int lx = static_cast<int>(rawLX) - static_cast<int>(joyRestLX); int ly = static_cast<int>(rawLY) - static_cast<int>(joyRestLY); joyLeft = (lx < -JOY_ROTATE_ZONE); joyRight = (lx > JOY_ROTATE_ZONE); joyThrust = (ly < -JOY_THRUST_ZONE); Sint16 rt = controller.getAxis(SDL_CONTROLLER_AXIS_TRIGGERRIGHT); joyFire = (rt > JOY_THRUST_ZONE) || controller.getButton(mx::Input_Button::BTN_A); } private: mx::VKSprite *pixel = nullptr; mx::VKSprite *starSprite = nullptr; Uint32 lastFrameTime = 0; static constexpr Uint32 targetFrameTime = 1000 / 60; float globalTime = 0.0f; Ship ship{}; Projectile projectiles[MAX_PROJECTILES]{}; Asteroid asteroids[MAX_ASTEROIDS]{}; Particle particles[MAX_PARTICLES]{}; Star stars[STAR_COUNT * 3]{}; bool starsInit = false; GameState state = STATE_COUNTDOWN; bool keyLeft = false, keyRight = false, keyThrust = false; bool keyFire = false; bool joyLeft = false, joyRight = false, joyThrust = false; bool joyFire = false; bool wasExploding = false; mx::Input controller; static constexpr Sint16 JOY_ROTATE_ZONE = 16000; static constexpr Sint16 JOY_THRUST_ZONE = 12000; static constexpr Sint16 JOY_RECENTER_ZONE = 8000; static constexpr int JOY_RECENTER_FRAMES = 12; Sint16 joyRestLX = 0, joyRestLY = 0; int joyCenterStable = 0; int countdownTimer = 0; int countdownDuration = 60; int countdownNumber = 3; int launchTimer = 0; int launchDuration = 60; float shipLaunchY = 0; int lastFontSize = 0; float sx() const { return static_cast<float>(w) / GAME_W; } float sy() const { return static_cast<float>(h) / GAME_H; } int toScreenX(float gx) const { return static_cast<int>(gx * sx()); } int toScreenY(float gy) const { return static_cast<int>(gy * sy()); } int toScreenW(float gw) const { return std::max(1, static_cast<int>(gw * sx())); } int toScreenH(float gh) const { return std::max(1, static_cast<int>(gh * sy())); } void setColor(float r, float g, float b, float a = 1.0f) { pixel->setShaderParams(r, g, b, a); } void drawPixel(float gx, float gy) { int psz = std::max(1, static_cast<int>(2.0f * std::min(sx(), sy()))); pixel->drawSpriteRect(toScreenX(gx), toScreenY(gy), psz, psz); } void drawRect(float gx, float gy, float gw, float gh) { pixel->drawSpriteRect(toScreenX(gx), toScreenY(gy), toScreenW(gw), toScreenH(gh)); } void drawLine(float x0f, float y0f, float x1f, float y1f) { int x0 = toScreenX(x0f), y0 = toScreenY(y0f); int x1 = toScreenX(x1f), y1 = toScreenY(y1f); int psz = std::max(1, static_cast<int>(1.5f * std::min(sx(), sy()))); bresenhamLine(x0, y0, x1, y1, [&](int px, int py) { pixel->drawSpriteRect(px, py, psz, psz); }); } void drawPoint(int screenX, int screenY, int size = 2) { pixel->drawSpriteRect(screenX, screenY, size, size); } void updateFontSize() { int fontSize = static_cast<int>(20.0f * (static_cast<float>(h) / 480.0f)); fontSize = std::max(14, std::min(128, fontSize)); if (fontSize != lastFontSize) { lastFontSize = fontSize; setFont("font.ttf", fontSize); clearTextQueue(); } } void initGame() { initShip(); initProjectiles(); initAsteroids(); initStars(); state = STATE_COUNTDOWN; countdownTimer = 0; countdownNumber = 3; wasExploding = false; } void restartGame() { initGame(); } void initStars() { int starIndex = 0; for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) { stars[starIndex].x = static_cast<float>(rand() % GAME_W); stars[starIndex].y = static_cast<float>(rand() % GAME_H); stars[starIndex].speed = 0.15f + (rand() % 40) / 100.0f; stars[starIndex].size = 0.8f + (rand() % 12) / 20.0f; stars[starIndex].brightness = 0.25f + (rand() % 45) / 100.0f; stars[starIndex].layer = 0; assignStarColor(stars[starIndex]); stars[starIndex].twinklePhase = (rand() % 628) / 100.0f; stars[starIndex].twinkleSpeed = 0.4f + (rand() % 150) / 100.0f; } for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) { stars[starIndex].x = static_cast<float>(rand() % GAME_W); stars[starIndex].y = static_cast<float>(rand() % GAME_H); stars[starIndex].speed = 0.4f + (rand() % 80) / 100.0f; stars[starIndex].size = 1.2f + (rand() % 18) / 20.0f; stars[starIndex].brightness = 0.45f + (rand() % 55) / 100.0f; stars[starIndex].layer = 1; assignStarColor(stars[starIndex]); stars[starIndex].twinklePhase = (rand() % 628) / 100.0f; stars[starIndex].twinkleSpeed = 0.8f + (rand() % 250) / 100.0f; } for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) { stars[starIndex].x = static_cast<float>(rand() % GAME_W); stars[starIndex].y = static_cast<float>(rand() % GAME_H); stars[starIndex].speed = 0.8f + (rand() % 180) / 100.0f; stars[starIndex].size = 1.8f + (rand() % 25) / 20.0f; stars[starIndex].brightness = 0.65f + (rand() % 35) / 100.0f; stars[starIndex].layer = 2; assignStarColor(stars[starIndex]); stars[starIndex].twinklePhase = (rand() % 628) / 100.0f; stars[starIndex].twinkleSpeed = 1.2f + (rand() % 350) / 100.0f; } starsInit = true; } void assignStarColor(Star& s) { float roll = (rand() % 1000) / 1000.0f; if (roll < 0.35f) { s.r = 0.85f + (rand() % 15) / 100.0f; s.g = 0.90f + (rand() % 10) / 100.0f; s.b = 1.0f; } else if (roll < 0.55f) { s.r = 0.5f + (rand() % 20) / 100.0f; s.g = 0.75f + (rand() % 15) / 100.0f; s.b = 1.0f; s.size *= 1.15f; s.brightness *= 1.1f; } else if (roll < 0.70f) { s.r = 1.0f; s.g = 0.95f + (rand() % 5) / 100.0f; s.b = 0.6f + (rand() % 30) / 100.0f; s.size *= 1.05f; } else if (roll < 0.82f) { s.r = 1.0f; s.g = 0.6f + (rand() % 20) / 100.0f; s.b = 0.2f + (rand() % 20) / 100.0f; s.size *= 1.1f; } else if (roll < 0.90f) { s.r = 1.0f; s.g = 0.3f + (rand() % 20) / 100.0f; s.b = 0.2f + (rand() % 15) / 100.0f; s.size *= 1.08f; } else if (roll < 0.96f) { s.r = 1.0f; s.g = 1.0f; s.b = 1.0f; s.size *= 1.4f; s.brightness *= 1.4f; } else { s.r = 0.9f; s.g = 0.95f; s.b = 1.0f; s.size *= 1.6f; s.brightness *= 1.5f; } } void initShip() { ship.x = GAME_W / 2.0f; ship.y = GAME_H / 2.0f; ship.vx = 0; ship.vy = 0; ship.angle = 0; ship.lives = 3; ship.fire_cooldown = 0; ship.burst_count = 0; ship.exploding = false; ship.explosion_timer = 0; ship.score = 0; ship.continuous_fire_timer = 0; ship.overheated = false; ship.overheat_cooldown = 0; for (auto &p : particles) p.active = false; } void initProjectiles() { for (auto &p : projectiles) p.active = false; } void initAsteroids() { for (auto &a : asteroids) a.active = false; for (int i = 0; i < 4; ++i) { float x, y; do { x = static_cast<float>(rand() % GAME_W); y = static_cast<float>(rand() % GAME_H); } while (hypotf(x - ship.x, y - ship.y) < 100); float vx = ((rand() % 100) - 50) / 50.0f; float vy = ((rand() % 100) - 50) / 50.0f; float radius = 20.0f + (rand() % 20); spawnAsteroid(x, y, vx, vy, radius); } } void updateShip() { if (ship.exploding) return; if (keyLeft || joyLeft) ship.angle -= 0.15f; if (keyRight || joyRight) ship.angle += 0.15f; if (keyThrust || joyThrust) { float thrust = 0.2f; ship.vx += sinf(ship.angle) * thrust; ship.vy += -cosf(ship.angle) * thrust; float spd = sqrtf(ship.vx * ship.vx + ship.vy * ship.vy); float maxSpd = 4.0f; if (spd > maxSpd) { ship.vx = (ship.vx / spd) * maxSpd; ship.vy = (ship.vy / spd) * maxSpd; } } ship.vx *= 0.98f; ship.vy *= 0.98f; ship.x += ship.vx; ship.y += ship.vy; if (ship.x < 0) ship.x += GAME_W; if (ship.x >= GAME_W) ship.x -= GAME_W; if (ship.y < 0) ship.y += GAME_H; if (ship.y >= GAME_H) ship.y -= GAME_H; if (ship.fire_cooldown > 0) ship.fire_cooldown--; } void updateFireTimer(const Uint8 *keys) { bool firing = keys[SDL_SCANCODE_SPACE] || joyFire; if (firing && !ship.overheated) { ship.continuous_fire_timer++; ship.overheat_cooldown = 0; if (ship.continuous_fire_timer >= 180) { ship.overheated = true; ship.overheat_cooldown = 0; ship.continuous_fire_timer = 0; ship.burst_count = 0; } } else if (firing && ship.overheated) { ship.overheat_cooldown = 0; } else { if (ship.overheated) { ship.overheat_cooldown++; if (ship.overheat_cooldown >= 180) { ship.overheated = false; ship.overheat_cooldown = 0; ship.continuous_fire_timer = 0; } } else { if (ship.continuous_fire_timer > 0) ship.continuous_fire_timer--; } } } bool canFire() { if (ship.overheated) return false; if (ship.fire_cooldown <= 0) { if (ship.burst_count < SHOTS_PER_BURST) { ship.fire_cooldown = FIRE_DELAY; ship.burst_count++; return true; } else { ship.fire_cooldown = FIRE_COOLDOWN; ship.burst_count = 0; return false; } } return false; } void fireProjectile(float x, float y, float angle) { for (auto &p : projectiles) { if (!p.active) { float c = cosf(angle), s = sinf(angle); float lx = 0, ly = -12; p.x = x + (lx * c - ly * s); p.y = y + (lx * s + ly * c); p.vx = sinf(angle) * PROJECTILE_SPEED; p.vy = -cosf(angle) * PROJECTILE_SPEED; p.lifetime = PROJECTILE_LIFETIME; p.active = true; break; } } } void updateProjectiles() { for (auto &p : projectiles) { if (!p.active) continue; p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x >= GAME_W || p.y < 0 || p.y >= GAME_H) { p.active = false; continue; } p.lifetime--; if (p.lifetime <= 0) p.active = false; } } void spawnAsteroid(float x, float y, float vx, float vy, float radius) { for (auto &a : asteroids) { if (!a.active) { a.x = x; a.y = y; a.vx = vx; a.vy = vy; a.radius = radius; a.active = true; a.rotation_angle = 0; a.rotation_speed = ((rand() % 100) - 50) / 1000.0f; for (int j = 0; j < ASTEROID_VERTICES; ++j) { float ang = static_cast<float>(j) / ASTEROID_VERTICES * 2 * M_PI; float vr = radius * (0.7f + (rand() % 30) / 100.0f); a.vertices[j][0] = cosf(ang) * vr; a.vertices[j][1] = sinf(ang) * vr; } break; } } } void updateAsteroids() { for (auto &a : asteroids) { if (!a.active) continue; a.x += a.vx; a.y += a.vy; a.rotation_angle += a.rotation_speed; if (a.x < 0) a.x += GAME_W; if (a.x >= GAME_W) a.x -= GAME_W; if (a.y < 0) a.y += GAME_H; if (a.y >= GAME_H) a.y -= GAME_H; } } void splitAsteroid(int idx) { float x = asteroids[idx].x; float y = asteroids[idx].y; float r = asteroids[idx].radius / 2.0f; if (r < MIN_ASTEROID_RADIUS) { createAsteroidExplosion(x, y); ship.score += SMALL_ASTEROID_POINTS; asteroids[idx].active = false; return; } int splits = 2 + (rand() % 2); for (int i = 0; i < splits; ++i) { float ang = (rand() % 628) / 100.0f; float spd = 0.5f + (rand() % 15) / 10.0f; float nvx = cosf(ang) * spd + asteroids[idx].vx * 0.3f; float nvy = sinf(ang) * spd + asteroids[idx].vy * 0.3f; float off = r * 0.5f; spawnAsteroid(x + cosf(ang) * off, y + sinf(ang) * off, nvx, nvy, r); } if (r >= 25.0f) ship.score += LARGE_ASTEROID_POINTS; else ship.score += MEDIUM_ASTEROID_POINTS; createAsteroidExplosion(x, y); asteroids[idx].active = false; } void checkAndSpawnAsteroids() { int active = 0; for (auto &a : asteroids) if (a.active) active++; int minAsteroids = 3; if (active < minAsteroids) { for (int s = 0; s < minAsteroids - active; ++s) { float x, y; int attempts = 0; do { if (rand() % 2) { x = (rand() % 2) ? -30.0f : GAME_W + 30.0f; y = static_cast<float>(rand() % GAME_H); } else { x = static_cast<float>(rand() % GAME_W); y = (rand() % 2) ? -30.0f : GAME_H + 30.0f; } attempts++; } while (hypotf(x - ship.x, y - ship.y) < 100.0f && attempts < 10); float vx = ((rand() % 100) - 50) / 50.0f; float vy = ((rand() % 100) - 50) / 50.0f; float radius; int roll = rand() % 100; if (roll < 20) radius = 30.0f + (rand() % 15); else if (roll < 60) radius = 20.0f + (rand() % 10); else radius = 12.0f + (rand() % 8); spawnAsteroid(x, y, vx, vy, radius); } } } void checkAsteroidCollisions() { for (int i = 0; i < MAX_ASTEROIDS; ++i) { if (!asteroids[i].active) continue; for (int j = i + 1; j < MAX_ASTEROIDS; ++j) { if (!asteroids[j].active) continue; float dx = asteroids[i].x - asteroids[j].x; float dy = asteroids[i].y - asteroids[j].y; float dist = sqrtf(dx * dx + dy * dy); float minDist = asteroids[i].radius + asteroids[j].radius; if (dist < minDist && dist > 0) { float nx = dx / dist, ny = dy / dist; float overlap = (minDist - dist) * 0.5f; asteroids[i].x += nx * overlap; asteroids[i].y += ny * overlap; asteroids[j].x -= nx * overlap; asteroids[j].y -= ny * overlap; float rvx = asteroids[i].vx - asteroids[j].vx; float rvy = asteroids[i].vy - asteroids[j].vy; float van = rvx * nx + rvy * ny; if (van > 0) continue; float restitution = 0.8f; float impulse = -(1 + restitution) * van * 0.5f; asteroids[i].vx += impulse * nx; asteroids[i].vy += impulse * ny; asteroids[j].vx -= impulse * nx; asteroids[j].vy -= impulse * ny; } } } } void checkShipAsteroidCollision() { if (ship.exploding) return; for (int i = 0; i < MAX_ASTEROIDS; ++i) { if (!asteroids[i].active) continue; float dx = ship.x - asteroids[i].x; float dy = ship.y - asteroids[i].y; float dist = sqrtf(dx * dx + dy * dy); float shipRadius = 8.0f; if (dist < asteroids[i].radius + shipRadius) { startShipExplosion(); createAsteroidExplosion(ship.x + dx * 0.5f, ship.y + dy * 0.5f); if (dist > 0) { float nx = -dx / dist, ny = -dy / dist; asteroids[i].vx += nx * 2.0f; asteroids[i].vy += ny * 2.0f; } break; } } } void checkProjectileAsteroidCollisions() { for (auto &p : projectiles) { if (!p.active) continue; for (int j = 0; j < MAX_ASTEROIDS; ++j) { if (!asteroids[j].active) continue; float dx = p.x - asteroids[j].x; float dy = p.y - asteroids[j].y; if (sqrtf(dx * dx + dy * dy) < asteroids[j].radius) { p.active = false; if (asteroids[j].radius >= 20.0f) ship.score += LARGE_ASTEROID_POINTS; else if (asteroids[j].radius >= MIN_ASTEROID_RADIUS) ship.score += MEDIUM_ASTEROID_POINTS; else ship.score += SMALL_ASTEROID_POINTS; splitAsteroid(j); break; } } } } void startShipExplosion() { if (ship.exploding) return; ship.exploding = true; ship.explosion_timer = EXPLOSION_DURATION; ship.lives--; ship.overheated = false; ship.overheat_cooldown = 0; ship.continuous_fire_timer = 0; ship.burst_count = 0; for (auto &p : particles) { p.x = ship.x + ((rand() % 20) - 10); p.y = ship.y + ((rand() % 20) - 10); float ang = (rand() % 628) / 100.0f; float spd = 0.5f + (rand() % 20) / 10.0f; p.vx = cosf(ang) * spd; p.vy = sinf(ang) * spd; p.lifetime = 30 + (rand() % 60); p.active = true; } } void createAsteroidExplosion(float x, float y) { int toCreate = 15 + (rand() % 10); int created = 0; for (auto &p : particles) { if (created >= toCreate) break; if (!p.active) { p.x = x + ((rand() % 16) - 8); p.y = y + ((rand() % 16) - 8); float ang = (rand() % 628) / 100.0f; float spd = 1.0f + (rand() % 20) / 10.0f; p.vx = cosf(ang) * spd; p.vy = sinf(ang) * spd; p.lifetime = 15 + (rand() % 25); p.active = true; created++; } } } void updateExplosion() { for (auto &p : particles) { if (!p.active) continue; p.x += p.vx; p.y += p.vy; p.vx *= 0.97f; p.vy *= 0.97f; p.lifetime--; if (p.lifetime <= 0) { p.active = false; continue; } if (p.x < 0) p.x += GAME_W; if (p.x >= GAME_W) p.x -= GAME_W; if (p.y < 0) p.y += GAME_H; if (p.y >= GAME_H) p.y -= GAME_H; } if (ship.exploding) { ship.explosion_timer--; if (ship.explosion_timer <= 0) ship.exploding = false; } } void handleDeathSequence() { if (wasExploding && !ship.exploding) { if (ship.lives > 0) { triggerRespawn(); } else { state = STATE_GAMEOVER; } wasExploding = false; } else if (ship.exploding) { wasExploding = true; } } void triggerRespawn() { ship.x = GAME_W / 2.0f; ship.y = GAME_H / 2.0f; ship.vx = 0; ship.vy = 0; ship.angle = 0; ship.exploding = false; ship.explosion_timer = 0; keyLeft = keyRight = keyThrust = keyFire = false; joyLeft = joyRight = joyThrust = joyFire = false; state = STATE_COUNTDOWN; countdownTimer = 0; countdownNumber = 3; } void updateCountdown() { countdownTimer++; if (countdownTimer >= countdownDuration) { countdownTimer = 0; countdownNumber--; if (countdownNumber < 0) { state = STATE_LAUNCH; launchTimer = 0; shipLaunchY = static_cast<float>(GAME_H); } } } void updateLaunch() { launchTimer++; if (launchTimer < launchDuration / 2) { shipLaunchY = GAME_H - (launchTimer * (GAME_H - ship.y) / (launchDuration / 2)); } else { shipLaunchY = ship.y; } if (launchTimer >= launchDuration) { state = STATE_PLAYING; if (controller.active()) { joyRestLX = controller.getAxis(SDL_CONTROLLER_AXIS_LEFTX); joyRestLY = controller.getAxis(SDL_CONTROLLER_AXIS_LEFTY); } } } void drawStars(float speedMul = 1.0f) { for (auto &s : stars) { s.y += s.speed * speedMul; if (s.y >= GAME_H) { s.x = static_cast<float>(rand() % GAME_W); s.y = 0; if (s.layer == 0) { s.speed = 0.15f + (rand() % 40) / 100.0f; s.size = 0.8f + (rand() % 12) / 20.0f; s.brightness = 0.25f + (rand() % 45) / 100.0f; } else if (s.layer == 1) { s.speed = 0.4f + (rand() % 80) / 100.0f; s.size = 1.2f + (rand() % 18) / 20.0f; s.brightness = 0.45f + (rand() % 55) / 100.0f; } else { s.speed = 0.8f + (rand() % 180) / 100.0f; s.size = 1.8f + (rand() % 25) / 20.0f; s.brightness = 0.65f + (rand() % 35) / 100.0f; } assignStarColor(s); s.twinklePhase = (rand() % 628) / 100.0f; } s.twinklePhase += s.twinkleSpeed * 0.016f; float twinkle = 0.7f + 0.3f * sinf(s.twinklePhase); float finalBrightness = s.brightness * twinkle; starSprite->setShaderParams( s.r * finalBrightness, s.g * finalBrightness, s.b * finalBrightness, finalBrightness ); int screenSize = static_cast<int>(s.size * std::min(sx(), sy())); screenSize = std::max(2, screenSize); starSprite->drawSpriteRect(toScreenX(s.x), toScreenY(s.y), screenSize, screenSize); } } void drawShipAt(float px, float py) { if (ship.exploding) { drawExplosion(); return; } float c = cosf(ship.angle), s = sinf(ship.angle); auto rot = [&](float lx, float ly, float &ox, float &oy) { ox = px + (lx * c - ly * s); oy = py + (lx * s + ly * c); }; float nose_x, nose_y, lw_x, lw_y, rw_x, rw_y; float le_x, le_y, re_x, re_y, ce_x, ce_y; float ld_x, ld_y, rd_x, rd_y; float cl_x, cl_y, cr_x, cr_y, cc_x, cc_y; rot(0, -15, nose_x, nose_y); rot(-12, 8, lw_x, lw_y); rot(12, 8, rw_x, rw_y); rot(-8, 12, le_x, le_y); rot(8, 12, re_x, re_y); rot(0, 10, ce_x, ce_y); rot(-6, 2, ld_x, ld_y); rot(6, 2, rd_x, rd_y); rot(-3, -8, cl_x, cl_y); rot(3, -8, cr_x, cr_y); rot(0, -5, cc_x, cc_y); if (ship.overheated) { setColor(1.0f, 0.0f, 0.0f, 1.0f); } else if (ship.continuous_fire_timer > 120) { int heat = ship.continuous_fire_timer - 120; setColor(1.0f, std::max(0.0f, 1.0f - heat / 64.0f), 0.0f, 1.0f); } else { setColor(1.0f, 1.0f, 1.0f, 1.0f); } drawLine(nose_x, nose_y, lw_x, lw_y); drawLine(nose_x, nose_y, rw_x, rw_y); drawLine(lw_x, lw_y, rw_x, rw_y); drawLine(lw_x, lw_y, le_x, le_y); drawLine(rw_x, rw_y, re_x, re_y); drawLine(le_x, le_y, ce_x, ce_y); drawLine(re_x, re_y, ce_x, ce_y); if (ship.overheated) setColor(0.78f, 0.0f, 0.0f, 1.0f); else if (ship.continuous_fire_timer > 120) setColor(0.78f, std::max(0.0f, 0.78f - (ship.continuous_fire_timer - 120) / 80.0f), 0.0f, 1.0f); else setColor(0.78f, 0.78f, 0.78f, 1.0f); drawLine(nose_x, nose_y, ld_x, ld_y); drawLine(nose_x, nose_y, rd_x, rd_y); drawLine(ld_x, ld_y, rd_x, rd_y); drawLine(ld_x, ld_y, lw_x, lw_y); drawLine(rd_x, rd_y, rw_x, rw_y); if (ship.overheated) setColor(0.0f, 0.5f, 0.5f, 1.0f); else setColor(0.0f, 1.0f, 1.0f, 1.0f); drawLine(cl_x, cl_y, cr_x, cr_y); drawLine(cl_x, cl_y, cc_x, cc_y); drawLine(cr_x, cr_y, cc_x, cc_y); setColor(0.59f, 0.59f, 0.59f, 1.0f); drawLine(cc_x, cc_y, ce_x, ce_y); if (keyThrust || joyThrust) { float f1x, f1y, f2x, f2y, f3x, f3y; rot(-6, 18, f1x, f1y); rot(0, 22, f2x, f2y); rot(6, 18, f3x, f3y); setColor(1.0f, 0.39f, 0.0f, 1.0f); drawLine(le_x, le_y, f1x, f1y); drawLine(ce_x, ce_y, f2x, f2y); drawLine(re_x, re_y, f3x, f3y); float ifx, ify; rot(0, 20, ifx, ify); setColor(1.0f, 1.0f, 0.0f, 1.0f); drawLine(ce_x, ce_y, ifx, ify); } } void drawProjectiles() { setColor(1.0f, 0.0f, 0.0f, 1.0f); for (auto &p : projectiles) { if (!p.active) continue; float endX = p.x - p.vx * 0.5f; float endY = p.y - p.vy * 0.5f; drawLine(p.x, p.y, endX, endY); } } void drawAsteroids() { for (int i = 0; i < MAX_ASTEROIDS; ++i) { if (!asteroids[i].active) continue; auto &a = asteroids[i]; float cosR = cosf(a.rotation_angle); float sinR = sinf(a.rotation_angle); float rv[ASTEROID_VERTICES][2]; for (int j = 0; j < ASTEROID_VERTICES; ++j) { rv[j][0] = a.vertices[j][0] * cosR - a.vertices[j][1] * sinR; rv[j][1] = a.vertices[j][0] * sinR + a.vertices[j][1] * cosR; } setColor(1.0f, 1.0f, 1.0f, 1.0f); for (int j = 0; j < ASTEROID_VERTICES; ++j) { int next = (j + 1) % ASTEROID_VERTICES; drawLine(a.x + rv[j][0], a.y + rv[j][1], a.x + rv[next][0], a.y + rv[next][1]); } float iv[ASTEROID_VERTICES][2]; for (int j = 0; j < ASTEROID_VERTICES; ++j) { iv[j][0] = rv[j][0] * 0.6f; iv[j][1] = rv[j][1] * 0.6f; } setColor(0.59f, 0.59f, 0.59f, 1.0f); for (int j = 0; j < ASTEROID_VERTICES; ++j) { int next = (j + 1) % ASTEROID_VERTICES; drawLine(a.x + iv[j][0], a.y + iv[j][1], a.x + iv[next][0], a.y + iv[next][1]); } setColor(0.39f, 0.39f, 0.39f, 1.0f); for (int j = 0; j < ASTEROID_VERTICES; j += 2) { drawLine(a.x + rv[j][0], a.y + rv[j][1], a.x + iv[j][0], a.y + iv[j][1]); } setColor(0.71f, 0.71f, 0.71f, 1.0f); for (int j = 0; j < ASTEROID_VERTICES; j += 2) { int next = (j + 1) % ASTEROID_VERTICES; drawLine(a.x + rv[j][0], a.y + rv[j][1], a.x + iv[next][0], a.y + iv[next][1]); } } } void drawExplosion() { for (auto &p : particles) { if (!p.active) continue; float lp = static_cast<float>(p.lifetime) / 50.0f; if (lp > 0.7f) setColor(1.0f, 1.0f, 1.0f, 1.0f); else if (lp > 0.5f) setColor(1.0f, 1.0f, 0.0f, 1.0f); else if (lp > 0.2f) setColor(1.0f, 0.5f, 0.0f, 1.0f); else setColor(1.0f, 0.0f, 0.0f, 1.0f); drawRect(p.x, p.y, 2, 2); } } void drawHUD() { std::string buf = std::format("Score: {}", ship.score); printText(buf.c_str(), toScreenX(10), toScreenY(10), {255, 255, 255, 255}); buf = std::format("Lives: {}", ship.lives); printText(buf.c_str(), toScreenX(10), toScreenY(30), {255, 255, 255, 255}); } void drawCountdown() { drawStars(0.3f); if (countdownNumber > 0) { std::string buf = std::format("{}", countdownNumber); int textW, textH; if (getTextDimensions(buf.c_str(), textW, textH)) { int centerX = w / 2; int centerY = h / 2; int textX = centerX - textW / 2; int textY = centerY - textH / 2; printText(buf.c_str(), textX, textY, {255, 255, 0, 255}); if ((countdownTimer / 10) % 2) { setColor(1.0f, 1.0f, 0.0f, 0.5f); int padding = std::max(15, static_cast<int>(15 * std::min(sx(), sy()))); int rectX = textX - padding; int rectY = textY - padding; int rectW = textW + padding * 2; int rectH = textH + padding * 2; int thickness = std::max(3, static_cast<int>(3 * std::min(sx(), sy()))); pixel->drawSpriteRect(rectX, rectY, rectW, thickness); pixel->drawSpriteRect(rectX, rectY + rectH - thickness, rectW, thickness); pixel->drawSpriteRect(rectX, rectY, thickness, rectH); pixel->drawSpriteRect(rectX + rectW - thickness, rectY, thickness, rectH); } } } else { int textW, textH; if (getTextDimensions("LAUNCH!", textW, textH)) { printText("LAUNCH!", w/2 - textW/2, h/2 - textH/2, {0, 255, 0, 255}); } else { printText("LAUNCH!", toScreenX(270), toScreenY(160), {0, 255, 0, 255}); } } int textW, textH; if (getTextDimensions("PREPARE FOR MISSION", textW, textH)) { printText("PREPARE FOR MISSION", w/2 - textW/2, h/2 + textH * 2, {255, 255, 255, 255}); } else { printText("PREPARE FOR MISSION", toScreenX(220), toScreenY(195), {255, 255, 255, 255}); } std::string lbuf = std::format("Lives: {}", ship.lives); std::string sbuf = std::format("Score: {}", ship.score); if (getTextDimensions(lbuf.c_str(), textW, textH)) { printText(lbuf.c_str(), w/2 - textW/2, h/2 + textH * 3 + 10, {255, 255, 255, 255}); if (getTextDimensions(sbuf.c_str(), textW, textH)) { printText(sbuf.c_str(), w/2 - textW/2, h/2 + textH * 4 + 15, {255, 255, 255, 255}); } } else { printText(lbuf.c_str(), toScreenX(260), toScreenY(215), {255, 255, 255, 255}); printText(sbuf.c_str(), toScreenX(260), toScreenY(235), {255, 255, 255, 255}); } } void drawLaunch() { drawStars(0.5f); if (launchTimer > launchDuration / 4) drawAsteroids(); float tempY = ship.y; ship.y = shipLaunchY; drawShipAt(ship.x, shipLaunchY); ship.y = tempY; if (launchTimer < launchDuration / 2) { setColor(1.0f, 0.39f, 0.0f, 1.0f); for (int i = 0; i < 12; ++i) { float tx = ship.x + (rand() % 8) - 4; float ty = shipLaunchY + 15 + i * 2; if (ty < GAME_H) drawPixel(tx, ty); } } if (launchTimer >= launchDuration / 2) { int textW, textH; if (getTextDimensions("MISSION START!", textW, textH)) { printText("MISSION START!", w/2 - textW/2, h/2 - textH/2, {0, 255, 255, 255}); } else { printText("MISSION START!", toScreenX(240), toScreenY(170), {0, 255, 255, 255}); } } } void drawGame() { drawStars(1.0f); drawShipAt(ship.x, ship.y); drawProjectiles(); drawAsteroids(); drawExplosion(); drawHUD(); } void drawGameOver() { drawStars(0.2f); printText("GAME OVER", toScreenX(255), toScreenY(130), {255, 0, 0, 255}); std::string buf = std::format("Final Score: {}", ship.score); printText(buf.c_str(), toScreenX(235), toScreenY(165), {255, 255, 255, 255}); printText("Press SPACE to begin", toScreenX(215), toScreenY(195), {255, 255, 0, 255}); } }; int main(int argc, char **argv) { Arguments args = proc_args(argc, argv); try { SpaceRoxWindow window(args.path, args.width, args.height, true); window.initVulkan(); window.loop(); window.cleanup(); } catch (mx::Exception &e) { SDL_Log("mx: Exception: %s\n", e.text().c_str()); return EXIT_FAILURE; } return EXIT_SUCCESS; }