pinball0.cxx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. #include <furi.h>
  2. #include <flipper_format/flipper_format.h>
  3. #include <notification/notification.h>
  4. #include <cstring>
  5. #include "pinball0.h"
  6. #include "notifications.h"
  7. /* generated by fbt from .png files in images folder */
  8. #include <pinball0_icons.h>
  9. // Gravity should be lower than 9.8 m/s^2 since the ball is on
  10. // an angled table. We could calc this and derive the actual
  11. // vertical vector based on the angle of the table yadda yadda yadda
  12. #define GRAVITY 3.0f // 9.8f
  13. #define PHYSICS_SUB_STEPS 5
  14. #define GAME_FPS 30
  15. #define TABLE_BUMP_AMOUNT 0.3l
  16. #define MANUAL_ADJUSTMENT 20
  17. #define PINBALL_SETTINGS_FILENAME ".pinball0.conf"
  18. #define PINBALL_SETTINGS_PATH APP_DATA_PATH(PINBALL_SETTINGS_FILENAME)
  19. #define PINBALL_SETTINGS_FILE_TYPE "Pinball0 Settings File"
  20. #define PINBALL_SETTINGS_FILE_VERSION 1
  21. void pinball_load_settings(PinballApp* pb) {
  22. FlipperFormat* fff_settings = flipper_format_file_alloc(pb->storage);
  23. FuriString* tmp_str = furi_string_alloc();
  24. uint32_t tmp_data32 = 0;
  25. // init the settings to default values, then overwrite them if
  26. // they appear in the settings file
  27. pb->settings.sound_enabled = true;
  28. pb->settings.led_enabled = false;
  29. pb->settings.vibrate_enabled = false;
  30. pb->settings.manual_mode = true;
  31. pb->selected_setting = 0;
  32. pb->max_settings = 4;
  33. do {
  34. if(!flipper_format_file_open_existing(fff_settings, PINBALL_SETTINGS_PATH)) {
  35. FURI_LOG_I(TAG, "SETTINGS: File not found, using defaults");
  36. break;
  37. }
  38. if(!flipper_format_read_header(fff_settings, tmp_str, &tmp_data32)) {
  39. FURI_LOG_E(TAG, "SETTINGS: Missing or incorrect header");
  40. break;
  41. }
  42. // do settings file version here? eh..
  43. if(flipper_format_read_uint32(fff_settings, "Sound", &tmp_data32, 1)) {
  44. pb->settings.sound_enabled = (tmp_data32 == 0) ? false : true;
  45. }
  46. if(flipper_format_read_uint32(fff_settings, "LED", &tmp_data32, 1)) {
  47. pb->settings.led_enabled = (tmp_data32 == 0) ? false : true;
  48. }
  49. if(flipper_format_read_uint32(fff_settings, "Vibrate", &tmp_data32, 1)) {
  50. pb->settings.vibrate_enabled = (tmp_data32 == 0) ? false : true;
  51. }
  52. if(flipper_format_read_uint32(fff_settings, "Manual", &tmp_data32, 1)) {
  53. pb->settings.manual_mode = (tmp_data32 == 0) ? false : true;
  54. }
  55. } while(false);
  56. furi_string_free(tmp_str);
  57. flipper_format_free(fff_settings);
  58. }
  59. void pinball_save_settings(PinballApp* pb) {
  60. FlipperFormat* fff_settings = flipper_format_file_alloc(pb->storage);
  61. uint32_t tmp_data32 = 0;
  62. FURI_LOG_I(TAG, "SETTINGS: Saving settings");
  63. do {
  64. if(!flipper_format_file_open_always(fff_settings, PINBALL_SETTINGS_PATH)) {
  65. FURI_LOG_E(TAG, "SETTINGS: Unable to open file for save!");
  66. break;
  67. }
  68. if(!flipper_format_write_header_cstr(
  69. fff_settings, PINBALL_SETTINGS_FILE_TYPE, PINBALL_SETTINGS_FILE_VERSION)) {
  70. FURI_LOG_E(TAG, "SETTINGS: Failed writing file type and version");
  71. break;
  72. }
  73. // now write out our settings data
  74. tmp_data32 = pb->settings.sound_enabled ? 1 : 0;
  75. if(!flipper_format_write_uint32(fff_settings, "Sound", &tmp_data32, 1)) {
  76. FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Sound'");
  77. break;
  78. }
  79. tmp_data32 = pb->settings.led_enabled ? 1 : 0;
  80. if(!flipper_format_write_uint32(fff_settings, "LED", &tmp_data32, 1)) {
  81. FURI_LOG_E(TAG, "SETTINGS: Failed to write 'LED'");
  82. break;
  83. }
  84. tmp_data32 = pb->settings.vibrate_enabled ? 1 : 0;
  85. if(!flipper_format_write_uint32(fff_settings, "Vibrate", &tmp_data32, 1)) {
  86. FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Vibrate'");
  87. break;
  88. }
  89. tmp_data32 = pb->settings.manual_mode ? 1 : 0;
  90. if(!flipper_format_write_uint32(fff_settings, "Manual", &tmp_data32, 1)) {
  91. FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Manual'");
  92. break;
  93. }
  94. } while(false);
  95. flipper_format_file_close(fff_settings);
  96. flipper_format_free(fff_settings);
  97. }
  98. void solve(PinballApp* pb, float dt) {
  99. Table* table = pb->table;
  100. float sub_dt = dt / PHYSICS_SUB_STEPS;
  101. for(int ss = 0; ss < PHYSICS_SUB_STEPS; ss++) {
  102. // apply gravity (and any other forces?)
  103. // FURI_LOG_I(TAG, "Applying gravity");
  104. if(table->balls_released) {
  105. float bump_amt = 1.0f;
  106. if(pb->keys[InputKeyUp]) {
  107. bump_amt = -1.04f;
  108. }
  109. for(auto& b : table->balls) {
  110. // We multiply GRAVITY by dt since gravity is based on seconds
  111. b.accelerate(Vec2(0, GRAVITY * bump_amt * sub_dt));
  112. }
  113. }
  114. // apply collisions (among moving objects)
  115. // only needed for multi-ball! - is this true? what about flippers...
  116. for(size_t b1 = 0; b1 < table->balls.size(); b1++) {
  117. for(size_t b2 = b1 + 1; b2 < table->balls.size(); b2++) {
  118. if(b1 != b2) {
  119. auto& ball1 = table->balls[b1];
  120. auto& ball2 = table->balls[b2];
  121. Vec2 axis = ball1.p - ball2.p;
  122. float dist2 = axis.mag2();
  123. float dist = sqrtf(dist2);
  124. float rr = ball1.r + ball2.r;
  125. if(dist < rr) {
  126. Vec2 v1 = ball1.p - ball1.prev_p;
  127. Vec2 v2 = ball2.p - ball2.prev_p;
  128. float factor = (dist - rr) / dist;
  129. ball1.p -= axis * factor * 0.5f;
  130. ball2.p -= axis * factor * 0.5f;
  131. float damping = 1.01f;
  132. float f1 = (damping * (axis.x * v1.x + axis.y * v1.y)) / dist2;
  133. float f2 = (damping * (axis.x * v2.x + axis.y * v2.y)) / dist2;
  134. v1.x += f2 * axis.x - f1 * axis.x;
  135. v2.x += f1 * axis.x - f2 * axis.x;
  136. v1.y += f2 * axis.y - f1 * axis.y;
  137. v2.y += f1 * axis.y - f2 * axis.y;
  138. ball1.prev_p = ball1.p - v1;
  139. ball2.prev_p = ball2.p - v2;
  140. }
  141. }
  142. }
  143. }
  144. // collisions with static objects and flippers
  145. for(auto& b : table->balls) {
  146. for(auto& o : table->objects) {
  147. if(o->physical && o->collide(b)) {
  148. o->reset_animation();
  149. continue;
  150. }
  151. }
  152. for(auto& f : table->flippers) {
  153. if(f.collide(b)) {
  154. continue;
  155. }
  156. }
  157. }
  158. // update positions - of balls AND flippers
  159. if(table->balls_released) {
  160. for(auto& b : table->balls) {
  161. b.update(sub_dt);
  162. }
  163. }
  164. for(auto& f : table->flippers) {
  165. f.update(sub_dt);
  166. }
  167. }
  168. // Did any balls fall off the table?
  169. size_t num_in_play = table->balls.size();
  170. auto i = table->balls.begin();
  171. while(i != table->balls.end()) {
  172. if(i->p.y > 1280 + 100) {
  173. FURI_LOG_I(TAG, "ball off table!");
  174. i = table->balls.erase(i);
  175. num_in_play--;
  176. } else {
  177. ++i;
  178. }
  179. }
  180. if(num_in_play == 0) {
  181. table->balls_released = false;
  182. table->num_lives--;
  183. if(table->num_lives > 0) {
  184. table->balls = table->balls_initial;
  185. }
  186. }
  187. }
  188. void pinball_app_init(PinballApp* pb) {
  189. furi_assert(pb);
  190. pb->storage = (Storage*)furi_record_open(RECORD_STORAGE);
  191. pb->notify = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION);
  192. pb->table = NULL;
  193. pb->tick = 0;
  194. pb->gameStarted = false;
  195. pb->game_mode = GM_TableSelect;
  196. pb->keys[InputKeyUp] = false;
  197. pb->keys[InputKeyDown] = false;
  198. pb->keys[InputKeyRight] = false;
  199. pb->keys[InputKeyLeft] = false;
  200. pinball_load_settings(pb);
  201. }
  202. // int modulo(int a, int b) {
  203. // return (a % b + b) % b;
  204. // }
  205. static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
  206. furi_assert(ctx);
  207. PinballApp* pb = (PinballApp*)ctx;
  208. furi_mutex_acquire(pb->mutex, FuriWaitForever);
  209. // What are we drawing? table select / menu or the actual game?
  210. switch(pb->game_mode) {
  211. case GM_TableSelect: {
  212. canvas_draw_icon(canvas, 0, 0, &I_pinball0_logo); // our sweet logo
  213. // draw the list of table names: display it as a carousel - where the list repeats
  214. // and the currently selected item is always in the middle, surrounded by pinballs
  215. const TableList& list = pb->table_list;
  216. int32_t y = 25;
  217. auto half_way = list.display_size / 2;
  218. for(auto i = 0; i < list.display_size; i++) {
  219. int index =
  220. (list.selected - half_way + i + list.menu_items.size()) % list.menu_items.size();
  221. const auto& menu_item = list.menu_items[index];
  222. canvas_draw_str_aligned(
  223. canvas,
  224. LCD_WIDTH / 2,
  225. y,
  226. AlignCenter,
  227. AlignTop,
  228. furi_string_get_cstr(menu_item.name));
  229. if(i == half_way) {
  230. canvas_draw_disc(canvas, 8, y + 3, 2);
  231. canvas_draw_disc(canvas, 56, y + 3, 2);
  232. }
  233. y += 12;
  234. }
  235. pb->table->draw(canvas);
  236. } break;
  237. case GM_Playing:
  238. pb->table->draw(canvas);
  239. break;
  240. case GM_GameOver: {
  241. // pb->table->draw(canvas);
  242. int32_t y = 56;
  243. size_t interval = 40;
  244. float theta = (float)((pb->tick % interval) / (interval * 1.0f)) * (float)(M_PI * 2);
  245. FURI_LOG_I(TAG, "tick: %lu, theta: %.4f", pb->tick, (double)theta);
  246. // float theta_offset = (float)((pb->tick % 16) / 16.0);
  247. float theta_offset = 0;
  248. // int32_t y_offset = y + sinf(theta) * 4;
  249. canvas_draw_icon(canvas, 16, y + sinf(theta) * 4, &I_Arcade_G);
  250. canvas_draw_icon(canvas, 24, y + sinf(theta + theta_offset) * 4, &I_Arcade_A);
  251. canvas_draw_icon(canvas, 32, y + sinf(theta + theta_offset * 2) * 4, &I_Arcade_M);
  252. canvas_draw_icon(canvas, 40, y + sinf(theta + theta_offset * 3) * 4, &I_Arcade_E);
  253. canvas_draw_icon(canvas, 16, y + sinf(theta) * 4 + 8, &I_Arcade_O);
  254. canvas_draw_icon(canvas, 24, y + sinf(theta + theta_offset) * 4 + 8, &I_Arcade_V);
  255. canvas_draw_icon(canvas, 32, y + sinf(theta + theta_offset * 2) * 4 + 8, &I_Arcade_E);
  256. canvas_draw_icon(canvas, 40, y + sinf(theta + theta_offset * 3) * 4 + 8, &I_Arcade_R);
  257. } break;
  258. case GM_Error: {
  259. // pb->text contains error message
  260. canvas_draw_icon(canvas, 0, 10, &I_Arcade_E);
  261. canvas_draw_icon(canvas, 8, 10, &I_Arcade_R);
  262. canvas_draw_icon(canvas, 16, 10, &I_Arcade_R);
  263. canvas_draw_icon(canvas, 24, 10, &I_Arcade_O);
  264. canvas_draw_icon(canvas, 32, 10, &I_Arcade_R);
  265. int x = 10;
  266. int y = 30;
  267. // split the string on \n and display each line
  268. // strtok is disabled - whyyy
  269. char buf[256];
  270. strncpy(buf, pb->text, 256);
  271. char* str = buf;
  272. char* p = buf;
  273. bool at_end = false;
  274. while(str != NULL) {
  275. while(p && *p != '\n' && *p != '\0')
  276. p++;
  277. if(p && *p == '\0') at_end = true;
  278. *p = '\0';
  279. canvas_draw_str_aligned(canvas, x, y, AlignLeft, AlignTop, str);
  280. if(at_end) {
  281. str = NULL;
  282. break;
  283. }
  284. str = p + 1;
  285. p = str;
  286. y += 12;
  287. }
  288. pb->table->draw(canvas);
  289. } break;
  290. case GM_Settings: {
  291. canvas_draw_str_aligned(canvas, 2, 10, AlignLeft, AlignTop, "SETTINGS");
  292. int x = 55;
  293. int y = 30;
  294. canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Sound");
  295. canvas_draw_circle(canvas, x, y + 3, 4);
  296. if(pb->settings.sound_enabled) {
  297. canvas_draw_disc(canvas, x, y + 3, 2);
  298. }
  299. if(pb->selected_setting == 0) {
  300. canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
  301. }
  302. y += 12;
  303. canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "LED");
  304. canvas_draw_circle(canvas, x, y + 3, 4);
  305. if(pb->settings.led_enabled) {
  306. canvas_draw_disc(canvas, x, y + 3, 2);
  307. }
  308. if(pb->selected_setting == 1) {
  309. canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
  310. }
  311. y += 12;
  312. canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Vibrate");
  313. canvas_draw_circle(canvas, x, y + 3, 4);
  314. if(pb->settings.vibrate_enabled) {
  315. canvas_draw_disc(canvas, x, y + 3, 2);
  316. }
  317. if(pb->selected_setting == 2) {
  318. canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
  319. }
  320. y += 12;
  321. canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Manual");
  322. canvas_draw_circle(canvas, x, y + 3, 4);
  323. if(pb->settings.manual_mode) {
  324. canvas_draw_disc(canvas, x, y + 3, 2);
  325. }
  326. if(pb->selected_setting == 3) {
  327. canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
  328. }
  329. pb->table->draw(canvas);
  330. } break;
  331. default:
  332. FURI_LOG_E(TAG, "Unknown Game Mode");
  333. break;
  334. }
  335. furi_mutex_release(pb->mutex);
  336. }
  337. static void pinball_input_callback(InputEvent* input_event, void* ctx) {
  338. furi_assert(ctx);
  339. FuriMessageQueue* event_queue = (FuriMessageQueue*)ctx;
  340. PinballEvent event = {.type = EventTypeKey, .input = *input_event};
  341. furi_message_queue_put(event_queue, &event, FuriWaitForever);
  342. }
  343. extern "C" int32_t pinball0_app(void* p) {
  344. UNUSED(p);
  345. PinballApp* app = (PinballApp*)malloc(sizeof(PinballApp));
  346. app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
  347. if(!app->mutex) {
  348. FURI_LOG_E(TAG, "Cannot create mutex!");
  349. free(app);
  350. return 0;
  351. }
  352. pinball_app_init(app);
  353. // read the list of tables from storage
  354. table_table_list_init(app);
  355. // load the table select table
  356. table_load_table(app, 0);
  357. FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PinballEvent));
  358. furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
  359. ViewPort* view_port = view_port_alloc();
  360. view_port_set_orientation(view_port, ViewPortOrientationVertical);
  361. view_port_draw_callback_set(view_port, pinball_draw_callback, app);
  362. view_port_input_callback_set(view_port, pinball_input_callback, event_queue);
  363. // Open the GUI and register view_port
  364. Gui* gui = (Gui*)furi_record_open(RECORD_GUI);
  365. gui_add_view_port(gui, view_port, GuiLayerFullscreen);
  366. notification_message(app->notify, &sequence_display_backlight_enforce_on);
  367. // dolphin_deed(DolphinDeedPluginGameStart);
  368. app->processing = true;
  369. float dt = 0.0f;
  370. uint32_t last_frame_time = furi_get_tick();
  371. FURI_LOG_I(TAG, "Starting event loop");
  372. PinballEvent event;
  373. while(app->processing) {
  374. FuriStatus event_status =
  375. furi_message_queue_get(event_queue, &event, 10); // TODO best rate?
  376. furi_mutex_acquire(app->mutex, FuriWaitForever);
  377. if(event_status == FuriStatusOk) {
  378. if(event.type == EventTypeKey) {
  379. if(event.input.type == InputTypePress || event.input.type == InputTypeLong ||
  380. event.input.type == InputTypeRepeat) {
  381. switch(event.input.key) {
  382. case InputKeyBack:
  383. if(app->game_mode == GM_Playing || app->game_mode == GM_GameOver ||
  384. app->game_mode == GM_Error || app->game_mode == GM_Settings) {
  385. if(app->game_mode == GM_Settings) {
  386. pinball_save_settings(app);
  387. }
  388. app->game_mode = GM_TableSelect;
  389. table_load_table(app, TABLE_SELECT);
  390. } else if(app->game_mode == GM_TableSelect) {
  391. app->processing = false;
  392. }
  393. break;
  394. case InputKeyRight:
  395. app->keys[InputKeyRight] = true;
  396. if(app->settings.manual_mode && app->table->balls_released == false) {
  397. app->table->balls[0].p.x += MANUAL_ADJUSTMENT;
  398. app->table->balls[0].prev_p.x += MANUAL_ADJUSTMENT;
  399. }
  400. for(auto& f : app->table->flippers) {
  401. if(f.side == Flipper::RIGHT) {
  402. f.powered = true;
  403. }
  404. }
  405. break;
  406. case InputKeyLeft:
  407. app->keys[InputKeyLeft] = true;
  408. if(app->settings.manual_mode && app->table->balls_released == false) {
  409. app->table->balls[0].p.x -= MANUAL_ADJUSTMENT;
  410. app->table->balls[0].prev_p.x -= MANUAL_ADJUSTMENT;
  411. }
  412. for(auto& f : app->table->flippers) {
  413. if(f.side == Flipper::LEFT) {
  414. f.powered = true;
  415. }
  416. }
  417. break;
  418. case InputKeyUp:
  419. switch(app->game_mode) {
  420. case GM_Playing:
  421. if(event.input.type == InputTypePress) {
  422. // we only set the key if it's a 'press' to ensure
  423. // a single table "bump"
  424. app->keys[InputKeyUp] = true;
  425. notify_table_bump(app);
  426. }
  427. if(app->settings.manual_mode && app->table->balls_released == false) {
  428. app->table->balls[0].p.y -= MANUAL_ADJUSTMENT;
  429. app->table->balls[0].prev_p.y -= MANUAL_ADJUSTMENT;
  430. }
  431. break;
  432. case GM_TableSelect:
  433. app->table_list.selected = (app->table_list.selected - 1 +
  434. app->table_list.menu_items.size()) %
  435. app->table_list.menu_items.size();
  436. break;
  437. case GM_Settings:
  438. if(app->selected_setting > 0) {
  439. app->selected_setting--;
  440. }
  441. break;
  442. default:
  443. break;
  444. }
  445. break;
  446. case InputKeyDown:
  447. switch(app->game_mode) {
  448. case GM_Playing:
  449. app->keys[InputKeyDown] = true;
  450. if(app->settings.manual_mode && app->table->balls_released == false) {
  451. app->table->balls[0].p.y += MANUAL_ADJUSTMENT;
  452. app->table->balls[0].prev_p.y += MANUAL_ADJUSTMENT;
  453. }
  454. break;
  455. case GM_TableSelect:
  456. app->table_list.selected = (app->table_list.selected + 1 +
  457. app->table_list.menu_items.size()) %
  458. app->table_list.menu_items.size();
  459. break;
  460. case GM_Settings:
  461. if(app->selected_setting < app->max_settings - 1) {
  462. app->selected_setting++;
  463. }
  464. break;
  465. default:
  466. break;
  467. }
  468. break;
  469. case InputKeyOk:
  470. switch(app->game_mode) {
  471. case GM_Playing:
  472. if(!app->table->balls_released) {
  473. app->gameStarted = true;
  474. app->table->balls_released = true;
  475. }
  476. break;
  477. case GM_TableSelect: {
  478. size_t sel = app->table_list.selected;
  479. if(sel == app->table_list.menu_items.size() - 1) {
  480. app->game_mode = GM_Settings;
  481. table_load_table(app, TABLE_SETTINGS);
  482. } else if(!table_load_table(app, sel + TABLE_INDEX_OFFSET)) {
  483. app->game_mode = GM_Error;
  484. table_load_table(app, TABLE_ERROR);
  485. notify_error_message(app);
  486. } else {
  487. app->game_mode = GM_Playing;
  488. }
  489. } break;
  490. case GM_Settings:
  491. switch(app->selected_setting) {
  492. case 0:
  493. app->settings.sound_enabled = !app->settings.sound_enabled;
  494. break;
  495. case 1:
  496. app->settings.led_enabled = !app->settings.led_enabled;
  497. break;
  498. case 2:
  499. app->settings.vibrate_enabled = !app->settings.vibrate_enabled;
  500. break;
  501. case 3:
  502. app->settings.manual_mode = !app->settings.manual_mode;
  503. break;
  504. default:
  505. break;
  506. }
  507. break;
  508. default:
  509. break;
  510. }
  511. break;
  512. default:
  513. break;
  514. }
  515. } else if(event.input.type == InputTypeRelease) {
  516. switch(event.input.key) {
  517. case InputKeyLeft: {
  518. app->keys[InputKeyLeft] = false;
  519. for(auto& f : app->table->flippers) {
  520. if(f.side == Flipper::LEFT) {
  521. f.powered = false;
  522. }
  523. }
  524. break;
  525. }
  526. case InputKeyRight: {
  527. app->keys[InputKeyRight] = false;
  528. for(auto& f : app->table->flippers) {
  529. if(f.side == Flipper::RIGHT) {
  530. f.powered = false;
  531. }
  532. }
  533. break;
  534. }
  535. case InputKeyUp:
  536. app->keys[InputKeyUp] = false;
  537. break;
  538. case InputKeyDown:
  539. app->keys[InputKeyDown] = false;
  540. // TODO: release plunger?
  541. break;
  542. default:
  543. break;
  544. }
  545. }
  546. }
  547. }
  548. solve(app, dt);
  549. for(auto& o : app->table->objects) {
  550. o->step_animation();
  551. }
  552. // check game state
  553. if(app->game_mode == GM_Playing && app->table->num_lives == 0) {
  554. FURI_LOG_W(TAG, "GAME OVER!");
  555. app->game_mode = GM_GameOver;
  556. }
  557. // no keys pressed - we should clear all input keys?
  558. view_port_update(view_port);
  559. furi_mutex_release(app->mutex);
  560. // game timing
  561. uint32_t time_lapsed = furi_get_tick() - last_frame_time;
  562. dt = time_lapsed / 1000.0f;
  563. while(dt < 1.0f / GAME_FPS) {
  564. time_lapsed = furi_get_tick() - last_frame_time;
  565. dt = time_lapsed / 1000.0f;
  566. }
  567. app->tick++;
  568. last_frame_time = furi_get_tick();
  569. }
  570. // general cleanup
  571. notification_message(app->notify, &sequence_display_backlight_enforce_auto);
  572. view_port_enabled_set(view_port, false);
  573. gui_remove_view_port(gui, view_port);
  574. furi_record_close(RECORD_GUI);
  575. furi_record_close(RECORD_STORAGE);
  576. furi_record_close(RECORD_NOTIFICATION);
  577. view_port_free(view_port);
  578. furi_message_queue_free(event_queue);
  579. furi_mutex_free(app->mutex);
  580. delete app->table;
  581. free(app);
  582. furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
  583. return 0;
  584. }