diff --git a/src/game/enemy.c b/src/game/enemy.c index 7e079a2..5e47627 100644 --- a/src/game/enemy.c +++ b/src/game/enemy.c @@ -5,9 +5,46 @@ #include "engine/renderer.h" #include "engine/particle.h" #include "engine/audio.h" +#include "config.h" #include #include +/* ── Shared helpers ───────────────────────────────── */ + +/* Kill enemy if it fell past the bottom of the level. */ +static bool enemy_check_level_bottom(Entity *self, const Tilemap *map, + EntityManager *em) { + float level_bottom = (float)(map->height * TILE_SIZE); + if (self->body.pos.y > level_bottom) { + self->flags |= ENTITY_DEAD; + self->health = 0; + entity_destroy(em, self); + return true; + } + return false; +} + +/* Check for cliff ahead of a ground enemy. Returns true if cliff detected. + * Uses a wider lookahead that scales with speed for fast-moving enemies. */ +static bool enemy_detect_cliff(const Body *body, float patrol_dir, + float speed, const Tilemap *map) { + if (!body->on_ground) return false; + + /* Scale lookahead with speed: at least 4px, up to speed/10 */ + float lookahead = speed * 0.1f; + if (lookahead < 4.0f) lookahead = 4.0f; + + float check_x = (patrol_dir > 0) ? + body->pos.x + body->size.x + lookahead : + body->pos.x - lookahead; + 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); + + return !tilemap_is_solid(map, tx, ty); +} + /* ════════════════════════════════════════════════════ * GRUNT - ground patrol enemy * ════════════════════════════════════════════════════ */ @@ -20,6 +57,9 @@ static void grunt_update(Entity *self, float dt, const Tilemap *map) { Body *body = &self->body; + /* Kill if fallen off bottom of level */ + if (enemy_check_level_bottom(self, map, s_grunt_em)) return; + /* Death sequence */ if (self->flags & ENTITY_DEAD) { animation_set(&self->anim, &anim_grunt_death); @@ -48,19 +88,10 @@ static void grunt_update(Entity *self, float dt, const Tilemap *map) { 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; - } + /* Turn around at ledge */ + if (enemy_detect_cliff(body, gd->patrol_dir, GRUNT_SPEED, map)) { + gd->patrol_dir = -gd->patrol_dir; + body->vel.x = 0; } /* Animation */ @@ -146,12 +177,14 @@ static Entity *find_player(EntityManager *em) { } 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; + /* Kill if fallen off bottom of level */ + if (enemy_check_level_bottom(self, map, s_flyer_em)) return; + /* Death sequence */ if (self->flags & ENTITY_DEAD) { animation_set(&self->anim, &anim_flyer_death); @@ -289,6 +322,9 @@ static void charger_update(Entity *self, float dt, const Tilemap *map) { Body *body = &self->body; + /* Kill if fallen off bottom of level */ + if (enemy_check_level_bottom(self, map, s_charger_em)) return; + /* Death sequence */ if (self->flags & ENTITY_DEAD) { animation_set(&self->anim, &anim_charger_death); @@ -319,14 +355,10 @@ static void charger_update(Entity *self, float dt, const Tilemap *map) { } /* Reverse at ledge */ - if (body->on_ground) { - float cx = (cd->patrol_dir > 0) ? - body->pos.x + body->size.x + 2.0f : - body->pos.x - 2.0f; - float cy = body->pos.y + body->size.y + 4.0f; - if (!tilemap_is_solid(map, world_to_tile(cx), world_to_tile(cy))) { - cd->patrol_dir = -cd->patrol_dir; - } + if (enemy_detect_cliff(body, cd->patrol_dir, + CHARGER_PATROL_SPEED, map)) { + cd->patrol_dir = -cd->patrol_dir; + body->vel.x = 0; } /* Detect player — horizontal line-of-sight */ @@ -510,10 +542,12 @@ static int count_alive_grunts(EntityManager *em) { } static void spawner_update(Entity *self, float dt, const Tilemap *map) { - (void)map; SpawnerData *sd = (SpawnerData *)self->data; if (!sd) return; + /* Kill if fallen off bottom of level */ + if (enemy_check_level_bottom(self, map, s_spawner_em)) return; + /* Death sequence */ if (self->flags & ENTITY_DEAD) { animation_set(&self->anim, &anim_spawner_death); diff --git a/src/game/level.c b/src/game/level.c index 9fb74a7..877c05d 100644 --- a/src/game/level.c +++ b/src/game/level.c @@ -228,11 +228,14 @@ static void damage_player(Entity *player, int damage, Entity *source) { ppd->inv_timer = PLAYER_INV_TIME; player->flags |= ENTITY_INVINCIBLE; - /* Knockback away from source */ + /* Knockback away from source, scaled by source speed */ if (source) { float knock_dir = (player->body.pos.x < source->body.pos.x) ? -1.0f : 1.0f; - player->body.vel.x = knock_dir * 150.0f; + float src_speed = fabsf(source->body.vel.x); + float knock_str = 150.0f; + if (src_speed > knock_str) knock_str = src_speed; + player->body.vel.x = knock_dir * knock_str; player->body.vel.y = -150.0f; } } @@ -308,7 +311,15 @@ static void handle_collisions(EntityManager *em) { if (a->flags & ENTITY_DEAD) stats_record_kill(); player->body.vel.y = -PLAYER_JUMP_FORCE * 0.7f; } else { - damage_player(player, a->damage, a); + /* Charger deals extra damage and knockback while charging */ + int dmg = a->damage; + if (a->type == ENT_ENEMY_CHARGER) { + ChargerData *cd = (ChargerData *)a->data; + if (cd && cd->state == CHARGER_CHARGE) { + dmg = 2; + } + } + damage_player(player, dmg, a); } } }