mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-06-29 21:52:03 -07:00
updated asteroids
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <storage/storage.h>
|
||||
#include <input/input.h>
|
||||
#include <gui/gui.h>
|
||||
#include <stdlib.h>
|
||||
@@ -10,24 +11,23 @@
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <math.h>
|
||||
|
||||
#ifndef PI
|
||||
#define PI 3.14159265358979f
|
||||
#endif
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#define TAG "Asteroids" // Used for logging
|
||||
#define DEBUG_MSG 1
|
||||
#define SCREEN_XRES 128
|
||||
#define SCREEN_YRES 64
|
||||
#define GAME_START_LIVES 3
|
||||
|
||||
/* The game uses the OK button both to fire and to accelerate the ship.
|
||||
* This makes it a lot more playable since the finger does not have to
|
||||
* move between two keys. However it is important that the extra time the
|
||||
* player needs to press the button to accelerate instead of just firing
|
||||
* is precisely selected to provide a smooth experience. After a few
|
||||
* attempts, it looks like 70 milliseconds is the right spot. */
|
||||
#define SHIP_ACCELERATION_KEYPRESS_TIME 70
|
||||
#define TTLBUL 30 /* Bullet time to live, in ticks. */
|
||||
#define MAXBUL 5 /* Max bullets on the screen. */
|
||||
#define MAXAST 32 /* Max asteroids on the screen. */
|
||||
#define SHIP_HIT_ANIMATION_LEN 15
|
||||
#define SAVING_DIRECTORY "/ext/apps/Games"
|
||||
#define SAVING_FILENAME "/ext/apps_data/asteroids/game_asteroids.save"
|
||||
#ifndef PI
|
||||
#define PI 3.14159265358979f
|
||||
#endif
|
||||
|
||||
/* ============================ Data structures ============================= */
|
||||
|
||||
@@ -36,7 +36,7 @@ typedef struct Ship {
|
||||
y, /* Ship y position. */
|
||||
vx, /* x velocity. */
|
||||
vy, /* y velocity. */
|
||||
rot; /* Current rotation. 2*PI full rotation. */
|
||||
rot; /* Current rotation. 2*PI full ortation. */
|
||||
} Ship;
|
||||
|
||||
typedef struct Bullet {
|
||||
@@ -51,21 +51,20 @@ typedef struct Asteroid {
|
||||
uint8_t shape_seed; /* Seed to give random shape. */
|
||||
} Asteroid;
|
||||
|
||||
#define MAXBUL 10 /* Max bullets on the screen. */
|
||||
#define MAXAST 32 /* Max asteroids on the screen. */
|
||||
#define SHIP_HIT_ANIMATION_LEN 15
|
||||
typedef struct AsteroidsApp {
|
||||
/* GUI */
|
||||
Gui* gui;
|
||||
ViewPort* view_port; /* We just use a raw viewport and we render
|
||||
everything into the low level canvas. */
|
||||
FuriMessageQueue* event_queue; /* Key press events go here. */
|
||||
FuriMessageQueue* event_queue; /* Keypress events go here. */
|
||||
|
||||
/* Game state. */
|
||||
int running; /* Once false exists the app. */
|
||||
bool gameover; /* Game over status. */
|
||||
bool gameover; /* Gameover status. */
|
||||
uint32_t ticks; /* Game ticks. Increments at each refresh. */
|
||||
uint32_t score; /* Game score. */
|
||||
uint32_t highscore; /* Highscore. Shown on Game Over Screen */
|
||||
bool is_new_highscore; /* Is the last score a new highscore? */
|
||||
uint32_t lives; /* Number of lives in the current game. */
|
||||
uint32_t ship_hit; /* When non zero, the ship was hit by an asteroid
|
||||
and we need to show an animation as long as
|
||||
@@ -90,10 +89,55 @@ typedef struct AsteroidsApp {
|
||||
bool fire; /* Short press detected: fire a bullet. */
|
||||
} AsteroidsApp;
|
||||
|
||||
/* ============================== Prototypes ================================ */
|
||||
const NotificationSequence sequence_thrusters = {
|
||||
&message_vibro_on,
|
||||
&message_delay_10,
|
||||
&message_vibro_off,
|
||||
NULL,
|
||||
};
|
||||
|
||||
const NotificationSequence sequence_brake = {
|
||||
&message_vibro_on,
|
||||
&message_delay_10,
|
||||
&message_delay_1,
|
||||
&message_delay_1,
|
||||
&message_vibro_off,
|
||||
NULL,
|
||||
};
|
||||
|
||||
const NotificationSequence sequence_crash = {
|
||||
&message_red_255,
|
||||
|
||||
&message_vibro_on,
|
||||
// &message_note_g5, // Play sound but currently disabled
|
||||
&message_delay_25,
|
||||
// &message_note_e5,
|
||||
&message_vibro_off,
|
||||
&message_sound_off,
|
||||
NULL,
|
||||
};
|
||||
|
||||
const NotificationSequence sequence_bullet_fired = {
|
||||
&message_vibro_on,
|
||||
// &message_note_g5, // Play sound but currently disabled. Need On/Off menu setting
|
||||
&message_delay_10,
|
||||
&message_delay_1,
|
||||
&message_delay_1,
|
||||
&message_delay_1,
|
||||
&message_delay_1,
|
||||
&message_delay_1,
|
||||
|
||||
// &message_note_e5,
|
||||
&message_vibro_off,
|
||||
&message_sound_off,
|
||||
NULL,
|
||||
};
|
||||
|
||||
/* ============================== Prototyeps ================================ */
|
||||
|
||||
// Only functions called before their definition are here.
|
||||
|
||||
bool load_game(AsteroidsApp* app);
|
||||
void save_game(AsteroidsApp* app);
|
||||
void restart_game_after_gameover(AsteroidsApp* app);
|
||||
uint32_t key_pressed_time(AsteroidsApp* app, InputKey key);
|
||||
|
||||
@@ -114,7 +158,7 @@ Poly ShipPoly = {{-3, 0, 3}, {-3, 6, -3}, 3};
|
||||
|
||||
Poly ShipFirePoly = {{-1.5, 0, 1.5}, {-3, -6, -3}, 3};
|
||||
|
||||
/* Rotate the point of the polygon 'poly' and store the new rotated
|
||||
/* Rotate the point of the poligon 'poly' and store the new rotated
|
||||
* polygon in 'rot'. The polygon is rotated by an angle 'a', with
|
||||
* center at 0,0. */
|
||||
void rotate_poly(Poly* rot, Poly* poly, float a) {
|
||||
@@ -165,7 +209,7 @@ void draw_bullet(Canvas* const canvas, Bullet* b) {
|
||||
/* Draw an asteroid. The asteroid shapes is computed on the fly and
|
||||
* is not stored in a permanent shape structure. In order to generate
|
||||
* the shape, we use an initial fixed shape that we resize according
|
||||
* to the asteroid size, perturbed according to the asteroid shape
|
||||
* to the asteroid size, perturbate according to the asteroid shape
|
||||
* seed, and finally draw it rotated of the right amount. */
|
||||
void draw_asteroid(Canvas* const canvas, Asteroid* ast) {
|
||||
Poly ap;
|
||||
@@ -241,8 +285,11 @@ void render_callback(Canvas* const canvas, void* ctx) {
|
||||
|
||||
/* Draw ship, asteroids, bullets. */
|
||||
draw_poly(canvas, &ShipPoly, app->ship.x, app->ship.y, app->ship.rot);
|
||||
if(key_pressed_time(app, InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME)
|
||||
|
||||
if(key_pressed_time(app, InputKeyUp) > 0) {
|
||||
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_thrusters);
|
||||
draw_poly(canvas, &ShipFirePoly, app->ship.x, app->ship.y, app->ship.rot);
|
||||
}
|
||||
|
||||
for(int j = 0; j < app->bullets_num; j++) draw_bullet(canvas, &app->bullets[j]);
|
||||
|
||||
@@ -252,6 +299,30 @@ void render_callback(Canvas* const canvas, void* ctx) {
|
||||
if(app->gameover) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
|
||||
// TODO: if new highscore, display blinking "New High Score"
|
||||
// Display High Score
|
||||
if(app->is_new_highscore) {
|
||||
canvas_draw_str(canvas, 22, 9, "New High Score!");
|
||||
} else {
|
||||
canvas_draw_str(canvas, 36, 9, "High Score");
|
||||
}
|
||||
|
||||
// Convert highscore to string
|
||||
int length = snprintf(NULL, 0, "%lu", app->highscore);
|
||||
char* str_high_score = malloc(length + 1);
|
||||
snprintf(str_high_score, length + 1, "%lu", app->highscore);
|
||||
|
||||
// Get length to center on screen
|
||||
int nDigits = 0;
|
||||
if(app->highscore > 0) {
|
||||
nDigits = floor(log10(app->highscore)) + 1;
|
||||
}
|
||||
|
||||
// Draw highscore centered
|
||||
canvas_draw_str(canvas, (SCREEN_XRES / 2) - (nDigits * 2), 20, str_high_score);
|
||||
free(str_high_score);
|
||||
|
||||
canvas_draw_str(canvas, 28, 35, "GAME OVER");
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, 25, 50, "Press OK to restart");
|
||||
@@ -295,6 +366,7 @@ bool objects_are_colliding(float x1, float y1, float r1, float x2, float y2, flo
|
||||
/* Create a new bullet headed in the same direction of the ship. */
|
||||
void ship_fire_bullet(AsteroidsApp* app) {
|
||||
if(app->bullets_num == MAXBUL) return;
|
||||
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_bullet_fired);
|
||||
Bullet* b = &app->bullets[app->bullets_num];
|
||||
b->x = app->ship.x;
|
||||
b->y = app->ship.y;
|
||||
@@ -316,7 +388,7 @@ void ship_fire_bullet(AsteroidsApp* app) {
|
||||
b->vx += app->ship.vx;
|
||||
b->vy += app->ship.vy;
|
||||
|
||||
b->ttl = 50; /* The bullet will disappear after N ticks. */
|
||||
b->ttl = TTLBUL; /* The bullet will disappear after N ticks. */
|
||||
app->bullets_num++;
|
||||
}
|
||||
|
||||
@@ -366,7 +438,7 @@ void remove_asteroid(AsteroidsApp* app, int id) {
|
||||
/* Called when an asteroid was reached by a bullet. The asteroid
|
||||
* hit is the one with the specified 'id'. */
|
||||
void asteroid_was_hit(AsteroidsApp* app, int id) {
|
||||
float sizelimit = 6; // Smaller than that, they disappear in one shot.
|
||||
float sizelimit = 6; // Smaller than that polverize in one shot.
|
||||
Asteroid* a = &app->asteroids[id];
|
||||
|
||||
/* Asteroid is large enough to break into fragments. */
|
||||
@@ -387,17 +459,19 @@ void asteroid_was_hit(AsteroidsApp* app, int id) {
|
||||
}
|
||||
} else {
|
||||
app->score++;
|
||||
if(app->score > app->highscore) {
|
||||
app->is_new_highscore = true;
|
||||
app->highscore = app->score; // Show on Game Over Screen and future main menu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Set game over state. When in game-over mode, the game displays a
|
||||
* game over text with a background of many asteroids floating around. */
|
||||
/* Set gameover state. When in game-over mode, the game displays a gameover
|
||||
* text with a background of many asteroids floating around. */
|
||||
void game_over(AsteroidsApp* app) {
|
||||
restart_game_after_gameover(app);
|
||||
if(app->is_new_highscore) save_game(app); // Save highscore but only on change
|
||||
app->gameover = true;
|
||||
int asteroids = 8;
|
||||
while(asteroids-- && add_asteroid(app) != NULL)
|
||||
;
|
||||
app->lives = GAME_START_LIVES; // Show 3 lives in game over screen to match new game start
|
||||
}
|
||||
|
||||
/* Function called when a collision between the asteroid and the
|
||||
@@ -422,16 +496,17 @@ void restart_game(AsteroidsApp* app) {
|
||||
app->bullets_num = 0;
|
||||
app->last_bullet_tick = 0;
|
||||
app->asteroids_num = 0;
|
||||
app->ship_hit = 0;
|
||||
}
|
||||
|
||||
/* Called after game over to restart the game. This function
|
||||
/* Called after gameover to restart the game. This function
|
||||
* also calls restart_game(). */
|
||||
void restart_game_after_gameover(AsteroidsApp* app) {
|
||||
app->gameover = false;
|
||||
app->ticks = 0;
|
||||
app->score = 0;
|
||||
app->ship_hit = 0;
|
||||
app->lives = GAME_START_LIVES - 1; /* -1 to account for current one. */
|
||||
app->is_new_highscore = false;
|
||||
app->lives = GAME_START_LIVES - 1;
|
||||
restart_game(app);
|
||||
}
|
||||
|
||||
@@ -505,6 +580,7 @@ void game_tick(void* ctx) {
|
||||
* 1. Ship was hit, we frozen the game as long as ship_hit isn't zero
|
||||
* again, and show an animation of a rotating ship. */
|
||||
if(app->ship_hit) {
|
||||
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_crash);
|
||||
app->ship.rot += 0.5;
|
||||
app->ship_hit--;
|
||||
view_port_update(app->view_port);
|
||||
@@ -515,7 +591,8 @@ void game_tick(void* ctx) {
|
||||
} else if(app->gameover) {
|
||||
/* 2. Game over. We need to update only background asteroids. In this
|
||||
* state the game just displays a GAME OVER text with the floating
|
||||
* asteroids in background. */
|
||||
* asteroids in backgroud. */
|
||||
|
||||
if(key_pressed_time(app, InputKeyOk) > 100) {
|
||||
restart_game_after_gameover(app);
|
||||
}
|
||||
@@ -524,13 +601,14 @@ void game_tick(void* ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Handle key presses. */
|
||||
/* Handle keypresses. */
|
||||
if(app->pressed[InputKeyLeft]) app->ship.rot -= .35;
|
||||
if(app->pressed[InputKeyRight]) app->ship.rot += .35;
|
||||
if(key_pressed_time(app, InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME) {
|
||||
if(app->pressed[InputKeyUp]) {
|
||||
app->ship.vx -= 0.5 * (float)sin(app->ship.rot);
|
||||
app->ship.vy += 0.5 * (float)cos(app->ship.rot);
|
||||
} else if(app->pressed[InputKeyDown]) {
|
||||
notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_brake);
|
||||
app->ship.vx *= 0.75;
|
||||
app->ship.vy *= 0.75;
|
||||
}
|
||||
@@ -567,6 +645,41 @@ void game_tick(void* ctx) {
|
||||
|
||||
/* ======================== Flipper specific code =========================== */
|
||||
|
||||
bool load_game(AsteroidsApp* app) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
uint16_t bytes_readed = 0;
|
||||
if(storage_file_open(file, SAVING_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
bytes_readed = storage_file_read(file, app, sizeof(AsteroidsApp));
|
||||
}
|
||||
storage_file_close(file);
|
||||
storage_file_free(file);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return bytes_readed == sizeof(AsteroidsApp);
|
||||
}
|
||||
|
||||
void save_game(AsteroidsApp* app) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
if(storage_common_stat(storage, SAVING_DIRECTORY, NULL) == FSE_NOT_EXIST) {
|
||||
if(!storage_simply_mkdir(storage, SAVING_DIRECTORY)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(storage_file_open(file, SAVING_FILENAME, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
storage_file_write(file, app, sizeof(AsteroidsApp));
|
||||
}
|
||||
storage_file_close(file);
|
||||
storage_file_free(file);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
/* Here all we do is putting the events into the queue that will be handled
|
||||
* in the while() loop of the app entry point function. */
|
||||
void input_callback(InputEvent* input_event, void* ctx) {
|
||||
@@ -579,6 +692,8 @@ void input_callback(InputEvent* input_event, void* ctx) {
|
||||
AsteroidsApp* asteroids_app_alloc() {
|
||||
AsteroidsApp* app = malloc(sizeof(AsteroidsApp));
|
||||
|
||||
load_game(app);
|
||||
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
app->view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(app->view_port, render_callback, app);
|
||||
@@ -587,6 +702,7 @@ AsteroidsApp* asteroids_app_alloc() {
|
||||
app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
|
||||
|
||||
app->running = 1; /* Turns 0 when back is pressed. */
|
||||
|
||||
restart_game_after_gameover(app);
|
||||
memset(app->pressed, 0, sizeof(app->pressed));
|
||||
return app;
|
||||
@@ -617,12 +733,15 @@ uint32_t key_pressed_time(AsteroidsApp* app, InputKey key) {
|
||||
|
||||
/* Handle keys interaction. */
|
||||
void asteroids_update_keypress_state(AsteroidsApp* app, InputEvent input) {
|
||||
// Allow Rapid fire
|
||||
if(input.key == InputKeyOk) {
|
||||
app->fire = true;
|
||||
}
|
||||
|
||||
if(input.type == InputTypePress) {
|
||||
app->pressed[input.key] = furi_get_tick();
|
||||
} else if(input.type == InputTypeRelease) {
|
||||
uint32_t dur = key_pressed_time(app, input.key);
|
||||
app->pressed[input.key] = 0;
|
||||
if(dur < 200 && input.key == InputKeyOk) app->fire = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,7 +765,9 @@ int32_t asteroids_app_entry(void* p) {
|
||||
|
||||
/* Handle navigation here. Then handle view-specific inputs
|
||||
* in the view specific handling function. */
|
||||
if(input.type == InputTypeShort && input.key == InputKeyBack) {
|
||||
if(input.type == InputTypeLong && input.key == InputKeyBack) {
|
||||
// Save High Score even if player didn't finish game
|
||||
if(app->is_new_highscore) save_game(app); // Save highscore but only on change
|
||||
app->running = 0;
|
||||
} else {
|
||||
asteroids_update_keypress_state(app, input);
|
||||
|
||||
Reference in New Issue
Block a user