- Refactor cause in draw_game_over_screen to an immediately invoked lambda, eliminating dead store warning - Rename sound effect files to standard-compliant names without double underscores to prevent reserved macro warnings in soundbank.h - Update source/game_audio.cpp to use clean SFX_* macro identifiers - Add codechecker_skip.txt to ignore external devkitPro toolchain headers - Ignore .codechecker/ output artifacts directory in .gitignore - Update AGENTS.md file tree and sound asset references
398 lines
12 KiB
C++
398 lines
12 KiB
C++
#include "game_render.h"
|
|
#include "game_logic.h"
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <stdio.h>
|
|
|
|
#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<int, 3> 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<int, 3> 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<int, 2> &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 EvilItem &apple : state.bad_apples) {
|
|
if (oamId >= 128) {
|
|
return;
|
|
}
|
|
const bool hflip = (((apple.x + apple.y) & 1) != 0);
|
|
oamSet(&oamSub, oamId, apple.x * 8, apple.y * 8, 0, 0, SpriteSize_8x8,
|
|
SpriteColorFormat_256Color, assets.gfx_apple_rotten, -1, false,
|
|
false, hflip, 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 EvilItem &apple : state.cursed_apples) {
|
|
if (oamId >= 128) {
|
|
return;
|
|
}
|
|
oamSet(&oamSub, oamId, apple.x * 8, apple.y * 8, 0, 0, SpriteSize_8x8,
|
|
SpriteColorFormat_256Color, assets.gfx_apple_cursed, -1, false,
|
|
false, hflip, false, false);
|
|
oamId++;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void draw_game_over_screen(const GameState &state) {
|
|
iprintf("\x1b[1;2H============================");
|
|
iprintf("\x1b[2;2H * GAME OVER * ");
|
|
iprintf("\x1b[3;2H============================");
|
|
|
|
iprintf("\x1b[5;4HFinal Score: %5i", state.score);
|
|
iprintf("\x1b[6;4HApples Eaten: %5i", state.applesEaten);
|
|
iprintf("\x1b[7;4HSnake Length: %5i", state.snakelength);
|
|
iprintf("\x1b[8;4HDifficulty: %s", state.hardMode ? "Hard " : "Normal");
|
|
|
|
iprintf("\x1b[10;4HCause of Defeat:");
|
|
const char *const cause = [&state]() {
|
|
switch (state.gameOverReason) {
|
|
case GameOverReason::SelfCollision:
|
|
return "Bite Collision (Hit Body)";
|
|
case GameOverReason::BarrierCollision:
|
|
return "Energy Barrier Impact";
|
|
case GameOverReason::RottenAppleDepleted:
|
|
return "Depleted by Rotten Apple";
|
|
default:
|
|
return "Collision";
|
|
}
|
|
}();
|
|
iprintf("\x1b[11;4H %s", cause);
|
|
|
|
iprintf("\x1b[14;2H----------------------------");
|
|
iprintf("\x1b[16;4HA / START : Play Again");
|
|
iprintf("\x1b[17;4HB : Main Menu");
|
|
iprintf("\x1b[18;4HR : Swap Screens");
|
|
iprintf("\x1b[20;2H============================");
|
|
}
|
|
|
|
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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void draw_barriers(const GameState &state, const RenderAssets &assets,
|
|
uint8_t frame) {
|
|
if (state.scene != Scene::Playing || !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<int>(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<uint16_t>(RGB15(12, 0, 3));
|
|
SPRITE_PALETTE_SUB[BARRIER_PAL_FILL] =
|
|
static_cast<uint16_t>(RGB15(22 + (wave * 3), 2 + wave, 4));
|
|
SPRITE_PALETTE_SUB[BARRIER_PAL_STRIPE] =
|
|
static_cast<uint16_t>(RGB15(31, 22 + (wave * 3), 6 + (wave * 4)));
|
|
|
|
// Warning colour ramps from dim orange to hot yellow-white
|
|
const int urgency =
|
|
31 - ((static_cast<int>(min_warn_frames) * 31) / BARRIER_WARN_FRAMES);
|
|
SPRITE_PALETTE_SUB[BARRIER_PAL_WARN] = static_cast<uint16_t>(
|
|
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 (state.gameOver) {
|
|
return;
|
|
}
|
|
|
|
HudCache &cache = assets.hudCache;
|
|
|
|
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<unsigned int>(freq_mhz / 1000U),
|
|
static_cast<unsigned int>((freq_mhz % 1000U) / 100U),
|
|
static_cast<unsigned int>(
|
|
(static_cast<uint32_t>(intensity) * 100U) / 31U));
|
|
if (state.hardMode) {
|
|
iprintf("\x1b[8;0HBarriers: max len %u ",
|
|
static_cast<unsigned int>(calculate_barrier_max_length(state)));
|
|
}
|
|
}
|
|
|
|
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 (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.
|
|
}
|