feat: add custom background rendering and speed scaling features for snake gameplay
This commit is contained in:
1 parent
93c0a44027
commit
60bf111400
13 files changed
+709
-24
No files matched your search
@@ -5,14 +5,14 @@ CompileFlags:
|
|||||||
- -mthumb-interwork
|
- -mthumb-interwork
|
||||||
Add:
|
Add:
|
||||||
- -isystem
|
- -isystem
|
||||||
- /opt/devkitpro/devkitARM/arm-none-eabi/include/c++/15.2.0
|
- /opt/devkitpro/devkitARM/arm-none-eabi/include/c++/16.1.0
|
||||||
- -isystem
|
- -isystem
|
||||||
- /opt/devkitpro/devkitARM/arm-none-eabi/include/c++/15.2.0/arm-none-eabi
|
- /opt/devkitpro/devkitARM/arm-none-eabi/include/c++/16.1.0/arm-none-eabi
|
||||||
- -isystem
|
- -isystem
|
||||||
- /opt/devkitpro/devkitARM/arm-none-eabi/include/c++/15.2.0/backward
|
- /opt/devkitpro/devkitARM/arm-none-eabi/include/c++/16.1.0/backward
|
||||||
- -isystem
|
- -isystem
|
||||||
- /opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/15.2.0/include
|
- /opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/16.1.0/include
|
||||||
- -isystem
|
- -isystem
|
||||||
- /opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/15.2.0/include-fixed
|
- /opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/16.1.0/include-fixed
|
||||||
- -isystem
|
- -isystem
|
||||||
- /opt/devkitpro/devkitARM/arm-none-eabi/include
|
- /opt/devkitpro/devkitARM/arm-none-eabi/include
|
||||||
@@ -29,6 +29,7 @@ snakePlus/
|
|||||||
├── include/ # C++ Header declarations
|
├── include/ # C++ Header declarations
|
||||||
│ ├── consts.h # Grid dimensions, scores, tick rates, legacy bitmaps
|
│ ├── consts.h # Grid dimensions, scores, tick rates, legacy bitmaps
|
||||||
│ ├── game_audio.h # AudioContext struct, MaxMod sound & BGM declarations
|
│ ├── game_audio.h # AudioContext struct, MaxMod sound & BGM declarations
|
||||||
|
│ ├── game_bg.h # BackgroundContext struct, animated dual-screen BG declarations
|
||||||
│ ├── game_input.h # InputContext struct, button scanning & menu logic
|
│ ├── game_input.h # InputContext struct, button scanning & menu logic
|
||||||
│ ├── game_logic.h # Snake mechanics, collision detection, apple spawning
|
│ ├── game_logic.h # Snake mechanics, collision detection, apple spawning
|
||||||
│ ├── game_render.h # RenderAssets struct, dual-screen console/OAM draw routines
|
│ ├── game_render.h # RenderAssets struct, dual-screen console/OAM draw routines
|
||||||
@@ -37,6 +38,7 @@ snakePlus/
|
|||||||
├── source/ # C++ Implementation files
|
├── source/ # C++ Implementation files
|
||||||
│ ├── main.cpp # Hardware setup, VBlank IRQ, timer ticks, game bootstrap
|
│ ├── main.cpp # Hardware setup, VBlank IRQ, timer ticks, game bootstrap
|
||||||
│ ├── game_audio.cpp # MaxMod soundbank loading, BGM loop, SFX dispatch
|
│ ├── game_audio.cpp # MaxMod soundbank loading, BGM loop, SFX dispatch
|
||||||
|
│ ├── game_bg.cpp # Dual-screen procedural tilemaps, scrolling & VBlank palette cycling
|
||||||
│ ├── game_input.cpp # Key mapping, inverted control logic, reverse-denial, menu navigation
|
│ ├── game_input.cpp # Key mapping, inverted control logic, reverse-denial, menu navigation
|
||||||
│ ├── game_logic.cpp # Position update, edge wrapping, collision resolution, RNG
|
│ ├── game_logic.cpp # Position update, edge wrapping, collision resolution, RNG
|
||||||
│ └── game_render.cpp # OAM sprite rendering, console text HUD & menu screens
|
│ └── game_render.cpp # OAM sprite rendering, console text HUD & menu screens
|
||||||
@@ -98,9 +100,16 @@ The game operates in one of two scenes ([`Scene`](include/game_state.h#L9-L12)):
|
|||||||
- **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 (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`).
|
||||||
- **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.
|
- **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
|
### 3.4 Game Modes & Progressive Speed Mechanics
|
||||||
- **Normal Mode:** Starts with length 4, timer frequency `TIMER_FREQ_1024(5)`, background color `RGB15(5,5,5)`.
|
- **Normal Mode:** Starts with length 4, base timer frequency 5.0 Hz (`SPEED_BASE_NORMAL_MHZ = 5000`), scaling up to 13.0 Hz (`SPEED_MAX_NORMAL_MHZ = 13000`).
|
||||||
- **Hard Mode:** Starts with length 2, timer frequency `TIMER_FREQ_1024(9)` (faster), background color `RGB15(10,5,5)`, spawns rotten apples.
|
- **Hard Mode:** Starts with length 2, base timer frequency 9.0 Hz (`SPEED_BASE_HARD_MHZ = 9000`), scaling up to 17.0 Hz (`SPEED_MAX_HARD_MHZ = 17000`), and spawns rotten apples.
|
||||||
|
- **Progressive Speed Scaling:** Each consumed regular apple increases `state.applesEaten`. Game speed scales linearly with consumed apples up to `SPEED_APPLES_CAP = 40`, granting smooth +200 mHz acceleration per apple. Timer 0 reload registers are recomputed on the fly (`calculate_timer_reload()`).
|
||||||
|
- **Progressive Background Intensity Scaling:** Visual effects intensify at the exact same rate (`calculate_intensity_31()`):
|
||||||
|
- Hardware drift scroll speeds accelerate on both top and bottom screens.
|
||||||
|
- Celestial node oscillation & twinkling breathing frequencies increase via fixed-point phase accumulation (`anim_phase_fp`).
|
||||||
|
- Color palettes dynamically interpolate from calm cyber tones towards blazing electric plasma cyan (Normal) or volcanic hyper-crimson / solar white-gold (Hard).
|
||||||
|
- Deep-space backdrop colors illuminate in tandem with the intensity level.
|
||||||
|
- Apple pickup shockwave pulses flare with heightened luminance.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -189,9 +198,22 @@ The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/ta
|
|||||||
- `bin2s` embeds `soundbank.bin` into assembly source `soundbank_bin.S` and header `soundbank_bin.h`.
|
- `bin2s` embeds `soundbank.bin` into assembly source `soundbank_bin.S` and header `soundbank_bin.h`.
|
||||||
- Audio runs via `libnds`'s `maxmod9` (`mm9`).
|
- Audio runs via `libnds`'s `maxmod9` (`mm9`).
|
||||||
|
|
||||||
### 6.3 Static Analysis & Tooling
|
### 6.3 Background Pipeline ([`include/game_bg.h`](include/game_bg.h), [`source/game_bg.cpp`](source/game_bg.cpp))
|
||||||
|
- **Dual-Engine Tiled Layers:** Both Main and Sub 2D engines allocate Background Layer 2 (`BgType_Text4bpp`, `BgSize_T_256x256`, `mapBase = 28`, `tileBase = 1`).
|
||||||
|
- **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 VBlank for silky smooth drift at 0% CPU cost. Drift speed accelerates dynamically as apples are consumed.
|
||||||
|
- **Dynamic VBlank 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).
|
||||||
|
- **Apple Pickup Wave:** Eating an apple triggers a glowing shockwave pulse (`bg_trigger_pulse()`) that ripples through the grid with luminance scaled by current intensity.
|
||||||
|
- **Deep-Space Backdrop Glow:** Screen backdrops illuminate in sync with intensity levels.
|
||||||
|
- **Game Over:** Palette dims to a somber dark charcoal / muted red tone with gentle drift deceleration.
|
||||||
|
|
||||||
|
### 6.4 Static Analysis & Tooling
|
||||||
- **Clang-Tidy:** Configured in [`.clang-tidy`](.clang-tidy). Enforces modern C++ guidelines, bugprone checks, readability conventions, and snake_case / CamelCase identifiers.
|
- **Clang-Tidy:** Configured in [`.clang-tidy`](.clang-tidy). Enforces modern C++ guidelines, bugprone checks, readability conventions, and snake_case / CamelCase identifiers.
|
||||||
- **Clangd:** Configured in [`.clangd`](.clangd). Removes `-mthumb-interwork` (which clang warns against) and explicitly includes devkitARM GCC 15.2.0 system headers.
|
- **Clangd:** Configured in [`.clangd`](.clangd). Removes `-mthumb-interwork` (which clang warns against) and explicitly includes devkitARM GCC 16.1.0 system headers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ const int START_LENGTH_HARD =2;
|
|||||||
const int APPLE_SCORE = 10;
|
const int APPLE_SCORE = 10;
|
||||||
const int CURSE_DURATION_TICKS = 40;
|
const int CURSE_DURATION_TICKS = 40;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
const int SPEED_MAX_NORMAL_MHZ = 13000; // 13.0 Hz (~77 ms per step)
|
||||||
|
const int SPEED_MAX_HARD_MHZ = 17000; // 17.0 Hz (~59 ms per step)
|
||||||
|
const int SPEED_APPLES_CAP = 40; // Apples consumed to reach max intensity & speed
|
||||||
|
|
||||||
// Screen size
|
// Screen size
|
||||||
const int X_MAX = 31;
|
const int X_MAX = 31;
|
||||||
const int Y_MAX = 23;
|
const int Y_MAX = 23;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#ifndef GAME_BG_H
|
||||||
|
#define GAME_BG_H
|
||||||
|
|
||||||
|
#include <nds.h>
|
||||||
|
#include "game_state.h"
|
||||||
|
|
||||||
|
struct BackgroundContext {
|
||||||
|
int bg_main_id = -1;
|
||||||
|
int bg_sub_id = -1;
|
||||||
|
|
||||||
|
// Fixed-point (8.8) scroll accumulators for smooth 60fps drift
|
||||||
|
uint16_t scroll_main_x_fp = 0;
|
||||||
|
uint16_t scroll_main_y_fp = 0;
|
||||||
|
uint16_t scroll_sub_x_fp = 0;
|
||||||
|
uint16_t scroll_sub_y_fp = 0;
|
||||||
|
|
||||||
|
// Animation frame counter & timers
|
||||||
|
uint8_t anim_phase = 0;
|
||||||
|
uint16_t anim_phase_fp = 0; // Fixed-point (8.8) phase accumulator
|
||||||
|
uint8_t pulse_timer = 0; // Triggered when an apple is eaten
|
||||||
|
|
||||||
|
// Cached state to optimize updates
|
||||||
|
bool hard_mode = false;
|
||||||
|
bool is_cursed = false;
|
||||||
|
bool game_over = false;
|
||||||
|
Scene scene = Scene::Menu;
|
||||||
|
};
|
||||||
|
|
||||||
|
void bg_setup(BackgroundContext &bg_ctx, int top_console_bg_id, int bottom_console_bg_id);
|
||||||
|
void bg_update(BackgroundContext &bg_ctx, const GameState &state, uint8_t frame);
|
||||||
|
void bg_trigger_pulse(BackgroundContext &bg_ctx);
|
||||||
|
void bg_set_scene(BackgroundContext &bg_ctx, Scene scene, bool hard_mode);
|
||||||
|
|
||||||
|
#endif // GAME_BG_H
|
||||||
@@ -13,4 +13,8 @@ void spawn_apples_rotten(GameState &state);
|
|||||||
void spawn_apples_cursed(GameState &state);
|
void spawn_apples_cursed(GameState &state);
|
||||||
void tick_status_effects(GameState &state);
|
void tick_status_effects(GameState &state);
|
||||||
|
|
||||||
|
uint8_t calculate_intensity_31(const GameState &state);
|
||||||
|
uint16_t calculate_timer_freq_mhz(const GameState &state);
|
||||||
|
uint16_t calculate_timer_reload(const GameState &state);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -15,9 +15,12 @@ struct HudCache {
|
|||||||
bool dirty = true;
|
bool dirty = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct BackgroundContext;
|
||||||
|
|
||||||
struct RenderAssets {
|
struct RenderAssets {
|
||||||
PrintConsole *topScreen = nullptr;
|
PrintConsole *topScreen = nullptr;
|
||||||
PrintConsole *bottomScreen = nullptr;
|
PrintConsole *bottomScreen = nullptr;
|
||||||
|
BackgroundContext *bg_context = nullptr;
|
||||||
|
|
||||||
u16 *gfx_segment = nullptr;
|
u16 *gfx_segment = nullptr;
|
||||||
u16 *gfx_segment_small = nullptr;
|
u16 *gfx_segment_small = nullptr;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct GameState {
|
|||||||
volatile uint16_t snakelength = 0;
|
volatile uint16_t snakelength = 0;
|
||||||
volatile uint16_t snakeDirection = 1;
|
volatile uint16_t snakeDirection = 1;
|
||||||
volatile uint16_t score = 0;
|
volatile uint16_t score = 0;
|
||||||
|
volatile uint16_t applesEaten = 0;
|
||||||
|
|
||||||
bool gameOver = false;
|
bool gameOver = false;
|
||||||
bool paused = false;
|
bool paused = false;
|
||||||
|
|||||||
@@ -0,0 +1,559 @@
|
|||||||
|
#include "game_bg.h"
|
||||||
|
#include "game_logic.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Helper functions to pack 4bpp pixels into 16-bit halfwords
|
||||||
|
constexpr uint16_t row_left(uint8_t p0, uint8_t p1, uint8_t p2, uint8_t p3) {
|
||||||
|
return static_cast<uint16_t>((static_cast<uint16_t>(p0 & 0x0FU)) |
|
||||||
|
(static_cast<uint16_t>(p1 & 0x0FU) << 4U) |
|
||||||
|
(static_cast<uint16_t>(p2 & 0x0FU) << 8U) |
|
||||||
|
(static_cast<uint16_t>(p3 & 0x0FU) << 12U));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t row_right(uint8_t p4, uint8_t p5, uint8_t p6, uint8_t p7) {
|
||||||
|
return static_cast<uint16_t>((static_cast<uint16_t>(p4 & 0x0FU)) |
|
||||||
|
(static_cast<uint16_t>(p5 & 0x0FU) << 4U) |
|
||||||
|
(static_cast<uint16_t>(p6 & 0x0FU) << 8U) |
|
||||||
|
(static_cast<uint16_t>(p7 & 0x0FU) << 12U));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t make_rgb15(uint32_t r, uint32_t g, uint32_t b) {
|
||||||
|
return static_cast<uint16_t>((r & 31U) | ((g & 31U) << 5U) |
|
||||||
|
((b & 31U) << 10U));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16 tiles, each tile is 8 rows * 2 halfwords = 16 halfwords (32 bytes per tile)
|
||||||
|
constexpr size_t TILE_COUNT = 16;
|
||||||
|
constexpr size_t WORDS_PER_TILE = 16;
|
||||||
|
uint16_t bg_tiles[TILE_COUNT * WORDS_PER_TILE];
|
||||||
|
|
||||||
|
void generate_bg_tiles() {
|
||||||
|
std::memset(bg_tiles, 0, sizeof(bg_tiles));
|
||||||
|
|
||||||
|
auto set_row = [](size_t tile_idx, size_t row_idx, uint16_t left,
|
||||||
|
uint16_t right) {
|
||||||
|
bg_tiles[tile_idx * WORDS_PER_TILE + row_idx * 2] = left;
|
||||||
|
bg_tiles[tile_idx * WORDS_PER_TILE + row_idx * 2 + 1] = right;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tile 0: Empty transparent tile (all 0)
|
||||||
|
|
||||||
|
// Tile 1: Grid intersection dot at (0,0) (color 2)
|
||||||
|
set_row(1, 0, row_left(2, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
|
||||||
|
// Tile 2: Grid intersection with faint guide ticks
|
||||||
|
set_row(2, 0, row_left(3, 1, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
set_row(2, 1, row_left(1, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
|
||||||
|
// Tile 3: Twinkling star / micro node (dim)
|
||||||
|
set_row(3, 0, row_left(2, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
set_row(3, 3, row_left(0, 0, 0, 4), row_right(0, 0, 0, 0));
|
||||||
|
|
||||||
|
// Tile 4: Twinkling star / medium cross
|
||||||
|
set_row(4, 0, row_left(2, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
set_row(4, 3, row_left(0, 0, 0, 0), row_right(4, 0, 0, 0));
|
||||||
|
set_row(4, 4, row_left(0, 0, 0, 4), row_right(5, 4, 0, 0));
|
||||||
|
set_row(4, 5, row_left(0, 0, 0, 0), row_right(4, 0, 0, 0));
|
||||||
|
|
||||||
|
// Tile 5: Twinkling star / bright diamond flare
|
||||||
|
set_row(5, 0, row_left(2, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
set_row(5, 2, row_left(0, 0, 0, 0), row_right(5, 0, 0, 0));
|
||||||
|
set_row(5, 3, row_left(0, 0, 0, 5), row_right(6, 5, 0, 0));
|
||||||
|
set_row(5, 4, row_left(0, 0, 5, 6), row_right(7, 6, 5, 0));
|
||||||
|
set_row(5, 5, row_left(0, 0, 0, 5), row_right(6, 5, 0, 0));
|
||||||
|
set_row(5, 6, row_left(0, 0, 0, 0), row_right(5, 0, 0, 0));
|
||||||
|
|
||||||
|
// Tile 6: Circuit horizontal line
|
||||||
|
set_row(6, 0, row_left(2, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
set_row(6, 4, row_left(1, 1, 1, 1), row_right(1, 1, 1, 1));
|
||||||
|
|
||||||
|
// Tile 7: Circuit vertical line
|
||||||
|
set_row(7, 0, row_left(2, 0, 0, 0), row_right(1, 0, 0, 0));
|
||||||
|
for (size_t r = 1; r < 8; ++r) {
|
||||||
|
set_row(7, r, row_left(0, 0, 0, 0), row_right(1, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile 8: Circuit corner (top to right)
|
||||||
|
set_row(8, 0, row_left(2, 0, 0, 0), row_right(1, 0, 0, 0));
|
||||||
|
for (size_t r = 1; r < 4; ++r) {
|
||||||
|
set_row(8, r, row_left(0, 0, 0, 0), row_right(1, 0, 0, 0));
|
||||||
|
}
|
||||||
|
set_row(8, 4, row_left(0, 0, 0, 0), row_right(5, 1, 1, 1));
|
||||||
|
|
||||||
|
// Tile 9: Circuit corner (left to down)
|
||||||
|
set_row(9, 0, row_left(2, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
set_row(9, 4, row_left(1, 1, 1, 1), row_right(5, 0, 0, 0));
|
||||||
|
for (size_t r = 5; r < 8; ++r) {
|
||||||
|
set_row(9, r, row_left(0, 0, 0, 0), row_right(1, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile 10: Circuit T-junction
|
||||||
|
set_row(10, 0, row_left(2, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
set_row(10, 4, row_left(1, 1, 1, 1), row_right(5, 1, 1, 1));
|
||||||
|
for (size_t r = 5; r < 8; ++r) {
|
||||||
|
set_row(10, r, row_left(0, 0, 0, 0), row_right(1, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile 11: Top-Left tech bracket
|
||||||
|
set_row(11, 0, row_left(9, 8, 8, 8), row_right(8, 8, 8, 0));
|
||||||
|
for (size_t r = 1; r < 7; ++r) {
|
||||||
|
set_row(11, r, row_left(8, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile 12: Top-Right tech bracket
|
||||||
|
set_row(12, 0, row_left(0, 8, 8, 8), row_right(8, 8, 8, 9));
|
||||||
|
for (size_t r = 1; r < 7; ++r) {
|
||||||
|
set_row(12, r, row_left(0, 0, 0, 0), row_right(0, 0, 0, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile 13: Bottom-Left tech bracket
|
||||||
|
for (size_t r = 1; r < 7; ++r) {
|
||||||
|
set_row(13, r, row_left(8, 0, 0, 0), row_right(0, 0, 0, 0));
|
||||||
|
}
|
||||||
|
set_row(13, 7, row_left(9, 8, 8, 8), row_right(8, 8, 8, 0));
|
||||||
|
|
||||||
|
// Tile 14: Bottom-Right tech bracket
|
||||||
|
for (size_t r = 1; r < 7; ++r) {
|
||||||
|
set_row(14, r, row_left(0, 0, 0, 0), row_right(0, 0, 0, 8));
|
||||||
|
}
|
||||||
|
set_row(14, 7, row_left(0, 8, 8, 8), row_right(8, 8, 8, 9));
|
||||||
|
|
||||||
|
// Tile 15: Tech border line (dashed)
|
||||||
|
set_row(15, 0, row_left(8, 8, 0, 8), row_right(8, 0, 8, 8));
|
||||||
|
set_row(15, 2, row_left(0, 1, 1, 0), row_right(0, 1, 1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 32x32 = 1024 map entries for Sub Screen (Playfield) and Main Screen (HUD/Menu)
|
||||||
|
uint16_t sub_map[1024];
|
||||||
|
uint16_t main_map[1024];
|
||||||
|
|
||||||
|
constexpr uint16_t map_entry(uint16_t tile, uint16_t pal = 1) {
|
||||||
|
return static_cast<uint16_t>((tile & 0x03FFU) |
|
||||||
|
((pal & 0x0FU) << 12U));
|
||||||
|
}
|
||||||
|
|
||||||
|
void generate_sub_map() {
|
||||||
|
for (size_t y = 0; y < 32; ++y) {
|
||||||
|
for (size_t x = 0; x < 32; ++x) {
|
||||||
|
const size_t idx = y * 32 + x;
|
||||||
|
// Default to crisp grid intersection dot
|
||||||
|
uint16_t tile = 1;
|
||||||
|
|
||||||
|
// Major coordinate marks every 4 cells
|
||||||
|
if ((x % 4 == 0) && (y % 4 == 0)) {
|
||||||
|
tile = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_map[idx] = map_entry(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curated celestial constellations & circuit clusters
|
||||||
|
struct NodePlacement {
|
||||||
|
uint8_t x, y, tile;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodePlacement nodes[] = {
|
||||||
|
// Sparkling stars
|
||||||
|
{3, 2, 4}, {7, 5, 5}, {14, 3, 3}, {19, 6, 4}, {27, 2, 5},
|
||||||
|
{5, 9, 3}, {11, 12, 5}, {22, 10, 4}, {29, 14, 3}, {2, 17, 4},
|
||||||
|
{8, 19, 5}, {16, 16, 3}, {25, 20, 5}, {13, 22, 4}, {20, 23, 3},
|
||||||
|
// Wraparound stars (rows 24..31)
|
||||||
|
{4, 26, 4}, {12, 28, 5}, {18, 25, 3}, {28, 27, 4}, {22, 30, 5},
|
||||||
|
// Circuit pathway 1
|
||||||
|
{10, 7, 8}, {11, 7, 6}, {12, 7, 9}, {12, 8, 7}, {12, 9, 10},
|
||||||
|
// Circuit pathway 2
|
||||||
|
{23, 15, 6}, {24, 15, 8}, {24, 16, 7}, {24, 17, 9}, {25, 17, 6},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &n : nodes) {
|
||||||
|
sub_map[n.y * 32 + n.x] = map_entry(n.tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void generate_main_map() {
|
||||||
|
for (size_t y = 0; y < 32; ++y) {
|
||||||
|
for (size_t x = 0; x < 32; ++x) {
|
||||||
|
const size_t idx = y * 32 + x;
|
||||||
|
uint16_t tile = 0;
|
||||||
|
|
||||||
|
// Subtle ambient stars in upper area (rows 1..15)
|
||||||
|
if (y >= 1 && y <= 15) {
|
||||||
|
if ((x * 7 + y * 13) % 23 == 0) {
|
||||||
|
tile = 3;
|
||||||
|
} else if ((x * 11 + y * 17) % 37 == 0) {
|
||||||
|
tile = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling retro horizon / cyber grid in lower area (rows 16..22 and 24..30)
|
||||||
|
if ((y >= 16 && y <= 22) || (y >= 24 && y <= 30)) {
|
||||||
|
if (y % 2 == 0) {
|
||||||
|
tile = 6; // Horizontal grid line
|
||||||
|
} else if (x % 4 == 0) {
|
||||||
|
tile = 7; // Vertical perspective trace
|
||||||
|
} else {
|
||||||
|
tile = 1; // Subtle grid dot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main_map[idx] = map_entry(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-tech terminal corner brackets (framing standard 32x24 screen)
|
||||||
|
main_map[0 * 32 + 0] = map_entry(11); // Top-Left
|
||||||
|
main_map[0 * 32 + 31] = map_entry(12); // Top-Right
|
||||||
|
main_map[23 * 32 + 0] = map_entry(13); // Bottom-Left
|
||||||
|
main_map[23 * 32 + 31] = map_entry(14); // Bottom-Right
|
||||||
|
|
||||||
|
// Top and bottom tech header lines
|
||||||
|
for (size_t x = 1; x < 31; ++x) {
|
||||||
|
main_map[0 * 32 + x] = map_entry(15);
|
||||||
|
main_map[23 * 32 + x] = map_entry(15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sine table (32 entries, values 0..31 scaled)
|
||||||
|
const uint8_t SINE_32[32] = {
|
||||||
|
16, 19, 22, 25, 27, 29, 31, 31, 31, 29, 27, 25, 22, 19, 16, 12,
|
||||||
|
9, 6, 4, 2, 0, 0, 0, 2, 4, 6, 9, 12, 16, 19, 22, 25};
|
||||||
|
|
||||||
|
// Color interpolation helper
|
||||||
|
constexpr uint16_t lerp_color(uint16_t c1, uint16_t c2, uint8_t t,
|
||||||
|
uint8_t max_t = 31) {
|
||||||
|
const int r1 = c1 & 31;
|
||||||
|
const int g1 = (c1 >> 5U) & 31;
|
||||||
|
const int b1 = (c1 >> 10U) & 31;
|
||||||
|
|
||||||
|
const int r2 = c2 & 31;
|
||||||
|
const int g2 = (c2 >> 5U) & 31;
|
||||||
|
const int b2 = (c2 >> 10U) & 31;
|
||||||
|
|
||||||
|
const int r = r1 + ((r2 - r1) * static_cast<int>(t)) / static_cast<int>(max_t);
|
||||||
|
const int g = g1 + ((g2 - g1) * static_cast<int>(t)) / static_cast<int>(max_t);
|
||||||
|
const int b = b1 + ((b2 - b1) * static_cast<int>(t)) / static_cast<int>(max_t);
|
||||||
|
|
||||||
|
return make_rgb15(static_cast<uint32_t>(r), static_cast<uint32_t>(g),
|
||||||
|
static_cast<uint32_t>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void bg_setup(BackgroundContext &bg_ctx, int top_console_bg_id,
|
||||||
|
int bottom_console_bg_id) {
|
||||||
|
// Initialize Background Layer 2 on Main Engine (Top Screen)
|
||||||
|
// mapBase 28 (offset 56KB), tileBase 1 (offset 16KB)
|
||||||
|
bg_ctx.bg_main_id = bgInit(2, BgType_Text4bpp, BgSize_T_256x256, 28, 1);
|
||||||
|
|
||||||
|
// Initialize Background Layer 2 on Sub Engine (Bottom Screen / Playfield)
|
||||||
|
bg_ctx.bg_sub_id = bgInitSub(2, BgType_Text4bpp, BgSize_T_256x256, 28, 1);
|
||||||
|
|
||||||
|
// Set Priorities:
|
||||||
|
// Main Engine: Console (Priority 0) in front of Background (Priority 3)
|
||||||
|
bgSetPriority(top_console_bg_id, 0);
|
||||||
|
bgSetPriority(bg_ctx.bg_main_id, 3);
|
||||||
|
|
||||||
|
// Sub Engine: Sprites (Priority 0) in front of Console (Priority 1) in front
|
||||||
|
// of Background (Priority 3)
|
||||||
|
bgSetPriority(bottom_console_bg_id, 1);
|
||||||
|
bgSetPriority(bg_ctx.bg_sub_id, 3);
|
||||||
|
|
||||||
|
// Generate graphics and maps
|
||||||
|
generate_bg_tiles();
|
||||||
|
generate_sub_map();
|
||||||
|
generate_main_map();
|
||||||
|
|
||||||
|
// DMA tiles to VRAM (tileBase 1)
|
||||||
|
dmaCopy(bg_tiles, bgGetGfxPtr(bg_ctx.bg_main_id), sizeof(bg_tiles));
|
||||||
|
dmaCopy(bg_tiles, bgGetGfxPtr(bg_ctx.bg_sub_id), sizeof(bg_tiles));
|
||||||
|
|
||||||
|
// DMA tilemaps to VRAM (mapBase 28)
|
||||||
|
dmaCopy(main_map, bgGetMapPtr(bg_ctx.bg_main_id), sizeof(main_map));
|
||||||
|
dmaCopy(sub_map, bgGetMapPtr(bg_ctx.bg_sub_id), sizeof(sub_map));
|
||||||
|
|
||||||
|
// Initialize scroll positions
|
||||||
|
bg_ctx.scroll_main_x_fp = 0;
|
||||||
|
bg_ctx.scroll_main_y_fp = 0;
|
||||||
|
bg_ctx.scroll_sub_x_fp = 0;
|
||||||
|
bg_ctx.scroll_sub_y_fp = 0;
|
||||||
|
|
||||||
|
// Latch initial background states
|
||||||
|
bgSetScroll(bg_ctx.bg_main_id, 0, 0);
|
||||||
|
bgSetScroll(bg_ctx.bg_sub_id, 0, 0);
|
||||||
|
bgUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void bg_trigger_pulse(BackgroundContext &bg_ctx) {
|
||||||
|
bg_ctx.pulse_timer = 16; // 16 frames of radiant energy pulse
|
||||||
|
}
|
||||||
|
|
||||||
|
void bg_set_scene(BackgroundContext &bg_ctx, Scene scene, bool hard_mode) {
|
||||||
|
bg_ctx.scene = scene;
|
||||||
|
bg_ctx.hard_mode = hard_mode;
|
||||||
|
if (scene == Scene::Menu) {
|
||||||
|
bg_ctx.anim_phase_fp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void bg_update(BackgroundContext &bg_ctx, const GameState &state,
|
||||||
|
uint8_t /*frame*/) {
|
||||||
|
const bool in_game = (state.scene == Scene::Playing);
|
||||||
|
const uint8_t intensity_31 = in_game ? calculate_intensity_31(state) : 0U;
|
||||||
|
|
||||||
|
// Animation phase with fractional accumulator: pulses faster at higher intensity
|
||||||
|
const uint16_t phase_step = static_cast<uint16_t>(
|
||||||
|
256U + ((static_cast<uint32_t>(intensity_31) * 280U) / 31U));
|
||||||
|
bg_ctx.anim_phase_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.anim_phase_fp + phase_step);
|
||||||
|
bg_ctx.anim_phase = static_cast<uint8_t>((bg_ctx.anim_phase_fp >> 8U) & 31U);
|
||||||
|
|
||||||
|
if (bg_ctx.pulse_timer > 0) {
|
||||||
|
bg_ctx.pulse_timer--;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool cursed = (state.curseTicksRemaining > 0);
|
||||||
|
const bool hard = state.hardMode;
|
||||||
|
const bool over = state.gameOver;
|
||||||
|
|
||||||
|
// 1. Hardware Scrolling Update
|
||||||
|
if (state.scene == Scene::Menu) {
|
||||||
|
// Menu: Majestic downward drift for the cyber-horizon feel
|
||||||
|
bg_ctx.scroll_main_y_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_main_y_fp + 40U); // ~0.15 px/frame
|
||||||
|
bg_ctx.scroll_main_x_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_main_x_fp + 16U); // ~0.06 px/frame
|
||||||
|
|
||||||
|
bg_ctx.scroll_sub_y_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_sub_y_fp + 32U);
|
||||||
|
bg_ctx.scroll_sub_x_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_sub_x_fp + 24U);
|
||||||
|
} else {
|
||||||
|
// In-Game Playfield:
|
||||||
|
if (over) {
|
||||||
|
// Gentle halt on game over
|
||||||
|
bg_ctx.scroll_sub_x_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_sub_x_fp + 4U);
|
||||||
|
} else if (cursed) {
|
||||||
|
// High-speed warp and glitch jitter when cursed!
|
||||||
|
const int16_t jitter =
|
||||||
|
(bg_ctx.anim_phase % 4U == 0U) ? 0x0180 : -0x0100;
|
||||||
|
const uint16_t warp_intensity = static_cast<uint16_t>(
|
||||||
|
(static_cast<uint32_t>(intensity_31) * 0x00A0U) / 31U);
|
||||||
|
bg_ctx.scroll_sub_x_fp = static_cast<uint16_t>(
|
||||||
|
bg_ctx.scroll_sub_x_fp + 0x0120U + warp_intensity +
|
||||||
|
static_cast<uint16_t>(jitter));
|
||||||
|
bg_ctx.scroll_sub_y_fp = static_cast<uint16_t>(
|
||||||
|
bg_ctx.scroll_sub_y_fp + 0x00E0U + (warp_intensity >> 1U));
|
||||||
|
} else {
|
||||||
|
// Smooth ambient drift scaling gradually with intensity
|
||||||
|
const uint16_t base_x = hard ? 24U : 16U;
|
||||||
|
const uint16_t base_y = hard ? 20U : 12U;
|
||||||
|
const uint16_t max_x = hard ? 76U : 56U;
|
||||||
|
const uint16_t max_y = hard ? 62U : 42U;
|
||||||
|
const uint16_t speed_x = static_cast<uint16_t>(
|
||||||
|
base_x + ((static_cast<uint32_t>(intensity_31) * (max_x - base_x)) / 31U));
|
||||||
|
const uint16_t speed_y = static_cast<uint16_t>(
|
||||||
|
base_y + ((static_cast<uint32_t>(intensity_31) * (max_y - base_y)) / 31U));
|
||||||
|
bg_ctx.scroll_sub_x_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_sub_x_fp + speed_x);
|
||||||
|
bg_ctx.scroll_sub_y_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_sub_y_fp + speed_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top screen HUD / horizon drift scaling with intensity
|
||||||
|
const uint16_t speed_main_y = static_cast<uint16_t>(
|
||||||
|
16U + ((static_cast<uint32_t>(intensity_31) * 24U) / 31U));
|
||||||
|
const uint16_t speed_main_x = static_cast<uint16_t>(
|
||||||
|
8U + ((static_cast<uint32_t>(intensity_31) * 12U) / 31U));
|
||||||
|
bg_ctx.scroll_main_y_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_main_y_fp + speed_main_y);
|
||||||
|
bg_ctx.scroll_main_x_fp =
|
||||||
|
static_cast<uint16_t>(bg_ctx.scroll_main_x_fp + speed_main_x);
|
||||||
|
}
|
||||||
|
|
||||||
|
bgSetScroll(bg_ctx.bg_main_id, bg_ctx.scroll_main_x_fp >> 8U,
|
||||||
|
bg_ctx.scroll_main_y_fp >> 8U);
|
||||||
|
bgSetScroll(bg_ctx.bg_sub_id, bg_ctx.scroll_sub_x_fp >> 8U,
|
||||||
|
bg_ctx.scroll_sub_y_fp >> 8U);
|
||||||
|
bgUpdate();
|
||||||
|
|
||||||
|
// 2. Dynamic Palette Animation during VBlank
|
||||||
|
// Palette Bank 1 (indices 16..31) in BG_PALETTE and BG_PALETTE_SUB
|
||||||
|
const uint8_t sine_val = SINE_32[bg_ctx.anim_phase];
|
||||||
|
const uint8_t sine_val_opposite = SINE_32[(bg_ctx.anim_phase + 16U) & 31U];
|
||||||
|
|
||||||
|
// Base theme definitions
|
||||||
|
uint16_t col_faint = 0;
|
||||||
|
uint16_t col_subtle = 0;
|
||||||
|
uint16_t col_primary = 0;
|
||||||
|
uint16_t col_star_dim = 0;
|
||||||
|
uint16_t col_star_med = 0;
|
||||||
|
uint16_t col_star_bright = 0;
|
||||||
|
uint16_t col_star_flare = 0;
|
||||||
|
uint16_t col_frame = 0;
|
||||||
|
uint16_t col_frame_accent = 0;
|
||||||
|
|
||||||
|
if (over) {
|
||||||
|
// Somber dark charcoal / muted red
|
||||||
|
col_faint = make_rgb15(3, 2, 2);
|
||||||
|
col_subtle = make_rgb15(6, 3, 3);
|
||||||
|
col_primary = make_rgb15(10, 4, 4);
|
||||||
|
col_star_dim = make_rgb15(5, 2, 2);
|
||||||
|
col_star_med = make_rgb15(8, 3, 3);
|
||||||
|
col_star_bright = make_rgb15(12, 4, 4);
|
||||||
|
col_star_flare = make_rgb15(16, 6, 6);
|
||||||
|
col_frame = make_rgb15(8, 3, 3);
|
||||||
|
col_frame_accent = make_rgb15(12, 4, 4);
|
||||||
|
} else if (cursed) {
|
||||||
|
// Radioactive neon purple / violet storm
|
||||||
|
const bool strobe = (bg_ctx.anim_phase % 6U < 3U);
|
||||||
|
col_faint = make_rgb15(8, 1, 10);
|
||||||
|
col_subtle = make_rgb15(14, 2, 18);
|
||||||
|
col_primary = strobe ? make_rgb15(24, 4, 30) : make_rgb15(18, 2, 24);
|
||||||
|
col_star_dim = make_rgb15(10, 0, 14);
|
||||||
|
col_star_med = make_rgb15(18, 0, 24);
|
||||||
|
col_star_bright = make_rgb15(26, 4, 30);
|
||||||
|
col_star_flare = make_rgb15(31, 16, 31);
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
col_faint = lerp_color(c_faint_0, c_faint_max, intensity_31);
|
||||||
|
col_subtle = lerp_color(c_subtle_0, c_subtle_max, intensity_31);
|
||||||
|
col_primary = lerp_color(c_primary_0, c_primary_max, intensity_31);
|
||||||
|
col_star_dim = lerp_color(c_star_dim_0, c_star_dim_max, intensity_31);
|
||||||
|
col_star_med = lerp_color(c_star_med_0, c_star_med_max, intensity_31);
|
||||||
|
col_star_bright = lerp_color(c_star_bright_0, c_star_bright_max, intensity_31);
|
||||||
|
col_star_flare = lerp_color(c_star_flare_0, c_star_flare_max, intensity_31);
|
||||||
|
col_frame = lerp_color(c_frame_0, c_frame_max, intensity_31);
|
||||||
|
col_frame_accent = lerp_color(c_frame_accent_0, c_frame_accent_max, intensity_31);
|
||||||
|
} else {
|
||||||
|
// Neon Cyan / Teal interpolating towards high-energy electric plasma cyan
|
||||||
|
const uint16_t c_faint_0 = make_rgb15(1, 4, 7);
|
||||||
|
const uint16_t c_subtle_0 = make_rgb15(2, 9, 14);
|
||||||
|
const uint16_t c_primary_0 = make_rgb15(4, 16, 22);
|
||||||
|
const uint16_t c_star_dim_0 = make_rgb15(2, 6, 12);
|
||||||
|
const uint16_t c_star_med_0 = make_rgb15(5, 14, 22);
|
||||||
|
const uint16_t c_star_bright_0 = make_rgb15(10, 24, 30);
|
||||||
|
const uint16_t c_star_flare_0 = make_rgb15(22, 30, 31);
|
||||||
|
const uint16_t c_frame_0 = make_rgb15(3, 12, 18);
|
||||||
|
const uint16_t c_frame_accent_0 = make_rgb15(6, 22, 28);
|
||||||
|
|
||||||
|
const uint16_t c_faint_max = make_rgb15(2, 7, 12);
|
||||||
|
const uint16_t c_subtle_max = make_rgb15(4, 15, 24);
|
||||||
|
const uint16_t c_primary_max = make_rgb15(8, 25, 31);
|
||||||
|
const uint16_t c_star_dim_max = make_rgb15(4, 12, 22);
|
||||||
|
const uint16_t c_star_med_max = make_rgb15(10, 22, 30);
|
||||||
|
const uint16_t c_star_bright_max = make_rgb15(18, 29, 31);
|
||||||
|
const uint16_t c_star_flare_max = make_rgb15(31, 31, 31);
|
||||||
|
const uint16_t c_frame_max = make_rgb15(6, 20, 28);
|
||||||
|
const uint16_t c_frame_accent_max = make_rgb15(12, 30, 31);
|
||||||
|
|
||||||
|
col_faint = lerp_color(c_faint_0, c_faint_max, intensity_31);
|
||||||
|
col_subtle = lerp_color(c_subtle_0, c_subtle_max, intensity_31);
|
||||||
|
col_primary = lerp_color(c_primary_0, c_primary_max, intensity_31);
|
||||||
|
col_star_dim = lerp_color(c_star_dim_0, c_star_dim_max, intensity_31);
|
||||||
|
col_star_med = lerp_color(c_star_med_0, c_star_med_max, intensity_31);
|
||||||
|
col_star_bright = lerp_color(c_star_bright_0, c_star_bright_max, intensity_31);
|
||||||
|
col_star_flare = lerp_color(c_star_flare_0, c_star_flare_max, intensity_31);
|
||||||
|
col_frame = lerp_color(c_frame_0, c_frame_max, intensity_31);
|
||||||
|
col_frame_accent = lerp_color(c_frame_accent_0, c_frame_accent_max, intensity_31);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
} else if (over) {
|
||||||
|
BG_PALETTE_SUB[0] = make_rgb15(3, 2, 2);
|
||||||
|
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);
|
||||||
|
const uint16_t anim_med = lerp_color(col_star_med, col_star_bright, sine_val_opposite);
|
||||||
|
const uint16_t anim_bright = lerp_color(col_star_bright, col_star_flare, sine_val);
|
||||||
|
|
||||||
|
// Apple pickup wave pulse: flares bright energy through grid and stars with extra intensity
|
||||||
|
uint16_t pulse_add = 0;
|
||||||
|
if (bg_ctx.pulse_timer > 0) {
|
||||||
|
const uint8_t pulse_val = bg_ctx.pulse_timer;
|
||||||
|
const uint32_t boost = (static_cast<uint32_t>(intensity_31) * 3U) / 31U;
|
||||||
|
pulse_add = (pulse_val > 10U)
|
||||||
|
? make_rgb15(6U + boost, 12U + boost, 14U + boost)
|
||||||
|
: make_rgb15(2U + (boost >> 1U), 5U + (boost >> 1U), 6U + (boost >> 1U));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Palette Bank 1 (indices 16..31) for both engines
|
||||||
|
// Sub Screen (Playfield):
|
||||||
|
BG_PALETTE_SUB[16] = 0; // Transparent (reveals backdrop color)
|
||||||
|
BG_PALETTE_SUB[17] = col_faint;
|
||||||
|
BG_PALETTE_SUB[18] = static_cast<uint16_t>(col_subtle + pulse_add);
|
||||||
|
BG_PALETTE_SUB[19] = static_cast<uint16_t>(col_primary + pulse_add);
|
||||||
|
BG_PALETTE_SUB[20] = anim_dim;
|
||||||
|
BG_PALETTE_SUB[21] = anim_med;
|
||||||
|
BG_PALETTE_SUB[22] = static_cast<uint16_t>(anim_bright + pulse_add);
|
||||||
|
BG_PALETTE_SUB[23] = col_star_flare;
|
||||||
|
BG_PALETTE_SUB[24] = col_frame;
|
||||||
|
BG_PALETTE_SUB[25] = col_frame_accent;
|
||||||
|
BG_PALETTE_SUB[26] = col_faint;
|
||||||
|
BG_PALETTE_SUB[27] = col_subtle;
|
||||||
|
BG_PALETTE_SUB[28] = col_primary;
|
||||||
|
BG_PALETTE_SUB[29] = anim_med;
|
||||||
|
BG_PALETTE_SUB[30] = col_frame_accent;
|
||||||
|
BG_PALETTE_SUB[31] = make_rgb15(31, 31, 31);
|
||||||
|
|
||||||
|
// Main Screen (Top Screen HUD & Menu):
|
||||||
|
BG_PALETTE[16] = 0; // Transparent
|
||||||
|
BG_PALETTE[17] = col_faint;
|
||||||
|
BG_PALETTE[18] = col_subtle;
|
||||||
|
BG_PALETTE[19] = col_primary;
|
||||||
|
BG_PALETTE[20] = anim_dim;
|
||||||
|
BG_PALETTE[21] = anim_med;
|
||||||
|
BG_PALETTE[22] = anim_bright;
|
||||||
|
BG_PALETTE[23] = col_star_flare;
|
||||||
|
BG_PALETTE[24] = col_frame;
|
||||||
|
BG_PALETTE[25] = col_frame_accent;
|
||||||
|
BG_PALETTE[26] = col_faint;
|
||||||
|
BG_PALETTE[27] = col_subtle;
|
||||||
|
BG_PALETTE[28] = col_primary;
|
||||||
|
BG_PALETTE[29] = anim_med;
|
||||||
|
BG_PALETTE[30] = col_frame_accent;
|
||||||
|
BG_PALETTE[31] = make_rgb15(31, 31, 31);
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ bool handle_input_frame(InputContext &ctx) {
|
|||||||
if (*ctx.internals) {
|
if (*ctx.internals) {
|
||||||
setBackdropColor(RGB15(0, 0, 7));
|
setBackdropColor(RGB15(0, 0, 7));
|
||||||
} else {
|
} else {
|
||||||
setBackdropColor(RGB15(0, 0, 0));
|
setBackdropColor(ctx.state->hardMode ? RGB15(6, 2, 2) : RGB15(2, 3, 6));
|
||||||
consoleSelect(ctx.topScreen);
|
consoleSelect(ctx.topScreen);
|
||||||
consoleClear();
|
consoleClear();
|
||||||
invalidate_hud(*ctx.renderAssets);
|
invalidate_hud(*ctx.renderAssets);
|
||||||
@@ -65,6 +65,8 @@ bool handle_input_frame(InputContext &ctx) {
|
|||||||
if ((ctx.state->paused || ctx.state->gameOver) && (keys != 0U) && ((keys & KEY_B) != 0U)) {
|
if ((ctx.state->paused || ctx.state->gameOver) && (keys != 0U) && ((keys & KEY_B) != 0U)) {
|
||||||
ctx.state->scene = Scene::Menu;
|
ctx.state->scene = Scene::Menu;
|
||||||
ctx.state->paused = false;
|
ctx.state->paused = false;
|
||||||
|
setBackdropColor(RGB15(2, 2, 6));
|
||||||
|
setBackdropColorSub(RGB15(2, 2, 6));
|
||||||
draw_menu(*ctx.state, *ctx.renderAssets, audio_is_music_enabled(*ctx.audio));
|
draw_menu(*ctx.state, *ctx.renderAssets, audio_is_music_enabled(*ctx.audio));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include <nds.h>
|
||||||
#include <consts.h>
|
#include <consts.h>
|
||||||
|
|
||||||
void update_snake(GameState &state) {
|
void update_snake(GameState &state) {
|
||||||
@@ -68,6 +69,7 @@ bool check_collision_apples(GameState &state) {
|
|||||||
|
|
||||||
state.score += APPLE_SCORE;
|
state.score += APPLE_SCORE;
|
||||||
state.snakelength++;
|
state.snakelength++;
|
||||||
|
state.applesEaten++;
|
||||||
state.apples.erase(it);
|
state.apples.erase(it);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -140,3 +142,33 @@ void tick_status_effects(GameState &state) {
|
|||||||
state.curseTicksRemaining--;
|
state.curseTicksRemaining--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t calculate_intensity_31(const GameState &state) {
|
||||||
|
const uint16_t current_apples = state.applesEaten;
|
||||||
|
const uint16_t apples =
|
||||||
|
std::min<uint16_t>(current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
|
||||||
|
return static_cast<uint8_t>((static_cast<uint32_t>(apples) * 31U) /
|
||||||
|
static_cast<uint32_t>(SPEED_APPLES_CAP));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t calculate_timer_freq_mhz(const GameState &state) {
|
||||||
|
const uint16_t base_mhz =
|
||||||
|
state.hardMode ? SPEED_BASE_HARD_MHZ : SPEED_BASE_NORMAL_MHZ;
|
||||||
|
const uint16_t max_mhz =
|
||||||
|
state.hardMode ? SPEED_MAX_HARD_MHZ : SPEED_MAX_NORMAL_MHZ;
|
||||||
|
const uint16_t current_apples = state.applesEaten;
|
||||||
|
const uint16_t apples =
|
||||||
|
std::min<uint16_t>(current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
|
||||||
|
const uint32_t diff = static_cast<uint32_t>(max_mhz - base_mhz);
|
||||||
|
return static_cast<uint16_t>(
|
||||||
|
base_mhz + ((static_cast<uint32_t>(apples) * diff) /
|
||||||
|
static_cast<uint32_t>(SPEED_APPLES_CAP)));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t calculate_timer_reload(const GameState &state) {
|
||||||
|
const uint32_t freq_mhz = calculate_timer_freq_mhz(state);
|
||||||
|
// Timer clock with ClockDivider_1024 is BUS_CLOCK >> 10 (32728 Hz)
|
||||||
|
const uint32_t timer_clock_mhz = (BUS_CLOCK >> 10U) * 1000U;
|
||||||
|
const uint32_t period_ticks = timer_clock_mhz / freq_mhz;
|
||||||
|
return static_cast<uint16_t>(-static_cast<int32_t>(period_ticks));
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "game_render.h"
|
#include "game_render.h"
|
||||||
|
#include "game_logic.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -188,6 +189,13 @@ void draw_stats(const GameState &state, RenderAssets &assets, bool internals,
|
|||||||
iprintf("\x1b[4;0Hx=%i\ny=%i", state.snake.front()[0],
|
iprintf("\x1b[4;0Hx=%i\ny=%i", state.snake.front()[0],
|
||||||
state.snake.front()[1]);
|
state.snake.front()[1]);
|
||||||
}
|
}
|
||||||
|
const uint16_t freq_mhz = calculate_timer_freq_mhz(state);
|
||||||
|
const uint8_t intensity = calculate_intensity_31(state);
|
||||||
|
iprintf("\x1b[6;0HApples: %i ", state.applesEaten);
|
||||||
|
iprintf("\x1b[7;0HSpeed: %u.%01u Hz (%u%%) ",
|
||||||
|
static_cast<unsigned int>(freq_mhz / 1000U),
|
||||||
|
static_cast<unsigned int>((freq_mhz % 1000U) / 100U),
|
||||||
|
static_cast<unsigned int>((static_cast<uint32_t>(intensity) * 100U) / 31U));
|
||||||
}
|
}
|
||||||
|
|
||||||
HudCache &cache = assets.hudCache;
|
HudCache &cache = assets.hudCache;
|
||||||
@@ -255,6 +263,9 @@ void draw_menu(const GameState &state, RenderAssets &assets,
|
|||||||
|
|
||||||
consoleSelect(assets.bottomScreen);
|
consoleSelect(assets.bottomScreen);
|
||||||
consoleClear();
|
consoleClear();
|
||||||
|
iprintf("\x1b[10;5H* SNAKE WITH A TWIST *");
|
||||||
|
iprintf("\x1b[12;8Hby Phillip Kuehne");
|
||||||
|
iprintf("\x1b[18;4HPress START or A to begin");
|
||||||
|
|
||||||
consoleSelect(assets.topScreen);
|
consoleSelect(assets.topScreen);
|
||||||
consoleClear();
|
consoleClear();
|
||||||
|
|||||||
+18
-8
@@ -8,6 +8,7 @@
|
|||||||
#include <game_render.h>
|
#include <game_render.h>
|
||||||
#include <game_audio.h>
|
#include <game_audio.h>
|
||||||
#include <game_input.h>
|
#include <game_input.h>
|
||||||
|
#include <game_bg.h>
|
||||||
|
|
||||||
#include "tilemap.h"
|
#include "tilemap.h"
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ volatile uint16_t &snakelength = gameState.snakelength;
|
|||||||
***/
|
***/
|
||||||
volatile uint16_t &snakeDirection = gameState.snakeDirection;
|
volatile uint16_t &snakeDirection = gameState.snakeDirection;
|
||||||
volatile uint16_t &score = gameState.score;
|
volatile uint16_t &score = gameState.score;
|
||||||
|
volatile uint16_t &applesEaten = gameState.applesEaten;
|
||||||
|
|
||||||
bool &gameOver = gameState.gameOver;
|
bool &gameOver = gameState.gameOver;
|
||||||
bool &paused = gameState.paused;
|
bool &paused = gameState.paused;
|
||||||
@@ -46,6 +48,7 @@ int sound;
|
|||||||
AudioContext audioContext;
|
AudioContext audioContext;
|
||||||
|
|
||||||
RenderAssets renderAssets;
|
RenderAssets renderAssets;
|
||||||
|
BackgroundContext bgContext;
|
||||||
|
|
||||||
const void * get_sprite_pointer(uint16_t spriteIndex) {
|
const void * get_sprite_pointer(uint16_t spriteIndex) {
|
||||||
return &tilemapTiles[spriteIndex*16];
|
return &tilemapTiles[spriteIndex*16];
|
||||||
@@ -64,8 +67,11 @@ void setup() {
|
|||||||
consoleInit(&bottomScreen, 3,BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0, false, true);
|
consoleInit(&bottomScreen, 3,BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0, false, true);
|
||||||
oamInit(&oamSub, SpriteMapping_1D_32, false);
|
oamInit(&oamSub, SpriteMapping_1D_32, false);
|
||||||
|
|
||||||
|
bg_setup(bgContext, topScreen.bgId, bottomScreen.bgId);
|
||||||
|
|
||||||
renderAssets.topScreen = &topScreen;
|
renderAssets.topScreen = &topScreen;
|
||||||
renderAssets.bottomScreen = &bottomScreen;
|
renderAssets.bottomScreen = &bottomScreen;
|
||||||
|
renderAssets.bg_context = &bgContext;
|
||||||
|
|
||||||
renderAssets.gfx_segment = 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_segment_small = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
|
||||||
@@ -127,6 +133,7 @@ void setup() {
|
|||||||
}
|
}
|
||||||
void handle_vblank() {
|
void handle_vblank() {
|
||||||
frame++;
|
frame++;
|
||||||
|
bg_update(bgContext, gameState, frame);
|
||||||
if (gameState.scene == Scene::Playing) {
|
if (gameState.scene == Scene::Playing) {
|
||||||
consoleSelect(&topScreen);
|
consoleSelect(&topScreen);
|
||||||
draw_stats(gameState, renderAssets, internals, audio_is_music_enabled(audioContext), frame);
|
draw_stats(gameState, renderAssets, internals, audio_is_music_enabled(audioContext), frame);
|
||||||
@@ -151,6 +158,8 @@ void do_tick() {
|
|||||||
check_collision_self(gameState);
|
check_collision_self(gameState);
|
||||||
if (check_collision_apples(gameState)) {
|
if (check_collision_apples(gameState)) {
|
||||||
audio_play_pickup(audioContext);
|
audio_play_pickup(audioContext);
|
||||||
|
bg_trigger_pulse(bgContext);
|
||||||
|
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
|
||||||
}
|
}
|
||||||
if (check_collision_apples_rotten(gameState)) {
|
if (check_collision_apples_rotten(gameState)) {
|
||||||
audio_play_boom(audioContext);
|
audio_play_boom(audioContext);
|
||||||
@@ -173,10 +182,11 @@ void do_tick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void init_game() {
|
void init_game() {
|
||||||
|
applesEaten = 0;
|
||||||
if (hardMode) {
|
if (hardMode) {
|
||||||
timerStart(0, ClockDivider_1024, static_cast<uint16_t>(TIMER_FREQ_1024(9)), do_tick);
|
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
|
||||||
setBackdropColorSub(RGB15(10,5,5));
|
setBackdropColorSub(RGB15(8, 3, 3));
|
||||||
|
setBackdropColor(RGB15(6, 2, 2));
|
||||||
score = 0;
|
score = 0;
|
||||||
snakelength = START_LENGTH_HARD;
|
snakelength = START_LENGTH_HARD;
|
||||||
snake = { {2,1,1}, {1,1,1} };
|
snake = { {2,1,1}, {1,1,1} };
|
||||||
@@ -197,8 +207,9 @@ void init_game() {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
timerStart(0, ClockDivider_1024, static_cast<uint16_t>(TIMER_FREQ_1024(5)), do_tick);
|
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
|
||||||
setBackdropColorSub(RGB15(5,5,5));
|
setBackdropColorSub(RGB15(3, 4, 7));
|
||||||
|
setBackdropColor(RGB15(2, 3, 6));
|
||||||
score = 0;
|
score = 0;
|
||||||
snakelength = START_LENGTH;
|
snakelength = START_LENGTH;
|
||||||
snake = { {4,1,1}, {3,1,1}, {2,1,1}, {1,1,1} };
|
snake = { {4,1,1}, {3,1,1}, {2,1,1}, {1,1,1} };
|
||||||
@@ -215,9 +226,6 @@ void init_game() {
|
|||||||
oamClear(&oamSub,0,128);
|
oamClear(&oamSub,0,128);
|
||||||
audio_start_music(audioContext);
|
audio_start_music(audioContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
@@ -226,6 +234,8 @@ int main(void) {
|
|||||||
irqSet(IRQ_VBLANK, handle_vblank);
|
irqSet(IRQ_VBLANK, handle_vblank);
|
||||||
|
|
||||||
setup();
|
setup();
|
||||||
|
setBackdropColor(RGB15(2, 2, 6));
|
||||||
|
setBackdropColorSub(RGB15(2, 2, 6));
|
||||||
draw_menu(gameState, renderAssets, audio_is_music_enabled(audioContext));
|
draw_menu(gameState, renderAssets, audio_is_music_enabled(audioContext));
|
||||||
|
|
||||||
InputContext inputContext;
|
InputContext inputContext;
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ cd "$ROOT_DIR" || exit 1
|
|||||||
|
|
||||||
EXTRA_ARGS=(
|
EXTRA_ARGS=(
|
||||||
-extra-arg=-isystem
|
-extra-arg=-isystem
|
||||||
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include/c++/15.2.0
|
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include/c++/16.1.0
|
||||||
-extra-arg=-isystem
|
-extra-arg=-isystem
|
||||||
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include/c++/15.2.0/arm-none-eabi
|
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include/c++/16.1.0/arm-none-eabi
|
||||||
-extra-arg=-isystem
|
-extra-arg=-isystem
|
||||||
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include/c++/15.2.0/backward
|
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include/c++/16.1.0/backward
|
||||||
-extra-arg=-isystem
|
-extra-arg=-isystem
|
||||||
-extra-arg=/opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/15.2.0/include
|
-extra-arg=/opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/16.1.0/include
|
||||||
-extra-arg=-isystem
|
-extra-arg=-isystem
|
||||||
-extra-arg=/opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/15.2.0/include-fixed
|
-extra-arg=/opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/16.1.0/include-fixed
|
||||||
-extra-arg=-isystem
|
-extra-arg=-isystem
|
||||||
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include
|
-extra-arg=/opt/devkitpro/devkitARM/arm-none-eabi/include
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user