408 lines
12 KiB
C
408 lines
12 KiB
C
|
|
/**
|
||
|
|
* @file player.c
|
||
|
|
* @brief The player's ship, its shots, and every collision in the game.
|
||
|
|
*
|
||
|
|
* Bullets and collision are C forever -- they are engine, not behavior. The
|
||
|
|
* per-frame budget for the script is spent on the forty things that think;
|
||
|
|
* nothing here thinks, it just moves and intersects.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <stdbool.h>
|
||
|
|
#include <stddef.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
#include <akstdlib.h>
|
||
|
|
|
||
|
|
#include <akgl/actor.h>
|
||
|
|
#include <akgl/controller.h>
|
||
|
|
#include <akgl/game.h>
|
||
|
|
#include <akgl/heap.h>
|
||
|
|
#include <akgl/registry.h>
|
||
|
|
#include <akgl/util.h>
|
||
|
|
|
||
|
|
#include "galaga.h"
|
||
|
|
|
||
|
|
/* Points per kill, indexed by enemy kind. */
|
||
|
|
static const int KILL_SCORE[GALAGA_ENEMY_KINDS] = {
|
||
|
|
/* bee butterfly boss */
|
||
|
|
50, 80, 150
|
||
|
|
};
|
||
|
|
|
||
|
|
static uint32_t PSHOT_SERIAL = 0;
|
||
|
|
|
||
|
|
/* -------------------------------------------------------------- hitboxes --- */
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Actor x/y is a sprite's top-left corner. Every box is inset from the
|
||
|
|
* artwork's rectangle, because the PNGs carry transparent margin and wing
|
||
|
|
* tips that should not kill anybody.
|
||
|
|
*/
|
||
|
|
static void player_box(akgl_Actor *actor, SDL_FRect *dest)
|
||
|
|
{
|
||
|
|
dest->x = actor->x + 12.0f;
|
||
|
|
dest->y = actor->y + 8.0f;
|
||
|
|
dest->w = 75.0f;
|
||
|
|
dest->h = 60.0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void enemy_box(akgl_Actor *actor, SDL_FRect *dest)
|
||
|
|
{
|
||
|
|
dest->x = actor->x + 8.0f;
|
||
|
|
dest->y = actor->y + 8.0f;
|
||
|
|
dest->w = 78.0f;
|
||
|
|
dest->h = 68.0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void shot_box(akgl_Actor *actor, SDL_FRect *dest)
|
||
|
|
{
|
||
|
|
dest->x = actor->x;
|
||
|
|
dest->y = actor->y;
|
||
|
|
dest->w = 9.0f;
|
||
|
|
dest->h = 54.0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------- player shots --- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Kill one enemy: score it, blow it up, free its slot.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *kill_enemy(int index)
|
||
|
|
{
|
||
|
|
akgl_Actor *actor = NULL;
|
||
|
|
galaga_Enemy *enemy = NULL;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (index < 0 || index >= GALAGA_MAX_ENEMIES),
|
||
|
|
AKERR_OUTOFBOUNDS, "enemy index %d", index);
|
||
|
|
actor = galaga_enemy_actors[index];
|
||
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "enemy %d is already gone", index);
|
||
|
|
enemy = &galaga_enemies[index];
|
||
|
|
|
||
|
|
galaga_game.score += KILL_SCORE[enemy->kind];
|
||
|
|
galaga_game.kills[enemy->kind] += 1;
|
||
|
|
PASS(errctx, galaga_boom_spawn(actor->x + 20.0f, actor->y + 20.0f));
|
||
|
|
PASS(errctx, akgl_heap_release_actor(actor));
|
||
|
|
galaga_enemy_actors[index] = NULL;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Move a player shot and test it against every live enemy.
|
||
|
|
*
|
||
|
|
* The classic O(shots x enemies) sweep: at most 2 x 40 rectangle tests a
|
||
|
|
* frame, which is noise. A hit costs the enemy a point of hp; the boss's
|
||
|
|
* second point is the script's business to survive, not this file's.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *player_shot_update(akgl_Actor *obj)
|
||
|
|
{
|
||
|
|
SDL_FRect mine;
|
||
|
|
SDL_FRect theirs;
|
||
|
|
bool hit = false;
|
||
|
|
int i = 0;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||
|
|
obj->y -= 900.0f * galaga_game.dt;
|
||
|
|
if ( obj->y < -60.0f ) {
|
||
|
|
galaga_game.player_shots_live -= 1;
|
||
|
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
shot_box(obj, &mine);
|
||
|
|
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
|
||
|
|
if ( galaga_enemy_actors[i] == NULL ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
enemy_box(galaga_enemy_actors[i], &theirs);
|
||
|
|
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
|
||
|
|
if ( !hit ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
galaga_enemies[i].hp -= 1;
|
||
|
|
if ( galaga_enemies[i].hp <= 0 ) {
|
||
|
|
PASS(errctx, kill_enemy(i));
|
||
|
|
}
|
||
|
|
galaga_game.player_shots_live -= 1;
|
||
|
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *player_fire(akgl_Actor *player)
|
||
|
|
{
|
||
|
|
akgl_Actor *shot = NULL;
|
||
|
|
char name[32];
|
||
|
|
int count = 0;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
|
||
|
|
|
||
|
|
PSHOT_SERIAL += 1;
|
||
|
|
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "pshot%u", PSHOT_SERIAL));
|
||
|
|
PASS(errctx, akgl_heap_next_actor(&shot));
|
||
|
|
PASS(errctx, akgl_actor_initialize(shot, name));
|
||
|
|
PASS(errctx, akgl_actor_set_character(shot, "galaga_playershot"));
|
||
|
|
/* AFTER initialize: it resets all seven hooks. */
|
||
|
|
shot->updatefunc = &player_shot_update;
|
||
|
|
shot->movement_controls_face = false;
|
||
|
|
shot->state = AKGL_ACTOR_STATE_ALIVE;
|
||
|
|
shot->visible = true;
|
||
|
|
shot->x = player->x + 45.0f;
|
||
|
|
shot->y = player->y - 44.0f;
|
||
|
|
|
||
|
|
galaga_game.player_shots_live += 1;
|
||
|
|
galaga_game.fire_cooldown = 0.22f;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------------------------- player hit --- */
|
||
|
|
|
||
|
|
static akerr_ErrorContext *player_hit(akgl_Actor *player)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
|
||
|
|
galaga_game.lives -= 1;
|
||
|
|
galaga_game.respawn_timer = 2.0f;
|
||
|
|
PASS(errctx, galaga_boom_spawn(player->x + 25.0f, player->y + 10.0f));
|
||
|
|
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The player's own update hook: motion, fire, and what can kill it.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *player_update(akgl_Actor *obj)
|
||
|
|
{
|
||
|
|
SDL_FRect mine;
|
||
|
|
SDL_FRect theirs;
|
||
|
|
bool hit = false;
|
||
|
|
float dx = 0.0f;
|
||
|
|
int i = 0;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||
|
|
|
||
|
|
dx = 0.0f;
|
||
|
|
if ( galaga_game.moveleft ) {
|
||
|
|
dx -= GALAGA_PLAYER_SPEED;
|
||
|
|
}
|
||
|
|
if ( galaga_game.moveright ) {
|
||
|
|
dx += GALAGA_PLAYER_SPEED;
|
||
|
|
}
|
||
|
|
obj->x += dx * galaga_game.dt;
|
||
|
|
if ( obj->x < GALAGA_PLAYER_MARGIN ) {
|
||
|
|
obj->x = GALAGA_PLAYER_MARGIN;
|
||
|
|
}
|
||
|
|
if ( obj->x > (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f ) {
|
||
|
|
obj->x = (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
galaga_game.fire_cooldown -= galaga_game.dt;
|
||
|
|
if ( galaga_game.firing
|
||
|
|
&& galaga_game.fire_cooldown <= 0.0f
|
||
|
|
&& galaga_game.player_shots_live < GALAGA_MAX_PLAYER_SHOTS
|
||
|
|
&& galaga_game.screen == GALAGA_SCREEN_PLAY ) {
|
||
|
|
PASS(errctx, player_fire(obj));
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Respawn grace: two seconds of blinking invulnerability. The blink is
|
||
|
|
* the `visible` flag, which is deliberate hiding -- the actor still
|
||
|
|
* updates, it just is not drawn on the off frames.
|
||
|
|
*/
|
||
|
|
if ( galaga_game.respawn_timer > 0.0f ) {
|
||
|
|
galaga_game.respawn_timer -= galaga_game.dt;
|
||
|
|
obj->visible = ((galaga_game.frame / 6) % 2) == 0;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
obj->visible = true;
|
||
|
|
|
||
|
|
player_box(obj, &mine);
|
||
|
|
|
||
|
|
/* Enemy shots. */
|
||
|
|
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||
|
|
if ( akgl_heap_actors[i].refcount == 0 ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( strncmp(akgl_heap_actors[i].name, "eshot", 5) != 0 ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
shot_box(&akgl_heap_actors[i], &theirs);
|
||
|
|
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
|
||
|
|
if ( hit ) {
|
||
|
|
galaga_game.enemy_shots_live -= 1;
|
||
|
|
PASS(errctx, akgl_heap_release_actor(&akgl_heap_actors[i]));
|
||
|
|
PASS(errctx, player_hit(obj));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Diving enemies. The formation never reaches this low, so testing all
|
||
|
|
* forty is the same answer as testing the divers, without a state read. */
|
||
|
|
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
|
||
|
|
if ( galaga_enemy_actors[i] == NULL ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
enemy_box(galaga_enemy_actors[i], &theirs);
|
||
|
|
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
|
||
|
|
if ( hit ) {
|
||
|
|
PASS(errctx, kill_enemy(i));
|
||
|
|
PASS(errctx, player_hit(obj));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------------------------- controls --- */
|
||
|
|
|
||
|
|
static akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)event;
|
||
|
|
galaga_game.moveleft = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)event;
|
||
|
|
galaga_game.moveleft = false;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *right_on(akgl_Actor *obj, SDL_Event *event)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)event;
|
||
|
|
galaga_game.moveright = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *right_off(akgl_Actor *obj, SDL_Event *event)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)event;
|
||
|
|
galaga_game.moveright = false;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *fire_on(akgl_Actor *obj, SDL_Event *event)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)event;
|
||
|
|
galaga_game.firing = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *fire_off(akgl_Actor *obj, SDL_Event *event)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)event;
|
||
|
|
galaga_game.firing = false;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *galaga_player_controls(void)
|
||
|
|
{
|
||
|
|
akgl_Control control;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
memset(&control, 0, sizeof(control));
|
||
|
|
control.event_on = SDL_EVENT_KEY_DOWN;
|
||
|
|
control.event_off = SDL_EVENT_KEY_UP;
|
||
|
|
|
||
|
|
control.key = SDLK_LEFT;
|
||
|
|
control.handler_on = &left_on;
|
||
|
|
control.handler_off = &left_off;
|
||
|
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||
|
|
|
||
|
|
control.key = SDLK_RIGHT;
|
||
|
|
control.handler_on = &right_on;
|
||
|
|
control.handler_off = &right_off;
|
||
|
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||
|
|
|
||
|
|
control.key = SDLK_SPACE;
|
||
|
|
control.handler_on = &fire_on;
|
||
|
|
control.handler_off = &fire_off;
|
||
|
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||
|
|
|
||
|
|
akgl_controlmaps[0].target = galaga_game.player;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------------------------------- spawn --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *galaga_player_spawn(void)
|
||
|
|
{
|
||
|
|
akgl_Actor *player = NULL;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_next_actor(&player));
|
||
|
|
PASS(errctx, akgl_actor_initialize(player, "player"));
|
||
|
|
PASS(errctx, akgl_actor_set_character(player, "galaga_player"));
|
||
|
|
/* AFTER initialize: it resets all seven hooks. */
|
||
|
|
player->updatefunc = &player_update;
|
||
|
|
player->movement_controls_face = false;
|
||
|
|
player->state = AKGL_ACTOR_STATE_ALIVE;
|
||
|
|
player->visible = true;
|
||
|
|
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
|
||
|
|
player->y = GALAGA_PLAYER_Y;
|
||
|
|
|
||
|
|
galaga_game.player = player;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------------------------- autoplay --- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Send one synthetic key event through the controller.
|
||
|
|
*
|
||
|
|
* Through akgl_controller_handle_event(), never the handlers directly: the
|
||
|
|
* point of autoplay is to exercise the same path a keyboard does.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *synth_key(SDL_Keycode key, bool down)
|
||
|
|
{
|
||
|
|
SDL_Event event;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
memset(&event, 0, sizeof(event));
|
||
|
|
event.type = (down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP);
|
||
|
|
event.key.key = key;
|
||
|
|
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The scripted pilot for headless runs: hold fire, sweep the floor.
|
||
|
|
*/
|
||
|
|
akerr_ErrorContext *galaga_player_autoplay(int frame)
|
||
|
|
{
|
||
|
|
int phase = 0;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
/* Hold fire until the wave has mostly assembled: shooting the entry
|
||
|
|
* stream point-blank empties the formation before it exists, which makes
|
||
|
|
* both the game and its figure worse. */
|
||
|
|
if ( frame == 300 ) {
|
||
|
|
PASS(errctx, synth_key(SDLK_SPACE, true));
|
||
|
|
}
|
||
|
|
phase = frame % 240;
|
||
|
|
if ( phase == 30 ) {
|
||
|
|
PASS(errctx, synth_key(SDLK_LEFT, true));
|
||
|
|
} else if ( phase == 90 ) {
|
||
|
|
PASS(errctx, synth_key(SDLK_LEFT, false));
|
||
|
|
PASS(errctx, synth_key(SDLK_RIGHT, true));
|
||
|
|
} else if ( phase == 210 ) {
|
||
|
|
PASS(errctx, synth_key(SDLK_RIGHT, false));
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|