Fix laser beam blocked by own tile, consolidate enemy-type checks

Beam raycast now skips the tile the turret is mounted on, so
wall-embedded laser turrets can shoot outward.

Extract entity_is_enemy() into engine/entity.c as single source
of truth for enemy-type checks. Replaces triplicated type lists
in level.c, drone.c, and projectile.c that caused the collision
bug when new enemy types were added.
This commit is contained in:
Thomas
2026-03-02 20:56:45 +00:00
parent 196b4f35b9
commit af0a9904c2
6 changed files with 33 additions and 19 deletions

View File

@@ -126,3 +126,17 @@ void entity_register(EntityManager *em, EntityType type,
em->render_fn[type] = render; em->render_fn[type] = render;
em->destroy_fn[type] = destroy; em->destroy_fn[type] = destroy;
} }
bool entity_is_enemy(const Entity *e) {
switch (e->type) {
case ENT_ENEMY_GRUNT:
case ENT_ENEMY_FLYER:
case ENT_TURRET:
case ENT_ENEMY_CHARGER:
case ENT_SPAWNER:
case ENT_LASER_TURRET:
return true;
default:
return false;
}
}

View File

@@ -80,4 +80,8 @@ void entity_register(EntityManager *em, EntityType type,
EntityUpdateFn update, EntityRenderFn render, EntityUpdateFn update, EntityRenderFn render,
EntityDestroyFn destroy); EntityDestroyFn destroy);
/* Returns true if the entity is a hostile enemy type.
* Single source of truth — add new enemy types here. */
bool entity_is_enemy(const Entity *e);
#endif /* JNR_ENTITY_H */ #endif /* JNR_ENTITY_H */

View File

