mirror of
https://github.com/eried/portapack-mayhem.git
synced 2025-11-19 19:42:24 -05:00
* 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
896 lines
No EOL
31 KiB
C++
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
|