#include "game_render.h" #include "game_logic.h" #include #include #include #include "tilemap.h" namespace { void draw_snake_head(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { if (oamId >= 128 || state.snake.empty()) { return; } const std::array dot = state.snake.front(); u16 *headGfx = nullptr; bool hflip = false; bool vflip = false; switch (state.snakeDirection) { case 0: // Up headGfx = assets.gfx_head_v; break; case 1: // Right headGfx = assets.gfx_head_h; break; case 2: // Down headGfx = assets.gfx_head_v; vflip = true; break; case 3: // Left headGfx = assets.gfx_head_h; hflip = true; break; default: headGfx = assets.gfx_head_h; break; } oamSet(&oamSub, oamId, dot[0] * 8, dot[1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, headGfx, -1, false, false, hflip, vflip, false); oamId++; } void draw_snake_tail(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { if (oamId >= 128 || state.snake.size() < 2) { return; } const std::array dot = state.snake.back(); const size_t var = (state.snake.size() - 1) % 2; u16 *tailGfx = nullptr; bool hflip = false; bool vflip = false; switch (state.snake.at(state.snake.size() - 2).at(2)) { case 0: // Up tailGfx = assets.gfx_tail_v[var]; vflip = true; break; case 1: // Right tailGfx = assets.gfx_tail_h[var]; hflip = true; break; case 2: // Down tailGfx = assets.gfx_tail_v[var]; break; case 3: // Left tailGfx = assets.gfx_tail_h[var]; break; default: tailGfx = assets.gfx_tail_h[var]; break; } oamSet(&oamSub, oamId, dot[0] * 8, dot[1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, tailGfx, -1, false, false, hflip, vflip, false); oamId++; } void draw_snake(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { if (state.snake.empty()) { return; } draw_snake_head(state, assets, oamId); for (size_t i = 1; i + 1 < state.snake.size(); i++) { if (oamId >= 128) { return; } oamSet(&oamSub, oamId, state.snake[i][0] * 8, state.snake[i][1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, (i % 2) ? assets.gfx_segment_small : assets.gfx_segment, -1, false, false, false, false, false); oamId++; } if (state.snake.size() > 1) { draw_snake_tail(state, assets, oamId); } } void draw_apples(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { u16 *appleGfx = (state.ticks % 4 == 0) ? assets.gfx_apple_blink : assets.gfx_apple; for (const std::array &apple : state.apples) { if (oamId >= 128) { return; } oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, appleGfx, -1, false, false, false, false, false); oamId++; } } void draw_apples_rotten(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { for (const std::array &apple : state.bad_apples) { if (oamId >= 128) { return; } oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, assets.gfx_apple_rotten, -1, false, false, false, false, false); oamId++; } } void draw_apples_cursed(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { for (const std::array &apple : state.cursed_apples) { if (oamId >= 128) { return; } oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, assets.gfx_apple_cursed, -1, false, false, false, false, false); oamId++; } } } // namespace void invalidate_hud(RenderAssets &assets) { assets.hudCache.dirty = true; } void draw_game(const GameState &state, const RenderAssets &assets) { uint16_t oamId = 0; if (!state.gameOver) { draw_apples(state, assets, oamId); draw_apples_rotten(state, assets, oamId); draw_apples_cursed(state, assets, oamId); draw_snake(state, assets, oamId); // Hide all unused sprites up to hardware limit (128) to eliminate trailing // ghost sprites if (oamId < 128) { oamClear(&oamSub, oamId, 128 - oamId); } return; } // On Game Over, hide all sprites so none are left dangling oamClear(&oamSub, 0, 128); iprintf("\x1b[11;12HGAME OVER!"); consoleSelect(assets.topScreen); iprintf("\x1b[10;10H Score: %i ", state.score); iprintf("\x1b[11;12HGAME OVER!"); } void draw_stats(const GameState &state, RenderAssets &assets, bool internals, bool musicEnabled, uint8_t frame) { if (internals) { iprintf("\x1b[0;0HFrame = %i\nTick = %i\ndir = %i", frame, state.ticks, state.snakeDirection); iprintf("\x1b[3;0HLength: %i", state.snakelength); if (!state.snake.empty()) { iprintf("\x1b[4;0Hx=%i\ny=%i", state.snake.front()[0], state.snake.front()[1]); } const uint16_t freq_mhz = calculate_timer_freq_mhz(state); const uint8_t intensity = calculate_intensity_31(state); iprintf("\x1b[6;0HApples: %i ", state.applesEaten); iprintf("\x1b[7;0HSpeed: %u.%01u Hz (%u%%) ", static_cast(freq_mhz / 1000U), static_cast((freq_mhz % 1000U) / 100U), static_cast((static_cast(intensity) * 100U) / 31U)); } HudCache &cache = assets.hudCache; const bool cursed = (state.curseTicksRemaining > 0); const bool needs_score_update = cache.dirty || (cache.score != state.score); const bool needs_curse_update = cache.dirty || (cache.cursed != cursed); const bool needs_pause_update = cache.dirty || (cache.paused != state.paused) || (cache.musicEnabled != musicEnabled) || (cache.hardMode != state.hardMode) || (cache.gameOver != state.gameOver); if (needs_score_update) { iprintf("\x1b[10;12HScore: %i", state.score); cache.score = state.score; } if (needs_curse_update) { if (cursed) { iprintf("\x1b[1;7HCONTROLS INVERTED!"); } else { iprintf("\x1b[1;7H "); } cache.cursed = cursed; } if (needs_pause_update) { if (!state.paused) { if (state.gameOver) { iprintf("\x1b[23;0HB = Main Menu"); } else if (cache.paused) { // Clear any leftover pause strings if previously paused iprintf("\x1b[21;0H "); iprintf("\x1b[22;0H "); iprintf("\x1b[23;0H "); } } else { iprintf("\x1b[21;0HB = Main Menu"); if (state.hardMode) { iprintf("\x1b[22;0HY = Restart in normal mode"); } else { iprintf("\x1b[22;0HY = Restart in Hard mode "); } if (musicEnabled) { iprintf("\x1b[23;0HX = Disable Music"); } else { iprintf("\x1b[23;0HX = Enable Music "); } } cache.paused = state.paused; cache.musicEnabled = musicEnabled; cache.hardMode = state.hardMode; cache.gameOver = state.gameOver; } cache.dirty = false; } void draw_menu(const GameState &state, RenderAssets &assets, bool musicEnabled) { invalidate_hud(assets); oamClear(&oamSub, 0, 128); oamUpdate(&oamSub); consoleSelect(assets.bottomScreen); consoleClear(); iprintf("\x1b[10;5H* SNAKE WITH A TWIST *"); iprintf("\x1b[12;8Hby Phillip Kuehne"); iprintf("\x1b[18;4HPress START or A to begin"); 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*/) { // Blinking is now handled directly by draw_apples() via sprite pointer // selection without DMA. }