Battle Zone

Source: src/battle_zone.c · 468 lines · 28 functions.

Screen captures

Views of this part running on the Mini Arcade.

Battle Zone game screen on the Mini Arcade
Battle Zone showing its vector-style battlefield.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

random_unit() · source line 38
static float random_unit(void)
  1. Exit this functionreturn (float)(esp_random() & 0xffffU) / 65535.0f;
Read original function source
static float random_unit(void)
{
    return (float)(esp_random() & 0xffffU) / 65535.0f;
}
clamp() · source line 43
static float clamp(float value, float minimum, float maximum)
  1. Exit this functionreturn value < minimum ? minimum : value > maximum ? maximum : value;
Read original function source
static float clamp(float value, float minimum, float maximum)
{
    return value < minimum ? minimum : value > maximum ? maximum : value;
}
wrap_angle() · source line 48
static float wrap_angle(float angle)
  1. WHILE loopangle < -PI

    Repeat body while the loop condition permits; then continue below.

    1. Executeangle += 2.0f * PI;
  2. WHILE loopangle >= PI

    Repeat body while the loop condition permits; then continue below.

    1. Executeangle -= 2.0f * PI;
  3. Exit this functionreturn angle;
Read original function source
static float wrap_angle(float angle)
{
    while (angle < -PI) angle += 2.0f * PI;
    while (angle >= PI) angle -= 2.0f * PI;
    return angle;
}
distance_squared() · source line 55
static float distance_squared(float ax, float az, float bx, float bz)
  1. Executeconst float dx = ax - bx, dz = az - bz;
  2. Exit this functionreturn dx * dx + dz * dz;
Read original function source
static float distance_squared(float ax, float az, float bx, float bz)
{
    const float dx = ax - bx, dz = az - bz;
    return dx * dx + dz * dz;
}
position_blocked() · source line 61
static bool position_blocked(float x, float z, float radius)

Calls into this documentation:

  1. FOR loopuint8_t index = 0U; index < MAX_COVERS; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst float combined_radius = covers[index].radius + radius;
      2. Decisiondistance_squared(x, z, covers[index].x, covers[index].z) < combined_radius * combined_radius
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool position_blocked(float x, float z, float radius)
{
    for (uint8_t index = 0U; index < MAX_COVERS; ++index) {
        const float combined_radius = covers[index].radius + radius;
        if (distance_squared(x, z, covers[index].x, covers[index].z) < combined_radius * combined_radius)
            return true;
    }
    return false;
}
reset_tank() · source line 71
static void reset_tank(uint32_t now_ms)

Calls into this documentation:

  1. Executetank_x = tank_z = 0.0f;
  2. Executetank_angle = -PI / 2.0f;
  3. Executeprotection_until = now_ms + PLAYER_PROTECTION_MS;
  4. Executeengine_running = false;
  5. ExecuteaudioSetEngineThrust(false);
  6. Executecontrols_ready = false;
Read original function source
static void reset_tank(uint32_t now_ms)
{
    tank_x = tank_z = 0.0f;
    tank_angle = -PI / 2.0f;
    protection_until = now_ms + PLAYER_PROTECTION_MS;
    engine_running = false;
    audioSetEngineThrust(false);
    controls_ready = false;
}
spawn_wave() · source line 81
static void spawn_wave(void)

Calls into this documentation:

  1. Executememset(enemies, 0, sizeof(enemies));
  2. Executememset(shots, 0, sizeof(shots));
  3. Executecovers[0] = (cover_t){-32.0f, -26.0f, 7.0f};
  4. Executecovers[1] = (cover_t){31.0f, -31.0f, 8.0f};
  5. Executecovers[2] = (cover_t){-43.0f, 24.0f, 6.0f};
  6. Executecovers[3] = (cover_t){37.0f, 31.0f, 7.0f};
  7. Executecovers[4] = (cover_t){0.0f, 57.0f, 7.0f};
  8. Executeconst uint8_t count = wave < 6U ? (uint8_t)(wave + 1U) : MAX_ENEMIES;
  9. FOR loopuint8_t index = 0U; index < count; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executefloat x, z;
      2. DO / WHILE loopposition_blocked(x, z, 3.5f)

        Run body, then test condition; repeat while true.

          1. Executeconst float angle = random_unit() * 2.0f * PI;
          2. Executeconst float distance = 42.0f + random_unit() * 42.0f;
          3. Executex = cosf(angle) * distance;
          4. Executez = sinf(angle) * distance;
      3. Executeenemies[index] = (enemy_t){x, z, true, 900U + (uint32_t)(random_unit() * 1600.0f)};
Read original function source
static void spawn_wave(void)
{
    memset(enemies, 0, sizeof(enemies));
    memset(shots, 0, sizeof(shots));
    covers[0] = (cover_t){-32.0f, -26.0f, 7.0f};
    covers[1] = (cover_t){31.0f, -31.0f, 8.0f};
    covers[2] = (cover_t){-43.0f, 24.0f, 6.0f};
    covers[3] = (cover_t){37.0f, 31.0f, 7.0f};
    covers[4] = (cover_t){0.0f, 57.0f, 7.0f};
    const uint8_t count = wave < 6U ? (uint8_t)(wave + 1U) : MAX_ENEMIES;
    for (uint8_t index = 0U; index < count; ++index) {
        float x, z;
        do {
            const float angle = random_unit() * 2.0f * PI;
            const float distance = 42.0f + random_unit() * 42.0f;
            x = cosf(angle) * distance;
            z = sinf(angle) * distance;
        } while (position_blocked(x, z, 3.5f));
        enemies[index] = (enemy_t){x, z, true,
                                   900U + (uint32_t)(random_unit() * 1600.0f)};
    }
}
begin_game() · source line 104
static void begin_game(uint32_t now_ms)

Calls into this documentation:

  1. Executescore = 0U;
  2. Executewave = 1U;
  3. Executelives = 3U;
  4. Executefire_due = accumulator = 0U;
  5. Executelast_update = now_ms;
  6. Executereset_tank(now_ms);
  7. Executespawn_wave();
  8. Executestate = BZ_PLAYING;
  9. ExecuteaudioPlay(AUDIO_SFX_GAME_START);
Read original function source
static void begin_game(uint32_t now_ms)
{
    score = 0U;
    wave = 1U;
    lives = 3U;
    fire_due = accumulator = 0U;
    last_update = now_ms;
    reset_tank(now_ms);
    spawn_wave();
    state = BZ_PLAYING;
    audioPlay(AUDIO_SFX_GAME_START);
}
fire_shot() · source line 117
static void fire_shot(bool enemy_shot, float x, float z, float angle)

Calls into this documentation:

  1. FOR loopuint8_t index = 0U; index < MAX_SHOTS; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decisionshots[index].remaining != 0U
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst float speed = enemy_shot ? ENEMY_SHOT_SPEED : PLAYER_SHOT_SPEED;
      3. Executeshots[index] = (shot_t){x, z, cosf(angle) * speed, sinf(angle) * speed, enemy_shot ? 1800U : 1300U, enemy_shot};
      4. ExecuteaudioPlay(enemy_shot ? AUDIO_SFX_ENEMY_SHOT : AUDIO_SFX_PLAYER_SHOT);
      5. Exit this functionreturn;
