#include "game/drone.h" #include "game/sprites.h" #include "game/projectile.h" #include "engine/renderer.h" #include "engine/particle.h" #include "engine/physics.h" #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif /* ── Stashed entity manager ────────────────────────── */ static EntityManager *s_em = NULL; /* ── Helpers ───────────────────────────────────────── */ /* Find the active player entity */ static Entity *find_player(void) { if (!s_em) return NULL; for (int i = 0; i < s_em->count; i++) { Entity *e = &s_em->entities[i]; if (e->active && e->type == ENT_PLAYER) return e; } return NULL; } /* Find nearest enemy within range */ static Entity *find_nearest_enemy(Vec2 from, float range) { if (!s_em) return NULL; Entity *best = NULL; float best_dist = range * range; for (int i = 0; i < s_em->count; i++) { Entity *e = &s_em->entities[i]; if (!e->active || e->health <= 0) continue; if (e->type != ENT_ENEMY_GRUNT && e->type != ENT_ENEMY_FLYER && e->type != ENT_TURRET && e->type != ENT_ENEMY_CHARGER && e->type != ENT_SPAWNER && e->type != ENT_LASER_TURRET) continue; Vec2 epos = vec2(e->body.pos.x + e->body.size.x * 0.5f, e->body.pos.y + e->body.size.y * 0.5f); float dx = epos.x - from.x; float dy = epos.y - from.y; float dist2 = dx * dx + dy * dy; if (dist2 < best_dist) { best_dist = dist2; best = e; } } return best; } /* ── Callbacks ─────────────────────────────────────── */ static void drone_update(Entity *self, float dt, const Tilemap *map) { (void)map; DroneData *dd = (DroneData *)self->data; if (!dd) return; /* Countdown lifetime */ dd->lifetime -= dt; if (dd->lifetime <= 0) { /* Expiry: death particles and destroy */ Vec2 center = vec2( self->body.pos.x + self->body.size.x * 0.5f, self->body.pos.y + self->body.size.y * 0.5f ); particle_emit_spark(center, (SDL_Color){50, 200, 255, 255}); entity_destroy(s_em, self); return; } /* Follow player — orbit around player center */ Entity *player = find_player(); if (!player) return; Vec2 pcenter = vec2( player->body.pos.x + player->body.size.x * 0.5f, player->body.pos.y + player->body.size.y * 0.5f ); dd->orbit_angle += DRONE_ORBIT_SPEED * dt; if (dd->orbit_angle > 2.0f * (float)M_PI) dd->orbit_angle -= 2.0f * (float)M_PI; float target_x = pcenter.x + cosf(dd->orbit_angle) * DRONE_ORBIT_RADIUS; float target_y = pcenter.y + sinf(dd->orbit_angle) * DRONE_ORBIT_RADIUS; /* Smooth movement toward orbit position */ float lerp = 8.0f * dt; if (lerp > 1.0f) lerp = 1.0f; self->body.pos.x += (target_x - self->body.pos.x - self->body.size.x * 0.5f) * lerp; self->body.pos.y += (target_y - self->body.pos.y - self->body.size.y * 0.5f) * lerp; /* Shooting */ dd->shoot_cooldown -= dt; if (dd->shoot_cooldown <= 0) { Vec2 drone_center = vec2( self->body.pos.x + self->body.size.x * 0.5f, self->body.pos.y + self->body.size.y * 0.5f ); Entity *target = find_nearest_enemy(drone_center, DRONE_SHOOT_RANGE); if (target) { Vec2 tcenter = vec2( target->body.pos.x + target->body.size.x * 0.5f, target->body.pos.y + target->body.size.y * 0.5f ); Vec2 dir = vec2(tcenter.x - drone_center.x, tcenter.y - drone_center.y); /* Spawn projectile from drone, counted as player bullet */ projectile_spawn_def(s_em, &WEAPON_PLASMA, drone_center, dir, true); dd->shoot_cooldown = DRONE_SHOOT_CD; /* Muzzle flash */ particle_emit_muzzle_flash(drone_center, dir); } } /* Trail particles — subtle engine glow */ if ((int)(dd->lifetime * 10.0f) % 3 == 0) { Vec2 center = vec2( self->body.pos.x + self->body.size.x * 0.5f, self->body.pos.y + self->body.size.y * 0.5f ); ParticleBurst b = { .origin = center, .count = 1, .speed_min = 5.0f, .speed_max = 15.0f, .life_min = 0.1f, .life_max = 0.3f, .size_min = 1.0f, .size_max = 1.5f, .spread = (float)M_PI * 2.0f, .direction = 0, .drag = 3.0f, .gravity_scale = 0.0f, .color = (SDL_Color){50, 180, 255, 150}, .color_vary = true, }; particle_emit(&b); } /* Blink when about to expire (last 3 seconds) */ animation_update(&self->anim, dt); } static void drone_render(Entity *self, const Camera *cam) { (void)cam; DroneData *dd = (DroneData *)self->data; if (!dd) return; /* Blink when about to expire */ if (dd->lifetime < 3.0f) { int blink = (int)(dd->lifetime * 8.0f); if (blink % 2 == 0) return; /* skip rendering every other frame */ } if (!g_spritesheet || !self->anim.def) return; SDL_Rect src = animation_current_rect(&self->anim); Body *body = &self->body; /* Center sprite on hitbox */ float draw_x = body->pos.x + body->size.x * 0.5f - SPRITE_CELL * 0.5f; float draw_y = body->pos.y + body->size.y * 0.5f - SPRITE_CELL * 0.5f; Sprite spr = { .texture = g_spritesheet, .src = src, .pos = vec2(draw_x, draw_y), .size = vec2(SPRITE_CELL, SPRITE_CELL), .flip_x = false, .flip_y = false, .layer = LAYER_ENTITIES, .alpha = 255, }; renderer_submit(&spr); } static void drone_destroy(Entity *self) { free(self->data); self->data = NULL; } /* ── Public API ────────────────────────────────────── */ void drone_register(EntityManager *em) { entity_register(em, ENT_DRONE, drone_update, drone_render, drone_destroy); s_em = em; } Entity *drone_spawn(EntityManager *em, Vec2 player_pos) { Entity *e = entity_spawn(em, ENT_DRONE, player_pos); if (!e) return NULL; e->body.size = vec2(10.0f, 10.0f); e->body.gravity_scale = 0.0f; /* floats freely */ e->health = 99; /* practically invulnerable */ e->max_health = 99; e->damage = 0; e->flags |= ENTITY_INVINCIBLE | ENTITY_ALWAYS_UPDATE; DroneData *dd = calloc(1, sizeof(DroneData)); dd->orbit_angle = 0.0f; dd->shoot_cooldown = 0.5f; /* short initial delay */ dd->lifetime = DRONE_DURATION; e->data = dd; animation_set(&e->anim, &anim_drone); /* Spawn burst */ particle_emit_spark(player_pos, (SDL_Color){50, 200, 255, 255}); printf("Drone deployed! (%0.0fs)\n", DRONE_DURATION); return e; }