#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 #include /* 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); } }