Jelajahi Sumber

Add air_arkanoid from https://github.com/flipperdevices/flipperzero-good-faps

git-subtree-dir: air_arkanoid
git-subtree-mainline: 9b5c83a2b1cf155b71c06d8cb1254a71a7774e5e
git-subtree-split: 7abc2e4bbbbc08e10ea7dca12fc812b3c55de65b
Willy-JL 1 tahun lalu
induk
melakukan
927849c9e7

+ 4 - 0
air_arkanoid/.catalog/CHANGELOG.md

@@ -0,0 +1,4 @@
+## 1.1
+ - Description update
+## 1.0
+ - Initial release

+ 5 - 0
air_arkanoid/.catalog/README.md

@@ -0,0 +1,5 @@
+# Air Arkanoid
+
+This is our simple version of the Arkanoid game that can use the motion-tracking sensor of the Video Game Module or Flipper Zero buttons to control the paddle. To play the game with buttons, disconnect the Video Game Module before running the game.
+
+The Air Arkanoid game also demonstrates how to integrate the motion-tracking sensor of the Video Game Module into your game using the [Flipper Zero Game Engine](https://github.com/flipperdevices/flipperzero-game-engine).

TEMPAT SAMPAH
air_arkanoid/.catalog/screenshots/1.png


TEMPAT SAMPAH
air_arkanoid/.catalog/screenshots/2.png


TEMPAT SAMPAH
air_arkanoid/.catalog/screenshots/3.png


+ 1 - 0
air_arkanoid/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/flipperdevices/flipperzero-good-faps dev air_arkanoid

+ 18 - 0
air_arkanoid/application.fam

@@ -0,0 +1,18 @@
+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_description="Arkanoid game that supports the Video Game Module motion sensor",
+    fap_version="1.1",
+    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",
+        ),
+    ),
+)

TEMPAT SAMPAH
air_arkanoid/assets/sprites/logo_air.fxbm


TEMPAT SAMPAH
air_arkanoid/assets/sprites/logo_arkanoid.fxbm


+ 1 - 0
air_arkanoid/engine

@@ -0,0 +1 @@
+Subproject commit e9ae35c61243982d06c9b06d8c65dea133129cf5

+ 25 - 0
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";

+ 4 - 0
air_arkanoid/fonts/fonts.h

@@ -0,0 +1,4 @@
+#pragma once
+#include <stdint.h>
+
+extern const uint8_t u8g2_font_u8glib_4_tr[];

+ 73 - 0
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);
+    }
+}

+ 35 - 0
air_arkanoid/game.h

@@ -0,0 +1,35 @@
+#pragma once
+#include "engine/engine.h"
+#include "engine/sensors/imu.h"
+#include <notification/notification_messages.h>
+
+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;

+ 21 - 0
air_arkanoid/game_settings.c

@@ -0,0 +1,21 @@
+#include <storage/storage.h>
+#include "game_settings.h"
+#include <lib/toolbox/saved_struct.h>
+
+#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);
+}

+ 6 - 0
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);

TEMPAT SAMPAH
air_arkanoid/icon.png


+ 357 - 0
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,
+};

+ 4 - 0
air_arkanoid/levels/level_game.h

@@ -0,0 +1,4 @@
+#pragma once
+#include "../game.h"
+
+extern const LevelBehaviour level_game;

+ 201 - 0
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),
+};

+ 4 - 0
air_arkanoid/levels/level_menu.h

@@ -0,0 +1,4 @@
+#pragma once
+#include "../game.h"
+
+extern const LevelBehaviour level_menu;

+ 57 - 0
air_arkanoid/levels/level_message.c

@@ -0,0 +1,57 @@
+#include <gui/elements.h>
+#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),
+};

+ 8 - 0
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;

+ 223 - 0
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,
+};

+ 4 - 0
air_arkanoid/levels/level_settings.h

@@ -0,0 +1,4 @@
+#pragma once
+#include "../game.h"
+
+extern const LevelBehaviour level_settings;

TEMPAT SAMPAH
air_arkanoid/sprites/logo_air.png


TEMPAT SAMPAH
air_arkanoid/sprites/logo_arkanoid.png