diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0454dac2751..d3b6327d5a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,8 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 2 + submodules: recursive + - name: Set up ufbt uses: flipperdevices/flipperzero-ufbt-action@v0.1.2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..dbb9a2b91e1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "air_arkanoid/engine"] + path = air_arkanoid/engine + url = https://github.com/flipperdevices/flipperzero-game-engine.git diff --git a/air_arkanoid/application.fam b/air_arkanoid/application.fam new file mode 100644 index 00000000000..6927dfa2947 --- /dev/null +++ b/air_arkanoid/application.fam @@ -0,0 +1,16 @@ +App( + appid="air_arkanoid", + name="Air Arkanoid", + apptype=FlipperAppType.EXTERNAL, + entry_point="game_app", + stack_size=4 * 1024, + fap_icon="icon.png", + fap_category="Games", + fap_file_assets="assets", + fap_extbuild=( + ExtFile( + path="${FAP_SRC_DIR}/assets", + command="python3 ${FAP_SRC_DIR}/engine/scripts/sprite_builder.py ${FAP_SRC_DIR.abspath}/sprites ${TARGET.abspath}/sprites", + ), + ), +) diff --git a/air_arkanoid/assets/sprites/logo_air.fxbm b/air_arkanoid/assets/sprites/logo_air.fxbm new file mode 100644 index 00000000000..96579a376fd Binary files /dev/null and b/air_arkanoid/assets/sprites/logo_air.fxbm differ diff --git a/air_arkanoid/assets/sprites/logo_arkanoid.fxbm b/air_arkanoid/assets/sprites/logo_arkanoid.fxbm new file mode 100644 index 00000000000..6a2a72f32bb Binary files /dev/null and b/air_arkanoid/assets/sprites/logo_arkanoid.fxbm differ diff --git a/air_arkanoid/engine b/air_arkanoid/engine new file mode 160000 index 00000000000..e9ae35c6124 --- /dev/null +++ b/air_arkanoid/engine @@ -0,0 +1 @@ +Subproject commit e9ae35c61243982d06c9b06d8c65dea133129cf5 diff --git a/air_arkanoid/fonts/fonts.c b/air_arkanoid/fonts/fonts.c new file mode 100644 index 00000000000..bd9d19b58b2 --- /dev/null +++ b/air_arkanoid/fonts/fonts.c @@ -0,0 +1,25 @@ +#include "fonts.h" + +const uint8_t u8g2_font_u8glib_4_tr[681] = + "`\0\2\2\3\3\1\3\4\5\6\0\377\4\377\5\377\0\325\1\272\2\214 \4@*!\5a**" + "\42\6\323\63I\5#\12\355y\325P\325P\25\0$\13\365\271\31\34\31\215\221A\4%\6d\66\261" + "\7&\11lv\61\305*\215\0'\5\321+\2(\6\362m\252\31)\7\362-\61U\12*\5\322." + "#+\7[ri%\0,\5\321)\2-\5\313\62\3.\5I*\1/\7d\366 \266\1\60\7" + "c\62#\251\21\61\6bn\253\0\62\7c\62\63\245\1\63\7c\62+\203\21\64\7c\62\61\215\30" + "\65\7c\62C\203\21\66\7c\62#\216\1\67\7c\62\63\225\0\70\7c\62G\32\1\71\6c\62" + "\347\10:\5Y*);\5\341)I<\5Zn\62=\6[\62\33\14>\6Z.Q\1\77\7b" + ".*\203\0@\10d\66Cm\60\2A\7dv*\216\31B\7d\66k\310!C\7cr\63\3" + "\1D\10d\66+\312\221\0E\10d\66G\312`\4F\10d\66C\203\225\1G\10d\66C\203\64" + "\6H\7d\66qL\31I\5a*#J\7c\62\63.\0K\10d\66q\244(\3L\6c\62" + "\261\34M\11e:\31\254\225\64\10N\7d\66q\251\31O\10dv*\312\244\0P\10d\66+\216" + "\224\1Q\11e:#\305\24\323\12R\6d\66\257\62S\10dvC\243\241\0T\7c\62+V\0" + "U\7d\66\321\34\2V\7d\66\321L\12W\11e:\31\250\244\272\0X\7c\62\251L\5Y\10" + "d\66qh\60\4Z\7d\66#\226#[\6\362-\253%\134\11d\66\31e\224Q\0]\6\362-" + "\252\65^\5\323s\15_\5\314\65#`\5\322/\61a\6[rG\0b\7c\62Q\245\5c\5" + "Z.Kd\7c\262i%\1e\7[\62#-\0f\7c\262)\255\4g\6\343\61g\22h\7" + "c\62Q%\25i\5a*Ij\7\352m\31$\5k\7c\62\61\255\2l\5a*#m\7]" + ":\252\245\12n\7[\62*\251\0o\7[\62#\215\0p\7\343\61*\255\10q\7\343q+\311\0" + "r\6Z.+\1s\7[r*)\0t\7criE\1u\7[\62I\215\0v\7[\62I" + "U\0w\10]:\31\250.\0x\6[\62\251\3y\7\343\61i\304\21z\6[\62\62\12{\10\363" + "q\252\314 \12|\5\361)\7}\11\363\61\62\203\230\222\2~\7\324wI%\0\177\7l\66C\232" + "C\0\0\0\4\377\377\0"; \ No newline at end of file diff --git a/air_arkanoid/fonts/fonts.h b/air_arkanoid/fonts/fonts.h new file mode 100644 index 00000000000..9e1c74ac1b6 --- /dev/null +++ b/air_arkanoid/fonts/fonts.h @@ -0,0 +1,4 @@ +#pragma once +#include + +extern const uint8_t u8g2_font_u8glib_4_tr[]; \ No newline at end of file diff --git a/air_arkanoid/game.c b/air_arkanoid/game.c new file mode 100644 index 00000000000..82e915bd2a6 --- /dev/null +++ b/air_arkanoid/game.c @@ -0,0 +1,73 @@ +#include "game.h" +#include "game_settings.h" +#include "levels/level_menu.h" +#include "levels/level_game.h" +#include "levels/level_settings.h" +#include "levels/level_message.h" + +const NotificationSequence sequence_sound_blip = { + &message_note_c7, + &message_delay_50, + &message_sound_off, + NULL, +}; + +const NotificationSequence sequence_sound_menu = { + &message_note_c6, + &message_delay_10, + &message_sound_off, + NULL, +}; + +void game_start(GameManager* game_manager, void* ctx) { + GameContext* context = ctx; + context->imu = imu_alloc(); + context->imu_present = imu_present(context->imu); + context->levels.menu = game_manager_add_level(game_manager, &level_menu); + context->levels.settings = game_manager_add_level(game_manager, &level_settings); + context->levels.game = game_manager_add_level(game_manager, &level_game); + context->levels.message = game_manager_add_level(game_manager, &level_message); + + if(!game_settings_load(&context->settings)) { + context->settings.sound = true; + context->settings.show_fps = false; + } + + context->app = furi_record_open(RECORD_NOTIFICATION); + context->game_manager = game_manager; + + game_manager_show_fps_set(context->game_manager, context->settings.show_fps); +} + +void game_stop(void* ctx) { + GameContext* context = ctx; + imu_free(context->imu); + + furi_record_close(RECORD_NOTIFICATION); +} + +const Game game = { + .target_fps = 30, + .show_fps = false, + .always_backlight = true, + .start = game_start, + .stop = game_stop, + .context_size = sizeof(GameContext), +}; + +void game_switch_sound(GameContext* context) { + context->settings.sound = !context->settings.sound; + game_settings_save(&context->settings); +} + +void game_switch_show_fps(GameContext* context) { + context->settings.show_fps = !context->settings.show_fps; + game_manager_show_fps_set(context->game_manager, context->settings.show_fps); + game_settings_save(&context->settings); +} + +void game_sound_play(GameContext* context, const NotificationSequence* sequence) { + if(context->settings.sound) { + notification_message(context->app, sequence); + } +} \ No newline at end of file diff --git a/air_arkanoid/game.h b/air_arkanoid/game.h new file mode 100644 index 00000000000..36f591bc33e --- /dev/null +++ b/air_arkanoid/game.h @@ -0,0 +1,35 @@ +#pragma once +#include "engine/engine.h" +#include "engine/sensors/imu.h" +#include + +typedef struct { + Level* menu; + Level* settings; + Level* game; + Level* message; +} Levels; + +typedef struct { + bool sound; + bool show_fps; +} Settings; + +typedef struct { + Imu* imu; + bool imu_present; + + Levels levels; + Settings settings; + + NotificationApp* app; + GameManager* game_manager; +} GameContext; + +void game_switch_sound(GameContext* context); + +void game_switch_show_fps(GameContext* context); + +void game_sound_play(GameContext* context, const NotificationSequence* sequence); + +extern const NotificationSequence sequence_sound_menu; \ No newline at end of file diff --git a/air_arkanoid/game_settings.c b/air_arkanoid/game_settings.c new file mode 100644 index 00000000000..713a49d3b02 --- /dev/null +++ b/air_arkanoid/game_settings.c @@ -0,0 +1,21 @@ +#include +#include "game_settings.h" +#include + +#define SETTINGS_PATH APP_DATA_PATH("settings.bin") +#define SETTINGS_VERSION (0) +#define SETTINGS_MAGIC (0x69) + +bool game_settings_load(Settings* settings) { + furi_assert(settings); + + return saved_struct_load( + SETTINGS_PATH, settings, sizeof(Settings), SETTINGS_MAGIC, SETTINGS_VERSION); +} + +bool game_settings_save(Settings* settings) { + furi_assert(settings); + + return saved_struct_save( + SETTINGS_PATH, settings, sizeof(Settings), SETTINGS_MAGIC, SETTINGS_VERSION); +} \ No newline at end of file diff --git a/air_arkanoid/game_settings.h b/air_arkanoid/game_settings.h new file mode 100644 index 00000000000..cec0b5dfe36 --- /dev/null +++ b/air_arkanoid/game_settings.h @@ -0,0 +1,6 @@ +#pragma once +#include "game.h" + +bool game_settings_save(Settings* settings); + +bool game_settings_load(Settings* settings); \ No newline at end of file diff --git a/air_arkanoid/icon.png b/air_arkanoid/icon.png new file mode 100644 index 00000000000..c35a2761401 Binary files /dev/null and b/air_arkanoid/icon.png differ diff --git a/air_arkanoid/levels/level_game.c b/air_arkanoid/levels/level_game.c new file mode 100644 index 00000000000..63528950eee --- /dev/null +++ b/air_arkanoid/levels/level_game.c @@ -0,0 +1,357 @@ +#include "level_game.h" +#include "level_message.h" + +const NotificationSequence sequence_sound_ball_collide = { + &message_note_c7, + &message_delay_50, + &message_sound_off, + NULL, +}; + +const NotificationSequence sequence_sound_ball_paddle_collide = { + &message_note_d6, + &message_delay_10, + &message_sound_off, + NULL, +}; + +const NotificationSequence sequence_sound_ball_lost = { + &message_vibro_on, + + &message_note_ds4, + &message_delay_10, + &message_sound_off, + &message_delay_10, + + &message_note_ds4, + &message_delay_10, + &message_sound_off, + &message_delay_10, + + &message_note_ds4, + &message_delay_10, + &message_sound_off, + &message_delay_10, + + &message_vibro_off, + NULL, +}; + +typedef enum { + GameEventBallLost, +} GameEvent; + +/****** Ball ******/ + +static const EntityDescription paddle_desc; + +typedef struct { + Vector speed; + float radius; + float max_speed; +} Ball; + +static void ball_reset(Ball* ball) { + ball->max_speed = 2; + ball->speed = (Vector){0, 0}; + ball->radius = 2; +} + +static void ball_start(Entity* self, GameManager* manager, void* context) { + UNUSED(manager); + Ball* ball = context; + ball_reset(ball); + entity_collider_add_circle(self, ball->radius); +} + +static void ball_set_angle(Ball* ball, float angle) { + ball->speed.x = cosf(angle * (M_PI / 180.0f)) * ball->max_speed; + ball->speed.y = sinf(angle * (M_PI / 180.0f)) * ball->max_speed; +} + +static void ball_update(Entity* entity, GameManager* manager, void* context) { + UNUSED(manager); + Ball* ball = context; + Vector pos = entity_pos_get(entity); + pos = vector_add(pos, ball->speed); + + const Vector screen = {128, 64}; + + // prevent to go out of screen + if(pos.x - ball->radius < 0) { + pos.x = ball->radius; + ball->speed.x = -ball->speed.x; + } else if(pos.x + ball->radius > screen.x) { + pos.x = screen.x - ball->radius; + ball->speed.x = -ball->speed.x; + } else if(pos.y - ball->radius < 0) { + pos.y = ball->radius; + ball->speed.y = -ball->speed.y; + } else if(pos.y - ball->radius > screen.y) { + Level* level = game_manager_current_level_get(manager); + level_send_event(level, entity, &paddle_desc, GameEventBallLost, (EntityEventValue){0}); + } + + entity_pos_set(entity, pos); +} + +static void ball_render(Entity* entity, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(manager); + Ball* ball = context; + Vector pos = entity_pos_get(entity); + canvas_draw_disc(canvas, pos.x, pos.y, ball->radius); +} + +static const EntityDescription ball_desc = { + .start = ball_start, + .stop = NULL, + .update = ball_update, + .render = ball_render, + .collision = NULL, + .event = NULL, + .context_size = sizeof(Ball), +}; + +/****** Block ******/ + +static const EntityDescription block_desc; + +typedef struct { + Vector size; +} Block; + +static void block_spawn(Level* level, Vector pos, Vector size) { + Entity* block = level_add_entity(level, &block_desc); + entity_collider_add_rect(block, size.x, size.y); + entity_pos_set(block, pos); + Block* block_context = entity_context_get(block); + block_context->size = size; +} + +static void block_render(Entity* entity, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(manager); + Block* block = context; + Vector pos = entity_pos_get(entity); + canvas_draw_box( + canvas, pos.x - block->size.x / 2, pos.y - block->size.y / 2, block->size.x, block->size.y); +} + +static void block_collision(Entity* self, Entity* other, GameManager* manager, void* context) { + UNUSED(manager); + + if(entity_description_get(other) == &ball_desc) { + Ball* ball = entity_context_get(other); + Block* block = context; + Vector ball_pos = entity_pos_get(other); + Vector block_pos = entity_pos_get(self); + + Vector closest = { + CLAMP(ball_pos.x, block_pos.x + block->size.x / 2, block_pos.x - block->size.x / 2), + CLAMP(ball_pos.y, block_pos.y + block->size.y / 2, block_pos.y - block->size.y / 2), + }; + + // change the ball speed based on the collision + Vector distance = vector_sub(ball_pos, closest); + if(fabsf(distance.x) < fabsf(distance.y)) { + ball->speed.y = -ball->speed.y; + } else { + ball->speed.x = -ball->speed.x; + } + + Level* level = game_manager_current_level_get(manager); + level_remove_entity(level, self); + + GameContext* game = game_manager_game_context_get(manager); + game_sound_play(game, &sequence_sound_ball_collide); + + if(level_entity_count(level, &block_desc) == 0) { + LevelMessageContext* message_context = level_context_get(game->levels.message); + furi_string_set(message_context->message, "You win!"); + game_manager_next_level_set(manager, game->levels.message); + } + } +} + +static const EntityDescription block_desc = { + .start = NULL, + .stop = NULL, + .update = NULL, + .render = block_render, + .collision = block_collision, + .event = NULL, + .context_size = sizeof(Block), +}; + +/****** Paddle ******/ + +static const Vector paddle_start_size = {30, 3}; + +typedef struct { + Vector size; + bool ball_launched; + Entity* ball; +} Paddle; + +static void paddle_start(Entity* self, GameManager* manager, void* context) { + UNUSED(manager); + Paddle* paddle = context; + paddle->size = paddle_start_size; + paddle->ball_launched = false; + entity_pos_set(self, (Vector){64, 61}); + entity_collider_add_rect(self, paddle->size.x, paddle->size.y); + + Level* level = game_manager_current_level_get(manager); + paddle->ball = level_add_entity(level, &ball_desc); +} + +static void paddle_stop(Entity* entity, GameManager* manager, void* context) { + UNUSED(entity); + Paddle* paddle = context; + + Level* level = game_manager_current_level_get(manager); + level_remove_entity(level, paddle->ball); + paddle->ball = NULL; +} + +static float paddle_x_from_angle(float angle) { + const float min_angle = -45.0f; + const float max_angle = 45.0f; + + return 128.0f * (angle - min_angle) / (max_angle - min_angle); +} + +static void paddle_update(Entity* entity, GameManager* manager, void* context) { + Paddle* paddle = context; + InputState input = game_manager_input_get(manager); + GameContext* game_context = game_manager_game_context_get(manager); + + Vector pos = entity_pos_get(entity); + float paddle_half = paddle->size.x / 2; + if(game_context->imu_present) { + pos.x = paddle_x_from_angle(-imu_pitch_get(game_context->imu)); + } else { + if(input.held & GameKeyLeft) { + pos.x -= 2; + } + if(input.held & GameKeyRight) { + pos.x += 2; + } + } + pos.x = CLAMP(pos.x, 128 - paddle_half, paddle_half); + entity_pos_set(entity, pos); + + if(input.pressed & GameKeyBack) { + game_manager_next_level_set(manager, game_context->levels.menu); + } + + if(input.pressed & GameKeyOk) { + if(!paddle->ball_launched) { + paddle->ball_launched = true; + + Ball* ball = entity_context_get(paddle->ball); + ball_set_angle(ball, 270.0f); + } + } + + if(!paddle->ball_launched) { + Vector ball_pos = entity_pos_get(paddle->ball); + Ball* ball = entity_context_get(paddle->ball); + ball_pos.x = pos.x; + ball_pos.y = pos.y - paddle->size.y / 2 - ball->radius; + entity_pos_set(paddle->ball, ball_pos); + } +} + +static void paddle_render(Entity* entity, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(manager); + Paddle* paddle = context; + Vector pos = entity_pos_get(entity); + float paddle_half = paddle->size.x / 2; + canvas_draw_box(canvas, pos.x - paddle_half, pos.y, paddle->size.x, paddle->size.y); +} + +static void paddle_collision(Entity* self, Entity* other, GameManager* manager, void* context) { + UNUSED(manager); + + if(entity_description_get(other) == &ball_desc) { + Ball* ball = entity_context_get(other); + Paddle* paddle = context; + Vector ball_pos = entity_pos_get(other); + Vector paddle_pos = entity_pos_get(self); + + float paddle_half = paddle->size.x / 2; + float paddle_center = paddle_pos.x; + float paddle_edge = paddle_center - paddle_half; + float paddle_edge_distance = ball_pos.x - paddle_edge; + float paddle_edge_distance_normalized = paddle_edge_distance / paddle->size.x; + + // lerp the angle based on the distance from the paddle center + float angle = 270.0f - 45.0f + 90.0f * paddle_edge_distance_normalized; + ball_set_angle(ball, angle); + + GameContext* game = game_manager_game_context_get(manager); + game_sound_play(game, &sequence_sound_ball_paddle_collide); + } +} + +static void paddle_event(Entity* self, GameManager* manager, EntityEvent event, void* context) { + UNUSED(manager); + UNUSED(self); + if(event.type == GameEventBallLost) { + Paddle* paddle = context; + paddle->ball_launched = false; + Ball* ball = entity_context_get(paddle->ball); + ball_reset(ball); + GameContext* game = game_manager_game_context_get(manager); + game_sound_play(game, &sequence_sound_ball_lost); + } +} + +static const EntityDescription paddle_desc = { + .start = paddle_start, + .stop = paddle_stop, + .update = paddle_update, + .render = paddle_render, + .collision = paddle_collision, + .event = paddle_event, + .context_size = sizeof(Paddle), +}; + +static void level_1_spawn(Level* level) { + level_add_entity(level, &paddle_desc); + const Vector block_size = {13, 5}; + const Vector screen = {128, 64}; + const int block_count_x = screen.x / block_size.x; + const int block_count_y = 6; + size_t block_spacing = 1; + + for(int y = 0; y < block_count_y; y++) { + for(int x = 0; x < block_count_x; x++) { + Vector pos = { + (x) * (block_size.x + block_spacing) + block_size.x / 2, + (y) * (block_size.y + block_spacing) + block_size.y / 2, + }; + block_spawn(level, pos, block_size); + } + } +} + +static void level_game_start(Level* level, GameManager* manager, void* context) { + UNUSED(manager); + UNUSED(context); + level_1_spawn(level); +} + +static void level_game_stop(Level* level, GameManager* manager, void* context) { + UNUSED(manager); + UNUSED(context); + level_clear(level); +} + +const LevelBehaviour level_game = { + .alloc = NULL, + .free = NULL, + .start = level_game_start, + .stop = level_game_stop, + .context_size = 0, +}; \ No newline at end of file diff --git a/air_arkanoid/levels/level_game.h b/air_arkanoid/levels/level_game.h new file mode 100644 index 00000000000..3eddc72c2b4 --- /dev/null +++ b/air_arkanoid/levels/level_game.h @@ -0,0 +1,4 @@ +#pragma once +#include "../game.h" + +extern const LevelBehaviour level_game; \ No newline at end of file diff --git a/air_arkanoid/levels/level_menu.c b/air_arkanoid/levels/level_menu.c new file mode 100644 index 00000000000..c50e23a65f7 --- /dev/null +++ b/air_arkanoid/levels/level_menu.c @@ -0,0 +1,201 @@ +#include "level_menu.h" +#include "../game.h" + +typedef struct { + Sprite* sprite; + Vector pos_start; + Vector pos_end; + float duration; + float time; +} MovingSpriteContext; + +/***** Moving Sprite *****/ + +static void moving_sprite_update(Entity* entity, GameManager* manager, void* context) { + UNUSED(manager); + MovingSpriteContext* sprite_context = context; + + // lerp position between start and end for duration + if(sprite_context->time < sprite_context->duration) { + Vector dir = vector_sub(sprite_context->pos_end, sprite_context->pos_start); + Vector len = vector_mulf(dir, sprite_context->time / sprite_context->duration); + Vector pos = vector_add(sprite_context->pos_start, len); + + entity_pos_set(entity, pos); + sprite_context->time += 1.0f; + } else { + entity_pos_set(entity, sprite_context->pos_end); + } +} + +static void + moving_sprite_render(Entity* entity, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(manager); + MovingSpriteContext* sprite_context = context; + + if(sprite_context->sprite) { + Vector pos = entity_pos_get(entity); + canvas_draw_sprite(canvas, sprite_context->sprite, pos.x, pos.y); + } +} + +static void moving_sprite_init( + Entity* entity, + GameManager* manager, + Vector start, + Vector end, + const char* sprite_name) { + MovingSpriteContext* sprite_context = entity_context_get(entity); + sprite_context->pos_start = start; + sprite_context->pos_end = end; + sprite_context->duration = 30.0f; + sprite_context->time = 0; + sprite_context->sprite = game_manager_sprite_load(manager, sprite_name); +} + +static void moving_sprite_reset(Entity* entity) { + MovingSpriteContext* sprite_context = entity_context_get(entity); + sprite_context->time = 0; +} + +static const EntityDescription moving_sprite_desc = { + .start = NULL, + .stop = NULL, + .update = moving_sprite_update, + .render = moving_sprite_render, + .collision = NULL, + .event = NULL, + .context_size = sizeof(MovingSpriteContext), +}; + +/***** Menu *****/ + +typedef struct { + int selected; +} MenuContext; + +static void menu_update(Entity* entity, GameManager* manager, void* context) { + UNUSED(entity); + MenuContext* menu_context = context; + GameContext* game_context = game_manager_game_context_get(manager); + + InputState input = game_manager_input_get(manager); + if(input.pressed & GameKeyBack) { + game_manager_game_stop(manager); + } + + if(input.pressed & GameKeyUp) { + menu_context->selected--; + if(menu_context->selected < 0) { + menu_context->selected = 2; + } + } + + if(input.pressed & GameKeyDown) { + menu_context->selected++; + if(menu_context->selected > 2) { + menu_context->selected = 0; + } + } + + if(input.pressed & GameKeyUp || input.pressed & GameKeyDown || input.pressed & GameKeyOk) { + game_sound_play(game_context, &sequence_sound_menu); + } + + if(input.pressed & GameKeyOk) { + switch(menu_context->selected) { + case 0: + game_manager_next_level_set(manager, game_context->levels.game); + break; + case 1: + game_manager_next_level_set(manager, game_context->levels.settings); + break; + case 2: + game_manager_game_stop(manager); + break; + + default: + break; + } + } +} + +#include "../fonts/fonts.h" + +static void menu_render(Entity* entity, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(entity); + UNUSED(manager); + MenuContext* menu_context = context; + const char* line_1 = "Play"; + const char* line_2 = "Settings"; + const char* line_3 = "Exit"; + + if(menu_context->selected == 0) { + line_1 = ">Play"; + } else if(menu_context->selected == 1) { + line_2 = ">Settings"; + } else if(menu_context->selected == 2) { + line_3 = ">Exit"; + } + + canvas_draw_str_aligned(canvas, 64, 39, AlignCenter, AlignCenter, line_1); + canvas_draw_str_aligned(canvas, 64, 49, AlignCenter, AlignCenter, line_2); + canvas_draw_str_aligned(canvas, 64, 59, AlignCenter, AlignCenter, line_3); +} + +static const EntityDescription menu_desc = { + .start = NULL, + .stop = NULL, + .update = menu_update, + .render = menu_render, + .collision = NULL, + .event = NULL, + .context_size = sizeof(MenuContext), +}; + +/***** Level *****/ + +typedef struct { + Entity* arkanoid; + Entity* air; +} LevelMenuContext; + +static void level_menu_alloc(Level* level, GameManager* manager, void* context) { + LevelMenuContext* menu_context = context; + + const float start = 256; // 0, due to the canvas draw limitations + + menu_context->arkanoid = level_add_entity(level, &moving_sprite_desc); + moving_sprite_init( + menu_context->arkanoid, + manager, + (Vector){.x = start - 50, .y = start + 11}, + (Vector){.x = start + 7, .y = start + 11}, + "logo_arkanoid.fxbm"); + + menu_context->air = level_add_entity(level, &moving_sprite_desc); + moving_sprite_init( + menu_context->air, + manager, + (Vector){.x = start + 20, .y = start - 27}, + (Vector){.x = start + 20, .y = start + 0}, + "logo_air.fxbm"); + + level_add_entity(level, &menu_desc); +} + +static void level_menu_start(Level* level, GameManager* manager, void* context) { + UNUSED(level); + UNUSED(manager); + LevelMenuContext* menu_context = context; + moving_sprite_reset(menu_context->arkanoid); + moving_sprite_reset(menu_context->air); +} + +const LevelBehaviour level_menu = { + .alloc = level_menu_alloc, + .free = NULL, + .start = level_menu_start, + .stop = NULL, + .context_size = sizeof(LevelMenuContext), +}; \ No newline at end of file diff --git a/air_arkanoid/levels/level_menu.h b/air_arkanoid/levels/level_menu.h new file mode 100644 index 00000000000..4f57f22e258 --- /dev/null +++ b/air_arkanoid/levels/level_menu.h @@ -0,0 +1,4 @@ +#pragma once +#include "../game.h" + +extern const LevelBehaviour level_menu; \ No newline at end of file diff --git a/air_arkanoid/levels/level_message.c b/air_arkanoid/levels/level_message.c new file mode 100644 index 00000000000..3c835bb1d3b --- /dev/null +++ b/air_arkanoid/levels/level_message.c @@ -0,0 +1,57 @@ +#include +#include "level_message.h" + +static void message_update(Entity* self, GameManager* manager, void* context) { + UNUSED(self); + UNUSED(context); + InputState input = game_manager_input_get(manager); + if(input.pressed & GameKeyOk || input.pressed & GameKeyBack) { + GameContext* ctx = game_manager_game_context_get(manager); + game_manager_next_level_set(manager, ctx->levels.menu); + } +} + +static void message_render(Entity* self, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(self); + UNUSED(manager); + UNUSED(context); + GameContext* game_ctx = game_manager_game_context_get(manager); + LevelMessageContext* ctx = level_context_get(game_ctx->levels.message); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned( + canvas, 64, 30, AlignCenter, AlignTop, furi_string_get_cstr(ctx->message)); + canvas_set_font(canvas, FontSecondary); +} + +static const EntityDescription message_desc = { + .start = NULL, + .stop = NULL, + .update = message_update, + .render = message_render, + .collision = NULL, + .event = NULL, + .context_size = 0, +}; + +static void level_alloc(Level* level, GameManager* manager, void* ctx) { + UNUSED(level); + UNUSED(manager); + LevelMessageContext* context = ctx; + context->message = furi_string_alloc(); + level_add_entity(level, &message_desc); +} + +static void level_free(Level* level, GameManager* manager, void* context) { + UNUSED(level); + UNUSED(manager); + LevelMessageContext* ctx = context; + furi_string_free(ctx->message); +} + +const LevelBehaviour level_message = { + .alloc = level_alloc, + .free = level_free, + .start = NULL, + .stop = NULL, + .context_size = sizeof(LevelMessageContext), +}; \ No newline at end of file diff --git a/air_arkanoid/levels/level_message.h b/air_arkanoid/levels/level_message.h new file mode 100644 index 00000000000..60e273c5999 --- /dev/null +++ b/air_arkanoid/levels/level_message.h @@ -0,0 +1,8 @@ +#pragma once +#include "../game.h" + +extern const LevelBehaviour level_message; + +typedef struct { + FuriString* message; +} LevelMessageContext; \ No newline at end of file diff --git a/air_arkanoid/levels/level_settings.c b/air_arkanoid/levels/level_settings.c new file mode 100644 index 00000000000..46112d94f8a --- /dev/null +++ b/air_arkanoid/levels/level_settings.c @@ -0,0 +1,223 @@ +#include "level_settings.h" + +/**** Menu ****/ + +typedef enum { + Sound = 0, + ShowFPS, + Back, +} MenuOption; + +typedef struct { + int selected; +} MenuContext; + +static void menu_update(Entity* entity, GameManager* manager, void* context) { + UNUSED(entity); + MenuContext* menu_context = context; + GameContext* game_context = game_manager_game_context_get(manager); + + InputState input = game_manager_input_get(manager); + + if(input.pressed & GameKeyUp || input.pressed & GameKeyDown || input.pressed & GameKeyOk) { + game_sound_play(game_context, &sequence_sound_menu); + } + + if(input.pressed & GameKeyBack) { + game_manager_next_level_set(manager, game_context->levels.menu); + } + + if(input.pressed & GameKeyUp) { + menu_context->selected--; + if(menu_context->selected < Sound) { + menu_context->selected = Back; + } + } + + if(input.pressed & GameKeyDown) { + menu_context->selected++; + if(menu_context->selected > Back) { + menu_context->selected = Sound; + } + } + + if(input.pressed & GameKeyOk) { + switch(menu_context->selected) { + case Sound: + game_switch_sound(game_context); + break; + case ShowFPS: + game_switch_show_fps(game_context); + break; + case Back: + game_manager_next_level_set(manager, game_context->levels.menu); + break; + + default: + break; + } + } + + if(input.pressed & GameKeyRight || input.pressed & GameKeyLeft) { + switch(menu_context->selected) { + case Sound: + game_switch_sound(game_context); + break; + case ShowFPS: + game_switch_show_fps(game_context); + break; + + default: + break; + } + } +} + +static void menu_render(Entity* entity, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(entity); + MenuContext* menu_context = context; + GameContext* game_context = game_manager_game_context_get(manager); + FuriString* line = furi_string_alloc_set("Sound: "); + + if(menu_context->selected == Sound) { + furi_string_set(line, ">Sound: "); + } + + if(game_context->settings.sound) { + furi_string_cat(line, "On"); + } else { + furi_string_cat(line, "Off"); + } + + canvas_draw_str_aligned( + canvas, 64 + 3, 18, AlignLeft, AlignCenter, furi_string_get_cstr(line)); + + furi_string_set(line, "FPS: "); + if(menu_context->selected == ShowFPS) { + furi_string_set(line, ">FPS: "); + } + + if(game_context->settings.show_fps) { + furi_string_cat(line, "On"); + } else { + furi_string_cat(line, "Off"); + } + + canvas_draw_str_aligned( + canvas, 64 + 3, 33, AlignLeft, AlignCenter, furi_string_get_cstr(line)); + + furi_string_set(line, "Back"); + + if(menu_context->selected == Back) { + furi_string_set(line, ">Back"); + } + + canvas_draw_str_aligned( + canvas, 64 + 3, 48, AlignLeft, AlignCenter, furi_string_get_cstr(line)); + + furi_string_free(line); +} + +static const EntityDescription menu_desc = { + .start = NULL, + .stop = NULL, + .update = menu_update, + .render = menu_render, + .collision = NULL, + .event = NULL, + .context_size = sizeof(MenuContext), +}; + +/**** IMU Debug ****/ + +typedef struct { + float pitch; + float roll; + float yaw; + bool imu_present; +} IMUDebugContext; + +static void imu_debug_start(Entity* self, GameManager* manager, void* ctx) { + UNUSED(self); + IMUDebugContext* context = ctx; + context->pitch = 0; + context->roll = 0; + context->yaw = 0; + GameContext* game_context = game_manager_game_context_get(manager); + context->imu_present = game_context->imu_present; +} + +static void imu_debug_update(Entity* self, GameManager* manager, void* ctx) { + UNUSED(self); + IMUDebugContext* context = (IMUDebugContext*)ctx; + GameContext* game_context = game_manager_game_context_get(manager); + + if(game_context->imu_present) { + context->pitch = imu_pitch_get(game_context->imu); + context->roll = imu_roll_get(game_context->imu); + context->yaw = imu_yaw_get(game_context->imu); + } +} + +static void imu_debug_render(Entity* self, GameManager* manager, Canvas* canvas, void* context) { + UNUSED(self); + UNUSED(manager); + + Vector pos = {32, 32}; + const float radius = 30; + const float max_angle = 45; + const float bubble_radius = 3; + + canvas_draw_circle(canvas, pos.x, pos.y, radius); + + IMUDebugContext* imu_debug_context = context; + if(imu_debug_context->imu_present) { + const float pitch = -CLAMP(imu_debug_context->pitch, max_angle, -max_angle); + const float roll = -CLAMP(imu_debug_context->roll, max_angle, -max_angle); + const float max_bubble_len = radius - bubble_radius - 2; + + Vector ball = { + max_bubble_len * (pitch / max_angle), + max_bubble_len * (roll / max_angle), + }; + + float bubble_len = sqrtf(ball.x * ball.x + ball.y * ball.y); + if(bubble_len > max_bubble_len) { + ball.x = ball.x * max_bubble_len / bubble_len; + ball.y = ball.y * max_bubble_len / bubble_len; + } + + ball = vector_add(pos, ball); + + canvas_draw_disc(canvas, ball.x, ball.y, bubble_radius); + } else { + canvas_draw_str_aligned(canvas, pos.x, pos.y + 1, AlignCenter, AlignCenter, "No IMU"); + } +} + +static const EntityDescription imu_debug_desc = { + .start = imu_debug_start, + .stop = NULL, + .update = imu_debug_update, + .render = imu_debug_render, + .collision = NULL, + .event = NULL, + .context_size = sizeof(IMUDebugContext), +}; + +/**** Level ****/ + +static void level_settings_alloc(Level* level, GameManager* manager, void* ctx) { + UNUSED(ctx); + UNUSED(manager); + level_add_entity(level, &imu_debug_desc); + level_add_entity(level, &menu_desc); +} + +const LevelBehaviour level_settings = { + .alloc = level_settings_alloc, + .free = NULL, + .start = NULL, + .stop = NULL, + .context_size = 0, +}; \ No newline at end of file diff --git a/air_arkanoid/levels/level_settings.h b/air_arkanoid/levels/level_settings.h new file mode 100644 index 00000000000..00cba9de864 --- /dev/null +++ b/air_arkanoid/levels/level_settings.h @@ -0,0 +1,4 @@ +#pragma once +#include "../game.h" + +extern const LevelBehaviour level_settings; \ No newline at end of file diff --git a/air_arkanoid/sprites/logo_air.png b/air_arkanoid/sprites/logo_air.png new file mode 100644 index 00000000000..71fa93382f9 Binary files /dev/null and b/air_arkanoid/sprites/logo_air.png differ diff --git a/air_arkanoid/sprites/logo_arkanoid.png b/air_arkanoid/sprites/logo_arkanoid.png new file mode 100644 index 00000000000..85accf21902 Binary files /dev/null and b/air_arkanoid/sprites/logo_arkanoid.png differ