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->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,
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 */

View File

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

View File

@@ -38,12 +38,14 @@ static Entity *find_player(EntityManager *em) {
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
* a solid tile or the map boundary. Step size is one
* pixel for accuracy. Max range prevents runaway. */
* a solid tile or the map boundary. Ignores the turret's
* own tile (skip_tx, skip_ty) so wall-mounted turrets can
* shoot outward. Max range prevents runaway. */
static void beam_raycast(const Tilemap *map,
float ox, float oy, float dx, float dy,
int skip_tx, int skip_ty,
float *out_x, float *out_y) {
float max_range = 800.0f; /* pixels */
float step = 1.0f;
@@ -58,6 +60,7 @@ static void beam_raycast(const Tilemap *map,
int tx = (int)(x / TILE_SIZE);
int ty = (int)(y / TILE_SIZE);
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;
}
@@ -111,10 +114,13 @@ static void laser_turret_update(Entity *self, float dt, const Tilemap *map) {
if (dx < 0) 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_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 */
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) {
/* Find the player */
Entity *player = NULL;
@@ -267,7 +261,7 @@ static void handle_collisions(EntityManager *em) {
bool hit = false;
/* Player bullet hits enemies */
if (from_player && is_enemy(b)) {
if (from_player && entity_is_enemy(b)) {
if (physics_overlap(&a->body, &b->body)) {
damage_entity(b, a->damage);
hit = true;
@@ -293,7 +287,7 @@ static void handle_collisions(EntityManager *em) {
/* ── Enemy contact damage to player ──── */
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)) {
/* Check if player is stomping (falling onto enemy from above) */
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 */
if (is_player_proj) {
if (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) continue;
if (!entity_is_enemy(e)) continue;
} else {
if (e->type != ENT_PLAYER) continue;
}