- Make handle_vblank strictly increment frame counter (frame++) - Move heavy frame logic (background updates, OAM updates, palette animations, and console stats) to update_frame() on the main thread synchronized with swiWaitForVBlank() - Eliminate 256-byte ARM9 IRQ stack (sp_irq) overflow caused by bg_update and iprintf that was corrupting DTCM stack and causing data aborts - Preserve all playfield entities on game over with top-screen defeat summary and smooth background deceleration Co-authored-by: Antigravity <antigravity@google.com>
383 lines
13 KiB
C++
383 lines
13 KiB
C++
#include <array>
|
|
#include <consts.h>
|
|
#include <deque>
|
|
#include <game_audio.h>
|
|
#include <game_bg.h>
|
|
#include <game_input.h>
|
|
#include <game_logic.h>
|
|
#include <game_render.h>
|
|
#include <game_state.h>
|
|
#include <nds.h>
|
|
#include <vector>
|
|
|
|
#include "tilemap.h"
|
|
|
|
using namespace std;
|
|
|
|
volatile uint8_t frame = 0;
|
|
volatile bool internals = false;
|
|
|
|
GameState gameState;
|
|
|
|
volatile uint16_t &ticks = gameState.ticks;
|
|
volatile uint16_t &snakelength = gameState.snakelength;
|
|
/***
|
|
* 0 = up
|
|
* 1 = right
|
|
* 2 = down
|
|
* 3 = left
|
|
***/
|
|
volatile uint16_t &snakeDirection = gameState.snakeDirection;
|
|
volatile uint16_t &score = gameState.score;
|
|
volatile uint16_t &applesEaten = gameState.applesEaten;
|
|
|
|
bool &gameOver = gameState.gameOver;
|
|
bool &paused = gameState.paused;
|
|
bool &hardMode = gameState.hardMode;
|
|
|
|
std::deque<std::array<int, 3>> &snake = gameState.snake;
|
|
std::vector<std::array<int, 2>> &apples = gameState.apples;
|
|
std::vector<EvilItem> &bad_apples = gameState.bad_apples;
|
|
|
|
// 31x23
|
|
PrintConsole topScreen;
|
|
PrintConsole bottomScreen;
|
|
|
|
int sound;
|
|
|
|
AudioContext audioContext;
|
|
|
|
RenderAssets renderAssets;
|
|
BackgroundContext bgContext;
|
|
InputContext inputContext;
|
|
|
|
const void *get_sprite_pointer(uint16_t spriteIndex) {
|
|
return &tilemapTiles[spriteIndex * 16];
|
|
}
|
|
|
|
namespace {
|
|
|
|
// 8x8 256-color tile data for Cursed Apple (vibrant electric purple apple with
|
|
// radioactive green horn & glowing core)
|
|
alignas(4) constexpr std::array<uint32_t, 16> cursed_apple_tile = {
|
|
0x10000000, 0x00001414, 0x10101000, 0x00101014, 0x12111110, 0x10111112,
|
|
0x13121110, 0x10111112, 0x13131110, 0x10111112, 0x13121110, 0x10111211,
|
|
0x11111000, 0x00101111, 0x10100000, 0x00101000,
|
|
};
|
|
|
|
// 8x8 256-color tile data for Rotten Apple (bruised amber decaying apple with
|
|
// dark sunken rot & toxic green mold)
|
|
alignas(4) constexpr std::array<uint32_t, 16> rotten_apple_tile = {
|
|
0x1E000000, 0x0000001E, 0x19191900, 0x0019191E, 0x1B1A1A19, 0x191A1A1B,
|
|
0x1D1B1A19, 0x191A1A1C, 0x1D1C1A19, 0x191A1C1D, 0x1C1A1A19, 0x191A1B1C,
|
|
0x1A1A1900, 0x00191A1A, 0x19190000, 0x00001919,
|
|
};
|
|
|
|
// 8x8 256-color tile data for solid fatal barrier (bordered hazard stripe
|
|
// block)
|
|
alignas(4) constexpr std::array<uint32_t, 16> barrier_solid_tile = {
|
|
0x15151515, 0x15151515, 0x16171715, 0x15171716, 0x16161715, 0x15161717,
|
|
0x17161615, 0x15161617, 0x17171615, 0x15171616, 0x16171715, 0x15171716,
|
|
0x16161715, 0x15161717, 0x15151515, 0x15151515,
|
|
};
|
|
|
|
// 8x8 256-color tile data for barrier warning announcement (pulsing reticle
|
|
// frame)
|
|
alignas(4) constexpr std::array<uint32_t, 16> barrier_warn_tile = {
|
|
0x00001818, 0x18180000, 0x18000018, 0x18000018, 0x18180000, 0x00001818,
|
|
0x00181800, 0x00181800, 0x00181800, 0x00181800, 0x18180000, 0x00001818,
|
|
0x18000018, 0x18000018, 0x00001818, 0x18180000,
|
|
};
|
|
|
|
} // namespace
|
|
|
|
void setup() {
|
|
// Set up screens
|
|
|
|
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);
|
|
consoleInit(&bottomScreen, 3, BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0,
|
|
false, true);
|
|
oamInit(&oamSub, SpriteMapping_1D_32, false);
|
|
|
|
bg_setup(bgContext, topScreen.bgId, bottomScreen.bgId);
|
|
|
|
renderAssets.topScreen = &topScreen;
|
|
renderAssets.bottomScreen = &bottomScreen;
|
|
renderAssets.bg_context = &bgContext;
|
|
|
|
renderAssets.gfx_segment =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_segment_small =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_head_h =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_head_v =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_apple =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_apple_blink =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_tail_h[0] =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_tail_h[1] =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_tail_v[0] =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_tail_v[1] =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_apple_rotten =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_apple_cursed =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_barrier =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
renderAssets.gfx_barrier_warn =
|
|
oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
|
|
|
dmaCopy(get_sprite_pointer(1), renderAssets.gfx_segment, 64);
|
|
dmaCopy(get_sprite_pointer(2), renderAssets.gfx_segment_small, 64);
|
|
dmaCopy(get_sprite_pointer(3), renderAssets.gfx_head_h, 64);
|
|
dmaCopy(get_sprite_pointer(4), renderAssets.gfx_head_v, 64);
|
|
dmaCopy(get_sprite_pointer(5), renderAssets.gfx_apple, 64);
|
|
dmaCopy(get_sprite_pointer(6), renderAssets.gfx_apple_blink, 64);
|
|
dmaCopy(get_sprite_pointer(7), renderAssets.gfx_tail_h[0], 64);
|
|
dmaCopy(get_sprite_pointer(8), renderAssets.gfx_tail_h[1], 64);
|
|
dmaCopy(get_sprite_pointer(9), renderAssets.gfx_tail_v[0], 64);
|
|
dmaCopy(get_sprite_pointer(10), renderAssets.gfx_tail_v[1], 64);
|
|
|
|
consoleSelect(&topScreen);
|
|
|
|
dmaCopy(tilemapPal, SPRITE_PALETTE_SUB, tilemapPalLen);
|
|
|
|
// Setup vibrant base colors for the Cursed Apple sprite (neon electric purple
|
|
// with toxic green horn)
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_OUTLINE] =
|
|
RGB15(5, 0, 12); // Deep mystical dark purple outline
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_FILL] =
|
|
RGB15(22, 2, 28); // Vibrant Electric Purple
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_GLOW] =
|
|
RGB15(29, 6, 31); // Radiant Neon Magenta Glow
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_CORE] =
|
|
RGB15(31, 26, 31); // Glowing White-Pink Core
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_LEAF] =
|
|
RGB15(2, 31, 10); // Toxic Radioactive Neon Green
|
|
|
|
// Setup initial colors for the hard-mode temporary fatal barriers
|
|
SPRITE_PALETTE_SUB[BARRIER_PAL_EDGE] = RGB15(12, 0, 3);
|
|
SPRITE_PALETTE_SUB[BARRIER_PAL_FILL] = RGB15(22, 2, 4);
|
|
SPRITE_PALETTE_SUB[BARRIER_PAL_STRIPE] = RGB15(31, 22, 6);
|
|
SPRITE_PALETTE_SUB[BARRIER_PAL_WARN] = RGB15(18, 4, 2);
|
|
|
|
// Setup initial colors for the Rotten Apple sprite (bruised amber decaying
|
|
// skin with dark rot & glowing green mold)
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_OUTLINE] = RGB15(2, 2, 1); // Crisp dark outline
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_SKIN] = RGB15(
|
|
25, 17,
|
|
4); // Bruised golden-amber decaying skin (high contrast against crimson)
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_DECAY] =
|
|
RGB15(5, 3, 1); // Sunken dark necrotic rot cavity
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_MOLD] =
|
|
RGB15(12, 24, 4); // Vivid toxic fungal green mold
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_SPORE] =
|
|
RGB15(22, 31, 10); // Radiant glowing spore pustule
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_STEM] =
|
|
RGB15(13, 8, 3); // Withered woody twig stem
|
|
|
|
dmaCopy(cursed_apple_tile.data(), renderAssets.gfx_apple_cursed, 64);
|
|
dmaCopy(rotten_apple_tile.data(), renderAssets.gfx_apple_rotten, 64);
|
|
dmaCopy(barrier_solid_tile.data(), renderAssets.gfx_barrier, 64);
|
|
dmaCopy(barrier_warn_tile.data(), renderAssets.gfx_barrier_warn, 64);
|
|
|
|
// Pre-reserve capacities to eliminate dynamic heap allocations inside Timer 0
|
|
// ISR (do_tick)
|
|
gameState.bad_apples.reserve(32);
|
|
gameState.cursed_apples.reserve(4);
|
|
gameState.apples.reserve(4);
|
|
|
|
audio_setup(audioContext);
|
|
}
|
|
void handle_vblank() {
|
|
frame++;
|
|
}
|
|
|
|
void update_frame() {
|
|
bg_update(bgContext, gameState, frame);
|
|
if (gameState.scene == Scene::Playing) {
|
|
consoleSelect(&topScreen);
|
|
if (gameState.gameOver) {
|
|
HudCache &cache = renderAssets.hudCache;
|
|
if (cache.dirty || (cache.gameOver != gameState.gameOver)) {
|
|
consoleClear();
|
|
draw_game_over_screen(gameState);
|
|
cache.gameOver = gameState.gameOver;
|
|
cache.dirty = false;
|
|
}
|
|
} else {
|
|
draw_stats(gameState, renderAssets, internals,
|
|
audio_is_music_enabled(audioContext), frame);
|
|
if (!gameState.paused) {
|
|
tick_barriers_frame(gameState);
|
|
}
|
|
}
|
|
draw_barriers(gameState, renderAssets, frame);
|
|
}
|
|
|
|
// Dynamic pulsing radioactive glow for Cursed Apples (smooth 60fps breathing)
|
|
const auto pulse = static_cast<uint8_t>(
|
|
(static_cast<uint32_t>(frame) >> 2U) & 7U); // 8-step cycle
|
|
const uint8_t wave =
|
|
(pulse < 4U) ? pulse
|
|
: static_cast<uint8_t>(7U - pulse); // 0, 1, 2, 3, 3, 2, 1, 0
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_FILL] =
|
|
static_cast<uint16_t>(RGB15(20 + (wave * 3), 2 + wave, 26 + wave));
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_GLOW] =
|
|
static_cast<uint16_t>(RGB15(26 + wave, 6 + (wave * 4), 31));
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_CORE] =
|
|
(wave >= 2U) ? static_cast<uint16_t>(RGB15(31, 31, 31))
|
|
: static_cast<uint16_t>(RGB15(31, 22 + (wave * 3), 31));
|
|
SPRITE_PALETTE_SUB[CURSE_PAL_LEAF] =
|
|
static_cast<uint16_t>(RGB15(2 + (wave * 2), 31, 10 + (wave * 3)));
|
|
|
|
// Dynamic festering mold & spore respiration for Rotten Apples (smooth 60fps
|
|
// organic pulse)
|
|
const auto rot_pulse = static_cast<uint8_t>(
|
|
(static_cast<uint32_t>(frame + 3U) >> 2U) &
|
|
7U); // 8-step cycle, phase-shifted from cursed apple
|
|
const uint8_t rot_wave =
|
|
(rot_pulse < 4U)
|
|
? rot_pulse
|
|
: static_cast<uint8_t>(7U - rot_pulse); // 0, 1, 2, 3, 3, 2, 1, 0
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_DECAY] =
|
|
static_cast<uint16_t>(RGB15(5 + rot_wave, 3 + rot_wave, 1));
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_MOLD] = static_cast<uint16_t>(
|
|
RGB15(10 + (rot_wave * 2), 22 + (rot_wave * 2), 3 + rot_wave));
|
|
SPRITE_PALETTE_SUB[ROTTEN_PAL_SPORE] =
|
|
(rot_wave >= 2U)
|
|
? static_cast<uint16_t>(RGB15(26, 31, 14))
|
|
: static_cast<uint16_t>(
|
|
RGB15(20 + (rot_wave * 2), 28 + rot_wave, 8 + (rot_wave * 2)));
|
|
|
|
oamUpdate(&oamSub);
|
|
// flashy_colors();
|
|
}
|
|
|
|
void do_tick() {
|
|
if (gameState.scene == Scene::Playing && !paused) {
|
|
ticks++;
|
|
if (!gameOver) {
|
|
const bool was_game_over = gameOver;
|
|
|
|
/*if (hardMode) {
|
|
timerStop(0);
|
|
timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(ticks+5),
|
|
do_tick);
|
|
}*/
|
|
|
|
update_snake(gameState);
|
|
audio_play_plop(audioContext);
|
|
check_collision_self(gameState);
|
|
if (check_collision_barriers(gameState)) {
|
|
audio_play_boom(audioContext);
|
|
}
|
|
if (check_collision_apples(gameState)) {
|
|
audio_play_pickup(audioContext);
|
|
bg_trigger_pulse(bgContext);
|
|
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState),
|
|
do_tick);
|
|
}
|
|
if (check_collision_apples_rotten(gameState)) {
|
|
audio_play_boom(audioContext);
|
|
}
|
|
if (check_collision_apples_cursed(gameState)) {
|
|
audio_play_boom(audioContext);
|
|
}
|
|
tick_evil_items(gameState);
|
|
spawn_apples(gameState);
|
|
spawn_apples_rotten(gameState);
|
|
spawn_apples_cursed(gameState);
|
|
spawn_barriers(gameState);
|
|
tick_status_effects(gameState);
|
|
if (!was_game_over && gameOver) {
|
|
audio_stop_music();
|
|
audio_play_fail(audioContext);
|
|
invalidate_hud(renderAssets);
|
|
}
|
|
// consoleSelect(&bottomScreen);
|
|
draw_game(gameState, renderAssets);
|
|
}
|
|
}
|
|
}
|
|
|
|
void init_game() {
|
|
timerStop(0);
|
|
applesEaten = 0;
|
|
reset_barriers(gameState);
|
|
gameState.bad_apples.reserve(32);
|
|
gameState.cursed_apples.reserve(4);
|
|
gameState.apples.reserve(4);
|
|
score = 0;
|
|
ticks = 0;
|
|
gameOver = false;
|
|
gameState.gameOverReason = GameOverReason::None;
|
|
snakeDirection = 1;
|
|
gameState.curseTicksRemaining = 0;
|
|
apples.clear();
|
|
bad_apples.clear();
|
|
gameState.cursed_apples.clear();
|
|
|
|
if (hardMode) {
|
|
setBackdropColorSub(RGB15(3, 1, 1));
|
|
setBackdropColor(RGB15(2, 1, 1));
|
|
snakelength = START_LENGTH_HARD;
|
|
snake = {{2, 1, 1}, {1, 1, 1}};
|
|
} else {
|
|
setBackdropColorSub(RGB15(3, 4, 7));
|
|
setBackdropColor(RGB15(2, 3, 6));
|
|
snakelength = START_LENGTH;
|
|
snake = {{4, 1, 1}, {3, 1, 1}, {2, 1, 1}, {1, 1, 1}};
|
|
}
|
|
|
|
consoleSelect(&topScreen);
|
|
consoleClear();
|
|
invalidate_hud(renderAssets);
|
|
consoleSelect(&bottomScreen);
|
|
consoleClear();
|
|
oamClear(&oamSub, 0, 128);
|
|
audio_start_music(audioContext);
|
|
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
|
|
}
|
|
|
|
int main(void) {
|
|
setup();
|
|
setBackdropColor(RGB15(2, 2, 6));
|
|
setBackdropColorSub(RGB15(2, 2, 6));
|
|
draw_menu(gameState, renderAssets, audio_is_music_enabled(audioContext));
|
|
|
|
inputContext.state = &gameState;
|
|
inputContext.renderAssets = &renderAssets;
|
|
inputContext.audio = &audioContext;
|
|
inputContext.internals = &internals;
|
|
inputContext.topScreen = &topScreen;
|
|
inputContext.init_game = init_game;
|
|
|
|
// Set up VBlank handler only after all subsystems, assets, and contexts are initialized
|
|
irqSet(IRQ_VBLANK, handle_vblank);
|
|
|
|
while (1) {
|
|
swiWaitForVBlank();
|
|
update_frame();
|
|
if (!handle_input_frame(inputContext)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|