diff --git a/AGENTS.md b/AGENTS.md index fcd0110..23b22db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,8 +97,8 @@ 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. - **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), `update_frame()` 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 (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. +- **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 `update_frame()` ([`source/main.cpp`](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 `update_frame()` ([`source/main.cpp`](source/main.cpp)), 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 @@ -207,7 +207,7 @@ The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/ta - **Main Engine:** BG2 renders a dynamic cyber horizon, corner brackets, and starfield behind the top-screen console HUD and menu options (`bgSetPriority(topScreen.bgId, 0)`, `bgSetPriority(bgMainId, 3)`). - **Sub Engine:** BG2 renders an animated retro cyber-grid with 8x8 cell alignment dots, ambient celestial nodes, and circuit pathways behind the snake and apple sprites (`bgSetPriority(bottomScreen.bgId, 1)`, `bgSetPriority(bgSubId, 3)`, sprites at priority 0). - **Smooth 60 FPS Hardware Scrolling:** Both background layers use fixed-point 8.8 subpixel accumulators with `bgSetScroll()` and `bgUpdate()` during `update_frame()` for silky smooth drift at 0% CPU cost. Drift speed accelerates dynamically as apples are consumed. -- **Dynamic VBlank Palette Animation & Progressive Intensity:** +- **Dynamic 60 FPS Palette Animation & Progressive Intensity:** - **Normal Mode:** Neon cyan / teal / midnight blue theme with oscillating sine-wave twinkling stars and breathing grid nodes, transitioning smoothly towards blazing electric plasma cyan at higher apple counts. - **Hard Mode:** Fiery amber / crimson cyber grid with faster pulse rate, transitioning towards volcanic hyper-crimson and incandescent solar white-gold. - **Cursed Mode:** Radioactive violet / magenta glitch strobe and high-speed warp drift during inverted controls (further intensified with apple count). @@ -232,17 +232,27 @@ The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/ta - **IRQ Stack Limit (256 bytes):** In `calico/lib/ds9.ld`, the ARM9 IRQ stack (`sp_irq`) has a total budget of only 256 bytes (`__sp_irq = __sp_usr + 0x100`), growing downwards directly into the user stack area. Therefore: - **Minimal VBlank ISR:** `handle_vblank()` must strictly do lightweight bookkeeping (`frame++`). All heavy lifting (`bg_update`, `draw_stats`, `iprintf`, `oamUpdate`, barrier frame ticking, dynamic palette math) must run on the main thread in `update_frame()` synchronized with `swiWaitForVBlank()`, utilizing the 16 KB DTCM user stack instead of overflowing the 256-byte IRQ stack. - **Zero dynamic allocations in ISRs:** Never perform dynamic heap allocation (`malloc`, `operator new`, vector reallocation) inside hardware interrupt handlers (`do_tick`, `handle_vblank`). Dynamic vectors like `bad_apples`, `cursed_apples`, and `apples` must pre-reserve capacity upfront on the main thread in `setup()` / `init_game()`. - - **Context objects in `.bss`:** Large structs such as `InputContext` must be allocated in `.bss` (global or static) rather than on `main()`'s DTCM stack frame to avoid stack-smash corruption if an interrupt handler pushes registers. - - **Safe state re-initialization:** In `init_game()`, call `timerStop(0)` before resetting game state or clearing containers, and only call `timerStart(0, ...)` at the very end when all state members and HUD caches are cleanly initialized. -3. **Hardware Registers & Memory Banking:** + - **Context objects in `.bss`:** Large structs such as `InputContext` and `RenderAssets` should be allocated in `.bss` (global or static) rather than on `main()`'s DTCM stack frame to conserve stack headroom and prevent stack-smash corruption. + - **Console Re-entrancy:** `consoleSelect()` and `iprintf()` are non-reentrant. Libnds relies on a global `currentConsole` pointer and internal formatting state. Never call `consoleSelect` or `iprintf` from an interrupt handler while the main thread also prints to the console. Centralize all console rendering on the main thread inside `update_frame()` or `draw_menu()`. + - **HUD Dirty-Flag Caching (`HudCache`):** Running ANSI terminal formatting and `iprintf` on every frame burns unnecessary cycles and causes text flicker. Use dirty flags (`HudCache`) to redraw console elements only when values change (score, apples, paused status, game over state). + - **Input Scanning Frequency & Bus Contention:** `scanKeys()` must be called once per frame synchronized with `swiWaitForVBlank()`. Calling `scanKeys()` in an unthrottled loop (spinning at 66 MHz) saturates ARM7 IPC queues (`touchRead`, `keypadGetState`), resets key-debounce edge detectors within microseconds, and leads to dropped inputs. + - **Boot Subsystem Sequencing:** Always complete hardware configuration, console initialization, and data structure setup (`setup()`) before enabling the VBlank interrupt (`irqSet(IRQ_VBLANK, handle_vblank)`). Enabling the ISR beforehand causes VBlank interrupts to fire while `consoleLoadFont` is streaming font tiles to VRAM, triggering Data Aborts at startup. + - **Safe State Re-initialization:** In `init_game()`, call `timerStop(0)` before resetting game state or clearing containers, and only call `timerStart(0, ...)` at the very end when all state members and HUD caches are cleanly initialized. +3. **Debugging Traps & Data Abort Diagnosis with GDB:** + - In Calico/devkitARM, unhandled Data Aborts route to `__arm_excpt_dabt` (`0x01ff8560`) and trap into an infinite loop at `__excpt_entry` (`0x01ff8594`). + - If melonDS appears unresponsive or logs `attempt to restore CPSR under bad mode`, attach `arm-none-eabi-gdb` to port 3333 (`target remote localhost:3333`): + - In `__excpt_entry`, inspect memory at `0x02fffd80`: the faulted instruction's address (`lr_abt - 8`) is pushed to `0x02fffd98`, and the faulted `SPSR` is at `0x02fffd90`. + - An unaligned/odd stack pointer (e.g. `sp = 0x02fffd8f`) indicates memory corruption caused by an `sp_irq` stack overflow clobbering `__sp_usr`. + - Use `info line *0x...` in GDB on the faulted PC to instantly locate the offending C++ line. +4. **Hardware Registers & Memory Banking:** - Graphic tiles and palettes must be transferred using DMA (`dmaCopy`) or direct memory copies into allocated VRAM / OAM memory banks. - Do not modify VRAM allocation configurations without ensuring compatibility with console text generators and OAM sub-engines. -4. **Toolchain Flags:** +5. **Toolchain Flags:** - Keep `-mthumb-interwork`, `-march=armv5te`, `-mtune=arm946e-s`, and specs `-specs=${DEVKITPRO_ROOT}/calico/share/ds9.specs` intact in [`CMakeLists.txt`](CMakeLists.txt). -5. **Code Style:** +6. **Code Style:** - Follow the naming and check options specified in [`.clang-tidy`](.clang-tidy) (functions/variables in `snake_case`, classes/structs in `CamelCase`). - Run `cmake --build build` to verify clean compilation without warnings or errors. -6. **Commit Message Attribution:** +7. **Commit Message Attribution:** - Every commit message generated by an AI agent **must** include a `Co-authored-by` trailer, separated from the subject/body by a blank line. Each agent should substitute its own name and contact address: ``` Co-authored-by: diff --git a/README.md b/README.md index fcb2192..47c3c88 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,15 @@ - Background tracker music ([`0rigin.xm`](sounds/0rigin.xm)) powered by MaxMod 9. - Sound effects for movement, pickups, rotten/cursed explosions, pause, and game over. - On-the-fly music toggle from the menu or pause screen. -- **Game Modes:** - - **Normal Mode:** Relaxed tick rate (`TIMER_FREQ_1024(5)`), starting length 4. - - **Hard Mode:** High-speed tick rate (`TIMER_FREQ_1024(9)`), starting length 2, and periodic rotten apple hazards. +- **Game Modes & Progressive Intensity:** + - **Normal Mode:** Starts at 5.0 Hz, scaling smoothly up to 13.0 Hz as apples are consumed. + - **Hard Mode:** Starts at 5.0 Hz and scales up to a blazing 17.0 Hz, spawning rotten apple hazards and dynamic temporary energy barriers. + - **Temporary Fatal Barriers:** Energy walls appear dynamically in Hard Mode after 3 apples. Each barrier flashes a warning reticle for 2.0 s before solidifying into a lethal hazard-striped obstacle for 4.0 s. + - **Dynamic Backgrounds:** Dual-screen cyber grids accelerate drift and intensify color palettes (plasma cyan in Normal, hyper-crimson in Hard, radioactive violet when cursed) in tandem with game speed. +- **Persistent Game Over Screen:** + - Collision state is preserved: snake, apples, and barriers remain visible on the playfield so players can review their final position. + - Background smoothly decelerates and dims into a somber tone. + - Dedicated top-screen summary presents final score, apples eaten, length, difficulty, and exact cause of defeat (Bite Collision, Energy Barrier Impact, or Rotten Apple Depletion). - **Main Menu & Pause Screen:** - Integrated start menu to toggle Hard Mode and Music. - In-game pause menu with instant restarts and mode toggles.