@@ -34,9 +34,7 @@ static Entity *find_nearest_enemy(Vec2 from, float range) {
for (int i = 0; i < s_em->count; i++) { for (int i = 0; i < s_em->count; i++) {
Entity *e = &s_em->entities[i]; Entity *e = &s_em->entities[i];
if (!e->active || e->health <= 0) continue; if (!e->active || e->health <= 0) continue;
if (e->type != ENT_ENEMY_GRUNT && e->type != ENT_ENEMY_FLYER && if (!entity_is_enemy(e)) continue;
e->type != ENT_TURRET && e->type != ENT_ENEMY_CHARGER &&
e->type != ENT_SPAWNER && e->type != ENT_LASER_TURRET) continue;
Vec2 epos = vec2(e->body.pos.x + e->body.size.x * 0.5f, Vec2 epos = vec2(e->body.pos.x + e->body.size.x * 0.5f,
e->body.pos.y + e->body.size.y * 0.5f); e->body.pos.y + e->body.size.y * 0.5f);
float dx = epos.x - from.x; float dx = epos.x - from.x;

View File

@@ -38,12 +38,14 @@ static Entity *find_player(EntityManager *em) {
return NULL; return NULL;
} }
/* Raycast from (ox, oy) along (dx, dy) in tile steps. /* Raycast from (ox, oy) along (dx, dy) in pixel steps.
* Returns the world-space endpoint where the beam hits * Returns the world-space endpoint where the beam hits
* a solid tile or the map boundary. Step size is one * a solid tile or the map boundary. Ignores the turret's
* pixel for accuracy. Max range prevents runaway. */ * own tile (skip_tx, skip_ty) so wall-mounted turrets can
* shoot outward. Max range prevents runaway. */
static void beam_raycast(const Tilemap *map, static void beam_raycast(const Tilemap *map,
float ox, float oy, float dx, float dy, float ox, float oy, float dx, float dy,
int skip_tx, int skip_ty,
float *out_x, float *out_y) { float *out_x, float *out_y) {
float max_range = 800.0f; /* pixels */ float max_range = 800.0f; /* pixels */
float step = 1.0f; float step = 1.0f;
@@ -58,6 +60,7 @@ static void beam_raycast(const Tilemap *map,
int tx = (int)(x / TILE_SIZE); int tx = (int)(x / TILE_SIZE);
int ty = (int)(y / TILE_SIZE); int ty = (int)(y / TILE_SIZE);
if (tx < 0 || ty < 0 || tx >= map->width || ty >= map->height) break; if (tx < 0 || ty < 0 || tx >= map->width || ty >= map->height) break;
if (tx == skip_tx && ty == skip_ty) continue;
if (tilemap_is_solid(map, tx, ty)) break; if (tilemap_is_solid(map, tx, ty)) break;
} }
@@ -111,10 +114,13 @@ static void laser_turret_update(Entity *self, float dt, const Tilemap *map) {
if (dx < 0) self->flags |= ENTITY_FACING_LEFT; if (dx < 0) self->flags |= ENTITY_FACING_LEFT;
else self->flags &= ~ENTITY_FACING_LEFT; else self->flags &= ~ENTITY_FACING_LEFT;
/* Compute beam endpoint via raycast */ /* Compute beam endpoint via raycast (skip turret's own tile) */
float dir_x = cosf(ld->aim_angle); float dir_x = cosf(ld->aim_angle);
float dir_y = sinf(ld->aim_angle); float dir_y = sinf(ld->aim_angle);
beam_raycast(map, cx, cy, dir_x, dir_y, &ld->beam_end_x, &ld->beam_end_y); int self_tx = (int)(cx / TILE_SIZE);
int self_ty = (int)(cy / TILE_SIZE);
beam_raycast(map, cx, cy, dir_x, dir_y, self_tx, self_ty,
&ld->beam_end_x, &ld->beam_end_y);
/* Damage cooldown */ /* Damage cooldown */
if (ld->damage_cd > 0) ld->damage_cd -= dt; if (ld->damage_cd > 0) ld->damage_cd -= dt;

View File

@@ -233,12 +233,6 @@ static void damage_player(Entity *player, int damage, Entity *source) {
} }
} }
static bool is_enemy(const Entity *e) {
return e->type == ENT_ENEMY_GRUNT || e->type == ENT_ENEMY_FLYER ||
e->type == ENT_TURRET || e->type == ENT_ENEMY_CHARGER ||
e->type == ENT_SPAWNER || e->type == ENT_LASER_TURRET;
}
static void handle_collisions(EntityManager *em) { static void handle_collisions(EntityManager *em) {
/* Find the player */ /* Find the player */
Entity *player = NULL; Entity *player = NULL;
@@ -267,7 +261,7 @@ static void handle_collisions(EntityManager *em) {
bool hit = false; bool hit = false;
/* Player bullet hits enemies */ /* Player bullet hits enemies */
if (from_player && is_enemy(b)) { if (from_player && entity_is_enemy(b)) {
if (physics_overlap(&a->body, &b->body)) { if (physics_overlap(&a->body, &b->body)) {
damage_entity(b, a->damage); damage_entity(b, a->damage);
hit = true; hit = true;
@@ -293,7 +287,7 @@ static void handle_collisions(EntityManager *em) {
/* ── Enemy contact damage to player ──── */ /* ── Enemy contact damage to player ──── */
if (player && !(player->flags & ENTITY_INVINCIBLE) && if (player && !(player->flags & ENTITY_INVINCIBLE) &&
is_enemy(a) && !(a->flags & ENTITY_DEAD)) { entity_is_enemy(a) && !(a->flags & ENTITY_DEAD)) {
if (physics_overlap(&a->body, &player->body)) { if (physics_overlap(&a->body, &player->body)) {
/* Check if player is stomping (falling onto enemy from above) */ /* Check if player is stomping (falling onto enemy from above) */
bool stomping = (player->body.vel.y > 0) && bool stomping = (player->body.vel.y > 0) &&

View File

@@ -251,9 +251,7 @@ static void projectile_update(Entity *self, float dt, const Tilemap *map) {
/* Player bullets target enemies, enemy bullets target player */ /* Player bullets target enemies, enemy bullets target player */
if (is_player_proj) { if (is_player_proj) {
if (e->type != ENT_ENEMY_GRUNT && e->type != ENT_ENEMY_FLYER && if (!entity_is_enemy(e)) continue;
e->type != ENT_TURRET && e->type != ENT_ENEMY_CHARGER &&
e->type != ENT_SPAWNER && e->type != ENT_LASER_TURRET) continue;
} else { } else {
if (e->type != ENT_PLAYER) continue; if (e->type != ENT_PLAYER) continue;
} }