Просмотр исходного кода

Make the Laser Tag game.

It's built, no errors in VSCode, no errors with uFBT, but still crashes.
RocketGod 1 год назад
Родитель
Сommit
3cca0c71cf
19 измененных файлов с 804 добавлено и 1 удалено
  1. 6 0
      .gitignore
  2. 4 1
      README.md
  3. 20 0
      application.fam
  4. 3 0
      docs/CHANGELOG.md
  5. 3 0
      docs/README.md
  6. 99 0
      game_state.c
  7. 44 0
      game_state.h
  8. BIN
      icons/laser_tag_10px.png
  9. BIN
      icons/toolkit.png
  10. 112 0
      infrared_controller.c
  11. 42 0
      infrared_controller.h
  12. 174 0
      laser_tag_app.c
  13. 31 0
      laser_tag_app.h
  14. 123 0
      laser_tag_icons.c
  15. 17 0
      laser_tag_icons.h
  16. 91 0
      laser_tag_view.c
  17. 16 0
      laser_tag_view.h
  18. 19 0
      manifest.yml
  19. BIN
      screenshots/todo.png

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig
+.env
+.ufbt

+ 4 - 1
README.md

@@ -1,2 +1,5 @@
 # Flipper-Zero-Laser-Tag
 # Flipper-Zero-Laser-Tag
