266 lines
8.1 KiB
C
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;
|
|
}
|