Read original function source
static void fire_shot(bool enemy_shot, float x, float z, float angle)
{
    for (uint8_t index = 0U; index < MAX_SHOTS; ++index) {
        if (shots[index].remaining != 0U) continue;
        const float speed = enemy_shot ? ENEMY_SHOT_SPEED : PLAYER_SHOT_SPEED;
        shots[index] = (shot_t){x, z, cosf(angle) * speed, sinf(angle) * speed,
                                enemy_shot ? 1800U : 1300U, enemy_shot};
        audioPlay(enemy_shot ? AUDIO_SFX_ENEMY_SHOT : AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}
fire_player_shot() · source line 129
static void fire_player_shot(uint32_t now_ms)

Calls into this documentation:

  1. Decisionfire_due != 0U
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executefire_shot(false, tank_x + cosf(tank_angle) * 4.0f, tank_z + sinf(tank_angle) * 4.0f, tank_angle);
  3. Executefire_due = PLAYER_SHOT_DELAY_MS;
  4. Execute(void)now_ms;
Read original function source
static void fire_player_shot(uint32_t now_ms)
{
    if (fire_due != 0U) return;
    fire_shot(false, tank_x + cosf(tank_angle) * 4.0f, tank_z + sinf(tank_angle) * 4.0f,
              tank_angle);
    fire_due = PLAYER_SHOT_DELAY_MS;
    (void)now_ms;
}
destroy_enemy() · source line 138
static void destroy_enemy(uint8_t index)

Calls into this documentation:

  1. Executeenemies[index].active = false;
  2. Executescore += 100U;
  3. ExecuteaudioPlay(AUDIO_SFX_HIT);
Read original function source
static void destroy_enemy(uint8_t index)
{
    enemies[index].active = false;
    score += 100U;
    audioPlay(AUDIO_SFX_HIT);
}
lose_life() · source line 145
static void lose_life(uint32_t now_ms)

Calls into this documentation:

  1. Decisionnow_ms < protection_until
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionlives != 0U
    True
    1. Execute--lives;
    False

    Continue after this decision.

  3. ExecuteaudioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
  4. ExecuteaudioSetEngineThrust(false);
  5. Executeengine_running = false;
  6. Decisionlives == 0U
    True
      1. Executestate = BZ_FINISHED;
    False
      1. Executememset(shots, 0, sizeof(shots));
      2. Executereset_tank(now_ms);
Read original function source
static void lose_life(uint32_t now_ms)
{
    if (now_ms < protection_until) return;
    if (lives != 0U) --lives;
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    audioSetEngineThrust(false);
    engine_running = false;
    if (lives == 0U) {
        state = BZ_FINISHED;
    } else {
        memset(shots, 0, sizeof(shots));
        reset_tank(now_ms);
    }
}
choose_auto_controls() · source line 160
static void choose_auto_controls(int *turn, float *drive, bool *fire)

Calls into this documentation:

  1. Executeconst enemy_t *target = NULL;
  2. Executefloat best_distance = 0.0f;
  3. FOR loopuint8_t index = 0U; index < MAX_ENEMIES; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decision!enemies[index].active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst float distance = distance_squared(tank_x, tank_z, enemies[index].x, enemies[index].z);
      3. Decisiontarget == NULL || distance < best_distance
        True
          1. Executetarget = &enemies[index];
          2. Executebest_distance = distance;
        False

        Continue after this decision.

  4. Decisiontarget == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executeconst float dx = target->x - tank_x, dz = target->z - tank_z;
  6. Executeconst float cross = cosf(tank_angle) * dz - sinf(tank_angle) * dx;
  7. Executeconst float forward = cosf(tank_angle) * dx + sinf(tank_angle) * dz;
  8. Execute*turn = cross > 2.0f ? 1 : cross < -2.0f ? -1 : 0;
  9. Execute*drive = forward < 18.0f ? -0.6f : forward > 50.0f ? 0.7f : 0.0f;
  10. Execute*fire = forward > 0.0f && fabsf(cross) < 3.5f;
Read original function source
static void choose_auto_controls(int *turn, float *drive, bool *fire)
{
    const enemy_t *target = NULL;
    float best_distance = 0.0f;
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) {
        if (!enemies[index].active) continue;
        const float distance = distance_squared(tank_x, tank_z, enemies[index].x, enemies[index].z);
        if (target == NULL || distance < best_distance) { target = &enemies[index]; best_distance = distance; }
    }
    if (target == NULL) return;
    const float dx = target->x - tank_x, dz = target->z - tank_z;
    const float cross = cosf(tank_angle) * dz - sinf(tank_angle) * dx;
    const float forward = cosf(tank_angle) * dx + sinf(tank_angle) * dz;
    *turn = cross > 2.0f ? 1 : cross < -2.0f ? -1 : 0;
    *drive = forward < 18.0f ? -0.6f : forward > 50.0f ? 0.7f : 0.0f;
    *fire = forward > 0.0f && fabsf(cross) < 3.5f;
}
update_enemies() · source line 178
static void update_enemies(uint32_t now_ms, float seconds)

Calls into this documentation:

  1. FOR loopuint8_t index = 0U; index < MAX_ENEMIES; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeenemy_t *enemy = &enemies[index];
      2. Decision!enemy->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst float dx = tank_x - enemy->x, dz = tank_z - enemy->z;
      4. Executeconst float distance = sqrtf(dx * dx + dz * dz);
      5. Decisiondistance > 0.1f
        True
          1. Executeconst float strafe = (index & 1U) == 0U ? 1.0f : -1.0f;
          2. Executeconst float next_x = enemy->x + (dx / distance * 6.0f - dz / distance * strafe * 2.0f) * seconds;
          3. Executeconst float next_z = enemy->z + (dz / distance * 6.0f + dx / distance * strafe * 2.0f) * seconds;
          4. Decision!position_blocked(next_x, next_z, 3.5f)
            True
              1. Executeenemy->x = next_x;
              2. Executeenemy->z = next_z;
            False

            Continue after this decision.

        False

        Continue after this decision.

      6. Decisionnow_ms >= enemy->fire_due && distance < 66.0f
        True
          1. Executefire_shot(true, enemy->x, enemy->z, atan2f(dz, dx));
          2. Executeenemy->fire_due = now_ms + 1200U + (uint32_t)(random_unit() * 1100.0f);
        False

        Continue after this decision.

      7. Decisiondistance < 5.0f
        True
        1. Executelose_life(now_ms);
        False

        Continue after this decision.

Read original function source
static void update_enemies(uint32_t now_ms, float seconds)
{
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) {
        enemy_t *enemy = &enemies[index];
        if (!enemy->active) continue;
        const float dx = tank_x - enemy->x, dz = tank_z - enemy->z;
        const float distance = sqrtf(dx * dx + dz * dz);
        if (distance > 0.1f) {
            const float strafe = (index & 1U) == 0U ? 1.0f : -1.0f;
            const float next_x = enemy->x + (dx / distance * 6.0f - dz / distance * strafe * 2.0f) * seconds;
            const float next_z = enemy->z + (dz / distance * 6.0f + dx / distance * strafe * 2.0f) * seconds;
            if (!position_blocked(next_x, next_z, 3.5f)) {
                enemy->x = next_x;
                enemy->z = next_z;
            }
        }
        if (now_ms >= enemy->fire_due && distance < 66.0f) {
            fire_shot(true, enemy->x, enemy->z, atan2f(dz, dx));
            enemy->fire_due = now_ms + 1200U + (uint32_t)(random_unit() * 1100.0f);
        }
        if (distance < 5.0f) lose_life(now_ms);
    }
}
update_shots() · source line 202
static void update_shots(uint32_t now_ms, float seconds)

Calls into this documentation:

  1. FOR loopuint8_t shot_index = 0U; shot_index < MAX_SHOTS; ++shot_index

    Repeat body while the loop condition permits; then continue below.

      1. Executeshot_t *shot = &shots[shot_index];
      2. Decisionshot->remaining == 0U
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeshot->x += shot->dx * seconds;
      4. Executeshot->z += shot->dz * seconds;
      5. Executeshot->remaining = shot->remaining > STEP_MS ? shot->remaining - STEP_MS : 0U;
      6. FOR loopuint8_t cover_index = 0U; cover_index < MAX_COVERS; ++cover_index

        Repeat body while the loop condition permits; then continue below.

          1. Executeconst cover_t *cover = &covers[cover_index];
          2. Decisiondistance_squared(shot->x, shot->z, cover->x, cover->z) >= cover->radius * cover->radius
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. Executeshot->remaining = 0U;
          4. Exit enclosing loop or switchbreak;
      7. Decisionshot->remaining == 0U
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      8. Decisionshot->enemy_shot
        True
          1. Decisiondistance_squared(shot->x, shot->z, tank_x, tank_z) < 14.0f
            True
              1. Executeshot->remaining = 0U;
              2. Executelose_life(now_ms);
            False

            Continue after this decision.

          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      9. FOR loopuint8_t enemy_index = 0U; enemy_index < MAX_ENEMIES; ++enemy_index

        Repeat body while the loop condition permits; then continue below.

          1. Decision!enemies[enemy_index].active
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Decisiondistance_squared(shot->x, shot->z, enemies[enemy_index].x, enemies[enemy_index].z) >= 18.0f
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. Executeshot->remaining = 0U;
          4. Executedestroy_enemy(enemy_index);
          5. Exit enclosing loop or switchbreak;
