/** * @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 #include #include #include #include #include #include #include #include #include #include #include #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); }