All three new entity types were absent from the hardcoded enemy-type checks in is_enemy() (contact damage + projectile hits), homing projectile targeting, and drone targeting. Also adds proper death particle colors for charger (orange) and spawner (purple).
219 lines
7.1 KiB
C
219 lines
7.1 KiB
C
#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 <stdlib.h>
|
|
#include <math.h>
|
|
|
|
#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;
|
|
}
|