forked from NotAShelf/rogged
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I2f5c7cac72c8772e5871b99026d106b46a6a6964
313 lines
8.3 KiB
C
313 lines
8.3 KiB
C
#include "enemy.h"
|
|
#include "combat.h"
|
|
#include "common.h"
|
|
#include "map.h"
|
|
#include "movement.h"
|
|
#include "rng.h"
|
|
#include "settings.h"
|
|
#include <string.h>
|
|
|
|
// Forward declaration
|
|
int is_enemy_at(const Enemy *enemies, int count, int x, int y);
|
|
|
|
void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
|
|
*count = 0;
|
|
|
|
// Number of enemies scales with floor
|
|
int num_enemies = 3 + floor + rng_int(0, 2);
|
|
if (num_enemies > MAX_ENEMIES)
|
|
num_enemies = MAX_ENEMIES;
|
|
|
|
// Enemy types available for this floor
|
|
int max_type = 1;
|
|
if (floor >= 2)
|
|
max_type = 2;
|
|
if (floor >= 4)
|
|
max_type = 3;
|
|
|
|
for (int i = 0; i < num_enemies; i++) {
|
|
// Find random floor position
|
|
int ex, ey;
|
|
get_random_floor_tile(map, &ex, &ey, 50);
|
|
|
|
// Don't spawn on player position
|
|
if (ex == p->position.x && ey == p->position.y) {
|
|
continue;
|
|
}
|
|
|
|
// Don't spawn on other enemies
|
|
if (is_enemy_at(enemies, *count, ex, ey)) {
|
|
continue;
|
|
}
|
|
|
|
// Create enemy
|
|
Enemy e;
|
|
memset(&e, 0, sizeof(Enemy));
|
|
e.position.x = ex;
|
|
e.position.y = ey;
|
|
e.alive = 1;
|
|
e.type = rng_int(ENEMY_GOBLIN, max_type);
|
|
e.effect_count = 0;
|
|
|
|
// Stats based on type and floor
|
|
// Attack scales with floor: +1 per 2 floors so deeper enemies hit harder
|
|
int floor_atk = floor / 2;
|
|
|
|
switch (e.type) {
|
|
case ENEMY_GOBLIN:
|
|
e.max_hp = ENEMY_BASE_HP + floor;
|
|
e.hp = e.max_hp;
|
|
e.attack = ENEMY_BASE_ATTACK + floor_atk;
|
|
e.speed = 55 + rng_int(0, 10);
|
|
e.dodge = 15;
|
|
e.block = 0;
|
|
e.dmg_class = DMG_POISON;
|
|
e.status_chance = 15;
|
|
e.crit_chance = 8;
|
|
e.crit_mult = 150;
|
|
e.resistance[DMG_SLASH] = 0;
|
|
e.resistance[DMG_IMPACT] = 0;
|
|
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;
|
|
e.hp = e.max_hp;
|
|
e.attack = ENEMY_BASE_ATTACK + 1 + floor_atk;
|
|
e.speed = 70 + rng_int(0, 10);
|
|
e.dodge = 5;
|
|
e.block = 0;
|
|
e.dmg_class = DMG_SLASH;
|
|
e.status_chance = 10;
|
|
e.crit_chance = 6;
|
|
e.crit_mult = 150;
|
|
e.resistance[DMG_SLASH] = -25;
|
|
e.resistance[DMG_IMPACT] = -50;
|
|
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;
|
|
e.hp = e.max_hp;
|
|
e.attack = ENEMY_BASE_ATTACK + 2 + floor_atk;
|
|
e.speed = 85 + rng_int(0, 10);
|
|
e.dodge = 0;
|
|
e.block = 3;
|
|
e.dmg_class = DMG_IMPACT;
|
|
e.status_chance = 20;
|
|
e.crit_chance = 5;
|
|
e.crit_mult = 175;
|
|
e.resistance[DMG_SLASH] = 0;
|
|
e.resistance[DMG_IMPACT] = 25;
|
|
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;
|
|
e.hp = e.max_hp;
|
|
e.attack = ENEMY_BASE_ATTACK;
|
|
e.speed = 60;
|
|
e.dodge = 0;
|
|
e.block = 0;
|
|
e.dmg_class = DMG_IMPACT;
|
|
e.status_chance = 0;
|
|
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;
|
|
|
|
enemies[i] = e;
|
|
(*count)++;
|
|
}
|
|
}
|
|
|
|
// Check if position has an enemy
|
|
int is_enemy_at(const Enemy *enemies, int count, int x, int y) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (enemies[i].alive && enemies[i].position.x == x && enemies[i].position.y == y) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// 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, e->vision_range);
|
|
}
|
|
|
|
|
|
// Move enemy toward player
|
|
static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
|
|
int dx = 0, dy = 0;
|
|
|
|
if (p->position.x > e->position.x)
|
|
dx = 1;
|
|
else if (p->position.x < e->position.x)
|
|
dx = -1;
|
|
|
|
if (p->position.y > e->position.y)
|
|
dy = 1;
|
|
else if (p->position.y < e->position.y)
|
|
dy = -1;
|
|
|
|
Vec2 dir = {dx, 0};
|
|
if (dx != 0) {
|
|
MoveResult r = try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
|
|
if (r == MOVE_RESULT_MOVED)
|
|
return;
|
|
}
|
|
|
|
dir.x = 0;
|
|
dir.y = dy;
|
|
if (dy != 0) {
|
|
try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
|
|
}
|
|
}
|
|
|
|
// Move enemy in a random direction (patrol)
|
|
static void enemy_patrol(Enemy *e, Map *map, Enemy *all_enemies, int enemy_count) {
|
|
if (rng_int(0, 100) > ENEMY_PATROL_MOVE_CHANCE)
|
|
return;
|
|
|
|
int dx = rng_int(-1, 1);
|
|
int dy = rng_int(-1, 1);
|
|
|
|
if (dx == 0 && dy == 0)
|
|
return;
|
|
|
|
int new_x = e->position.x + dx;
|
|
int 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;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Stunned enemies skip their action
|
|
if (combat_has_effect(e->effects, e->effect_count, EFFECT_STUN))
|
|
return;
|
|
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {
|
|
for (int i = 0; i < count; i++) {
|
|
Enemy *e = &enemies[i];
|
|
|
|
if (!e->alive)
|
|
continue;
|
|
|
|
e->cooldown -= e->speed;
|
|
if (e->cooldown <= 0) {
|
|
enemy_act(e, p, map, enemies, count);
|
|
e->cooldown = 100;
|
|
}
|
|
}
|
|
}
|