From 71343311ebd827066fadafb1d4a7870dc706869d Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 9 Apr 2026 12:31:52 +0300 Subject: [PATCH] enemy: add alert memory; vision variance based on type Signed-off-by: NotAShelf Change-Id: I2f5c7cac72c8772e5871b99026d106b46a6a6964 --- src/common.h | 5 +++ src/enemy.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/map.c | 2 ++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/common.h b/src/common.h index b8dd19f..474e133 100644 --- a/src/common.h +++ b/src/common.h @@ -104,6 +104,11 @@ typedef struct { int status_chance; int crit_chance; // crit chance percentage (0-100) int crit_mult; // crit damage multiplier percentage (e.g. 150 = 1.5x) + // vision + int vision_range; + int alert; // 1 = aware of player, searching + int last_known_x; // last position where enemy saw player + int last_known_y; // status effects StatusEffect effects[MAX_EFFECTS]; int effect_count; diff --git a/src/enemy.c b/src/enemy.c index 33308c5..57eff34 100644 --- a/src/enemy.c +++ b/src/enemy.c @@ -70,6 +70,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) { e.resistance[DMG_PIERCE] = 0; e.resistance[DMG_FIRE] = -25; e.resistance[DMG_POISON] = 50; + e.vision_range = 7; break; case ENEMY_SKELETON: e.max_hp = ENEMY_BASE_HP + floor + 2; @@ -87,6 +88,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) { e.resistance[DMG_PIERCE] = 50; e.resistance[DMG_FIRE] = 25; e.resistance[DMG_POISON] = 75; + e.vision_range = 6; break; case ENEMY_ORC: e.max_hp = ENEMY_BASE_HP + floor + 4; @@ -104,6 +106,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) { e.resistance[DMG_PIERCE] = -25; e.resistance[DMG_FIRE] = 0; e.resistance[DMG_POISON] = 0; + e.vision_range = 5; break; default: e.max_hp = ENEMY_BASE_HP; @@ -117,6 +120,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) { e.crit_chance = ENEMY_CRIT_CHANCE; e.crit_mult = ENEMY_CRIT_MULT; memset(e.resistance, 0, sizeof(e.resistance)); + e.vision_range = ENEMY_VIEW_RANGE; break; } e.cooldown = e.speed; @@ -138,7 +142,7 @@ int is_enemy_at(const Enemy *enemies, int count, int x, int y) { // Check if enemy can see player (within view range and line of sight) static int can_see_player(Enemy *e, Player *p, Map *map) { - return can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, ENEMY_VIEW_RANGE); + return can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, e->vision_range); } @@ -190,7 +194,68 @@ static void enemy_patrol(Enemy *e, Map *map, Enemy *all_enemies, int enemy_count } } -// Perform a single action for an enemy (attack if visible, otherwise patrol) +// Move enemy toward last known player position +static void enemy_move_to_last_known(Enemy *e, Map *map, Enemy *all_enemies, int enemy_count) { + int dx = 0, dy = 0; + + if (e->last_known_x > e->position.x) + dx = 1; + else if (e->last_known_x < e->position.x) + dx = -1; + + if (e->last_known_y > e->position.y) + dy = 1; + else if (e->last_known_y < e->position.y) + dy = -1; + + int new_x = e->position.x + dx; + int new_y = e->position.y; + + if (dx != 0 && is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y)) { + e->position.x = new_x; + } else if (dy != 0) { + new_x = e->position.x; + new_y = e->position.y + dy; + if (is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y)) { + e->position.x = new_x; + e->position.y = new_y; + } + } + + if (e->position.x == e->last_known_x && e->position.y == e->last_known_y) + e->alert = 0; +} + +// Check if position is within alert radius of another enemy +static int is_nearby_enemy(const Enemy *enemies, int count, int x, int y, int radius) { + for (int i = 0; i < count; i++) { + if (!enemies[i].alive) + continue; + int dx = enemies[i].position.x - x; + int dy = enemies[i].position.y - y; + if (dx * dx + dy * dy <= radius * radius) + return 1; + } + return 0; +} + +// Propagate alert to nearby enemies +static void propagate_alert(Enemy *trigger_enemy, Enemy *all_enemies, int enemy_count) { + for (int i = 0; i < enemy_count; i++) { + Enemy *e = &all_enemies[i]; + if (!e->alive || e == trigger_enemy) + continue; + if (e->alert) + continue; + if (is_nearby_enemy(all_enemies, enemy_count, e->position.x, e->position.y, 5)) { + e->alert = 1; + e->last_known_x = trigger_enemy->last_known_x; + e->last_known_y = trigger_enemy->last_known_y; + } + } +} + +// Perform a single action for an enemy (attack if visible, otherwise patrol or search) void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) { if (!e->alive) return; @@ -201,19 +266,34 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun int can_see = can_see_player(e, p, map); + // If we can see the player, update alert state and last known position + if (can_see) { + e->alert = 1; + e->last_known_x = p->position.x; + e->last_known_y = p->position.y; + } + // Attack if adjacent to player if (can_see && can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, 1)) { combat_enemy_attack(e, p); + propagate_alert(e, all_enemies, enemy_count); return; } // Move toward player if visible if (can_see) { enemy_move_toward_player(e, p, map, all_enemies, enemy_count); + propagate_alert(e, all_enemies, enemy_count); return; } - // Player not visible - patrol randomly + // If alert but can't see player, move toward last known position + if (e->alert) { + enemy_move_to_last_known(e, map, all_enemies, enemy_count); + return; + } + + // Not alert - patrol randomly enemy_patrol(e, map, all_enemies, enemy_count); } diff --git a/src/map.c b/src/map.c index a29889b..b9d5bbc 100644 --- a/src/map.c +++ b/src/map.c @@ -12,6 +12,8 @@ void map_init(Map *map) { map->tiles[y][x] = TILE_WALL; } } + memset(map->visible, 0, sizeof(map->visible)); + memset(map->remembered, 0, sizeof(map->remembered)); map->room_count = 0; }