- Laser Tag game for Flipper Zero
+
+ Laser Tag game for Flipper Zero. --->  Not working yet, so please help!
+
+ ![rocketgod_logo](https://github.com/RocketGod-git/shodanbot/assets/57732082/7929b554-0fba-4c2b-b22d-6772d23c4a18)

+ 20 - 0
application.fam

@@ -0,0 +1,20 @@
+App(
+    appid="laser_tag",
+    name="Laser Tag",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="laser_tag_app",
+    cdefines=["APP_LASER_TAG"],
+    fap_category="Games",
+    fap_author="@RocketGod-git",
+    fap_version="0.1",
+    fap_description="Laser Tag game for Flipper Zero",
+    fap_icon="icons/laser_tag_10px.png",
+    fap_libs=["assets"],
+    fap_weburl="https://github.com/RocketGod-Git/Flipper-Zero-Laser-Tag",
+    requires=[
+        "gui",
+        "infrared",
+    ],
+    stack_size=2 * 1024,
+    order=10,
+)

+ 3 - 0
docs/CHANGELOG.md

@@ -0,0 +1,3 @@
+## v1.0
+
+- Initial release.

+ 3 - 0
docs/README.md

@@ -0,0 +1,3 @@
+# Flipper Zero - Laser Tag
+
+Not working, yet. 

+ 99 - 0
game_state.c

@@ -0,0 +1,99 @@
+#include "game_state.h"
+#include <furi.h>
+
+struct GameState {
+    LaserTagTeam team;
+    uint8_t health;
+    uint16_t score;
+    uint16_t ammo;
+    uint32_t game_time;
+    bool game_over;
+};
+
+GameState* game_state_alloc() {
+    GameState* state = malloc(sizeof(GameState));
+    state->team = TeamRed;
+    state->health = 100;
+    state->score = 0;
+    state->ammo = 100;
+    state->game_time = 0;
+    state->game_over = false;
+    return state;
+}
+
+void game_state_free(GameState* state) {
+    free(state);
+}
+
+void game_state_reset(GameState* state) {
+    state->health = 100;
+    state->score = 0;
+    state->ammo = 100;
+    state->game_time = 0;
+    state->game_over = false;
+}
+
+void game_state_set_team(GameState* state, LaserTagTeam team) {
+    state->team = team;
+}
+
+LaserTagTeam game_state_get_team(GameState* state) {
+    return state->team;
+}
+
+void game_state_decrease_health(GameState* state, uint8_t amount) {
+    if(state->health > amount) {
+        state->health -= amount;
+    } else {
+        state->health = 0;
+        state->game_over = true;
+    }
+}
+
+void game_state_increase_health(GameState* state, uint8_t amount) {
+    state->health = (state->health + amount > 100) ? 100 : state->health + amount;
+}
+
+uint8_t game_state_get_health(GameState* state) {
+    return state->health;
+}
+
+void game_state_increase_score(GameState* state, uint16_t points) {
+    state->score += points;
+}
+
+uint16_t game_state_get_score(GameState* state) {
+    return state->score;
+}
+
+void game_state_decrease_ammo(GameState* state, uint16_t amount) {
+    if(state->ammo > amount) {
+        state->ammo -= amount;
+    } else {
+        state->ammo = 0;
+    }
+}
+
+void game_state_increase_ammo(GameState* state, uint16_t amount) {
+    state->ammo += amount;
+}
+
+uint16_t game_state_get_ammo(GameState* state) {
+    return state->ammo;
+}
+
+void game_state_update_time(GameState* state, uint32_t delta_time) {
+    state->game_time += delta_time;
+}
+
+uint32_t game_state_get_time(GameState* state) {
+    return state->game_time;
+}
+
+bool game_state_is_game_over(GameState* state) {
+    return state->game_over;
+}
+
+void game_state_set_game_over(GameState* state, bool game_over) {
+    state->game_over = game_over;
+}

+ 44 - 0
game_state.h

@@ -0,0 +1,44 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+typedef enum {
+    TeamRed,
+    TeamBlue
+} LaserTagTeam;
+
+typedef enum {
+    LaserTagStateTeamSelect,
+    LaserTagStateGame,
+} LaserTagState;
+
+typedef struct GameState GameState;
+
+GameState* game_state_alloc();
+void game_state_free(GameState* state);
+void game_state_reset(GameState* state);
+
+void game_state_set_team(GameState* state, LaserTagTeam team);
+LaserTagTeam game_state_get_team(GameState* state);
+
+void game_state_decrease_health(GameState* state, uint8_t amount);
+void game_state_increase_health(GameState* state, uint8_t amount);
+uint8_t game_state_get_health(GameState* state);
+
+void game_state_increase_score(GameState* state, uint16_t points);
+uint16_t game_state_get_score(GameState* state);
+
+void game_state_decrease_ammo(GameState* state, uint16_t amount);
+void game_state_increase_ammo(GameState* state, uint16_t amount);
+uint16_t game_state_get_ammo(GameState* state);
+
+void game_state_update_time(GameState* state, uint32_t delta_time);
+uint32_t game_state_get_time(GameState* state);
+
+bool game_state_is_game_over(GameState* state);
+void game_state_set_game_over(GameState* state, bool game_over);
+
+#define INITIAL_HEALTH 100
+#define INITIAL_AMMO 100
+#define MAX_HEALTH 100

BIN
icons/laser_tag_10px.png


BIN
icons/toolkit.png


+ 112 - 0
infrared_controller.c

@@ -0,0 +1,112 @@
+#include "infrared_controller.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <infrared.h>
+#include <infrared_worker.h>
+#include <stdlib.h>
+
+#define TAG "LaserTagInfrared"
+
+#define IR_COMMAND_RED_TEAM 0xA1
+#define IR_COMMAND_BLUE_TEAM 0xB2
+
+struct InfraredController {
+    LaserTagTeam team;
+    InfraredWorker* worker;
+    FuriThread* rx_thread;
+    volatile bool rx_running;
+    volatile bool hit_received;
+};
+
+static void infrared_rx_callback(void* context, InfraredWorkerSignal* received_signal) {
+    InfraredController* controller = (InfraredController*)context;
+    
+    const InfraredMessage* message = infrared_worker_get_decoded_signal(received_signal);
+    if (message != NULL) {
+        uint32_t received_command = message->address;
+        if((controller->team == TeamRed && received_command == IR_COMMAND_BLUE_TEAM) ||
+           (controller->team == TeamBlue && received_command == IR_COMMAND_RED_TEAM)) {
+            controller->hit_received = true;
+        }
+    }
+}
+
+static int32_t infrared_rx_thread(void* context) {
+    InfraredController* controller = (InfraredController*)context;
+    
+    while(controller->rx_running) {
+        infrared_worker_rx_start(controller->worker);
+        furi_thread_flags_wait(0, FuriFlagWaitAny, 10);
+    }
+
+    return 0;
+}
+
+InfraredController* infrared_controller_alloc() {
+    InfraredController* controller = malloc(sizeof(InfraredController));
+    controller->team = TeamRed;
+    controller->worker = infrared_worker_alloc();
+    controller->rx_running = true;
+    controller->hit_received = false;
+
+    infrared_worker_rx_set_received_signal_callback(controller->worker, infrared_rx_callback, controller);
+
+    controller->rx_thread = furi_thread_alloc();
+    furi_thread_set_name(controller->rx_thread, "IR_Rx_Thread");
+    furi_thread_set_stack_size(controller->rx_thread, 1024);
+    furi_thread_set_context(controller->rx_thread, controller);
+    furi_thread_set_callback(controller->rx_thread, infrared_rx_thread);
+    furi_thread_start(controller->rx_thread);
+
+    infrared_worker_rx_start(controller->worker);
+
+    return controller;
+}
+
+void infrared_controller_free(InfraredController* controller) {
+    furi_assert(controller);
+
+    controller->rx_running = false;
+    furi_thread_join(controller->rx_thread);
+    furi_thread_free(controller->rx_thread);
+
+    infrared_worker_rx_stop(controller->worker);
+    infrared_worker_free(controller->worker);
+    free(controller);
+}
+
+void infrared_controller_set_team(InfraredController* controller, LaserTagTeam team) {
+    furi_assert(controller);
+    controller->team = team;
+}
+
+void infrared_controller_send(InfraredController* controller) {
+    furi_assert(controller);
+    uint32_t command = (controller->team == TeamRed) ? IR_COMMAND_RED_TEAM : IR_COMMAND_BLUE_TEAM;
+    InfraredMessage message = {
+        .protocol = InfraredProtocolNEC,
+        .address = 0x00,
+        .command = command,
+        .repeat = false
+    };
+    
+    infrared_worker_set_decoded_signal(controller->worker, &message);
+    
+    infrared_worker_tx_set_get_signal_callback(
+        controller->worker,
+        infrared_worker_tx_get_signal_steady_callback,
+        NULL);
+        
+    infrared_worker_tx_start(controller->worker);
+    
+    furi_delay_ms(250);  // Delay to ensure the signal is sent
+    
+    infrared_worker_tx_stop(controller->worker);
+}
+
+bool infrared_controller_receive(InfraredController* controller) {
+    furi_assert(controller);
+    bool hit = controller->hit_received;
+    controller->hit_received = false;
+    return hit;
+}

+ 42 - 0
infrared_controller.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <stdbool.h>
+#include "game_state.h"  // For LaserTagTeam enum
+
+typedef struct InfraredController InfraredController;
+
+/**
+ * Allocate and initialize an InfraredController.
+ * @return Pointer to the newly allocated InfraredController.
+ */
+InfraredController* infrared_controller_alloc();
+
+/**
+ * Free an InfraredController and its resources.
+ * @param controller Pointer to the InfraredController to free.
+ */
+void infrared_controller_free(InfraredController* controller);
+
+/**
+ * Set the team for the InfraredController.
+ * @param controller Pointer to the InfraredController.
+ * @param team The team to set (TeamRed or TeamBlue).
+ */
+void infrared_controller_set_team(InfraredController* controller, LaserTagTeam team);
+
+/**
+ * Send an infrared signal corresponding to the controller's team.
+ * @param controller Pointer to the InfraredController.
+ */
+void infrared_controller_send(InfraredController* controller);
+
+/**
+ * Check if a hit has been received from the opposite team.
+ * @param controller Pointer to the InfraredController.
+ * @return true if a hit was received, false otherwise.
+ */
+bool infrared_controller_receive(InfraredController* controller);
+
+// IR command definitions
+#define IR_COMMAND_RED_TEAM 0xA1
+#define IR_COMMAND_BLUE_TEAM 0xB2

+ 174 - 0
laser_tag_app.c

@@ -0,0 +1,174 @@
+#include "laser_tag_app.h"
+#include "laser_tag_view.h"
+#include "infrared_controller.h"
+#include "game_state.h"
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <input/input.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+
+#define TAG "LaserTag"
+
+struct LaserTagApp {
+    Gui* gui;
+    ViewPort* view_port;
+    LaserTagView* view;
+    FuriMessageQueue* event_queue;
+    FuriTimer* timer;
+    NotificationApp* notifications;
+    InfraredController* ir_controller;
+    GameState* game_state;
+    LaserTagState state;
+};
+
+const NotificationSequence sequence_vibro_1 = {
+    &message_vibro_on,
+    &message_vibro_off,
+    NULL
+};
+
+static void laser_tag_app_timer_callback(void* context) {
+    furi_assert(context);
+    LaserTagApp* app = context;
+    game_state_update_time(app->game_state, 1);
+    laser_tag_view_update(app->view, app->game_state);
+}
+
+static void laser_tag_app_input_callback(InputEvent* input_event, void* context) {
+    furi_assert(context);
+    LaserTagApp* app = context;
+    furi_message_queue_put(app->event_queue, input_event, FuriWaitForever);
+}
+
+static void laser_tag_app_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    LaserTagApp* app = context;
+    if(app->state == LaserTagStateTeamSelect) {
+        canvas_clear(canvas);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 32, 32, "Select Team:");
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 32, 48, "LEFT: Red  RIGHT: Blue");
+    } else {
+        laser_tag_view_draw(laser_tag_view_get_view(app->view), canvas);
+    }
+}
+
+LaserTagApp* laser_tag_app_alloc() {
+    LaserTagApp* app = malloc(sizeof(LaserTagApp));
+
+    app->gui = furi_record_open(RECORD_GUI);
+    app->view_port = view_port_alloc();
+    app->view = laser_tag_view_alloc();
+    app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+    app->ir_controller = infrared_controller_alloc();
+    app->game_state = game_state_alloc();
+    app->state = LaserTagStateTeamSelect;
+
+    view_port_draw_callback_set(app->view_port, laser_tag_app_draw_callback, app);
+    view_port_input_callback_set(app->view_port, laser_tag_app_input_callback, app);
+    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
+
+    app->timer = furi_timer_alloc(laser_tag_app_timer_callback, FuriTimerTypePeriodic, app);
+    furi_timer_start(app->timer, furi_kernel_get_tick_frequency());
+
+    return app;
+}
+
+void laser_tag_app_free(LaserTagApp* app) {
+    furi_assert(app);
+
+    furi_timer_free(app->timer);
+    view_port_enabled_set(app->view_port, false);
+    gui_remove_view_port(app->gui, app->view_port);
+    view_port_free(app->view_port);
+    laser_tag_view_free(app->view);
+    furi_message_queue_free(app->event_queue);
+    infrared_controller_free(app->ir_controller);
+    game_state_free(app->game_state);
+
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_NOTIFICATION);
+
+    free(app);
+}
+
+void laser_tag_app_fire(LaserTagApp* app) {
+    infrared_controller_send(app->ir_controller);
+    game_state_decrease_ammo(app->game_state, 1);
+    notification_message(app->notifications, &sequence_blink_blue_100);
+}
+
+void laser_tag_app_handle_hit(LaserTagApp* app) {
+    game_state_decrease_health(app->game_state, 10);
+    notification_message(app->notifications, &sequence_vibro_1);
+}
+
+void laser_tag_app_enter_game_state(LaserTagApp* app) {
+    app->state = LaserTagStateGame;
+    game_state_reset(app->game_state);
+    laser_tag_view_update(app->view, app->game_state);
+}
+
+int32_t laser_tag_app(void* p) {
+    UNUSED(p);
+    LaserTagApp* app = laser_tag_app_alloc();
+
+    InputEvent event;
+    bool running = true;
+    while(running) {
+        FuriStatus status = furi_message_queue_get(app->event_queue, &event, 100);
+        if(status == FuriStatusOk) {
+            if(event.type == InputTypePress) {
+                if(app->state == LaserTagStateTeamSelect) {
+                    switch(event.key) {
+                    case InputKeyLeft:
+                        infrared_controller_set_team(app->ir_controller, TeamRed);
+                        game_state_set_team(app->game_state, TeamRed);
+                        laser_tag_app_enter_game_state(app);
+                        break;
+                    case InputKeyRight:
+                        infrared_controller_set_team(app->ir_controller, TeamBlue);
+                        game_state_set_team(app->game_state, TeamBlue);
+                        laser_tag_app_enter_game_state(app);
+                        break;
+                    default:
+                        break;
+                    }
+                } else {
+                    switch(event.key) {
+                    case InputKeyBack:
+                        running = false;
+                        break;
+                    case InputKeyOk:
+                        laser_tag_app_fire(app);
+                        break;
+                    default:
+                        break;
+                    }
+                }
+            }
+        }
+
+        if(app->state == LaserTagStateGame) {
+            if(infrared_controller_receive(app->ir_controller)) {
+                laser_tag_app_handle_hit(app);
+            }
+
+            if(game_state_is_game_over(app->game_state)) {
+                notification_message(app->notifications, &sequence_error);
+                running = false;
+            }
+
+            laser_tag_view_update(app->view, app->game_state);
+        }
+
+        view_port_update(app->view_port);
+    }
+
+    laser_tag_app_free(app);
+    return 0;
+}

