#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) { const bool hflip = ((state.ticks % 6) < 3); 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, hflip, false, false); oamId++; } } } // namespace void invalidate_hud(RenderAssets &assets) { assets.hudCache.dirty = true; } void draw_game(const GameState &state, const RenderAssets &assets) { // OAM slots below BARRIER_OAM_SLOTS are owned by draw_barriers() (VBlank) uint16_t oamId = BARRIER_OAM_SLOTS; 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_barriers(const GameState &state, const RenderAssets &assets, uint8_t frame) { if (state.scene != Scene::Playing || state.gameOver || !state.hardMode) { oamClear(&oamSub, 0, BARRIER_OAM_SLOTS); return; } // Most imminent warning drives the shared warning colour intensity uint16_t min_warn_frames = BARRIER_WARN_FRAMES; for (size_t b = 0; b < state.barriers.size(); b++) { const Barrier &barrier = state.barriers.at(b); const int base_slot = static_cast(b) * BARRIER_MAX_LENGTH; if (barrier.phase == BarrierPhase::Inactive) { oamClear(&oamSub, base_slot, BARRIER_MAX_LENGTH); continue; } const uint16_t frames_left = barrier.framesLeft; bool visible = true; u16 *gfx = assets.gfx_barrier; if (barrier.phase == BarrierPhase::Warning) { min_warn_frames = std::min(min_warn_frames, frames_left); if (frames_left < BARRIER_SOLIDIFY_FRAMES) { // About to become solid: rapid flicker between outline and solid look gfx = (((frames_left >> 1U) & 1U) != 0U) ? assets.gfx_barrier : assets.gfx_barrier_warn; } else { // Blink period shrinks from 24 to 4 frames as activation approaches const int elapsed = BARRIER_WARN_FRAMES - frames_left; const int span = BARRIER_WARN_FRAMES - BARRIER_SOLIDIFY_FRAMES; const int period = std::max(4, 24 - ((elapsed * 20) / span)); visible = (elapsed % period) < (period / 2); gfx = assets.gfx_barrier_warn; } } else if (frames_left < BARRIER_FADE_FRAMES) { // Still fatal, but slowly blinking to signal it's about to vanish visible = ((frames_left >> 3U) & 1U) == 0U; } for (int i = 0; i < BARRIER_MAX_LENGTH; i++) { const int slot = base_slot + i; if (i >= barrier.length) { oamClear(&oamSub, slot, 1); continue; } const int cx = barrier.x + (barrier.vertical ? 0 : i); const int cy = barrier.y + (barrier.vertical ? i : 0); oamSet(&oamSub, slot, cx * 8, cy * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, gfx, -1, false, !visible, false, false, false); } } // Palette animation (shared by all barriers) const uint8_t pulse = (frame >> 1U) & 7U; const int wave = (pulse < 4U) ? pulse : (7 - pulse); // 0..3..0 SPRITE_PALETTE_SUB[BARRIER_PAL_EDGE] = static_cast(RGB15(12, 0, 3)); SPRITE_PALETTE_SUB[BARRIER_PAL_FILL] = static_cast(RGB15(22 + (wave * 3), 2 + wave, 4)); SPRITE_PALETTE_SUB[BARRIER_PAL_STRIPE] = static_cast(RGB15(31, 22 + (wave * 3), 6 + (wave * 4))); // Warning colour ramps from dim orange to hot yellow-white const int urgency = 31 - ((static_cast(min_warn_frames) * 31) / BARRIER_WARN_FRAMES); SPRITE_PALETTE_SUB[BARRIER_PAL_WARN] = static_cast( RGB15(18 + ((urgency * 13) / 31), 4 + ((urgency * 22) / 31), 2 + ((urgency * 10) / 31))); } 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)); if (state.hardMode) { iprintf("\x1b[8;0HBarriers: max len %u ", static_cast(calculate_barrier_max_length(state))); } } 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 *hard_mode_label = state.hardMode ? "ON " : "OFF"; const char *music_label = musicEnabled ? "ON " : "OFF"; iprintf("\x1b[8;7H%c Start Game", state.menuSelection == 0 ? '>' : ' '); iprintf("\x1b[10;7H%c Hard Mode: %s", state.menuSelection == 1 ? '>' : ' ', hard_mode_label); iprintf("\x1b[12;7H%c Music: %s", state.menuSelection == 2 ? '>' : ' ', music_label); 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. }