diff --git a/AGENTS.md b/AGENTS.md index d81f0cf..9a17784 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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>` 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 diff --git a/include/consts.h b/include/consts.h index a995580..34c02b2 100644 --- a/include/consts.h +++ b/include/consts.h @@ -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) diff --git a/source/game_render.cpp b/source/game_render.cpp index a6f600c..951f250 100644 --- a/source/game_render.cpp +++ b/source/game_render.cpp @@ -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 &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++; } } diff --git a/source/main.cpp b/source/main.cpp index 9c4ceb9..7e741b8 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -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(get_sprite_pointer(11)); - uint8_t cursedTilePixels[64]; - for (int i = 0; i < 64; i++) { - switch (rottenTilePixels[i]) { - case 5: cursedTilePixels[i] = static_cast(CURSE_PAL_SHADOW); break; - case 7: cursedTilePixels[i] = static_cast(CURSE_PAL_OUTLINE); break; - case 10: cursedTilePixels[i] = static_cast(CURSE_PAL_FILL); break; - case 12: cursedTilePixels[i] = static_cast(CURSE_PAL_GLOW); break; - default: cursedTilePixels[i] = rottenTilePixels[i]; break; - } + // Copy cursed apple tile directly into allocated VRAM (32 halfwords) + auto *destAppleCursed = static_cast(renderAssets.gfx_apple_cursed); + const auto *srcAppleCursed = reinterpret_cast(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(7U - pulse); // 0, 1, 2, 3, 3, 2, 1, 0 + SPRITE_PALETTE_SUB[CURSE_PAL_FILL] = static_cast(RGB15(20 + (wave * 3), 2 + wave, 26 + wave)); + SPRITE_PALETTE_SUB[CURSE_PAL_GLOW] = static_cast(RGB15(26 + wave, 6 + (wave * 4), 31)); + SPRITE_PALETTE_SUB[CURSE_PAL_CORE] = (wave >= 2U) + ? static_cast(RGB15(31, 31, 31)) + : static_cast(RGB15(31, 22 + (wave * 3), 31)); + SPRITE_PALETTE_SUB[CURSE_PAL_LEAF] = static_cast(RGB15(2 + (wave * 2), 31, 10 + (wave * 3))); + oamUpdate(&oamSub); //flashy_colors(); }