feat: add finite lifetimes and despawn mechanics for rotten and cursed apples
This commit is contained in:
1 parent
9a3a0bd9fd
commit
adda5db2f4
8 files changed
+159
-82
No files matched your search
@@ -96,8 +96,8 @@ The game operates in one of two scenes ([`Scene`](include/game_state.h#L9-L12)):
|
|||||||
### 3.3 Game Elements & Apples
|
### 3.3 Game Elements & Apples
|
||||||
- **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.
|
- **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.
|
- **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.
|
- **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).
|
- **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.
|
||||||
- **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 & Progressive Speed Mechanics
|
### 3.4 Game Modes & Progressive Speed Mechanics
|
||||||
@@ -226,6 +226,10 @@ The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/ta
|
|||||||
- Be mindful of OAM hardware limitations (Sub engine supports up to 128 sprites; current game allocates sprite slots per apple and snake segment).
|
- Be mindful of OAM hardware limitations (Sub engine supports up to 128 sprites; current game allocates sprite slots per apple and snake segment).
|
||||||
2. **Interrupt & Concurrency Safety:**
|
2. **Interrupt & Concurrency Safety:**
|
||||||
- Variables accessed across both the main loop, timer ticks, and VBlank IRQ (such as `ticks`, `frame`, `snakelength`, `snakeDirection`, `internals`) must maintain appropriate `volatile` qualifiers or thread-safe synchronization.
|
- Variables accessed across both the main loop, timer ticks, and VBlank IRQ (such as `ticks`, `frame`, `snakelength`, `snakeDirection`, `internals`) must maintain appropriate `volatile` qualifiers or thread-safe synchronization.
|
||||||
|
- **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:
|
||||||
|
- **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:**
|
3. **Hardware Registers & Memory Banking:**
|
||||||
- Graphic tiles and palettes must be transferred using DMA (`dmaCopy`) or direct memory copies into allocated VRAM / OAM memory banks.
|
- 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.
|
- Do not modify VRAM allocation configurations without ensuring compatibility with console text generators and OAM sub-engines.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
- **Classic Gameplay with a Twist:**
|
- **Classic Gameplay with a Twist:**
|
||||||
- Classic grid-based snake action wrapped on a 32×24 playfield with edge-wrapping.
|
- 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.
|
- **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!
|
- **Rotten Apples:** Shrink snake length by 1; shrinking below length 2 results in Game Over! Despawn after 80 ticks with a flashing warning animation.
|
||||||
- **Cursed Apples:** Invert steering controls (Up ↔ Down, Left ↔ Right) for 40 ticks, requiring quick reflexes and adapted steering.
|
- **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:**
|
- **Dual-Screen Layout:**
|
||||||
- **Top Screen:** Real-time scoreboard, status alerts (*"CONTROLS INVERTED!"*), pause overlay, and optional debug stats.
|
- **Top Screen:** Real-time scoreboard, status alerts (*"CONTROLS INVERTED!"*), pause overlay, and optional debug stats.
|
||||||
- **Bottom Screen:** Smooth 2D hardware sprite rendering using DS Sub-engine OAM.
|
- **Bottom Screen:** Smooth 2D hardware sprite rendering using DS Sub-engine OAM.
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ 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;
|
||||||
|
|
||||||
|
// 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
|
// Cursed apple palette indices in SPRITE_PALETTE_SUB
|
||||||
const uint16_t CURSE_PAL_OUTLINE = 16;
|
const uint16_t CURSE_PAL_OUTLINE = 16;
|
||||||
const uint16_t CURSE_PAL_FILL = 17;
|
const uint16_t CURSE_PAL_FILL = 17;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ void spawn_apples(GameState &state);
|
|||||||
void spawn_apples_rotten(GameState &state);
|
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);
|
||||||
|
void tick_evil_items(GameState &state);
|
||||||
|
|
||||||
uint8_t calculate_intensity_31(const GameState &state);
|
uint8_t calculate_intensity_31(const GameState &state);
|
||||||
uint16_t calculate_timer_freq_mhz(const GameState &state);
|
uint16_t calculate_timer_freq_mhz(const GameState &state);
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ struct Barrier {
|
|||||||
uint16_t framesLeft = 0; // Frames remaining in the current phase
|
uint16_t framesLeft = 0; // Frames remaining in the current phase
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Evil item (rotten apple or cursed apple) with position and lifetime in game ticks
|
||||||
|
struct EvilItem {
|
||||||
|
int8_t x = 0;
|
||||||
|
int8_t y = 0;
|
||||||
|
uint16_t ticksRemaining = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct GameState {
|
struct GameState {
|
||||||
volatile uint16_t ticks = 0;
|
volatile uint16_t ticks = 0;
|
||||||
volatile uint16_t snakelength = 0;
|
volatile uint16_t snakelength = 0;
|
||||||
@@ -55,8 +62,8 @@ struct GameState {
|
|||||||
|
|
||||||
std::deque<std::array<int, 3>> snake;
|
std::deque<std::array<int, 3>> snake;
|
||||||
std::vector<std::array<int, 2>> apples;
|
std::vector<std::array<int, 2>> apples;
|
||||||
std::vector<std::array<int, 2>> bad_apples;
|
std::vector<EvilItem> bad_apples;
|
||||||
std::vector<std::array<int, 2>> cursed_apples;
|
std::vector<EvilItem> cursed_apples;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+88
-33
@@ -3,8 +3,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
#include <nds.h>
|
|
||||||
#include <consts.h>
|
#include <consts.h>
|
||||||
|
#include <nds.h>
|
||||||
|
|
||||||
void update_snake(GameState &state) {
|
void update_snake(GameState &state) {
|
||||||
std::array<int, 3> headpos = state.snake.front();
|
std::array<int, 3> headpos = state.snake.front();
|
||||||
@@ -79,8 +79,8 @@ bool check_collision_apples_rotten(GameState &state) {
|
|||||||
|
|
||||||
auto it =
|
auto it =
|
||||||
std::find_if(state.bad_apples.begin(), state.bad_apples.end(),
|
std::find_if(state.bad_apples.begin(), state.bad_apples.end(),
|
||||||
[&](const std::array<int, 2> &apple) {
|
[&](const EvilItem &apple) {
|
||||||
return apple[0] == headpos[0] && apple[1] == headpos[1];
|
return apple.x == headpos[0] && apple.y == headpos[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (it == state.bad_apples.end()) {
|
if (it == state.bad_apples.end()) {
|
||||||
@@ -100,8 +100,8 @@ bool check_collision_apples_cursed(GameState &state) {
|
|||||||
|
|
||||||
auto it =
|
auto it =
|
||||||
std::find_if(state.cursed_apples.begin(), state.cursed_apples.end(),
|
std::find_if(state.cursed_apples.begin(), state.cursed_apples.end(),
|
||||||
[&](const std::array<int, 2> &apple) {
|
[&](const EvilItem &apple) {
|
||||||
return apple[0] == headpos[0] && apple[1] == headpos[1];
|
return apple.x == headpos[0] && apple.y == headpos[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (it == state.cursed_apples.end()) {
|
if (it == state.cursed_apples.end()) {
|
||||||
@@ -127,19 +127,64 @@ void spawn_apples(GameState &state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void spawn_apples_rotten(GameState &state) {
|
void spawn_apples_rotten(GameState &state) {
|
||||||
if ((state.ticks % 10 == 0) && state.hardMode && ((rand() % 10) < 10) &&
|
if ((state.ticks % 10 == 0) && state.hardMode &&
|
||||||
(state.bad_apples.size() < 20)) {
|
(state.bad_apples.size() < 20)) {
|
||||||
std::array<int, 2> badApple = {rand() % (X_MAX + 1), rand() % (Y_MAX + 1)};
|
int x = rand() % (X_MAX + 1);
|
||||||
state.bad_apples.push_back(badApple);
|
int y = rand() % (Y_MAX + 1);
|
||||||
|
for (int attempt = 0; attempt < 16 && (is_barrier_cell(state, x, y, true) ||
|
||||||
|
(!state.snake.empty() &&
|
||||||
|
state.snake.front()[0] == x &&
|
||||||
|
state.snake.front()[1] == y));
|
||||||
|
attempt++) {
|
||||||
|
x = rand() % (X_MAX + 1);
|
||||||
|
y = rand() % (Y_MAX + 1);
|
||||||
|
}
|
||||||
|
state.bad_apples.push_back({static_cast<int8_t>(x), static_cast<int8_t>(y),
|
||||||
|
BAD_APPLE_LIFETIME_TICKS});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void spawn_apples_cursed(GameState &state) {
|
void spawn_apples_cursed(GameState &state) {
|
||||||
if (state.cursed_apples.empty() && (state.ticks % 20 == 0) &&
|
if (state.cursed_apples.empty() && (state.ticks % 20 == 0) &&
|
||||||
((rand() % 5) == 0)) {
|
((rand() % 5) == 0)) {
|
||||||
std::array<int, 2> cursedApple = {rand() % (X_MAX + 1),
|
int x = rand() % (X_MAX + 1);
|
||||||
rand() % (Y_MAX + 1)};
|
int y = rand() % (Y_MAX + 1);
|
||||||
state.cursed_apples.push_back(cursedApple);
|
for (int attempt = 0; attempt < 16 && (is_barrier_cell(state, x, y, true) ||
|
||||||
|
(!state.snake.empty() &&
|
||||||
|
state.snake.front()[0] == x &&
|
||||||
|
state.snake.front()[1] == y));
|
||||||
|
attempt++) {
|
||||||
|
x = rand() % (X_MAX + 1);
|
||||||
|
y = rand() % (Y_MAX + 1);
|
||||||
|
}
|
||||||
|
state.cursed_apples.push_back({static_cast<int8_t>(x),
|
||||||
|
static_cast<int8_t>(y),
|
||||||
|
CURSED_APPLE_LIFETIME_TICKS});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick_evil_items(GameState &state) {
|
||||||
|
for (auto it = state.bad_apples.begin(); it != state.bad_apples.end();) {
|
||||||
|
if (it->ticksRemaining > 0) {
|
||||||
|
it->ticksRemaining--;
|
||||||
|
}
|
||||||
|
if (it->ticksRemaining == 0) {
|
||||||
|
it = state.bad_apples.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = state.cursed_apples.begin();
|
||||||
|
it != state.cursed_apples.end();) {
|
||||||
|
if (it->ticksRemaining > 0) {
|
||||||
|
it->ticksRemaining--;
|
||||||
|
}
|
||||||
|
if (it->ticksRemaining == 0) {
|
||||||
|
it = state.cursed_apples.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,8 +196,8 @@ void tick_status_effects(GameState &state) {
|
|||||||
|
|
||||||
uint8_t calculate_intensity_31(const GameState &state) {
|
uint8_t calculate_intensity_31(const GameState &state) {
|
||||||
const uint16_t current_apples = state.applesEaten;
|
const uint16_t current_apples = state.applesEaten;
|
||||||
const uint16_t apples =
|
const uint16_t apples = std::min<uint16_t>(
|
||||||
std::min<uint16_t>(current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
|
current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
|
||||||
return static_cast<uint8_t>((static_cast<uint32_t>(apples) * 31U) /
|
return static_cast<uint8_t>((static_cast<uint32_t>(apples) * 31U) /
|
||||||
static_cast<uint32_t>(SPEED_APPLES_CAP));
|
static_cast<uint32_t>(SPEED_APPLES_CAP));
|
||||||
}
|
}
|
||||||
@@ -163,12 +208,12 @@ uint16_t calculate_timer_freq_mhz(const GameState &state) {
|
|||||||
const uint16_t max_mhz =
|
const uint16_t max_mhz =
|
||||||
state.hardMode ? SPEED_MAX_HARD_MHZ : SPEED_MAX_NORMAL_MHZ;
|
state.hardMode ? SPEED_MAX_HARD_MHZ : SPEED_MAX_NORMAL_MHZ;
|
||||||
const uint16_t current_apples = state.applesEaten;
|
const uint16_t current_apples = state.applesEaten;
|
||||||
const uint16_t apples =
|
const uint16_t apples = std::min<uint16_t>(
|
||||||
std::min<uint16_t>(current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
|
current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
|
||||||
const uint32_t diff = static_cast<uint32_t>(max_mhz - base_mhz);
|
const uint32_t diff = static_cast<uint32_t>(max_mhz - base_mhz);
|
||||||
return static_cast<uint16_t>(
|
return static_cast<uint16_t>(base_mhz +
|
||||||
base_mhz + ((static_cast<uint32_t>(apples) * diff) /
|
((static_cast<uint32_t>(apples) * diff) /
|
||||||
static_cast<uint32_t>(SPEED_APPLES_CAP)));
|
static_cast<uint32_t>(SPEED_APPLES_CAP)));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t calculate_timer_reload(const GameState &state) {
|
uint16_t calculate_timer_reload(const GameState &state) {
|
||||||
@@ -208,11 +253,18 @@ bool cell_in_list(const std::vector<std::array<int, 2>> &list, int x, int y) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool cell_in_list(const std::vector<EvilItem> &list, int x, int y) {
|
||||||
|
return std::any_of(list.begin(), list.end(), [&](const EvilItem &item) {
|
||||||
|
return item.x == x && item.y == y;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool cell_blocked_for_barrier(const GameState &state, int x, int y) {
|
bool cell_blocked_for_barrier(const GameState &state, int x, int y) {
|
||||||
if (is_barrier_cell(state, x, y, true)) {
|
if (is_barrier_cell(state, x, y, true)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cell_in_list(state.apples, x, y) || cell_in_list(state.bad_apples, x, y) ||
|
if (cell_in_list(state.apples, x, y) ||
|
||||||
|
cell_in_list(state.bad_apples, x, y) ||
|
||||||
cell_in_list(state.cursed_apples, x, y)) {
|
cell_in_list(state.cursed_apples, x, y)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -311,7 +363,8 @@ void tick_barriers_frame(GameState &state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.barrierSpawnCooldown > 0) {
|
if (state.barrierSpawnCooldown > 0) {
|
||||||
state.barrierSpawnCooldown = static_cast<uint16_t>(state.barrierSpawnCooldown - 1U);
|
state.barrierSpawnCooldown =
|
||||||
|
static_cast<uint16_t>(state.barrierSpawnCooldown - 1U);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Barrier &barrier : state.barriers) {
|
for (Barrier &barrier : state.barriers) {
|
||||||
@@ -337,9 +390,9 @@ void spawn_barriers(GameState &state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int in_use = static_cast<int>(
|
const int in_use = static_cast<int>(std::count_if(
|
||||||
std::count_if(state.barriers.begin(), state.barriers.end(),
|
state.barriers.begin(), state.barriers.end(),
|
||||||
[](const Barrier &b) { return b.phase != BarrierPhase::Inactive; }));
|
[](const Barrier &b) { return b.phase != BarrierPhase::Inactive; }));
|
||||||
const int max_allowed = max_concurrent_barriers(state);
|
const int max_allowed = max_concurrent_barriers(state);
|
||||||
const int available = max_allowed - in_use;
|
const int available = max_allowed - in_use;
|
||||||
if (available <= 0) {
|
if (available <= 0) {
|
||||||
@@ -359,9 +412,9 @@ void spawn_barriers(GameState &state) {
|
|||||||
const int length_range = current_max - BARRIER_MIN_LENGTH + 1;
|
const int length_range = current_max - BARRIER_MIN_LENGTH + 1;
|
||||||
|
|
||||||
for (int spawn_idx = 0; spawn_idx < to_spawn; spawn_idx++) {
|
for (int spawn_idx = 0; spawn_idx < to_spawn; spawn_idx++) {
|
||||||
auto free_slot =
|
auto free_slot = std::find_if(
|
||||||
std::find_if(state.barriers.begin(), state.barriers.end(),
|
state.barriers.begin(), state.barriers.end(),
|
||||||
[](const Barrier &b) { return b.phase == BarrierPhase::Inactive; });
|
[](const Barrier &b) { return b.phase == BarrierPhase::Inactive; });
|
||||||
if (free_slot == state.barriers.end()) {
|
if (free_slot == state.barriers.end()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -370,11 +423,13 @@ void spawn_barriers(GameState &state) {
|
|||||||
for (int attempt = 0; attempt < 48; attempt++) {
|
for (int attempt = 0; attempt < 48; attempt++) {
|
||||||
Barrier candidate;
|
Barrier candidate;
|
||||||
candidate.vertical = (rand() % 2) == 0;
|
candidate.vertical = (rand() % 2) == 0;
|
||||||
candidate.length = static_cast<uint8_t>(
|
candidate.length =
|
||||||
BARRIER_MIN_LENGTH + (rand() % length_range));
|
static_cast<uint8_t>(BARRIER_MIN_LENGTH + (rand() % length_range));
|
||||||
// Keep the whole line on screen (barriers never wrap around edges)
|
// Keep the whole line on screen (barriers never wrap around edges)
|
||||||
const int max_x = candidate.vertical ? X_MAX : (X_MAX + 1 - candidate.length);
|
const int max_x =
|
||||||
const int max_y = candidate.vertical ? (Y_MAX + 1 - candidate.length) : Y_MAX;
|
candidate.vertical ? X_MAX : (X_MAX + 1 - candidate.length);
|
||||||
|
const int max_y =
|
||||||
|
candidate.vertical ? (Y_MAX + 1 - candidate.length) : Y_MAX;
|
||||||
candidate.x = static_cast<int8_t>(rand() % (max_x + 1));
|
candidate.x = static_cast<int8_t>(rand() % (max_x + 1));
|
||||||
candidate.y = static_cast<int8_t>(rand() % (max_y + 1));
|
candidate.y = static_cast<int8_t>(rand() % (max_y + 1));
|
||||||
|
|
||||||
@@ -401,9 +456,9 @@ void spawn_barriers(GameState &state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (spawned_count > 0) {
|
if (spawned_count > 0) {
|
||||||
state.barrierSpawnCooldown = static_cast<uint16_t>(
|
state.barrierSpawnCooldown =
|
||||||
calculate_barrier_interval_frames(state) +
|
static_cast<uint16_t>(calculate_barrier_interval_frames(state) +
|
||||||
(rand() % (BARRIER_INTERVAL_JITTER + 1)));
|
(rand() % (BARRIER_INTERVAL_JITTER + 1)));
|
||||||
} else {
|
} else {
|
||||||
state.barrierSpawnCooldown = BARRIER_RETRY_FRAMES;
|
state.barrierSpawnCooldown = BARRIER_RETRY_FRAMES;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-6
@@ -125,13 +125,18 @@ void draw_apples(const GameState &state, const RenderAssets &assets,
|
|||||||
|
|
||||||
void draw_apples_rotten(const GameState &state, const RenderAssets &assets,
|
void draw_apples_rotten(const GameState &state, const RenderAssets &assets,
|
||||||
uint16_t &oamId) {
|
uint16_t &oamId) {
|
||||||
for (const std::array<int, 2> &apple : state.bad_apples) {
|
for (const EvilItem &apple : state.bad_apples) {
|
||||||
if (oamId >= 128) {
|
if (oamId >= 128) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8,
|
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,
|
SpriteColorFormat_256Color, assets.gfx_apple_rotten, -1, false,
|
||||||
false, false, false, false);
|
is_hidden, false, false, false);
|
||||||
oamId++;
|
oamId++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,13 +144,18 @@ void draw_apples_rotten(const GameState &state, const RenderAssets &assets,
|
|||||||
void draw_apples_cursed(const GameState &state, const RenderAssets &assets,
|
void draw_apples_cursed(const GameState &state, const RenderAssets &assets,
|
||||||
uint16_t &oamId) {
|
uint16_t &oamId) {
|
||||||
const bool hflip = ((state.ticks % 6) < 3);
|
const bool hflip = ((state.ticks % 6) < 3);
|
||||||
for (const std::array<int, 2> &apple : state.cursed_apples) {
|
for (const EvilItem &apple : state.cursed_apples) {
|
||||||
if (oamId >= 128) {
|
if (oamId >= 128) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8,
|
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,
|
SpriteColorFormat_256Color, assets.gfx_apple_cursed, -1, false,
|
||||||
false, hflip, false, false);
|
is_hidden, hflip, false, false);
|
||||||
oamId++;
|
oamId++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-37
@@ -37,7 +37,7 @@ bool &hardMode = gameState.hardMode;
|
|||||||
|
|
||||||
std::deque<std::array<int, 3>> &snake = gameState.snake;
|
std::deque<std::array<int, 3>> &snake = gameState.snake;
|
||||||
std::vector<std::array<int, 2>> &apples = gameState.apples;
|
std::vector<std::array<int, 2>> &apples = gameState.apples;
|
||||||
std::vector<std::array<int, 2>> &bad_apples = gameState.bad_apples;
|
std::vector<EvilItem> &bad_apples = gameState.bad_apples;
|
||||||
|
|
||||||
// 31x23
|
// 31x23
|
||||||
PrintConsole topScreen;
|
PrintConsole topScreen;
|
||||||
@@ -49,6 +49,7 @@ AudioContext audioContext;
|
|||||||
|
|
||||||
RenderAssets renderAssets;
|
RenderAssets renderAssets;
|
||||||
BackgroundContext bgContext;
|
BackgroundContext bgContext;
|
||||||
|
InputContext inputContext;
|
||||||
|
|
||||||
const void * get_sprite_pointer(uint16_t spriteIndex) {
|
const void * get_sprite_pointer(uint16_t spriteIndex) {
|
||||||
return &tilemapTiles[spriteIndex*16];
|
return &tilemapTiles[spriteIndex*16];
|
||||||
@@ -172,6 +173,11 @@ void setup() {
|
|||||||
dest_barrier_warn[i] = src_barrier_warn[i];
|
dest_barrier_warn[i] = src_barrier_warn[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-reserve capacities to eliminate dynamic heap allocations inside Timer 0 ISR (do_tick)
|
||||||
|
gameState.bad_apples.reserve(32);
|
||||||
|
gameState.cursed_apples.reserve(4);
|
||||||
|
gameState.apples.reserve(4);
|
||||||
|
|
||||||
audio_setup(audioContext);
|
audio_setup(audioContext);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -229,6 +235,7 @@ void do_tick() {
|
|||||||
if (check_collision_apples_cursed(gameState)) {
|
if (check_collision_apples_cursed(gameState)) {
|
||||||
audio_play_boom(audioContext);
|
audio_play_boom(audioContext);
|
||||||
}
|
}
|
||||||
|
tick_evil_items(gameState);
|
||||||
spawn_apples(gameState);
|
spawn_apples(gameState);
|
||||||
spawn_apples_rotten(gameState);
|
spawn_apples_rotten(gameState);
|
||||||
spawn_apples_cursed(gameState);
|
spawn_apples_cursed(gameState);
|
||||||
@@ -245,51 +252,41 @@ void do_tick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void init_game() {
|
void init_game() {
|
||||||
|
timerStop(0);
|
||||||
applesEaten = 0;
|
applesEaten = 0;
|
||||||
reset_barriers(gameState);
|
reset_barriers(gameState);
|
||||||
|
gameState.bad_apples.reserve(32);
|
||||||
|
gameState.cursed_apples.reserve(4);
|
||||||
|
gameState.apples.reserve(4);
|
||||||
|
score = 0;
|
||||||
|
ticks = 0;
|
||||||
|
gameOver = false;
|
||||||
|
snakeDirection = 1;
|
||||||
|
gameState.curseTicksRemaining = 0;
|
||||||
|
apples.clear();
|
||||||
|
bad_apples.clear();
|
||||||
|
gameState.cursed_apples.clear();
|
||||||
|
|
||||||
if (hardMode) {
|
if (hardMode) {
|
||||||
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
|
|
||||||
setBackdropColorSub(RGB15(8, 3, 3));
|
setBackdropColorSub(RGB15(8, 3, 3));
|
||||||
setBackdropColor(RGB15(6, 2, 2));
|
setBackdropColor(RGB15(6, 2, 2));
|
||||||
score = 0;
|
|
||||||
snakelength = START_LENGTH_HARD;
|
snakelength = START_LENGTH_HARD;
|
||||||
snake = { {2,1,1}, {1,1,1} };
|
snake = { {2,1,1}, {1,1,1} };
|
||||||
apples.clear();
|
} else {
|
||||||
bad_apples.clear();
|
|
||||||
snakeDirection = 1;
|
|
||||||
ticks = 0;
|
|
||||||
gameOver=false;
|
|
||||||
gameState.curseTicksRemaining = 0;
|
|
||||||
gameState.cursed_apples.clear();
|
|
||||||
consoleSelect(&topScreen);
|
|
||||||
consoleClear();
|
|
||||||
invalidate_hud(renderAssets);
|
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
consoleClear();
|
|
||||||
oamClear(&oamSub,0,128);
|
|
||||||
audio_start_music(audioContext);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
|
|
||||||
setBackdropColorSub(RGB15(3, 4, 7));
|
setBackdropColorSub(RGB15(3, 4, 7));
|
||||||
setBackdropColor(RGB15(2, 3, 6));
|
setBackdropColor(RGB15(2, 3, 6));
|
||||||
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} };
|
||||||
snakeDirection = 1;
|
|
||||||
ticks = 0;
|
|
||||||
gameOver=false;
|
|
||||||
gameState.curseTicksRemaining = 0;
|
|
||||||
gameState.cursed_apples.clear();
|
|
||||||
consoleSelect(&topScreen);
|
|
||||||
consoleClear();
|
|
||||||
invalidate_hud(renderAssets);
|
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
consoleClear();
|
|
||||||
oamClear(&oamSub,0,128);
|
|
||||||
audio_start_music(audioContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consoleSelect(&topScreen);
|
||||||
|
consoleClear();
|
||||||
|
invalidate_hud(renderAssets);
|
||||||
|
consoleSelect(&bottomScreen);
|
||||||
|
consoleClear();
|
||||||
|
oamClear(&oamSub, 0, 128);
|
||||||
|
audio_start_music(audioContext);
|
||||||
|
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
@@ -302,7 +299,6 @@ int main(void) {
|
|||||||
setBackdropColorSub(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.state = &gameState;
|
inputContext.state = &gameState;
|
||||||
inputContext.renderAssets = &renderAssets;
|
inputContext.renderAssets = &renderAssets;
|
||||||
inputContext.audio = &audioContext;
|
inputContext.audio = &audioContext;
|
||||||
@@ -310,12 +306,10 @@ int main(void) {
|
|||||||
inputContext.topScreen = &topScreen;
|
inputContext.topScreen = &topScreen;
|
||||||
inputContext.init_game = init_game;
|
inputContext.init_game = init_game;
|
||||||
|
|
||||||
|
|
||||||
while(1) {
|
while(1) {
|
||||||
if (!handle_input_frame(inputContext)) {
|
if (!handle_input_frame(inputContext)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Reference in new issue
Block a user