feat: overhaul cursed apple sprite with custom tile data, vibrant palette, and dynamic pulsing glow

This commit is contained in:
phillip committed 2026-10-03 21:02:59 +02:00
1 parent 60bf111400
commit 08a4b41280
4 files changed
+44 -25

No files matched your search

+1 -1
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.
- **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.
- **Cursed Apple (Purple):** Spawns occasionally. Inverts player steering controls (Up ↔ Down, Left ↔ Right) for `CURSE_DURATION_TICKS` (40 ticks). The sprite is procedurally constructed at startup in [`main.cpp:91-112`](source/main.cpp#L91-L112) by copying the rotten apple tile pixels and mapping them to dedicated palette entries (`CURSE_PAL_SHADOW`, `CURSE_PAL_OUTLINE`, `CURSE_PAL_FILL`, `CURSE_PAL_GLOW`).
- **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).
- **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.
### 3.4 Game Modes & Progressive Speed Mechanics
+7
View File
@@ -7,6 +7,13 @@ const int START_LENGTH_HARD = 2;
const int APPLE_SCORE = 10;
const int CURSE_DURATION_TICKS = 40;
// Cursed apple palette indices in SPRITE_PALETTE_SUB
const uint16_t CURSE_PAL_OUTLINE = 16;
const uint16_t CURSE_PAL_FILL = 17;
const uint16_t CURSE_PAL_GLOW = 18;
const uint16_t CURSE_PAL_CORE = 19;
const uint16_t CURSE_PAL_LEAF = 20;
// Speed scaling configuration (gradual acceleration based on consumed apples)
const int SPEED_BASE_NORMAL_MHZ = 5000; // 5.0 Hz (~200 ms per step)
const int SPEED_BASE_HARD_MHZ = 9000; // 9.0 Hz (~111 ms per step)
+2 -1
View File
@@ -138,13 +138,14 @@ void draw_apples_rotten(const GameState &state, const RenderAssets &assets,
void draw_apples_cursed(const GameState &state, const RenderAssets &assets,
uint16_t &oamId) {
const bool hflip = ((state.ticks % 6) < 3);
for (const std::array<int, 2> &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);
false, hflip, false, false);
oamId++;
}
}
+34 -23
View File
@@ -100,33 +100,33 @@ void setup() {
consoleSelect(&topScreen);
dmaCopy(tilemapPal, SPRITE_PALETTE_SUB, tilemapPalLen);
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
// Recolor a copy of the rotten-apple tile (shadow/outline/fill/rot-spots) into unused
// palette slots to make the "cursed" apple sprite, since all 12 tiles are already in use.
const uint16_t CURSE_PAL_SHADOW = 14;
const uint16_t CURSE_PAL_OUTLINE = 15;
const uint16_t CURSE_PAL_FILL = 16;
const uint16_t CURSE_PAL_GLOW = 17;
// 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,
};
SPRITE_PALETTE_SUB[CURSE_PAL_SHADOW] = RGB15(9, 5, 12);
SPRITE_PALETTE_SUB[CURSE_PAL_OUTLINE] = RGB15(10, 0, 14);
SPRITE_PALETTE_SUB[CURSE_PAL_FILL] = RGB15(18, 0, 22);
SPRITE_PALETTE_SUB[CURSE_PAL_GLOW] = RGB15(28, 0, 24);
const auto *rottenTilePixels = static_cast<const uint8_t *>(get_sprite_pointer(11));
uint8_t cursedTilePixels[64];
for (int i = 0; i < 64; i++) {
switch (rottenTilePixels[i]) {
case 5: cursedTilePixels[i] = static_cast<uint8_t>(CURSE_PAL_SHADOW); break;
case 7: cursedTilePixels[i] = static_cast<uint8_t>(CURSE_PAL_OUTLINE); break;
case 10: cursedTilePixels[i] = static_cast<uint8_t>(CURSE_PAL_FILL); break;
case 12: cursedTilePixels[i] = static_cast<uint8_t>(CURSE_PAL_GLOW); break;
default: cursedTilePixels[i] = rottenTilePixels[i]; break;
// Copy cursed apple tile directly into allocated VRAM (32 halfwords)
auto *destAppleCursed = static_cast<uint16_t *>(renderAssets.gfx_apple_cursed);
const auto *srcAppleCursed = reinterpret_cast<const uint16_t *>(CURSED_APPLE_TILE);
for (size_t i = 0; i < 32; i++) {
destAppleCursed[i] = srcAppleCursed[i];
}
}
dmaCopy(cursedTilePixels, renderAssets.gfx_apple_cursed, 64);
audio_setup(audioContext);
@@ -138,6 +138,17 @@ void handle_vblank() {
consoleSelect(&topScreen);
draw_stats(gameState, renderAssets, internals, audio_is_music_enabled(audioContext), frame);
}
// 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))
: 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)));
oamUpdate(&oamSub);
//flashy_colors();
}