scorched_tanks_game_app.c 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. #include <furi.h>
  2. #include <gui/gui.h>
  3. #include <input/input.h>
  4. #include <stdlib.h>
  5. #include <math.h>
  6. #include <notification/notification.h>
  7. #include <notification/notification_messages.h>
  8. #define SCREEN_WIDTH 128
  9. #define SCREEN_HEIGHT 64
  10. #define PLAYER_INIT_LOCATION_X 20
  11. #define PLAYER_INIT_AIM 45
  12. #define PLAYER_INIT_POWER 50
  13. #define ENEMY_INIT_LOCATION_X 108
  14. #define TANK_BARREL_LENGTH 8
  15. #define GRAVITY_FORCE 32
  16. #define MIN_GROUND_HEIGHT 35
  17. #define MAX_GROUND_HEIGHT 55
  18. #define MAX_FIRE_POWER 100
  19. #define MIN_FIRE_POWER 0
  20. #define TANK_COLLIDER_SIZE 3
  21. // That's a filthy workaround but sin(player.aimAngle) breaks it all... If you're able to fix it, please do create a PR!
  22. double scorched_tanks_sin[91] = {
  23. 0.000, -0.017, -0.035, -0.052, -0.070, -0.087, -0.105, -0.122, -0.139, -0.156, -0.174, -0.191,
  24. -0.208, -0.225, -0.242, -0.259, -0.276, -0.292, -0.309, -0.326, -0.342, -0.358, -0.375, -0.391,
  25. -0.407, -0.423, -0.438, -0.454, -0.469, -0.485, -0.500, -0.515, -0.530, -0.545, -0.559, -0.574,
  26. -0.588, -0.602, -0.616, -0.629, -0.643, -0.656, -0.669, -0.682, -0.695, -0.707, -0.719, -0.731,
  27. -0.743, -0.755, -0.766, -0.777, -0.788, -0.799, -0.809, -0.819, -0.829, -0.839, -0.848, -0.857,
  28. -0.866, -0.875, -0.883, -0.891, -0.899, -0.906, -0.914, -0.921, -0.927, -0.934, -0.940, -0.946,
  29. -0.951, -0.956, -0.961, -0.966, -0.970, -0.974, -0.978, -0.982, -0.985, -0.988, -0.990, -0.993,
  30. -0.995, -0.996, -0.998, -0.999, -0.999, -1.000, -1.000};
  31. double scorched_tanks_cos[91] = {
  32. 1.000, 1.000, 0.999, 0.999, 0.998, 0.996, 0.995, 0.993, 0.990, 0.988, 0.985, 0.982, 0.978,
  33. 0.974, 0.970, 0.966, 0.961, 0.956, 0.951, 0.946, 0.940, 0.934, 0.927, 0.921, 0.914, 0.906,
  34. 0.899, 0.891, 0.883, 0.875, 0.866, 0.857, 0.848, 0.839, 0.829, 0.819, 0.809, 0.799, 0.788,
  35. 0.777, 0.766, 0.755, 0.743, 0.731, 0.719, 0.707, 0.695, 0.682, 0.669, 0.656, 0.643, 0.629,
  36. 0.616, 0.602, 0.588, 0.574, 0.559, 0.545, 0.530, 0.515, 0.500, 0.485, 0.469, 0.454, 0.438,
  37. 0.423, 0.407, 0.391, 0.375, 0.358, 0.342, 0.326, 0.309, 0.292, 0.276, 0.259, 0.242, 0.225,
  38. 0.208, 0.191, 0.174, 0.156, 0.139, 0.122, 0.105, 0.087, 0.070, 0.052, 0.035, 0.017, 0.000};
  39. double scorched_tanks_tan[91] = {
  40. 0.000, -0.017, -0.035, -0.052, -0.070, -0.087, -0.105, -0.123, -0.141, -0.158, -0.176,
  41. -0.194, -0.213, -0.231, -0.249, -0.268, -0.287, -0.306, -0.325, -0.344, -0.364, -0.384,
  42. -0.404, -0.424, -0.445, -0.466, -0.488, -0.510, -0.532, -0.554, -0.577, -0.601, -0.625,
  43. -0.649, -0.674, -0.700, -0.727, -0.754, -0.781, -0.810, -0.839, -0.869, -0.900, -0.932,
  44. -0.966, -1.000, -1.036, -1.072, -1.111, -1.150, -1.192, -1.235, -1.280, -1.327, -1.376,
  45. -1.428, -1.483, -1.540, -1.600, -1.664, -1.732, -1.804, -1.881, -1.963, -2.050, -2.144,
  46. -2.246, -2.356, -2.475, -2.605, -2.747, -2.904, -3.078, -3.271, -3.487, -3.732, -4.011,
  47. -4.331, -4.704, -5.144, -5.671, -6.313, -7.115, -8.144, -9.513, -11.429, -14.298, -19.077,
  48. -28.627, -57.254, -90747.269};
  49. typedef struct {
  50. // +-----x
  51. // |
  52. // |
  53. // y
  54. uint8_t x;
  55. uint8_t y;
  56. } Point;
  57. typedef struct {
  58. unsigned char locationX;
  59. unsigned char hp;
  60. int aimAngle;
  61. unsigned char firePower;
  62. bool isShooting;
  63. } Tank;
  64. typedef struct {
  65. Point ground[SCREEN_WIDTH];
  66. Tank player;
  67. Tank enemy;
  68. bool isPlayerTurn;
  69. unsigned char trajectoryY[SCREEN_WIDTH];
  70. unsigned char trajectoryAnimationStep;
  71. Point bulletPosition;
  72. } Game;
  73. typedef enum {
  74. EventTypeTick,
  75. EventTypeKey,
  76. } EventType;
  77. typedef struct {
  78. EventType type;
  79. InputEvent input;
  80. } ScorchedTanksEvent;
  81. int scorched_tanks_random(int min, int max) {
  82. return min + rand() % ((max + 1) - min);
  83. }
  84. void scorched_tanks_generate_ground(Game* game_state) {
  85. auto lastHeight = 45;
  86. for(unsigned char a = 0; a < SCREEN_WIDTH; a++) {
  87. auto diffHeight = scorched_tanks_random(-2, 3);
  88. auto changeLength = scorched_tanks_random(1, 6);
  89. if(diffHeight == 0) {
  90. changeLength = 1;
  91. }
  92. for(int b = 0; b < changeLength; b++) {
  93. if(a + b < SCREEN_WIDTH) {
  94. auto index = a + b;
  95. auto newPoint = lastHeight + diffHeight;
  96. newPoint = newPoint < MIN_GROUND_HEIGHT ? MIN_GROUND_HEIGHT : newPoint;
  97. newPoint = newPoint > MAX_GROUND_HEIGHT ? MAX_GROUND_HEIGHT : newPoint;
  98. game_state->ground[index].x = index;
  99. game_state->ground[index].y = newPoint;
  100. lastHeight = newPoint;
  101. } else {
  102. a += b;
  103. break;
  104. }
  105. }
  106. a += changeLength - 1;
  107. }
  108. }
  109. void scorched_tanks_init_game(Game* game_state) {
  110. game_state->player.locationX = PLAYER_INIT_LOCATION_X;
  111. game_state->player.aimAngle = PLAYER_INIT_AIM;
  112. game_state->player.firePower = PLAYER_INIT_POWER;
  113. game_state->enemy.locationX = ENEMY_INIT_LOCATION_X;
  114. for (int x = 0; x < SCREEN_WIDTH; x++)
  115. {
  116. game_state->trajectoryY[x] = 0;
  117. }
  118. scorched_tanks_generate_ground(game_state);
  119. }
  120. void scorched_tanks_calculate_trajectory(Game* game_state) {
  121. if(game_state->player.isShooting) {
  122. int x0 = game_state->player.locationX;
  123. int y0 = game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE;
  124. int v0 = game_state->player.firePower;
  125. int g = GRAVITY_FORCE;
  126. int angle = game_state->player.aimAngle;
  127. if(x0 + game_state->trajectoryAnimationStep > SCREEN_WIDTH ||
  128. game_state->bulletPosition.x > SCREEN_WIDTH ||
  129. game_state->bulletPosition.y > game_state->ground[game_state->bulletPosition.x].y) {
  130. game_state->player.isShooting = false;
  131. game_state->bulletPosition.x = 0;
  132. game_state->bulletPosition.y = 0;
  133. return;
  134. }
  135. unsigned char distanceToEnemyX = game_state->enemy.locationX - game_state->bulletPosition.x;
  136. unsigned char distanceToEnemyY = game_state->ground[game_state->enemy.locationX].y - TANK_COLLIDER_SIZE - game_state->bulletPosition.y;
  137. int totalDistanceToEnemy = sqrt(distanceToEnemyX*distanceToEnemyX+distanceToEnemyY*distanceToEnemyY);
  138. if (totalDistanceToEnemy <= TANK_COLLIDER_SIZE)
  139. {
  140. game_state->player.isShooting = false;
  141. scorched_tanks_init_game(game_state);
  142. return;
  143. }
  144. auto x = game_state->trajectoryAnimationStep;
  145. auto y = y0 + x * scorched_tanks_tan[angle] -
  146. g * x * x / -(2 * v0 * v0 * scorched_tanks_cos[angle] * scorched_tanks_cos[angle]);
  147. x += x0;
  148. if(x % 4 == 0) {
  149. game_state->trajectoryY[x] = y;
  150. }
  151. game_state->bulletPosition.x = x;
  152. game_state->bulletPosition.y = y;
  153. game_state->trajectoryAnimationStep++;
  154. }
  155. }
  156. static void scorched_tanks_render_callback(Canvas* const canvas, void* ctx) {
  157. const Game* game_state = acquire_mutex((ValueMutex*)ctx, 25);
  158. if(game_state == NULL) {
  159. return;
  160. }
  161. canvas_draw_frame(canvas, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  162. canvas_set_color(canvas, ColorBlack);
  163. if(game_state->player.isShooting) {
  164. canvas_draw_dot(canvas, game_state->bulletPosition.x, game_state->bulletPosition.y);
  165. }
  166. for(int a = 1; a < SCREEN_WIDTH; a++) {
  167. canvas_draw_line(
  168. canvas,
  169. game_state->ground[a - 1].x,
  170. game_state->ground[a - 1].y,
  171. game_state->ground[a].x,
  172. game_state->ground[a].y);
  173. if(game_state->trajectoryY[a] != 0) {
  174. canvas_draw_dot(canvas, a, game_state->trajectoryY[a]);
  175. }
  176. }
  177. canvas_draw_disc(
  178. canvas,
  179. game_state->enemy.locationX,
  180. game_state->ground[game_state->enemy.locationX].y - TANK_COLLIDER_SIZE,
  181. 3);
  182. canvas_draw_circle(
  183. canvas,
  184. game_state->player.locationX,
  185. game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE,
  186. 3);
  187. auto aimX1 = game_state->player.locationX;
  188. auto aimY1 = game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE;
  189. double sinFromAngle = scorched_tanks_sin[game_state->player.aimAngle];
  190. double cosFromAngle = scorched_tanks_cos[game_state->player.aimAngle];
  191. int aimX2 = aimX1 + TANK_BARREL_LENGTH * cosFromAngle;
  192. int aimY2 = aimY1 + TANK_BARREL_LENGTH * sinFromAngle;
  193. canvas_draw_line(canvas, aimX1, aimY1, aimX2, aimY2);
  194. canvas_set_font(canvas, FontSecondary);
  195. canvas_draw_str(canvas, 55, 10, "Scorched Tanks");
  196. char buffer[12];
  197. snprintf(buffer, sizeof(buffer), "a: %u", game_state->player.aimAngle);
  198. canvas_draw_str(canvas, 2, 10, buffer);
  199. snprintf(buffer, sizeof(buffer), "p: %u", game_state->player.firePower);
  200. canvas_draw_str(canvas, 27, 10, buffer);
  201. release_mutex((ValueMutex*)ctx, game_state);
  202. }
  203. static void scorched_tanks_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
  204. furi_assert(event_queue);
  205. ScorchedTanksEvent event = {.type = EventTypeKey, .input = *input_event};
  206. furi_message_queue_put(event_queue, &event, FuriWaitForever);
  207. }
  208. static void scorched_tanks_update_timer_callback(FuriMessageQueue* event_queue) {
  209. furi_assert(event_queue);
  210. ScorchedTanksEvent event = {.type = EventTypeTick};
  211. furi_message_queue_put(event_queue, &event, 0);
  212. }
  213. static void scorched_tanks_increase_power(Game* game_state)
  214. {
  215. if (game_state->player.firePower < MAX_FIRE_POWER && !game_state->player.isShooting)
  216. {
  217. game_state->player.firePower++;
  218. }
  219. }
  220. static void scorched_tanks_decrease_power(Game* game_state)
  221. {
  222. if (game_state->player.firePower > MIN_FIRE_POWER && !game_state->player.isShooting)
  223. {
  224. game_state->player.firePower--;
  225. }
  226. }
  227. static void scorched_tanks_aim_up(Game* game_state) {
  228. if(game_state->player.aimAngle < 90 && !game_state->player.isShooting) {
  229. game_state->player.aimAngle++;
  230. }
  231. }
  232. static void scorched_tanks_aim_down(Game* game_state) {
  233. if(game_state->player.aimAngle > 0 && !game_state->player.isShooting) {
  234. game_state->player.aimAngle--;
  235. }
  236. }
  237. const NotificationSequence sequence_long_vibro = {
  238. &message_vibro_on,
  239. &message_delay_500,
  240. &message_vibro_off,
  241. NULL,
  242. };
  243. static void scorched_tanks_fire(Game* game_state) {
  244. if(!game_state->player.isShooting) {
  245. game_state->bulletPosition.x = game_state->player.locationX;
  246. game_state->bulletPosition.y = game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE;
  247. game_state->trajectoryAnimationStep = 0;
  248. for(int x = 0; x < SCREEN_WIDTH; x++) {
  249. game_state->trajectoryY[x] = 0;
  250. }
  251. game_state->player.isShooting = true;
  252. NotificationApp* notification = furi_record_open("notification");
  253. notification_message(notification, &sequence_long_vibro);
  254. notification_message(notification, &sequence_blink_white_100);
  255. furi_record_close("notification");
  256. }
  257. }
  258. int32_t scorched_tanks_game_app(void* p) {
  259. UNUSED(p);
  260. srand(DWT->CYCCNT);
  261. FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(ScorchedTanksEvent));
  262. Game* game_state = malloc(sizeof(Game));
  263. scorched_tanks_init_game(game_state);
  264. ValueMutex state_mutex;
  265. if(!init_mutex(&state_mutex, game_state, sizeof(ScorchedTanksEvent))) {
  266. FURI_LOG_E("ScorchedTanks", "cannot create mutex\r\n");
  267. free(game_state);
  268. return 255;
  269. }
  270. ViewPort* view_port = view_port_alloc();
  271. view_port_draw_callback_set(view_port, scorched_tanks_render_callback, &state_mutex);
  272. view_port_input_callback_set(view_port, scorched_tanks_input_callback, event_queue);
  273. FuriTimer* timer =
  274. furi_timer_alloc(scorched_tanks_update_timer_callback, FuriTimerTypePeriodic, event_queue);
  275. furi_timer_start(timer, 2000);
  276. // Open GUI and register view_port
  277. Gui* gui = furi_record_open(RECORD_GUI);
  278. gui_add_view_port(gui, view_port, GuiLayerFullscreen);
  279. ScorchedTanksEvent event;
  280. for(bool processing = true; processing;) {
  281. FuriStatus event_status = furi_message_queue_get(event_queue, &event, 50);
  282. if(event.type == EventTypeKey) { // && game->isPlayerTurn
  283. if(event.input.type == InputTypeRepeat || event.input.type == InputTypeShort) {
  284. switch(event.input.key) {
  285. case InputKeyUp:
  286. scorched_tanks_aim_up(game_state);
  287. break;
  288. case InputKeyDown:
  289. scorched_tanks_aim_down(game_state);
  290. break;
  291. case InputKeyRight:
  292. scorched_tanks_increase_power(game_state);
  293. break;
  294. case InputKeyLeft:
  295. scorched_tanks_decrease_power(game_state);
  296. break;
  297. case InputKeyOk:
  298. scorched_tanks_fire(game_state);
  299. break;
  300. case InputKeyBack:
  301. processing = false;
  302. break;
  303. }
  304. }
  305. } else if(event.type == EventTypeTick) {
  306. scorched_tanks_calculate_trajectory(game_state);
  307. }
  308. view_port_update(view_port);
  309. release_mutex(&state_mutex, game_state);
  310. }
  311. furi_timer_free(timer);
  312. view_port_enabled_set(view_port, false);
  313. gui_remove_view_port(gui, view_port);
  314. furi_record_close(RECORD_GUI);
  315. view_port_free(view_port);
  316. furi_message_queue_free(event_queue);
  317. delete_mutex(&state_mutex);
  318. free(game_state);
  319. return 0;
  320. }