From 8bf999c33b2c6447a877f7f7e572e4b5088401dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phillip=20K=C3=BChne?= Date: Thu, 3 Sep 2026 22:27:49 +0200 Subject: [PATCH] Add a main menu scene with Start/Hard Mode/Music options The game now boots into a menu (Start Game, Hard Mode toggle, Music toggle) navigated with Up/Down and A/Start, instead of dropping straight into a match. B returns to the menu from a paused or game-over state. --- include/game_render.h | 1 + include/game_state.h | 10 ++++++++++ source/game_input.cpp | 37 +++++++++++++++++++++++++++++++++++++ source/game_render.cpp | 29 +++++++++++++++++++++++++++++ source/main.cpp | 15 ++++++++------- 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/include/game_render.h b/include/game_render.h index 72772ba..2ca215c 100644 --- a/include/game_render.h +++ b/include/game_render.h @@ -20,6 +20,7 @@ struct RenderAssets { void draw_game(const GameState &state, const RenderAssets &assets); void draw_stats(const GameState &state, bool internals, bool musicEnabled, uint8_t frame); +void draw_menu(const GameState &state, const RenderAssets &assets, bool musicEnabled); void blink_apples(const GameState &state, u16 *gfx_apple); #endif diff --git a/include/game_state.h b/include/game_state.h index 7384161..8746442 100644 --- a/include/game_state.h +++ b/include/game_state.h @@ -6,6 +6,13 @@ #include #include +enum class Scene { + Menu, + Playing, +}; + +constexpr int MENU_ITEM_COUNT = 3; + struct GameState { volatile uint16_t ticks = 0; volatile uint16_t snakelength = 0; @@ -16,6 +23,9 @@ struct GameState { bool paused = false; bool hardMode = false; + Scene scene = Scene::Menu; + int menuSelection = 0; + uint16_t curseTicksRemaining = 0; std::deque> snake; diff --git a/source/game_input.cpp b/source/game_input.cpp index e49a0b9..04949d8 100644 --- a/source/game_input.cpp +++ b/source/game_input.cpp @@ -29,6 +29,43 @@ bool handle_input_frame(InputContext &ctx) { } } + if (ctx.state->scene == Scene::Menu) { + if ((keys != 0U) && ((keys & (KEY_UP | KEY_DOWN)) != 0U)) { + const int delta = ((keys & KEY_UP) != 0U) ? -1 : 1; + ctx.state->menuSelection = (ctx.state->menuSelection + delta + MENU_ITEM_COUNT) % MENU_ITEM_COUNT; + draw_menu(*ctx.state, *ctx.renderAssets, audio_is_music_enabled(*ctx.audio)); + } + + if ((keys != 0U) && (((keys & KEY_A) != 0U) || ((keys & KEY_START) != 0U))) { + switch (ctx.state->menuSelection) { + case 0: + ctx.state->scene = Scene::Playing; + ctx.init_game(); + draw_game(*ctx.state, *ctx.renderAssets); + break; + case 1: + ctx.state->hardMode = !ctx.state->hardMode; + draw_menu(*ctx.state, *ctx.renderAssets, audio_is_music_enabled(*ctx.audio)); + break; + case 2: + audio_toggle_music(*ctx.audio); + draw_menu(*ctx.state, *ctx.renderAssets, audio_is_music_enabled(*ctx.audio)); + break; + default: + break; + } + } + + return true; + } + + if ((ctx.state->paused || ctx.state->gameOver) && (keys != 0U) && ((keys & KEY_B) != 0U)) { + ctx.state->scene = Scene::Menu; + ctx.state->paused = false; + draw_menu(*ctx.state, *ctx.renderAssets, audio_is_music_enabled(*ctx.audio)); + return true; + } + if (!ctx.state->paused && !ctx.state->gameOver) { const bool cursed = ctx.state->curseTicksRemaining > 0; diff --git a/source/game_render.cpp b/source/game_render.cpp index 470bca4..4eea852 100644 --- a/source/game_render.cpp +++ b/source/game_render.cpp @@ -143,9 +143,14 @@ void draw_stats(const GameState &state, bool internals, bool musicEnabled, uint8 } if (!state.paused) { + if (state.gameOver) { + iprintf("\x1b[23;0HB = Main Menu"); + } return; } + iprintf("\x1b[21;0HB = Main Menu"); + if (musicEnabled) { iprintf("\x1b[23;0HX = Disable Music"); } else { @@ -159,6 +164,30 @@ void draw_stats(const GameState &state, bool internals, bool musicEnabled, uint8 } } +void draw_menu(const GameState &state, const RenderAssets &assets, bool musicEnabled) { + oamClear(&oamSub, 0, 0); + oamUpdate(&oamSub); + + consoleSelect(assets.bottomScreen); + consoleClear(); + + consoleSelect(assets.topScreen); + consoleClear(); + + iprintf("\x1b[3;10HSNAKE PLUS"); + iprintf("\x1b[5;5H---------------------"); + + const char *hardModeLabel = state.hardMode ? "ON " : "OFF"; + const char *musicLabel = musicEnabled ? "ON " : "OFF"; + + iprintf("\x1b[8;7H%c Start Game", state.menuSelection == 0 ? '>' : ' '); + iprintf("\x1b[10;7H%c Hard Mode: %s", state.menuSelection == 1 ? '>' : ' ', hardModeLabel); + iprintf("\x1b[12;7H%c Music: %s", state.menuSelection == 2 ? '>' : ' ', musicLabel); + + iprintf("\x1b[19;3HUP/DOWN: Navigate"); + iprintf("\x1b[20;3HA/START: Select"); +} + void blink_apples(const GameState &state, u16 *gfx_apple) { if (state.ticks % 4) { dmaCopy(get_sprite_pointer(5), gfx_apple, 64); diff --git a/source/main.cpp b/source/main.cpp index d3adc88..62c0994 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -57,6 +57,7 @@ void setup() { videoSetMode(MODE_0_2D); videoSetModeSub(MODE_0_2D); + vramSetBankA(VRAM_A_MAIN_BG); vramSetBankC(VRAM_C_SUB_BG); vramSetBankD(VRAM_D_SUB_SPRITE); consoleInit(&topScreen, 3,BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0, true, true); @@ -115,14 +116,16 @@ void setup() { } void handle_vblank() { frame++; - consoleSelect(&topScreen); - draw_stats(gameState, internals, audio_is_music_enabled(audioContext), frame); + if (gameState.scene == Scene::Playing) { + consoleSelect(&topScreen); + draw_stats(gameState, internals, audio_is_music_enabled(audioContext), frame); + } oamUpdate(&oamSub); //flashy_colors(); } void do_tick() { - if (!paused) { + if (gameState.scene == Scene::Playing && !paused) { ticks++; if (!gameOver){ const bool wasGameOver = gameOver; @@ -211,9 +214,7 @@ int main(void) { irqSet(IRQ_VBLANK, handle_vblank); setup(); - init_game(); - - // Start timer for game ticks + draw_menu(gameState, renderAssets, audio_is_music_enabled(audioContext)); InputContext inputContext; inputContext.state = &gameState; @@ -223,7 +224,7 @@ int main(void) { inputContext.topScreen = &topScreen; inputContext.init_game = init_game; - + while(1) { if (!handle_input_frame(inputContext)) { break;