Improve enemy AI cliff detection, add level-bottom kill, and charger knockback
Some checks failed
Deploy / deploy (push) Failing after 1m14s

Enemies that fall past the bottom of the level are now instantly destroyed,
preventing them from accumulating off-screen. Cliff detection uses a
speed-scaled lookahead so faster enemies reverse earlier. Charger deals
double damage during charge and knockback scales with the attacker's speed.
This commit was merged in pull request #23.
This commit is contained in:
2026-03-16 14:52:36 +00:00
committed by tas
parent 84a257f9b9
commit f65e8dd9ea
2 changed files with 71 additions and 26 deletions

View File

@@ -5,9 +5,46 @@
#include "engine/renderer.h"
#include "engine/particle.h"
#include "engine/audio.h"
#include "config.h"
#include <stdlib.h>
#include <math.h>
/* ── 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);

View File

@@ -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);
}
}
}