+ 31 - 0
laser_tag_app.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include <notification/notification_messages.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/button_menu.h>
+
+#define FRAME_WIDTH 128
+#define FRAME_HEIGHT 64
+
+typedef struct LaserTagApp LaserTagApp;
+
+LaserTagApp* laser_tag_app_alloc();
+
+void laser_tag_app_free(LaserTagApp* app);
+
+int32_t laser_tag_app(void* p);
+
+void laser_tag_app_set_view_port(LaserTagApp* app, View* view);
+
+void laser_tag_app_switch_to_next_scene(LaserTagApp* app);
+
+void laser_tag_app_fire(LaserTagApp* app);
+
+void laser_tag_app_handle_hit(LaserTagApp* app);

+ 123 - 0
laser_tag_icons.c

@@ -0,0 +1,123 @@
+#include "laser_tag_icons.h"
+#include <gui/icon_i.h>
+
+const uint8_t laser_gun_icon_data[] = {
+    0b00000000, 0b00000000,
+    0b00000001, 0b10000000,
+    0b00000011, 0b11000000,
+    0b00000111, 0b11100000,
+    0b00001111, 0b11110000,
+    0b00011111, 0b11111000,
+    0b11111111, 0b11111110,
+    0b11111111, 0b11111111,
+};
+
+const uint8_t health_icon_data[] = {
+    0b00001100, 0b00110000,
+    0b00011110, 0b01111000,
+    0b00111111, 0b11111100,
+    0b01111111, 0b11111110,
+    0b01111111, 0b11111110,
+    0b00111111, 0b11111100,
+    0b00011111, 0b11111000,
+    0b00000111, 0b11100000,
+};
+
+const uint8_t ammo_icon_data[] = {
+    0b00011000, 0b00011000,
+    0b00111100, 0b00111100,
+    0b01111110, 0b01111110,
+    0b11111111, 0b11111111,
+    0b11111111, 0b11111111,
+    0b01111110, 0b01111110,
+    0b00111100, 0b00111100,
+    0b00011000, 0b00011000,
+};
+
+const uint8_t team_red_icon_data[] = {
+    0b00011000, 0b00011000,
+    0b00111100, 0b00111100,
+    0b01111110, 0b01111110,
+    0b11111111, 0b11111111,
+    0b11111111, 0b11111111,
+    0b01111110, 0b01111110,
+    0b00111100, 0b00111100,
+    0b00011000, 0b00011000,
+};
+
+const uint8_t team_blue_icon_data[] = {
+    0b11100111, 0b11100111,
+    0b11000011, 0b11000011,
+    0b10000001, 0b10000001,
+    0b00000000, 0b00000000,
+    0b00000000, 0b00000000,
+    0b10000001, 0b10000001,
+    0b11000011, 0b11000011,
+    0b11100111, 0b11100111,
+};
+
+const uint8_t game_over_icon_data[] = {
+    0b11111111, 0b11111111,
+    0b10000000, 0b00000001,
+    0b10111101, 0b10111101,
+    0b10100001, 0b10100001,
+    0b10100001, 0b10100001,
+    0b10111101, 0b10111101,
+    0b10000000, 0b00000001,
+    0b11111111, 0b11111111,
+};
+
+const uint8_t* const laser_gun_icon_frames[] = {laser_gun_icon_data};
+const uint8_t* const health_icon_frames[] = {health_icon_data};
+const uint8_t* const ammo_icon_frames[] = {ammo_icon_data};
+const uint8_t* const team_red_icon_frames[] = {team_red_icon_data};
+const uint8_t* const team_blue_icon_frames[] = {team_blue_icon_data};
+const uint8_t* const game_over_icon_frames[] = {game_over_icon_data};
+
+const Icon I_laser_gun_icon = {
+    .width = 16,
+    .height = 8,
+    .frame_count = 1,
+    .frame_rate = 0,
+    .frames = laser_gun_icon_frames,
+};
+
+const Icon I_health_icon = {
+    .width = 16,
+    .height = 8,
+    .frame_count = 1,
+    .frame_rate = 0,
+    .frames = health_icon_frames,
+};
+
+const Icon I_ammo_icon = {
+    .width = 16,
+    .height = 8,
+    .frame_count = 1,
+    .frame_rate = 0,
+    .frames = ammo_icon_frames,
+};
+
+const Icon I_team_red_icon = {
+    .width = 16,
+    .height = 8,
+    .frame_count = 1,
+    .frame_rate = 0,
+    .frames = team_red_icon_frames,
+};
+
+const Icon I_team_blue_icon = {
+    .width = 16,
+    .height = 8,
+    .frame_count = 1,
+    .frame_rate = 0,
+    .frames = team_blue_icon_frames,
+};
+
+const Icon I_game_over_icon = {
+    .width = 16,
+    .height = 8,
+    .frame_count = 1,
+    .frame_rate = 0,
+    .frames = game_over_icon_frames,
+};

