MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
space.cpp
Go to the documentation of this file.
1
2#include "mxvk/argz.hpp"
3#include "mxvk/mxvk.hpp"
5
6#include <SDL3/SDL.h>
7
8#include <algorithm>
9#include <cmath>
10#include <cstdint>
11#include <cstdlib>
12#include <cstring>
13#include <ctime>
14#include <format>
15#include <iostream>
16
17#ifndef M_PI
18#define M_PI 3.14159265358979323846
19#endif
20
21#ifndef asteroids_ASSET_DIR
22#define asteroids_ASSET_DIR "."
23#endif
24
25constexpr int GAME_W = 640;
26constexpr int GAME_H = 360;
27constexpr int STAR_COUNT = 800;
28constexpr int MAX_PROJECTILES = 50;
29constexpr int MAX_ASTEROIDS = 30;
30constexpr int MAX_PARTICLES = 40;
31constexpr int ASTEROID_VERTICES = 8;
32constexpr float PROJECTILE_SPEED = 5.0f;
33constexpr int PROJECTILE_LIFETIME = 60;
34constexpr int FIRE_COOLDOWN = 5;
35constexpr int SHOTS_PER_BURST = 5;
36constexpr int FIRE_DELAY = 3;
37constexpr int EXPLOSION_DURATION = 90;
38constexpr float MIN_ASTEROID_RADIUS = 8.0f;
39constexpr int LARGE_ASTEROID_POINTS = 20;
40constexpr int MEDIUM_ASTEROID_POINTS = 50;
41constexpr int SMALL_ASTEROID_POINTS = 100;
42
57
58struct Projectile {
59 float x, y;
60 float vx, vy;
61 float lifetime;
62 bool active;
63};
64
65struct Asteroid {
66 float x, y;
67 float vx, vy;
68 float radius;
69 bool active;
73};
74
75struct Particle {
76 float x, y;
77 float vx, vy;
79 bool active;
80};
81
82struct Star {
83 float x, y, speed;
84 float size;
86 int layer;
87 float r, g, b;
90};
91
98
99template <typename Func>
100void bresenhamLine(int x0, int y0, int x1, int y1, Func plot) {
101 int dx = abs(x1 - x0);
102 int dy = abs(y1 - y0);
103 int sx = (x0 < x1) ? 1 : -1;
104 int sy = (y0 < y1) ? 1 : -1;
105 int err = dx - dy;
106 while (true) {
107 plot(x0, y0);
108 if (x0 == x1 && y0 == y1)
109 break;
110 int e2 = 2 * err;
111 if (e2 > -dy) {
112 err -= dy;
113 x0 += sx;
114 }
115 if (e2 < dx) {
116 err += dx;
117 y0 += sy;
118 }
119 }
120}
121
123 public:
124 SpaceRoxWindow(const std::string &path, int wx, int wy, bool full, bool enable_vsync)
125 : mxvk::VK_Window("-[ SpaceRox - MXVK ]-", wx, wy, full, MXVK_VALIDATION, enable_vsync),
126 current_path((path.empty() || path == ".") ? std::string(asteroids_ASSET_DIR) : path),
127 w(wx),
128 h(wy) {
129 srand(static_cast<unsigned>(time(nullptr)));
130 }
131 virtual ~SpaceRoxWindow() {
132 if (gamepad != nullptr) {
133 SDL_CloseGamepad(gamepad);
134 gamepad = nullptr;
135 }
136 }
137
138 void proc() override {
139 initializeResources();
140 if (!resources_initialized || pixel == nullptr || starSprite == nullptr) {
141 return;
142 }
143
144 refreshWindowSize();
145
146 Uint32 currentTime = SDL_GetTicks();
147 Uint32 frameTime = currentTime - lastFrameTime;
148 if (frameTime < targetFrameTime) {
149 SDL_Delay(targetFrameTime - frameTime);
150 currentTime = SDL_GetTicks();
151 frameTime = currentTime - lastFrameTime;
152 }
153
154 if (lastFrameTime > 0) {
155 float deltaTime = frameTime / 1000.0f;
156 globalTime += deltaTime;
157 }
158 lastFrameTime = currentTime;
159
160 updateFontSize();
161
163
164 const bool *keys = SDL_GetKeyboardState(nullptr);
165
166 switch (state) {
167 case STATE_COUNTDOWN:
168 updateCountdown();
169 drawCountdown();
170 break;
171 case STATE_LAUNCH:
172 updateLaunch();
173 drawLaunch();
174 break;
175 case STATE_PLAYING:
176
177 if ((keys[SDL_SCANCODE_SPACE] || joyFire) && canFire()) {
178 fireProjectile(ship.x, ship.y, ship.angle);
179 }
180 updateShip();
181 updateFireTimer(keys);
182 updateProjectiles();
183 updateAsteroids();
184 updateExplosion();
185 checkAsteroidCollisions();
186 checkShipAsteroidCollision();
187 checkProjectileAsteroidCollisions();
188 checkAndSpawnAsteroids();
189 handleDeathSequence();
190 drawGame();
191 break;
192 case STATE_GAMEOVER:
193 drawGameOver();
194 break;
195 }
196 }
197
198 void event(SDL_Event &e) override {
199 switch (e.type) {
200 case SDL_EVENT_QUIT:
201 exit();
202 break;
203 case SDL_EVENT_KEY_DOWN:
204 if (e.key.key == SDLK_ESCAPE) {
205 exit();
206 }
207 if (e.key.key == SDLK_SPACE && state == STATE_GAMEOVER) {
208 restartGame();
209 }
210 if (state == STATE_PLAYING) {
211 if (e.key.key == SDLK_LEFT)
212 keyLeft = true;
213 if (e.key.key == SDLK_RIGHT)
214 keyRight = true;
215 if (e.key.key == SDLK_UP)
216 keyThrust = true;
217 }
218 break;
219 case SDL_EVENT_KEY_UP:
220 if (state == STATE_PLAYING) {
221 if (e.key.key == SDLK_LEFT)
222 keyLeft = false;
223 if (e.key.key == SDLK_RIGHT)
224 keyRight = false;
225 if (e.key.key == SDLK_UP)
226 keyThrust = false;
227 }
228 break;
229 case SDL_EVENT_GAMEPAD_ADDED:
230 openGamepad(e.gdevice.which);
231 joyRestLX = 0;
232 joyRestLY = 0;
233 break;
234 case SDL_EVENT_GAMEPAD_REMOVED:
235 if (gamepad != nullptr && e.gdevice.which == gamepadId) {
236 SDL_CloseGamepad(gamepad);
237 gamepad = nullptr;
238 gamepadId = 0;
239 joyLeft = joyRight = joyThrust = joyFire = false;
240 joyRestLX = 0;
241 joyRestLY = 0;
242 joyCenterStable = 0;
243 }
244 break;
245 case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
246 handleControllerButton(e.gbutton.button);
247 break;
248 case SDL_EVENT_GAMEPAD_BUTTON_UP:
249 handleControllerButtonUp(e.gbutton.button);
250 break;
251 }
252 }
253
254 void handleControllerButton(Uint8 button) {
255 switch (state) {
256 case STATE_GAMEOVER:
257 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
258 restartGame();
259 }
260 break;
261 case STATE_PLAYING:
262 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
263 joyFire = true;
264 } else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
265
266 } else if (button == SDL_GAMEPAD_BUTTON_START) {
267 }
268 break;
269 default:
270 break;
271 }
272 }
273
274 void handleControllerButtonUp(Uint8 button) {
275 if (state == STATE_PLAYING) {
276 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
277 joyFire = false;
278 }
279 }
280 }
281
283 if (gamepad == nullptr) {
284 joyLeft = joyRight = joyThrust = joyFire = false;
285 joyCenterStable = 0;
286 return;
287 }
288
289 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_START)) {
290 exit();
291 return;
292 }
293
294 if (state != STATE_PLAYING || ship.exploding) {
295 joyLeft = joyRight = joyThrust = joyFire = false;
296 return;
297 }
298
299 Sint16 rawLX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
300 Sint16 rawLY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
301
302 if (abs(rawLX) < JOY_RECENTER_ZONE && abs(rawLY) < JOY_RECENTER_ZONE) {
303 if (joyCenterStable < JOY_RECENTER_FRAMES) {
304 joyCenterStable++;
305 }
306 if (joyCenterStable >= JOY_RECENTER_FRAMES) {
307 joyRestLX = rawLX;
308 joyRestLY = rawLY;
309 }
310 } else {
311 joyCenterStable = 0;
312 }
313
314 int lx = static_cast<int>(rawLX) - static_cast<int>(joyRestLX);
315 int ly = static_cast<int>(rawLY) - static_cast<int>(joyRestLY);
316 joyLeft = (lx < -JOY_ROTATE_ZONE);
317 joyRight = (lx > JOY_ROTATE_ZONE);
318 joyThrust = (ly < -JOY_THRUST_ZONE);
319 Sint16 rt = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
320 joyFire = (rt > JOY_THRUST_ZONE) || SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_SOUTH);
321 }
322
323 private:
324 std::string current_path;
325 bool resources_initialized = false;
326 int w = GAME_W;
327 int h = GAME_H;
328
329 mxvk::VK_Sprite *pixel = nullptr;
330 mxvk::VK_Sprite *starSprite = nullptr;
331
332 Uint32 lastFrameTime = 0;
333 static constexpr Uint32 targetFrameTime = 1000 / 60;
334 float globalTime = 0.0f;
335
336 Ship ship{};
337 Projectile projectiles[MAX_PROJECTILES]{};
338 Asteroid asteroids[MAX_ASTEROIDS]{};
339 Particle particles[MAX_PARTICLES]{};
340 Star stars[STAR_COUNT * 3]{};
341 bool starsInit = false;
342
344 bool keyLeft = false, keyRight = false, keyThrust = false;
345 bool keyFire = false;
346 bool joyLeft = false, joyRight = false, joyThrust = false;
347 bool joyFire = false;
348 bool wasExploding = false;
349
350 SDL_Gamepad *gamepad = nullptr;
351 SDL_JoystickID gamepadId = 0;
352 static constexpr Sint16 JOY_ROTATE_ZONE = 16000;
353 static constexpr Sint16 JOY_THRUST_ZONE = 12000;
354 static constexpr Sint16 JOY_RECENTER_ZONE = 8000;
355 static constexpr int JOY_RECENTER_FRAMES = 12;
356 Sint16 joyRestLX = 0, joyRestLY = 0;
357 int joyCenterStable = 0;
358
359 Uint32 countdownTimer = 0;
360 Uint32 countdownDuration = 1000;
361 Uint32 countdownStartMs = 0;
362 int countdownNumber = 3;
363 Uint32 launchTimer = 0;
364 Uint32 launchDuration = 1000;
365 Uint32 launchStartMs = 0;
366 float shipLaunchY = 0;
367
368 int lastFontSize = 0;
369
370 float sx() const { return static_cast<float>(w) / GAME_W; }
371 float sy() const { return static_cast<float>(h) / GAME_H; }
372
373 int toScreenX(float gx) const { return static_cast<int>(gx * sx()); }
374 int toScreenY(float gy) const { return static_cast<int>(gy * sy()); }
375 int toScreenW(float gw) const { return std::max(1, static_cast<int>(gw * sx())); }
376 int toScreenH(float gh) const { return std::max(1, static_cast<int>(gh * sy())); }
377
378 void refreshWindowSize() {
379 if (window != nullptr) {
380 SDL_GetWindowSizeInPixels(window.get(), &w, &h);
381 }
382 }
383
384 bool openGamepad(SDL_JoystickID id) {
385 if (gamepad != nullptr && gamepadId == id) {
386 return true;
387 }
388 if (gamepad != nullptr) {
389 SDL_CloseGamepad(gamepad);
390 gamepad = nullptr;
391 gamepadId = 0;
392 }
393 gamepad = SDL_OpenGamepad(id);
394 if (gamepad == nullptr) {
395 return false;
396 }
397 gamepadId = id;
398 return true;
399 }
400
401 void tryOpenFirstGamepad() {
402 if (gamepad != nullptr) {
403 return;
404 }
405 int count = 0;
406 SDL_JoystickID *ids = SDL_GetGamepads(&count);
407 if (ids == nullptr || count <= 0) {
408 if (ids != nullptr) {
409 SDL_free(ids);
410 }
411 return;
412 }
413 openGamepad(ids[0]);
414 SDL_free(ids);
415 }
416
417 void initializeResources() {
418 if (resources_initialized) {
419 return;
420 }
421
422 tryOpenFirstGamepad();
423
424 std::array<std::uint8_t, 4 * 4 * 4> starPixels{};
425 for (int y = 0; y < 4; y++) {
426 for (int x = 0; x < 4; x++) {
427 float dx = (x - 1.5f) / 1.5f;
428 float dy = (y - 1.5f) / 1.5f;
429 float dist = sqrtf(dx * dx + dy * dy);
430 std::uint8_t alpha = (dist < 1.0f) ? static_cast<std::uint8_t>(255.0f * (1.0f - dist)) : 0;
431 const int idx = (y * 4 + x) * 4;
432 starPixels[idx + 0] = 255;
433 starPixels[idx + 1] = 255;
434 starPixels[idx + 2] = 255;
435 starPixels[idx + 3] = alpha;
436 }
437 }
438
439 starSprite = createSprite(
440 4,
441 4,
442 current_path + "/data/sprite_vert.spv",
443 current_path + "/data/sprite_frag.spv");
444 starSprite->updateTexture(starPixels.data(), 4, 4, 4 * 4);
445
446 const std::array<std::uint8_t, 2 * 2 * 4> whitePixels{
447 255,
448 255,
449 255,
450 255,
451 255,
452 255,
453 255,
454 255,
455 255,
456 255,
457 255,
458 255,
459 255,
460 255,
461 255,
462 255,
463 };
464
465 pixel = createSprite(
466 2,
467 2,
468 current_path + "/data/sprite_vert.spv",
469 current_path + "/data/solid_frag.spv");
470 pixel->updateTexture(whitePixels.data(), 2, 2, 2 * 4);
471
472 initGame();
473 resources_initialized = true;
474 }
475
476 void setColor(float r, float g, float b, float a = 1.0f) {
477 pixel->setShaderParams(r, g, b, a);
478 }
479
480 void drawPixel(float gx, float gy) {
481 int psz = std::max(1, static_cast<int>(2.0f * std::min(sx(), sy())));
482 pixel->drawSpriteRect(toScreenX(gx), toScreenY(gy), psz, psz);
483 }
484
485 void drawRect(float gx, float gy, float gw, float gh) {
486 pixel->drawSpriteRect(toScreenX(gx), toScreenY(gy), toScreenW(gw), toScreenH(gh));
487 }
488
489 void drawLine(float x0f, float y0f, float x1f, float y1f) {
490 int x0 = toScreenX(x0f), y0 = toScreenY(y0f);
491 int x1 = toScreenX(x1f), y1 = toScreenY(y1f);
492 int psz = std::max(1, static_cast<int>(1.5f * std::min(sx(), sy())));
493 bresenhamLine(x0, y0, x1, y1, [&](int px, int py) {
494 pixel->drawSpriteRect(px, py, psz, psz);
495 });
496 }
497
498 void drawPoint(int screenX, int screenY, int size = 2) {
499 pixel->drawSpriteRect(screenX, screenY, size, size);
500 }
501
502 void updateFontSize() {
503 int fontSize = static_cast<int>(20.0f * (static_cast<float>(h) / 480.0f));
504 fontSize = std::max(14, std::min(128, fontSize));
505 if (fontSize != lastFontSize) {
506 lastFontSize = fontSize;
507 setFont(current_path + "/data/font.ttf", fontSize);
509 }
510 }
511
512 void initGame() {
513 initShip();
514 initProjectiles();
515 initAsteroids();
516 initStars();
517 state = STATE_COUNTDOWN;
518 countdownTimer = 0;
519 countdownNumber = 3;
520 countdownStartMs = SDL_GetTicks();
521 launchStartMs = 0;
522 wasExploding = false;
523 }
524
525 void restartGame() {
526 initGame();
527 }
528
529 void initStars() {
530 int starIndex = 0;
531
532 for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) {
533 stars[starIndex].x = static_cast<float>(rand() % GAME_W);
534 stars[starIndex].y = static_cast<float>(rand() % GAME_H);
535 stars[starIndex].speed = 0.15f + (rand() % 40) / 100.0f;
536 stars[starIndex].size = 0.8f + (rand() % 12) / 20.0f;
537 stars[starIndex].brightness = 0.25f + (rand() % 45) / 100.0f;
538 stars[starIndex].layer = 0;
539 assignStarColor(stars[starIndex]);
540 stars[starIndex].twinklePhase = (rand() % 628) / 100.0f;
541 stars[starIndex].twinkleSpeed = 0.4f + (rand() % 150) / 100.0f;
542 }
543
544 for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) {
545 stars[starIndex].x = static_cast<float>(rand() % GAME_W);
546 stars[starIndex].y = static_cast<float>(rand() % GAME_H);
547 stars[starIndex].speed = 0.4f + (rand() % 80) / 100.0f;
548 stars[starIndex].size = 1.2f + (rand() % 18) / 20.0f;
549 stars[starIndex].brightness = 0.45f + (rand() % 55) / 100.0f;
550 stars[starIndex].layer = 1;
551 assignStarColor(stars[starIndex]);
552 stars[starIndex].twinklePhase = (rand() % 628) / 100.0f;
553 stars[starIndex].twinkleSpeed = 0.8f + (rand() % 250) / 100.0f;
554 }
555
556 for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) {
557 stars[starIndex].x = static_cast<float>(rand() % GAME_W);
558 stars[starIndex].y = static_cast<float>(rand() % GAME_H);
559 stars[starIndex].speed = 0.8f + (rand() % 180) / 100.0f;
560 stars[starIndex].size = 1.8f + (rand() % 25) / 20.0f;
561 stars[starIndex].brightness = 0.65f + (rand() % 35) / 100.0f;
562 stars[starIndex].layer = 2;
563 assignStarColor(stars[starIndex]);
564 stars[starIndex].twinklePhase = (rand() % 628) / 100.0f;
565 stars[starIndex].twinkleSpeed = 1.2f + (rand() % 350) / 100.0f;
566 }
567 starsInit = true;
568 }
569
570 void assignStarColor(Star &s) {
571 float roll = (rand() % 1000) / 1000.0f;
572 if (roll < 0.35f) {
573
574 s.r = 0.85f + (rand() % 15) / 100.0f;
575 s.g = 0.90f + (rand() % 10) / 100.0f;
576 s.b = 1.0f;
577 } else if (roll < 0.55f) {
578
579 s.r = 0.5f + (rand() % 20) / 100.0f;
580 s.g = 0.75f + (rand() % 15) / 100.0f;
581 s.b = 1.0f;
582 s.size *= 1.15f;
583 s.brightness *= 1.1f;
584 } else if (roll < 0.70f) {
585
586 s.r = 1.0f;
587 s.g = 0.95f + (rand() % 5) / 100.0f;
588 s.b = 0.6f + (rand() % 30) / 100.0f;
589 s.size *= 1.05f;
590 } else if (roll < 0.82f) {
591
592 s.r = 1.0f;
593 s.g = 0.6f + (rand() % 20) / 100.0f;
594 s.b = 0.2f + (rand() % 20) / 100.0f;
595 s.size *= 1.1f;
596 } else if (roll < 0.90f) {
597
598 s.r = 1.0f;
599 s.g = 0.3f + (rand() % 20) / 100.0f;
600 s.b = 0.2f + (rand() % 15) / 100.0f;
601 s.size *= 1.08f;
602 } else if (roll < 0.96f) {
603
604 s.r = 1.0f;
605 s.g = 1.0f;
606 s.b = 1.0f;
607 s.size *= 1.4f;
608 s.brightness *= 1.4f;
609 } else {
610
611 s.r = 0.9f;
612 s.g = 0.95f;
613 s.b = 1.0f;
614 s.size *= 1.6f;
615 s.brightness *= 1.5f;
616 }
617 }
618
619 void initShip() {
620 ship.x = GAME_W / 2.0f;
621 ship.y = GAME_H / 2.0f;
622 ship.vx = 0;
623 ship.vy = 0;
624 ship.angle = 0;
625 ship.lives = 3;
626 ship.fire_cooldown = 0;
627 ship.burst_count = 0;
628 ship.exploding = false;
629 ship.explosion_timer = 0;
630 ship.score = 0;
631 ship.continuous_fire_timer = 0;
632 ship.overheated = false;
633 ship.overheat_cooldown = 0;
634 for (auto &p : particles)
635 p.active = false;
636 }
637
638 void initProjectiles() {
639 for (auto &p : projectiles)
640 p.active = false;
641 }
642
643 void initAsteroids() {
644 for (auto &a : asteroids)
645 a.active = false;
646 for (int i = 0; i < 4; ++i) {
647 float x, y;
648 do {
649 x = static_cast<float>(rand() % GAME_W);
650 y = static_cast<float>(rand() % GAME_H);
651 } while (hypotf(x - ship.x, y - ship.y) < 100);
652 float vx = ((rand() % 100) - 50) / 50.0f;
653 float vy = ((rand() % 100) - 50) / 50.0f;
654 float radius = 20.0f + (rand() % 20);
655 spawnAsteroid(x, y, vx, vy, radius);
656 }
657 }
658
659 void updateShip() {
660 if (ship.exploding)
661 return;
662 if (keyLeft || joyLeft)
663 ship.angle -= 0.15f;
664 if (keyRight || joyRight)
665 ship.angle += 0.15f;
666 if (keyThrust || joyThrust) {
667 float thrust = 0.2f;
668 ship.vx += sinf(ship.angle) * thrust;
669 ship.vy += -cosf(ship.angle) * thrust;
670 float spd = sqrtf(ship.vx * ship.vx + ship.vy * ship.vy);
671 float maxSpd = 4.0f;
672 if (spd > maxSpd) {
673 ship.vx = (ship.vx / spd) * maxSpd;
674 ship.vy = (ship.vy / spd) * maxSpd;
675 }
676 }
677 ship.vx *= 0.98f;
678 ship.vy *= 0.98f;
679 ship.x += ship.vx;
680 ship.y += ship.vy;
681
682 if (ship.x < 0)
683 ship.x += GAME_W;
684 if (ship.x >= GAME_W)
685 ship.x -= GAME_W;
686 if (ship.y < 0)
687 ship.y += GAME_H;
688 if (ship.y >= GAME_H)
689 ship.y -= GAME_H;
690 if (ship.fire_cooldown > 0)
691 ship.fire_cooldown--;
692 }
693
694 void updateFireTimer(const bool *keys) {
695 bool firing = keys[SDL_SCANCODE_SPACE] || joyFire;
696 if (firing && !ship.overheated) {
697 ship.continuous_fire_timer++;
698 ship.overheat_cooldown = 0;
699 if (ship.continuous_fire_timer >= 180) {
700 ship.overheated = true;
701 ship.overheat_cooldown = 0;
702 ship.continuous_fire_timer = 0;
703 ship.burst_count = 0;
704 }
705 } else if (firing && ship.overheated) {
706 ship.overheat_cooldown = 0;
707 } else {
708 if (ship.overheated) {
709 ship.overheat_cooldown++;
710 if (ship.overheat_cooldown >= 180) {
711 ship.overheated = false;
712 ship.overheat_cooldown = 0;
713 ship.continuous_fire_timer = 0;
714 }
715 } else {
716 if (ship.continuous_fire_timer > 0)
717 ship.continuous_fire_timer--;
718 }
719 }
720 }
721
722 bool canFire() {
723 if (ship.overheated)
724 return false;
725 if (ship.fire_cooldown <= 0) {
726 if (ship.burst_count < SHOTS_PER_BURST) {
727 ship.fire_cooldown = FIRE_DELAY;
728 ship.burst_count++;
729 return true;
730 } else {
731 ship.fire_cooldown = FIRE_COOLDOWN;
732 ship.burst_count = 0;
733 return false;
734 }
735 }
736 return false;
737 }
738
739 void fireProjectile(float x, float y, float angle) {
740 for (auto &p : projectiles) {
741 if (!p.active) {
742 float c = cosf(angle), s = sinf(angle);
743 float lx = 0, ly = -12;
744 p.x = x + (lx * c - ly * s);
745 p.y = y + (lx * s + ly * c);
746 p.vx = sinf(angle) * PROJECTILE_SPEED;
747 p.vy = -cosf(angle) * PROJECTILE_SPEED;
748 p.lifetime = PROJECTILE_LIFETIME;
749 p.active = true;
750 break;
751 }
752 }
753 }
754
755 void updateProjectiles() {
756 for (auto &p : projectiles) {
757 if (!p.active)
758 continue;
759 p.x += p.vx;
760 p.y += p.vy;
761 if (p.x < 0 || p.x >= GAME_W || p.y < 0 || p.y >= GAME_H) {
762 p.active = false;
763 continue;
764 }
765 p.lifetime--;
766 if (p.lifetime <= 0)
767 p.active = false;
768 }
769 }
770
771 void spawnAsteroid(float x, float y, float vx, float vy, float radius) {
772 for (auto &a : asteroids) {
773 if (!a.active) {
774 a.x = x;
775 a.y = y;
776 a.vx = vx;
777 a.vy = vy;
778 a.radius = radius;
779 a.active = true;
780 a.rotation_angle = 0;
781 a.rotation_speed = ((rand() % 100) - 50) / 1000.0f;
782 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
783 float ang = static_cast<float>(j) / ASTEROID_VERTICES * 2 * M_PI;
784 float vr = radius * (0.7f + (rand() % 30) / 100.0f);
785 a.vertices[j][0] = cosf(ang) * vr;
786 a.vertices[j][1] = sinf(ang) * vr;
787 }
788 break;
789 }
790 }
791 }
792
793 void updateAsteroids() {
794 for (auto &a : asteroids) {
795 if (!a.active)
796 continue;
797 a.x += a.vx;
798 a.y += a.vy;
799 a.rotation_angle += a.rotation_speed;
800 if (a.x < 0)
801 a.x += GAME_W;
802 if (a.x >= GAME_W)
803 a.x -= GAME_W;
804 if (a.y < 0)
805 a.y += GAME_H;
806 if (a.y >= GAME_H)
807 a.y -= GAME_H;
808 }
809 }
810
811 void splitAsteroid(int idx) {
812 float x = asteroids[idx].x;
813 float y = asteroids[idx].y;
814 float r = asteroids[idx].radius / 2.0f;
815 if (r < MIN_ASTEROID_RADIUS) {
816 createAsteroidExplosion(x, y);
817 ship.score += SMALL_ASTEROID_POINTS;
818 asteroids[idx].active = false;
819 return;
820 }
821 int splits = 2 + (rand() % 2);
822 for (int i = 0; i < splits; ++i) {
823 float ang = (rand() % 628) / 100.0f;
824 float spd = 0.5f + (rand() % 15) / 10.0f;
825 float nvx = cosf(ang) * spd + asteroids[idx].vx * 0.3f;
826 float nvy = sinf(ang) * spd + asteroids[idx].vy * 0.3f;
827 float off = r * 0.5f;
828 spawnAsteroid(x + cosf(ang) * off, y + sinf(ang) * off, nvx, nvy, r);
829 }
830 if (r >= 25.0f)
831 ship.score += LARGE_ASTEROID_POINTS;
832 else
833 ship.score += MEDIUM_ASTEROID_POINTS;
834 createAsteroidExplosion(x, y);
835 asteroids[idx].active = false;
836 }
837
838 void checkAndSpawnAsteroids() {
839 int active = 0;
840 for (auto &a : asteroids)
841 if (a.active)
842 active++;
843 int minAsteroids = 3;
844 if (active < minAsteroids) {
845 for (int s = 0; s < minAsteroids - active; ++s) {
846 float x, y;
847 int attempts = 0;
848 do {
849 if (rand() % 2) {
850 x = (rand() % 2) ? -30.0f : GAME_W + 30.0f;
851 y = static_cast<float>(rand() % GAME_H);
852 } else {
853 x = static_cast<float>(rand() % GAME_W);
854 y = (rand() % 2) ? -30.0f : GAME_H + 30.0f;
855 }
856 attempts++;
857 } while (hypotf(x - ship.x, y - ship.y) < 100.0f && attempts < 10);
858 float vx = ((rand() % 100) - 50) / 50.0f;
859 float vy = ((rand() % 100) - 50) / 50.0f;
860 float radius;
861 int roll = rand() % 100;
862 if (roll < 20)
863 radius = 30.0f + (rand() % 15);
864 else if (roll < 60)
865 radius = 20.0f + (rand() % 10);
866 else
867 radius = 12.0f + (rand() % 8);
868 spawnAsteroid(x, y, vx, vy, radius);
869 }
870 }
871 }
872
873 void checkAsteroidCollisions() {
874 for (int i = 0; i < MAX_ASTEROIDS; ++i) {
875 if (!asteroids[i].active)
876 continue;
877 for (int j = i + 1; j < MAX_ASTEROIDS; ++j) {
878 if (!asteroids[j].active)
879 continue;
880 float dx = asteroids[i].x - asteroids[j].x;
881 float dy = asteroids[i].y - asteroids[j].y;
882 float dist = sqrtf(dx * dx + dy * dy);
883 float minDist = asteroids[i].radius + asteroids[j].radius;
884 if (dist < minDist && dist > 0) {
885 float nx = dx / dist, ny = dy / dist;
886 float overlap = (minDist - dist) * 0.5f;
887 asteroids[i].x += nx * overlap;
888 asteroids[i].y += ny * overlap;
889 asteroids[j].x -= nx * overlap;
890 asteroids[j].y -= ny * overlap;
891 float rvx = asteroids[i].vx - asteroids[j].vx;
892 float rvy = asteroids[i].vy - asteroids[j].vy;
893 float van = rvx * nx + rvy * ny;
894 if (van > 0)
895 continue;
896 float restitution = 0.8f;
897 float impulse = -(1 + restitution) * van * 0.5f;
898 asteroids[i].vx += impulse * nx;
899 asteroids[i].vy += impulse * ny;
900 asteroids[j].vx -= impulse * nx;
901 asteroids[j].vy -= impulse * ny;
902 }
903 }
904 }
905 }
906
907 void checkShipAsteroidCollision() {
908 if (ship.exploding)
909 return;
910 for (int i = 0; i < MAX_ASTEROIDS; ++i) {
911 if (!asteroids[i].active)
912 continue;
913 float dx = ship.x - asteroids[i].x;
914 float dy = ship.y - asteroids[i].y;
915 float dist = sqrtf(dx * dx + dy * dy);
916 float shipRadius = 8.0f;
917 if (dist < asteroids[i].radius + shipRadius) {
918 startShipExplosion();
919 createAsteroidExplosion(ship.x + dx * 0.5f, ship.y + dy * 0.5f);
920 if (dist > 0) {
921 float nx = -dx / dist, ny = -dy / dist;
922 asteroids[i].vx += nx * 2.0f;
923 asteroids[i].vy += ny * 2.0f;
924 }
925 break;
926 }
927 }
928 }
929
930 void checkProjectileAsteroidCollisions() {
931 for (auto &p : projectiles) {
932 if (!p.active)
933 continue;
934 for (int j = 0; j < MAX_ASTEROIDS; ++j) {
935 if (!asteroids[j].active)
936 continue;
937 float dx = p.x - asteroids[j].x;
938 float dy = p.y - asteroids[j].y;
939 if (sqrtf(dx * dx + dy * dy) < asteroids[j].radius) {
940 p.active = false;
941 if (asteroids[j].radius >= 20.0f)
942 ship.score += LARGE_ASTEROID_POINTS;
943 else if (asteroids[j].radius >= MIN_ASTEROID_RADIUS)
944 ship.score += MEDIUM_ASTEROID_POINTS;
945 else
946 ship.score += SMALL_ASTEROID_POINTS;
947 splitAsteroid(j);
948 break;
949 }
950 }
951 }
952 }
953
954 void startShipExplosion() {
955 if (ship.exploding)
956 return;
957 ship.exploding = true;
958 ship.explosion_timer = EXPLOSION_DURATION;
959 ship.lives--;
960 ship.overheated = false;
961 ship.overheat_cooldown = 0;
962 ship.continuous_fire_timer = 0;
963 ship.burst_count = 0;
964 for (auto &p : particles) {
965 p.x = ship.x + ((rand() % 20) - 10);
966 p.y = ship.y + ((rand() % 20) - 10);
967 float ang = (rand() % 628) / 100.0f;
968 float spd = 0.5f + (rand() % 20) / 10.0f;
969 p.vx = cosf(ang) * spd;
970 p.vy = sinf(ang) * spd;
971 p.lifetime = 30 + (rand() % 60);
972 p.active = true;
973 }
974 }
975
976 void createAsteroidExplosion(float x, float y) {
977 int toCreate = 15 + (rand() % 10);
978 int created = 0;
979 for (auto &p : particles) {
980 if (created >= toCreate)
981 break;
982 if (!p.active) {
983 p.x = x + ((rand() % 16) - 8);
984 p.y = y + ((rand() % 16) - 8);
985 float ang = (rand() % 628) / 100.0f;
986 float spd = 1.0f + (rand() % 20) / 10.0f;
987 p.vx = cosf(ang) * spd;
988 p.vy = sinf(ang) * spd;
989 p.lifetime = 15 + (rand() % 25);
990 p.active = true;
991 created++;
992 }
993 }
994 }
995
996 void updateExplosion() {
997 for (auto &p : particles) {
998 if (!p.active)
999 continue;
1000 p.x += p.vx;
1001 p.y += p.vy;
1002 p.vx *= 0.97f;
1003 p.vy *= 0.97f;
1004 p.lifetime--;
1005 if (p.lifetime <= 0) {
1006 p.active = false;
1007 continue;
1008 }
1009 if (p.x < 0)
1010 p.x += GAME_W;
1011 if (p.x >= GAME_W)
1012 p.x -= GAME_W;
1013 if (p.y < 0)
1014 p.y += GAME_H;
1015 if (p.y >= GAME_H)
1016 p.y -= GAME_H;
1017 }
1018 if (ship.exploding) {
1019 ship.explosion_timer--;
1020 if (ship.explosion_timer <= 0)
1021 ship.exploding = false;
1022 }
1023 }
1024
1025 void handleDeathSequence() {
1026 if (wasExploding && !ship.exploding) {
1027 if (ship.lives > 0) {
1028 triggerRespawn();
1029 } else {
1030 state = STATE_GAMEOVER;
1031 }
1032 wasExploding = false;
1033 } else if (ship.exploding) {
1034 wasExploding = true;
1035 }
1036 }
1037
1038 void triggerRespawn() {
1039 ship.x = GAME_W / 2.0f;
1040 ship.y = GAME_H / 2.0f;
1041 ship.vx = 0;
1042 ship.vy = 0;
1043 ship.angle = 0;
1044 ship.exploding = false;
1045 ship.explosion_timer = 0;
1046 keyLeft = keyRight = keyThrust = keyFire = false;
1047 joyLeft = joyRight = joyThrust = joyFire = false;
1048 state = STATE_COUNTDOWN;
1049 countdownTimer = 0;
1050 countdownNumber = 3;
1051 countdownStartMs = SDL_GetTicks();
1052 launchStartMs = 0;
1053 }
1054
1055 void updateCountdown() {
1056 const Uint32 now = SDL_GetTicks();
1057 const Uint32 elapsed = now - countdownStartMs;
1058
1059 countdownTimer = elapsed % countdownDuration;
1060 countdownNumber = 3 - static_cast<int>(elapsed / countdownDuration);
1061
1062 if (elapsed >= (countdownDuration * 4U)) {
1063 state = STATE_LAUNCH;
1064 launchTimer = 0;
1065 launchStartMs = now;
1066 shipLaunchY = static_cast<float>(GAME_H);
1067 }
1068 }
1069
1070 void updateLaunch() {
1071 const Uint32 now = SDL_GetTicks();
1072 launchTimer = std::min<Uint32>(now - launchStartMs, launchDuration);
1073 if (launchTimer < launchDuration / 2U) {
1074 shipLaunchY = static_cast<float>(GAME_H) -
1075 (static_cast<float>(launchTimer) * (static_cast<float>(GAME_H) - ship.y) /
1076 static_cast<float>(launchDuration / 2U));
1077 } else {
1078 shipLaunchY = ship.y;
1079 }
1080 if (launchTimer >= launchDuration) {
1081 state = STATE_PLAYING;
1082 if (gamepad != nullptr) {
1083 joyRestLX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1084 joyRestLY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1085 }
1086 }
1087 }
1088
1089 void drawStars(float speedMul = 1.0f) {
1090
1091 for (auto &s : stars) {
1092 s.y += s.speed * speedMul;
1093 if (s.y >= GAME_H) {
1094 s.x = static_cast<float>(rand() % GAME_W);
1095 s.y = 0;
1096
1097 if (s.layer == 0) {
1098 s.speed = 0.15f + (rand() % 40) / 100.0f;
1099 s.size = 0.8f + (rand() % 12) / 20.0f;
1100 s.brightness = 0.25f + (rand() % 45) / 100.0f;
1101 } else if (s.layer == 1) {
1102 s.speed = 0.4f + (rand() % 80) / 100.0f;
1103 s.size = 1.2f + (rand() % 18) / 20.0f;
1104 s.brightness = 0.45f + (rand() % 55) / 100.0f;
1105 } else {
1106 s.speed = 0.8f + (rand() % 180) / 100.0f;
1107 s.size = 1.8f + (rand() % 25) / 20.0f;
1108 s.brightness = 0.65f + (rand() % 35) / 100.0f;
1109 }
1110 assignStarColor(s);
1111 s.twinklePhase = (rand() % 628) / 100.0f;
1112 }
1113
1114 s.twinklePhase += s.twinkleSpeed * 0.016f;
1115 float twinkle = 0.7f + 0.3f * sinf(s.twinklePhase);
1116 float finalBrightness = s.brightness * twinkle;
1117
1118 starSprite->setShaderParams(
1119 s.r * finalBrightness,
1120 s.g * finalBrightness,
1121 s.b * finalBrightness,
1122 finalBrightness);
1123
1124 int screenSize = static_cast<int>(s.size * std::min(sx(), sy()));
1125 screenSize = std::max(2, screenSize);
1126 starSprite->drawSpriteRect(toScreenX(s.x), toScreenY(s.y), screenSize, screenSize);
1127 }
1128 }
1129
1130 void drawShipAt(float px, float py) {
1131 if (ship.exploding) {
1132 drawExplosion();
1133 return;
1134 }
1135 float c = cosf(ship.angle), s = sinf(ship.angle);
1136 auto rot = [&](float lx, float ly, float &ox, float &oy) {
1137 ox = px + (lx * c - ly * s);
1138 oy = py + (lx * s + ly * c);
1139 };
1140
1141 float nose_x, nose_y, lw_x, lw_y, rw_x, rw_y;
1142 float le_x, le_y, re_x, re_y, ce_x, ce_y;
1143 float ld_x, ld_y, rd_x, rd_y;
1144 float cl_x, cl_y, cr_x, cr_y, cc_x, cc_y;
1145
1146 rot(0, -15, nose_x, nose_y);
1147 rot(-12, 8, lw_x, lw_y);
1148 rot(12, 8, rw_x, rw_y);
1149 rot(-8, 12, le_x, le_y);
1150 rot(8, 12, re_x, re_y);
1151 rot(0, 10, ce_x, ce_y);
1152 rot(-6, 2, ld_x, ld_y);
1153 rot(6, 2, rd_x, rd_y);
1154 rot(-3, -8, cl_x, cl_y);
1155 rot(3, -8, cr_x, cr_y);
1156 rot(0, -5, cc_x, cc_y);
1157
1158 if (ship.overheated) {
1159 setColor(1.0f, 0.0f, 0.0f, 1.0f);
1160 } else if (ship.continuous_fire_timer > 120) {
1161 int heat = ship.continuous_fire_timer - 120;
1162 setColor(1.0f, std::max(0.0f, 1.0f - heat / 64.0f), 0.0f, 1.0f);
1163 } else {
1164 setColor(1.0f, 1.0f, 1.0f, 1.0f);
1165 }
1166
1167 drawLine(nose_x, nose_y, lw_x, lw_y);
1168 drawLine(nose_x, nose_y, rw_x, rw_y);
1169 drawLine(lw_x, lw_y, rw_x, rw_y);
1170
1171 drawLine(lw_x, lw_y, le_x, le_y);
1172 drawLine(rw_x, rw_y, re_x, re_y);
1173 drawLine(le_x, le_y, ce_x, ce_y);
1174 drawLine(re_x, re_y, ce_x, ce_y);
1175
1176 if (ship.overheated)
1177 setColor(0.78f, 0.0f, 0.0f, 1.0f);
1178 else if (ship.continuous_fire_timer > 120)
1179 setColor(0.78f, std::max(0.0f, 0.78f - (ship.continuous_fire_timer - 120) / 80.0f), 0.0f, 1.0f);
1180 else
1181 setColor(0.78f, 0.78f, 0.78f, 1.0f);
1182 drawLine(nose_x, nose_y, ld_x, ld_y);
1183 drawLine(nose_x, nose_y, rd_x, rd_y);
1184 drawLine(ld_x, ld_y, rd_x, rd_y);
1185 drawLine(ld_x, ld_y, lw_x, lw_y);
1186 drawLine(rd_x, rd_y, rw_x, rw_y);
1187
1188 if (ship.overheated)
1189 setColor(0.0f, 0.5f, 0.5f, 1.0f);
1190 else
1191 setColor(0.0f, 1.0f, 1.0f, 1.0f);
1192 drawLine(cl_x, cl_y, cr_x, cr_y);
1193 drawLine(cl_x, cl_y, cc_x, cc_y);
1194 drawLine(cr_x, cr_y, cc_x, cc_y);
1195
1196 setColor(0.59f, 0.59f, 0.59f, 1.0f);
1197 drawLine(cc_x, cc_y, ce_x, ce_y);
1198
1199 if (keyThrust || joyThrust) {
1200 float f1x, f1y, f2x, f2y, f3x, f3y;
1201 rot(-6, 18, f1x, f1y);
1202 rot(0, 22, f2x, f2y);
1203 rot(6, 18, f3x, f3y);
1204 setColor(1.0f, 0.39f, 0.0f, 1.0f);
1205 drawLine(le_x, le_y, f1x, f1y);
1206 drawLine(ce_x, ce_y, f2x, f2y);
1207 drawLine(re_x, re_y, f3x, f3y);
1208 float ifx, ify;
1209 rot(0, 20, ifx, ify);
1210 setColor(1.0f, 1.0f, 0.0f, 1.0f);
1211 drawLine(ce_x, ce_y, ifx, ify);
1212 }
1213 }
1214
1215 void drawProjectiles() {
1216 setColor(1.0f, 0.0f, 0.0f, 1.0f);
1217 for (auto &p : projectiles) {
1218 if (!p.active)
1219 continue;
1220 float endX = p.x - p.vx * 0.5f;
1221 float endY = p.y - p.vy * 0.5f;
1222 drawLine(p.x, p.y, endX, endY);
1223 }
1224 }
1225
1226 void drawAsteroids() {
1227 for (int i = 0; i < MAX_ASTEROIDS; ++i) {
1228 if (!asteroids[i].active)
1229 continue;
1230 auto &a = asteroids[i];
1231 float cosR = cosf(a.rotation_angle);
1232 float sinR = sinf(a.rotation_angle);
1233
1234 float rv[ASTEROID_VERTICES][2];
1235 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1236 rv[j][0] = a.vertices[j][0] * cosR - a.vertices[j][1] * sinR;
1237 rv[j][1] = a.vertices[j][0] * sinR + a.vertices[j][1] * cosR;
1238 }
1239
1240 setColor(1.0f, 1.0f, 1.0f, 1.0f);
1241 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1242 int next = (j + 1) % ASTEROID_VERTICES;
1243 drawLine(a.x + rv[j][0], a.y + rv[j][1],
1244 a.x + rv[next][0], a.y + rv[next][1]);
1245 }
1246
1247 float iv[ASTEROID_VERTICES][2];
1248 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1249 iv[j][0] = rv[j][0] * 0.6f;
1250 iv[j][1] = rv[j][1] * 0.6f;
1251 }
1252 setColor(0.59f, 0.59f, 0.59f, 1.0f);
1253 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1254 int next = (j + 1) % ASTEROID_VERTICES;
1255 drawLine(a.x + iv[j][0], a.y + iv[j][1],
1256 a.x + iv[next][0], a.y + iv[next][1]);
1257 }
1258
1259 setColor(0.39f, 0.39f, 0.39f, 1.0f);
1260 for (int j = 0; j < ASTEROID_VERTICES; j += 2) {
1261 drawLine(a.x + rv[j][0], a.y + rv[j][1],
1262 a.x + iv[j][0], a.y + iv[j][1]);
1263 }
1264
1265 setColor(0.71f, 0.71f, 0.71f, 1.0f);
1266 for (int j = 0; j < ASTEROID_VERTICES; j += 2) {
1267 int next = (j + 1) % ASTEROID_VERTICES;
1268 drawLine(a.x + rv[j][0], a.y + rv[j][1],
1269 a.x + iv[next][0], a.y + iv[next][1]);
1270 }
1271 }
1272 }
1273
1274 void drawExplosion() {
1275 for (auto &p : particles) {
1276 if (!p.active)
1277 continue;
1278 float lp = static_cast<float>(p.lifetime) / 50.0f;
1279 if (lp > 0.7f)
1280 setColor(1.0f, 1.0f, 1.0f, 1.0f);
1281 else if (lp > 0.5f)
1282 setColor(1.0f, 1.0f, 0.0f, 1.0f);
1283 else if (lp > 0.2f)
1284 setColor(1.0f, 0.5f, 0.0f, 1.0f);
1285 else
1286 setColor(1.0f, 0.0f, 0.0f, 1.0f);
1287 drawRect(p.x, p.y, 2, 2);
1288 }
1289 }
1290
1291 void drawHUD() {
1292 std::string buf = std::format("Score: {}", ship.score);
1293 printText(buf.c_str(), toScreenX(10), toScreenY(10), {255, 255, 255, 255});
1294 buf = std::format("Lives: {}", ship.lives);
1295 printText(buf.c_str(), toScreenX(10), toScreenY(30), {255, 255, 255, 255});
1296 }
1297
1298 void drawCountdown() {
1299 drawStars(0.3f);
1300 if (countdownNumber > 0) {
1301 std::string buf = std::format("{}", countdownNumber);
1302
1303 int textW, textH;
1304 if (getTextDimensions(buf.c_str(), textW, textH)) {
1305
1306 int centerX = w / 2;
1307 int centerY = h / 2;
1308 int textX = centerX - textW / 2;
1309 int textY = centerY - textH / 2;
1310 printText(buf.c_str(), textX, textY, {255, 255, 0, 255});
1311
1312 if (((countdownTimer / 167U) % 2U) != 0U) {
1313 setColor(1.0f, 1.0f, 0.0f, 0.5f);
1314 int padding = std::max(15, static_cast<int>(15 * std::min(sx(), sy())));
1315 int rectX = textX - padding;
1316 int rectY = textY - padding;
1317 int rectW = textW + padding * 2;
1318 int rectH = textH + padding * 2;
1319 int thickness = std::max(3, static_cast<int>(3 * std::min(sx(), sy())));
1320
1321 pixel->drawSpriteRect(rectX, rectY, rectW, thickness);
1322
1323 pixel->drawSpriteRect(rectX, rectY + rectH - thickness, rectW, thickness);
1324
1325 pixel->drawSpriteRect(rectX, rectY, thickness, rectH);
1326
1327 pixel->drawSpriteRect(rectX + rectW - thickness, rectY, thickness, rectH);
1328 }
1329 }
1330 } else {
1331
1332 int textW, textH;
1333 if (getTextDimensions("LAUNCH!", textW, textH)) {
1334 printText("LAUNCH!", w / 2 - textW / 2, h / 2 - textH / 2, {0, 255, 0, 255});
1335 } else {
1336 printText("LAUNCH!", toScreenX(270), toScreenY(160), {0, 255, 0, 255});
1337 }
1338 }
1339
1340 int textW, textH;
1341 if (getTextDimensions("PREPARE FOR MISSION", textW, textH)) {
1342 printText("PREPARE FOR MISSION", w / 2 - textW / 2, h / 2 + textH * 2, {255, 255, 255, 255});
1343 } else {
1344 printText("PREPARE FOR MISSION", toScreenX(220), toScreenY(195), {255, 255, 255, 255});
1345 }
1346
1347 std::string lbuf = std::format("Lives: {}", ship.lives);
1348 std::string sbuf = std::format("Score: {}", ship.score);
1349 if (getTextDimensions(lbuf.c_str(), textW, textH)) {
1350 printText(lbuf.c_str(), w / 2 - textW / 2, h / 2 + textH * 3 + 10, {255, 255, 255, 255});
1351 if (getTextDimensions(sbuf.c_str(), textW, textH)) {
1352 printText(sbuf.c_str(), w / 2 - textW / 2, h / 2 + textH * 4 + 15, {255, 255, 255, 255});
1353 }
1354 } else {
1355 printText(lbuf.c_str(), toScreenX(260), toScreenY(215), {255, 255, 255, 255});
1356 printText(sbuf.c_str(), toScreenX(260), toScreenY(235), {255, 255, 255, 255});
1357 }
1358 }
1359
1360 void drawLaunch() {
1361 drawStars(0.5f);
1362 if (launchTimer > launchDuration / 4)
1363 drawAsteroids();
1364
1365 float tempY = ship.y;
1366 ship.y = shipLaunchY;
1367 drawShipAt(ship.x, shipLaunchY);
1368 ship.y = tempY;
1369
1370 if (launchTimer < launchDuration / 2) {
1371 setColor(1.0f, 0.39f, 0.0f, 1.0f);
1372 for (int i = 0; i < 12; ++i) {
1373 float tx = ship.x + (rand() % 8) - 4;
1374 float ty = shipLaunchY + 15 + i * 2;
1375 if (ty < GAME_H)
1376 drawPixel(tx, ty);
1377 }
1378 }
1379 if (launchTimer >= launchDuration / 2) {
1380 int textW, textH;
1381 if (getTextDimensions("MISSION START!", textW, textH)) {
1382 printText("MISSION START!", w / 2 - textW / 2, h / 2 - textH / 2, {0, 255, 255, 255});
1383 } else {
1384 printText("MISSION START!", toScreenX(240), toScreenY(170), {0, 255, 255, 255});
1385 }
1386 }
1387 }
1388
1389 void drawGame() {
1390 drawStars(1.0f);
1391 drawShipAt(ship.x, ship.y);
1392 drawProjectiles();
1393 drawAsteroids();
1394 drawExplosion();
1395 drawHUD();
1396 }
1397
1398 void drawGameOver() {
1399 drawStars(0.2f);
1400 printText("GAME OVER", toScreenX(255), toScreenY(130), {255, 0, 0, 255});
1401
1402 std::string buf = std::format("Final Score: {}", ship.score);
1403 printText(buf.c_str(), toScreenX(235), toScreenY(165), {255, 255, 255, 255});
1404 printText("Press SPACE to begin", toScreenX(215), toScreenY(195), {255, 255, 0, 255});
1405 }
1406};
1407
1408int main(int argc, char **argv) {
1409 Arguments args = proc_args(argc, argv);
1410 try {
1411 SpaceRoxWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
1412 window.loop();
1413 } catch (mxvk::Exception &e) {
1414 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
1415 return EXIT_FAILURE;
1416 } catch (ArgException<std::string> &e) {
1417 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
1418 return EXIT_FAILURE;
1419 }
1420 return EXIT_SUCCESS;
1421}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:872
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:178
void event(SDL_Event &e) override
Handle one SDL event.
Definition space.cpp:198
void handleControllerButtonUp(Uint8 button)
Definition space.cpp:274
SpaceRoxWindow(const std::string &path, int wx, int wy, bool full, bool enable_vsync)
Definition space.cpp:124
void handleControllerButton(Uint8 button)
Definition space.cpp:254
void proc() override
Execute one processing/update step.
Definition space.cpp:138
void pollController()
Definition space.cpp:282
virtual ~SpaceRoxWindow()
Definition space.cpp:131
std::string text() const
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:37
void loop()
Run the main event/render loop.
Definition mxvk.cpp:600
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:3477
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:3069
void clearTextQueue()
Clear all queued text draw calls for the current frame.
Definition mxvk.cpp:3063
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
Definition mxvk.hpp:480
void exit()
Request loop termination.
Definition mxvk.cpp:1126
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:2991
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3018
int main(void)
Definition main.cpp:7
#define MXVK_VALIDATION
Definition mxvk.hpp:27
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
constexpr int SMALL_ASTEROID_POINTS
Definition space.cpp:41
constexpr int MEDIUM_ASTEROID_POINTS
Definition space.cpp:40
constexpr int GAME_H
Definition space.cpp:26
constexpr int GAME_W
Definition space.cpp:25
constexpr float MIN_ASTEROID_RADIUS
Definition space.cpp:38
constexpr int PROJECTILE_LIFETIME
Definition space.cpp:33
GameState
Definition space.cpp:92
@ STATE_GAMEOVER
Definition space.cpp:96
@ STATE_COUNTDOWN
Definition space.cpp:93
@ STATE_LAUNCH
Definition space.cpp:94
@ STATE_PLAYING
Definition space.cpp:95
constexpr int STAR_COUNT
Definition space.cpp:27
constexpr int FIRE_COOLDOWN
Definition space.cpp:34
constexpr int ASTEROID_VERTICES
Definition space.cpp:31
void bresenhamLine(int x0, int y0, int x1, int y1, Func plot)
Definition space.cpp:100
constexpr int MAX_PROJECTILES
Definition space.cpp:28
constexpr int MAX_PARTICLES
Definition space.cpp:30
#define asteroids_ASSET_DIR
Definition space.cpp:22
constexpr int SHOTS_PER_BURST
Definition space.cpp:35
constexpr float PROJECTILE_SPEED
Definition space.cpp:32
constexpr int MAX_ASTEROIDS
Definition space.cpp:29
constexpr int LARGE_ASTEROID_POINTS
Definition space.cpp:39
#define M_PI
Definition space.cpp:18
constexpr int FIRE_DELAY
Definition space.cpp:36
constexpr int EXPLOSION_DURATION
Definition space.cpp:37
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:730
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:736
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:750
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:733
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:735
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732
bool active
Definition space.cpp:69
float vertices[ASTEROID_VERTICES][2]
Definition space.cpp:70
float x
Definition space.cpp:66
float y
Definition space.cpp:66
float rotation_speed
Definition space.cpp:72
float vx
Definition space.cpp:67
float vy
Definition space.cpp:67
float radius
Definition space.cpp:68
float rotation_angle
Definition space.cpp:71
float y
Definition space.cpp:76
int lifetime
Definition space.cpp:78
bool active
Definition space.cpp:79
float vx
Definition space.cpp:77
float vy
Definition space.cpp:77
float x
Definition space.cpp:76
float y
Definition space.cpp:59
float x
Definition space.cpp:59
float vy
Definition space.cpp:60
float vx
Definition space.cpp:60
bool active
Definition space.cpp:62
float lifetime
Definition space.cpp:61
Definition space.cpp:43
float angle
Definition space.cpp:46
float y
Definition space.cpp:44
int continuous_fire_timer
Definition space.cpp:53
int overheat_cooldown
Definition space.cpp:55
int burst_count
Definition space.cpp:49
int score
Definition space.cpp:52
int explosion_timer
Definition space.cpp:51
int fire_cooldown
Definition space.cpp:48
bool overheated
Definition space.cpp:54
float vx
Definition space.cpp:45
float x
Definition space.cpp:44
bool exploding
Definition space.cpp:50
float vy
Definition space.cpp:45
int lives
Definition space.cpp:47
Definition space.cpp:82
float speed
Definition space.cpp:83
float brightness
Definition space.cpp:85
float x
Definition space.cpp:83
float y
Definition space.cpp:83
float twinkleSpeed
Definition space.cpp:89
float r
Definition space.cpp:87
float twinklePhase
Definition space.cpp:88
float g
Definition space.cpp:87
float size
Definition space.cpp:84
int layer
Definition space.cpp:86
float b
Definition space.cpp:87