Read original function source
static void update_shots(uint32_t now_ms, float seconds)
{
    for (uint8_t shot_index = 0U; shot_index < MAX_SHOTS; ++shot_index) {
        shot_t *shot = &shots[shot_index];
        if (shot->remaining == 0U) continue;
        shot->x += shot->dx * seconds;
        shot->z += shot->dz * seconds;
        shot->remaining = shot->remaining > STEP_MS ? shot->remaining - STEP_MS : 0U;
        for (uint8_t cover_index = 0U; cover_index < MAX_COVERS; ++cover_index) {
            const cover_t *cover = &covers[cover_index];
            if (distance_squared(shot->x, shot->z, cover->x, cover->z) >= cover->radius * cover->radius) continue;
            shot->remaining = 0U;
            break;
        }
        if (shot->remaining == 0U) continue;
        if (shot->enemy_shot) {
            if (distance_squared(shot->x, shot->z, tank_x, tank_z) < 14.0f) {
                shot->remaining = 0U;
                lose_life(now_ms);
            }
            continue;
        }
        for (uint8_t enemy_index = 0U; enemy_index < MAX_ENEMIES; ++enemy_index) {
            if (!enemies[enemy_index].active) continue;
            if (distance_squared(shot->x, shot->z, enemies[enemy_index].x, enemies[enemy_index].z) >= 18.0f) continue;
            shot->remaining = 0U;
            destroy_enemy(enemy_index);
            break;
        }
    }
}
enemies_remaining() · source line 234
static bool enemies_remaining(void)
  1. FOR loopuint8_t index = 0U; index < MAX_ENEMIES; ++index

    Repeat body while the loop condition permits; then continue below.

    1. Decisionenemies[index].active
      True
      1. Exit this functionreturn true;
      False

      Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool enemies_remaining(void)
{
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) if (enemies[index].active) return true;
    return false;
}
simulate_step() · source line 240
static void simulate_step(int turn, float drive, bool fire, uint32_t now_ms)

Calls into this documentation:

  1. Executeconst float seconds = STEP_MS / 1000.0f;
  2. Decisionfire_due > 0U
    True
    1. Executefire_due = fire_due > STEP_MS ? fire_due - STEP_MS : 0U;
    False

    Continue after this decision.

  3. Executetank_angle = wrap_angle(tank_angle + (float)turn * 2.25f * seconds);
  4. Executeconst bool moving = drive > 0.05f || drive < -0.05f;
  5. Decisionengine_running != moving
    True
      1. Executeengine_running = moving;
      2. ExecuteaudioSetEngineThrust(moving);
    False

    Continue after this decision.

  6. Executeconst float next_x = clamp(tank_x + cosf(tank_angle) * drive * 30.0f * seconds, -ARENA_HALF_SIZE, ARENA_HALF_SIZE);
  7. Executeconst float next_z = clamp(tank_z + sinf(tank_angle) * drive * 30.0f * seconds, -ARENA_HALF_SIZE, ARENA_HALF_SIZE);
  8. Decision!position_blocked(next_x, next_z, 4.0f)
    True
      1. Executetank_x = next_x;
      2. Executetank_z = next_z;
    False

    Continue after this decision.

  9. Decisionfire
    True
    1. Executefire_player_shot(now_ms);
    False

    Continue after this decision.

  10. Executeupdate_enemies(now_ms, seconds);
  11. Executeupdate_shots(now_ms, seconds);
  12. Decision!enemies_remaining() && state == BZ_PLAYING
    True
      1. ExecuteaudioPlay(AUDIO_SFX_WAVE_CLEAR);
      2. Execute++wave;
      3. Executespawn_wave();
    False

    Continue after this decision.

  13. Decisionnow_ms >= radar_due
    True
      1. ExecuteaudioPlay(AUDIO_SFX_MENU_MOVE);
      2. Executeradar_due = now_ms + 950U;
    False

    Continue after this decision.

Read original function source
static void simulate_step(int turn, float drive, bool fire, uint32_t now_ms)
{
    const float seconds = STEP_MS / 1000.0f;
    if (fire_due > 0U) fire_due = fire_due > STEP_MS ? fire_due - STEP_MS : 0U;
    tank_angle = wrap_angle(tank_angle + (float)turn * 2.25f * seconds);
    const bool moving = drive > 0.05f || drive < -0.05f;
    if (engine_running != moving) {
        engine_running = moving;
        audioSetEngineThrust(moving);
    }
    const float next_x = clamp(tank_x + cosf(tank_angle) * drive * 30.0f * seconds,
                               -ARENA_HALF_SIZE, ARENA_HALF_SIZE);
    const float next_z = clamp(tank_z + sinf(tank_angle) * drive * 30.0f * seconds,
                               -ARENA_HALF_SIZE, ARENA_HALF_SIZE);
    if (!position_blocked(next_x, next_z, 4.0f)) {
        tank_x = next_x;
        tank_z = next_z;
    }
    if (fire) fire_player_shot(now_ms);
    update_enemies(now_ms, seconds);
    update_shots(now_ms, seconds);
    if (!enemies_remaining() && state == BZ_PLAYING) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        ++wave;
        spawn_wave();
    }
    if (now_ms >= radar_due) {
        audioPlay(AUDIO_SFX_MENU_MOVE);
        radar_due = now_ms + 950U;
    }
}
startBattleZone() · source line 272
void startBattleZone(uint32_t now_ms)

Calls into this documentation:

  1. Executerunning = true;
  2. Executestate = BZ_READY;
  3. Executeauto_mode = false;
  4. Executelast_update = now_ms;
  5. Executeradar_due = now_ms + 700U;
  6. ExecutemodeSelectionReset(&mode_input);
Read original function source
void startBattleZone(uint32_t now_ms)
{
    running = true;
    state = BZ_READY;
    auto_mode = false;
    last_update = now_ms;
    radar_due = now_ms + 700U;
    modeSelectionReset(&mode_input);
}
stopBattleZone() · source line 282
void stopBattleZone(void)

Calls into this documentation:

  1. ExecuteaudioSetEngineThrust(false);
  2. Executeengine_running = false;
  3. Executerunning = false;
