418 lines
12 KiB
C
418 lines
12 KiB
C
#include "comp/entity.h"
|
|
#include "comp/physical.h"
|
|
#include "comp/visible.h"
|
|
#include "comp/mechanical.h"
|
|
#include "comp/fighter.h"
|
|
#include "aoe.h"
|
|
#include "enemies.h"
|
|
#include "skills.h"
|
|
#include <gint/defs/util.h>
|
|
#include <string.h>
|
|
|
|
/* Declare animations for an enemy */
|
|
#define ANIMS(name, I, W, A, H, D) \
|
|
.anim_idle = &anims_ ## name ## _ ## I, \
|
|
.anim_walking = &anims_ ## name ## _ ## W, \
|
|
.anim_attack = &anims_ ## name ## _ ## A, \
|
|
.anim_hit = &anims_ ## name ## _ ## H, \
|
|
.anim_death = &anims_ ## name ## _ ## D
|
|
|
|
struct bat_ai {
|
|
fixed_t retreat_period;
|
|
};
|
|
|
|
static enemy_t const slime_1 = {
|
|
.name = "Slime",
|
|
.level = 1,
|
|
ANIMS(slime, Idle, Walking, Attack, Hit, Death),
|
|
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
|
|
.limits = {
|
|
.max_speed = fix(1),
|
|
.friction = fix(0.6),
|
|
.max_disruption_speed = fix(999.0),
|
|
},
|
|
.stats_base = { .HP=10, .ATK=8, .MAG=5, .DEF=5 },
|
|
.stats_growth = { .HP=8, .ATK=4, .MAG=4, .DEF=2 },
|
|
|
|
.shadow_size = 4,
|
|
.xp = 2,
|
|
.z = 0,
|
|
.ai_data_size = 0,
|
|
};
|
|
|
|
static enemy_t const bat_2 = {
|
|
.name = "Bat",
|
|
.level = 2,
|
|
ANIMS(bat, Idle, Idle, Idle, Hit, Death),
|
|
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
|
|
.limits = {
|
|
.max_speed = fix(1.8),
|
|
.friction = fix(0.8),
|
|
.max_disruption_speed = fix(999.0),
|
|
},
|
|
.stats_base = { .HP=12, .ATK=10, .MAG=5, .DEF=5 },
|
|
.stats_growth = { .HP=10, .ATK=3, .MAG=2, .DEF=1 },
|
|
|
|
.shadow_size = 4,
|
|
.xp = 8,
|
|
.z = fix(0.75),
|
|
.ai_data_size = sizeof(struct bat_ai),
|
|
};
|
|
|
|
static enemy_t const fire_slime_4 = {
|
|
.name = "Fire slime",
|
|
.level = 4,
|
|
ANIMS(fire_slime, Idle, Walking, Attack, Hit, Death),
|
|
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
|
|
.limits = {
|
|
.max_speed = fix(1),
|
|
.friction = fix(0.6),
|
|
.max_disruption_speed = fix(999.0),
|
|
},
|
|
/* Same as slime/1 */
|
|
.stats_base = { .HP=10, .ATK=8, .MAG=5, .DEF=5 },
|
|
.stats_growth = { .HP=8, .ATK=4, .MAG=4, .DEF=2 },
|
|
|
|
.shadow_size = 4,
|
|
.xp = 22,
|
|
.z = 0,
|
|
.ai_data_size = 0,
|
|
};
|
|
|
|
static enemy_t const albinos_bat_6 = {
|
|
.name = "Albinos bat",
|
|
.level = 6,
|
|
ANIMS(albinos_bat, Idle, Idle, Idle, Hit, Death),
|
|
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
|
|
.limits = {
|
|
.max_speed = fix(2.4),
|
|
.friction = fix(0.7),
|
|
.max_disruption_speed = fix(999.0),
|
|
},
|
|
/* Same as bat/2 */
|
|
.stats_base = { .HP=12, .ATK=10, .MAG=5, .DEF=5 },
|
|
.stats_growth = { .HP=10, .ATK=3, .MAG=2, .DEF=1 },
|
|
|
|
.shadow_size = 4,
|
|
.xp = 40,
|
|
.z = fix(0.5),
|
|
.ai_data_size = sizeof(struct bat_ai),
|
|
};
|
|
|
|
static enemy_t const gunslinger_8 = {
|
|
.name = "Gunslinger",
|
|
.level = 8,
|
|
ANIMS(gunslinger, Idle, Walking, Fire, Hit, Death),
|
|
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
|
|
.limits = {
|
|
.max_speed = fix(1.8),
|
|
.friction = fix(0.8),
|
|
.max_disruption_speed = fix(999.0),
|
|
},
|
|
.stats_base = { .HP=16, .ATK=12, .MAG=3, .DEF=6 },
|
|
.stats_growth = { .HP=12, .ATK=4, .MAG=1, .DEF=1 },
|
|
|
|
.shadow_size = 4,
|
|
.xp = 60,
|
|
.z = fix(0.25),
|
|
.ai_data_size = 0,
|
|
};
|
|
|
|
static enemy_t const water_slime_8 = {
|
|
.name = "Water slime",
|
|
.level = 8,
|
|
ANIMS(water_slime, Idle, Walking, Attack, Hit, Death),
|
|
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
|
|
.limits = {
|
|
.max_speed = fix(1.2),
|
|
.friction = fix(0.4),
|
|
.max_disruption_speed = fix(999.0),
|
|
},
|
|
/* Same as slime/1 */
|
|
.stats_base = { .HP=10, .ATK=8, .MAG=5, .DEF=5 },
|
|
.stats_growth = { .HP=8, .ATK=4, .MAG=4, .DEF=2 },
|
|
|
|
.shadow_size = 4,
|
|
.xp = 32,
|
|
.z = 0,
|
|
.ai_data_size = 0,
|
|
};
|
|
|
|
static enemy_t const chemical_slime_10 = {
|
|
.name = "Chemical slime",
|
|
.level = 10,
|
|
ANIMS(chemical_slime, Idle, Walking, Attack, Hit, Death),
|
|
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
|
|
.limits = {
|
|
.max_speed = fix(0.6),
|
|
.friction = fix(0.8),
|
|
.max_disruption_speed = fix(999.0),
|
|
},
|
|
/* Like as slime/1 but boosted MAG over ATK */
|
|
.stats_base = { .HP=10, .ATK=8, .MAG=5, .DEF=5 },
|
|
.stats_growth = { .HP=8, .ATK=2, .MAG=6, .DEF=2 },
|
|
|
|
.shadow_size = 4,
|
|
.xp = 71,
|
|
.z = 0,
|
|
.ai_data_size = 0,
|
|
};
|
|
|
|
static enemy_t const * const enemies[] = {
|
|
[ENEMY_SLIME_1] = &slime_1,
|
|
[ENEMY_BAT_2] = &bat_2,
|
|
[ENEMY_FIRE_SLIME_4] = &fire_slime_4,
|
|
[ENEMY_ALBINOS_BAT_6] = &albinos_bat_6,
|
|
[ENEMY_GUNSLINGER_8] = &gunslinger_8,
|
|
[ENEMY_WATER_SLIME_8] = &water_slime_8,
|
|
[ENEMY_CHEMICAL_SLIME_10] = &chemical_slime_10,
|
|
};
|
|
|
|
enemy_t const *enemy_data(int enemy_id)
|
|
{
|
|
if(enemy_id < 1 || (size_t)enemy_id >= sizeof enemies / sizeof *enemies)
|
|
return NULL;
|
|
|
|
return enemies[enemy_id];
|
|
}
|
|
|
|
entity_t *enemy_make(int enemy_id)
|
|
{
|
|
if(enemy_id < 0 || (size_t)enemy_id >= sizeof enemies / sizeof *enemies)
|
|
return NULL;
|
|
|
|
entity_t *e = entity_make(physical, visible, mechanical, fighter);
|
|
if(e == NULL)
|
|
return NULL;
|
|
|
|
enemy_t const *data = enemies[enemy_id];
|
|
|
|
/* These will probably be overridden by the caller */
|
|
|
|
physical_t *p = getcomp(e, physical);
|
|
p->x = fix(0);
|
|
p->y = fix(0);
|
|
p->hitbox = data->hitbox;
|
|
p->facing = LEFT;
|
|
|
|
visible_t *v = getcomp(e, visible);
|
|
v->z = data->z;
|
|
v->sprite_plane = VERTICAL;
|
|
v->shadow_size = data->shadow_size;
|
|
visible_set_anim(e, data->anim_idle, 1);
|
|
|
|
mechanical_t *m = getcomp(e, mechanical);
|
|
m->limits = &data->limits;
|
|
m->vx = fix(0);
|
|
m->vy = fix(0);
|
|
m->dash = fix(0);
|
|
m->dash_facing = 0xff;
|
|
|
|
/* Instantiate fighter statistics */
|
|
fighter_t *f = getcomp(e, fighter);
|
|
memset(f, 0, sizeof *f);
|
|
|
|
fighter_stats_t stats = fighter_stats_instantiate(&data->stats_base,
|
|
&data->stats_growth, data->level);
|
|
fighter_set_stats(f, &stats);
|
|
f->HP = f->HP_max;
|
|
f->combo_length = 1;
|
|
f->enemy = malloc(sizeof *f->enemy + data->ai_data_size);
|
|
f->enemy->id = data;
|
|
f->enemy->pathfind_dir = (vec2){ 0, 0 };
|
|
f->enemy->pathfind_cycles = 0;
|
|
f->enemy->ai_data = (void *)f->enemy + sizeof *f->enemy;
|
|
|
|
/* Specify skills and initialize AI data */
|
|
if(enemy_id == ENEMY_BAT_2 || enemy_id == ENEMY_ALBINOS_BAT_6) {
|
|
struct bat_ai *ai_data = f->enemy->ai_data;
|
|
ai_data->retreat_period = 0;
|
|
if(enemy_id == ENEMY_ALBINOS_BAT_6) {
|
|
f->skills[1] = SKILL_SPEED;
|
|
}
|
|
}
|
|
else if(enemy_id == ENEMY_FIRE_SLIME_4) {
|
|
f->skills[1] = AOE_FIRE_CHARGE;
|
|
}
|
|
else if(enemy_id == ENEMY_GUNSLINGER_8) {
|
|
f->skills[0] = AOE_PROJECTILE;
|
|
}
|
|
else if(enemy_id == ENEMY_WATER_SLIME_8) {
|
|
f->skills[1] = AOE_WATER_CHARGE;
|
|
}
|
|
else if(enemy_id == ENEMY_CHEMICAL_SLIME_10) {
|
|
f->skills[1] = AOE_CHEMICAL_CHARGE;
|
|
}
|
|
|
|
return e;
|
|
}
|
|
|
|
/* Enemy AIs */
|
|
|
|
static void move_towards_player(game_t *g, entity_t *e, fixed_t dt)
|
|
{
|
|
physical_t *p = getcomp(e, physical);
|
|
fighter_t *f = getcomp(e, fighter);
|
|
vec2 pos = physical_pos(e);
|
|
vec2 direction = { 0, 0 };
|
|
|
|
if(dist2(pos, physical_pos(g->player)) <= fix(0.25)) /* 0.5^2 */
|
|
return;
|
|
|
|
if(f->enemy && f->enemy->pathfind_cycles > 0) {
|
|
direction = f->enemy->pathfind_dir;
|
|
f->enemy->pathfind_cycles--;
|
|
}
|
|
else {
|
|
pfg_path_t path = pfg_inwards(&g->paths_to_player, vec_f2i(pos));
|
|
if(path.points) {
|
|
direction = pfc_shortcut_one(&path, pos, physical_pos(g->player),
|
|
p->hitbox);
|
|
pfg_path_free(&path);
|
|
}
|
|
|
|
if(direction.x && direction.y) {
|
|
direction.x -= pos.x;
|
|
direction.y -= pos.y;
|
|
}
|
|
|
|
if(f->enemy) {
|
|
f->enemy->pathfind_dir = direction;
|
|
f->enemy->pathfind_cycles = 4;
|
|
}
|
|
}
|
|
|
|
mechanical_move(e, direction, dt, g->map);
|
|
if(direction.x > 0) p->facing = RIGHT;
|
|
else if(direction.x < 0) p->facing = LEFT;
|
|
else if(p->x < getcomp(g->player, physical)->x) p->facing = RIGHT;
|
|
else p->facing = LEFT;
|
|
}
|
|
|
|
static bool move_within_range_of_player(game_t *g, entity_t *e, fixed_t range,
|
|
fixed_t dt)
|
|
{
|
|
if(dist2(physical_pos(e), physical_pos(g->player)) <= fmul(range, range))
|
|
return true;
|
|
|
|
move_towards_player(g, e, dt);
|
|
return false;
|
|
}
|
|
|
|
static bool move_away_from_player_range(game_t *g, entity_t *e, fixed_t range,
|
|
fixed_t dt)
|
|
{
|
|
physical_t *p = getcomp(e, physical);
|
|
fighter_t *f = getcomp(e, fighter);
|
|
if(!p || !f || !f->enemy)
|
|
return true;
|
|
|
|
if(dist2(physical_pos(e), physical_pos(g->player)) > fmul(range, range))
|
|
return true;
|
|
|
|
/* Escape opposite the direction of normal movement */
|
|
vec2 direction = f->enemy->pathfind_dir;
|
|
direction = (vec2){ -direction.x, -direction.y };
|
|
|
|
mechanical_move(e, direction, dt, g->map);
|
|
if(p->x < getcomp(g->player, physical)->x) p->facing = RIGHT;
|
|
else p->facing = LEFT;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool contact_attack(game_t *g, entity_t *e)
|
|
{
|
|
fighter_t *f = getcomp(e, fighter);
|
|
if(f->current_attack)
|
|
return false;
|
|
|
|
physical_t *p = getcomp(e, physical);
|
|
physical_t *player_p = getcomp(g->player, physical);
|
|
|
|
vec2 dir = { player_p->x - p->x, player_p->y - p->y };
|
|
dir = fnormalize(dir);
|
|
|
|
entity_t *aoe = aoe_make_attack(AOE_HIT, e, dir);
|
|
game_add_entity(g, aoe);
|
|
|
|
f->current_attack = aoe;
|
|
f->attack_follows_movement = true;
|
|
visible_set_anim(e, f->enemy->id->anim_attack, 2);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool use_skill_towards_player(game_t *g, entity_t *e, int slot)
|
|
{
|
|
physical_t *p = getcomp(e, physical);
|
|
physical_t *player_p = getcomp(g->player, physical);
|
|
|
|
vec2 dir = { player_p->x - p->x, player_p->y - p->y };
|
|
dir = fnormalize(dir);
|
|
|
|
return skill_use(g, e, slot, dir);
|
|
}
|
|
|
|
void enemy_ai(game_t *g, entity_t *e, fixed_t dt)
|
|
{
|
|
fighter_t *f = getcomp(e, fighter);
|
|
|
|
if(f->enemy->id == &slime_1) {
|
|
if(move_within_range_of_player(g, e, fix(0.5), dt)) {
|
|
contact_attack(g, e);
|
|
}
|
|
}
|
|
|
|
else if(f->enemy->id == &bat_2 || f->enemy->id == &albinos_bat_6) {
|
|
struct bat_ai *ai_data = f->enemy->ai_data;
|
|
|
|
/* After an attack, move away for a couple of seconds */
|
|
if(ai_data->retreat_period > 0) {
|
|
move_away_from_player_range(g, e, fix(3.0), dt);
|
|
ai_data->retreat_period = max(ai_data->retreat_period-dt, fix(0));
|
|
}
|
|
/* Otherwise, get close and attack */
|
|
else {
|
|
/* Albinos bats try and use their speed skills whenever possible */
|
|
if(f->enemy->id == &albinos_bat_6) {
|
|
skill_use(g, e, 1, (vec2){0,0});
|
|
}
|
|
|
|
if(move_within_range_of_player(g, e, fix(0.5), dt)) {
|
|
contact_attack(g, e);
|
|
ai_data->retreat_period = fix(1.5);
|
|
}
|
|
}
|
|
}
|
|
|
|
else if(f->enemy->id == &fire_slime_4 || f->enemy->id == &water_slime_8
|
|
|| f->enemy->id == &chemical_slime_10) {
|
|
if(move_within_range_of_player(g, e, fix(0.75), dt)) {
|
|
/* Use the fire charge attack; if it fails, normal attack */
|
|
if(use_skill_towards_player(g, e, 1)) {
|
|
if(f->enemy->id == &fire_slime_4)
|
|
visible_set_anim(e, &anims_fire_slime_Fire, 2);
|
|
if(f->enemy->id == &water_slime_8)
|
|
visible_set_anim(e, &anims_water_slime_Fire, 2);
|
|
if(f->enemy->id == &chemical_slime_10)
|
|
visible_set_anim(e, &anims_chemical_slime_Fire, 2);
|
|
}
|
|
else {
|
|
contact_attack(g, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
else if(f->enemy->id == &gunslinger_8) {
|
|
rect hitbox = { fix(-1/16), fix(1/16), fix(-1/16), fix(1/16) };
|
|
bool clear = raycast_clear_hitbox(g->map, physical_pos(e),
|
|
physical_pos(g->player), hitbox);
|
|
|
|
if(clear)
|
|
use_skill_towards_player(g, e, 0);
|
|
else
|
|
move_towards_player(g, e, dt);
|
|
}
|
|
}
|