#include #include #include #include #include #include #include #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define PLAYER_INIT_LOCATION_X 20 #define PLAYER_INIT_AIM 45 #define PLAYER_INIT_POWER 50 #define ENEMY_INIT_LOCATION_X 108 #define TANK_BARREL_LENGTH 8 #define GRAVITY_FORCE (double)0.5 #define MIN_GROUND_HEIGHT 35 #define MAX_GROUND_HEIGHT 55 #define MAX_FIRE_POWER 100 #define MIN_FIRE_POWER 0 #define TANK_COLLIDER_SIZE 3 #define MAX_WIND 10 #define MAX_PLAYER_DIFF_X 20 #define MAX_ENEMY_DIFF_X 20 // That's a filthy workaround but sin(player.aimAngle) breaks it all... If you're able to fix it, please do create a PR! double scorched_tanks_sin[91] = { 0.000, -0.017, -0.035, -0.052, -0.070, -0.087, -0.105, -0.122, -0.139, -0.156, -0.174, -0.191, -0.208, -0.225, -0.242, -0.259, -0.276, -0.292, -0.309, -0.326, -0.342, -0.358, -0.375, -0.391, -0.407, -0.423, -0.438, -0.454, -0.469, -0.485, -0.500, -0.515, -0.530, -0.545, -0.559, -0.574, -0.588, -0.602, -0.616, -0.629, -0.643, -0.656, -0.669, -0.682, -0.695, -0.707, -0.719, -0.731, -0.743, -0.755, -0.766, -0.777, -0.788, -0.799, -0.809, -0.819, -0.829, -0.839, -0.848, -0.857, -0.866, -0.875, -0.883, -0.891, -0.899, -0.906, -0.914, -0.921, -0.927, -0.934, -0.940, -0.946, -0.951, -0.956, -0.961, -0.966, -0.970, -0.974, -0.978, -0.982, -0.985, -0.988, -0.990, -0.993, -0.995, -0.996, -0.998, -0.999, -0.999, -1.000, -1.000}; double scorched_tanks_cos[91] = { 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, 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, 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, 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, 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, 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, 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}; double scorched_tanks_tan[91] = { 0.000, -0.017, -0.035, -0.052, -0.070, -0.087, -0.105, -0.123, -0.141, -0.158, -0.176, -0.194, -0.213, -0.231, -0.249, -0.268, -0.287, -0.306, -0.325, -0.344, -0.364, -0.384, -0.404, -0.424, -0.445, -0.466, -0.488, -0.510, -0.532, -0.554, -0.577, -0.601, -0.625, -0.649, -0.674, -0.700, -0.727, -0.754, -0.781, -0.810, -0.839, -0.869, -0.900, -0.932, -0.966, -1.000, -1.036, -1.072, -1.111, -1.150, -1.192, -1.235, -1.280, -1.327, -1.376, -1.428, -1.483, -1.540, -1.600, -1.664, -1.732, -1.804, -1.881, -1.963, -2.050, -2.144, -2.246, -2.356, -2.475, -2.605, -2.747, -2.904, -3.078, -3.271, -3.487, -3.732, -4.011, -4.331, -4.704, -5.144, -5.671, -6.313, -7.115, -8.144, -9.513, -11.429, -14.298, -19.077, -28.627, -57.254, -90747.269}; uint8_t scorched_tanks_ground_modifiers[SCREEN_WIDTH] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 28, 26, 24, 22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; typedef struct { // +-----x // | // | // y uint8_t x; uint8_t y; } Point; typedef struct { // +-----x // | // | // y double x; double y; } PointDetailed; typedef struct { uint8_t locationX; uint8_t hp; int aimAngle; uint8_t firePower; } Tank; typedef struct { Point ground[SCREEN_WIDTH]; Tank player; Tank enemy; bool isPlayerTurn; bool isShooting; int windSpeed; Point trajectory[SCREEN_WIDTH]; uint8_t trajectoryAnimationStep; PointDetailed bulletPosition; PointDetailed bulletVector; FuriMutex* mutex; } Game; typedef enum { EventTypeTick, EventTypeKey, } EventType; typedef struct { EventType type; InputEvent input; } ScorchedTanksEvent; int scorched_tanks_random(int min, int max) { return min + rand() % ((max + 1) - min); } void scorched_tanks_generate_ground(Game* game_state) { int lastHeight = 45; for(uint8_t a = 0; a < SCREEN_WIDTH; a++) { int diffHeight = scorched_tanks_random(-2, 3); int changeLength = scorched_tanks_random(1, 6); if(diffHeight == 0) { changeLength = 1; } for(int b = 0; b < changeLength; b++) { if(a + b < SCREEN_WIDTH) { int index = a + b; int newPoint = lastHeight + diffHeight; newPoint = newPoint < MIN_GROUND_HEIGHT ? MIN_GROUND_HEIGHT : newPoint; newPoint = newPoint > MAX_GROUND_HEIGHT ? MAX_GROUND_HEIGHT : newPoint; game_state->ground[index].x = index; game_state->ground[index].y = newPoint - scorched_tanks_ground_modifiers[a]; lastHeight = newPoint; } else { a += b; break; } } a += changeLength - 1; } } void scorched_tanks_init_game(Game* game_state) { game_state->player.locationX = PLAYER_INIT_LOCATION_X + scorched_tanks_random(0, MAX_PLAYER_DIFF_X) - MAX_PLAYER_DIFF_X / 2; game_state->player.aimAngle = PLAYER_INIT_AIM; game_state->player.firePower = PLAYER_INIT_POWER; game_state->enemy.aimAngle = PLAYER_INIT_AIM; game_state->enemy.firePower = PLAYER_INIT_POWER; game_state->enemy.locationX = ENEMY_INIT_LOCATION_X + scorched_tanks_random(0, MAX_ENEMY_DIFF_X) - MAX_ENEMY_DIFF_X / 2; game_state->isPlayerTurn = true; game_state->windSpeed = scorched_tanks_random(0, MAX_WIND); for(int x = 0; x < SCREEN_WIDTH; x++) { game_state->trajectory[x].x = 0; game_state->trajectory[x].y = 0; } scorched_tanks_generate_ground(game_state); } void scorched_tanks_calculate_trajectory(Game* game_state) { if(game_state->isShooting) { game_state->bulletVector.x += ((double)game_state->windSpeed - MAX_WIND / 2) / 40; game_state->bulletVector.y += GRAVITY_FORCE; game_state->bulletPosition.x += game_state->bulletVector.x; game_state->bulletPosition.y += game_state->bulletVector.y; int totalDistanceToEnemy = 100; if(game_state->isPlayerTurn) { double distanceToEnemyX = game_state->enemy.locationX - game_state->bulletPosition.x; double distanceToEnemyY = game_state->ground[game_state->enemy.locationX].y - TANK_COLLIDER_SIZE - game_state->bulletPosition.y; totalDistanceToEnemy = sqrt(distanceToEnemyX * distanceToEnemyX + distanceToEnemyY * distanceToEnemyY); } else { double distanceToEnemyX = game_state->player.locationX - game_state->bulletPosition.x; double distanceToEnemyY = game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE - game_state->bulletPosition.y; totalDistanceToEnemy = sqrt(distanceToEnemyX * distanceToEnemyX + distanceToEnemyY * distanceToEnemyY); } if(totalDistanceToEnemy <= TANK_COLLIDER_SIZE) { game_state->isShooting = false; scorched_tanks_init_game(game_state); game_state->isPlayerTurn = !game_state->isPlayerTurn; return; } if(game_state->bulletPosition.x > SCREEN_WIDTH || game_state->bulletPosition.y > game_state->ground[(int)round(game_state->bulletPosition.x)].y) { game_state->isShooting = false; game_state->bulletPosition.x = 0; game_state->bulletPosition.y = 0; game_state->windSpeed = scorched_tanks_random(0, MAX_WIND); game_state->isPlayerTurn = !game_state->isPlayerTurn; return; } if(game_state->bulletPosition.y > 0) { game_state->trajectory[game_state->trajectoryAnimationStep].x = round(game_state->bulletPosition.x); game_state->trajectory[game_state->trajectoryAnimationStep].y = round(game_state->bulletPosition.y); game_state->trajectoryAnimationStep++; } } } static void scorched_tanks_draw_tank(Canvas* const canvas, uint8_t x, uint8_t y, bool isPlayer) { uint8_t lineIndex = 0; if(isPlayer) { // Draw tank base canvas_draw_line(canvas, x - 3, y - lineIndex, x + 3, y - lineIndex); lineIndex++; canvas_draw_line(canvas, x - 4, y - lineIndex, x + 4, y - lineIndex); lineIndex++; canvas_draw_line(canvas, x - 4, y - lineIndex, x + 4, y - lineIndex); lineIndex++; // draw turret canvas_draw_line(canvas, x - 2, y - lineIndex, x + 1, y - lineIndex); lineIndex++; canvas_draw_line(canvas, x - 2, y - lineIndex, x, y - lineIndex); lineIndex++; } else { // Draw tank base canvas_draw_line(canvas, x - 3, y - lineIndex, x + 3, y - lineIndex); lineIndex++; canvas_draw_line(canvas, x - 4, y - lineIndex, x + 4, y - lineIndex); lineIndex++; canvas_draw_line(canvas, x - 4, y - lineIndex, x + 4, y - lineIndex); lineIndex++; // draw turret canvas_draw_line(canvas, x - 1, y - lineIndex, x + 2, y - lineIndex); lineIndex++; canvas_draw_line(canvas, x, y - lineIndex, x + 2, y - lineIndex); lineIndex++; } } static void scorched_tanks_render_callback(Canvas* const canvas, void* ctx) { furi_assert(ctx); const Game* game_state = ctx; furi_mutex_acquire(game_state->mutex, FuriWaitForever); canvas_draw_frame(canvas, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); canvas_set_color(canvas, ColorBlack); if(game_state->isShooting) { canvas_draw_dot(canvas, game_state->bulletPosition.x, game_state->bulletPosition.y); } for(int a = 1; a < SCREEN_WIDTH; a++) { canvas_draw_line( canvas, game_state->ground[a - 1].x, game_state->ground[a - 1].y, game_state->ground[a].x, game_state->ground[a].y); if(game_state->trajectory[a].y != 0) { canvas_draw_dot(canvas, game_state->trajectory[a].x, game_state->trajectory[a].y); } } scorched_tanks_draw_tank( canvas, game_state->enemy.locationX, game_state->ground[game_state->enemy.locationX].y - TANK_COLLIDER_SIZE, true); scorched_tanks_draw_tank( canvas, game_state->player.locationX, game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE, false); int aimX1 = 0; int aimY1 = 0; int aimX2 = 0; int aimY2 = 0; if(game_state->isPlayerTurn) { aimX1 = game_state->player.locationX; aimY1 = game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE; double sinFromAngle = scorched_tanks_sin[game_state->player.aimAngle]; double cosFromAngle = scorched_tanks_cos[game_state->player.aimAngle]; aimX2 = aimX1 + TANK_BARREL_LENGTH * cosFromAngle; aimY2 = aimY1 + TANK_BARREL_LENGTH * sinFromAngle; aimX1 += 1; aimX2 += 1; } else { aimX1 = game_state->enemy.locationX; aimY1 = game_state->ground[game_state->enemy.locationX].y - TANK_COLLIDER_SIZE; double sinFromAngle = scorched_tanks_sin[game_state->enemy.aimAngle]; double cosFromAngle = scorched_tanks_cos[game_state->enemy.aimAngle]; aimX2 = aimX1 + TANK_BARREL_LENGTH * cosFromAngle; aimY2 = aimY1 + TANK_BARREL_LENGTH * sinFromAngle; aimX2 = aimX1 - (aimX2 - aimX1); aimX1 -= 1; aimX2 -= 1; } canvas_draw_line(canvas, aimX1, aimY1 - 3, aimX2, aimY2 - 3); canvas_set_font(canvas, FontSecondary); char buffer2[18]; snprintf(buffer2, sizeof(buffer2), "wind: %i", game_state->windSpeed - MAX_WIND / 2); canvas_draw_str(canvas, 55, 10, buffer2); if(game_state->isPlayerTurn) { canvas_draw_str(canvas, 93, 10, "player1"); char buffer[12]; snprintf(buffer, sizeof(buffer), "a: %u", game_state->player.aimAngle); canvas_draw_str(canvas, 2, 10, buffer); snprintf(buffer, sizeof(buffer), "p: %u", game_state->player.firePower); canvas_draw_str(canvas, 27, 10, buffer); } else { canvas_draw_str(canvas, 93, 10, "player2"); char buffer[12]; snprintf(buffer, sizeof(buffer), "a: %u", game_state->enemy.aimAngle); canvas_draw_str(canvas, 2, 10, buffer); snprintf(buffer, sizeof(buffer), "p: %u", game_state->enemy.firePower); canvas_draw_str(canvas, 27, 10, buffer); } furi_mutex_release(game_state->mutex); } static void scorched_tanks_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) { furi_assert(event_queue); ScorchedTanksEvent event = {.type = EventTypeKey, .input = *input_event}; furi_message_queue_put(event_queue, &event, FuriWaitForever); } static void scorched_tanks_update_timer_callback(FuriMessageQueue* event_queue) { furi_assert(event_queue); ScorchedTanksEvent event = {.type = EventTypeTick}; furi_message_queue_put(event_queue, &event, 0); } static void scorched_tanks_increase_power(Game* game_state) { if(game_state->player.firePower < MAX_FIRE_POWER && !game_state->isShooting) { if(game_state->isPlayerTurn && game_state->player.firePower < MAX_FIRE_POWER) { game_state->player.firePower++; } if(!game_state->isPlayerTurn && game_state->enemy.firePower < MAX_FIRE_POWER) { game_state->enemy.firePower++; } } } static void scorched_tanks_decrease_power(Game* game_state) { if(game_state->player.firePower > MIN_FIRE_POWER && !game_state->isShooting) { if(game_state->isPlayerTurn && game_state->player.firePower > MIN_FIRE_POWER) { game_state->player.firePower--; } if(!game_state->isPlayerTurn && game_state->enemy.firePower > MIN_FIRE_POWER) { game_state->enemy.firePower--; } } } static void scorched_tanks_aim_up(Game* game_state) { if(!game_state->isShooting) { if(game_state->isPlayerTurn && game_state->player.aimAngle < 90) { game_state->player.aimAngle++; } if(!game_state->isPlayerTurn && game_state->enemy.aimAngle < 90) { game_state->enemy.aimAngle++; } } } static void scorched_tanks_aim_down(Game* game_state) { if(game_state->player.aimAngle > 0 && !game_state->isShooting) { if(game_state->isPlayerTurn) { game_state->player.aimAngle--; } else { game_state->enemy.aimAngle--; } } } const NotificationSequence sequence_long_vibro = { &message_vibro_on, &message_delay_500, &message_vibro_off, NULL, }; static void scorched_tanks_fire(Game* game_state) { if(!game_state->isShooting) { if(game_state->isPlayerTurn) { double sinFromAngle = scorched_tanks_sin[game_state->player.aimAngle]; double cosFromAngle = scorched_tanks_cos[game_state->player.aimAngle]; uint8_t aimX1 = game_state->player.locationX; uint8_t aimY1 = game_state->ground[game_state->player.locationX].y - TANK_COLLIDER_SIZE; int aimX2 = aimX1 + TANK_BARREL_LENGTH * cosFromAngle; int aimY2 = aimY1 + TANK_BARREL_LENGTH * sinFromAngle; game_state->bulletPosition.x = aimX2; game_state->bulletPosition.y = aimY2; game_state->bulletVector.x = scorched_tanks_cos[game_state->player.aimAngle] * ((double)game_state->player.firePower / 10); game_state->bulletVector.y = scorched_tanks_sin[game_state->player.aimAngle] * ((double)game_state->player.firePower / 10); } else { double sinFromAngle = scorched_tanks_sin[game_state->enemy.aimAngle]; double cosFromAngle = scorched_tanks_cos[game_state->enemy.aimAngle]; uint8_t aimX1 = game_state->enemy.locationX; uint8_t aimY1 = game_state->ground[game_state->enemy.locationX].y - TANK_COLLIDER_SIZE; int aimX2 = aimX1 + TANK_BARREL_LENGTH * cosFromAngle; int aimY2 = aimY1 + TANK_BARREL_LENGTH * sinFromAngle; aimX2 = aimX1 - (aimX2 - aimX1); game_state->bulletPosition.x = aimX2; game_state->bulletPosition.y = aimY2; game_state->bulletVector.x = -scorched_tanks_cos[game_state->enemy.aimAngle] * ((double)game_state->enemy.firePower / 10); game_state->bulletVector.y = scorched_tanks_sin[game_state->enemy.aimAngle] * ((double)game_state->enemy.firePower / 10); } game_state->trajectoryAnimationStep = 0; for(int x = 0; x < SCREEN_WIDTH; x++) { game_state->trajectory[x].x = 0; game_state->trajectory[x].y = 0; } game_state->isShooting = true; NotificationApp* notification = furi_record_open("notification"); notification_message(notification, &sequence_long_vibro); notification_message(notification, &sequence_blink_white_100); furi_record_close("notification"); } } int32_t scorched_tanks_game_app(void* p) { UNUSED(p); srand(DWT->CYCCNT); FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(ScorchedTanksEvent)); Game* game_state = malloc(sizeof(Game)); scorched_tanks_init_game(game_state); game_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); if(!game_state->mutex) { FURI_LOG_E("ScorchedTanks", "cannot create mutex\r\n"); furi_message_queue_free(event_queue); free(game_state); return 255; } ViewPort* view_port = view_port_alloc(); view_port_draw_callback_set(view_port, scorched_tanks_render_callback, game_state); view_port_input_callback_set(view_port, scorched_tanks_input_callback, event_queue); FuriTimer* timer = furi_timer_alloc(scorched_tanks_update_timer_callback, FuriTimerTypePeriodic, event_queue); furi_timer_start(timer, 2000); // Open GUI and register view_port Gui* gui = furi_record_open(RECORD_GUI); gui_add_view_port(gui, view_port, GuiLayerFullscreen); ScorchedTanksEvent event; for(bool processing = true; processing;) { furi_message_queue_get(event_queue, &event, 50); furi_mutex_acquire(game_state->mutex, FuriWaitForever); if(event.type == EventTypeKey) { // && game->isPlayerTurn if(event.input.type == InputTypeRepeat || event.input.type == InputTypeShort) { switch(event.input.key) { case InputKeyUp: scorched_tanks_aim_up(game_state); break; case InputKeyDown: scorched_tanks_aim_down(game_state); break; case InputKeyRight: scorched_tanks_increase_power(game_state); break; case InputKeyLeft: scorched_tanks_decrease_power(game_state); break; case InputKeyOk: scorched_tanks_fire(game_state); break; case InputKeyBack: processing = false; break; default: break; } } } else if(event.type == EventTypeTick) { scorched_tanks_calculate_trajectory(game_state); } view_port_update(view_port); furi_mutex_release(game_state->mutex); } furi_timer_free(timer); view_port_enabled_set(view_port, false); gui_remove_view_port(gui, view_port); furi_record_close(RECORD_GUI); view_port_free(view_port); furi_message_queue_free(event_queue); furi_mutex_free(game_state->mutex); free(game_state); return 0; }