/** * @file enemies.c * @brief The formation, the wave, and the hook that hands each enemy to BASIC. * * C owns the grid, the wave table and the spawn timing; BASIC owns everything * an enemy does after it exists. The formation slot arrives in SELF@.HOMEX% / * HOMEY%, so even the idle breathing of the grid is the script's, computed * relative to home. */ #include #include #include #include #include #include #include #include #include #include #include #include "galaga.h" galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES]; akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES]; /* Explosion lifetimes, indexed by heap slot. An explosion is an actor with * nothing to decide, so its whole state is one countdown. */ static float BOOM_TTL[AKGL_MAX_HEAP_ACTOR]; /* A spawn serial per shot so registry names never collide while two shots * with the same slot number are briefly both alive. */ static uint32_t SHOT_SERIAL = 0; static uint32_t BOOM_SERIAL = 0; /* * The wave, one row per formation row. Columns are 0..9 at GALAGA_COL_PITCH; * `first` and `count` say which columns the row fills. 40 enemies: 4 bosses, * 16 butterflies, 20 bees -- 59 of the 64 actor heap slots at peak, counting * the player, two player shots, eight enemy shots and eight explosions. */ static const struct { int32_t kind; /* GALAGA_ENEMY_* */ int row; /* formation row */ int first; /* first column filled */ int count; /* columns filled */ int32_t hp; } WAVE_ROWS[] = { /* kind row first count hp */ { GALAGA_ENEMY_BOSS, 0, 3, 4, 2 }, { GALAGA_ENEMY_BUTTERFLY, 1, 1, 8, 1 }, { GALAGA_ENEMY_BUTTERFLY, 2, 1, 8, 1 }, { GALAGA_ENEMY_BEE, 3, 0, 10, 1 }, { GALAGA_ENEMY_BEE, 4, 0, 10, 1 } }; #define WAVE_ROW_COUNT ((int)(sizeof(WAVE_ROWS) / sizeof(WAVE_ROWS[0]))) /* Enemy kind -> character name, the render half of the dispatch table. */ static char *ENEMY_CHARACTER[GALAGA_ENEMY_KINDS] = { "galaga_bee", "galaga_butterfly", "galaga_boss" }; /* --------------------------------------------------------------- random --- */ /* * No RND verb exists (issue #16), so the engine is the script's only source * of randomness: it refreshes GAME@.RND% each frame and SELF@.RND% each call * from this PRNG. A hand-rolled LCG rather than rand() so a headless run is * the same game on every libc. */ static uint32_t PRNG_STATE = 0x12345678u; float galaga_random(void) { PRNG_STATE = PRNG_STATE * 1664525u + 1013904223u; return (float)(PRNG_STATE >> 8) / (float)0x01000000u; } /* -------------------------------------------------------------- helpers --- */ static akerr_ErrorContext *release_actor(akgl_Actor *actor) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor"); PASS(errctx, akgl_heap_release_actor(actor)); SUCCEED_RETURN(errctx); } /* ---------------------------------------------------------------- shots --- */ /** * @brief Move an enemy shot; release it once it has left the screen. */ static akerr_ErrorContext *enemy_shot_update(akgl_Actor *obj) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); obj->y += 380.0f * galaga_game.dt; if ( obj->y > (float)GALAGA_VIEW_HEIGHT + 60.0f ) { galaga_game.enemy_shots_live -= 1; PASS(errctx, release_actor(obj)); } SUCCEED_RETURN(errctx); } /** * @brief Consume an enemy's fire flag: take an actor and aim it downward. * * The script only raises a flag. Spawning takes a slot from the actor heap, * and pool exhaustion must be a C-side refusal with the house error context -- * so C consumes the flag and does the spawn. The engine also enforces the * eight-shot cap by simply not consuming the flag's wish. */ static akerr_ErrorContext *enemy_fire(galaga_Enemy *enemy, akgl_Actor *from) { akgl_Actor *shot = NULL; char name[32]; int count = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "enemy"); FAIL_ZERO_RETURN(errctx, from, AKERR_NULLPOINTER, "from"); enemy->fire = 0; if ( galaga_game.enemy_shots_live >= GALAGA_MAX_ENEMY_SHOTS ) { SUCCEED_RETURN(errctx); } SHOT_SERIAL += 1; PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "eshot%u", SHOT_SERIAL)); PASS(errctx, akgl_heap_next_actor(&shot)); PASS(errctx, akgl_actor_initialize(shot, name)); PASS(errctx, akgl_actor_set_character(shot, "galaga_enemyshot")); /* AFTER initialize: it resets all seven hooks. */ shot->updatefunc = &enemy_shot_update; shot->movement_controls_face = false; shot->state = AKGL_ACTOR_STATE_ALIVE; /* akgl_actor_initialize() does not raise `visible`; a hand-spawned actor * that skips this line exists, moves and collides -- invisibly. */ shot->visible = true; /* Actor x/y is a sprite's top-left corner; the shot leaves the enemy's * midline. Enemy sprites run 93..104 wide, the shot is 9. */ shot->x = from->x + 46.0f; shot->y = from->y + 60.0f; galaga_game.enemy_shots_live += 1; galaga_game.shots[enemy->kind] += 1; SUCCEED_RETURN(errctx); } /* ----------------------------------------------------------- explosions --- */ static akerr_ErrorContext *boom_update(akgl_Actor *obj) { ptrdiff_t slot = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); slot = obj - akgl_heap_actors; BOOM_TTL[slot] -= galaga_game.dt; if ( BOOM_TTL[slot] <= 0.0f ) { PASS(errctx, release_actor(obj)); } SUCCEED_RETURN(errctx); } akerr_ErrorContext *galaga_boom_spawn(float x, float y) { akgl_Actor *boom = NULL; char name[32]; int count = 0; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_next_actor(&boom)); BOOM_SERIAL += 1; CATCH(errctx, aksl_snprintf(&count, name, sizeof(name), "boom%u", BOOM_SERIAL)); CATCH(errctx, akgl_actor_initialize(boom, name)); CATCH(errctx, akgl_actor_set_character(boom, "galaga_boom")); boom->updatefunc = &boom_update; boom->movement_controls_face = false; boom->state = AKGL_ACTOR_STATE_ALIVE; boom->visible = true; boom->x = x; boom->y = y; BOOM_TTL[boom - akgl_heap_actors] = 0.25f; } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKGL_ERR_HEAP) { /* Explosions are decoration. When the heap is momentarily full the * right outcome is no explosion, not a dead frame -- this is the one * spawn that absorbs exhaustion. */ } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /* --------------------------------------------------------------- enemies --- */ /** * @brief The custom update hook: one enemy, once per frame, thought in BASIC. * * The whole body is the protocol from docs/20: refresh the inbox, hand the * pair to the script, consume the outbox. akgl_game_update() calls this in * place of akgl_actor_update() because spawn replaced the hook. */ static akerr_ErrorContext *enemy_update(akgl_Actor *obj) { galaga_Enemy *enemy = NULL; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); enemy = (galaga_Enemy *)obj->actorData; FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "an enemy actor with no galaga_Enemy attached"); enemy->rnd = galaga_random(); PASS(errctx, galaga_script_update_enemy(enemy, obj, galaga_game.dt)); if ( enemy->fire != 0 ) { PASS(errctx, enemy_fire(enemy, obj)); } SUCCEED_RETURN(errctx); } akerr_ErrorContext *galaga_wave_spawn(void) { akgl_Actor *actor = NULL; galaga_Enemy *enemy = NULL; char name[32]; int row = 0; int col = 0; int index = 0; int count = 0; PREPARE_ERROR(errctx); for ( row = 0; row < WAVE_ROW_COUNT; row++ ) { for ( col = 0; col < WAVE_ROWS[row].count; col++ ) { FAIL_NONZERO_RETURN(errctx, (index >= GALAGA_MAX_ENEMIES), AKERR_OUTOFBOUNDS, "The wave table places more than %d enemies", GALAGA_MAX_ENEMIES); enemy = &galaga_enemies[index]; memset(enemy, 0, sizeof(*enemy)); enemy->kind = WAVE_ROWS[row].kind; enemy->state = GALAGA_ES_ENTERING; enemy->homex = (float)(GALAGA_FORM_LEFT + (WAVE_ROWS[row].first + col) * GALAGA_COL_PITCH); enemy->homey = (float)(GALAGA_FORM_TOP + WAVE_ROWS[row].row * GALAGA_ROW_PITCH); enemy->hp = WAVE_ROWS[row].hp; /* Stagger the entries: each enemy's clock starts in the past, and * the script holds still until its own t crosses zero. */ enemy->t = -0.08f * (float)index; PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "enemy%02d", index)); PASS(errctx, akgl_heap_next_actor(&actor)); PASS(errctx, akgl_actor_initialize(actor, name)); PASS(errctx, akgl_actor_set_character(actor, ENEMY_CHARACTER[enemy->kind])); /* AFTER initialize: it resets all seven hooks. */ actor->updatefunc = &enemy_update; actor->actorData = enemy; /* Nothing here moves by state bits, and an actor whose state word * matches no character mapping is silently not drawn -- so facing * stays entirely out of the state word. */ actor->movement_controls_face = false; actor->state = AKGL_ACTOR_STATE_ALIVE; /* akgl_actor_initialize() does not raise `visible` -- the map * loader copies it from map data, and there is no map here. Skip * this and the whole wave exists, moves, fires and dies without * ever being drawn. */ actor->visible = true; /* Off screen above, pouring in from whichever side is closer. */ actor->x = (enemy->homex < (float)GALAGA_VIEW_WIDTH / 2.0f) ? -80.0f : (float)GALAGA_VIEW_WIDTH + 80.0f; actor->y = -80.0f; galaga_enemy_actors[index] = actor; index += 1; } } SUCCEED_RETURN(errctx); } akerr_ErrorContext *galaga_wave_release(void) { int i = 0; PREPARE_ERROR(errctx); for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) { if ( galaga_enemy_actors[i] != NULL ) { PASS(errctx, release_actor(galaga_enemy_actors[i])); galaga_enemy_actors[i] = NULL; } } SUCCEED_RETURN(errctx); } int galaga_enemies_alive(void) { int i = 0; int alive = 0; for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) { if ( galaga_enemy_actors[i] != NULL ) { alive += 1; } } return alive; }