+ 17 - 0
laser_tag_icons.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include <gui/icon.h>
+
+extern const Icon I_laser_gun_icon;
+extern const Icon I_health_icon;
+extern const Icon I_ammo_icon;
+extern const Icon I_team_red_icon;
+extern const Icon I_team_blue_icon;
+extern const Icon I_game_over_icon;
+
+#define LASER_GUN_ICON (&I_laser_gun_icon)
+#define HEALTH_ICON (&I_health_icon)
+#define AMMO_ICON (&I_ammo_icon)
+#define TEAM_RED_ICON (&I_team_red_icon)
+#define TEAM_BLUE_ICON (&I_team_blue_icon)
+#define GAME_OVER_ICON (&I_game_over_icon)

+ 91 - 0
laser_tag_view.c

@@ -0,0 +1,91 @@
+#include "laser_tag_view.h"
+#include "laser_tag_icons.h"
+#include <furi.h>
+#include <gui/elements.h>
+
+struct LaserTagView {
+    View* view;
+};
+
+typedef struct {
+    LaserTagTeam team;
+    uint8_t health;
+    uint16_t ammo;
+    uint16_t score;
+    uint32_t game_time;
+    bool game_over;
+} LaserTagViewModel;
+
+static void laser_tag_view_draw_callback(Canvas* canvas, void* model) {
+    LaserTagViewModel* m = model;
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_icon(canvas, 0, 0, m->team == TeamRed ? TEAM_RED_ICON : TEAM_BLUE_ICON);
+    canvas_draw_icon(canvas, 0, 10, HEALTH_ICON);
+    canvas_draw_str_aligned(canvas, 20, 14, AlignLeft, AlignBottom, furi_string_get_cstr(furi_string_alloc_printf("%d", m->health)));
+    canvas_draw_icon(canvas, 0, 20, AMMO_ICON);
+    canvas_draw_str_aligned(canvas, 20, 24, AlignLeft, AlignBottom, furi_string_get_cstr(furi_string_alloc_printf("%d", m->ammo)));
+    canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignBottom, "Score:");
+    canvas_draw_str_aligned(canvas, 64, 20, AlignCenter, AlignBottom, furi_string_get_cstr(furi_string_alloc_printf("%d", m->score)));
+    uint32_t minutes = m->game_time / 60;
+    uint32_t seconds = m->game_time % 60;
+    canvas_draw_str_aligned(canvas, 64, 40, AlignCenter, AlignBottom, furi_string_get_cstr(furi_string_alloc_printf("%02ld:%02ld", minutes, seconds)));
+    canvas_draw_icon(canvas, 112, 0, LASER_GUN_ICON);
+
+    if(m->game_over) {
+        canvas_draw_icon(canvas, 56, 28, GAME_OVER_ICON);
+    }
+}
+
+static bool laser_tag_view_input_callback(InputEvent* event, void* context) {
+    UNUSED(event);
+    UNUSED(context);
+    return false;
+}
+
+LaserTagView* laser_tag_view_alloc() {
+    LaserTagView* laser_tag_view = malloc(sizeof(LaserTagView));
+    laser_tag_view->view = view_alloc();
+    view_set_context(laser_tag_view->view, laser_tag_view);
+    view_allocate_model(laser_tag_view->view, ViewModelTypeLocking, sizeof(LaserTagViewModel));
+    view_set_draw_callback(laser_tag_view->view, laser_tag_view_draw_callback);
+    view_set_input_callback(laser_tag_view->view, laser_tag_view_input_callback);
+    return laser_tag_view;
+}
+
+void laser_tag_view_draw(View* view, Canvas* canvas) {
+    LaserTagViewModel* model = view_get_model(view);
+    laser_tag_view_draw_callback(canvas, model);
+    view_commit_model(view, false);
+}
+
+void laser_tag_view_free(LaserTagView* laser_tag_view) {
+    furi_assert(laser_tag_view);
+    view_free(laser_tag_view->view);
+    free(laser_tag_view);
+}
+
+View* laser_tag_view_get_view(LaserTagView* laser_tag_view) {
+    furi_assert(laser_tag_view);
+    return laser_tag_view->view;
+}
+
+void laser_tag_view_update(LaserTagView* laser_tag_view, GameState* game_state) {
+    furi_assert(laser_tag_view);
+    furi_assert(game_state);
+
+    with_view_model(
+        laser_tag_view->view,
+        LaserTagViewModel * model,
+        {
+            model->team = game_state_get_team(game_state);
+            model->health = game_state_get_health(game_state);
+            model->ammo = game_state_get_ammo(game_state);
+            model->score = game_state_get_score(game_state);
+            model->game_time = game_state_get_time(game_state);
+            model->game_over = game_state_is_game_over(game_state);
+        },
+        true);
+}

