MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
defender_combat.cpp
Go to the documentation of this file.
1#include "defender_window.hpp"
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <cstddef>
7#include <format>
8#include <string>
9
10namespace defender {
11
12 void DefenderWindow::fire_projectile() {
13 const glm::vec3 forward{ship_forward_direction, 0.0f, 0.0f};
14 glm::vec3 muzzle = ship.position + forward * 1.35f + glm::vec3(0.0f, -0.22f, 0.0f);
15 muzzle.x = wrap_world_x(muzzle.x);
16
17 for (auto &projectile : projectiles) {
18 if (projectile.active) {
19 continue;
20 }
21 projectile.position = muzzle;
22 projectile.prev_position = muzzle;
23 projectile.velocity = forward * PROJECTILE_SPEED + glm::vec3(ship.velocity.x * 0.25f, 0.0f, 0.0f);
24 projectile.color = PROJECTILE_COLOR;
25 projectile.lifetime = 0.0f;
26 projectile.active = true;
27#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
28 play_sound(shoot_sound);
29#endif
30 log_game(std::format("Laser fired from {}.", format_vec3(muzzle)));
31 return;
32 }
33 }
34
35 void DefenderWindow::update_projectiles(float dt) {
36 for (auto &projectile : projectiles) {
37 if (!projectile.active) {
38 continue;
39 }
40 projectile.prev_position = projectile.position;
41 projectile.position += projectile.velocity * dt;
42 projectile.position.x = wrap_world_x(projectile.position.x);
43 projectile.lifetime += dt;
44 if (projectile.lifetime >= PROJECTILE_LIFETIME) {
45 projectile.active = false;
46 }
47 }
48 }
49
50 void DefenderWindow::check_projectile_ufo_hits() {
51 constexpr float beam_collision_length = 7.4f;
52
53 for (auto &projectile : projectiles) {
54 if (!projectile.active) {
55 continue;
56 }
57
58 const float direction = projectile.velocity.x >= 0.0f ? 1.0f : -1.0f;
59 const float x0 = projectile.prev_position.x;
60 const float x1 = x0 + wrapped_delta_x(projectile.position.x, x0) + direction * beam_collision_length;
61 const float min_x = std::min(x0, x1);
62 const float max_x = std::max(x0, x1);
63
64 for (auto &ufo : ufos) {
65 if (!ufo.active) {
66 continue;
67 }
68 const int frame = current_ufo_frame(ufo);
69 const SpriteAlphaBounds &bounds = ufo_sprite_bounds[static_cast<std::size_t>(ufo.sprite_set)][static_cast<std::size_t>(frame)];
70 const glm::vec2 draw_size = ufo_draw_size(ufo, current_ufo_pulse(ufo));
71 const glm::vec2 body_center{
72 nearest_world_x(ufo.position.x, (x0 + x1) * 0.5f) + ((bounds.min_x + bounds.max_x) * 0.5f) * draw_size.x,
73 ufo.position.y + ((bounds.min_y + bounds.max_y) * 0.5f) * draw_size.y,
74 };
75 const glm::vec2 body_radius{
76 std::max(0.001f, (bounds.max_x - bounds.min_x) * 0.5f * draw_size.x),
77 std::max(0.001f, (bounds.max_y - bounds.min_y) * 0.5f * draw_size.y),
78 };
79 const bool overlaps_x = (body_center.x + body_radius.x) >= min_x && (body_center.x - body_radius.x) <= max_x;
80 const bool overlaps_y = std::abs(body_center.y - projectile.position.y) <= body_radius.y;
81 if (!overlaps_x || !overlaps_y) {
82 continue;
83 }
84
85 if (ufo.sprite_set == UfoSpriteSet::Alien) {
86 spawn_alien_explosion(ufo.position);
87 } else {
88 spawn_ufo_explosion(ufo.position);
89 }
90#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
91 play_sound(ufo_explosion_sound);
92#endif
93 ufo.active = false;
94 ufo.respawn_timer = space::random_float(1.2f, 2.8f);
95 projectile.active = false;
96 score += 5;
97 log_game(std::format("{} destroyed. Score={}.", ufo.sprite_set == UfoSpriteSet::Alien ? "Alien" : "UFO", score));
98 break;
99 }
100 }
101 }
102
103 void DefenderWindow::check_projectile_asteroid_hits() {
104 constexpr float beam_collision_length = 7.4f;
105
106 for (auto &projectile : projectiles) {
107 if (!projectile.active) {
108 continue;
109 }
110
111 const float direction = projectile.velocity.x >= 0.0f ? 1.0f : -1.0f;
112 const float x0 = projectile.prev_position.x;
113 const float x1 = x0 + wrapped_delta_x(projectile.position.x, x0) + direction * beam_collision_length;
114 const float min_x = std::min(x0, x1);
115 const float max_x = std::max(x0, x1);
116
117 for (auto &asteroid : asteroids) {
118 if (!asteroid.active) {
119 continue;
120 }
121
122 const float asteroid_x = nearest_world_x(asteroid.position.x, (x0 + x1) * 0.5f);
123 const float closest_x = std::clamp(asteroid_x, min_x, max_x);
124 const glm::vec2 delta{asteroid_x - closest_x, asteroid.position.y - projectile.position.y};
125 if (glm::dot(delta, delta) > asteroid.collision_radius * asteroid.collision_radius) {
126 continue;
127 }
128
129 spawn_ufo_explosion(asteroid.position, asteroid.scale);
130#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
131 play_sound(asteroid_explosion_sound);
132#endif
133 asteroid.active = false;
134 asteroid.respawn_timer = space::random_float(1.0f, 2.5f);
135 projectile.active = false;
136 score += 10;
137 log_game(std::format("Asteroid destroyed. Score={}.", score));
138 break;
139 }
140 }
141 }
142
143 void DefenderWindow::check_ship_ufo_collisions() {
144 constexpr float ship_half_width = 1.75f;
145 constexpr float ship_half_height = 0.85f;
146
147 for (auto &ufo : ufos) {
148 if (!ufo.active) {
149 continue;
150 }
151
152 const int frame = current_ufo_frame(ufo);
153 const SpriteAlphaBounds &bounds = ufo_sprite_bounds[static_cast<std::size_t>(ufo.sprite_set)][static_cast<std::size_t>(frame)];
154 const float pulse = current_ufo_pulse(ufo);
155 const glm::vec2 draw_size = ufo_draw_size(ufo, pulse);
156 const glm::vec2 body_center{
157 nearest_world_x(ufo.position.x, ship.position.x) + ((bounds.min_x + bounds.max_x) * 0.5f) * draw_size.x,
158 ufo.position.y + ((bounds.min_y + bounds.max_y) * 0.5f) * draw_size.y,
159 };
160 const glm::vec2 body_radius{
161 std::max(0.001f, (bounds.max_x - bounds.min_x) * 0.5f * draw_size.x),
162 std::max(0.001f, (bounds.max_y - bounds.min_y) * 0.5f * draw_size.y),
163 };
164
165 const float closest_x = std::clamp(body_center.x, ship.position.x - ship_half_width, ship.position.x + ship_half_width);
166 const float closest_y = std::clamp(body_center.y, ship.position.y - ship_half_height, ship.position.y + ship_half_height);
167 const glm::vec2 normalized_delta{
168 (closest_x - body_center.x) / body_radius.x,
169 (closest_y - body_center.y) / body_radius.y,
170 };
171 if (glm::dot(normalized_delta, normalized_delta) > 1.0f) {
172 continue;
173 }
174
175 if (ufo.sprite_set == UfoSpriteSet::Alien) {
176 spawn_alien_explosion(ufo.position);
177 } else {
178 spawn_ufo_explosion(ufo.position);
179 }
180 spawn_ship_explosion(ship.position);
181#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
182 play_sound(ufo_explosion_sound);
183#endif
184 ufo.active = false;
185 ufo.respawn_timer = space::random_float(1.5f, 3.0f);
186 log_game("Ship collided with UFO.", SDL_Color{255, 150, 90, 255});
187 lose_life();
188 break;
189 }
190 }
191
192 void DefenderWindow::check_ship_asteroid_collisions() {
193 constexpr float ship_half_width = 1.75f;
194 constexpr float ship_half_height = 0.85f;
195
196 for (auto &asteroid : asteroids) {
197 if (!asteroid.active) {
198 continue;
199 }
200
201 const float asteroid_x = nearest_world_x(asteroid.position.x, ship.position.x);
202 const float closest_x = std::clamp(asteroid_x, ship.position.x - ship_half_width, ship.position.x + ship_half_width);
203 const float closest_y = std::clamp(asteroid.position.y, ship.position.y - ship_half_height, ship.position.y + ship_half_height);
204 const glm::vec2 delta{asteroid_x - closest_x, asteroid.position.y - closest_y};
205 if (glm::dot(delta, delta) > asteroid.collision_radius * asteroid.collision_radius) {
206 continue;
207 }
208
209 spawn_ufo_explosion(asteroid.position, asteroid.scale);
210 spawn_ship_explosion(ship.position);
211#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
212 play_sound(asteroid_explosion_sound);
213#endif
214 asteroid.active = false;
215 asteroid.respawn_timer = space::random_float(1.0f, 2.5f);
216 log_game("Ship collided with asteroid.", SDL_Color{255, 150, 90, 255});
217 lose_life();
218 break;
219 }
220 }
221
222 void DefenderWindow::lose_life() {
223#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
224 play_sound(crash_sound);
225#endif
226 --lives;
227 clear_projectiles();
228 ship.visible = false;
229 log_game(std::format("Ship destroyed. Lives remaining: {}.", std::max(0, lives)), SDL_Color{255, 120, 80, 255});
230 if (lives <= 0) {
231 lives = 0;
232 game_over = true;
233 ship.velocity = glm::vec3(0.0f);
234 ship.current_speed = 0.0f;
235 log_game(std::format("Game over. Final score: {}.", score), SDL_Color{255, 90, 90, 255});
236 return;
237 }
238
239 ship_respawning = true;
240 respawn_timer = 2.2f;
241 ship.velocity = glm::vec3(0.0f);
242 ship.current_speed = 0.0f;
243 }
244
245 void DefenderWindow::update_respawn(float dt) {
246 respawn_timer -= dt;
247 if (respawn_timer > 0.0f) {
248 return;
249 }
250
251 reset_ship_to_origin();
252 ship_respawning = false;
253 start_launch_countdown();
254 log_game("Ship respawned at origin.");
255 }
256
257 void DefenderWindow::reset_ship_to_origin() {
258 camera_center_x = 0.0f;
259 camera_position = glm::vec3(0.0f, CAMERA_HEIGHT, CAMERA_DISTANCE);
260 reset_ufos_for_origin_start();
261 ship.position = glm::vec3(0.0f, 0.0f, 0.0f);
262 ship.prev_position = ship.position;
263 ship.velocity = glm::vec3(0.0f);
264 ship.current_speed = 0.0f;
265 ship.visible = true;
266 ship_forward_direction = 1.0f;
267 barrel_roll_direction = 1.0f;
268 barrel_roll_progress = 0.0f;
269 barrel_roll_angle = 0.0f;
270 ship.rotation = glm::vec3(0.0f, -90.0f, 0.0f);
271 }
272
273 void DefenderWindow::reset_ufos_for_origin_start() {
274 level_active = false;
275 for (auto &ufo : ufos) {
276 ufo.active = false;
277 ufo.respawn_timer = space::random_float(0.4f, 4.0f);
278 }
279 for (auto &asteroid : asteroids) {
280 asteroid.active = false;
281 asteroid.respawn_timer = space::random_float(0.7f, 4.8f);
282 }
283 }
284
285 void DefenderWindow::restart_game() {
286 score = 0;
287 lives = 5;
288 level = 1;
289 game_over = false;
290 ship_respawning = false;
291 respawn_timer = 0.0f;
292 reverse_pressed = false;
293 propulsion_pressed = false;
294 up_pressed = false;
295 down_pressed = false;
296 roll_left_pressed = false;
297 roll_right_pressed = false;
298 fire_pressed = false;
299 clear_projectiles();
300 clear_particles();
301 reset_ship_to_origin();
302 start_launch_countdown();
303 log_game("Game state reset: score=0 lives=5.");
304 }
305
306 void DefenderWindow::clear_projectiles() {
307 for (auto &projectile : projectiles) {
308 projectile.active = false;
309 }
310 ship.fire_cooldown = 0;
311 }
312
313 void DefenderWindow::draw_projectiles() {
314 for (const auto &projectile : projectiles) {
315 if (!projectile.active) {
316 continue;
317 }
318 const float life_factor = std::clamp(1.0f - (projectile.lifetime / PROJECTILE_LIFETIME), 0.0f, 1.0f);
319 const float flash = (std::sin(elapsed_seconds * 95.0f) > 0.0f) ? 1.0f : 0.48f;
320 const float beam_length = 6.4f + 1.2f * life_factor;
321 const float core_thickness = 0.085f + 0.025f * flash;
322 const float glow_thickness = 0.30f + 0.10f * flash;
323 const float direction = projectile.velocity.x >= 0.0f ? 1.0f : -1.0f;
324 const glm::vec3 beam_center_offset{direction * beam_length * 0.38f, 0.0f, 0.0f};
325 constexpr std::array<glm::vec3, 6> laser_colors = {
326 glm::vec3{1.0f, 0.14f, 0.14f},
327 glm::vec3{1.0f, 0.86f, 0.10f},
328 glm::vec3{0.18f, 1.0f, 0.26f},
329 glm::vec3{0.12f, 0.76f, 1.0f},
330 glm::vec3{0.42f, 0.20f, 1.0f},
331 glm::vec3{1.0f, 0.18f, 0.84f},
332 };
333 const std::size_t color_index = std::min(
334 laser_colors.size() - 1,
335 static_cast<std::size_t>(space::random_float(0.0f, static_cast<float>(laser_colors.size()))));
336 const glm::vec3 &laser_color = laser_colors[color_index];
337 const glm::vec4 glow_color{
338 std::clamp(laser_color.r * (0.55f + 0.45f * flash), 0.0f, 1.0f),
339 std::clamp(laser_color.g * (0.55f + 0.45f * flash), 0.0f, 1.0f),
340 std::clamp(laser_color.b * (0.55f + 0.45f * flash), 0.0f, 1.0f),
341 std::clamp(0.20f + 0.32f * flash * life_factor, 0.0f, 1.0f),
342 };
343 const glm::vec4 core_color{
344 std::clamp(laser_color.r * (0.88f + 0.12f * flash), 0.0f, 1.0f),
345 std::clamp(laser_color.g * (0.88f + 0.12f * flash), 0.0f, 1.0f),
346 std::clamp(laser_color.b * (0.88f + 0.12f * flash), 0.0f, 1.0f),
347 std::clamp(0.45f + 0.55f * flash * life_factor, 0.0f, 1.0f),
348 };
349
350 const glm::vec3 draw_position = nearest_world_position(projectile.position, camera_position.x) + beam_center_offset;
351 projectile_sprite->drawSprite(draw_position, glm::vec2(beam_length, glow_thickness), glow_color);
352 projectile_sprite->drawSprite(draw_position, glm::vec2(beam_length, core_thickness), core_color);
353 }
354 }
355
356 void DefenderWindow::spawn_ufo_explosion(const glm::vec3 &position, float explosion_scale) {
357 constexpr std::array<glm::vec3, 4> colors = {
358 glm::vec3{1.0f, 1.0f, 1.0f},
359 glm::vec3{1.0f, 0.86f, 0.34f},
360 glm::vec3{1.0f, 0.42f, 0.08f},
361 glm::vec3{0.75f, 0.90f, 1.0f},
362 };
363 spawn_enemy_explosion(position, explosion_scale, colors);
364 }
365
366 void DefenderWindow::spawn_alien_explosion(const glm::vec3 &position, float explosion_scale) {
367 constexpr std::array<glm::vec3, 4> colors = {
368 glm::vec3{0.82f, 1.0f, 0.52f},
369 glm::vec3{0.35f, 1.0f, 0.18f},
370 glm::vec3{0.08f, 0.72f, 0.18f},
371 glm::vec3{0.70f, 1.0f, 0.82f},
372 };
373 spawn_enemy_explosion(position, explosion_scale, colors);
374 }
375
376 void DefenderWindow::spawn_ship_explosion(const glm::vec3 &position, float explosion_scale) {
377 constexpr std::array<glm::vec3, 4> colors = {
378 glm::vec3{1.0f, 0.72f, 0.62f},
379 glm::vec3{1.0f, 0.20f, 0.12f},
380 glm::vec3{0.76f, 0.02f, 0.02f},
381 glm::vec3{1.0f, 0.36f, 0.22f},
382 };
383 spawn_enemy_explosion(position, explosion_scale, colors, true);
384 }
385
386 void DefenderWindow::spawn_enemy_explosion(const glm::vec3 &position, float explosion_scale, const std::array<glm::vec3, 4> &wave_colors, bool color_flash) {
387 struct ExplosionWave {
388 float min_speed;
389 float max_speed;
390 float min_size;
391 float max_size;
392 float min_lifetime;
393 float max_lifetime;
394 };
395
396 constexpr std::array<ExplosionWave, 4> waves = {
397 ExplosionWave{24.0f, 42.0f, 0.42f, 0.78f, 0.75f, 1.35f},
398 ExplosionWave{16.0f, 32.0f, 0.32f, 0.58f, 0.90f, 1.60f},
399 ExplosionWave{10.0f, 24.0f, 0.20f, 0.42f, 1.05f, 1.85f},
400 ExplosionWave{5.0f, 16.0f, 0.12f, 0.26f, 1.20f, 2.10f},
401 };
402
403 const float visual_scale = std::clamp(explosion_scale, 0.75f, 2.35f);
404 const float particle_count_scale = std::clamp(visual_scale, 0.75f, 1.80f);
405 const int particles_per_wave = std::max(1, static_cast<int>(std::round(60.0f * particle_count_scale)));
406 const float speed_scale = std::lerp(0.85f, 1.35f, std::clamp((visual_scale - 0.75f) / 1.60f, 0.0f, 1.0f));
407 const float lifetime_scale = std::lerp(0.90f, 1.20f, std::clamp((visual_scale - 0.75f) / 1.60f, 0.0f, 1.0f));
408
409 for (std::size_t wave_index = 0; wave_index < waves.size(); ++wave_index) {
410 const ExplosionWave &wave = waves[wave_index];
411 const glm::vec3 &wave_color = wave_colors[wave_index];
412 for (int i = 0; i < particles_per_wave; ++i) {
413 space::Particle *particle = find_free_particle();
414 if (particle == nullptr) {
415 return;
416 }
417 const glm::vec3 dir = space::normalize_or_zero(glm::vec3(
418 space::random_float(-1.0f, 1.0f),
419 space::random_float(-0.75f, 0.75f),
420 space::random_float(-0.35f, 0.35f)));
421 particle->position = position + dir * space::random_float(0.1f, 0.8f) * visual_scale;
422 particle->velocity = dir * space::random_float(wave.min_speed, wave.max_speed) * speed_scale;
423 particle->color = glm::vec4(
424 wave_color.r * space::random_float(0.9f, 1.1f),
425 wave_color.g * space::random_float(0.9f, 1.1f),
426 wave_color.b * space::random_float(0.9f, 1.1f),
427 0.1f);
428 particle->color_flash = color_flash;
429 particle->size = space::random_float(wave.min_size, wave.max_size) * visual_scale;
430 particle->lifetime = 0.0f;
431 particle->max_lifetime = space::random_float(wave.min_lifetime, wave.max_lifetime) * lifetime_scale;
432 particle->active = true;
433 }
434 }
435 }
436
437 space::Particle *DefenderWindow::find_free_particle() {
438 for (auto &particle : particles) {
439 if (!particle.active) {
440 return &particle;
441 }
442 }
443 return nullptr;
444 }
445
446 void DefenderWindow::update_particles(float dt) {
447 for (auto &particle : particles) {
448 if (!particle.active) {
449 continue;
450 }
451 particle.position += particle.velocity * dt;
452 particle.velocity *= 0.975f;
453 particle.velocity.y -= 0.35f * dt;
454 particle.lifetime += dt;
455
456 const float life_ratio = particle.lifetime / particle.max_lifetime;
457 if (life_ratio >= 1.0f) {
458 particle.active = false;
459 continue;
460 }
461 if (life_ratio < 0.2f) {
462 particle.color.a = life_ratio / 0.2f;
463 } else if (life_ratio > 0.75f) {
464 particle.color.a = (1.0f - life_ratio) / 0.25f;
465 } else {
466 particle.color.a = 1.0f;
467 }
468 if (particle.color_flash) {
469 constexpr std::array<glm::vec3, 6> flash_colors = {
470 glm::vec3{1.0f, 0.15f, 0.15f},
471 glm::vec3{1.0f, 0.85f, 0.12f},
472 glm::vec3{0.18f, 1.0f, 0.28f},
473 glm::vec3{0.15f, 0.78f, 1.0f},
474 glm::vec3{0.45f, 0.18f, 1.0f},
475 glm::vec3{1.0f, 0.20f, 0.85f},
476 };
477 const std::size_t color_index = std::min(
478 flash_colors.size() - 1,
479 static_cast<std::size_t>(space::random_float(0.0f, static_cast<float>(flash_colors.size()))));
480 const glm::vec3 &flash_color = flash_colors[color_index];
481 particle.color.r = flash_color.r * space::random_float(0.85f, 1.0f);
482 particle.color.g = flash_color.g * space::random_float(0.85f, 1.0f);
483 particle.color.b = flash_color.b * space::random_float(0.85f, 1.0f);
484 }
485 particle.size *= life_ratio < 0.30f ? 1.012f : 0.988f;
486 if (particle.color.a < 0.01f) {
487 particle.active = false;
488 }
489 }
490 }
491
492 void DefenderWindow::draw_particles() {
493 for (const auto &particle : particles) {
494 if (!particle.active) {
495 continue;
496 }
497 effect_sprite->drawSprite(nearest_world_position(particle.position, camera_position.x), glm::vec2(particle.size), particle.color);
498 }
499 }
500
501 void DefenderWindow::clear_particles() {
502 for (auto &particle : particles) {
503 particle.active = false;
504 }
505 }
506
507} // namespace defender
glm::vec3 nearest_world_position(glm::vec3 position, float origin_x)
constexpr glm::vec4 PROJECTILE_COLOR
constexpr float PROJECTILE_LIFETIME
float nearest_world_x(float target, float origin)
float wrapped_delta_x(float target, float origin)
float wrap_world_x(float x)
constexpr float CAMERA_DISTANCE
constexpr float PROJECTILE_SPEED
constexpr float CAMERA_HEIGHT
glm::vec3 normalize_or_zero(const glm::vec3 &value)
Normalizes a direction with a stable fallback.
float random_float(float min_value, float max_value)
Generates a uniformly distributed floating-point value.
float max_lifetime
Initial lifetime in seconds.
bool color_flash
Whether the particle flashes as it ages.
bool active
Whether this slot is active.
glm::vec3 position
Current world-space position.
float lifetime
Remaining lifetime in seconds.
float size
Rendered point size.
glm::vec3 velocity
World-space velocity.
glm::vec4 color
Render color.