feat: add handcrafted 8x8 sprite art and VBlank bioluminescent animation for rotten apples

This commit is contained in:
phillip committed 2026-10-03 23:49:24 +02:00
1 parent 8559159fc0
commit 91908d1f15
6 files changed
+183 -121

No files matched your search

+2 -2
View File
@@ -97,7 +97,7 @@ The game operates in one of two scenes ([`Scene`](include/game_state.h#L9-L12)):
- **Snake Body:** Represented as `std::deque<std::array<int, 3>>` where each entry is `{x, y, direction}`. Alternating segments use regular and small sprite tiles. Head and tail sprites flip dynamically according to direction.
- **Immediate Head Rotation Feedback:** When the player presses a D-Pad direction while moving, `handle_input_frame()` validates that the input is not a reverse into the neck. If valid, `state.snakeDirection` is immediately updated and `draw_game()` is called on the main thread before the next movement tick. In `draw_snake_head()`, the head sprite tile pointer (`gfx_head_v` vs `gfx_head_h`) and hardware flip flags (`hflip`, `vflip`) are selected directly from `state.snakeDirection` and written to shadow OAM (`oamSub`). On the subsequent 60 FPS VBlank (~16.6 ms), `handle_vblank()` commits shadow OAM to hardware registers via `oamUpdate(&oamSub)`, providing instantaneous visual feedback that the snake has turned to face the new heading before the slower Timer 0 tick (5.0–17.0 Hz) advances grid coordinates via `update_snake()`.
- **Normal Apple (Red):** Spawns if count < 1. Awards +10 score, increases length by 1, plays pickup sound. Flashes/blinks every 4 ticks.
- **Rotten Apple (Brown/Green):** Spawns in Hard Mode (periodic chance up to 20 on screen). Decreases length by 1, plays explosion SFX. If length drops below 2, triggers Game Over. Despawns after `BAD_APPLE_LIFETIME_TICKS` (80 ticks).
- **Rotten Apple (Bruised Amber & Toxic Green Mold):** Spawns in Hard Mode (periodic chance up to 20 on screen). Decreases length by 1, plays explosion SFX. If length drops below 2, triggers Game Over. Rendered using a dedicated handcrafted 8x8 sprite with bruised golden-amber decaying skin, sunken dark necrotic rot cavities, a withered dead twig stem, and festering green mold colonies with radiant toxic spore pustules. Uses high-contrast dedicated palette entries (`ROTTEN_PAL_OUTLINE`, `ROTTEN_PAL_SKIN`, `ROTTEN_PAL_DECAY`, `ROTTEN_PAL_MOLD`, `ROTTEN_PAL_SPORE`, `ROTTEN_PAL_STEM`) specifically tuned for high visibility and contrast against the Hard Mode crimson background. Its mold and spore pustules organically breathe and fester with toxic bioluminescence every frame at 60 FPS in [`handle_vblank()`](source/main.cpp), and sprites alternate horizontal orientation based on grid coordinates (`hflip`) in [`draw_apples_rotten()`](source/game_render.cpp) for natural visual variety across the playfield. Despawns after `BAD_APPLE_LIFETIME_TICKS` (80 ticks).
- **Cursed Apple (Electric Neon Purple & Radioactive Green):** Spawns occasionally. Inverts player steering controls (Up ↔ Down, Left ↔ Right) for `CURSE_DURATION_TICKS` (40 ticks). Rendered using a dedicated 8x8 sprite with a glowing core rune and toxic green horn, using high-contrast dedicated palette entries (`CURSE_PAL_OUTLINE`, `CURSE_PAL_FILL`, `CURSE_PAL_GLOW`, `CURSE_PAL_CORE`, `CURSE_PAL_LEAF`). Its palette dynamically breathes and pulses with radioactive energy every frame at 60 FPS in [`handle_vblank()`](source/main.cpp#L134-L153), and its sprite sways/twitches with mystical instability in [`draw_apples_cursed()`](source/game_render.cpp#L139-L151). Despawns after `CURSED_APPLE_LIFETIME_TICKS` (90 ticks), clearing the way for new cursed apples to spawn if previously avoided.
- **Reverse-Direction Denial:** Pressing the D-Pad in the exact opposite direction (i.e. into the snake's own neck) is silently denied instead of causing an immediate Game Over. The game computes the would-be next head position (including edge-wrapping) and compares it to the neck segment; if they match, the direction change is rejected and a subtle "denied" thud SFX plays.
@@ -192,7 +192,7 @@ The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/ta
### 6.2 Asset Pipeline
- **Graphics Pipeline:**
- [`gfx/tilemap.png`](gfx/tilemap.png) + [`gfx/tilemap.grit`](gfx/tilemap.grit) are processed by `grit` into `${CMAKE_CURRENT_BINARY_DIR}/tilemap.s` and `tilemap.h`.
- Sprites are referenced by index: tile 1 (segment), 2 (small segment), 3 (head horizontal), 4 (head vertical), 5/6 (apple flash frames), 7/8/9/10 (tail variations), 11 (rotten apple). All 12 sprite tiles are preloaded once into VRAM at startup; directional and blinking variants are rendered via pointer selection and hardware flipping flags without mid-frame DMA transfers.
- Sprites are referenced by index: tile 1 (segment), 2 (small segment), 3 (head horizontal), 4 (head vertical), 5/6 (apple flash frames), 7/8/9/10 (tail variations). Special entities (Rotten Apple, Cursed Apple, Barrier solid, and Barrier warning reticle) use dedicated handcrafted 8x8 256-color tile data copied directly into VRAM at startup. All sprites are preloaded once into VRAM; directional, orientation, and blinking variants are rendered via pointer selection and hardware flipping flags without mid-frame DMA transfers.
- Active OAM sprites are clamped at hardware limit (128) and trailing unused sprites are hidden every frame via `oamClear(&oamSub, oamId, 128 - oamId)`.
- **Decoupled Steering & Render Flow:** Steering inputs update `state.snakeDirection` and invoke `draw_game()` immediately on the main thread, updating the snake head's sprite pointer and flipping flags in shadow OAM (`oamSub`). Display updates are synchronized via VBlank (`handle_vblank()`), which flushes `oamSub` to hardware registers with `oamUpdate(&oamSub)` at 60 FPS (~16.6 ms) independently of the slower Timer 0 movement ticks (5.0–17.0 Hz).
- Console text HUD rendering in `draw_stats()` is throttled via dirty-flag caching (`HudCache`), eliminating redundant `iprintf` calls on VBlank.
+1 -1
View File
@@ -9,7 +9,7 @@
- **Classic Gameplay with a Twist:**
- Classic grid-based snake action wrapped on a 32×24 playfield with edge-wrapping.
- **Normal Apples:** Boost length and score (+10), with flashing animation and pickup sounds.
- **Rotten Apples:** Shrink snake length by 1; shrinking below length 2 results in Game Over! Despawn after 80 ticks.
- **Rotten Apples:** Shrink snake length by 1; shrinking below length 2 results in Game Over! Rendered with custom 8x8 pixel art featuring bruised amber decaying skin, dark rot cavities, a withered twig stem, and festering green mold spores with 60 FPS bioluminescent respiration. Despawn after 80 ticks.
- **Cursed Apples:** Invert steering controls (Up ↔ Down, Left ↔ Right) for 40 ticks, requiring quick reflexes and adapted steering. Despawn after 90 ticks if avoided, allowing new curses to spawn later.
- **Dual-Screen Layout:**
- **Top Screen:** Real-time scoreboard, status alerts (*"CONTROLS INVERTED!"*), pause overlay, and optional debug stats.
+8
View File
@@ -25,6 +25,14 @@ const uint16_t BARRIER_PAL_FILL = 22;
const uint16_t BARRIER_PAL_STRIPE = 23;
const uint16_t BARRIER_PAL_WARN = 24;
// Rotten apple palette indices in SPRITE_PALETTE_SUB (animated every VBlank)
const uint16_t ROTTEN_PAL_OUTLINE = 25; // Crisp dark outline rim
const uint16_t ROTTEN_PAL_SKIN = 26; // Bruised golden-amber decaying apple skin
const uint16_t ROTTEN_PAL_DECAY = 27; // Sunken dark necrotic rot cavity
const uint16_t ROTTEN_PAL_MOLD = 28; // Vivid toxic fungal green mold (animated in VBlank)
const uint16_t ROTTEN_PAL_SPORE = 29; // Radiant glowing spore pustule (animated in VBlank)
const uint16_t ROTTEN_PAL_STEM = 30; // Withered dry dead woody stem
// Hard-mode temporary fatal barriers (all timings in 60 Hz VBlank frames,
// so they stay constant in wall-clock time while the tick rate accelerates)
const int BARRIER_START_APPLES = 3; // Barriers only start appearing after eating a few apples
+26 -21
View File
@@ -425,26 +425,26 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
col_frame = make_rgb15(18, 2, 22);
col_frame_accent = make_rgb15(28, 6, 31);
} else if (hard) {
// Fiery amber / crimson cyber grid interpolating towards volcanic hyper-crimson
const uint16_t c_faint_0 = make_rgb15(5, 2, 1);
const uint16_t c_subtle_0 = make_rgb15(10, 4, 2);
const uint16_t c_primary_0 = make_rgb15(18, 8, 3);
const uint16_t c_star_dim_0 = make_rgb15(8, 3, 1);
const uint16_t c_star_med_0 = make_rgb15(16, 7, 2);
const uint16_t c_star_bright_0 = make_rgb15(26, 14, 4);
const uint16_t c_star_flare_0 = make_rgb15(31, 24, 10);
const uint16_t c_frame_0 = make_rgb15(14, 5, 2);
const uint16_t c_frame_accent_0 = make_rgb15(26, 10, 4);
// Fiery amber / crimson cyber grid interpolating towards volcanic hyper-crimson (toned darker for high item contrast)
const uint16_t c_faint_0 = make_rgb15(3, 1, 1);
const uint16_t c_subtle_0 = make_rgb15(6, 2, 1);
const uint16_t c_primary_0 = make_rgb15(12, 4, 2);
const uint16_t c_star_dim_0 = make_rgb15(6, 2, 1);
const uint16_t c_star_med_0 = make_rgb15(11, 4, 2);
const uint16_t c_star_bright_0 = make_rgb15(20, 9, 3);
const uint16_t c_star_flare_0 = make_rgb15(28, 18, 6);
const uint16_t c_frame_0 = make_rgb15(9, 3, 1);
const uint16_t c_frame_accent_0 = make_rgb15(18, 6, 2);
const uint16_t c_faint_max = make_rgb15(8, 3, 2);
const uint16_t c_subtle_max = make_rgb15(16, 7, 3);
const uint16_t c_primary_max = make_rgb15(28, 14, 5);
const uint16_t c_star_dim_max = make_rgb15(14, 5, 2);
const uint16_t c_star_med_max = make_rgb15(24, 11, 3);
const uint16_t c_star_bright_max = make_rgb15(31, 20, 6);
const uint16_t c_star_flare_max = make_rgb15(31, 31, 18);
const uint16_t c_frame_max = make_rgb15(22, 8, 3);
const uint16_t c_frame_accent_max = make_rgb15(31, 16, 5);
const uint16_t c_faint_max = make_rgb15(5, 2, 1);
const uint16_t c_subtle_max = make_rgb15(9, 3, 2);
const uint16_t c_primary_max = make_rgb15(18, 6, 2);
const uint16_t c_star_dim_max = make_rgb15(9, 3, 1);
const uint16_t c_star_med_max = make_rgb15(16, 6, 2);
const uint16_t c_star_bright_max = make_rgb15(26, 12, 4);
const uint16_t c_star_flare_max = make_rgb15(31, 26, 10);
const uint16_t c_frame_max = make_rgb15(15, 5, 2);
const uint16_t c_frame_accent_max = make_rgb15(25, 10, 3);
col_faint = lerp_color(c_faint_0, c_faint_max, intensity_31);
col_subtle = lerp_color(c_subtle_0, c_subtle_max, intensity_31);
@@ -491,8 +491,8 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
// Deep space backdrop color dynamically illuminates with intensity
if (in_game && !over) {
if (hard) {
BG_PALETTE_SUB[0] = lerp_color(make_rgb15(8, 3, 3), make_rgb15(12, 4, 4), intensity_31);
BG_PALETTE[0] = lerp_color(make_rgb15(6, 2, 2), make_rgb15(9, 3, 3), intensity_31);
BG_PALETTE_SUB[0] = lerp_color(make_rgb15(3, 1, 1), make_rgb15(6, 2, 2), intensity_31);
BG_PALETTE[0] = lerp_color(make_rgb15(2, 1, 1), make_rgb15(5, 1, 2), intensity_31);
} else {
BG_PALETTE_SUB[0] = lerp_color(make_rgb15(3, 4, 7), make_rgb15(5, 7, 12), intensity_31);
BG_PALETTE[0] = lerp_color(make_rgb15(2, 3, 6), make_rgb15(3, 5, 10), intensity_31);
@@ -500,10 +500,15 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
} else if (over) {
BG_PALETTE_SUB[0] = make_rgb15(3, 2, 2);
BG_PALETTE[0] = make_rgb15(2, 1, 1);
} else {
if (hard) {
BG_PALETTE_SUB[0] = make_rgb15(3, 1, 1);
BG_PALETTE[0] = make_rgb15(2, 1, 1);
} else {
BG_PALETTE_SUB[0] = make_rgb15(2, 2, 6);
BG_PALETTE[0] = make_rgb15(2, 2, 6);
}
}
// Animate twinkling stars using sine oscillations
const uint16_t anim_dim = lerp_color(col_star_dim, col_star_med, sine_val);
+2 -1
View File
@@ -129,9 +129,10 @@ void draw_apples_rotten(const GameState &state, const RenderAssets &assets,
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, false, false, false);
false, hflip, false, false);
oamId++;
}
}
+144 -96
View File
@@ -1,14 +1,14 @@
#include <nds.h>
#include <vector>
#include <deque>
#include <array>
#include <consts.h>
#include <game_state.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_audio.h>
#include <game_input.h>
#include <game_bg.h>
#include <game_state.h>
#include <nds.h>
#include <vector>
#include "tilemap.h"
@@ -55,6 +55,42 @@ 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
@@ -64,8 +100,10 @@ void setup() {
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);
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);
@@ -74,20 +112,34 @@ void setup() {
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);
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);
@@ -99,18 +151,23 @@ void setup() {
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);
dmaCopy(get_sprite_pointer(11), renderAssets.gfx_apple_rotten, 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 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);
@@ -118,75 +175,41 @@ void setup() {
SPRITE_PALETTE_SUB[BARRIER_PAL_STRIPE] = RGB15(31, 22, 6);
SPRITE_PALETTE_SUB[BARRIER_PAL_WARN] = RGB15(18, 4, 2);
// 8x8 256-color tile data for Cursed Apple (vibrant electric purple apple with radioactive green horn & glowing core)
static const uint32_t CURSED_APPLE_TILE[16] = {
0x10000000, 0x00001414,
0x10101000, 0x00101014,
0x12111110, 0x10111112,
0x13121110, 0x10111112,
0x13131110, 0x10111112,
0x13121110, 0x10111211,
0x11111000, 0x00101111,
0x10100000, 0x00101000,
};
// 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
// 8x8 256-color tile data for solid fatal barrier (bordered hazard stripe block)
static const uint32_t BARRIER_SOLID_TILE[16] = {
0x15151515, 0x15151515,
0x16171715, 0x15171716,
0x16161715, 0x15161717,
0x17161615, 0x15161617,
0x17171615, 0x15171616,
0x16171715, 0x15171716,
0x16161715, 0x15161717,
0x15151515, 0x15151515,
};
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);
// 8x8 256-color tile data for barrier warning announcement (pulsing reticle frame)
static const uint32_t BARRIER_WARN_TILE[16] = {
0x00001818, 0x18180000,
0x18000018, 0x18000018,
0x18180000, 0x00001818,
0x00181800, 0x00181800,
0x00181800, 0x00181800,
0x18180000, 0x00001818,
0x18000018, 0x18000018,
0x00001818, 0x18180000,
};
// Copy cursed apple tile directly into allocated VRAM (32 halfwords)
auto *dest_apple_cursed = static_cast<uint16_t *>(renderAssets.gfx_apple_cursed);
const auto *src_apple_cursed = reinterpret_cast<const uint16_t *>(CURSED_APPLE_TILE);
for (size_t i = 0; i < 32; i++) {
dest_apple_cursed[i] = src_apple_cursed[i];
}
auto *dest_barrier = static_cast<uint16_t *>(renderAssets.gfx_barrier);
const auto *src_barrier = reinterpret_cast<const uint16_t *>(BARRIER_SOLID_TILE);
for (size_t i = 0; i < 32; i++) {
dest_barrier[i] = src_barrier[i];
}
auto *dest_barrier_warn = static_cast<uint16_t *>(renderAssets.gfx_barrier_warn);
const auto *src_barrier_warn = reinterpret_cast<const uint16_t *>(BARRIER_WARN_TILE);
for (size_t i = 0; i < 32; i++) {
dest_barrier_warn[i] = src_barrier_warn[i];
}
// Pre-reserve capacities to eliminate dynamic heap allocations inside Timer 0 ISR (do_tick)
// 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++;
bg_update(bgContext, gameState, frame);
if (gameState.scene == Scene::Playing) {
consoleSelect(&topScreen);
draw_stats(gameState, renderAssets, internals, audio_is_music_enabled(audioContext), frame);
draw_stats(gameState, renderAssets, internals,
audio_is_music_enabled(audioContext), frame);
if (!gameState.paused) {
tick_barriers_frame(gameState);
}
@@ -195,13 +218,36 @@ void handle_vblank() {
// Dynamic pulsing radioactive glow for Cursed Apples (smooth 60fps breathing)
const uint8_t pulse = (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))
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)));
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 uint8_t rot_pulse = ((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();
@@ -215,7 +261,8 @@ void do_tick() {
/*if (hardMode) {
timerStop(0);
timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(ticks+5), do_tick);
timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(ticks+5),
do_tick);
}*/
update_snake(gameState);
@@ -227,7 +274,8 @@ void do_tick() {
if (check_collision_apples(gameState)) {
audio_play_pickup(audioContext);
bg_trigger_pulse(bgContext);
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState),
do_tick);
}
if (check_collision_apples_rotten(gameState)) {
audio_play_boom(audioContext);
@@ -268,8 +316,8 @@ void init_game() {
gameState.cursed_apples.clear();
if (hardMode) {
setBackdropColorSub(RGB15(8, 3, 3));
setBackdropColor(RGB15(6, 2, 2));
setBackdropColorSub(RGB15(3, 1, 1));
setBackdropColor(RGB15(2, 1, 1));
snakelength = START_LENGTH_HARD;
snake = {{2, 1, 1}, {1, 1, 1}};
} else {