diff --git a/AGENTS.md b/AGENTS.md index 7927dc3..1d4a013 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,9 +95,10 @@ The game operates in one of two scenes ([`Scene`](include/game_state.h#L9-L12)): ### 3.3 Game Elements & Apples - **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. +- **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); flashes during its last `EVIL_ITEM_BLINK_TICKS` (20 ticks, accelerating to rapid strobe during the final 8 ticks) to signal expiration. -- **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) with the same warning blink, clearing the way for new cursed apples to spawn if previously avoided. +- **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). +- **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. ### 3.4 Game Modes & Progressive Speed Mechanics @@ -193,6 +194,7 @@ The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/ta - [`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. - 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. - **Audio Pipeline:** - Audio files in [`sounds/`](sounds/) are compiled by `mmutil` into `soundbank.bin` and header `soundbank.h`. diff --git a/README.md b/README.md index ba2d227..4183e15 100644 --- a/README.md +++ b/README.md @@ -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 with a flashing warning animation. + - **Rotten Apples:** Shrink snake length by 1; shrinking below length 2 results in Game Over! 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. diff --git a/include/consts.h b/include/consts.h index 1c4baed..0588c2d 100644 --- a/include/consts.h +++ b/include/consts.h @@ -11,8 +11,6 @@ const int CURSE_DURATION_TICKS = 40; // Evil item lifetimes (in game ticks, so density stays balanced across speed scaling) const int BAD_APPLE_LIFETIME_TICKS = 80; // Rotten apple lasts 80 ticks (~8 spawn cycles) const int CURSED_APPLE_LIFETIME_TICKS = 90; // Cursed apple lasts 90 ticks before despawning -const int EVIL_ITEM_BLINK_TICKS = 20; // Last 20 ticks: flashes to warn player of imminent despawn -const int EVIL_ITEM_FAST_BLINK_TICKS = 8; // Last 8 ticks: rapid strobe before disappearing // Cursed apple palette indices in SPRITE_PALETTE_SUB const uint16_t CURSE_PAL_OUTLINE = 16; diff --git a/source/game_render.cpp b/source/game_render.cpp index 2d2c141..9d4930e 100644 --- a/source/game_render.cpp +++ b/source/game_render.cpp @@ -129,14 +129,9 @@ void draw_apples_rotten(const GameState &state, const RenderAssets &assets, if (oamId >= 128) { return; } - const bool blink = (apple.ticksRemaining <= EVIL_ITEM_BLINK_TICKS); - const bool is_hidden = - blink && ((apple.ticksRemaining <= EVIL_ITEM_FAST_BLINK_TICKS) - ? ((state.ticks % 2) == 0) - : (((state.ticks / 2) % 2) == 0)); oamSet(&oamSub, oamId, apple.x * 8, apple.y * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, assets.gfx_apple_rotten, -1, false, - is_hidden, false, false, false); + false, false, false, false); oamId++; } } @@ -148,14 +143,9 @@ void draw_apples_cursed(const GameState &state, const RenderAssets &assets, if (oamId >= 128) { return; } - const bool blink = (apple.ticksRemaining <= EVIL_ITEM_BLINK_TICKS); - const bool is_hidden = - blink && ((apple.ticksRemaining <= EVIL_ITEM_FAST_BLINK_TICKS) - ? ((state.ticks % 2) == 0) - : (((state.ticks / 2) % 2) == 0)); oamSet(&oamSub, oamId, apple.x * 8, apple.y * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, assets.gfx_apple_cursed, -1, false, - is_hidden, hflip, false, false); + false, hflip, false, false); oamId++; } }