From add7b357e1f6823890e6e2f884c6f18b6dd45efa Mon Sep 17 00:00:00 2001 From: Sergei Gavrilov Date: Tue, 13 Feb 2024 05:12:06 +1000 Subject: [PATCH] Add Air Arkanoid (#141) --- application.fam | 16 ++ assets/sprites/logo_air.fxbm | Bin 0 -> 102 bytes assets/sprites/logo_arkanoid.fxbm | Bin 0 -> 327 bytes engine | 1 + fonts/fonts.c | 25 +++ fonts/fonts.h | 4 + game.c | 73 ++++++ game.h | 35 +++ game_settings.c | 21 ++ game_settings.h | 6 + icon.png | Bin 0 -> 7884 bytes levels/level_game.c | 357 ++++++++++++++++++++++++++++++ levels/level_game.h | 4 + levels/level_menu.c | 201 +++++++++++++++++ levels/level_menu.h | 4 + levels/level_message.c | 57 +++++ levels/level_message.h | 8 + levels/level_settings.c | 223 +++++++++++++++++++ levels/level_settings.h | 4 + sprites/logo_air.png | Bin 0 -> 1934 bytes sprites/logo_arkanoid.png | Bin 0 -> 2330 bytes 21 files changed, 1039 insertions(+) create mode 100644 application.fam create mode 100644 assets/sprites/logo_air.fxbm create mode 100644 assets/sprites/logo_arkanoid.fxbm create mode 160000 engine create mode 100644 fonts/fonts.c create mode 100644 fonts/fonts.h create mode 100644 game.c create mode 100644 game.h create mode 100644 game_settings.c create mode 100644 game_settings.h create mode 100644 icon.png create mode 100644 levels/level_game.c create mode 100644 levels/level_game.h create mode 100644 levels/level_menu.c create mode 100644 levels/level_menu.h create mode 100644 levels/level_message.c create mode 100644 levels/level_message.h create mode 100644 levels/level_settings.c create mode 100644 levels/level_settings.h create mode 100644 sprites/logo_air.png create mode 100644 sprites/logo_arkanoid.png diff --git a/application.fam b/application.fam new file mode 100644 index 00000000000..6927dfa2947 --- /dev/null +++ b/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/assets/sprites/logo_air.fxbm b/assets/sprites/logo_air.fxbm new file mode 100644 index 0000000000000000000000000000000000000000..96579a376fdd4ae57750dba0439f20b83deac0c1 GIT binary patch literal 102 zcmYddU|>)NVj&=I0Ahv%%zQwCU7msA0fdlaVEDk!aGv=CKl^|7AN>44f)7a4$ne*( l|Fh?}VE_~L7X3C1{1)$lM8$s_2KE9lQD?)zkYFIg004WF8D{_h literal 0 HcmV?d00001 diff --git a/assets/sprites/logo_arkanoid.fxbm b/assets/sprites/logo_arkanoid.fxbm new file mode 100644 index 0000000000000000000000000000000000000000..6a2a72f32bba4aeb6e4e188a46b140b8eb8f6daf GIT binary patch literal 327 zcmaKmu};G<7=+L2;057{p^|G}qr*=;^hNpz9T-w6O5nXZxKEH*FzS#IM;&4*hMzwY zAP^EK-SFMra61J&0=N_Zr^D|u;pu0P5LWn_5Hf;B`W!TZQ6v6EE78&>)4wRAErOhB z1bKMwjdjV~gbw+Zhx6;bt%|3_$c~3`+MIl0%8asUhjFHpuQF{jWE1&F#k)il@xEYP zm7}1|s@hWQ>bBG_lP}60q@BA`w@qPQ8Ns)So6Nq\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/fonts/fonts.h b/fonts/fonts.h new file mode 100644 index 00000000000..9e1c74ac1b6 --- /dev/null +++ b/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/game.c b/game.c new file mode 100644 index 00000000000..82e915bd2a6 --- /dev/null +++ b/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/game.h b/game.h new file mode 100644 index 00000000000..36f591bc33e --- /dev/null +++ b/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/game_settings.c b/game_settings.c new file mode 100644 index 00000000000..713a49d3b02 --- /dev/null +++ b/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/game_settings.h b/game_settings.h new file mode 100644 index 00000000000..cec0b5dfe36 --- /dev/null +++ b/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/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c35a27614013c73a40c642fb81d104e051c46f1f GIT binary patch literal 7884 zcmeHLc|4Tu*PpD}M<^7H870e@9W$1)Qzm1N(|g8>DCR*+_~_3XXSI!*fIy3HBkD+w^u$I3YOPrc#khi zeVS3(q<_f6s&UEGh^Ij3ZPn`Kg0AtCvoE*8HhSMM%Ki0d`6PR0l+_=mcllMK)>!9` z>CMMa+`fESwU(vaTCg28i1jeE#^i|d)QYoDUMw~<%xkT@RJNKz_E)#a59{G0%C8)R z#U)nYlF$g-19w%gN5Hy+cok0|0}q-Klyn)JFL>`;#?FPo#v4aJ=uLziDH-iMnA=vn zr9dF4^R9BMaY;d0-Vuz|&1k%uDSmNpbuV#r3TZZ8J~Z;`j}`BxFLc~H-?`VbJyY@7 zX5_Q#%vre+Lhm(JseBoLdP6&$HftE8zv#T^`GGoi3|~(f(wT3QsE0fn-*f7EqjHf< z0!r&ORN%62(b_-SCe}?Csc2`J?~ zjYxfQc)7r*p&V-Ph;u!y(ltlICS-%#2fMi#&`(deT20Hx%u*6+cI-Dv zuP`6B!WxP$RAjYsBo3u7EM8A<9$F!$Wd&1#Xqn<~D62zZPtpB!2Ml9lK?7^wx}DRf zc8ErszK}e!K+5ir)LGpMku#a9ZxN2^)`t)}j5`Kr0&1Qfc7K{vNk=s5tMNqYr zk!zOiX*2r0IVW^_ud&m)5`7Bk@u@xW#d4w6TM*Hw?l+pILT7cH*U|OA zz`M<;s!GT(fWAfkn4evtc2&JsV)=%ZO9IBt-dC6HVkGD8ZoK*EgF%qH=Wv?&3AC&Hus0!LML|BDC@#snp3~qjGax-9FDqGPutxsw*|hY; z<-Kld@>gc8LT_?+-71NARwC7nY|=zn6=>z5s-CWHdN^yr)vPWlPL(WMl%`0kKj*~+SJvX2s}|GFl`DE`)Q_6>gMtVd4Qpxl)% z>!@pf#UWiK@a^HW1{Z10v64HZH6GAc!sg3ekP5J#~34ND4eU3-+-gy0X9J&rw z^jgwKyK_Zxg@{FRtcC?7r3EA3kf~O(ejzsHs#V{x#?qnDv+pcjyLF<|YQrCjeR?TH6^UU(xQJd;4_CXDt&bRLoXhza8~Qix+JU`V8nqpwzF(8E)BU%-3qn13PzcM=w^j7K<0@mZ zC{%iynMo{)z4YZpBYHycsFnh%Z{66I_C3?|2uYc#ES)zY(<|%a4J3KLT7~p5^RW$s zDzA_!;d%{sTra#FRH{*+*}{S*-5}!k)HDp=bgZ3P;o3~&{Pv`4tr*fgQ6#Q>ahplU zS)kxhQY*ijmniTf-RoO7u4_^798!KkU9`T}xPI$~3HXLH4Po*8&@5}$j{V4}x98{u zy%oFEyF2#o`?Yk-(_`_Dmr?a~gbx2}gU|Gb`|iY6vWeSB4W5CS(Xr>Fn@d!ZiMGMa9f;gPB3Z&0%GSO()l6$(`=G?N{1jAYR&>VJ(r{ zk#M%bcxXAgXNhcRu)m^5t+Hi>1S{kAYlxZsez+%hc(fty^8Ow8(wmP1;2~REjL4D! z{yogoAx;%4zccmz@DF1?Gao-wSN$@oiC=xLf%snL{#uy}{N31+JQ?Sv#hQb=%WufX zb?!NEfpb6rPd@j`T~Jm!^F-`fI5Q>WbS#96vJy z->ipG^-=d@omQnjB8R1xG7qFXMd++rfo9!0oN~!(%5qWSreN_w+k=6waqX8DwzFe0 zSZf`d6Wtq7+O|2<+E9 zZk$}TpZ_{F9*SN)F)$|5=R4pFEAMBk|7+D%ksywv#Y6A5#Ma4oE*dpKo>?kqdP}xF zNAZ=M&WS%r53)=19`ioxJvI~faO=y~q?{#_FS$?NQje(0yVf{q`rpu0dXoLN-@mZf2zmv{O-<1Fw~TaK!H3WTM96qTIA*5&u&ZgYD?GnwdQ9K4pZH{~Jxk z-Ys@DE5$+&*To#1>Wr7VA}hP?>8_`nz1bq2aTV`c0CqpM8*OBfB0uNKIwT&|$kLAReo@rvjLZu@{Cn+a zOFN6i-9V~j4GgdlSB+OfE|t_2;19GOi}(QT0bVmEy)|pSmu#C+y)}!toA-f`i@seE{r^P+5PuJW;X#Axo&-Rssm>-H`gOT1g>&(av>UfdbK z?Bx4Y|7`cH!*^MIHb-Vs^k$qgFW&X&a(yyvap>ymE;gC*DPxca&IhOENSWxkxV{jki5ralgb{lR=ad5>2Sb1acA`m$+jAI6E<~Fx4UZR0!d6IHq@2fEitm?2~_{4-Yep( z!;VX|w+_`l`Pb5s&57c$7Ny-@FdqEcS#4wg;y*TU<3nE#zFy$4-aBP!e?L^U)qT3Q zsjjXken#L_d1blxiO3wWdq>qE5GacYz6$s_JCSJYa6<~6O$7{N!a3lp0t8~VI)+1` zg#tVn6$oas%;Dp8SKu%v-5l;sa7H_Gtbq`wLtG@_9_QjgiwmU@>G0JS(q=Iv5Fi}j zQD8CQVJt2w#vDF}O9ID2F$xZwGvS4r!+pSVux3XBFuWn&5RJ5rVMb%&7Sb@YNIHY$ zW@GmW0=zPZhwykD5(>rV^9}hpLv~~^3PU6kQD`g*i$#JKNNy~PM~OkQxCkM{JcbRx zrA0D1JSLk36Jk=R>?oc&91iMXpZyExI6HrVXK_EV0P=x~p>R+bLo_Np9QExCF3&a^ z1o`C9-_GEAfNxYNH-O8IilhOy(Ey8w_!dIA5xyV@LkebwbLN5q$@^|GI_--cCn_>* z&W27y0bxKm2+9Th!+d8ie2e=s<2&Dknz?Ykb%Ybb=CQdUY|h`$`J(>IL>`mz59}U?h8qR~yNyuRo9)lejZVq>*&|t3aUu`{@;eb1jB4iC?L?jY$ zMkYACu@M1_BYZXT0wTF!WeY(uXhR%!PD7`W>_A8gSfR{tN-%)pu!83l!u*r0*>!B ztjwu=zUs{3##q!m0dp$fzg2;GCRs-U6dpU$gUt^66oHWKImd-+XRxKwDLje|g$G8D zMPqR!42FazdSHx5I2?(9*GJ<>Uki!^vczP>{&&{G1_m<&@xkrjz~qAK$Igl7yP7)? zF@H6G8OEGzXE4}YV#=y{!U~Qv~JTS&2H2C*9G}QmV87?LI&z{giD6C)ryq$dN zU!Of>{>`4n6LC}=6-z`?Ou%lBCou3xB9%r%5;1ra0*+!tq2cGZ{XfNN5B3o-HVhiJ zYJRKf{$q?73{sdrFn2@}{{JvW24Dox7&sHKj}URfW`aSQm=LK*V>E?|CsODpc$&%g zPX8&FKOW=vCHg6kW+>sE@^g)uq5e{Z^9JACTEYC!%fMSU_ymCZ@(A#$Fu|1b_uT>hZxN0{`HR z)c@*0XjS^p1EK$i1EE?Sjt6+^BSBqfZ!>rP19@v*w+$RgaU6WP5QvPD@VfwVG)EN_ zO7a|?Z6!x!q~x@WlFq(Z1&YiaZ7e-hAM86|DGh3gf0~ z_q^J;#!NNss90v{O@nMvzHf|aYCvBF>LLkUDhfZWDQ>=A&>~#g(bmPL%xYu8zX4oe BRBiwO literal 0 HcmV?d00001 diff --git a/levels/level_game.c b/levels/level_game.c new file mode 100644 index 00000000000..63528950eee --- /dev/null +++ b/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/levels/level_game.h b/levels/level_game.h new file mode 100644 index 00000000000..3eddc72c2b4 --- /dev/null +++ b/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/levels/level_menu.c b/levels/level_menu.c new file mode 100644 index 00000000000..c50e23a65f7 --- /dev/null +++ b/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/levels/level_menu.h b/levels/level_menu.h new file mode 100644 index 00000000000..4f57f22e258 --- /dev/null +++ b/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/levels/level_message.c b/levels/level_message.c new file mode 100644 index 00000000000..3c835bb1d3b --- /dev/null +++ b/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/levels/level_message.h b/levels/level_message.h new file mode 100644 index 00000000000..60e273c5999 --- /dev/null +++ b/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/levels/level_settings.c b/levels/level_settings.c new file mode 100644 index 00000000000..46112d94f8a --- /dev/null +++ b/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/levels/level_settings.h b/levels/level_settings.h new file mode 100644 index 00000000000..00cba9de864 --- /dev/null +++ b/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/sprites/logo_air.png b/sprites/logo_air.png new file mode 100644 index 0000000000000000000000000000000000000000..71fa93382f93805f68097d601cdc927046ed4310 GIT binary patch literal 1934 zcmah~3rrJd96z5CHXJBZhlE^8VkQyG~b(uAQQ$>d4*h|UPl-in}{?qWkBSz z*&&V~$`qHKG_C?1S_Ui>t-=O6yD*e8sjy`-y;yHogK{df+5whUXXhEKR~Z#1EG0Qu z=^~(j4R9ptvRP@CaH%jsmw^BIZ5%@d3b#sy73vMBnsESBCX$K7SaL9`bePOUuI8O# zW4KabfsBD2|j;xs#adYA{?McVOrkr*FIwwE&;!Kj|9+RuJ!s?pTcGvbr6g!}F# zxbWRZxjpIJc|syMDIo zNz2bC`qw3HEsz#~Ii@2DDG~O_)7$_2(z~v<-i+yy=|?vm&U{-}w5GH7TF>pseFe|h zq2lM8&23lP!XooSr1O*uX015?)?_B=+x-iG_NnGrpZm4oD@oPe*XM0$64fMGPq#+h zlLrkpbtT>3c*(szZ0okEXZJq5<@sROwx{3RxwEOK^1gggt1h(16Mn6JM(ggd?Hg{8 zwJVBpV!8$v*X(%nTt1M`FFm5$9Iog{Kfk0x-zw^eiPoRGIdtV@@!wZ#OMahTe;}i8 z?akU-`lpM2dpuZYtBFkO9k|bk|D` zgg&`NDCelvvv#!ap-=t!t0y_b8CufR-cjGY{D%8s!^ZdC=(L=iJAM3e_yP*DXBM#t zGP#F;#%zh5JQHs2)oN1nk{S-?_f4xsC$0Z@Xy}JWhBL^I+M4{up|jNK%Au^mu`%w%c6vquk)hGuZK-409()se2fqB3`yQFI1nET^L);wdWja{nesIrJ_wf0DP~d*6HSe%WF-s{S}8osSH)AAWq2~7NT=|a^oUqZ`h1O83kL=`ODrfLFyagb zS&Rk~jasCzjTZ&q*~bVB*;JVMQaD}}3CTzbhkUudTpk?Y3`r=h4vm&iYc~dx6i#Ls zGm0Rosj1vlA(y1|2wyA~BRl~j5cmKMA3Dv%U=|+}?a5NuIpjF4p$IcWkS2)b#FEGq zMhe3KhrUE+G0Q z7gL+_7%+jrgZ(jEP*iEe^!O}7%OrP_bpqkINzWqfL^gn}iy?&_gqbD+R8n9Izy`&4 zVp3QvLb~`-_kh^Zf+SiE$`FhJw@LO<3onMY3ZK2_ONN$DPvMBsCcablqF0 zy(^KfX?zJwg7gU7o^_Yi1nlh3z{&%=6X{rDO-A!UPgl03@oe4y|`g==k9RnnFHt)78lhP=Y8%~RG!^(*qTE)xEtJ9Vo_nY-`c zmeQJY6%HA_67hX*8*+~L9tg6gZ5>`$+<5#UUHN)z{qGN#`xV3qWS$nVM@#tM~Z|TnoA9Hkj?(A_LJ#(ONyu^FtoQuN- zkxoZ9@(NuGHV(P4^F_0D zN%6)vhg(|K)GTZjO)d?0sj<4<%Nkm`&9!i46Si!QI%-_y)2SJo-RoxJq6za3hUd76 zPlQ~Yo}wz{o*3t;I{WDJ?b8YG?kr2Z>5)|y+PL)5vS!tr$v0lT&opL?4zBG}te^gN z(tF;AiRlM+I=m`bFe(BGOGp}VNmSByD>D3kVwub9W>hjpCL6iAqQrFe$#rW$XsSzM zZpDeLEwh{QUaZZYJNTsj^jMF+v%#;VlUbprIh;W??CF(1Y0wBzp=_l*C^mk@llVW@ zRYL>z?)dZ>In>{)U{uRlkz8GNX?xg&2Hmr4h4=loTB#+_cxL=h^5#C4=C&;UnCx@Z ziU@A_JGlQ?pIF^vlIxj>J^y@pQGUJU?yB6w`rl=oPMnb0Rv)zfzW4S5evIdcs9ASo zLfj;McD_%73tvaCevoC&JayHr_VmN@*oLM$)t2Goc2E31eMi${$qh+Fbg#^oQ*LjO z(*=I&qei{d>(^pO_3;^P^4juE_qMKov+TLsQcf7JVMi~+zFAtu#{2vnq BP)z^; literal 0 HcmV?d00001