+ 16 - 0
laser_tag_view.h

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gui/view.h>
+#include "game_state.h"
+
+typedef struct LaserTagView LaserTagView;
+
+LaserTagView* laser_tag_view_alloc();
+
+void laser_tag_view_draw(View* view, Canvas* canvas);
+
+void laser_tag_view_free(LaserTagView* laser_tag_view);
+
+View* laser_tag_view_get_view(LaserTagView* laser_tag_view);
+
+void laser_tag_view_update(LaserTagView* laser_tag_view, GameState* game_state);

+ 19 - 0
manifest.yml

@@ -0,0 +1,19 @@
+# Manifest required for the Flipper Application Catalog
+# https://github.com/flipperdevices/flipper-application-catalog/blob/main/documentation/Manifest.md
+author: "@CodyTolene"
+category: "Sub-GHz"
+changelog: "@./docs/CHANGELOG.md"
+description: "@./docs/README.md"
+icon: "icons/toolkit.png"
+id: "toolkit"
+name: "Development Toolkit"
+screenshots:
+  - "screenshots/todo.png"
+short_description: "A Flipper Zero Development Toolkit for all to use!"
+sourcecode:
+  location:
+    commit_sha: ... # Update this with the latest commit sha
+    origin: https://github.com/CodyTolene/Flipper-Zero-Development-Toolkit.git
+    subdir: src-fap
+  type: git
+version: 1.0

BIN
screenshots/todo.png