Files
major_tom/src/game/enemy.c
2026-02-28 18:03:47 +00:00

266 lines
8.1 KiB
C

#include "game/enemy.h"
#include "game/sprites.h"
#include "game/projectile.h"
#include "engine/physics.h"
#include "engine/renderer.h"
#include <stdlib.h>
#include <math.h>
/* ════════════════════════════════════════════════════
* GRUNT - ground patrol enemy
* ════════════════════════════════════════════════════ */
static EntityManager *s_grunt_em = NULL;
static void grunt_update(Entity *self, float dt, const Tilemap *map) {
GruntData *gd = (GruntData *)self->data;
if (!gd) return;
Body *body = &self->body;
/* Death sequence */
if (self->flags & ENTITY_DEAD) {
animation_set(&self->anim, &anim_grunt_death);
animation_update(&self->anim, dt);
gd->death_timer -= dt;
body->vel.x = 0;
if (gd->death_timer <= 0) {
entity_destroy(s_grunt_em, self);
}
return;
}
/* Patrol: walk in one direction, reverse when hitting a wall
or about to walk off a ledge */
body->vel.x = gd->patrol_dir * GRUNT_SPEED;
/* Set facing direction */
if (gd->patrol_dir < 0) self->flags |= ENTITY_FACING_LEFT;
else self->flags &= ~ENTITY_FACING_LEFT;
/* Physics */
physics_update(body, dt, map);
/* Turn around on wall collision */
if (body->on_wall_left || body->on_wall_right) {
gd->patrol_dir = -gd->patrol_dir;
}
/* Turn around at ledge: check if there's ground ahead */
if (body->on_ground) {
float check_x = (gd->patrol_dir > 0) ?
body->pos.x + body->size.x + 2.0f :
body->pos.x - 2.0f;
float check_y = body->pos.y + body->size.y + 4.0f;
int tx = world_to_tile(check_x);
int ty = world_to_tile(check_y);
if (!tilemap_is_solid(map, tx, ty)) {
gd->patrol_dir = -gd->patrol_dir;
}
}
/* Animation */
if (fabsf(body->vel.x) > 1.0f) {
animation_set(&self->anim, &anim_grunt_walk);
} else {
animation_set(&self->anim, &anim_grunt_idle);
}
animation_update(&self->anim, dt);
}
static void grunt_render(Entity *self, const Camera *cam) {
(void)cam;
Body *body = &self->body;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
Vec2 render_pos = vec2(
body->pos.x - 2.0f,
body->pos.y
);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = render_pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = (self->flags & ENTITY_FACING_LEFT) != 0,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
} else {
SDL_Color color = {200, 60, 60, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void grunt_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void grunt_register(EntityManager *em) {
entity_register(em, ENT_ENEMY_GRUNT, grunt_update, grunt_render, grunt_destroy);
s_grunt_em = em;
}
Entity *grunt_spawn(EntityManager *em, Vec2 pos) {
Entity *e = entity_spawn(em, ENT_ENEMY_GRUNT, pos);
if (!e) return NULL;
e->body.size = vec2(GRUNT_WIDTH, GRUNT_HEIGHT);
e->body.gravity_scale = 1.0f;
e->health = GRUNT_HEALTH;
e->max_health = GRUNT_HEALTH;
e->damage = 1;
GruntData *gd = calloc(1, sizeof(GruntData));
gd->patrol_dir = 1.0f;
gd->death_timer = 0.3f;
e->data = gd;
return e;
}
/* ════════════════════════════════════════════════════
* FLYER - flying enemy
* ════════════════════════════════════════════════════ */
static EntityManager *s_flyer_em = NULL;
/* Helper: find the player entity in the manager */
static Entity *find_player(EntityManager *em) {
for (int i = 0; i < em->count; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type == ENT_PLAYER) return e;
}
return NULL;
}
static void flyer_update(Entity *self, float dt, const Tilemap *map) {
(void)map; /* flyers don't collide with tiles */
FlyerData *fd = (FlyerData *)self->data;
if (!fd) return;
Body *body = &self->body;
/* Death sequence */
if (self->flags & ENTITY_DEAD) {
animation_set(&self->anim, &anim_flyer_death);
animation_update(&self->anim, dt);
/* Fall when dead */
body->vel.y += physics_get_gravity() * dt;
if (body->vel.y > MAX_FALL_SPEED) body->vel.y = MAX_FALL_SPEED;
body->pos.y += body->vel.y * dt;
fd->death_timer -= dt;
if (fd->death_timer <= 0) {
entity_destroy(s_flyer_em, self);
}
return;
}
/* Bob up and down */
fd->bob_timer += dt;
float bob_offset = sinf(fd->bob_timer * FLYER_BOB_SPD) * FLYER_BOB_AMP;
body->pos.y = fd->base_y + bob_offset;
/* Chase player if in range */
Entity *player = find_player(s_flyer_em);
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
float px = player->body.pos.x + player->body.size.x * 0.5f;
float fx = body->pos.x + body->size.x * 0.5f;
float dist = px - fx;
if (fabsf(dist) < FLYER_DETECT) {
/* Move toward player */
if (dist < -2.0f) {
body->pos.x -= FLYER_SPEED * dt;
self->flags |= ENTITY_FACING_LEFT;
} else if (dist > 2.0f) {
body->pos.x += FLYER_SPEED * dt;
self->flags &= ~ENTITY_FACING_LEFT;
}
/* Shoot at player */
fd->shoot_timer -= dt;
if (fd->shoot_timer <= 0 && s_flyer_em) {
fd->shoot_timer = FLYER_SHOOT_CD;
bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
float bx = facing_left ?
body->pos.x - 4.0f :
body->pos.x + body->size.x;
float by = body->pos.y + body->size.y * 0.4f;
projectile_spawn(s_flyer_em, vec2(bx, by), facing_left, false);
}
}
}
/* Animation */
animation_set(&self->anim, &anim_flyer_fly);
animation_update(&self->anim, dt);
}
static void flyer_render(Entity *self, const Camera *cam) {
(void)cam;
Body *body = &self->body;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
Vec2 render_pos = vec2(
body->pos.x - 1.0f,
body->pos.y - 2.0f
);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = render_pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = (self->flags & ENTITY_FACING_LEFT) != 0,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
} else {
SDL_Color color = {150, 50, 180, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void flyer_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void flyer_register(EntityManager *em) {
entity_register(em, ENT_ENEMY_FLYER, flyer_update, flyer_render, flyer_destroy);
s_flyer_em = em;
}
Entity *flyer_spawn(EntityManager *em, Vec2 pos) {
Entity *e = entity_spawn(em, ENT_ENEMY_FLYER, pos);
if (!e) return NULL;
e->body.size = vec2(FLYER_WIDTH, FLYER_HEIGHT);
e->body.gravity_scale = 0.0f; /* flyers don't fall */
e->health = FLYER_HEALTH;
e->max_health = FLYER_HEALTH;
e->damage = 1;
FlyerData *fd = calloc(1, sizeof(FlyerData));
fd->base_y = pos.y;
fd->death_timer = 0.5f;
fd->shoot_timer = FLYER_SHOOT_CD;
e->data = fd;
return e;
}