#include "game/enemy.h" #include "game/sprites.h" #include "game/projectile.h" #include "engine/physics.h" #include "engine/renderer.h" #include #include /* ════════════════════════════════════════════════════ * 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; }