portapack-mayhem/firmware/application/external/spaceinv/ui_spaceinv.cpp
RocketGod 1d8f53b49e
Space invaders UI improvements for all device screens including new PortaRF (#2810)
* Dynamic screen for PortaRF

Dynamic screen size for new PortaRF device.

* Format code

* Space Invaders improvements for all devices including new PortaRF

Space Invaders improvements for all devices including new PortaRF
2025-10-09 22:30:30 +02:00

896 lines
No EOL
31 KiB
C++

/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#include "ui_spaceinv.hpp"
namespace ui::external_app::spaceinv {
// Dynamic game constants
static int SCREEN_WIDTH = 240;
static int SCREEN_HEIGHT = 320;
static int PLAYER_WIDTH = 26;
static int PLAYER_HEIGHT = 16;
static int PLAYER_Y = 280;
static int BULLET_WIDTH = 3;
static int BULLET_HEIGHT = 10;
static int BULLET_SPEED = 8;
static int MAX_BULLETS = 3;
static int INVADER_WIDTH = 20;
static int INVADER_HEIGHT = 16;
static int INVADER_ROWS = 5;
static int INVADER_COLS = 6;
static int INVADER_GAP_X = 10;
static int INVADER_GAP_Y = 8;
static int MAX_ENEMY_BULLETS = 3;
static int ENEMY_BULLET_SPEED = 3;
static int INFO_BAR_HEIGHT = 50;
static int INVADER_MOVE_AMOUNT = 8;
static int INVADER_DROP_AMOUNT = 15;
// Game state
static int game_state = 0; // 0=menu, 1=playing, 2=game over, 3=wave_complete
static bool initialized = false;
static int player_x = 120;
static uint32_t score = 0;
static int lives = 3;
static uint32_t wave = 1;
static uint32_t speed_bonus = 0;
static uint32_t wave_complete_timer = 0;
// Cannon Bullets
static int bullet_x[3] = {0, 0, 0};
static int bullet_y[3] = {0, 0, 0};
static bool bullet_active[3] = {false, false, false};
// Enemy bullets
static int enemy_bullet_x[3] = {0, 0, 0};
static int enemy_bullet_y[3] = {0, 0, 0};
static bool enemy_bullet_active[3] = {false, false, false};
static uint32_t enemy_fire_counter = 0;
// Invaders
static bool invaders[5][11]; // Max possible columns
static int invaders_x = 40;
static int invaders_y = 60;
static int invader_direction = 1;
static uint32_t invader_move_counter = 0;
static uint32_t invader_move_delay = 30;
static bool invader_animation_frame = false;
// Menu state
static bool menu_initialized = false;
static bool game_over_initialized = false;
static bool blink_state = true;
static uint32_t blink_counter = 0;
static int16_t prompt_x = 0;
// Timer
static Ticker game_timer;
static Callback game_update_callback = nullptr;
static uint32_t game_update_timeout = 0;
static uint32_t game_update_counter = 0;
// Painter
static Painter painter;
// For high score access
static SpaceInvadersView* current_instance = nullptr;
void check_game_timer() {
if (game_update_callback) {
if (++game_update_counter >= game_update_timeout) {
game_update_counter = 0;
game_update_callback();
}
}
}
void Ticker::attach(Callback func, double delay_sec) {
game_update_callback = func;
game_update_timeout = delay_sec * 60;
}
void Ticker::detach() {
game_update_callback = nullptr;
}
static void init_dimensions() {
SCREEN_WIDTH = ui::screen_width;
SCREEN_HEIGHT = ui::screen_height;
// Scale player based on screen size
PLAYER_WIDTH = SCREEN_WIDTH / 9;
PLAYER_HEIGHT = SCREEN_HEIGHT / 20;
PLAYER_Y = SCREEN_HEIGHT - 40;
// Keep invader size fixed for proper pixel art
INVADER_WIDTH = 20;
INVADER_HEIGHT = 16;
// Calculate columns based on available width
int available_width = SCREEN_WIDTH - 40; // 20px margins on each side
INVADER_COLS = available_width / (INVADER_WIDTH + INVADER_GAP_X);
if (INVADER_COLS > 11) INVADER_COLS = 11;
if (INVADER_COLS < 6) INVADER_COLS = 6;
// Center the invader formation
int formation_width = INVADER_COLS * INVADER_WIDTH + (INVADER_COLS - 1) * INVADER_GAP_X;
invaders_x = (SCREEN_WIDTH - formation_width) / 2;
invaders_y = INFO_BAR_HEIGHT + 10;
// Scale movement
INVADER_MOVE_AMOUNT = SCREEN_WIDTH / 30;
INVADER_DROP_AMOUNT = SCREEN_HEIGHT / 21;
}
static void init_invaders() {
// Initialize invader array based on dynamic columns
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
invaders[row][col] = true;
}
}
// Reset position to center
int formation_width = INVADER_COLS * INVADER_WIDTH + (INVADER_COLS - 1) * INVADER_GAP_X;
invaders_x = (SCREEN_WIDTH - formation_width) / 2;
invaders_y = INFO_BAR_HEIGHT + 10;
invader_direction = 1;
invader_move_counter = 0;
// Clear any active enemy bullets
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
enemy_bullet_active[i] = false;
}
// Clear any active player bullets
for (int i = 0; i < MAX_BULLETS; i++) {
bullet_active[i] = false;
}
}
static void save_high_score() {
if (current_instance && score > current_instance->highScore) {
current_instance->highScore = score;
// Settings are automatically saved when the view is destroyed
}
}
static void init_menu() {
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
auto style_green = *ui::Theme::getInstance()->fg_green;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
auto style_red = *ui::Theme::getInstance()->fg_red;
// Title section
painter.draw_string({UI_POS_X_CENTER(15), 30}, style_green, "SPACE INVADERS");
painter.draw_string({UI_POS_X_CENTER(24), 55}, style_yellow, "========================");
// Instructions box
int box_width = 22 * 8;
int box_x = UI_POS_X_CENTER(22);
painter.fill_rectangle({box_x - 5, 85, box_width + 10, 80}, Color::black());
painter.draw_rectangle({box_x - 5, 85, box_width + 10, 80}, Color::white());
painter.draw_string({box_x, 95}, style_cyan, " ROTARY: MOVE SHIP");
painter.draw_string({box_x, 115}, style_cyan, " SELECT: FIRE");
painter.draw_string({box_x, 135}, style_cyan, " DEFEND EARTH!");
// Point values
painter.draw_string({UI_POS_X_CENTER(16), 175}, style_red, "TOP ROW = 30 PTS");
painter.draw_string({UI_POS_X_CENTER(16), 190}, style_yellow, "MID ROW = 20 PTS");
painter.draw_string({UI_POS_X_CENTER(16), 205}, style_green, "BOT ROW = 10 PTS");
// High score
if (current_instance) {
auto style_white = *ui::Theme::getInstance()->fg_light;
std::string high_score_text = "HIGH SCORE: " + std::to_string(current_instance->highScore);
painter.draw_string({UI_POS_X_CENTER(high_score_text.length()), 225}, style_white, high_score_text);
}
// Set prompt position (will be used for blinking text)
prompt_x = UI_POS_X_CENTER(12);
menu_initialized = true;
}
static void init_game_over() {
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
auto style_white = *ui::Theme::getInstance()->fg_light;
// Game Over title
painter.draw_string({UI_POS_X_CENTER(9), 40}, style_red, "GAME OVER");
painter.draw_string({UI_POS_X_CENTER(24), 65}, style_yellow, "========================");
// Score box
int box_width = 22 * 8;
int box_x = UI_POS_X_CENTER(22);
painter.fill_rectangle({box_x - 5, 90, box_width + 10, 80}, Color::black());
painter.draw_rectangle({box_x - 5, 90, box_width + 10, 80}, Color::white());
// Display scores
std::string score_text = "SCORE: " + std::to_string(score);
painter.draw_string({UI_POS_X_CENTER(score_text.length()), 110}, style_cyan, score_text);
std::string wave_text = "WAVE: " + std::to_string(wave);
painter.draw_string({UI_POS_X_CENTER(wave_text.length()), 130}, style_cyan, wave_text);
// High score section
if (current_instance) {
if (score > current_instance->highScore) {
// New high score!
painter.draw_string({UI_POS_X_CENTER(15), 190}, style_yellow, "NEW HIGH SCORE!");
std::string high_score_text = std::to_string(score);
painter.draw_string({UI_POS_X_CENTER(high_score_text.length()), 210}, style_yellow, high_score_text);
} else {
// Show existing high score
painter.draw_string({UI_POS_X_CENTER(11), 190}, style_white, "HIGH SCORE:");
std::string high_score_text = std::to_string(current_instance->highScore);
painter.draw_string({UI_POS_X_CENTER(high_score_text.length()), 210}, style_white, high_score_text);
}
}
// Set prompt position for blinking text
prompt_x = UI_POS_X_CENTER(12);
game_over_initialized = true;
}
static void draw_player() {
// Clear the entire player area
painter.fill_rectangle({0, PLAYER_Y - 4, SCREEN_WIDTH, PLAYER_HEIGHT + 8}, Color::black());
// Draw the classic Space Invaders cannon
Color green = Color::green();
// Scale the drawing based on player size
int unit_x = PLAYER_WIDTH / 26;
int unit_y = PLAYER_HEIGHT / 16;
// Top part - the cannon barrel
painter.fill_rectangle({player_x + 12 * unit_x, PLAYER_Y, 2 * unit_x, 2 * unit_y}, green);
// Upper body
painter.fill_rectangle({player_x + 11 * unit_x, PLAYER_Y + 2 * unit_y, 4 * unit_x, 2 * unit_y}, green);
painter.fill_rectangle({player_x + 10 * unit_x, PLAYER_Y + 4 * unit_y, 6 * unit_x, 2 * unit_y}, green);
// Main body
painter.fill_rectangle({player_x + 2 * unit_x, PLAYER_Y + 6 * unit_y, 22 * unit_x, 10 * unit_y}, green);
painter.fill_rectangle({player_x + unit_x, PLAYER_Y + 8 * unit_y, 24 * unit_x, 8 * unit_y}, green);
painter.fill_rectangle({player_x, PLAYER_Y + 10 * unit_y, 26 * unit_x, 6 * unit_y}, green);
}
static void draw_invader(int row, int col) {
int x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
// Clear the area first
painter.fill_rectangle({x, y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
// Different colors and shapes for different rows
Color color;
if (row == 0) {
color = Color::red(); // Top row - 30 points
} else if (row == 1 || row == 2) {
color = Color::yellow(); // Middle rows - 20 points
} else {
color = Color::green(); // Bottom rows - 10 points
}
// Draw different invader types based on row
if (row == 0) {
// Top row - squid-like invader (30 points)
if (invader_animation_frame) {
// Frame 1 - arms down
painter.draw_hline({x + 8, y + 2}, 4, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 4, y + 4}, 12, color);
painter.draw_hline({x + 2, y + 5}, 16, color);
painter.draw_hline({x + 2, y + 6}, 16, color);
painter.draw_hline({x, y + 7}, 20, color);
painter.draw_hline({x, y + 8}, 20, color);
painter.draw_hline({x + 2, y + 9}, 2, color);
painter.draw_hline({x + 6, y + 9}, 8, color);
painter.draw_hline({x + 16, y + 9}, 2, color);
painter.draw_hline({x + 4, y + 10}, 2, color);
painter.draw_hline({x + 14, y + 10}, 2, color);
} else {
// Frame 2 - arms up
painter.draw_hline({x + 8, y + 2}, 4, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 4, y + 4}, 12, color);
painter.draw_hline({x + 2, y + 5}, 16, color);
painter.draw_hline({x + 2, y + 6}, 16, color);
painter.draw_hline({x, y + 7}, 20, color);
painter.draw_hline({x, y + 8}, 20, color);
painter.draw_hline({x + 4, y + 9}, 2, color);
painter.draw_hline({x + 8, y + 9}, 4, color);
painter.draw_hline({x + 14, y + 9}, 2, color);
painter.draw_hline({x + 2, y + 10}, 2, color);
painter.draw_hline({x + 16, y + 10}, 2, color);
}
} else if (row == 1 || row == 2) {
// Middle rows - crab-like invader (20 points)
if (invader_animation_frame) {
// Frame 1 - arms in
painter.draw_hline({x + 4, y + 2}, 2, color);
painter.draw_hline({x + 14, y + 2}, 2, color);
painter.draw_hline({x + 2, y + 3}, 2, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 16, y + 3}, 2, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 6, color);
painter.draw_hline({x + 8, y + 5}, 4, color);
painter.draw_hline({x + 14, y + 5}, 6, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 2, y + 7}, 16, color);
painter.draw_hline({x + 4, y + 8}, 2, color);
painter.draw_hline({x + 14, y + 8}, 2, color);
painter.draw_hline({x + 2, y + 9}, 4, color);
painter.draw_hline({x + 14, y + 9}, 4, color);
} else {
// Frame 2 - arms out
painter.draw_hline({x + 4, y + 2}, 2, color);
painter.draw_hline({x + 14, y + 2}, 2, color);
painter.draw_hline({x + 2, y + 3}, 2, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 16, y + 3}, 2, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 6, color);
painter.draw_hline({x + 8, y + 5}, 4, color);
painter.draw_hline({x + 14, y + 5}, 6, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 2, y + 7}, 16, color);
painter.draw_hline({x + 4, y + 8}, 2, color);
painter.draw_hline({x + 14, y + 8}, 2, color);
painter.draw_hline({x, y + 9}, 2, color);
painter.draw_hline({x + 6, y + 9}, 2, color);
painter.draw_hline({x + 12, y + 9}, 2, color);
painter.draw_hline({x + 18, y + 9}, 2, color);
}
} else {
// Bottom rows - octopus-like invader (10 points)
if (invader_animation_frame) {
// Frame 1
painter.draw_hline({x + 6, y + 2}, 8, color);
painter.draw_hline({x + 4, y + 3}, 12, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 20, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 4, y + 7}, 4, color);
painter.draw_hline({x + 12, y + 7}, 4, color);
painter.draw_hline({x + 2, y + 8}, 4, color);
painter.draw_hline({x + 8, y + 8}, 4, color);
painter.draw_hline({x + 14, y + 8}, 4, color);
painter.draw_hline({x + 4, y + 9}, 2, color);
painter.draw_hline({x + 14, y + 9}, 2, color);
} else {
// Frame 2
painter.draw_hline({x + 6, y + 2}, 8, color);
painter.draw_hline({x + 4, y + 3}, 12, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 20, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 4, y + 7}, 4, color);
painter.draw_hline({x + 12, y + 7}, 4, color);
painter.draw_hline({x + 2, y + 8}, 4, color);
painter.draw_hline({x + 8, y + 8}, 4, color);
painter.draw_hline({x + 14, y + 8}, 4, color);
painter.draw_hline({x + 2, y + 9}, 2, color);
painter.draw_hline({x + 16, y + 9}, 2, color);
}
}
}
static void draw_all_invaders() {
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
draw_invader(row, col);
}
}
}
}
static void clear_all_invaders() {
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
int x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
painter.fill_rectangle({x, y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
}
}
}
}
static void clear_all_enemy_bullets() {
// Clear any visible enemy bullets
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (enemy_bullet_active[i]) {
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
enemy_bullet_active[i] = false;
}
}
}
static void fire_enemy_bullet() {
// Find a random invader to fire from
int active_count = 0;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) active_count++;
}
}
if (active_count == 0) return;
int shooter = rand() % active_count;
int count = 0;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
if (count == shooter) {
// Fire from this invader
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (!enemy_bullet_active[i]) {
int x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
enemy_bullet_x[i] = x + INVADER_WIDTH / 2;
enemy_bullet_y[i] = y + INVADER_HEIGHT;
enemy_bullet_active[i] = true;
return;
}
}
}
count++;
}
}
}
}
static void update_enemy_bullets() {
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (enemy_bullet_active[i]) {
// Clear old position
if (enemy_bullet_y[i] != INFO_BAR_HEIGHT - 1) {
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
}
enemy_bullet_y[i] += ENEMY_BULLET_SPEED;
if (enemy_bullet_y[i] > SCREEN_HEIGHT) {
enemy_bullet_active[i] = false;
} else {
// Draw at new position
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::red());
// Check collision with player
if (enemy_bullet_x[i] >= player_x &&
enemy_bullet_x[i] <= player_x + PLAYER_WIDTH &&
enemy_bullet_y[i] >= PLAYER_Y &&
enemy_bullet_y[i] <= PLAYER_Y + PLAYER_HEIGHT) {
// Player hit!
enemy_bullet_active[i] = false;
lives--;
// Update lives display
auto style = *ui::Theme::getInstance()->fg_green;
painter.fill_rectangle({5, 30, 100, 20}, Color::black());
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
if (lives <= 0) {
game_state = 2; // Game over
save_high_score();
}
}
}
}
}
}
static void update_invaders() {
uint32_t adjusted_delay = (speed_bonus >= invader_move_delay) ? 1 : invader_move_delay - speed_bonus;
if (++invader_move_counter >= adjusted_delay) {
invader_move_counter = 0;
// Clear old positions
clear_all_invaders();
// Check bounds
int leftmost = INVADER_COLS;
int rightmost = -1;
for (int col = 0; col < INVADER_COLS; col++) {
for (int row = 0; row < INVADER_ROWS; row++) {
if (invaders[row][col]) {
if (col < leftmost) leftmost = col;
if (col > rightmost) rightmost = col;
}
}
}
bool hit_edge = false;
if (invader_direction > 0) {
// Moving right - check rightmost invader with margin
int right_edge = invaders_x + rightmost * (INVADER_WIDTH + INVADER_GAP_X) + INVADER_WIDTH;
if (right_edge >= SCREEN_WIDTH - 5) { // 5px margin
hit_edge = true;
}
} else {
// Moving left - check leftmost invader with margin
int left_edge = invaders_x + leftmost * (INVADER_WIDTH + INVADER_GAP_X);
if (left_edge <= 5) { // 5px margin
hit_edge = true;
}
}
// Move invaders
if (hit_edge) {
invaders_y += INVADER_DROP_AMOUNT;
invader_direction = -invader_direction;
} else {
invaders_x += invader_direction * INVADER_MOVE_AMOUNT;
}
// Toggle animation frame
invader_animation_frame = !invader_animation_frame;
// Draw new positions
draw_all_invaders();
}
// Enemy firing logic
if (!current_instance || !current_instance->easy_mode) {
if (++enemy_fire_counter >= 120) { // Fire every 2 seconds at 60fps
enemy_fire_counter = 0;
fire_enemy_bullet();
}
}
}
static void fire_bullet() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullet_active[i]) {
bullet_active[i] = true;
bullet_x[i] = player_x + PLAYER_WIDTH / 2 - BULLET_WIDTH / 2;
bullet_y[i] = PLAYER_Y - BULLET_HEIGHT;
break;
}
}
}
static void update_bullets() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (bullet_active[i]) {
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
bullet_y[i] -= BULLET_SPEED;
if (bullet_y[i] < INFO_BAR_HEIGHT) {
bullet_active[i] = false;
} else {
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::white());
}
}
}
}
static void check_collisions() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullet_active[i]) continue;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (!invaders[row][col]) continue;
int inv_x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int inv_y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
// Simple collision check
if (bullet_x[i] < inv_x + INVADER_WIDTH &&
bullet_x[i] + BULLET_WIDTH > inv_x &&
bullet_y[i] < inv_y + INVADER_HEIGHT &&
bullet_y[i] + BULLET_HEIGHT > inv_y) {
// Hit!
bullet_active[i] = false;
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
invaders[row][col] = false;
painter.fill_rectangle({inv_x, inv_y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
// Score based on row
if (row == 0) {
score += 30;
} else if (row == 1 || row == 2) {
score += 20;
} else {
score += 10;
}
return;
}
}
}
}
}
static void check_wave_complete() {
// Check if all invaders are destroyed
bool all_destroyed = true;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
all_destroyed = false;
break;
}
}
if (!all_destroyed) break;
}
if (all_destroyed) {
// Next wave!
wave++;
speed_bonus = (wave - 1) * 3; // Each wave is slightly faster
if (speed_bonus > 15) speed_bonus = 15; // Cap the speed increase
// Clear any enemy bullets
clear_all_enemy_bullets();
// Set state to wave complete
game_state = 3;
wave_complete_timer = 60; // 1 second
// Clear screen and show wave message
painter.fill_rectangle({0, INFO_BAR_HEIGHT + 1, SCREEN_WIDTH, SCREEN_HEIGHT - INFO_BAR_HEIGHT - 1}, Color::black());
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
auto style = *ui::Theme::getInstance()->fg_green;
std::string wave_text = "WAVE " + std::to_string(wave);
painter.draw_string({UI_POS_X_CENTER(wave_text.length()), SCREEN_HEIGHT / 2}, style, wave_text);
return;
}
// Check if invaders reached player
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
if (y + INVADER_HEIGHT >= PLAYER_Y) {
game_state = 2; // Game over
save_high_score();
return;
}
}
}
}
}
void game_timer_check() {
if (game_state == 0) {
// Menu state
if (!menu_initialized) {
init_menu();
}
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
auto style = *ui::Theme::getInstance()->fg_red;
int blink_y = 250; // Position below the high score
if (blink_state) {
painter.draw_string({prompt_x, blink_y}, style, "PRESS SELECT");
} else {
painter.fill_rectangle({prompt_x, blink_y, 12 * 8, 20}, Color::black());
}
}
} else if (game_state == 1) {
// Playing state
update_bullets();
update_enemy_bullets();
update_invaders();
check_collisions();
check_wave_complete();
// Update score display
static uint32_t last_score = 999999;
if (score != last_score) {
last_score = score;
auto style = *ui::Theme::getInstance()->fg_green;
painter.fill_rectangle({5, 10, 100, 20}, Color::black());
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
}
// Periodically redraw the border line
static uint32_t border_redraw_counter = 0;
if (++border_redraw_counter >= 60) {
border_redraw_counter = 0;
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
}
} else if (game_state == 2) {
// Game over state
if (!game_over_initialized) {
init_game_over();
}
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
auto style = *ui::Theme::getInstance()->fg_red;
int blink_y = 250; // Position below high score section
if (blink_state) {
painter.draw_string({prompt_x, blink_y}, style, "PRESS SELECT");
} else {
painter.fill_rectangle({prompt_x, blink_y, 12 * 8, 20}, Color::black());
}
}
} else if (game_state == 3) {
// Wave complete state - wait for timer
if (--wave_complete_timer <= 0) {
game_state = 1;
// Clear and redraw game area
painter.fill_rectangle({0, INFO_BAR_HEIGHT + 1, SCREEN_WIDTH, SCREEN_HEIGHT - INFO_BAR_HEIGHT - 1}, Color::black());
// Restore UI
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
// Initialize new wave
init_invaders();
draw_all_invaders();
draw_player();
}
}
}
SpaceInvadersView::SpaceInvadersView(NavigationView& nav)
: nav_{nav} {
add_children({&dummy, &button_difficulty});
// Initialize dimensions first
current_instance = this;
init_dimensions();
// Now reposition button with proper centering
int button_y = SCREEN_HEIGHT - 45; // 45px from bottom
int button_x = (SCREEN_WIDTH - 100) / 2; // Center horizontally
button_difficulty.set_parent_rect({button_x, button_y, 100, 20});
game_timer.attach(&game_timer_check, 1.0 / 60.0);
// Update button text based on loaded setting
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
button_difficulty.on_select = [this](Button&) {
easy_mode = !easy_mode;
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
};
}
SpaceInvadersView::~SpaceInvadersView() {
current_instance = nullptr;
}
void SpaceInvadersView::on_show() {
}
void SpaceInvadersView::paint(Painter& painter) {
(void)painter;
if (!initialized) {
initialized = true;
game_state = 0;
menu_initialized = false;
game_over_initialized = false;
blink_state = true;
blink_counter = 0;
player_x = SCREEN_WIDTH / 2 - PLAYER_WIDTH / 2;
score = 0;
lives = 3;
wave = 1;
speed_bonus = 0;
// Initialize arrays
for (int i = 0; i < MAX_BULLETS; i++) {
bullet_active[i] = false;
bullet_x[i] = 0;
bullet_y[i] = 0;
}
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
enemy_bullet_active[i] = false;
enemy_bullet_x[i] = 0;
enemy_bullet_y[i] = 0;
}
init_invaders();
}
}
void SpaceInvadersView::frame_sync() {
check_game_timer();
set_dirty();
}
bool SpaceInvadersView::on_encoder(const EncoderEvent delta) {
if (game_state == 1) {
int move_speed = SCREEN_WIDTH / 48; // Scale movement speed
if (delta > 0) {
player_x += move_speed;
if (player_x > SCREEN_WIDTH - PLAYER_WIDTH) player_x = SCREEN_WIDTH - PLAYER_WIDTH;
draw_player();
} else if (delta < 0) {
player_x -= move_speed;
if (player_x < 0) player_x = 0;
draw_player();
}
set_dirty();
}
return true;
}
bool SpaceInvadersView::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (game_state == 0) {
// Start game
game_state = 1;
button_difficulty.hidden(true);
score = 0;
lives = 3;
wave = 1;
speed_bonus = 0;
invader_move_delay = 30;
enemy_fire_counter = 0;
init_invaders();
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: 0");
painter.draw_string({5, 30}, style, "Lives: 3");
draw_all_invaders();
draw_player();
set_dirty();
} else if (game_state == 1) {
fire_bullet();
} else if (game_state == 2) {
// Return to menu
game_state = 0;
menu_initialized = false;
game_over_initialized = false;
button_difficulty.hidden(false);
set_dirty();
}
}
return true;
}
} // namespace ui::external_app::spaceinv