mirror of
https://github.com/eried/portapack-mayhem.git
synced 2025-11-18 19:20:43 -05:00
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
This commit is contained in:
parent
db324a5225
commit
1d8f53b49e
2 changed files with 168 additions and 151 deletions
|
|
@ -10,22 +10,27 @@
|
||||||
|
|
||||||
namespace ui::external_app::spaceinv {
|
namespace ui::external_app::spaceinv {
|
||||||
|
|
||||||
// Game constants
|
// Dynamic game constants
|
||||||
#define PLAYER_WIDTH 26
|
static int SCREEN_WIDTH = 240;
|
||||||
#define PLAYER_HEIGHT 16
|
static int SCREEN_HEIGHT = 320;
|
||||||
#define PLAYER_Y 280
|
static int PLAYER_WIDTH = 26;
|
||||||
#define BULLET_WIDTH 3
|
static int PLAYER_HEIGHT = 16;
|
||||||
#define BULLET_HEIGHT 10
|
static int PLAYER_Y = 280;
|
||||||
#define BULLET_SPEED 8
|
static int BULLET_WIDTH = 3;
|
||||||
#define MAX_BULLETS 3
|
static int BULLET_HEIGHT = 10;
|
||||||
#define INVADER_WIDTH 20
|
static int BULLET_SPEED = 8;
|
||||||
#define INVADER_HEIGHT 16
|
static int MAX_BULLETS = 3;
|
||||||
#define INVADER_ROWS 5 // Classic 5 rows
|
static int INVADER_WIDTH = 20;
|
||||||
#define INVADER_COLS 6 // Reduced for more movement space on narrow PP screen
|
static int INVADER_HEIGHT = 16;
|
||||||
#define INVADER_GAP_X 10
|
static int INVADER_ROWS = 5;
|
||||||
#define INVADER_GAP_Y 8 // Gap between invaders
|
static int INVADER_COLS = 6;
|
||||||
#define MAX_ENEMY_BULLETS 3
|
static int INVADER_GAP_X = 10;
|
||||||
#define ENEMY_BULLET_SPEED 3
|
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
|
// Game state
|
||||||
static int game_state = 0; // 0=menu, 1=playing, 2=game over, 3=wave_complete
|
static int game_state = 0; // 0=menu, 1=playing, 2=game over, 3=wave_complete
|
||||||
|
|
@ -38,18 +43,18 @@ static uint32_t speed_bonus = 0;
|
||||||
static uint32_t wave_complete_timer = 0;
|
static uint32_t wave_complete_timer = 0;
|
||||||
|
|
||||||
// Cannon Bullets
|
// Cannon Bullets
|
||||||
static int bullet_x[MAX_BULLETS] = {0, 0, 0};
|
static int bullet_x[3] = {0, 0, 0};
|
||||||
static int bullet_y[MAX_BULLETS] = {0, 0, 0};
|
static int bullet_y[3] = {0, 0, 0};
|
||||||
static bool bullet_active[MAX_BULLETS] = {false, false, false};
|
static bool bullet_active[3] = {false, false, false};
|
||||||
|
|
||||||
// Enemy bullets
|
// Enemy bullets
|
||||||
static int enemy_bullet_x[MAX_ENEMY_BULLETS] = {0, 0, 0};
|
static int enemy_bullet_x[3] = {0, 0, 0};
|
||||||
static int enemy_bullet_y[MAX_ENEMY_BULLETS] = {0, 0, 0};
|
static int enemy_bullet_y[3] = {0, 0, 0};
|
||||||
static bool enemy_bullet_active[MAX_ENEMY_BULLETS] = {false, false, false};
|
static bool enemy_bullet_active[3] = {false, false, false};
|
||||||
static uint32_t enemy_fire_counter = 0;
|
static uint32_t enemy_fire_counter = 0;
|
||||||
|
|
||||||
// Invaders
|
// Invaders
|
||||||
static bool invaders[INVADER_ROWS][INVADER_COLS];
|
static bool invaders[5][11]; // Max possible columns
|
||||||
static int invaders_x = 40;
|
static int invaders_x = 40;
|
||||||
static int invaders_y = 60;
|
static int invaders_y = 60;
|
||||||
static int invader_direction = 1;
|
static int invader_direction = 1;
|
||||||
|
|
@ -94,15 +99,47 @@ void Ticker::detach() {
|
||||||
game_update_callback = nullptr;
|
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() {
|
static void init_invaders() {
|
||||||
// Initialize invader array
|
// Initialize invader array based on dynamic columns
|
||||||
for (int row = 0; row < INVADER_ROWS; row++) {
|
for (int row = 0; row < INVADER_ROWS; row++) {
|
||||||
for (int col = 0; col < INVADER_COLS; col++) {
|
for (int col = 0; col < INVADER_COLS; col++) {
|
||||||
invaders[row][col] = true;
|
invaders[row][col] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
invaders_x = 40;
|
|
||||||
invaders_y = 60;
|
// 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_direction = 1;
|
||||||
invader_move_counter = 0;
|
invader_move_counter = 0;
|
||||||
|
|
||||||
|
|
@ -125,138 +162,114 @@ static void save_high_score() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void init_menu() {
|
static void init_menu() {
|
||||||
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
|
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
|
||||||
|
|
||||||
auto style_green = *ui::Theme::getInstance()->fg_green;
|
auto style_green = *ui::Theme::getInstance()->fg_green;
|
||||||
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
|
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
|
||||||
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
|
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
|
||||||
|
auto style_red = *ui::Theme::getInstance()->fg_red;
|
||||||
|
|
||||||
int16_t screen_width = 240;
|
// Title section
|
||||||
int16_t title_x = (screen_width - 15 * 8) / 2;
|
painter.draw_string({UI_POS_X_CENTER(15), 30}, style_green, "SPACE INVADERS");
|
||||||
int16_t divider_width = 24 * 8;
|
painter.draw_string({UI_POS_X_CENTER(24), 55}, style_yellow, "========================");
|
||||||
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());
|
// Instructions box
|
||||||
painter.draw_string({title_x, 42}, style_green, "SPACE INVADERS");
|
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({divider_x, 70}, style_yellow, "========================");
|
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!");
|
||||||
|
|
||||||
painter.fill_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::black());
|
// Point values
|
||||||
painter.draw_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::white());
|
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");
|
||||||
|
|
||||||
painter.draw_string({instruction_x, 110}, style_cyan, " ROTARY: MOVE SHIP");
|
// High score
|
||||||
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) {
|
if (current_instance) {
|
||||||
auto style_white = *ui::Theme::getInstance()->fg_light;
|
auto style_white = *ui::Theme::getInstance()->fg_light;
|
||||||
std::string high_score_text = "HIGH SCORE: " + std::to_string(current_instance->highScore);
|
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({UI_POS_X_CENTER(high_score_text.length()), 225}, style_white, high_score_text);
|
||||||
painter.draw_string({high_score_x, 240}, style_white, high_score_text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set prompt position (will be used for blinking text)
|
||||||
|
prompt_x = UI_POS_X_CENTER(12);
|
||||||
|
|
||||||
menu_initialized = true;
|
menu_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void init_game_over() {
|
static void init_game_over() {
|
||||||
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
|
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
|
||||||
|
|
||||||
auto style_red = *ui::Theme::getInstance()->fg_red;
|
auto style_red = *ui::Theme::getInstance()->fg_red;
|
||||||
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
|
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
|
||||||
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
|
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
|
||||||
auto style_white = *ui::Theme::getInstance()->fg_light;
|
auto style_white = *ui::Theme::getInstance()->fg_light;
|
||||||
|
|
||||||
int16_t screen_width = 240;
|
|
||||||
|
|
||||||
// Game Over title
|
// Game Over title
|
||||||
int16_t title_x = (screen_width - 9 * 8) / 2;
|
painter.draw_string({UI_POS_X_CENTER(9), 40}, style_red, "GAME OVER");
|
||||||
painter.draw_string({title_x, 42}, style_red, "GAME OVER");
|
painter.draw_string({UI_POS_X_CENTER(24), 65}, style_yellow, "========================");
|
||||||
|
|
||||||
// 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
|
// Score box
|
||||||
int16_t box_width = 22 * 8;
|
int box_width = 22 * 8;
|
||||||
int16_t box_x = (screen_width - box_width) / 2;
|
int box_x = UI_POS_X_CENTER(22);
|
||||||
painter.fill_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::black());
|
painter.fill_rectangle({box_x - 5, 90, box_width + 10, 80}, Color::black());
|
||||||
painter.draw_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::white());
|
painter.draw_rectangle({box_x - 5, 90, box_width + 10, 80}, Color::white());
|
||||||
|
|
||||||
// Display scores
|
// Display scores
|
||||||
std::string score_text = "SCORE: " + std::to_string(score);
|
std::string score_text = "SCORE: " + std::to_string(score);
|
||||||
int16_t score_x = (screen_width - score_text.length() * 8) / 2;
|
painter.draw_string({UI_POS_X_CENTER(score_text.length()), 110}, style_cyan, score_text);
|
||||||
painter.draw_string({score_x, 120}, style_cyan, score_text);
|
|
||||||
|
|
||||||
std::string wave_text = "WAVE: " + std::to_string(wave);
|
std::string wave_text = "WAVE: " + std::to_string(wave);
|
||||||
int16_t wave_x = (screen_width - wave_text.length() * 8) / 2;
|
painter.draw_string({UI_POS_X_CENTER(wave_text.length()), 130}, style_cyan, wave_text);
|
||||||
painter.draw_string({wave_x, 140}, style_cyan, wave_text);
|
|
||||||
|
|
||||||
// High score section
|
// High score section
|
||||||
if (current_instance) {
|
if (current_instance) {
|
||||||
if (score > current_instance->highScore) {
|
if (score > current_instance->highScore) {
|
||||||
// New high score!
|
// New high score!
|
||||||
std::string new_high = "NEW HIGH SCORE!";
|
painter.draw_string({UI_POS_X_CENTER(15), 190}, style_yellow, "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);
|
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({UI_POS_X_CENTER(high_score_text.length()), 210}, style_yellow, high_score_text);
|
||||||
painter.draw_string({high_score_x, 220}, style_yellow, high_score_text);
|
|
||||||
} else {
|
} else {
|
||||||
// Show existing high score
|
// Show existing high score
|
||||||
std::string high_label = "HIGH SCORE:";
|
painter.draw_string({UI_POS_X_CENTER(11), 190}, style_white, "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);
|
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({UI_POS_X_CENTER(high_score_text.length()), 210}, style_white, high_score_text);
|
||||||
painter.draw_string({high_score_x, 220}, style_white, high_score_text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set prompt position for blinking text
|
||||||
|
prompt_x = UI_POS_X_CENTER(12);
|
||||||
game_over_initialized = true;
|
game_over_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void draw_player() {
|
static void draw_player() {
|
||||||
// Clear the entire player area
|
// Clear the entire player area
|
||||||
painter.fill_rectangle({0, PLAYER_Y - 4, 240, PLAYER_HEIGHT + 8}, Color::black());
|
painter.fill_rectangle({0, PLAYER_Y - 4, SCREEN_WIDTH, PLAYER_HEIGHT + 8}, Color::black());
|
||||||
|
|
||||||
// Draw the classic Space Invaders cannon
|
// Draw the classic Space Invaders cannon
|
||||||
Color green = Color::green();
|
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
|
// Top part - the cannon barrel
|
||||||
painter.draw_hline({player_x + 12, PLAYER_Y}, 2, green);
|
painter.fill_rectangle({player_x + 12 * unit_x, PLAYER_Y, 2 * unit_x, 2 * unit_y}, green);
|
||||||
painter.draw_hline({player_x + 12, PLAYER_Y + 1}, 2, green);
|
|
||||||
|
|
||||||
// Upper body
|
// Upper body
|
||||||
painter.draw_hline({player_x + 11, PLAYER_Y + 2}, 4, green);
|
painter.fill_rectangle({player_x + 11 * unit_x, PLAYER_Y + 2 * unit_y, 4 * unit_x, 2 * unit_y}, green);
|
||||||
painter.draw_hline({player_x + 11, PLAYER_Y + 3}, 4, green);
|
painter.fill_rectangle({player_x + 10 * unit_x, PLAYER_Y + 4 * unit_y, 6 * unit_x, 2 * unit_y}, 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
|
// Main body
|
||||||
painter.draw_hline({player_x + 2, PLAYER_Y + 6}, 22, green);
|
painter.fill_rectangle({player_x + 2 * unit_x, PLAYER_Y + 6 * unit_y, 22 * unit_x, 10 * unit_y}, green);
|
||||||
painter.draw_hline({player_x + 2, PLAYER_Y + 7}, 22, green);
|
painter.fill_rectangle({player_x + unit_x, PLAYER_Y + 8 * unit_y, 24 * unit_x, 8 * unit_y}, green);
|
||||||
painter.draw_hline({player_x + 1, PLAYER_Y + 8}, 24, green);
|
painter.fill_rectangle({player_x, PLAYER_Y + 10 * unit_y, 26 * unit_x, 6 * unit_y}, 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) {
|
static void draw_invader(int row, int col) {
|
||||||
|
|
@ -452,14 +465,14 @@ static void fire_enemy_bullet() {
|
||||||
static void update_enemy_bullets() {
|
static void update_enemy_bullets() {
|
||||||
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
|
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
|
||||||
if (enemy_bullet_active[i]) {
|
if (enemy_bullet_active[i]) {
|
||||||
// Clear old position - but protect the border line
|
// Clear old position
|
||||||
if (enemy_bullet_y[i] != 49) { // Don't clear if we're exactly on the border line
|
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());
|
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
|
||||||
}
|
}
|
||||||
|
|
||||||
enemy_bullet_y[i] += ENEMY_BULLET_SPEED;
|
enemy_bullet_y[i] += ENEMY_BULLET_SPEED;
|
||||||
|
|
||||||
if (enemy_bullet_y[i] > 320) {
|
if (enemy_bullet_y[i] > SCREEN_HEIGHT) {
|
||||||
enemy_bullet_active[i] = false;
|
enemy_bullet_active[i] = false;
|
||||||
} else {
|
} else {
|
||||||
// Draw at new position
|
// Draw at new position
|
||||||
|
|
@ -498,7 +511,7 @@ static void update_invaders() {
|
||||||
// Clear old positions
|
// Clear old positions
|
||||||
clear_all_invaders();
|
clear_all_invaders();
|
||||||
|
|
||||||
// Check bounds - find the actual leftmost and rightmost invaders
|
// Check bounds
|
||||||
int leftmost = INVADER_COLS;
|
int leftmost = INVADER_COLS;
|
||||||
int rightmost = -1;
|
int rightmost = -1;
|
||||||
|
|
||||||
|
|
@ -513,25 +526,25 @@ static void update_invaders() {
|
||||||
|
|
||||||
bool hit_edge = false;
|
bool hit_edge = false;
|
||||||
if (invader_direction > 0) {
|
if (invader_direction > 0) {
|
||||||
// Moving right - check rightmost invader
|
// Moving right - check rightmost invader with margin
|
||||||
int right_edge = invaders_x + rightmost * (INVADER_WIDTH + INVADER_GAP_X) + INVADER_WIDTH;
|
int right_edge = invaders_x + rightmost * (INVADER_WIDTH + INVADER_GAP_X) + INVADER_WIDTH;
|
||||||
if (right_edge >= 240) {
|
if (right_edge >= SCREEN_WIDTH - 5) { // 5px margin
|
||||||
hit_edge = true;
|
hit_edge = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Moving left - check leftmost invader
|
// Moving left - check leftmost invader with margin
|
||||||
int left_edge = invaders_x + leftmost * (INVADER_WIDTH + INVADER_GAP_X);
|
int left_edge = invaders_x + leftmost * (INVADER_WIDTH + INVADER_GAP_X);
|
||||||
if (left_edge <= 0) {
|
if (left_edge <= 5) { // 5px margin
|
||||||
hit_edge = true;
|
hit_edge = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move invaders
|
// Move invaders
|
||||||
if (hit_edge) {
|
if (hit_edge) {
|
||||||
invaders_y += 15; // Move down
|
invaders_y += INVADER_DROP_AMOUNT;
|
||||||
invader_direction = -invader_direction;
|
invader_direction = -invader_direction;
|
||||||
} else {
|
} else {
|
||||||
invaders_x += invader_direction * 8; // Horizontal movement
|
invaders_x += invader_direction * INVADER_MOVE_AMOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle animation frame
|
// Toggle animation frame
|
||||||
|
|
@ -541,7 +554,7 @@ static void update_invaders() {
|
||||||
draw_all_invaders();
|
draw_all_invaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enemy firing logic - only in hard mode or always with lower frequency
|
// Enemy firing logic
|
||||||
if (!current_instance || !current_instance->easy_mode) {
|
if (!current_instance || !current_instance->easy_mode) {
|
||||||
if (++enemy_fire_counter >= 120) { // Fire every 2 seconds at 60fps
|
if (++enemy_fire_counter >= 120) { // Fire every 2 seconds at 60fps
|
||||||
enemy_fire_counter = 0;
|
enemy_fire_counter = 0;
|
||||||
|
|
@ -568,7 +581,7 @@ static void update_bullets() {
|
||||||
|
|
||||||
bullet_y[i] -= BULLET_SPEED;
|
bullet_y[i] -= BULLET_SPEED;
|
||||||
|
|
||||||
if (bullet_y[i] < 50) {
|
if (bullet_y[i] < INFO_BAR_HEIGHT) {
|
||||||
bullet_active[i] = false;
|
bullet_active[i] = false;
|
||||||
} else {
|
} else {
|
||||||
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::white());
|
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::white());
|
||||||
|
|
@ -600,7 +613,7 @@ static void check_collisions() {
|
||||||
invaders[row][col] = false;
|
invaders[row][col] = false;
|
||||||
painter.fill_rectangle({inv_x, inv_y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
|
painter.fill_rectangle({inv_x, inv_y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
|
||||||
|
|
||||||
// Score based on row: top=30, middle=20, bottom=10
|
// Score based on row
|
||||||
if (row == 0) {
|
if (row == 0) {
|
||||||
score += 30;
|
score += 30;
|
||||||
} else if (row == 1 || row == 2) {
|
} else if (row == 1 || row == 2) {
|
||||||
|
|
@ -635,23 +648,20 @@ static void check_wave_complete() {
|
||||||
speed_bonus = (wave - 1) * 3; // Each wave is slightly faster
|
speed_bonus = (wave - 1) * 3; // Each wave is slightly faster
|
||||||
if (speed_bonus > 15) speed_bonus = 15; // Cap the speed increase
|
if (speed_bonus > 15) speed_bonus = 15; // Cap the speed increase
|
||||||
|
|
||||||
// Clear any enemy bullets before transitioning
|
// Clear any enemy bullets
|
||||||
clear_all_enemy_bullets();
|
clear_all_enemy_bullets();
|
||||||
|
|
||||||
// Set state to wave complete and start timer
|
// Set state to wave complete
|
||||||
game_state = 3;
|
game_state = 3;
|
||||||
wave_complete_timer = 60; // 1 second at 60fps
|
wave_complete_timer = 60; // 1 second
|
||||||
|
|
||||||
// Clear screen and show wave message - but preserve the border line
|
// Clear screen and show wave message
|
||||||
painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line at 49
|
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());
|
||||||
// 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;
|
auto style = *ui::Theme::getInstance()->fg_green;
|
||||||
std::string wave_text = "WAVE " + std::to_string(wave);
|
std::string wave_text = "WAVE " + std::to_string(wave);
|
||||||
int wave_x = (240 - wave_text.length() * 8) / 2;
|
painter.draw_string({UI_POS_X_CENTER(wave_text.length()), SCREEN_HEIGHT / 2}, style, wave_text);
|
||||||
painter.draw_string({wave_x, 150}, style, wave_text);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -661,8 +671,8 @@ static void check_wave_complete() {
|
||||||
for (int col = 0; col < INVADER_COLS; col++) {
|
for (int col = 0; col < INVADER_COLS; col++) {
|
||||||
if (invaders[row][col]) {
|
if (invaders[row][col]) {
|
||||||
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
|
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
|
||||||
if (y + INVADER_HEIGHT >= PLAYER_Y) { // Actual collision with player
|
if (y + INVADER_HEIGHT >= PLAYER_Y) {
|
||||||
game_state = 2; // Game over
|
game_state = 2; // Game over
|
||||||
save_high_score();
|
save_high_score();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -683,10 +693,12 @@ void game_timer_check() {
|
||||||
blink_state = !blink_state;
|
blink_state = !blink_state;
|
||||||
|
|
||||||
auto style = *ui::Theme::getInstance()->fg_red;
|
auto style = *ui::Theme::getInstance()->fg_red;
|
||||||
|
int blink_y = 250; // Position below the high score
|
||||||
|
|
||||||
if (blink_state) {
|
if (blink_state) {
|
||||||
painter.draw_string({prompt_x, 260}, style, "PRESS SELECT");
|
painter.draw_string({prompt_x, blink_y}, style, "PRESS SELECT");
|
||||||
} else {
|
} else {
|
||||||
painter.fill_rectangle({prompt_x, 260, 16 * 8, 20}, Color::black());
|
painter.fill_rectangle({prompt_x, blink_y, 12 * 8, 20}, Color::black());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (game_state == 1) {
|
} else if (game_state == 1) {
|
||||||
|
|
@ -704,16 +716,14 @@ void game_timer_check() {
|
||||||
auto style = *ui::Theme::getInstance()->fg_green;
|
auto style = *ui::Theme::getInstance()->fg_green;
|
||||||
painter.fill_rectangle({5, 10, 100, 20}, Color::black());
|
painter.fill_rectangle({5, 10, 100, 20}, Color::black());
|
||||||
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
|
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
|
||||||
|
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
|
||||||
// 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
|
// Periodically redraw the border line
|
||||||
static uint32_t border_redraw_counter = 0;
|
static uint32_t border_redraw_counter = 0;
|
||||||
if (++border_redraw_counter >= 60) { // Once per second - might make less if causing flickering
|
if (++border_redraw_counter >= 60) {
|
||||||
border_redraw_counter = 0;
|
border_redraw_counter = 0;
|
||||||
painter.draw_hline({0, 49}, 240, Color::white());
|
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
|
||||||
}
|
}
|
||||||
} else if (game_state == 2) {
|
} else if (game_state == 2) {
|
||||||
// Game over state
|
// Game over state
|
||||||
|
|
@ -726,30 +736,29 @@ void game_timer_check() {
|
||||||
blink_state = !blink_state;
|
blink_state = !blink_state;
|
||||||
|
|
||||||
auto style = *ui::Theme::getInstance()->fg_red;
|
auto style = *ui::Theme::getInstance()->fg_red;
|
||||||
|
int blink_y = 250; // Position below high score section
|
||||||
|
|
||||||
if (blink_state) {
|
if (blink_state) {
|
||||||
painter.draw_string({prompt_x, 260}, style, "PRESS SELECT");
|
painter.draw_string({prompt_x, blink_y}, style, "PRESS SELECT");
|
||||||
} else {
|
} else {
|
||||||
painter.fill_rectangle({prompt_x, 260, 16 * 8, 20}, Color::black());
|
painter.fill_rectangle({prompt_x, blink_y, 12 * 8, 20}, Color::black());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (game_state == 3) {
|
} else if (game_state == 3) {
|
||||||
// Wave complete state - wait for timer
|
// Wave complete state - wait for timer
|
||||||
if (--wave_complete_timer <= 0) {
|
if (--wave_complete_timer <= 0) {
|
||||||
// Timer expired, start next wave
|
|
||||||
game_state = 1;
|
game_state = 1;
|
||||||
|
|
||||||
// Clear and redraw game area - preserve border line
|
// Clear and redraw game area
|
||||||
painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line
|
painter.fill_rectangle({0, INFO_BAR_HEIGHT + 1, SCREEN_WIDTH, SCREEN_HEIGHT - INFO_BAR_HEIGHT - 1}, Color::black());
|
||||||
|
|
||||||
// Restore UI
|
// Restore UI
|
||||||
auto style = *ui::Theme::getInstance()->fg_green;
|
auto style = *ui::Theme::getInstance()->fg_green;
|
||||||
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
|
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
|
||||||
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
|
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
|
||||||
|
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
|
||||||
|
|
||||||
// Redraw the border line in case it was damaged again
|
// Initialize new wave
|
||||||
painter.draw_hline({0, 49}, 240, Color::white());
|
|
||||||
|
|
||||||
// Initialize new wave of invaders
|
|
||||||
init_invaders();
|
init_invaders();
|
||||||
draw_all_invaders();
|
draw_all_invaders();
|
||||||
draw_player();
|
draw_player();
|
||||||
|
|
@ -760,7 +769,16 @@ void game_timer_check() {
|
||||||
SpaceInvadersView::SpaceInvadersView(NavigationView& nav)
|
SpaceInvadersView::SpaceInvadersView(NavigationView& nav)
|
||||||
: nav_{nav} {
|
: nav_{nav} {
|
||||||
add_children({&dummy, &button_difficulty});
|
add_children({&dummy, &button_difficulty});
|
||||||
|
|
||||||
|
// Initialize dimensions first
|
||||||
current_instance = this;
|
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);
|
game_timer.attach(&game_timer_check, 1.0 / 60.0);
|
||||||
|
|
||||||
// Update button text based on loaded setting
|
// Update button text based on loaded setting
|
||||||
|
|
@ -769,12 +787,10 @@ SpaceInvadersView::SpaceInvadersView(NavigationView& nav)
|
||||||
button_difficulty.on_select = [this](Button&) {
|
button_difficulty.on_select = [this](Button&) {
|
||||||
easy_mode = !easy_mode;
|
easy_mode = !easy_mode;
|
||||||
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
|
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
|
||||||
// Settings will be saved when the view is destroyed
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
SpaceInvadersView::~SpaceInvadersView() {
|
SpaceInvadersView::~SpaceInvadersView() {
|
||||||
// Settings are automatically saved when destroyed
|
|
||||||
current_instance = nullptr;
|
current_instance = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -791,7 +807,7 @@ void SpaceInvadersView::paint(Painter& painter) {
|
||||||
game_over_initialized = false;
|
game_over_initialized = false;
|
||||||
blink_state = true;
|
blink_state = true;
|
||||||
blink_counter = 0;
|
blink_counter = 0;
|
||||||
player_x = 107;
|
player_x = SCREEN_WIDTH / 2 - PLAYER_WIDTH / 2;
|
||||||
score = 0;
|
score = 0;
|
||||||
lives = 3;
|
lives = 3;
|
||||||
wave = 1;
|
wave = 1;
|
||||||
|
|
@ -821,12 +837,13 @@ void SpaceInvadersView::frame_sync() {
|
||||||
|
|
||||||
bool SpaceInvadersView::on_encoder(const EncoderEvent delta) {
|
bool SpaceInvadersView::on_encoder(const EncoderEvent delta) {
|
||||||
if (game_state == 1) {
|
if (game_state == 1) {
|
||||||
|
int move_speed = SCREEN_WIDTH / 48; // Scale movement speed
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
player_x += 5;
|
player_x += move_speed;
|
||||||
if (player_x > 214) player_x = 214;
|
if (player_x > SCREEN_WIDTH - PLAYER_WIDTH) player_x = SCREEN_WIDTH - PLAYER_WIDTH;
|
||||||
draw_player();
|
draw_player();
|
||||||
} else if (delta < 0) {
|
} else if (delta < 0) {
|
||||||
player_x -= 5;
|
player_x -= move_speed;
|
||||||
if (player_x < 0) player_x = 0;
|
if (player_x < 0) player_x = 0;
|
||||||
draw_player();
|
draw_player();
|
||||||
}
|
}
|
||||||
|
|
@ -850,8 +867,8 @@ bool SpaceInvadersView::on_key(const KeyEvent key) {
|
||||||
enemy_fire_counter = 0;
|
enemy_fire_counter = 0;
|
||||||
init_invaders();
|
init_invaders();
|
||||||
|
|
||||||
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
|
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
|
||||||
painter.draw_hline({0, 49}, 240, Color::white());
|
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
|
||||||
|
|
||||||
auto style = *ui::Theme::getInstance()->fg_green;
|
auto style = *ui::Theme::getInstance()->fg_green;
|
||||||
painter.draw_string({5, 10}, style, "Score: 0");
|
painter.draw_string({5, 10}, style, "Score: 0");
|
||||||
|
|
@ -876,4 +893,4 @@ bool SpaceInvadersView::on_key(const KeyEvent key) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui::external_app::spaceinv
|
} // namespace ui::external_app::spaceinv
|
||||||
|
|
@ -54,7 +54,7 @@ class SpaceInvadersView : public View {
|
||||||
NavigationView& nav_;
|
NavigationView& nav_;
|
||||||
|
|
||||||
Button button_difficulty{
|
Button button_difficulty{
|
||||||
{70, 285, 100, 20},
|
{70, 275, 100, 20},
|
||||||
"Mode: HARD"};
|
"Mode: HARD"};
|
||||||
|
|
||||||
app_settings::SettingsManager settings_{
|
app_settings::SettingsManager settings_{
|
||||||
|
|
@ -76,4 +76,4 @@ class SpaceInvadersView : public View {
|
||||||
|
|
||||||
} // namespace ui::external_app::spaceinv
|
} // namespace ui::external_app::spaceinv
|
||||||
|
|
||||||
#endif /* __UI_SPACEINV_H__ */
|
#endif /* __UI_SPACEINV_H__ */
|
||||||
Loading…
Add table
Add a link
Reference in a new issue