359 lines
8.2 KiB
C
359 lines
8.2 KiB
C
/*
|
|
* This software is licensed under the terms of the MIT License.
|
|
* See COPYING for further information.
|
|
* ---
|
|
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
|
|
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
|
|
*/
|
|
|
|
#include "enemy.h"
|
|
|
|
#include "audio/audio.h"
|
|
#include "entity.h"
|
|
#include "global.h"
|
|
#include "list.h"
|
|
#include "projectile.h"
|
|
#include "resource/resource.h"
|
|
#include "stageobjects.h"
|
|
#include "util/glm.h"
|
|
|
|
#ifdef create_enemy_p
|
|
#undef create_enemy_p
|
|
#endif
|
|
|
|
#ifdef DEBUG
|
|
Enemy *_enemy_attach_dbginfo(Enemy *e, DebugInfo *dbg) {
|
|
memcpy(&e->debug, dbg, sizeof(DebugInfo));
|
|
set_debug_info(dbg);
|
|
return e;
|
|
}
|
|
#endif
|
|
|
|
static void ent_draw_enemy(EntityInterface *ent);
|
|
static DamageResult ent_damage_enemy(EntityInterface *ienemy, const DamageInfo *dmg);
|
|
|
|
static void fix_pos0_visual(Enemy *e) {
|
|
if(e->flags & EFLAG_NO_VISUAL_CORRECTION) {
|
|
return;
|
|
}
|
|
|
|
double x = re(e->pos0_visual);
|
|
double y = im(e->pos0_visual);
|
|
double ofs = 21;
|
|
|
|
if(x <= 0 && x > -ofs) {
|
|
x = -ofs;
|
|
} else if(x >= VIEWPORT_W && x < VIEWPORT_W + ofs) {
|
|
x = VIEWPORT_W + ofs;
|
|
}
|
|
|
|
if(y <= 0 && y > -ofs) {
|
|
y = -ofs;
|
|
} else if(y >= VIEWPORT_H && y < VIEWPORT_H + ofs) {
|
|
y = VIEWPORT_H + ofs;
|
|
}
|
|
|
|
e->pos0_visual = x + y * I;
|
|
}
|
|
|
|
static inline void _signal_event_with_damage_info(Enemy *e, CoEvent *evt, DamageInfo *dmg, void (*sigfunc)(CoEvent*)) {
|
|
assert(e->damage_info == NULL);
|
|
e->damage_info = dmg;
|
|
sigfunc(evt);
|
|
assert(e->damage_info == dmg);
|
|
e->damage_info = NULL;
|
|
}
|
|
|
|
static void signal_event_with_damage_info(Enemy *e, CoEvent *evt, DamageInfo *dmg) {
|
|
_signal_event_with_damage_info(e, evt, dmg, coevent_signal);
|
|
}
|
|
|
|
static void signal_event_once_with_damage_info(Enemy *e, CoEvent *evt, DamageInfo *dmg) {
|
|
_signal_event_with_damage_info(e, evt, dmg, coevent_signal_once);
|
|
}
|
|
|
|
static inline void enemy_update(Enemy *e, int t) {
|
|
assert(e->damage_info == NULL);
|
|
assert(t >= 0);
|
|
|
|
// TODO: backport unified left/right move animations from the obsolete `newart` branch
|
|
cmplx v = move_update(&e->pos, &e->move);
|
|
e->moving = fabs(re(v)) >= 1;
|
|
e->dir = re(v) < 0;
|
|
}
|
|
|
|
Enemy *create_enemy_p(EnemyList *enemies, cmplx pos, float hp, EnemyVisual visual) {
|
|
if(IN_DRAW_CODE) {
|
|
log_fatal("Tried to spawn an enemy while in drawing code");
|
|
}
|
|
|
|
auto e = alist_append(enemies, STAGE_ACQUIRE_OBJ(Enemy));
|
|
e->moving = false;
|
|
e->dir = 0;
|
|
e->birthtime = global.frames;
|
|
e->pos = pos;
|
|
e->pos0 = pos;
|
|
e->pos0_visual = pos;
|
|
e->spawn_hp = hp;
|
|
e->hp = hp;
|
|
e->flags = 0;
|
|
e->visual = visual;
|
|
e->hurt_radius = 7;
|
|
e->hit_radius = 30;
|
|
|
|
e->ent.draw_layer = LAYER_ENEMY;
|
|
e->ent.draw_func = ent_draw_enemy;
|
|
e->ent.damage_func = ent_damage_enemy;
|
|
|
|
COEVENT_INIT_ARRAY(e->events);
|
|
fix_pos0_visual(e);
|
|
ent_register(&e->ent, ENT_TYPE_ID(Enemy));
|
|
|
|
return e;
|
|
}
|
|
|
|
static void enemy_death_effect(cmplx pos) {
|
|
for(int i = 0; i < 10; i++) {
|
|
RNG_ARRAY(rng, 2);
|
|
PARTICLE(
|
|
.sprite = "flare",
|
|
.pos = pos,
|
|
.timeout = 10,
|
|
.draw_rule = pdraw_timeout_fade(1, 0),
|
|
.move = move_linear(vrng_range(rng[0], 3, 13) * vrng_dir(rng[1])),
|
|
);
|
|
}
|
|
|
|
PARTICLE(
|
|
.proto = pp_blast,
|
|
.pos = pos,
|
|
.timeout = 20,
|
|
.draw_rule = pdraw_blast(),
|
|
.flags = PFLAG_REQUIREDPARTICLE
|
|
);
|
|
|
|
PARTICLE(
|
|
.proto = pp_blast,
|
|
.pos = pos,
|
|
.timeout = 20,
|
|
.draw_rule = pdraw_blast(),
|
|
.flags = PFLAG_REQUIREDPARTICLE
|
|
);
|
|
|
|
PARTICLE(
|
|
.proto = pp_blast,
|
|
.pos = pos,
|
|
.timeout = 15,
|
|
.draw_rule = pdraw_timeout_scalefade(0, rng_f32_range(1, 2), 1, 0),
|
|
.flags = PFLAG_REQUIREDPARTICLE
|
|
);
|
|
}
|
|
|
|
static void *_delete_enemy(ListAnchor *enemies, List* enemy, void *arg) {
|
|
Enemy *e = (Enemy*)enemy;
|
|
|
|
if(e->hp <= 0 && !(e->flags & EFLAG_NO_DEATH_EXPLOSION)) {
|
|
play_sfx("enemydeath");
|
|
enemy_death_effect(e->pos);
|
|
|
|
for(Projectile *p = global.projs.first; p; p = p->next) {
|
|
if(p->type == PROJ_ENEMY && !(p->flags & PFLAG_NOCOLLISION) && cabs(p->pos - e->pos) < 64) {
|
|
spawn_and_collect_item(e->pos, ITEM_PIV, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
COEVENT_CANCEL_ARRAY(e->events);
|
|
ent_unregister(&e->ent);
|
|
STAGE_RELEASE_OBJ(alist_unlink(enemies, e));
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void delete_enemy(EnemyList *enemies, Enemy* enemy) {
|
|
_delete_enemy((ListAnchor*)enemies, (List*)enemy, NULL);
|
|
}
|
|
|
|
void delete_enemies(EnemyList *enemies) {
|
|
alist_foreach(enemies, _delete_enemy, NULL);
|
|
}
|
|
|
|
cmplx enemy_visual_pos(Enemy *enemy) {
|
|
if(enemy->flags & EFLAG_NO_VISUAL_CORRECTION) {
|
|
return enemy->pos;
|
|
}
|
|
|
|
real t = (global.frames - enemy->birthtime) / 30.0;
|
|
|
|
if(t >= 1) {
|
|
return enemy->pos;
|
|
}
|
|
|
|
cmplx p = enemy->pos - enemy->pos0;
|
|
p += t * enemy->pos0 + (1 - t) * enemy->pos0_visual;
|
|
|
|
return p;
|
|
}
|
|
|
|
static void draw_enemy(Enemy *e) {
|
|
e->visual.draw(e, (EnemyDrawParams) {
|
|
.time = global.frames - e->birthtime,
|
|
.pos = enemy_visual_pos(e),
|
|
});
|
|
}
|
|
|
|
static void ent_draw_enemy(EntityInterface *ent) {
|
|
Enemy *e = ENT_CAST(ent, Enemy);
|
|
|
|
if(!e->visual.draw) {
|
|
return;
|
|
}
|
|
|
|
#ifdef ENEMY_DEBUG
|
|
static Enemy prev_state;
|
|
memcpy(&prev_state, e, sizeof(Enemy));
|
|
#endif
|
|
|
|
draw_enemy(e);
|
|
|
|
#ifdef ENEMY_DEBUG
|
|
if(memcmp(&prev_state, e, sizeof(Enemy))) {
|
|
set_debug_info(&e->debug);
|
|
log_fatal("Enemy modified its own state in draw rule");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool enemy_is_vulnerable(Enemy *enemy) {
|
|
return !(enemy->flags & EFLAG_INVULNERABLE);
|
|
}
|
|
|
|
bool enemy_is_targetable(Enemy *enemy) {
|
|
return !(enemy->flags & EFLAG_NO_HIT);
|
|
}
|
|
|
|
bool enemy_in_viewport(Enemy *enemy) {
|
|
// FIXME: Ideally this is supposed to be the size of the visual, as in with projectiles, but
|
|
// we don't have access to this information here.
|
|
real base = 60;
|
|
|
|
real s = base + enemy->max_viewport_dist;
|
|
|
|
return
|
|
re(enemy->pos) >= -s &&
|
|
re(enemy->pos) <= VIEWPORT_W + s &&
|
|
im(enemy->pos) >= -s &&
|
|
im(enemy->pos) <= VIEWPORT_H + s;
|
|
}
|
|
|
|
void enemy_kill(Enemy *enemy) {
|
|
signal_event_once_with_damage_info(enemy, &enemy->events.killed, NULL);
|
|
enemy->flags |= EFLAG_KILLED | EFLAG_NO_HIT | EFLAG_NO_HURT | EFLAG_INVULNERABLE;
|
|
enemy->hp = 0;
|
|
}
|
|
|
|
void enemy_kill_all(EnemyList *enemies) {
|
|
for(Enemy *e = enemies->first; e; e = e->next) {
|
|
enemy_kill(e);
|
|
}
|
|
}
|
|
|
|
static DamageResult ent_damage_enemy(EntityInterface *ienemy, const DamageInfo *dmg) {
|
|
Enemy *enemy = ENT_CAST(ienemy, Enemy);
|
|
|
|
if(UNLIKELY(enemy->flags & EFLAG_KILLED)) {
|
|
return DMG_RESULT_INAPPLICABLE;
|
|
}
|
|
|
|
DamageInfo ndmg = *dmg;
|
|
signal_event_with_damage_info(enemy, &enemy->events.predamage, &ndmg);
|
|
|
|
if(
|
|
!enemy_is_vulnerable(enemy) ||
|
|
ndmg.type == DMG_ENEMY_SHOT ||
|
|
ndmg.type == DMG_ENEMY_COLLISION
|
|
) {
|
|
return DMG_RESULT_IMMUNE;
|
|
}
|
|
|
|
enemy->hp -= ndmg.amount;
|
|
signal_event_with_damage_info(enemy, &enemy->events.damaged, &ndmg);
|
|
|
|
if(enemy->hp <= 0) {
|
|
signal_event_once_with_damage_info(enemy, &enemy->events.killed, &ndmg);
|
|
enemy_kill(enemy);
|
|
|
|
if(ndmg.type == DMG_PLAYER_DISCHARGE) {
|
|
spawn_and_collect_items(enemy->pos, 1, ITEM_VOLTAGE, (int)max(1, enemy->spawn_hp / 100));
|
|
}
|
|
}
|
|
|
|
if(enemy->hp < enemy->spawn_hp * 0.1) {
|
|
play_sfx_loop("hit1");
|
|
} else {
|
|
play_sfx_loop("hit0");
|
|
}
|
|
|
|
return DMG_RESULT_OK;
|
|
}
|
|
|
|
float enemy_get_hurt_radius(Enemy *enemy) {
|
|
return (enemy->flags & EFLAG_NO_HURT) ? 0 : enemy->hurt_radius;
|
|
}
|
|
|
|
static bool should_auto_kill(Enemy *enemy) {
|
|
return
|
|
(enemy->hp <= 0) ||
|
|
(!(enemy->flags & EFLAG_NO_AUTOKILL) && !enemy_in_viewport(enemy));
|
|
}
|
|
|
|
void process_enemies(EnemyList *enemies) {
|
|
for(Enemy *enemy = enemies->first, *next; enemy; enemy = next) {
|
|
next = enemy->next;
|
|
|
|
if(enemy->flags & EFLAG_KILLED) {
|
|
signal_event_once_with_damage_info(enemy, &enemy->events.killed, NULL);
|
|
delete_enemy(enemies, enemy);
|
|
continue;
|
|
}
|
|
|
|
enemy_update(enemy, global.frames - enemy->birthtime);
|
|
|
|
float hurt_radius = enemy_get_hurt_radius(enemy);
|
|
|
|
if(hurt_radius > 0 && cabs(enemy->pos - global.plr.pos) < hurt_radius) {
|
|
ent_damage(&global.plr.ent, &(DamageInfo) { .type = DMG_ENEMY_COLLISION });
|
|
}
|
|
|
|
if(should_auto_kill(enemy)) {
|
|
delete_enemy(enemies, enemy);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
void enemies_preload(ResourceGroup *rg) {
|
|
res_group_preload(rg, RES_ANIM, RESF_DEFAULT,
|
|
"enemy/fairy_blue",
|
|
"enemy/fairy_red",
|
|
"enemy/bigfairy",
|
|
"enemy/hugefairy",
|
|
"enemy/superfairy",
|
|
NULL);
|
|
|
|
res_group_preload(rg, RES_SPRITE, RESF_DEFAULT,
|
|
"fairy_circle",
|
|
"fairy_circle_red",
|
|
"fairy_circle_big",
|
|
"fairy_circle_big_and_mean",
|
|
"enemy/swirl",
|
|
NULL);
|
|
|
|
res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT,
|
|
"sprite_fairy",
|
|
NULL);
|
|
|
|
res_group_preload(rg, RES_SFX, RESF_OPTIONAL,
|
|
"enemydeath",
|
|
NULL);
|
|
}
|