/** * @file main.c * @brief Startup, the frame loop, the screens, and teardown. * * The startup order is libakgl's one sequence that works (deps/libakgl * include/akgl/game.h): metadata, akgl_game_init(), screen properties, * akgl_render_2d_init(), a physics backend -- and then, new in this example, * the interpreter. The scripts compute, the engine draws; the interpreter is * lent no devices at all, so a script that tries SPRITE is refused by name. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "galaga.h" /** @brief Where the example's assets live. CMake defines it; `--assets` overrides. */ #ifndef GALAGA_ASSET_DIR #define GALAGA_ASSET_DIR "." #endif /** @brief The enemy script. CMake defines it; `--script` overrides. */ #ifndef GALAGA_SCRIPT_PATH #define GALAGA_SCRIPT_PATH "galaga.bas" #endif /** @brief The HUD font. CMake defines it; headless runs still need it for the UI. */ #ifndef GALAGA_FONT_PATH #define GALAGA_FONT_PATH "font.ttf" #endif #define GALAGA_PATH_MAX 1024 galaga_Game galaga_game; galaga_Shared galaga_shared; /** @brief Where `--screenshot` writes, and on which frame. NULL means never. */ static char *SHOTPATH = NULL; static int SHOTFRAME = 0; /** @brief Set in HANDLE_DEFAULT and read after FINISH; see the note in main. */ static int FAILED = 0; /* ------------------------------------------------------------- starfield --- */ /* * No parallax facility exists in libakgl and none is needed: a fixed array of * stars advanced per frame and drawn with akgl_draw_point() between * frame_start and akgl_game_update(). Two speed bands give the depth for * free -- the slow band reads as far away. */ #define GALAGA_STARS 96 static struct { float x; float y; float speed; Uint8 bright; } STARS[GALAGA_STARS]; static void starfield_seed(void) { int i = 0; for ( i = 0; i < GALAGA_STARS; i++ ) { STARS[i].x = galaga_random() * (float)GALAGA_VIEW_WIDTH; STARS[i].y = galaga_random() * (float)GALAGA_VIEW_HEIGHT; if ( (i % 2) == 0 ) { STARS[i].speed = 40.0f; /* the far band */ STARS[i].bright = 110; } else { STARS[i].speed = 110.0f; /* the near band */ STARS[i].bright = 220; } } } static akerr_ErrorContext *starfield_draw(void) { SDL_Color color = { 255, 255, 255, 255 }; int i = 0; PREPARE_ERROR(errctx); for ( i = 0; i < GALAGA_STARS; i++ ) { STARS[i].y += STARS[i].speed * galaga_game.dt; if ( STARS[i].y > (float)GALAGA_VIEW_HEIGHT ) { STARS[i].y -= (float)GALAGA_VIEW_HEIGHT; STARS[i].x = galaga_random() * (float)GALAGA_VIEW_WIDTH; } color.r = STARS[i].bright; color.g = STARS[i].bright; color.b = STARS[i].bright; PASS(errctx, akgl_draw_point(akgl_renderer, STARS[i].x, STARS[i].y, color)); } SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------ screenshots --- */ /** * @brief Read the render target back and write it out as a PNG. * * Called after everything has drawn and before the frame is presented, * because SDL_RenderPresent is where the target stops being readable. The * figures in docs/20 and docs/21 are output from this program rather than * pictures somebody took once, so they cannot show a game that no longer * exists. */ static akerr_ErrorContext *save_screenshot(char *path) { SDL_Surface *shot = NULL; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_RETURN(errctx, shot, AKGL_ERR_SDL, "SDL_RenderReadPixels: %s", SDL_GetError()); ATTEMPT { FAIL_ZERO_BREAK(errctx, IMG_SavePNG(shot, path), AKGL_ERR_SDL, "IMG_SavePNG(%s): %s", path, SDL_GetError()); } CLEANUP { SDL_DestroySurface(shot); } PROCESS(errctx) { } FINISH(errctx, true); SDL_Log("Wrote %s", path); SUCCEED_RETURN(errctx); } /* ---------------------------------------------------------------- assets --- */ static char *SPRITE_FILES[] = { "sprite_galaga_player.json", "sprite_galaga_bee.json", "sprite_galaga_butterfly.json", "sprite_galaga_boss.json", "sprite_galaga_boss_hurt.json", "sprite_galaga_playershot.json", "sprite_galaga_enemyshot.json", "sprite_galaga_boom.json", NULL }; static char *CHARACTER_FILES[] = { "character_galaga_player.json", "character_galaga_bee.json", "character_galaga_butterfly.json", "character_galaga_boss.json", "character_galaga_playershot.json", "character_galaga_enemyshot.json", "character_galaga_boom.json", NULL }; static akerr_ErrorContext *asset_path(char *dir, char *name, char *dest, size_t size) { int count = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, dir, AKERR_NULLPOINTER, "dir"); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest"); PASS(errctx, aksl_snprintf(&count, dest, size, "%s/%s", dir, name)); SUCCEED_RETURN(errctx); } /** * @brief Sprites first, characters second. Not a preference: a character's * JSON names its sprites by registry name, so a character loaded first fails * on the first sprite it cannot find. */ static akerr_ErrorContext *load_assets(char *assetdir) { char path[GALAGA_PATH_MAX]; int i = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, assetdir, AKERR_NULLPOINTER, "assetdir"); for ( i = 0; SPRITE_FILES[i] != NULL; i++ ) { PASS(errctx, asset_path(assetdir, SPRITE_FILES[i], (char *)&path, sizeof(path))); PASS(errctx, akgl_sprite_load_json((char *)&path)); } for ( i = 0; CHARACTER_FILES[i] != NULL; i++ ) { PASS(errctx, asset_path(assetdir, CHARACTER_FILES[i], (char *)&path, sizeof(path))); PASS(errctx, akgl_character_load_json((char *)&path)); } SUCCEED_RETURN(errctx); } /* --------------------------------------------------------------- startup --- */ /** @brief Replacement for akgl_game.lowfpsfunc, which logs a line per frame. */ static void galaga_lowfps(void) { } static akerr_ErrorContext *startup(void) { PREPARE_ERROR(errctx); PASS(errctx, aksl_strncpy((char *)&akgl_game.name, sizeof(akgl_game.name), "akbasic galaga tutorial", sizeof(akgl_game.name) - 1)); PASS(errctx, aksl_strncpy((char *)&akgl_game.version, sizeof(akgl_game.version), "1.0.0", sizeof(akgl_game.version) - 1)); PASS(errctx, aksl_strncpy((char *)&akgl_game.uri, sizeof(akgl_game.uri), "net.aklabs.akbasic.galaga", sizeof(akgl_game.uri) - 1)); PASS(errctx, akgl_game_init()); akgl_game.lowfpsfunc = &galaga_lowfps; /* Properties before the renderer: akgl_render_2d_init reads both, and an * unset one defaults to the string "0" -- a zero-sized window. */ PASS(errctx, akgl_set_property("game.screenwidth", "1280")); PASS(errctx, akgl_set_property("game.screenheight", "960")); PASS(errctx, akgl_render_2d_init(akgl_renderer)); FAIL_ZERO_RETURN( errctx, SDL_SetRenderLogicalPresentation( akgl_renderer->sdl_renderer, GALAGA_VIEW_WIDTH, GALAGA_VIEW_HEIGHT, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE), AKGL_ERR_SDL, "%s", SDL_GetError() ); /* The view is what the camera looks through, so it says the same thing. */ akgl_camera->x = 0.0f; akgl_camera->y = 0.0f; akgl_camera->w = (float)GALAGA_VIEW_WIDTH; akgl_camera->h = (float)GALAGA_VIEW_HEIGHT; /* * akgl_game_init does NOT install a physics backend, whatever physics.h's * file comment says (libakgl docs/14-physics.md). Null physics accepts * every call and moves nothing: whatever writes x and y directly is the * mover, and in this game that is BASIC writing through ACTOR@. */ PASS(errctx, akgl_physics_init_null(akgl_physics)); SUCCEED_RETURN(errctx); } /* ---------------------------------------------------------------- the UI --- */ static akgl_UiMenu TITLE_MENU = { "titlemenu", { "START", "QUIT" }, 2, 0, false, NULL }; static akgl_UiMenu AGAIN_MENU = { "againmenu", { "PLAY AGAIN", "QUIT" }, 2, 0, false, NULL }; /* Clay borrows label text until frame_end, so these cannot be locals. */ static char HUD_SCORE[64]; static char HUD_LIVES[64]; /** * @brief Draw a headline centred above the menu, in the banner font. * * Direct text rather than a ui label: the menu owns AKGL_UI_ANCHOR_CENTER, * and a label anchored there disappears behind it -- there is no * top-centre anchor to reach for. Drawn before the UI bracket, so the menu * still paints over it if the two ever meet. */ static akerr_ErrorContext *draw_banner(char *text) { SDL_Color ink = { 235, 235, 235, 255 }; TTF_Font *font = NULL; int w = 0; int h = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "text"); font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "banner", NULL); FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "the banner font is not loaded"); PASS(errctx, akgl_text_measure(font, text, &w, &h)); PASS(errctx, akgl_text_rendertextat(font, text, ink, 0, (GALAGA_VIEW_WIDTH - w) / 2, 280)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *declare_title(void) { PREPARE_ERROR(errctx); PASS(errctx, akgl_ui_menu(&TITLE_MENU)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *declare_play(void) { int count = 0; PREPARE_ERROR(errctx); PASS(errctx, aksl_snprintf(&count, HUD_SCORE, sizeof(HUD_SCORE), "SCORE %06d", galaga_game.score)); PASS(errctx, aksl_snprintf(&count, HUD_LIVES, sizeof(HUD_LIVES), "LIVES %d WAVE %d", galaga_game.lives, galaga_shared.wave)); PASS(errctx, akgl_ui_label("score", HUD_SCORE, AKGL_UI_ANCHOR_TOP_LEFT, NULL)); PASS(errctx, akgl_ui_label("lives", HUD_LIVES, AKGL_UI_ANCHOR_TOP_RIGHT, NULL)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *declare_end(void) { PREPARE_ERROR(errctx); PASS(errctx, akgl_ui_menu(&AGAIN_MENU)); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------ transitions --- */ static akerr_ErrorContext *start_game(void) { PREPARE_ERROR(errctx); galaga_game.score = 0; galaga_game.lives = 3; memset(galaga_game.kills, 0, sizeof(galaga_game.kills)); memset(galaga_game.shots, 0, sizeof(galaga_game.shots)); galaga_game.respawn_timer = 0.0f; galaga_shared.wave = 1; galaga_game.player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f; PASS(errctx, galaga_wave_spawn()); galaga_game.screen = GALAGA_SCREEN_PLAY; SUCCEED_RETURN(errctx); } /** * @brief End-of-round bookkeeping: notice a cleared wave or a spent ship. */ static akerr_ErrorContext *check_transitions(bool *running) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running"); (void)running; if ( galaga_game.screen != GALAGA_SCREEN_PLAY ) { SUCCEED_RETURN(errctx); } if ( galaga_game.lives <= 0 ) { PASS(errctx, galaga_wave_release()); galaga_game.screen = GALAGA_SCREEN_GAMEOVER; SUCCEED_RETURN(errctx); } if ( galaga_enemies_alive() == 0 ) { galaga_game.screen = GALAGA_SCREEN_VICTORY; } SUCCEED_RETURN(errctx); } /** * @brief Consume a menu activation. The menu never clears `activated`; the * state machine that acts on it does. */ static akerr_ErrorContext *consume_menus(bool *running) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running"); if ( galaga_game.screen == GALAGA_SCREEN_TITLE && TITLE_MENU.activated ) { TITLE_MENU.activated = false; if ( TITLE_MENU.selected == 0 ) { PASS(errctx, start_game()); } else { *running = false; } } if ( (galaga_game.screen == GALAGA_SCREEN_GAMEOVER || galaga_game.screen == GALAGA_SCREEN_VICTORY) && AGAIN_MENU.activated ) { AGAIN_MENU.activated = false; if ( AGAIN_MENU.selected == 0 ) { PASS(errctx, galaga_wave_release()); PASS(errctx, start_game()); } else { *running = false; } } SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------- the frame --- */ /** * @brief Route one event: the UI gets first refusal, then the menus, then * the controller. A consumed event goes no further. */ static akerr_ErrorContext *route_event(SDL_Event *event, bool *running) { bool consumed = false; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event"); FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running"); if ( event->type == SDL_EVENT_QUIT ) { *running = false; SUCCEED_RETURN(errctx); } PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed)); if ( consumed ) { SUCCEED_RETURN(errctx); } if ( galaga_game.screen == GALAGA_SCREEN_TITLE ) { PASS(errctx, akgl_ui_menu_handle_event(&TITLE_MENU, event, &consumed)); } else if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER || galaga_game.screen == GALAGA_SCREEN_VICTORY ) { PASS(errctx, akgl_ui_menu_handle_event(&AGAIN_MENU, event, &consumed)); } if ( consumed ) { SUCCEED_RETURN(errctx); } /* Every event, unconditionally: one that no control map binds is not an * error, it is a call that did nothing. */ PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, event)); SUCCEED_RETURN(errctx); } /** @brief The previous frame's timestamp, for dt. Stamped once at loop start. */ static uint64_t LAST_NS = 0; static akerr_ErrorContext *frame(bool *running) { SDL_Event event; uint64_t now = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running"); while ( SDL_PollEvent(&event) == true ) { PASS(errctx, route_event(&event, running)); } galaga_game.frame += 1; if ( galaga_game.autoplay ) { if ( galaga_game.screen == GALAGA_SCREEN_TITLE && galaga_game.frame >= 8 ) { PASS(errctx, start_game()); } /* * On an end screen the pilot presses Return, which drives the real * menu path -- declare, handle, activate, restart -- so a headless * run that dies keeps exercising the game instead of idling. */ if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER || galaga_game.screen == GALAGA_SCREEN_VICTORY ) { if ( (galaga_game.frame % 30) == 0 ) { SDL_Event press; memset(&press, 0, sizeof(press)); press.type = SDL_EVENT_KEY_DOWN; press.key.key = SDLK_RETURN; PASS(errctx, route_event(&press, running)); } } PASS(errctx, galaga_player_autoplay(galaga_game.frame)); } /* * dt from the wall clock, clamped: a debugger pause or a stalled runner * must not become one frame of teleporting enemies. The clamp is a 30 Hz * frame, the slowest game this is still worth playing at. */ now = SDL_GetTicksNS(); galaga_game.dt = (float)(now - LAST_NS) / 1e9f; LAST_NS = now; if ( galaga_game.dt > (1.0f / 30.0f) ) { galaga_game.dt = 1.0f / 30.0f; } /* The shared frame state, refreshed before any enemy thinks. The engine * filling GAME@.RND% is the issue #16 route: no RND verb exists. */ galaga_shared.playerx = galaga_game.player->x + 50.0f; galaga_shared.playery = galaga_game.player->y; galaga_shared.rnd = galaga_random(); PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); PASS(errctx, starfield_draw()); /* * akgl_game_update is update-every-actor, step-the-physics, draw-the- * world. Updating every actor is where the forty scripts run: each * enemy's updatefunc is the hook in enemies.c, and that hook is a BASIC * call. Held back on the menu screens so the world stands still there. */ if ( galaga_game.screen == GALAGA_SCREEN_PLAY ) { PASS(errctx, akgl_game_update(NULL)); PASS(errctx, check_transitions(running)); } /* The banner is direct text, drawn before the UI bracket. */ if ( galaga_game.screen == GALAGA_SCREEN_TITLE ) { PASS(errctx, draw_banner("GALAGA")); } else if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER ) { PASS(errctx, draw_banner("GAME OVER")); } else if ( galaga_game.screen == GALAGA_SCREEN_VICTORY ) { PASS(errctx, draw_banner("VICTORY")); } /* The UI bracket sits between akgl_game_update and frame_end, exactly as * libakgl docs/22-ui.md draws it. */ PASS(errctx, akgl_ui_frame_begin()); switch ( galaga_game.screen ) { case GALAGA_SCREEN_TITLE: PASS(errctx, declare_title()); break; case GALAGA_SCREEN_PLAY: PASS(errctx, declare_play()); break; case GALAGA_SCREEN_GAMEOVER: case GALAGA_SCREEN_VICTORY: PASS(errctx, declare_end()); break; } PASS(errctx, akgl_ui_frame_end(akgl_renderer)); PASS(errctx, consume_menus(running)); if ( (SHOTPATH != NULL) && (galaga_game.frame == SHOTFRAME) ) { PASS(errctx, save_screenshot(SHOTPATH)); } PASS(errctx, akgl_renderer->frame_end(akgl_renderer)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *run(int frames) { bool running = true; PREPARE_ERROR(errctx); LAST_NS = SDL_GetTicksNS(); while ( running == true ) { PASS(errctx, frame(&running)); if ( (frames > 0) && (galaga_game.frame >= frames) ) { running = false; } /* A crude frame limiter. A game on a real display should ask SDL for * vsync; this one has to work under the dummy video driver, where * there is nothing to sync to. */ SDL_Delay(16); } SUCCEED_RETURN(errctx); } /* -------------------------------------------------------------- teardown --- */ /** * @brief Give back what the process is holding. * * There is no akgl_game_shutdown; teardown is the application's. Fonts have * to unload before TTF_Quit destroys them underneath the registry. IGNORE() * on every call: a teardown failure must not mask whatever error is already * being reported. */ static void shutdown_game(void) { int i = 0; IGNORE(akgl_ui_shutdown()); IGNORE(akgl_text_unloadallfonts()); for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) { if ( akgl_heap_actors[i].refcount > 0 ) { IGNORE(akgl_heap_release_actor(&akgl_heap_actors[i])); } } TTF_Quit(); SDL_Quit(); } /* ------------------------------------------------------------------ args --- */ static akerr_ErrorContext *parse_args(int argc, char *argv[], char **assetdir, char **script, int *frames) { int i = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, assetdir, AKERR_NULLPOINTER, "assetdir"); FAIL_ZERO_RETURN(errctx, script, AKERR_NULLPOINTER, "script"); FAIL_ZERO_RETURN(errctx, frames, AKERR_NULLPOINTER, "frames"); for ( i = 1; i < argc; i++ ) { if ( strcmp(argv[i], "--autoplay") == 0 ) { galaga_game.autoplay = true; } else if ( strcmp(argv[i], "--frames") == 0 ) { i += 1; FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--frames needs a count"); PASS(errctx, aksl_atoi(argv[i], frames)); } else if ( strcmp(argv[i], "--assets") == 0 ) { i += 1; FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--assets needs a directory"); *assetdir = argv[i]; } else if ( strcmp(argv[i], "--script") == 0 ) { i += 1; FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--script needs a path"); *script = argv[i]; } else if ( strcmp(argv[i], "--screenshot") == 0 ) { i += 1; FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot needs a path"); SHOTPATH = argv[i]; } else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) { i += 1; FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot-frame needs a number"); PASS(errctx, aksl_atoi(argv[i], &SHOTFRAME)); } else { FAIL_RETURN( errctx, AKERR_VALUE, "usage: galaga [--assets DIR] [--script PATH] [--frames N]" " [--autoplay] [--screenshot PATH] [--screenshot-frame N]" ); } } SUCCEED_RETURN(errctx); } int main(int argc, char *argv[]) { char *assetdir = GALAGA_ASSET_DIR; char *script = GALAGA_SCRIPT_PATH; int frames = 0; uint16_t fontid = 0; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, parse_args(argc, argv, &assetdir, &script, &frames)); CATCH(errctx, startup()); CATCH(errctx, load_assets(assetdir)); /* The engine refuses to start when the script will not boot: a game * whose enemies cannot think is not a game missing a feature. */ CATCH(errctx, galaga_script_boot(script)); CATCH(errctx, akgl_ui_init(GALAGA_VIEW_WIDTH, GALAGA_VIEW_HEIGHT)); CATCH(errctx, akgl_text_loadfont("hud", GALAGA_FONT_PATH, 28)); CATCH(errctx, akgl_text_loadfont("banner", GALAGA_FONT_PATH, 84)); CATCH(errctx, akgl_ui_font_register("hud", &fontid)); CATCH(errctx, galaga_player_spawn()); CATCH(errctx, galaga_player_controls()); starfield_seed(); galaga_game.screen = GALAGA_SCREEN_TITLE; galaga_game.lives = 3; CATCH(errctx, run(frames)); } CLEANUP { shutdown_game(); } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "galaga could not run"); /* Set a flag rather than returning: leaving a HANDLE block early * skips FINISH's RELEASE_ERROR and leaks the context's pool slot. */ FAILED = 1; /* FINISH_NORETURN rather than FINISH: FINISH expands a return that an * int-returning function cannot compile. */ } FINISH_NORETURN(errctx); /* * The readout is the evidence: exiting 0 is not proof the wave flew. A * headless CI log gets the same line a reader's terminal does, and the * script-error count is the line's whole reason to exist -- a wave of * dumb enemies still exits 0. */ SDL_Log( "galaga: %d frames, screen %d, score %d, alive %d, kills bee %d bfly %d boss %d," " shots bee %d bfly %d boss %d, script errors %d", galaga_game.frame, (int)galaga_game.screen, galaga_game.score, galaga_enemies_alive(), galaga_game.kills[GALAGA_ENEMY_BEE], galaga_game.kills[GALAGA_ENEMY_BUTTERFLY], galaga_game.kills[GALAGA_ENEMY_BOSS], galaga_game.shots[GALAGA_ENEMY_BEE], galaga_game.shots[GALAGA_ENEMY_BUTTERFLY], galaga_game.shots[GALAGA_ENEMY_BOSS], galaga_game.script_errors); return FAILED; }