pinball0.cxx 23 KB

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