diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index c6f52b44a..581bbf3d0 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -220,6 +220,10 @@ set(EXTCPPSRC #detector_rx external/detector_rx/main.cpp external/detector_rx/ui_detector_rx.cpp + + #space_invaders + external/spaceinv/main.cpp + external/spaceinv/ui_spaceinv.cpp ) set(EXTAPPLIST @@ -276,4 +280,5 @@ set(EXTAPPLIST level gfxeq detector_rx + spaceinv ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 18116ad94..9a8963428 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -76,6 +76,7 @@ MEMORY ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k + ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k } SECTIONS @@ -398,5 +399,11 @@ SECTIONS *(*ui*external_app*dinogame*); } > ram_external_app_dinogame + .external_app_spaceinv : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_spaceinv.application_information)); + *(*ui*external_app*spaceinv*); + } > ram_external_app_spaceinv + } diff --git a/firmware/application/external/spaceinv/main.cpp b/firmware/application/external/spaceinv/main.cpp new file mode 100644 index 000000000..d7c9fe244 --- /dev/null +++ b/firmware/application/external/spaceinv/main.cpp @@ -0,0 +1,69 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#include "ui.hpp" +#include "ui_spaceinv.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::spaceinv { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::spaceinv + +extern "C" { + +__attribute__((section(".external_app.app_spaceinv.application_information"), used)) application_information_t _application_information_spaceinv = { + (uint8_t*)0x00000000, + ui::external_app::spaceinv::initialize_app, + CURRENT_HEADER_VERSION, + VERSION_MD5, + + "Space Invaders", + { + 0x18, + 0x3C, + 0x7E, + 0xDB, + 0xFF, + 0xFF, + 0x24, + 0x66, + 0x42, + 0x00, + 0x24, + 0x18, + 0x24, + 0x42, + 0x81, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + }, + ui::Color::yellow().v, + app_location_t::GAMES, + -1, + + {0, 0, 0, 0}, + 0x00000000, +}; +} // namespace ui::external_app::spaceinv diff --git a/firmware/application/external/spaceinv/ui_spaceinv.cpp b/firmware/application/external/spaceinv/ui_spaceinv.cpp new file mode 100644 index 000000000..9c02d9dea --- /dev/null +++ b/firmware/application/external/spaceinv/ui_spaceinv.cpp @@ -0,0 +1,879 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#include "ui_spaceinv.hpp" + +namespace ui::external_app::spaceinv { + +// Game constants +#define PLAYER_WIDTH 26 +#define PLAYER_HEIGHT 16 +#define PLAYER_Y 280 +#define BULLET_WIDTH 3 +#define BULLET_HEIGHT 10 +#define BULLET_SPEED 8 +#define MAX_BULLETS 3 +#define INVADER_WIDTH 20 +#define INVADER_HEIGHT 16 +#define INVADER_ROWS 5 // Classic 5 rows +#define INVADER_COLS 6 // Reduced for more movement space on narrow PP screen +#define INVADER_GAP_X 10 +#define INVADER_GAP_Y 8 // Gap between invaders +#define MAX_ENEMY_BULLETS 3 +#define ENEMY_BULLET_SPEED 3 + +// 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[MAX_BULLETS] = {0, 0, 0}; +static int bullet_y[MAX_BULLETS] = {0, 0, 0}; +static bool bullet_active[MAX_BULLETS] = {false, false, false}; + +// Enemy bullets +static int enemy_bullet_x[MAX_ENEMY_BULLETS] = {0, 0, 0}; +static int enemy_bullet_y[MAX_ENEMY_BULLETS] = {0, 0, 0}; +static bool enemy_bullet_active[MAX_ENEMY_BULLETS] = {false, false, false}; +static uint32_t enemy_fire_counter = 0; + +// Invaders +static bool invaders[INVADER_ROWS][INVADER_COLS]; +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_invaders() { + // Initialize invader array + for (int row = 0; row < INVADER_ROWS; row++) { + for (int col = 0; col < INVADER_COLS; col++) { + invaders[row][col] = true; + } + } + invaders_x = 40; + invaders_y = 60; + 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, 240, 320}, 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; + + int16_t screen_width = 240; + int16_t title_x = (screen_width - 15 * 8) / 2; + int16_t divider_width = 24 * 8; + int16_t divider_x = (screen_width - divider_width) / 2; + int16_t instruction_width = 22 * 8; + int16_t instruction_x = (screen_width - instruction_width) / 2; + int16_t prompt_width = 12 * 8; + prompt_x = (screen_width - prompt_width) / 2; + + painter.fill_rectangle({0, 30, screen_width, 30}, Color::black()); + painter.draw_string({title_x, 42}, style_green, "SPACE INVADERS"); + + painter.draw_string({divider_x, 70}, style_yellow, "========================"); + + painter.fill_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::black()); + painter.draw_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::white()); + + painter.draw_string({instruction_x, 110}, style_cyan, " ROTARY: MOVE SHIP"); + painter.draw_string({instruction_x, 130}, style_cyan, " SELECT: FIRE"); + painter.draw_string({instruction_x, 150}, style_cyan, " DEFEND EARTH!"); + + // Draw point values + auto style_purple = *ui::Theme::getInstance()->fg_magenta; + painter.draw_string({50, 190}, style_purple, "TOP ROW = 30 PTS"); + painter.draw_string({50, 205}, style_yellow, "MID ROW = 20 PTS"); + painter.draw_string({50, 220}, style_green, "BOT ROW = 10 PTS"); + + // Draw 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); + int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2; + painter.draw_string({high_score_x, 240}, style_white, high_score_text); + } + + menu_initialized = true; +} + +static void init_game_over() { + painter.fill_rectangle({0, 0, 240, 320}, 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; + + int16_t screen_width = 240; + + // Game Over title + int16_t title_x = (screen_width - 9 * 8) / 2; + painter.draw_string({title_x, 42}, style_red, "GAME OVER"); + + // Divider + int16_t divider_width = 24 * 8; + int16_t divider_x = (screen_width - divider_width) / 2; + painter.draw_string({divider_x, 70}, style_yellow, "========================"); + + // Score box + int16_t box_width = 22 * 8; + int16_t box_x = (screen_width - box_width) / 2; + painter.fill_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::black()); + painter.draw_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::white()); + + // Display scores + std::string score_text = "SCORE: " + std::to_string(score); + int16_t score_x = (screen_width - score_text.length() * 8) / 2; + painter.draw_string({score_x, 120}, style_cyan, score_text); + + std::string wave_text = "WAVE: " + std::to_string(wave); + int16_t wave_x = (screen_width - wave_text.length() * 8) / 2; + painter.draw_string({wave_x, 140}, style_cyan, wave_text); + + // High score section + if (current_instance) { + if (score > current_instance->highScore) { + // New high score! + std::string new_high = "NEW HIGH SCORE!"; + int16_t new_high_x = (screen_width - new_high.length() * 8) / 2; + painter.draw_string({new_high_x, 200}, style_yellow, new_high); + + std::string high_score_text = std::to_string(score); + int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2; + painter.draw_string({high_score_x, 220}, style_yellow, high_score_text); + } else { + // Show existing high score + std::string high_label = "HIGH SCORE:"; + int16_t label_x = (screen_width - high_label.length() * 8) / 2; + painter.draw_string({label_x, 200}, style_white, high_label); + + std::string high_score_text = std::to_string(current_instance->highScore); + int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2; + painter.draw_string({high_score_x, 220}, style_white, high_score_text); + } + } + + game_over_initialized = true; +} + +static void draw_player() { + // Clear the entire player area + painter.fill_rectangle({0, PLAYER_Y - 4, 240, PLAYER_HEIGHT + 8}, Color::black()); + + // Draw the classic Space Invaders cannon + Color green = Color::green(); + + // Top part - the cannon barrel + painter.draw_hline({player_x + 12, PLAYER_Y}, 2, green); + painter.draw_hline({player_x + 12, PLAYER_Y + 1}, 2, green); + + // Upper body + painter.draw_hline({player_x + 11, PLAYER_Y + 2}, 4, green); + painter.draw_hline({player_x + 11, PLAYER_Y + 3}, 4, green); + painter.draw_hline({player_x + 10, PLAYER_Y + 4}, 6, green); + painter.draw_hline({player_x + 10, PLAYER_Y + 5}, 6, green); + + // Main body + painter.draw_hline({player_x + 2, PLAYER_Y + 6}, 22, green); + painter.draw_hline({player_x + 2, PLAYER_Y + 7}, 22, green); + painter.draw_hline({player_x + 1, PLAYER_Y + 8}, 24, green); + painter.draw_hline({player_x + 1, PLAYER_Y + 9}, 24, green); + painter.draw_hline({player_x, PLAYER_Y + 10}, 26, green); + painter.draw_hline({player_x, PLAYER_Y + 11}, 26, green); + painter.draw_hline({player_x, PLAYER_Y + 12}, 26, green); + painter.draw_hline({player_x, PLAYER_Y + 13}, 26, green); + painter.draw_hline({player_x, PLAYER_Y + 14}, 26, green); + painter.draw_hline({player_x, PLAYER_Y + 15}, 26, 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 - but protect the border line + if (enemy_bullet_y[i] != 49) { // Don't clear if we're exactly on the border line + 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] > 320) { + 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 - find the actual leftmost and rightmost invaders + 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 + int right_edge = invaders_x + rightmost * (INVADER_WIDTH + INVADER_GAP_X) + INVADER_WIDTH; + if (right_edge >= 240) { + hit_edge = true; + } + } else { + // Moving left - check leftmost invader + int left_edge = invaders_x + leftmost * (INVADER_WIDTH + INVADER_GAP_X); + if (left_edge <= 0) { + hit_edge = true; + } + } + + // Move invaders + if (hit_edge) { + invaders_y += 15; // Move down + invader_direction = -invader_direction; + } else { + invaders_x += invader_direction * 8; // Horizontal movement + } + + // Toggle animation frame + invader_animation_frame = !invader_animation_frame; + + // Draw new positions + draw_all_invaders(); + } + + // Enemy firing logic - only in hard mode or always with lower frequency + 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] < 50) { + 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: top=30, middle=20, bottom=10 + 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 before transitioning + clear_all_enemy_bullets(); + + // Set state to wave complete and start timer + game_state = 3; + wave_complete_timer = 60; // 1 second at 60fps + + // Clear screen and show wave message - but preserve the border line + painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line at 49 + + // Redraw the border line to ensure it's intact - stupid bullet clearing wants to damage it + painter.draw_hline({0, 49}, 240, Color::white()); + + auto style = *ui::Theme::getInstance()->fg_green; + std::string wave_text = "WAVE " + std::to_string(wave); + int wave_x = (240 - wave_text.length() * 8) / 2; + painter.draw_string({wave_x, 150}, 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) { // Actual collision with player + 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; + if (blink_state) { + painter.draw_string({prompt_x, 260}, style, "PRESS SELECT"); + } else { + painter.fill_rectangle({prompt_x, 260, 16 * 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)); + + // Redraw border line after score update (in case it was damaged) - stupid bullet clearing wants to damage it + painter.draw_hline({0, 49}, 240, Color::white()); + } + + // Periodically redraw the border line to fix any damage because it's a pain in the ass + static uint32_t border_redraw_counter = 0; + if (++border_redraw_counter >= 60) { // Once per second - might make less if causing flickering + border_redraw_counter = 0; + painter.draw_hline({0, 49}, 240, 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; + if (blink_state) { + painter.draw_string({prompt_x, 260}, style, "PRESS SELECT"); + } else { + painter.fill_rectangle({prompt_x, 260, 16 * 8, 20}, Color::black()); + } + } + } else if (game_state == 3) { + // Wave complete state - wait for timer + if (--wave_complete_timer <= 0) { + // Timer expired, start next wave + game_state = 1; + + // Clear and redraw game area - preserve border line + painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line + + // 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)); + + // Redraw the border line in case it was damaged again + painter.draw_hline({0, 49}, 240, Color::white()); + + // Initialize new wave of invaders + init_invaders(); + draw_all_invaders(); + draw_player(); + } + } +} + +SpaceInvadersView::SpaceInvadersView(NavigationView& nav) + : nav_{nav} { + add_children({&dummy, &button_difficulty}); + current_instance = this; + 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"); + // Settings will be saved when the view is destroyed + }; +} + +SpaceInvadersView::~SpaceInvadersView() { + // Settings are automatically saved when destroyed + 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 = 107; + 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) { + if (delta > 0) { + player_x += 5; + if (player_x > 214) player_x = 214; + draw_player(); + } else if (delta < 0) { + player_x -= 5; + 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, 240, 320}, Color::black()); + painter.draw_hline({0, 49}, 240, 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 diff --git a/firmware/application/external/spaceinv/ui_spaceinv.hpp b/firmware/application/external/spaceinv/ui_spaceinv.hpp new file mode 100644 index 000000000..0aee4c0c9 --- /dev/null +++ b/firmware/application/external/spaceinv/ui_spaceinv.hpp @@ -0,0 +1,79 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#ifndef __UI_SPACEINV_H__ +#define __UI_SPACEINV_H__ + +#include "ui.hpp" +#include "ui_navigation.hpp" +#include "event_m0.hpp" +#include "message.hpp" +#include "irq_controls.hpp" +#include "random.hpp" +#include "lpc43xx_cpp.hpp" +#include "ui_widget.hpp" +#include "app_settings.hpp" + +namespace ui::external_app::spaceinv { + +using Callback = void (*)(void); + +class Ticker { + public: + Ticker() = default; + void attach(Callback func, double delay_sec); + void detach(); +}; + +void check_game_timer(); +void game_timer_check(); + +class SpaceInvadersView : public View { + public: + SpaceInvadersView(NavigationView& nav); + ~SpaceInvadersView(); // Destructor will trigger settings save + void on_show() override; + + std::string title() const override { return "Space Invaders"; } + + void focus() override { dummy.focus(); } + void paint(Painter& painter) override; + void frame_sync(); + bool on_encoder(const EncoderEvent event) override; + bool on_key(KeyEvent key) override; + + uint32_t highScore = 0; + bool easy_mode = false; + + private: + NavigationView& nav_; + + Button button_difficulty{ + {70, 285, 100, 20}, + "Mode: HARD"}; + + app_settings::SettingsManager settings_{ + "spaceinv", + app_settings::Mode::NO_RF, + {{"highscore"sv, &highScore}, + {"easy_mode"sv, &easy_mode}}}; + + Button dummy{ + {240, 0, 0, 0}, + ""}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { + this->frame_sync(); + }}; +}; + +} // namespace ui::external_app::spaceinv + +#endif /* __UI_SPACEINV_H__ */