From 477d035dce57b7f3a6fdc469768576cc85f14f2d Mon Sep 17 00:00:00 2001 From: RocketGod <57732082+RocketGod-git@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:31:26 -0700 Subject: [PATCH] Dino Game Dynamic Screen for PortaRF plus sprite animation improvements (#2811) * Dino Game Dynamic Screen for PortaRF plus sprite animation improvements Dino Game Dynamic Screen for PortaRF plus sprite animation improvements * Format code per usual --- .../external/dinogame/ui_dinogame.cpp | 200 +++++++++++------- .../external/dinogame/ui_dinogame.hpp | 59 +++--- 2 files changed, 155 insertions(+), 104 deletions(-) diff --git a/firmware/application/external/dinogame/ui_dinogame.cpp b/firmware/application/external/dinogame/ui_dinogame.cpp index 0734ed735..20a108e57 100644 --- a/firmware/application/external/dinogame/ui_dinogame.cpp +++ b/firmware/application/external/dinogame/ui_dinogame.cpp @@ -43,7 +43,7 @@ const Color pp_colors[] = { // Drawing functions void cls() { - painter.fill_rectangle({0, 0, portapack::display.width(), portapack::display.height()}, Color::black()); + painter.fill_rectangle({0, 0, ui::screen_width, ui::screen_height}, Color::black()); } void fillrect(int x1, int y1, int x2, int y2, int color) { @@ -100,6 +100,15 @@ DinoGameView::DinoGameView(NavigationView& nav) : nav_{nav}, bird_info{}, game_timer{} { add_children({&dummy, &button_difficulty}); current_instance = this; + + // Initialize dimensions first + init_dimensions(); + + // Now reposition button with proper centering + int button_y = SCREEN_HEIGHT - 100; + int button_x = (SCREEN_WIDTH - 100) / 2; + button_difficulty.set_parent_rect({button_x, button_y, 100, 20}); + game_timer.attach(&game_timer_check, 1.0 / 60.0); button_difficulty.on_select = [this](Button&) { @@ -126,6 +135,33 @@ void DinoGameView::frame_sync() { set_dirty(); } +void DinoGameView::init_dimensions() { + SCREEN_WIDTH = ui::screen_width; + SCREEN_HEIGHT = ui::screen_height; + + // Scale game area based on screen size + GAME_AREA_HEIGHT = (SCREEN_HEIGHT * 160) / 320; // Scale proportionally + GAME_AREA_TOP = SCREEN_HEIGHT / 4; + + // Calculate positions + DINO_Y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_HEIGHT; + DINO_DUCK_Y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_DUCK_HEIGHT; + BIRD_Y_UP = GAME_AREA_TOP + 20; + BIRD_Y_DOWN = GAME_AREA_TOP + (GAME_AREA_HEIGHT * 60) / 160; + + // Scale jump height + JUMP_MAX_HEIGHT = (GAME_AREA_HEIGHT * 70) / 160; + + // Scale distances based on screen width + MIN_OBSTACLE_DISTANCE = (SCREEN_WIDTH * 300) / 240; + MAX_OBSTACLE_DISTANCE = (SCREEN_WIDTH * 600) / 240; + + // Scale horizontal position + DINO_X = SCREEN_WIDTH / 8; + + last_dino_y = DINO_Y; +} + void DinoGameView::init_game() { game_state = GameState::MENU; menu_initialized = false; @@ -142,7 +178,7 @@ void DinoGameView::new_game() { duck_timer = 0; displayedGameOver = false; bird_info.inGame = false; - bird_info.x_offset = 320; + bird_info.x_offset = SCREEN_WIDTH; bird_info.y_offset = 0; bird_info.y_velocity = 0; jumping = false; @@ -181,9 +217,9 @@ void DinoGameView::show_menu() { painter.draw_string({UI_POS_X_CENTER(10), 60}, style_title, "DINO GAME"); // Draw instructions - painter.draw_string({UI_POS_X_CENTER(19), 130}, style, "SELECT: Jump/Start"); - painter.draw_string({UI_POS_X_CENTER(11), 150}, style, "DOWN: Duck"); - painter.draw_string({UI_POS_X_CENTER(17), 170}, style, "Avoid obstacles!"); + painter.draw_string({UI_POS_X_CENTER(19), 120}, style, "SELECT: Jump/Start"); + painter.draw_string({UI_POS_X_CENTER(11), 140}, style, "DOWN: Duck"); + painter.draw_string({UI_POS_X_CENTER(17), 160}, style, "Avoid obstacles!"); // Draw high score draw_high_score(); @@ -194,14 +230,21 @@ void DinoGameView::show_menu() { // Show difficulty button button_difficulty.hidden(false); - // Animate the menu dino - bool menu_run_frame = (blink_counter / 15) % 2; + // Animate the menu dino - use frame counter for smooth animation + bool menu_run_frame = (blink_counter / 10) % 2; - // Clear previous dino position - fillrect(103, 90, 103 + DINO_WIDTH, 90 + DINO_HEIGHT, Black); + int menu_dino_x = UI_POS_X_CENTER(DINO_WIDTH); + int menu_dino_y = 90; + + // Clear previous dino position - clear a bit extra to handle any artifacts + static bool last_menu_frame = false; + if (last_menu_frame != menu_run_frame || blink_counter == 0) { + fillrect(menu_dino_x - 5, menu_dino_y - 5, menu_dino_x + DINO_WIDTH + 5, menu_dino_y + DINO_HEIGHT + 5, Black); + last_menu_frame = menu_run_frame; + } // Draw animated dino - draw_dino_at(103, 90, false, menu_run_frame); + draw_dino_at(menu_dino_x, menu_dino_y, false, menu_run_frame); // Blinking start prompt auto style_prompt = *ui::Theme::getInstance()->fg_light; @@ -209,9 +252,10 @@ void DinoGameView::show_menu() { blink_counter = 0; blink_state = !blink_state; - painter.fill_rectangle({UI_POS_X_CENTER(17), 258, 130, 20}, Color::black()); + int prompt_y = SCREEN_HEIGHT - 60; + painter.fill_rectangle({UI_POS_X_CENTER(17), prompt_y - 2, 130, 20}, Color::black()); if (blink_state) { - painter.draw_string({UI_POS_X_CENTER(17), 260}, style_prompt, "* PRESS SELECT *"); + painter.draw_string({UI_POS_X_CENTER(17), prompt_y}, style_prompt, "* PRESS SELECT *"); } } } @@ -220,33 +264,30 @@ void DinoGameView::show_game_over() { if (!displayedGameOver) { displayedGameOver = true; - // Clear the last normal dino position - if (last_ducking) { - fillrect(DINO_X, last_dino_y, DINO_X + DINO_DUCK_WIDTH, last_dino_y + DINO_DUCK_HEIGHT, Black); - } else { - fillrect(DINO_X, last_dino_y, DINO_X + DINO_WIDTH, last_dino_y + DINO_HEIGHT, Black); - } + // Clear the entire play area to ensure clean display + fillrect(0, GAME_AREA_TOP, SCREEN_WIDTH, GAME_AREA_HEIGHT + 20, Black); - // Draw the game over dino sprite - draw_dino_sprite(DINO_X, DINO_Y, dino_gameover); + // Draw the game over dino sprite in the center + int gameover_dino_x = UI_POS_X_CENTER(DINO_WIDTH); + int gameover_dino_y = GAME_AREA_TOP + 40; + draw_dino_sprite(gameover_dino_x, gameover_dino_y, dino_gameover); auto style = *ui::Theme::getInstance()->fg_light; auto style_score = *ui::Theme::getInstance()->fg_medium; // Game over text - painter.draw_string({UI_POS_X_CENTER(10), 70}, style, "GAME OVER"); + painter.draw_string({UI_POS_X_CENTER(10), GAME_AREA_TOP + 90}, style, "GAME OVER"); // Show final score std::string score_text = "SCORE: " + score_to_string(score); - int score_x = (screen_width - score_text.length() * 8) / 2; - painter.draw_string({score_x, 90}, style_score, score_text); + painter.draw_string({UI_POS_X_CENTER(score_text.length()), GAME_AREA_TOP + 110}, style_score, score_text); - painter.draw_string({UI_POS_X_CENTER(16), 110}, style, "SELECT TO RETRY"); + painter.draw_string({UI_POS_X_CENTER(16), GAME_AREA_TOP + 130}, style, "SELECT TO RETRY"); // Update high score if (score > highScore) { highScore = score; - painter.draw_string({UI_POS_X_CENTER(16), 130}, style, "NEW HIGH SCORE!"); + painter.draw_string({UI_POS_X_CENTER(16), GAME_AREA_TOP + 150}, style, "NEW HIGH SCORE!"); } } } @@ -263,7 +304,7 @@ void DinoGameView::game_loop() { // Update ground animation ground_offset = (ground_offset + GAME_SPEED_BASE + speed_modifier) % 20; - // Clear only the game area (not the whole screen to reduce flicker) + // Draw ground draw_ground(); // Update and draw obstacles @@ -295,38 +336,38 @@ void DinoGameView::game_loop() { // Update run animation if (get_steps() % (10 - speed_modifier) == 0) runstate = !runstate; - // Draw dino with minimal redraw + // Draw dino with proper clearing int current_dino_y = jumping ? (DINO_Y - jumpHeight) : (ducking ? DINO_DUCK_Y : DINO_Y); - // Clear old dino position more precisely + // Only clear and redraw if something changed if (current_dino_y != last_dino_y || runstate != last_runstate || ducking != last_ducking) { - // Clear based on last state + // Clear old position with extra margin to prevent artifacts if (last_ducking) { - fillrect(DINO_X, last_dino_y, DINO_X + DINO_DUCK_WIDTH, last_dino_y + DINO_DUCK_HEIGHT, Black); + fillrect(DINO_X - 2, last_dino_y - 2, DINO_X + DINO_DUCK_WIDTH + 2, last_dino_y + DINO_DUCK_HEIGHT + 2, Black); } else { - fillrect(DINO_X, last_dino_y, DINO_X + DINO_WIDTH, last_dino_y + DINO_HEIGHT, Black); + fillrect(DINO_X - 2, last_dino_y - 2, DINO_X + DINO_WIDTH + 2, last_dino_y + DINO_HEIGHT + 2, Black); } - } - // Draw dino at new position - if (jumping) { - draw_dino_sprite(DINO_X, current_dino_y, dino_default); - } else if (ducking) { - if (runstate) - draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_leftstep); - else - draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_rightstep); - } else { - if (runstate) - draw_dino_sprite(DINO_X, current_dino_y, dino_leftstep); - else - draw_dino_sprite(DINO_X, current_dino_y, dino_rightstep); - } + // Draw dino at new position + if (jumping) { + draw_dino_sprite(DINO_X, current_dino_y, dino_default); + } else if (ducking) { + if (runstate) + draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_leftstep); + else + draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_rightstep); + } else { + if (runstate) + draw_dino_sprite(DINO_X, current_dino_y, dino_leftstep); + else + draw_dino_sprite(DINO_X, current_dino_y, dino_rightstep); + } - // Update last state - last_dino_y = current_dino_y; - last_ducking = ducking; - last_runstate = runstate; + // Update last state + last_dino_y = current_dino_y; + last_ducking = ducking; + last_runstate = runstate; + } // Check collisions check_collision(); @@ -346,14 +387,14 @@ void DinoGameView::draw_ground() { int ground_y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT; // Clear ground area - fillrect(0, ground_y, 320, ground_y + GROUND_HEIGHT, Black); + fillrect(0, ground_y, SCREEN_WIDTH, ground_y + GROUND_HEIGHT, Black); // Draw ground line - painter.draw_hline({0, ground_y}, 320, Color::white()); - painter.draw_hline({0, ground_y + 1}, 320, Color::dark_grey()); + painter.draw_hline({0, ground_y}, SCREEN_WIDTH, Color::white()); + painter.draw_hline({0, ground_y + 1}, SCREEN_WIDTH, Color::dark_grey()); // Draw ground texture - for (int x = -ground_offset; x < 320; x += 20) { + for (int x = -ground_offset; x < SCREEN_WIDTH; x += 20) { painter.draw_hline({x, ground_y + 3}, 10, Color::dark_grey()); painter.draw_hline({x + 5, ground_y + 5}, 5, Color::dark_grey()); } @@ -364,7 +405,7 @@ void DinoGameView::update_obstacles() { for (int i = 0; i < MAX_OBSTACLES; i++) { if (obstacles[i].active) { // Clear old position - if (obstacles[i].last_x != obstacles[i].x && obstacles[i].last_x < 320) { + if (obstacles[i].last_x != obstacles[i].x && obstacles[i].last_x < SCREEN_WIDTH) { clear_obstacle_area(obstacles[i].last_x, obstacles[i].width + 10, obstacles[i].height + 10); } @@ -394,8 +435,8 @@ void DinoGameView::update_obstacles() { for (int i = 0; i < MAX_OBSTACLES; i++) { if (!obstacles[i].active) { obstacles[i].active = true; - obstacles[i].x = 320; - obstacles[i].last_x = 320; + obstacles[i].x = SCREEN_WIDTH; + obstacles[i].last_x = SCREEN_WIDTH; obstacles[i].type = rand() % 4; // Set obstacle dimensions based on type @@ -495,7 +536,7 @@ void DinoGameView::manage_bird() { // Only spawn bird if no obstacles are too close bool obstacle_nearby = false; for (int i = 0; i < MAX_OBSTACLES; i++) { - if (obstacles[i].active && obstacles[i].x > 200) { + if (obstacles[i].active && obstacles[i].x > SCREEN_WIDTH * 0.8) { obstacle_nearby = true; break; } @@ -504,7 +545,7 @@ void DinoGameView::manage_bird() { // Randomly spawn bird if no nearby obstacles if (!obstacle_nearby && rand() % 400 == 0 && get_steps() > 100) { bird_info.inGame = true; - bird_info.x_offset = 320; + bird_info.x_offset = SCREEN_WIDTH; bird_info.y_offset = 0; bird_info.y_velocity = easy_mode ? 0 : (rand() % 3) - 1; // No vertical movement in easy mode bird_info.y_position = BirdPosition::DOWN; // Always spawn at standing height in easy mode @@ -514,16 +555,16 @@ void DinoGameView::manage_bird() { int base_y = (bird_info.y_position == BirdPosition::UP) ? BIRD_Y_UP : BIRD_Y_DOWN; int current_y = base_y + bird_info.y_offset; - // Clear previous bird position - if (last_bird_x < 320 && last_bird_x >= -BIRD_WIDTH) { - int clear_start_x = last_bird_x; - int clear_end_x = last_bird_x + BIRD_WIDTH; + // Clear previous bird position with extra margin + if (last_bird_x < SCREEN_WIDTH && last_bird_x >= -BIRD_WIDTH) { + int clear_start_x = last_bird_x - 2; + int clear_end_x = last_bird_x + BIRD_WIDTH + 2; if (clear_start_x < 0) clear_start_x = 0; - if (clear_end_x > 320) clear_end_x = 320; + if (clear_end_x > SCREEN_WIDTH) clear_end_x = SCREEN_WIDTH; if (clear_end_x > clear_start_x) { - fillrect(clear_start_x, last_bird_y, clear_end_x, last_bird_y + BIRD_HEIGHT, Black); + fillrect(clear_start_x, last_bird_y - 2, clear_end_x, last_bird_y + BIRD_HEIGHT + 2, Black); } } @@ -549,7 +590,6 @@ void DinoGameView::manage_bird() { } } else { // In easy mode, keep bird at perfect ducking height - // Bird should hit standing dino but miss ducking dino current_y = DINO_Y - 10; // Position bird just above ducking height } @@ -557,7 +597,7 @@ void DinoGameView::manage_bird() { if (get_steps() % 15 == 0) bird_info.flop = !bird_info.flop; // Draw bird only if any part is visible - if (bird_info.x_offset > -BIRD_WIDTH && bird_info.x_offset < 320) { + if (bird_info.x_offset > -BIRD_WIDTH && bird_info.x_offset < SCREEN_WIDTH) { if (bird_info.flop) { draw_bird_sprite(bird_info.x_offset, current_y, pterodactyl_upflop); } else { @@ -590,6 +630,7 @@ void DinoGameView::draw_dino_sprite(int x, int y, const uint16_t* sprite) { width = DINO_WIDTH; } + // Draw sprite efficiently with run-length optimization for (int dy = 0; dy < height; dy++) { int run_start = -1; uint16_t run_color = 0; @@ -623,16 +664,21 @@ void DinoGameView::draw_dino_sprite(int x, int y, const uint16_t* sprite) { } void DinoGameView::draw_bird_sprite(int x, int y, const uint16_t* sprite) { + // Draw sprite efficiently with clipping for (int dy = 0; dy < BIRD_HEIGHT; dy++) { int run_start = -1; uint16_t run_color = 0; for (int dx = 0; dx < BIRD_WIDTH; dx++) { // Skip pixels that would be off-screen - if (x + dx < 0 || x + dx >= 320) { + if (x + dx < 0 || x + dx >= SCREEN_WIDTH) { // End any current run - if (run_start != -1) { - painter.fill_rectangle({x + run_start, y + dy, dx - run_start, 1}, Color(run_color)); + if (run_start != -1 && x + run_start < SCREEN_WIDTH) { + int run_width = dx - run_start; + if (x + run_start + run_width > SCREEN_WIDTH) { + run_width = SCREEN_WIDTH - (x + run_start); + } + painter.fill_rectangle({x + run_start, y + dy, run_width, 1}, Color(run_color)); run_start = -1; } continue; @@ -660,10 +706,10 @@ void DinoGameView::draw_bird_sprite(int x, int y, const uint16_t* sprite) { } } - if (run_start != -1 && x + run_start < 320) { + if (run_start != -1 && x + run_start < SCREEN_WIDTH) { int width = BIRD_WIDTH - run_start; - if (x + run_start + width > 320) { - width = 320 - (x + run_start); + if (x + run_start + width > SCREEN_WIDTH) { + width = SCREEN_WIDTH - (x + run_start); } if (width > 0) { painter.fill_rectangle({x + run_start, y + dy, width, 1}, Color(run_color)); @@ -683,7 +729,7 @@ void DinoGameView::draw_dino_at(int x, int y, bool is_ducking, bool run_frame) { if (run_frame) { draw_dino_sprite(x, y, dino_leftstep); } else { - draw_dino_sprite(x, y, dino_default); + draw_dino_sprite(x, y, dino_rightstep); } } } @@ -743,8 +789,8 @@ void DinoGameView::draw_current_score() { void DinoGameView::draw_high_score() { auto style = *ui::Theme::getInstance()->fg_light; - painter.fill_rectangle({UI_POS_X_RIGHT(9), 28, UI_POS_WIDTH(9), 18}, Color::black()); - painter.draw_string({UI_POS_X_RIGHT(9), 30}, style, "HI " + score_to_string(highScore)); + painter.fill_rectangle({UI_POS_X_RIGHT(90), 28, 90, 18}, Color::black()); + painter.draw_string({UI_POS_X_RIGHT(90), 30}, style, "HI " + score_to_string(highScore)); } void DinoGameView::jump() { @@ -805,4 +851,4 @@ bool DinoGameView::on_encoder(const EncoderEvent delta) { return true; } -} // namespace ui::external_app::dinogame +} // namespace ui::external_app::dinogame \ No newline at end of file diff --git a/firmware/application/external/dinogame/ui_dinogame.hpp b/firmware/application/external/dinogame/ui_dinogame.hpp index 076336534..49a519084 100644 --- a/firmware/application/external/dinogame/ui_dinogame.hpp +++ b/firmware/application/external/dinogame/ui_dinogame.hpp @@ -41,29 +41,6 @@ enum { Black, }; -// Game constants -#define DINO_WIDTH 34 -#define DINO_HEIGHT 36 -#define DINO_DUCK_WIDTH 45 -#define DINO_DUCK_HEIGHT 22 -#define BIRD_WIDTH 34 -#define BIRD_HEIGHT 27 -#define GROUND_HEIGHT 10 -#define GAME_AREA_TOP 78 -#define GAME_AREA_HEIGHT 160 -#define DINO_X 30 -#define DINO_Y (GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_HEIGHT) -#define DINO_DUCK_Y (GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_DUCK_HEIGHT) -#define BIRD_Y_UP (GAME_AREA_TOP + 20) -#define BIRD_Y_DOWN (GAME_AREA_TOP + 60) -#define JUMP_MAX_HEIGHT 70 -#define JUMP_SPEED 3 -#define GAME_SPEED_BASE 3 -#define SPRITE_COLOR 0x528A -#define TRANSPARENT_COLOR 0xFFFF -#define MIN_OBSTACLE_DISTANCE 300 -#define MAX_OBSTACLE_DISTANCE 600 - // Game states enum class GameState { MENU, @@ -159,6 +136,33 @@ class DinoGameView : public View { bool initialized = false; NavigationView& nav_; + // Dynamic screen dimensions + int SCREEN_WIDTH = 240; + int SCREEN_HEIGHT = 320; + int DINO_WIDTH = 34; + int DINO_HEIGHT = 36; + int DINO_DUCK_WIDTH = 45; + int DINO_DUCK_HEIGHT = 22; + int BIRD_WIDTH = 34; + int BIRD_HEIGHT = 27; + int GROUND_HEIGHT = 10; + int GAME_AREA_TOP = 78; + int GAME_AREA_HEIGHT = 160; + int DINO_X = 30; + int DINO_Y = 0; // Will be calculated + int DINO_DUCK_Y = 0; // Will be calculated + int BIRD_Y_UP = 0; // Will be calculated + int BIRD_Y_DOWN = 0; // Will be calculated + int JUMP_MAX_HEIGHT = 70; + int JUMP_SPEED = 3; + int GAME_SPEED_BASE = 3; + int MIN_OBSTACLE_DISTANCE = 300; + int MAX_OBSTACLE_DISTANCE = 600; + + // Sprite constants + static constexpr uint16_t SPRITE_COLOR = 0x528A; + static constexpr uint16_t TRANSPARENT_COLOR = 0xFFFF; + // Game variables static constexpr uint8_t MAX_OBSTACLES = 1; SimpleObstacle obstacles[MAX_OBSTACLES]; @@ -185,7 +189,7 @@ class DinoGameView : public View { int32_t obstacle_spawn_timer = 0; // Position tracking for minimal redraw - int16_t last_dino_y = DINO_Y; + int16_t last_dino_y = 0; int16_t last_bird_x = -1; int16_t last_bird_y = -1; uint8_t duck_timer = 0; @@ -202,6 +206,7 @@ class DinoGameView : public View { Ticker game_timer; // Private methods + void init_dimensions(); void init_game(); void new_game(); void update_obstacles(); @@ -229,7 +234,7 @@ class DinoGameView : public View { bool easy_mode = false; Button button_difficulty{ - {UI_POS_X_CENTER(13), 195, 13 * 8, 20}, + {70, 195, 100, 20}, "Mode: HARD"}; app_settings::SettingsManager settings_{ @@ -239,7 +244,7 @@ class DinoGameView : public View { {"easy_mode"sv, &easy_mode}}}; Button dummy{ - {screen_width, 0, 0, 0}, + {240, 0, 0, 0}, ""}; MessageHandlerRegistration message_handler_frame_sync{ @@ -251,4 +256,4 @@ class DinoGameView : public View { } // namespace ui::external_app::dinogame -#endif /* __UI_DINOGAME_H__ */ +#endif /* __UI_DINOGAME_H__ */ \ No newline at end of file