Read original function source
void stopBattleZone(void)
{
    audioSetEngineThrust(false);
    engine_running = false;
    running = false;
}
battleZoneAction() · source line 289
void battleZoneAction(bool start, bool stop_auto, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionstate == BZ_READY && start
    True
    1. Executebegin_game(now_ms);
    False
    1. Decisionstate == BZ_FINISHED && start
      True
      1. Executebegin_game(now_ms);
      False
      1. Decisionstate == BZ_PLAYING && auto_mode && stop_auto
        True
          1. Executeauto_mode = false;
          2. ExecutemodeSelectionReset(&mode_input);
          3. ExecuteaudioSetEngineThrust(false);
          4. Executeengine_running = false;
          5. Executestate = BZ_READY;
        False

        Continue after this decision.

Read original function source
void battleZoneAction(bool start, bool stop_auto, uint32_t now_ms)
{
    if (!running) return;
    if (state == BZ_READY && start) begin_game(now_ms);
    else if (state == BZ_FINISHED && start) begin_game(now_ms);
    else if (state == BZ_PLAYING && auto_mode && stop_auto) {
        auto_mode = false;
        modeSelectionReset(&mode_input);
        audioSetEngineThrust(false);
        engine_running = false;
        state = BZ_READY;
    }
}
updateBattleZone() · source line 303
void updateBattleZone(int x_offset, int y_offset, bool valid, bool left, bool right,
                      bool fire, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint32_t elapsed = now_ms - last_update;
  3. Executelast_update = now_ms;
  4. Decisionstate == BZ_READY
    True
      1. DecisionmodeSelectionNext(&mode_input, x_offset, y_offset, valid, left, right, now_ms)
        True
        1. Executeauto_mode = !auto_mode;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionstate != BZ_PLAYING
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decision!valid
    True
    1. Executecontrols_ready = false;
    False
    1. Decisionx_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350
      True
      1. Executecontrols_ready = true;
      False

      Continue after this decision.

  7. Executeint turn = left || right ? (int)right - (int)left : valid && controls_ready ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0;
  8. Executefloat drive = valid && controls_ready ? (y_offset > 600 ? 1.0f : y_offset < -600 ? -0.65f : 0.0f) : 0.0f;
  9. Executebool should_fire = fire;
  10. Decisionauto_mode
    True
    1. Executechoose_auto_controls(&turn, &drive, &should_fire);
    False

    Continue after this decision.

  11. Executeaccumulator += elapsed > 100U ? 100U : elapsed;
  12. WHILE loopaccumulator >= STEP_MS && state == BZ_PLAYING

    Repeat body while the loop condition permits; then continue below.

      1. Executeaccumulator -= STEP_MS;
      2. Executesimulate_step(turn, drive, should_fire, now_ms);
Read original function source
void updateBattleZone(int x_offset, int y_offset, bool valid, bool left, bool right,
                      bool fire, uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == BZ_READY) {
        if (modeSelectionNext(&mode_input, x_offset, y_offset, valid, left, right, now_ms)) auto_mode = !auto_mode;
        return;
    }
    if (state != BZ_PLAYING) return;
    if (!valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350) controls_ready = true;
    int turn = left || right ? (int)right - (int)left :
        valid && controls_ready ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0;
    float drive = valid && controls_ready ? (y_offset > 600 ? 1.0f : y_offset < -600 ? -0.65f : 0.0f) : 0.0f;
    bool should_fire = fire;
    if (auto_mode) choose_auto_controls(&turn, &drive, &should_fire);
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == BZ_PLAYING) {
        accumulator -= STEP_MS;
        simulate_step(turn, drive, should_fire, now_ms);
    }
}
project() · source line 328
static bool project(float world_x, float world_z, float *side, float *forward)
  1. Executeconst float dx = world_x - tank_x, dz = world_z - tank_z;
  2. Execute*side = -sinf(tank_angle) * dx + cosf(tank_angle) * dz;
  3. Execute*forward = cosf(tank_angle) * dx + sinf(tank_angle) * dz;
  4. Exit this functionreturn *forward > 3.0f;
Read original function source
static bool project(float world_x, float world_z, float *side, float *forward)
{
    const float dx = world_x - tank_x, dz = world_z - tank_z;
    *side = -sinf(tank_angle) * dx + cosf(tank_angle) * dz;
    *forward = cosf(tank_angle) * dx + sinf(tank_angle) * dz;
    return *forward > 3.0f;
}
drawBattleZoneLandscape() · source line 336
void drawBattleZoneLandscape(uint16_t *frame)

Calls into this documentation:

  1. Executeconst uint16_t green = COLOR_LIME;
  2. Executeconst int horizon = BATTLE_ZONE_HORIZON_Y;
  3. Executestatic const int terrain_bands[] = {58, 76, 101, 134, 166};
  4. FOR loopsize_t index = 0U; index < sizeof(terrain_bands) / sizeof(terrain_bands[0]); ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst int y = terrain_bands[index];
      2. Executedraw_line(frame, 0, y, LCD_WIDTH - 1, y, COLOR_DARKGREEN);
  5. FOR loopint lane = -3; lane <= 3; ++lane

    Repeat body while the loop condition permits; then continue below.

    1. Executedraw_line(frame, 160 + lane * 10, horizon, 160 + lane * 50, LCD_HEIGHT - 1, COLOR_DARKGREEN);
  6. Executedraw_line(frame, 0, horizon, LCD_WIDTH - 1, horizon, green);
  7. Executedraw_line(frame, 15, horizon, 58, horizon - 12, COLOR_DARKGREEN);
  8. Executedraw_line(frame, 58, horizon - 12, 104, horizon, COLOR_DARKGREEN);
  9. Executedraw_line(frame, 212, horizon, 258, horizon - 15, COLOR_DARKGREEN);
  10. Executedraw_line(frame, 258, horizon - 15, 307, horizon, COLOR_DARKGREEN);
Read original function source
void drawBattleZoneLandscape(uint16_t *frame)
{
    const uint16_t green = COLOR_LIME;
    const int horizon = BATTLE_ZONE_HORIZON_Y;
    static const int terrain_bands[] = {58, 76, 101, 134, 166};
    for (size_t index = 0U; index < sizeof(terrain_bands) / sizeof(terrain_bands[0]); ++index) {
        const int y = terrain_bands[index];
        draw_line(frame, 0, y, LCD_WIDTH - 1, y, COLOR_DARKGREEN);
    }
    for (int lane = -3; lane <= 3; ++lane)
        draw_line(frame, 160 + lane * 10, horizon, 160 + lane * 50, LCD_HEIGHT - 1, COLOR_DARKGREEN);
    draw_line(frame, 0, horizon, LCD_WIDTH - 1, horizon, green);
    draw_line(frame, 15, horizon, 58, horizon - 12, COLOR_DARKGREEN);
    draw_line(frame, 58, horizon - 12, 104, horizon, COLOR_DARKGREEN);
    draw_line(frame, 212, horizon, 258, horizon - 15, COLOR_DARKGREEN);
    draw_line(frame, 258, horizon - 15, 307, horizon, COLOR_DARKGREEN);
}
draw_enemy() · source line 354
static void draw_enemy(uint16_t *frame, const enemy_t *enemy)

Calls into this documentation:

  1. Executefloat side, forward;
  2. Decision!project(enemy->x, enemy->z, &side, &forward) || fabsf(side) > forward * 1.7f
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeconst int centre_x = (int)lroundf(160.0f + side * 108.0f / forward);
  4. Executeconst int base_y = (int)lroundf(43.0f + 118.0f / forward);
  5. Executeconst int width = (int)clamp(150.0f / forward, 4.0f, 38.0f);
  6. Executeconst int height = width / 2 + 2;
  7. Executedraw_rect(frame, centre_x - width / 2, base_y - height, width, height, COLOR_LIME);
  8. Executedraw_line(frame, centre_x - width / 2 - 2, base_y, centre_x + width / 2 + 2, base_y, COLOR_LIME);
  9. Executedraw_line(frame, centre_x, base_y - height, centre_x, base_y - height - width / 2, COLOR_LIME);
Read original function source
static void draw_enemy(uint16_t *frame, const enemy_t *enemy)
{
    float side, forward;
    if (!project(enemy->x, enemy->z, &side, &forward) || fabsf(side) > forward * 1.7f) return;
    const int centre_x = (int)lroundf(160.0f + side * 108.0f / forward);
    const int base_y = (int)lroundf(43.0f + 118.0f / forward);
    const int width = (int)clamp(150.0f / forward, 4.0f, 38.0f);
    const int height = width / 2 + 2;
    draw_rect(frame, centre_x - width / 2, base_y - height, width, height, COLOR_LIME);
    draw_line(frame, centre_x - width / 2 - 2, base_y, centre_x + width / 2 + 2, base_y, COLOR_LIME);
    draw_line(frame, centre_x, base_y - height, centre_x, base_y - height - width / 2, COLOR_LIME);
}
draw_cover() · source line 367
static void draw_cover(uint16_t *frame, const cover_t *cover)

Calls into this documentation:

  1. Executefloat side, forward;
  2. Decision!project(cover->x, cover->z, &side, &forward) || fabsf(side) > forward * 1.7f
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeconst int centre_x = (int)lroundf(160.0f + side * 108.0f / forward);
  4. Executeconst int base_y = (int)lroundf(43.0f + 118.0f / forward);
  5. Executeconst int width = (int)clamp(230.0f / forward, 7.0f, 54.0f);
  6. Executeconst int height = width / 2 + 5;
  7. Executedraw_rect(frame, centre_x - width / 2, base_y - height, width, height, COLOR_LIME);
  8. Executedraw_line(frame, centre_x - width / 2 - 3, base_y - height, centre_x, base_y - height - height / 2, COLOR_LIME);
  9. Executedraw_line(frame, centre_x, base_y - height - height / 2, centre_x + width / 2 + 3, base_y - height, COLOR_LIME);
