enemy: extended events

This is based on similar work done for projectiles in f25c8894
Enables interception of damage events and ability to determine the cause
of death.

This also subtly changes the scheduling of 'killed' events; they are
fired as soon as the death is registered now instead of just before
deletion.
This commit is contained in:
Andrei Alexeyev 2021-05-03 21:37:22 +03:00
parent b269ecde52
commit 7da2c7e5ad
No known key found for this signature in database
GPG key ID: 72D26128040B9690
2 changed files with 69 additions and 11 deletions

View file

@ -57,14 +57,32 @@ static void fix_pos0_visual(Enemy *e) {
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 int enemy_call_logic_rule(Enemy *e, int t) {
assert(e->damage_info == NULL);
if(t == EVENT_KILLED) {
coevent_signal(&e->events.killed);
signal_event_once_with_damage_info(e, &e->events.killed, NULL);
}
if(e->logic_rule) {
return e->logic_rule(e, t);
} else {
} else if(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(creal(v)) >= 1;
@ -116,7 +134,7 @@ Enemy *create_enemy_p(EnemyList *enemies, cmplx pos, float hp, EnemyVisualRule v
e->ent.draw_func = ent_draw_enemy;
e->ent.damage_func = ent_damage_enemy;
coevent_init(&e->events.killed);
COEVENT_INIT_ARRAY(e->events);
fix_pos0_visual(e);
ent_register(&e->ent, ENT_TYPE_ID(Enemy));
@ -161,7 +179,7 @@ static void enemy_death_effect(cmplx pos) {
);
}
static void* _delete_enemy(ListAnchor *enemies, List* enemy, void *arg) {
static void *_delete_enemy(ListAnchor *enemies, List* enemy, void *arg) {
Enemy *e = (Enemy*)enemy;
if(e->hp <= 0 && !(e->flags & EFLAG_NO_DEATH_EXPLOSION)) {
@ -175,8 +193,8 @@ static void* _delete_enemy(ListAnchor *enemies, List* enemy, void *arg) {
}
}
COEVENT_CANCEL_ARRAY(e->events);
enemy_call_logic_rule(e, EVENT_DEATH);
coevent_cancel(&e->events.killed);
ent_unregister(&e->ent);
objpool_release(stage_object_pools.enemies, alist_unlink(enemies, enemy));
@ -279,6 +297,7 @@ bool enemy_in_viewport(Enemy *enemy) {
}
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;
}
@ -292,16 +311,29 @@ void enemy_kill_all(EnemyList *enemies) {
static DamageResult ent_damage_enemy(EntityInterface *ienemy, const DamageInfo *dmg) {
Enemy *enemy = ENT_CAST(ienemy, Enemy);
if(!enemy_is_vulnerable(enemy) || dmg->type == DMG_ENEMY_SHOT || dmg->type == DMG_ENEMY_COLLISION) {
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 -= dmg->amount;
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(dmg->type == DMG_PLAYER_DISCHARGE) {
if(ndmg.type == DMG_PLAYER_DISCHARGE) {
spawn_and_collect_items(enemy->pos, 1, ITEM_VOLTAGE, (int)imax(1, enemy->spawn_hp / 100));
}
}

View file

@ -71,9 +71,35 @@ DEFINE_ENTITY_TYPE(Enemy, {
EnemyLogicRule logic_rule;
EnemyVisualRule visual_rule;
struct {
CoEvent killed;
} events;
COEVENTS_ARRAY(
predamage,
damaged,
killed
) events;
/*
* This field is usually NULL except during handling of "predamage", "damaged", and "killed"
* events.
*
* "Predamage" events are run any time something attempts to damage the enemy, before the
* damage is applied. The event handler may modify the DamageInfo struct to affect the outcome.
*
* "Damaged" events are run after the damage has been applied. Immunities are not considered
* successful applications. `damage_info` describes the damage that has been applied.
*
* If after the "damaged" event health drops to 0 or lower, the "killed" event is signaled and
* the enemy is marked for removal. `damage_info` describes the cause of death.
*
* The "damaged" event is also signaled when an enemy is killed through non-damage means, such
* as calling `enemy_kill()` directly. In that case, `damage_info` is NULL. This does NOT happen
* if the enemy is auto-removed due to going out of bounds - if you want to catch that case,
* watch for a cancelled "killed" event.
*
* `damage_info` is also NULL when handling a cancelled event of any type.
*
* The pointer becomes invalid as soon as the event handler yields.
*/
DamageInfo *damage_info;
EnemyFlag flags;