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);
17 for (
auto &projectile : projectiles) {
18 if (projectile.active) {
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);
25 projectile.lifetime = 0.0f;
26 projectile.active =
true;
27#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
28 play_sound(shoot_sound);
30 log_game(std::format(
"Laser fired from {}.", format_vec3(muzzle)));
35 void DefenderWindow::update_projectiles(
float dt) {
36 for (
auto &projectile : projectiles) {
37 if (!projectile.active) {
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;
45 projectile.active =
false;
50 void DefenderWindow::check_projectile_ufo_hits() {
51 constexpr float beam_collision_length = 7.4f;
53 for (
auto &projectile : projectiles) {
54 if (!projectile.active) {
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);
64 for (
auto &ufo : ufos) {
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,
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),
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) {
86 spawn_alien_explosion(ufo.position);
88 spawn_ufo_explosion(ufo.position);
90#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
91 play_sound(ufo_explosion_sound);
95 projectile.active =
false;
97 log_game(std::format(
"{} destroyed. Score={}.", ufo.sprite_set ==
UfoSpriteSet::Alien ?
"Alien" :
"UFO", score));
103 void DefenderWindow::check_projectile_asteroid_hits() {
104 constexpr float beam_collision_length = 7.4f;
106 for (
auto &projectile : projectiles) {
107 if (!projectile.active) {
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);
117 for (
auto &asteroid : asteroids) {
118 if (!asteroid.active) {
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) {
129 spawn_ufo_explosion(asteroid.position, asteroid.scale);
130#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
131 play_sound(asteroid_explosion_sound);
133 asteroid.active =
false;
135 projectile.active =
false;
137 log_game(std::format(
"Asteroid destroyed. Score={}.", score));
143 void DefenderWindow::check_ship_ufo_collisions() {
144 constexpr float ship_half_width = 1.75f;
145 constexpr float ship_half_height = 0.85f;
147 for (
auto &ufo : ufos) {
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,
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),
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,
171 if (glm::dot(normalized_delta, normalized_delta) > 1.0f) {
176 spawn_alien_explosion(ufo.position);
178 spawn_ufo_explosion(ufo.position);
180 spawn_ship_explosion(ship.position);
181#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
182 play_sound(ufo_explosion_sound);
186 log_game(
"Ship collided with UFO.", SDL_Color{255, 150, 90, 255});
192 void DefenderWindow::check_ship_asteroid_collisions() {
193 constexpr float ship_half_width = 1.75f;
194 constexpr float ship_half_height = 0.85f;
196 for (
auto &asteroid : asteroids) {
197 if (!asteroid.active) {
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) {
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);
214 asteroid.active =
false;
216 log_game(
"Ship collided with asteroid.", SDL_Color{255, 150, 90, 255});
222 void DefenderWindow::lose_life() {
223#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
224 play_sound(crash_sound);
228 ship.visible =
false;
229 log_game(std::format(
"Ship destroyed. Lives remaining: {}.", std::max(0, lives)), SDL_Color{255, 120, 80, 255});
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});
239 ship_respawning =
true;
240 respawn_timer = 2.2f;
241 ship.velocity = glm::vec3(0.0f);
242 ship.current_speed = 0.0f;
245 void DefenderWindow::update_respawn(
float dt) {
247 if (respawn_timer > 0.0f) {
251 reset_ship_to_origin();
252 ship_respawning =
false;
253 start_launch_countdown();
254 log_game(
"Ship respawned at origin.");
257 void DefenderWindow::reset_ship_to_origin() {
258 camera_center_x = 0.0f;
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;
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);
273 void DefenderWindow::reset_ufos_for_origin_start() {
274 level_active =
false;
275 for (
auto &ufo : ufos) {
279 for (
auto &asteroid : asteroids) {
280 asteroid.active =
false;
285 void DefenderWindow::restart_game() {
290 ship_respawning =
false;
291 respawn_timer = 0.0f;
292 reverse_pressed =
false;
293 propulsion_pressed =
false;
295 down_pressed =
false;
296 roll_left_pressed =
false;
297 roll_right_pressed =
false;
298 fire_pressed =
false;
301 reset_ship_to_origin();
302 start_launch_countdown();
303 log_game(
"Game state reset: score=0 lives=5.");
306 void DefenderWindow::clear_projectiles() {
307 for (
auto &projectile : projectiles) {
308 projectile.active =
false;
310 ship.fire_cooldown = 0;
313 void DefenderWindow::draw_projectiles() {
314 for (
const auto &projectile : projectiles) {
315 if (!projectile.active) {
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},
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),
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),
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);
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},
363 spawn_enemy_explosion(position, explosion_scale, colors);
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},
373 spawn_enemy_explosion(position, explosion_scale, colors);
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},
383 spawn_enemy_explosion(position, explosion_scale, colors,
true);
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 {
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},
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));
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) {
423 particle->
color = glm::vec4(
437 space::Particle *DefenderWindow::find_free_particle() {
438 for (
auto &particle : particles) {
446 void DefenderWindow::update_particles(
float dt) {
447 for (
auto &particle : particles) {
457 if (life_ratio >= 1.0f) {
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;
466 particle.
color.a = 1.0f;
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},
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];
485 particle.
size *= life_ratio < 0.30f ? 1.012f : 0.988f;
486 if (particle.
color.a < 0.01f) {
492 void DefenderWindow::draw_particles() {
493 for (
const auto &particle : particles) {
501 void DefenderWindow::clear_particles() {
502 for (
auto &particle : particles) {
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.