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:
RocketGod 2025-10-09 13:30:30 -07:00 committed by GitHub
parent db324a5225
commit 1d8f53b49e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 168 additions and 151 deletions

View file

@ -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

View file

@ -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__ */