315 lines
10 KiB
C
315 lines
10 KiB
C
|
|
/**
|
||
|
|
* @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 <stdbool.h>
|
||
|
|
#include <stddef.h>
|
||
|
|
#include <stdint.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
#include <akstdlib.h>
|
||
|
|
|
||
|
|
#include <akgl/actor.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/game.h>
|
||
|
|
#include <akgl/heap.h>
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|