Read original function source
static void draw_cover(uint16_t *frame, const cover_t *cover)
{
    float side, forward;
    if (!project(cover->x, cover->z, &side, &forward) || fabsf(side) > forward * 1.7f) return;
    const int centre_x = (int)lroundf(160.0f + side * 108.0f / forward);
    const int base_y = (int)lroundf(43.0f + 118.0f / forward);
    const int width = (int)clamp(230.0f / forward, 7.0f, 54.0f);
    const int height = width / 2 + 5;
    draw_rect(frame, centre_x - width / 2, base_y - height, width, height, COLOR_LIME);
    draw_line(frame, centre_x - width / 2 - 3, base_y - height,
              centre_x, base_y - height - height / 2, COLOR_LIME);
    draw_line(frame, centre_x, base_y - height - height / 2,
              centre_x + width / 2 + 3, base_y - height, COLOR_LIME);
}
draw_radar_point() · source line 382
static void draw_radar_point(uint16_t *frame, int centre_x, int centre_y, int radius,
                             float world_x, float world_z, uint16_t colour)

Calls into this documentation:

  1. Executefloat offset_x = world_x - tank_x;
  2. Executefloat offset_z = world_z - tank_z;
  3. Executeconst float distance = sqrtf(offset_x * offset_x + offset_z * offset_z);
  4. Executeconst float maximum_distance = ARENA_HALF_SIZE;
  5. Decisiondistance > maximum_distance
    True
      1. Executeoffset_x *= maximum_distance / distance;
      2. Executeoffset_z *= maximum_distance / distance;
    False

    Continue after this decision.

  6. Executeconst int x = centre_x + (int)(offset_x * (radius - 3) / maximum_distance);
  7. Executeconst int y = centre_y + (int)(offset_z * (radius - 3) / maximum_distance);
  8. Executefill_circle(frame, x, y, 2, colour);
Read original function source
static void draw_radar_point(uint16_t *frame, int centre_x, int centre_y, int radius,
                             float world_x, float world_z, uint16_t colour)
{
    float offset_x = world_x - tank_x;
    float offset_z = world_z - tank_z;
    const float distance = sqrtf(offset_x * offset_x + offset_z * offset_z);
    const float maximum_distance = ARENA_HALF_SIZE;
    if (distance > maximum_distance) {
        offset_x *= maximum_distance / distance;
        offset_z *= maximum_distance / distance;
    }
    const int x = centre_x + (int)(offset_x * (radius - 3) / maximum_distance);
    const int y = centre_y + (int)(offset_z * (radius - 3) / maximum_distance);
    fill_circle(frame, x, y, 2, colour);
}
draw_radar() · source line 398
static void draw_radar(uint16_t *frame)

Calls into this documentation:

  1. Executeconst int centre_x = 284, centre_y = 135, radius = 25;
  2. Executedraw_ring(frame, centre_x, centre_y, radius, COLOR_DARKGREEN);
  3. Executedraw_line(frame, centre_x - radius, centre_y, centre_x + radius, centre_y, COLOR_DARKGREEN);
  4. Executedraw_line(frame, centre_x, centre_y - radius, centre_x, centre_y + radius, COLOR_DARKGREEN);
  5. Executefill_circle(frame, centre_x, centre_y, 2, COLOR_LIME);
  6. Executedraw_line(frame, centre_x, centre_y, centre_x + (int)(cosf(tank_angle) * 6.0f), centre_y + (int)(sinf(tank_angle) * 6.0f), COLOR_LIME);
  7. FOR loopuint8_t index = 0U; index < MAX_ENEMIES; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decision!enemies[index].active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executedraw_radar_point(frame, centre_x, centre_y, radius, enemies[index].x, enemies[index].z, COLOR_RED);
  8. FOR loopuint8_t index = 0U; index < MAX_COVERS; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executedraw_radar_point(frame, centre_x, centre_y, radius, covers[index].x, covers[index].z, COLOR_YELLOW);
Read original function source
static void draw_radar(uint16_t *frame)
{
    const int centre_x = 284, centre_y = 135, radius = 25;
    draw_ring(frame, centre_x, centre_y, radius, COLOR_DARKGREEN);
    draw_line(frame, centre_x - radius, centre_y, centre_x + radius, centre_y, COLOR_DARKGREEN);
    draw_line(frame, centre_x, centre_y - radius, centre_x, centre_y + radius, COLOR_DARKGREEN);
    fill_circle(frame, centre_x, centre_y, 2, COLOR_LIME);
    draw_line(frame, centre_x, centre_y,
              centre_x + (int)(cosf(tank_angle) * 6.0f),
              centre_y + (int)(sinf(tank_angle) * 6.0f), COLOR_LIME);
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) {
        if (!enemies[index].active) continue;
        draw_radar_point(frame, centre_x, centre_y, radius,
                         enemies[index].x, enemies[index].z, COLOR_RED);
    }
    for (uint8_t index = 0U; index < MAX_COVERS; ++index) {
        draw_radar_point(frame, centre_x, centre_y, radius,
                         covers[index].x, covers[index].z, COLOR_YELLOW);
    }
}
drawBattleZone() · source line 419
void drawBattleZone(uint16_t *frame)

