/* * This software is licensed under the terms of the MIT License. * See COPYING for further information. * --- * Copyright (c) 2011-2024, Lukas Weber . * Copyright (c) 2012-2024, Andrei Alexeyev . */ #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); }