pinball0.cxx 27 KB

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