Calls into this documentation:

  1. Decision!running || frame == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeclear(frame, COLOR_BLACK);
  3. Executechar text[40];
  4. Decisionstate == BZ_READY
    True
      1. Executedraw_text_centred(frame, 160, 15, "BATTLE ZONE", 3, COLOR_LIME);
      2. Executedraw_text_centred(frame, 83, 55, "PLAY", 2, auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
      3. Executedraw_text_centred(frame, 237, 55, "AUTO", 2, auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
      4. Executedraw_text_centred(frame, 160, 92, "D/B/STICK CHANGE A/K START", 1, COLOR_LIGHTGRAY);
      5. Executedraw_text_centred(frame, 160, 111, "D/B TURN STICK UP/DOWN DRIVE", 1, COLOR_LIGHTGRAY);
      6. Executedraw_text_centred(frame, 160, 130, "HOLD A/K FIRE", 1, COLOR_LIGHTGRAY);
      7. Executedraw_text_centred(frame, 160, 155, "BOOT BACK", 1, COLOR_DARKGREY);
      8. Exit this functionreturn;
    False

    Continue after this decision.

  5. ExecutedrawBattleZoneLandscape(frame);
  6. FOR loopuint8_t index = 0U; index < MAX_COVERS; ++index

    Repeat body while the loop condition permits; then continue below.

    1. Executedraw_cover(frame, &covers[index]);
  7. FOR loopuint8_t index = 0U; index < MAX_ENEMIES; ++index

    Repeat body while the loop condition permits; then continue below.

    1. Decisionenemies[index].active
      True
      1. Executedraw_enemy(frame, &enemies[index]);
      False

      Continue after this decision.

  8. FOR loopuint8_t index = 0U; index < MAX_SHOTS; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decisionshots[index].remaining == 0U
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executefloat side, forward;
      3. Decision!project(shots[index].x, shots[index].z, &side, &forward) || fabsf(side) > forward * 1.7f
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      4. Executeconst int x = (int)lroundf(160.0f + side * 108.0f / forward);
      5. Executeconst int y = (int)lroundf(43.0f + 118.0f / forward);
      6. Executeconst float size = shots[index].enemy_shot ? 26.0f / forward : 11.0f / forward;
      7. Executeconst int radius = (int)clamp(size + 1.0f, 1.0f, shots[index].enemy_shot ? 7.0f : 3.0f);
      8. Executefill_rect(frame, x - radius, y - radius, radius * 2 + 1, radius * 2 + 1, shots[index].enemy_shot ? COLOR_RED : COLOR_YELLOW);
  9. Executedraw_radar(frame);
  10. Executesnprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
  11. Executedraw_text(frame, 4, 3, text, 1, COLOR_LIME);
  12. Executesnprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
  13. Executedraw_text_centred(frame, 160, 3, text, 1, COLOR_LIME);
  14. Executesnprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
  15. Executedraw_text(frame, 258, 3, text, 1, COLOR_LIME);
  16. Decisionprotection_until > last_update && ((protection_until - last_update) / 120U) % 2U == 0U
    True
    1. Executedraw_text_centred(frame, 160, 154, "READY", 1, COLOR_YELLOW);
    False

    Continue after this decision.

  17. Decisionauto_mode
    True
    1. Executedraw_text(frame, 4, 154, "AUTO", 1, COLOR_CYAN);
    False

    Continue after this decision.

  18. Decisionstate == BZ_FINISHED
    True
      1. Executefill_rect(frame, 48, 66, 224, 38, COLOR_BLACK);
      2. Executedraw_text_centred(frame, 160, 70, "TANK DESTROYED", 2, COLOR_RED);
      3. Executedraw_text_centred(frame, 160, 90, "A/K NEW GAME", 1, COLOR_LIGHTGRAY);
    False

    Continue after this decision.

Read original function source
void drawBattleZone(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    clear(frame, COLOR_BLACK);
    char text[40];
    if (state == BZ_READY) {
        draw_text_centred(frame, 160, 15, "BATTLE ZONE", 3, COLOR_LIME);
        draw_text_centred(frame, 83, 55, "PLAY", 2, auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
        draw_text_centred(frame, 237, 55, "AUTO", 2, auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
        draw_text_centred(frame, 160, 92, "D/B/STICK CHANGE  A/K START", 1, COLOR_LIGHTGRAY);
        draw_text_centred(frame, 160, 111, "D/B TURN  STICK UP/DOWN DRIVE", 1, COLOR_LIGHTGRAY);
        draw_text_centred(frame, 160, 130, "HOLD A/K FIRE", 1, COLOR_LIGHTGRAY);
        draw_text_centred(frame, 160, 155, "BOOT BACK", 1, COLOR_DARKGREY);
        return;
    }
    drawBattleZoneLandscape(frame);
    for (uint8_t index = 0U; index < MAX_COVERS; ++index) draw_cover(frame, &covers[index]);
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) if (enemies[index].active) draw_enemy(frame, &enemies[index]);
    for (uint8_t index = 0U; index < MAX_SHOTS; ++index) {
        if (shots[index].remaining == 0U) continue;
        float side, forward;
        if (!project(shots[index].x, shots[index].z, &side, &forward) || fabsf(side) > forward * 1.7f) continue;
        const int x = (int)lroundf(160.0f + side * 108.0f / forward);
        const int y = (int)lroundf(43.0f + 118.0f / forward);
        const float size = shots[index].enemy_shot ? 26.0f / forward : 11.0f / forward;
        const int radius = (int)clamp(size + 1.0f, 1.0f, shots[index].enemy_shot ? 7.0f : 3.0f);
        fill_rect(frame, x - radius, y - radius, radius * 2 + 1, radius * 2 + 1,
                  shots[index].enemy_shot ? COLOR_RED : COLOR_YELLOW);
    }
    draw_radar(frame);
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    draw_text(frame, 4, 3, text, 1, COLOR_LIME);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
    draw_text_centred(frame, 160, 3, text, 1, COLOR_LIME);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
    draw_text(frame, 258, 3, text, 1, COLOR_LIME);
    if (protection_until > last_update && ((protection_until - last_update) / 120U) % 2U == 0U)
        draw_text_centred(frame, 160, 154, "READY", 1, COLOR_YELLOW);
    if (auto_mode) draw_text(frame, 4, 154, "AUTO", 1, COLOR_CYAN);
    if (state == BZ_FINISHED) {
        fill_rect(frame, 48, 66, 224, 38, COLOR_BLACK);
        draw_text_centred(frame, 160, 70, "TANK DESTROYED", 2, COLOR_RED);
        draw_text_centred(frame, 160, 90, "A/K NEW GAME", 1, COLOR_LIGHTGRAY);
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "battle_zone.h"

#include "audio.h"
#include "esp_random.h"
#include "game_graphics.h"
#include "mode_selection.h"
#include "PET2001_font.h"

#include <math.h>
#include <stdio.h>
#include <string.h>

#define PI 3.14159265f
#define STEP_MS 20U
#define MAX_ENEMIES 8U
#define MAX_SHOTS 8U
#define MAX_COVERS 5U
#define PLAYER_SHOT_DELAY_MS 240U
#define PLAYER_PROTECTION_MS 1800U
#define ENEMY_SHOT_SPEED 30.0f
#define PLAYER_SHOT_SPEED 72.0f
#define ARENA_HALF_SIZE 92.0f

typedef enum { BZ_READY, BZ_PLAYING, BZ_FINISHED } battle_zone_state_t;
typedef struct { float x, z; bool active; uint32_t fire_due; } enemy_t;
typedef struct { float x, z, dx, dz; uint32_t remaining; bool enemy_shot; } shot_t;
typedef struct { float x, z, radius; } cover_t;

static battle_zone_state_t state;
static enemy_t enemies[MAX_ENEMIES];
static shot_t shots[MAX_SHOTS];
static cover_t covers[MAX_COVERS];
static float tank_x, tank_z, tank_angle;
static uint32_t score, wave, last_update, accumulator, fire_due, protection_until, radar_due;
static uint8_t lives;
static bool running, auto_mode, controls_ready, engine_running;
static mode_selection_input_t mode_input;

static float random_unit(void)
{
    return (float)(esp_random() & 0xffffU) / 65535.0f;
}

static float clamp(float value, float minimum, float maximum)
{
    return value < minimum ? minimum : value > maximum ? maximum : value;
}

static float wrap_angle(float angle)
{
    while (angle < -PI) angle += 2.0f * PI;
    while (angle >= PI) angle -= 2.0f * PI;
    return angle;
}

static float distance_squared(float ax, float az, float bx, float bz)
{
    const float dx = ax - bx, dz = az - bz;
    return dx * dx + dz * dz;
}

static bool position_blocked(float x, float z, float radius)
{
    for (uint8_t index = 0U; index < MAX_COVERS; ++index) {
        const float combined_radius = covers[index].radius + radius;
        if (distance_squared(x, z, covers[index].x, covers[index].z) < combined_radius * combined_radius)
            return true;
    }
    return false;
}

static void reset_tank(uint32_t now_ms)
{
    tank_x = tank_z = 0.0f;
    tank_angle = -PI / 2.0f;
    protection_until = now_ms + PLAYER_PROTECTION_MS;
    engine_running = false;
    audioSetEngineThrust(false);
    controls_ready = false;
}

static void spawn_wave(void)
{
    memset(enemies, 0, sizeof(enemies));
    memset(shots, 0, sizeof(shots));
    covers[0] = (cover_t){-32.0f, -26.0f, 7.0f};
    covers[1] = (cover_t){31.0f, -31.0f, 8.0f};
    covers[2] = (cover_t){-43.0f, 24.0f, 6.0f};
    covers[3] = (cover_t){37.0f, 31.0f, 7.0f};
    covers[4] = (cover_t){0.0f, 57.0f, 7.0f};
    const uint8_t count = wave < 6U ? (uint8_t)(wave + 1U) : MAX_ENEMIES;
    for (uint8_t index = 0U; index < count; ++index) {
        float x, z;
        do {
            const float angle = random_unit() * 2.0f * PI;
            const float distance = 42.0f + random_unit() * 42.0f;
            x = cosf(angle) * distance;
            z = sinf(angle) * distance;
        } while (position_blocked(x, z, 3.5f));
        enemies[index] = (enemy_t){x, z, true,
                                   900U + (uint32_t)(random_unit() * 1600.0f)};
    }
}

static void begin_game(uint32_t now_ms)
{
    score = 0U;
    wave = 1U;
    lives = 3U;
    fire_due = accumulator = 0U;
    last_update = now_ms;
    reset_tank(now_ms);
    spawn_wave();
    state = BZ_PLAYING;
    audioPlay(AUDIO_SFX_GAME_START);
}

static void fire_shot(bool enemy_shot, float x, float z, float angle)
{
    for (uint8_t index = 0U; index < MAX_SHOTS; ++index) {
        if (shots[index].remaining != 0U) continue;
        const float speed = enemy_shot ? ENEMY_SHOT_SPEED : PLAYER_SHOT_SPEED;
        shots[index] = (shot_t){x, z, cosf(angle) * speed, sinf(angle) * speed,
                                enemy_shot ? 1800U : 1300U, enemy_shot};
        audioPlay(enemy_shot ? AUDIO_SFX_ENEMY_SHOT : AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}

static void fire_player_shot(uint32_t now_ms)
{
    if (fire_due != 0U) return;
    fire_shot(false, tank_x + cosf(tank_angle) * 4.0f, tank_z + sinf(tank_angle) * 4.0f,
              tank_angle);
    fire_due = PLAYER_SHOT_DELAY_MS;
    (void)now_ms;
}

static void destroy_enemy(uint8_t index)
{
    enemies[index].active = false;
    score += 100U;
    audioPlay(AUDIO_SFX_HIT);
}

static void lose_life(uint32_t now_ms)
{
    if (now_ms < protection_until) return;
    if (lives != 0U) --lives;
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    audioSetEngineThrust(false);
    engine_running = false;
    if (lives == 0U) {
        state = BZ_FINISHED;
    } else {
        memset(shots, 0, sizeof(shots));
        reset_tank(now_ms);
    }
}

static void choose_auto_controls(int *turn, float *drive, bool *fire)
{
    const enemy_t *target = NULL;
    float best_distance = 0.0f;
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) {
        if (!enemies[index].active) continue;
        const float distance = distance_squared(tank_x, tank_z, enemies[index].x, enemies[index].z);
        if (target == NULL || distance < best_distance) { target = &enemies[index]; best_distance = distance; }
    }
    if (target == NULL) return;
    const float dx = target->x - tank_x, dz = target->z - tank_z;
    const float cross = cosf(tank_angle) * dz - sinf(tank_angle) * dx;
    const float forward = cosf(tank_angle) * dx + sinf(tank_angle) * dz;
    *turn = cross > 2.0f ? 1 : cross < -2.0f ? -1 : 0;
    *drive = forward < 18.0f ? -0.6f : forward > 50.0f ? 0.7f : 0.0f;
    *fire = forward > 0.0f && fabsf(cross) < 3.5f;
}

static void update_enemies(uint32_t now_ms, float seconds)
{
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) {
        enemy_t *enemy = &enemies[index];
        if (!enemy->active) continue;
        const float dx = tank_x - enemy->x, dz = tank_z - enemy->z;
        const float distance = sqrtf(dx * dx + dz * dz);
        if (distance > 0.1f) {
            const float strafe = (index & 1U) == 0U ? 1.0f : -1.0f;
            const float next_x = enemy->x + (dx / distance * 6.0f - dz / distance * strafe * 2.0f) * seconds;
            const float next_z = enemy->z + (dz / distance * 6.0f + dx / distance * strafe * 2.0f) * seconds;
            if (!position_blocked(next_x, next_z, 3.5f)) {
                enemy->x = next_x;
                enemy->z = next_z;
            }
        }
        if (now_ms >= enemy->fire_due && distance < 66.0f) {
            fire_shot(true, enemy->x, enemy->z, atan2f(dz, dx));
            enemy->fire_due = now_ms + 1200U + (uint32_t)(random_unit() * 1100.0f);
        }
        if (distance < 5.0f) lose_life(now_ms);
    }
}

static void update_shots(uint32_t now_ms, float seconds)
{
    for (uint8_t shot_index = 0U; shot_index < MAX_SHOTS; ++shot_index) {
        shot_t *shot = &shots[shot_index];
        if (shot->remaining == 0U) continue;
        shot->x += shot->dx * seconds;
        shot->z += shot->dz * seconds;
        shot->remaining = shot->remaining > STEP_MS ? shot->remaining - STEP_MS : 0U;
        for (uint8_t cover_index = 0U; cover_index < MAX_COVERS; ++cover_index) {
            const cover_t *cover = &covers[cover_index];
            if (distance_squared(shot->x, shot->z, cover->x, cover->z) >= cover->radius * cover->radius) continue;
            shot->remaining = 0U;
            break;
        }
        if (shot->remaining == 0U) continue;
        if (shot->enemy_shot) {
            if (distance_squared(shot->x, shot->z, tank_x, tank_z) < 14.0f) {
                shot->remaining = 0U;
                lose_life(now_ms);
            }
            continue;
        }
        for (uint8_t enemy_index = 0U; enemy_index < MAX_ENEMIES; ++enemy_index) {
            if (!enemies[enemy_index].active) continue;
            if (distance_squared(shot->x, shot->z, enemies[enemy_index].x, enemies[enemy_index].z) >= 18.0f) continue;
            shot->remaining = 0U;
            destroy_enemy(enemy_index);
            break;
        }
    }
}

static bool enemies_remaining(void)
{
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) if (enemies[index].active) return true;
    return false;
}

static void simulate_step(int turn, float drive, bool fire, uint32_t now_ms)
{
    const float seconds = STEP_MS / 1000.0f;
    if (fire_due > 0U) fire_due = fire_due > STEP_MS ? fire_due - STEP_MS : 0U;
    tank_angle = wrap_angle(tank_angle + (float)turn * 2.25f * seconds);
    const bool moving = drive > 0.05f || drive < -0.05f;
    if (engine_running != moving) {
        engine_running = moving;
        audioSetEngineThrust(moving);
    }
    const float next_x = clamp(tank_x + cosf(tank_angle) * drive * 30.0f * seconds,
                               -ARENA_HALF_SIZE, ARENA_HALF_SIZE);
    const float next_z = clamp(tank_z + sinf(tank_angle) * drive * 30.0f * seconds,
                               -ARENA_HALF_SIZE, ARENA_HALF_SIZE);
    if (!position_blocked(next_x, next_z, 4.0f)) {
        tank_x = next_x;
        tank_z = next_z;
    }
    if (fire) fire_player_shot(now_ms);
    update_enemies(now_ms, seconds);
    update_shots(now_ms, seconds);
    if (!enemies_remaining() && state == BZ_PLAYING) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        ++wave;
        spawn_wave();
    }
    if (now_ms >= radar_due) {
        audioPlay(AUDIO_SFX_MENU_MOVE);
        radar_due = now_ms + 950U;
    }
}

void startBattleZone(uint32_t now_ms)
{
    running = true;
    state = BZ_READY;
    auto_mode = false;
    last_update = now_ms;
    radar_due = now_ms + 700U;
    modeSelectionReset(&mode_input);
}

void stopBattleZone(void)
{
    audioSetEngineThrust(false);
    engine_running = false;
    running = false;
}

void battleZoneAction(bool start, bool stop_auto, uint32_t now_ms)
{
    if (!running) return;
    if (state == BZ_READY && start) begin_game(now_ms);
    else if (state == BZ_FINISHED && start) begin_game(now_ms);
    else if (state == BZ_PLAYING && auto_mode && stop_auto) {
        auto_mode = false;
        modeSelectionReset(&mode_input);
        audioSetEngineThrust(false);
        engine_running = false;
        state = BZ_READY;
    }
}

void updateBattleZone(int x_offset, int y_offset, bool valid, bool left, bool right,
                      bool fire, uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == BZ_READY) {
        if (modeSelectionNext(&mode_input, x_offset, y_offset, valid, left, right, now_ms)) auto_mode = !auto_mode;
        return;
    }
    if (state != BZ_PLAYING) return;
    if (!valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350) controls_ready = true;
    int turn = left || right ? (int)right - (int)left :
        valid && controls_ready ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0;
    float drive = valid && controls_ready ? (y_offset > 600 ? 1.0f : y_offset < -600 ? -0.65f : 0.0f) : 0.0f;
    bool should_fire = fire;
    if (auto_mode) choose_auto_controls(&turn, &drive, &should_fire);
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == BZ_PLAYING) {
        accumulator -= STEP_MS;
        simulate_step(turn, drive, should_fire, now_ms);
    }
}

static bool project(float world_x, float world_z, float *side, float *forward)
{
    const float dx = world_x - tank_x, dz = world_z - tank_z;
    *side = -sinf(tank_angle) * dx + cosf(tank_angle) * dz;
    *forward = cosf(tank_angle) * dx + sinf(tank_angle) * dz;
    return *forward > 3.0f;
}

void drawBattleZoneLandscape(uint16_t *frame)
{
    const uint16_t green = COLOR_LIME;
    const int horizon = BATTLE_ZONE_HORIZON_Y;
    static const int terrain_bands[] = {58, 76, 101, 134, 166};
    for (size_t index = 0U; index < sizeof(terrain_bands) / sizeof(terrain_bands[0]); ++index) {
        const int y = terrain_bands[index];
        draw_line(frame, 0, y, LCD_WIDTH - 1, y, COLOR_DARKGREEN);
    }
    for (int lane = -3; lane <= 3; ++lane)
        draw_line(frame, 160 + lane * 10, horizon, 160 + lane * 50, LCD_HEIGHT - 1, COLOR_DARKGREEN);
    draw_line(frame, 0, horizon, LCD_WIDTH - 1, horizon, green);
    draw_line(frame, 15, horizon, 58, horizon - 12, COLOR_DARKGREEN);
    draw_line(frame, 58, horizon - 12, 104, horizon, COLOR_DARKGREEN);
    draw_line(frame, 212, horizon, 258, horizon - 15, COLOR_DARKGREEN);
    draw_line(frame, 258, horizon - 15, 307, horizon, COLOR_DARKGREEN);
}

static void draw_enemy(uint16_t *frame, const enemy_t *enemy)
{
    float side, forward;
    if (!project(enemy->x, enemy->z, &side, &forward) || fabsf(side) > forward * 1.7f) return;
    const int centre_x = (int)lroundf(160.0f + side * 108.0f / forward);
    const int base_y = (int)lroundf(43.0f + 118.0f / forward);
    const int width = (int)clamp(150.0f / forward, 4.0f, 38.0f);
    const int height = width / 2 + 2;
    draw_rect(frame, centre_x - width / 2, base_y - height, width, height, COLOR_LIME);
    draw_line(frame, centre_x - width / 2 - 2, base_y, centre_x + width / 2 + 2, base_y, COLOR_LIME);
    draw_line(frame, centre_x, base_y - height, centre_x, base_y - height - width / 2, COLOR_LIME);
}

static void draw_cover(uint16_t *frame, const cover_t *cover)
{
    float side, forward;
    if (!project(cover->x, cover->z, &side, &forward) || fabsf(side) > forward * 1.7f) return;
    const int centre_x = (int)lroundf(160.0f + side * 108.0f / forward);
    const int base_y = (int)lroundf(43.0f + 118.0f / forward);
    const int width = (int)clamp(230.0f / forward, 7.0f, 54.0f);
    const int height = width / 2 + 5;
    draw_rect(frame, centre_x - width / 2, base_y - height, width, height, COLOR_LIME);
    draw_line(frame, centre_x - width / 2 - 3, base_y - height,
              centre_x, base_y - height - height / 2, COLOR_LIME);
    draw_line(frame, centre_x, base_y - height - height / 2,
              centre_x + width / 2 + 3, base_y - height, COLOR_LIME);
}

static void draw_radar_point(uint16_t *frame, int centre_x, int centre_y, int radius,
                             float world_x, float world_z, uint16_t colour)
{
    float offset_x = world_x - tank_x;
    float offset_z = world_z - tank_z;
    const float distance = sqrtf(offset_x * offset_x + offset_z * offset_z);
    const float maximum_distance = ARENA_HALF_SIZE;
    if (distance > maximum_distance) {
        offset_x *= maximum_distance / distance;
        offset_z *= maximum_distance / distance;
    }
    const int x = centre_x + (int)(offset_x * (radius - 3) / maximum_distance);
    const int y = centre_y + (int)(offset_z * (radius - 3) / maximum_distance);
    fill_circle(frame, x, y, 2, colour);
}

static void draw_radar(uint16_t *frame)
{
    const int centre_x = 284, centre_y = 135, radius = 25;
    draw_ring(frame, centre_x, centre_y, radius, COLOR_DARKGREEN);
    draw_line(frame, centre_x - radius, centre_y, centre_x + radius, centre_y, COLOR_DARKGREEN);
    draw_line(frame, centre_x, centre_y - radius, centre_x, centre_y + radius, COLOR_DARKGREEN);
    fill_circle(frame, centre_x, centre_y, 2, COLOR_LIME);
    draw_line(frame, centre_x, centre_y,
              centre_x + (int)(cosf(tank_angle) * 6.0f),
              centre_y + (int)(sinf(tank_angle) * 6.0f), COLOR_LIME);
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) {
        if (!enemies[index].active) continue;
        draw_radar_point(frame, centre_x, centre_y, radius,
                         enemies[index].x, enemies[index].z, COLOR_RED);
    }
    for (uint8_t index = 0U; index < MAX_COVERS; ++index) {
        draw_radar_point(frame, centre_x, centre_y, radius,
                         covers[index].x, covers[index].z, COLOR_YELLOW);
    }
}

void drawBattleZone(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    clear(frame, COLOR_BLACK);
    char text[40];
    if (state == BZ_READY) {
        drawPet2001TextCentredTransparent(frame, 160, 15, "BATTLE ZONE", COLOR_LIME, 3);
        drawPet2001TextCentredTransparent(frame, 83, 55, "PLAY",
                                          auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW, 2);
        drawPet2001TextCentredTransparent(frame, 237, 55, "AUTO",
                                          auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY, 2);
        drawPet2001TextCentredTransparent(frame, 160, 92,
                                          "D/B/STICK CHANGE  A/K START", COLOR_LIGHTGRAY, 1);
        drawPet2001TextCentredTransparent(frame, 160, 111,
                                          "D/B TURN  STICK UP/DOWN DRIVE", COLOR_LIGHTGRAY, 1);
        drawPet2001TextCentredTransparent(frame, 160, 130, "HOLD A/K FIRE", COLOR_LIGHTGRAY, 1);
        drawPet2001TextCentredTransparent(frame, 160, 155, "BOOT BACK", COLOR_DARKGREY, 1);
        return;
    }
    drawBattleZoneLandscape(frame);
    for (uint8_t index = 0U; index < MAX_COVERS; ++index) draw_cover(frame, &covers[index]);
    for (uint8_t index = 0U; index < MAX_ENEMIES; ++index) if (enemies[index].active) draw_enemy(frame, &enemies[index]);
    for (uint8_t index = 0U; index < MAX_SHOTS; ++index) {
        if (shots[index].remaining == 0U) continue;
        float side, forward;
        if (!project(shots[index].x, shots[index].z, &side, &forward) || fabsf(side) > forward * 1.7f) continue;
        const int x = (int)lroundf(160.0f + side * 108.0f / forward);
        const int y = (int)lroundf(43.0f + 118.0f / forward);
        const float size = shots[index].enemy_shot ? 26.0f / forward : 11.0f / forward;
        const int radius = (int)clamp(size + 1.0f, 1.0f, shots[index].enemy_shot ? 7.0f : 3.0f);
        fill_rect(frame, x - radius, y - radius, radius * 2 + 1, radius * 2 + 1,
                  shots[index].enemy_shot ? COLOR_RED : COLOR_YELLOW);
    }
    draw_radar(frame);
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    drawPet2001TextTransparent(frame, 4, 3, text, COLOR_LIME, 1);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
    drawPet2001TextCentredTransparent(frame, 160, 3, text, COLOR_LIME, 1);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
    drawPet2001TextTransparent(frame, 258, 3, text, COLOR_LIME, 1);
    if (protection_until > last_update && ((protection_until - last_update) / 120U) % 2U == 0U)
        drawPet2001TextCentredTransparent(frame, 160, 154, "READY", COLOR_YELLOW, 1);
    if (auto_mode) drawPet2001TextTransparent(frame, 4, 154, "AUTO", COLOR_CYAN, 1);
    if (state == BZ_FINISHED) {
        fill_rect(frame, 48, 66, 224, 38, COLOR_BLACK);
        drawPet2001TextCentredTransparent(frame, 160, 70, "TANK DESTROYED", COLOR_RED, 2);
        drawPet2001TextCentredTransparent(frame, 160, 90, "A/K NEW GAME", COLOR_LIGHTGRAY, 1);
    }
}