Missile Command

Source: src/missile_command.c · 792 lines · 31 functions.

Screen captures

Views of this part running on the Mini Arcade.

Missile Command game screen on the Mini Arcade
Missile Command showing the cities and incoming attack.

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

clampValue() · source line 166
static float clampValue(float value, float minimum, float maximum)
  1. Exit this functionreturn value < minimum ? minimum : value > maximum ? maximum : value;
Read original function source
static float clampValue(float value, float minimum, float maximum)
{
    return value < minimum ? minimum : value > maximum ? maximum : value;
}
livingCities() · source line 171
static unsigned livingCities(void)
  1. Executeunsigned count = 0;
  2. FOR loopunsigned i = 0; i < CITY_COUNT; ++i

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

    1. Decisioncities[i]
      True
      1. Execute++count;
      False

      Continue after this decision.

  3. Exit this functionreturn count;
Read original function source
static unsigned livingCities(void)
{
    unsigned count = 0;
    for (unsigned i = 0; i < CITY_COUNT; ++i) if (cities[i]) ++count;
    return count;
}
difficulty() · source line 178
static unsigned difficulty(void)
  1. Exit this functionreturn wave <= 10U ? (unsigned)(wave - 1U) : 9U;
Read original function source
static unsigned difficulty(void)
{
    return wave <= 10U ? (unsigned)(wave - 1U) : 9U;
}
addPoints() · source line 183
static void addPoints(uint32_t points)
  1. Executescore = score > UINT32_MAX - points ? UINT32_MAX : score + points;
Read original function source
static void addPoints(uint32_t points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
}
updateWaveTally() · source line 188
static void updateWaveTally(uint32_t elapsed)

Calls into this documentation:

  1. ExecutewaveTallyElapsed = elapsed;
  2. Executeconst uint32_t missileElapsed = elapsed < WAVE_TALLY_MISSILES_MS ? elapsed : WAVE_TALLY_MISSILES_MS;
  3. Executeconst uint32_t cityElapsed = elapsed <= WAVE_TALLY_MISSILES_MS ? 0U : elapsed - WAVE_TALLY_MISSILES_MS < WAVE_TALLY_CITIES_MS ? elapsed - WAVE_TALLY_MISSILES_MS : WAVE_TALLY_CITIES_MS;
  4. Executeconst uint32_t missiles = waveMissileBonus * missileElapsed / WAVE_TALLY_MISSILES_MS;
  5. Executeconst uint32_t cities = waveCityBonus * cityElapsed / WAVE_TALLY_CITIES_MS;
  6. Decisionmissiles > waveMissileAwarded
    True
    1. ExecuteaddPoints(missiles - waveMissileAwarded);
    False

    Continue after this decision.

  7. Decisioncities > waveCityAwarded
    True
    1. ExecuteaddPoints(cities - waveCityAwarded);
    False

    Continue after this decision.

  8. ExecutewaveMissileAwarded = missiles;
  9. ExecutewaveCityAwarded = cities;
Read original function source
static void updateWaveTally(uint32_t elapsed)
{
    waveTallyElapsed = elapsed;
    const uint32_t missileElapsed = elapsed < WAVE_TALLY_MISSILES_MS ? elapsed : WAVE_TALLY_MISSILES_MS;
    const uint32_t cityElapsed = elapsed <= WAVE_TALLY_MISSILES_MS ? 0U
        : elapsed - WAVE_TALLY_MISSILES_MS < WAVE_TALLY_CITIES_MS
            ? elapsed - WAVE_TALLY_MISSILES_MS : WAVE_TALLY_CITIES_MS;
    const uint32_t missiles = waveMissileBonus * missileElapsed / WAVE_TALLY_MISSILES_MS;
    const uint32_t cities = waveCityBonus * cityElapsed / WAVE_TALLY_CITIES_MS;

    if (missiles > waveMissileAwarded) addPoints(missiles - waveMissileAwarded);
    if (cities > waveCityAwarded) addPoints(cities - waveCityAwarded);
    waveMissileAwarded = missiles;
    waveCityAwarded = cities;
}
segmentHitsCircle() · source line 205
static bool segmentHitsCircle(float ax, float ay, float bx, float by,
                               float cx, float cy, float radius)

Calls into this documentation:

  1. Executeconst float dx = bx - ax, dy = by - ay;
  2. Executeconst float lengthSquared = dx * dx + dy * dy;
  3. Executeconst float fraction = lengthSquared > 0.0f ? clampValue(((cx - ax) * dx + (cy - ay) * dy) / lengthSquared, 0.0f, 1.0f) : 0.0f;
  4. Executeconst float distanceX = ax + fraction * dx - cx;
  5. Executeconst float distanceY = ay + fraction * dy - cy;
  6. Exit this functionreturn distanceX * distanceX + distanceY * distanceY <= radius * radius;
Read original function source
static bool segmentHitsCircle(float ax, float ay, float bx, float by,
                               float cx, float cy, float radius)
{
    const float dx = bx - ax, dy = by - ay;
    const float lengthSquared = dx * dx + dy * dy;
    const float fraction = lengthSquared > 0.0f
        ? clampValue(((cx - ax) * dx + (cy - ay) * dy) / lengthSquared, 0.0f, 1.0f)
        : 0.0f;
    const float distanceX = ax + fraction * dx - cx;
    const float distanceY = ay + fraction * dy - cy;
    return distanceX * distanceX + distanceY * distanceY <= radius * radius;
}
clipAxis() · source line 218
static bool clipAxis(float origin, float delta, float minimum, float maximum,
                      float *enter, float *leave)
  1. Decisiondelta == 0.0f
    True
    1. Exit this functionreturn origin >= minimum && origin <= maximum;
    False

    Continue after this decision.

  2. Executefloat first = (minimum - origin) / delta;
  3. Executefloat last = (maximum - origin) / delta;
  4. Decisionfirst > last
    True
      1. Executeconst float swap = first;
      2. Executefirst = last;
      3. Executelast = swap;
    False

    Continue after this decision.

  5. Decisionfirst > *enter
    True
    1. Execute*enter = first;
    False

    Continue after this decision.

  6. Decisionlast < *leave
    True
    1. Execute*leave = last;
    False

    Continue after this decision.

  7. Exit this functionreturn *enter <= *leave;
Read original function source
static bool clipAxis(float origin, float delta, float minimum, float maximum,
                      float *enter, float *leave)
{
    if (delta == 0.0f) return origin >= minimum && origin <= maximum;
    float first = (minimum - origin) / delta;
    float last = (maximum - origin) / delta;
    if (first > last) {
        const float swap = first;
        first = last;
        last = swap;
    }
    if (first > *enter) *enter = first;
    if (last < *leave) *leave = last;
    return *enter <= *leave;
}
segmentHitsTarget() · source line 234
static bool segmentHitsTarget(const Missile *missile, float centreX, float width)

Calls into this documentation:

  1. Executefloat enter = 0.0f, leave = 1.0f;
  2. Exit this functionreturn clipAxis(missile->previousX, missile->x - missile->previousX, centreX - width * 0.5f, centreX + width * 0.5f, &enter, &leave) && clipAxis(missile->previousY, missile->y - missile->previousY, TARGET_TOP, GROUND_Y, &enter, &leave);
Read original function source
static bool segmentHitsTarget(const Missile *missile, float centreX, float width)
{
    float enter = 0.0f, leave = 1.0f;
    return clipAxis(missile->previousX, missile->x - missile->previousX,
                    centreX - width * 0.5f, centreX + width * 0.5f, &enter, &leave) &&
           clipAxis(missile->previousY, missile->y - missile->previousY,
                    TARGET_TOP, GROUND_Y, &enter, &leave);
}
startExplosion() · source line 243
static void startExplosion(float x, float y, bool defensive)
  1. FOR loopunsigned i = 0; i < MAX_EXPLOSIONS; ++i

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

      1. Decisionexplosions[i].active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeexplosions[i] = (Explosion){ .x = x, .y = y, .radius = 2.0f, .age = 0, .active = true, .defensive = defensive, };
      3. Exit this functionreturn;
  2. ExecuteESP_LOGE("missile_command", "Explosion capacity exceeded");
Read original function source
static void startExplosion(float x, float y, bool defensive)
{
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        if (explosions[i].active) continue;
        explosions[i] = (Explosion){
            .x = x, .y = y, .radius = 2.0f, .age = 0,
            .active = true, .defensive = defensive,
        };
        return;
    }
    ESP_LOGE("missile_command", "Explosion capacity exceeded");
}
updateExplosions() · source line 256
static void updateExplosions(void)
  1. FOR loopunsigned i = 0; i < MAX_EXPLOSIONS; ++i

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

      1. ExecuteExplosion *explosion = &explosions[i];
      2. Decision!explosion->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeexplosion->age += STEP_MS;
      4. Decisionexplosion->age >= BLAST_LIFE_MS
        True
          1. Executeexplosion->active = false;
        False
        1. Decisionexplosion->age < BLAST_GROW_MS
          True
            1. Executeexplosion->radius = 2.0f + (BLAST_RADIUS - 2.0f) * explosion->age / BLAST_GROW_MS;
          False
          1. Decisionexplosion->age < BLAST_GROW_MS + BLAST_HOLD_MS
            True
              1. Executeexplosion->radius = BLAST_RADIUS;
            False
              1. Executeexplosion->radius = BLAST_RADIUS * (BLAST_LIFE_MS - explosion->age) / BLAST_SHRINK_MS;
Read original function source
static void updateExplosions(void)
{
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        Explosion *explosion = &explosions[i];
        if (!explosion->active) continue;
        explosion->age += STEP_MS;
        if (explosion->age >= BLAST_LIFE_MS) {
            explosion->active = false;
        } else if (explosion->age < BLAST_GROW_MS) {
            explosion->radius = 2.0f + (BLAST_RADIUS - 2.0f) * explosion->age / BLAST_GROW_MS;
        } else if (explosion->age < BLAST_GROW_MS + BLAST_HOLD_MS) {
            explosion->radius = BLAST_RADIUS;
        } else {
            explosion->radius = BLAST_RADIUS * (BLAST_LIFE_MS - explosion->age) / BLAST_SHRINK_MS;
        }
    }
}
makeMissile() · source line 274
static Missile makeMissile(float x, float y, float targetX, float targetY,
                            float speed, unsigned target)
  1. Executeconst float dx = targetX - x, dy = targetY - y;
  2. Executeconst float distance = sqrtf(dx * dx + dy * dy);
  3. Exit this functionreturn (Missile){ .startX = x, .startY = y, .x = x, .y = y, .previousX = x, .previousY = y, .targetX = targetX, .targetY = targetY, .vx = distance > 0.0f ? dx * speed / distance : 0.0f, .vy = distance > 0.0f ? dy * speed / distance : 0.0f, .target = target, .active = true, .arrived = false, };
Read original function source
static Missile makeMissile(float x, float y, float targetX, float targetY,
                            float speed, unsigned target)
{
    const float dx = targetX - x, dy = targetY - y;
    const float distance = sqrtf(dx * dx + dy * dy);
    return (Missile){
        .startX = x, .startY = y, .x = x, .y = y, .previousX = x, .previousY = y,
        .targetX = targetX, .targetY = targetY,
        .vx = distance > 0.0f ? dx * speed / distance : 0.0f,
        .vy = distance > 0.0f ? dy * speed / distance : 0.0f,
        .target = target, .active = true, .arrived = false,
    };
}
moveMissile() · source line 288
static void moveMissile(Missile *missile)
  1. Executemissile->previousX = missile->x;
  2. Executemissile->previousY = missile->y;
  3. Executeconst float dx = missile->targetX - missile->x;
  4. Executeconst float dy = missile->targetY - missile->y;
  5. Executeconst float stepX = missile->vx * STEP_MS / 1000.0f;
  6. Executeconst float stepY = missile->vy * STEP_MS / 1000.0f;
  7. Decisiondx * dx + dy * dy <= stepX * stepX + stepY * stepY
    True
      1. Executemissile->x = missile->targetX;
      2. Executemissile->y = missile->targetY;
      3. Executemissile->arrived = true;
    False
      1. Executemissile->x += stepX;
      2. Executemissile->y += stepY;
Read original function source
static void moveMissile(Missile *missile)
{
    missile->previousX = missile->x;
    missile->previousY = missile->y;
    const float dx = missile->targetX - missile->x;
    const float dy = missile->targetY - missile->y;
    const float stepX = missile->vx * STEP_MS / 1000.0f;
    const float stepY = missile->vy * STEP_MS / 1000.0f;
    if (dx * dx + dy * dy <= stepX * stepX + stepY * stepY) {
        missile->x = missile->targetX;
        missile->y = missile->targetY;
        missile->arrived = true;
    } else {
        missile->x += stepX;
        missile->y += stepY;
    }
}
spawnEnemy() · source line 306
static void spawnEnemy(void)

Calls into this documentation:

  1. Executeunsigned targets[CITY_COUNT + BASE_COUNT], count = 0;
  2. FOR loopunsigned i = 0; i < CITY_COUNT; ++i

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

    1. Decisioncities[i]
      True
      1. Executetargets[count++] = i;
      False

      Continue after this decision.

  3. FOR loopunsigned i = 0; i < BASE_COUNT; ++i

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

    1. Decisionbases[i].alive
      True
      1. Executetargets[count++] = CITY_COUNT + i;
      False

      Continue after this decision.

  4. Decisioncount == 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  5. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

      1. Decisionenemies[i].active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst unsigned target = targets[esp_random() % count];
      3. Executeconst float targetX = target < CITY_COUNT ? cityX[target] : baseX[target - CITY_COUNT];
      4. Executeenemies[i] = makeMissile(5.0f + esp_random() % 310U, 14.0f, targetX, TARGET_TOP + TARGET_HEIGHT * 0.5f, 16.0f + difficulty() * 2.5f, target);
      5. Execute--enemiesToSpawn;
      6. ExecutespawnDelay = 900U - difficulty() * 55U + esp_random() % 301U;
      7. Exit this functionreturn;
Read original function source
static void spawnEnemy(void)
{
    unsigned targets[CITY_COUNT + BASE_COUNT], count = 0;
    for (unsigned i = 0; i < CITY_COUNT; ++i) if (cities[i]) targets[count++] = i;
    for (unsigned i = 0; i < BASE_COUNT; ++i) if (bases[i].alive) targets[count++] = CITY_COUNT + i;
    if (count == 0) return;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (enemies[i].active) continue;
        const unsigned target = targets[esp_random() % count];
        const float targetX = target < CITY_COUNT ? cityX[target] : baseX[target - CITY_COUNT];
        enemies[i] = makeMissile(5.0f + esp_random() % 310U, 14.0f,
                                 targetX, TARGET_TOP + TARGET_HEIGHT * 0.5f,
                                 16.0f + difficulty() * 2.5f, target);
        --enemiesToSpawn;
        spawnDelay = 900U - difficulty() * 55U + esp_random() % 301U;
        return;
    }
}
fireBase() · source line 325
static void fireBase(unsigned base)

Calls into this documentation:

  1. Decision!bases[base].alive || bases[base].ammo == 0 || bases[base].fireDelay != 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. FOR loopunsigned i = 0; i < MAX_INTERCEPTORS; ++i

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

      1. Decisioninterceptors[i].active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeinterceptors[i] = makeMissile(baseX[base], TARGET_TOP, aimX, aimY, base == 1U ? 180.0f : 140.0f, base);
      3. Execute--bases[base].ammo;
      4. Executebases[base].fireDelay = FIRE_DELAY_MS;
      5. ExecuteaudioPlay(AUDIO_SFX_PLAYER_SHOT);
      6. Exit this functionreturn;
Read original function source
static void fireBase(unsigned base)
{
    if (!bases[base].alive || bases[base].ammo == 0 || bases[base].fireDelay != 0) return;
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        if (interceptors[i].active) continue;
        interceptors[i] = makeMissile(baseX[base], TARGET_TOP, aimX, aimY,
                                      base == 1U ? 180.0f : 140.0f, base);
        --bases[base].ammo;
        bases[base].fireDelay = FIRE_DELAY_MS;
        audioPlay(AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}
defensiveBlastCovers() · source line 339
static bool defensiveBlastCovers(const Missile *enemy)
  1. FOR loopunsigned i = 0; i < MAX_EXPLOSIONS; ++i

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

      1. Executeconst Explosion *explosion = &explosions[i];
      2. Decision!explosion->active || !explosion->defensive
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst float dx = enemy->x - explosion->x;
      4. Executeconst float dy = enemy->y - explosion->y;
      5. Executeconst float radius = explosion->radius + 8.0f;
      6. Decisiondx * dx + dy * dy <= radius * radius
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool defensiveBlastCovers(const Missile *enemy)
{
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        const Explosion *explosion = &explosions[i];
        if (!explosion->active || !explosion->defensive) continue;
        const float dx = enemy->x - explosion->x;
        const float dy = enemy->y - explosion->y;
        const float radius = explosion->radius + 8.0f;
        if (dx * dx + dy * dy <= radius * radius) return true;
    }
    return false;
}
interceptorCovers() · source line 352
static bool interceptorCovers(const Missile *enemy)
  1. Executeconst float coverage = BLAST_RADIUS + 8.0f;
  2. FOR loopunsigned i = 0; i < MAX_INTERCEPTORS; ++i

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

      1. Executeconst Missile *interceptor = &interceptors[i];
      2. Decision!interceptor->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst float dx = enemy->x - interceptor->targetX;
      4. Executeconst float dy = enemy->y - interceptor->targetY;
      5. Decisiondx * dx + dy * dy <= coverage * coverage
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  3. Exit this functionreturn false;
Read original function source
static bool interceptorCovers(const Missile *enemy)
{
    const float coverage = BLAST_RADIUS + 8.0f;
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        const Missile *interceptor = &interceptors[i];
        if (!interceptor->active) continue;
        const float dx = enemy->x - interceptor->targetX;
        const float dy = enemy->y - interceptor->targetY;
        if (dx * dx + dy * dy <= coverage * coverage) return true;
    }
    return false;
}
closestAutoBase() · source line 365
static int closestAutoBase(float target_x)
  1. Executeint best_base = -1;
  2. Executefloat best_distance = 0.0f;
  3. FOR loopunsigned base = 0; base < BASE_COUNT; ++base

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

      1. Decision!bases[base].alive || bases[base].ammo == 0U || bases[base].fireDelay != 0U
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst float distance = fabsf(target_x - (float)baseX[base]);
      3. Decisionbest_base < 0 || distance < best_distance
        True
          1. Executebest_base = (int)base;
          2. Executebest_distance = distance;
        False

        Continue after this decision.

  4. Exit this functionreturn best_base;
Read original function source
static int closestAutoBase(float target_x)
{
    int best_base = -1;
    float best_distance = 0.0f;
    for (unsigned base = 0; base < BASE_COUNT; ++base) {
        if (!bases[base].alive || bases[base].ammo == 0U || bases[base].fireDelay != 0U) continue;
        const float distance = fabsf(target_x - (float)baseX[base]);
        if (best_base < 0 || distance < best_distance) {
            best_base = (int)base;
            best_distance = distance;
        }
    }
    return best_base;
}
autoFireAtThreat() · source line 380
static void autoFireAtThreat(void)

Calls into this documentation:

  1. Executeconst Missile *threat = NULL;
  2. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

      1. Executeconst Missile *enemy = &enemies[i];
      2. Decision!enemy->active || defensiveBlastCovers(enemy) || interceptorCovers(enemy)
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Decisionthreat == NULL || enemy->y > threat->y
        True
        1. Executethreat = enemy;
        False

        Continue after this decision.

  3. Decisionthreat == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  4. Executeconst int base = closestAutoBase(threat->x);
  5. Decisionbase < 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  6. Executeconst float interceptor_speed = base == 1 ? 180.0f : 140.0f;
  7. Executeconst float distance_x = threat->x - (float)baseX[base];
  8. Executeconst float distance_y = threat->y - (float)TARGET_TOP;
  9. Executeconst float travel_ms = sqrtf(distance_x * distance_x + distance_y * distance_y) * 1000.0f / interceptor_speed;
  10. ExecuteaimX = clampValue(threat->x + threat->vx * travel_ms / 1000.0f, 5.0f, 314.0f);
  11. ExecuteaimY = clampValue(threat->y + threat->vy * travel_ms / 1000.0f, 20.0f, 137.0f);
  12. ExecutefireBase((unsigned)base);
Read original function source
static void autoFireAtThreat(void)
{
    const Missile *threat = NULL;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        const Missile *enemy = &enemies[i];
        if (!enemy->active || defensiveBlastCovers(enemy) || interceptorCovers(enemy)) continue;
        if (threat == NULL || enemy->y > threat->y) threat = enemy;
    }
    if (threat == NULL) return;

    const int base = closestAutoBase(threat->x);
    if (base < 0) return;
    const float interceptor_speed = base == 1 ? 180.0f : 140.0f;
    const float distance_x = threat->x - (float)baseX[base];
    const float distance_y = threat->y - (float)TARGET_TOP;
    const float travel_ms = sqrtf(distance_x * distance_x + distance_y * distance_y) * 1000.0f / interceptor_speed;
    aimX = clampValue(threat->x + threat->vx * travel_ms / 1000.0f, 5.0f, 314.0f);
    aimY = clampValue(threat->y + threat->vy * travel_ms / 1000.0f, 20.0f, 137.0f);
    fireBase((unsigned)base);
}
beginWave() · source line 401
static void beginWave(void)

Calls into this documentation:

  1. Executememset(enemies, 0, sizeof(enemies));
  2. Executememset(interceptors, 0, sizeof(interceptors));
  3. Executememset(explosions, 0, sizeof(explosions));
  4. FOR loopunsigned i = 0; i < BASE_COUNT; ++i

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

    1. Executebases[i] = (Base){AMMO_PER_BASE, 0, true};
  5. ExecuteenemiesToSpawn = 12U + difficulty() * 4U;
  6. ExecutespawnDelay = 1000U;
  7. Executeaccumulator = 0;
  8. ExecutefireReady = false;
  9. ExecutegameState = STATE_PLAYING;
  10. ExecuteESP_LOGI("missile_command", "WAVE %u: %u cities", (unsigned)wave, livingCities());
Read original function source
static void beginWave(void)
{
    memset(enemies, 0, sizeof(enemies));
    memset(interceptors, 0, sizeof(interceptors));
    memset(explosions, 0, sizeof(explosions));
    for (unsigned i = 0; i < BASE_COUNT; ++i) bases[i] = (Base){AMMO_PER_BASE, 0, true};
    enemiesToSpawn = 12U + difficulty() * 4U;
    spawnDelay = 1000U;
    accumulator = 0;
    fireReady = false;
    gameState = STATE_PLAYING;
    ESP_LOGI("missile_command", "WAVE %u: %u cities", (unsigned)wave, livingCities());
}
beginGame() · source line 415
static void beginGame(uint32_t now_ms)

Calls into this documentation:

  1. Executescore = waveBonus = waveMissileBonus = waveCityBonus = 0;
  2. ExecutewaveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0;
  3. ExecutewaveMissilesRemaining = waveCitiesRemaining = 0;
  4. Executewave = 1;
  5. FOR loopunsigned i = 0; i < CITY_COUNT; ++i

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

    1. Executecities[i] = true;
  6. ExecuteaimX = 160.0f;
  7. ExecuteaimY = 80.0f;
  8. ExecutecontrolsReady = false;
  9. ExecutelastUpdate = now_ms;
  10. ExecutebeginWave();
  11. ExecuteaudioPlay(AUDIO_SFX_GAME_START);
Read original function source
static void beginGame(uint32_t now_ms)
{
    score = waveBonus = waveMissileBonus = waveCityBonus = 0;
    waveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0;
    waveMissilesRemaining = waveCitiesRemaining = 0;
    wave = 1;
    for (unsigned i = 0; i < CITY_COUNT; ++i) cities[i] = true;
    aimX = 160.0f;
    aimY = 80.0f;
    controlsReady = false;
    lastUpdate = now_ms;
    beginWave();
    audioPlay(AUDIO_SFX_GAME_START);
}
finishGame() · source line 430
static void finishGame(void)

Calls into this documentation:

  1. ExecutegameState = STATE_FINISHED;
  2. ExecutestateStarted = lastUpdate;
  3. ExecuteaudioPlay(AUDIO_SFX_GAME_OVER);
  4. ExecuteESP_LOGI("missile_command", "GAME OVER: SCORE %u, WAVE %u", (unsigned)score, (unsigned)wave);
  5. Executeconst uint32_t gold = score / 1000U;
  6. Decisiongold != 0 && !addUserGoldWon(gold)
    True
      1. ExecuteESP_LOGE("missile_command", "Failed to save %u Gold", (unsigned)gold);
    False

    Continue after this decision.

Read original function source
static void finishGame(void)
{
    gameState = STATE_FINISHED;
    stateStarted = lastUpdate;
    audioPlay(AUDIO_SFX_GAME_OVER);
    ESP_LOGI("missile_command", "GAME OVER: SCORE %u, WAVE %u", (unsigned)score, (unsigned)wave);
    const uint32_t gold = score / 1000U;
    if (gold != 0 && !addUserGoldWon(gold)) {
        ESP_LOGE("missile_command", "Failed to save %u Gold", (unsigned)gold);
    }
}
waveIsQuiet() · source line 442
static bool waveIsQuiet(void)
  1. DecisionenemiesToSpawn != 0
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

    1. Decisionenemies[i].active
      True
      1. Exit this functionreturn false;
      False

      Continue after this decision.

  3. FOR loopunsigned i = 0; i < MAX_INTERCEPTORS; ++i

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

    1. Decisioninterceptors[i].active
      True
      1. Exit this functionreturn false;
      False

      Continue after this decision.

  4. FOR loopunsigned i = 0; i < MAX_EXPLOSIONS; ++i

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

    1. Decisionexplosions[i].active
      True
      1. Exit this functionreturn false;
      False

      Continue after this decision.

  5. Exit this functionreturn true;
Read original function source
static bool waveIsQuiet(void)
{
    if (enemiesToSpawn != 0) return false;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) return false;
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) if (interceptors[i].active) return false;
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) if (explosions[i].active) return false;
    return true;
}
resolveCollisions() · source line 451
static void resolveCollisions(void)

Calls into this documentation:

  1. Executebool chainReaction;
  2. DO / WHILE loopchainReaction

    Run body, then test condition; repeat while true.

      1. ExecutechainReaction = false;
      2. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

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

            Continue after this decision.

          3. FOR loopunsigned j = 0; j < MAX_EXPLOSIONS; ++j

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

              1. Executeconst Explosion *explosion = &explosions[j];
              2. Decision!explosion->active || !explosion->defensive
                True
                1. Next loop iterationcontinue;
                False

                Continue after this decision.

              3. Decision!segmentHitsCircle(enemy->previousX, enemy->previousY, enemy->x, enemy->y, explosion->x, explosion->y, explosion->radius)
                True
                1. Next loop iterationcontinue;
                False

                Continue after this decision.

              4. Executeenemy->active = false;
              5. ExecuteaddPoints(25U);
              6. ExecutestartExplosion(enemy->x, enemy->y, true);
              7. ExecuteaudioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
              8. ExecutechainReaction = true;
              9. Exit enclosing loop or switchbreak;
  3. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

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

        Continue after this decision.

      3. Executeconst bool city = enemy->target < CITY_COUNT;
      4. Executeconst unsigned index = city ? enemy->target : enemy->target - CITY_COUNT;
      5. Decision!enemy->arrived && !segmentHitsTarget(enemy, city ? cityX[index] : baseX[index], city ? CITY_WIDTH : BASE_WIDTH)
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      6. Executeenemy->active = false;
      7. Decisioncity
        True
        1. Executecities[index] = false;
        False
          1. Executebases[index].alive = false;
          2. Executebases[index].ammo = 0;
      8. ExecutestartExplosion(enemy->x, enemy->y, false);
      9. ExecuteaudioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
Read original function source
static void resolveCollisions(void)
{
    // Resolve new chain reactions in this same step, regardless of array order.
    bool chainReaction;
    do {
        chainReaction = false;
        for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
            Missile *enemy = &enemies[i];
            if (!enemy->active) continue;
            for (unsigned j = 0; j < MAX_EXPLOSIONS; ++j) {
                const Explosion *explosion = &explosions[j];
                if (!explosion->active || !explosion->defensive) continue;
                if (!segmentHitsCircle(enemy->previousX, enemy->previousY, enemy->x, enemy->y,
                                        explosion->x, explosion->y, explosion->radius)) continue;
                enemy->active = false;
                addPoints(25U);
                startExplosion(enemy->x, enemy->y, true);
                audioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
                chainReaction = true;
                break;
            }
        }
    } while (chainReaction);

    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        Missile *enemy = &enemies[i];
        if (!enemy->active) continue;
        const bool city = enemy->target < CITY_COUNT;
        const unsigned index = city ? enemy->target : enemy->target - CITY_COUNT;
        if (!enemy->arrived && !segmentHitsTarget(enemy, city ? cityX[index] : baseX[index],
                                                  city ? CITY_WIDTH : BASE_WIDTH)) continue;
        enemy->active = false;
        if (city) cities[index] = false;
        else {
            bases[index].alive = false;
            bases[index].ammo = 0;
        }
        // Ground impacts are visual effects; they do not earn defensive chain hits.
        startExplosion(enemy->x, enemy->y, false);
        audioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
    }
}
simulateStep() · source line 494
static void simulateStep(float aimVelocityX, float aimVelocityY, const bool fire[BASE_COUNT])

Calls into this documentation:

  1. ExecuteaimX = clampValue(aimX + aimVelocityX * STEP_MS / 1000.0f, 5.0f, 314.0f);
  2. ExecuteaimY = clampValue(aimY + aimVelocityY * STEP_MS / 1000.0f, 20.0f, 137.0f);
  3. ExecuteupdateExplosions();
  4. FOR loopunsigned i = 0; i < BASE_COUNT; ++i

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

      1. Executebases[i].fireDelay = bases[i].fireDelay > STEP_MS ? bases[i].fireDelay - STEP_MS : 0;
      2. DecisionfireReady && fire[i]
        True
        1. ExecutefireBase(i);
        False

        Continue after this decision.

  5. ExecutespawnDelay = spawnDelay > STEP_MS ? spawnDelay - STEP_MS : 0;
  6. DecisionenemiesToSpawn != 0 && spawnDelay == 0
    True
    1. ExecutespawnEnemy();
    False

    Continue after this decision.

  7. FOR loopunsigned i = 0; i < MAX_INTERCEPTORS; ++i

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

      1. ExecuteMissile *missile = &interceptors[i];
      2. Decision!missile->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. ExecutemoveMissile(missile);
      4. Decisionmissile->arrived
        True
          1. Executemissile->active = false;
          2. ExecutestartExplosion(missile->x, missile->y, true);
          3. ExecuteaudioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
        False

        Continue after this decision.

  8. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

    1. Decisionenemies[i].active
      True
      1. ExecutemoveMissile(&enemies[i]);
      False

      Continue after this decision.

  9. ExecuteresolveCollisions();
  10. DecisionlivingCities() == 0
    True
      1. ExecutefinishGame();
    False
    1. DecisionwaveIsQuiet()
      True
        1. ExecutewaveMissilesRemaining = 0;
        2. FOR loopunsigned i = 0; i < BASE_COUNT; ++i

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

          1. ExecutewaveMissilesRemaining += bases[i].ammo;
        3. ExecutewaveCitiesRemaining = livingCities();
        4. ExecutewaveMissileBonus = waveMissilesRemaining * 5U;
        5. ExecutewaveCityBonus = waveCitiesRemaining * 100U;
        6. ExecutewaveBonus = waveMissileBonus + waveCityBonus;
        7. ExecutewaveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0;
        8. ExecutegameState = STATE_WAVE_CLEAR;
        9. ExecutestateStarted = lastUpdate;
        10. ExecuteaudioPlay(AUDIO_SFX_WAVE_CLEAR);
      False

      Continue after this decision.

Read original function source
static void simulateStep(float aimVelocityX, float aimVelocityY, const bool fire[BASE_COUNT])
{
    aimX = clampValue(aimX + aimVelocityX * STEP_MS / 1000.0f, 5.0f, 314.0f);
    aimY = clampValue(aimY + aimVelocityY * STEP_MS / 1000.0f, 20.0f, 137.0f);
    updateExplosions();
    for (unsigned i = 0; i < BASE_COUNT; ++i) {
        bases[i].fireDelay = bases[i].fireDelay > STEP_MS ? bases[i].fireDelay - STEP_MS : 0;
        if (fireReady && fire[i]) fireBase(i);
    }
    spawnDelay = spawnDelay > STEP_MS ? spawnDelay - STEP_MS : 0;
    if (enemiesToSpawn != 0 && spawnDelay == 0) spawnEnemy();

    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        Missile *missile = &interceptors[i];
        if (!missile->active) continue;
        moveMissile(missile);
        if (missile->arrived) {
            missile->active = false;
            startExplosion(missile->x, missile->y, true);
            audioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
        }
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) moveMissile(&enemies[i]);
    resolveCollisions();

    if (livingCities() == 0) {
        finishGame();
    } else if (waveIsQuiet()) {
        waveMissilesRemaining = 0;
        for (unsigned i = 0; i < BASE_COUNT; ++i) waveMissilesRemaining += bases[i].ammo;
        waveCitiesRemaining = livingCities();
        waveMissileBonus = waveMissilesRemaining * 5U;
        waveCityBonus = waveCitiesRemaining * 100U;
        waveBonus = waveMissileBonus + waveCityBonus;
        waveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0;
        gameState = STATE_WAVE_CLEAR;
        stateStarted = lastUpdate;
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
    }
}
axisVelocity() · source line 535
static float axisVelocity(int offset)

Calls into this documentation:

  1. Decisionoffset >= -350 && offset <= 350
    True
    1. Exit this functionreturn 0.0f;
    False

    Continue after this decision.

  2. Executeconst float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f;
  3. Exit this functionreturn clampValue(adjusted / 1500.0f, -1.0f, 1.0f) * 140.0f;
Read original function source
static float axisVelocity(int offset)
{
    if (offset >= -350 && offset <= 350) return 0.0f;
    const float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f;
    return clampValue(adjusted / 1500.0f, -1.0f, 1.0f) * 140.0f;
}
startMissileCommand() · source line 542
void startMissileCommand(uint32_t now_ms)

Calls into this documentation:

  1. Executestarfield_initialize(&stars, MISSILE_STAR_COUNT, 0, 0, LCD_WIDTH, GROUND_Y, STARFIELD_GREYSCALE);
  2. Executerunning = true;
  3. Executeauto_mode = false;
  4. Executeai_speed = AI_SPEED_DEFAULT_INDEX;
  5. ExecutemodeSelectionReset(&mode_input);
  6. ExecutelastUpdate = now_ms;
  7. ExecutegameState = STATE_MODE;
  8. ExecuteESP_LOGI("missile_command", "START");
Read original function source
void startMissileCommand(uint32_t now_ms)
{
    starfield_initialize(&stars, MISSILE_STAR_COUNT, 0, 0, LCD_WIDTH, GROUND_Y,
                         STARFIELD_GREYSCALE);
    running = true;
    auto_mode = false;
    ai_speed = AI_SPEED_DEFAULT_INDEX;
    modeSelectionReset(&mode_input);
    lastUpdate = now_ms;
    gameState = STATE_MODE;
    ESP_LOGI("missile_command", "START");
}
stopMissileCommand() · source line 555
void stopMissileCommand(void)

Calls into this documentation:

  1. ExecutecancelHighScoreEntry();
  2. Executerunning = false;
  3. ExecuteESP_LOGI("missile_command", "STOP");
Read original function source
void stopMissileCommand(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("missile_command", "STOP");
}
missileCommandAction() · source line 562
void missileCommandAction(bool action, bool speed, uint32_t now_ms)

Calls into this documentation:

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

    Continue after this decision.

  2. DecisiongameState == STATE_MODE
    True
      1. Decisionaction
        True
          1. ExecutebeginGame(now_ms);
          2. Executeai_due = now_ms + aiSpeedDelay(ai_speed);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionauto_mode
    True
      1. Decisionspeed
        True
          1. ExecutecycleAISpeed(&ai_speed);
          2. Executeai_due = now_ms + aiSpeedDelay(ai_speed);
        False

        Continue after this decision.

      2. Decisionaction
        True
          1. Executeauto_mode = false;
          2. ExecutemodeSelectionReset(&mode_input);
          3. ExecutegameState = STATE_MODE;
        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisionaction && gameState == STATE_READY
    True
    1. ExecutebeginGame(now_ms);
    False

    Continue after this decision.

Read original function source
void missileCommandAction(bool action, bool speed, uint32_t now_ms)
{
    if (!running) return;
    if (gameState == STATE_MODE) {
        if (action) {
            beginGame(now_ms);
            ai_due = now_ms + aiSpeedDelay(ai_speed);
        }
        return;
    }
    if (auto_mode) {
        if (speed) {
            cycleAISpeed(&ai_speed);
            ai_due = now_ms + aiSpeedDelay(ai_speed);
        }
        if (action) {
            auto_mode = false;
            modeSelectionReset(&mode_input);
            gameState = STATE_MODE;
        }
        return;
    }
    if (action && gameState == STATE_READY) beginGame(now_ms);
}
updateMissileCommand() · source line 587
void updateMissileCommand(int x_offset, int y_offset, bool valid,
                          bool fire_left, bool fire_centre, bool fire_right,
                          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 - lastUpdate;
  3. ExecutelastUpdate = now_ms;
  4. Executestarfield_update(&stars, elapsed);
  5. DecisiongameState == STATE_MODE
    True
      1. DecisionmodeSelectionNext(&mode_input, x_offset, y_offset, valid, fire_left, fire_right, now_ms)
        True
          1. Executeauto_mode = !auto_mode;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. DecisiongameState == STATE_FINISHED && auto_mode
    True
      1. Decisionnow_ms - stateStarted >= aiRestartDelay()
        True
          1. ExecutebeginGame(now_ms);
          2. Executeai_due = now_ms + aiSpeedDelay(ai_speed);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  7. DecisiongameState == STATE_FINISHED && now_ms - stateStarted >= 1000U
    True
      1. ExecutestartHighScoreEntry(HIGH_SCORE_MISSILE_COMMAND, score, now_ms);
      2. ExecutegameState = STATE_SCORES;
    False

    Continue after this decision.

  8. DecisiongameState == STATE_SCORES
    True
      1. DecisionupdateHighScoreEntry(x_offset, y_offset, valid, now_ms)
        True
        1. ExecutegameState = STATE_READY;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  9. DecisiongameState == STATE_WAVE_CLEAR
    True
      1. ExecuteupdateWaveTally(now_ms - stateStarted);
      2. DecisionwaveTallyElapsed >= WAVE_CLEAR_MS
        True
          1. Decisionwave < UINT32_MAX
            True
            1. Execute++wave;
            False

            Continue after this decision.

          2. ExecutebeginWave();
        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  10. DecisiongameState != STATE_PLAYING
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  11. Decisionauto_mode && now_ms >= ai_due
    True
      1. ExecuteautoFireAtThreat();
      2. Executeai_due = now_ms + aiSpeedDelay(ai_speed);
    False

    Continue after this decision.

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

      Continue after this decision.

  13. Decision!fire_left && !fire_centre && !fire_right
    True
    1. ExecutefireReady = true;
    False

    Continue after this decision.

  14. Executefloat velocityX = !auto_mode && valid && controlsReady ? axisVelocity(x_offset) : 0.0f;
  15. Executefloat velocityY = !auto_mode && valid && controlsReady ? -axisVelocity(y_offset) : 0.0f;
  16. Executeconst float speed = sqrtf(velocityX * velocityX + velocityY * velocityY);
  17. Decisionspeed > 140.0f
    True
      1. ExecutevelocityX *= 140.0f / speed;
      2. ExecutevelocityY *= 140.0f / speed;
    False

    Continue after this decision.

  18. Executeconst bool fire[BASE_COUNT] = {auto_mode ? false : fire_left, auto_mode ? false : fire_centre, auto_mode ? false : fire_right};
  19. Executeaccumulator += elapsed > 100U ? 100U : elapsed;
  20. WHILE loopaccumulator >= STEP_MS && gameState == STATE_PLAYING

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

      1. Executeaccumulator -= STEP_MS;
      2. ExecutesimulateStep(velocityX, velocityY, fire);
Read original function source
void updateMissileCommand(int x_offset, int y_offset, bool valid,
                          bool fire_left, bool fire_centre, bool fire_right,
                          uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - lastUpdate;
    lastUpdate = now_ms;
    starfield_update(&stars, elapsed);
    if (gameState == STATE_MODE) {
        if (modeSelectionNext(&mode_input, x_offset, y_offset, valid, fire_left, fire_right, now_ms)) {
            auto_mode = !auto_mode;
        }
        return;
    }
    if (gameState == STATE_FINISHED && auto_mode) {
        if (now_ms - stateStarted >= aiRestartDelay()) {
            beginGame(now_ms);
            ai_due = now_ms + aiSpeedDelay(ai_speed);
        }
        return;
    }
    if (gameState == STATE_FINISHED && now_ms - stateStarted >= 1000U) {
        startHighScoreEntry(HIGH_SCORE_MISSILE_COMMAND, score, now_ms);
        gameState = STATE_SCORES;
    }
    if (gameState == STATE_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, valid, now_ms)) gameState = STATE_READY;
        return;
    }
    if (gameState == STATE_WAVE_CLEAR) {
        updateWaveTally(now_ms - stateStarted);
        if (waveTallyElapsed >= WAVE_CLEAR_MS) {
            if (wave < UINT32_MAX) ++wave;
            beginWave();
        }
        return;
    }
    if (gameState != STATE_PLAYING) return;
    if (auto_mode && now_ms >= ai_due) {
        autoFireAtThreat();
        ai_due = now_ms + aiSpeedDelay(ai_speed);
    }
    if (!valid) controlsReady = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
        controlsReady = true;
    // Release the start/fire buttons before consuming ammunition in a new wave.
    if (!fire_left && !fire_centre && !fire_right) fireReady = true;
    float velocityX = !auto_mode && valid && controlsReady ? axisVelocity(x_offset) : 0.0f;
    float velocityY = !auto_mode && valid && controlsReady ? -axisVelocity(y_offset) : 0.0f;
    const float speed = sqrtf(velocityX * velocityX + velocityY * velocityY);
    if (speed > 140.0f) {
        velocityX *= 140.0f / speed;
        velocityY *= 140.0f / speed;
    }
    const bool fire[BASE_COUNT] = {auto_mode ? false : fire_left, auto_mode ? false : fire_centre,
                                   auto_mode ? false : fire_right};
    // Bound catch-up after a stall; the complete game can advance without drawing.
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
        accumulator -= STEP_MS;
        simulateStep(velocityX, velocityY, fire);
    }
}
drawGround() · source line 651
static void drawGround(uint16_t *frame)

Calls into this documentation:

  1. Executefill_rect(frame, 0, GROUND_Y, LCD_WIDTH, LCD_HEIGHT - GROUND_Y, COLOR_BLACK);
  2. Executedraw_line(frame, 0, GROUND_Y, LCD_WIDTH - 1, GROUND_Y, COLOR_DARKORANGE);
  3. FOR loopunsigned i = 0; i < CITY_COUNT; ++i

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

      1. Executeconst int x = cityX[i] - CITY_WIDTH / 2;
      2. Decision!cities[i]
        True
          1. Executedraw_bitmap_scaled(frame, x, TARGET_TOP, destroyedCityBitmap, CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_DARKGRAY);
          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executedraw_bitmap_scaled(frame, x, TARGET_TOP, cityBitmap, CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_CYAN);
  4. Executestatic const char baseButtons[BASE_COUNT] = {'D', 'A', 'B'};
  5. FOR loopunsigned i = 0; i < BASE_COUNT; ++i

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

      1. Executeconst int x = baseX[i];
      2. Executeconst uint16_t color = bases[i].alive ? baseColors[i] : COLOR_DARKGRAY;
      3. Decisionbases[i].alive
        True
          1. Executedraw_bitmap_scaled(frame, x - BASE_WIDTH / 2, TARGET_TOP, baseBitmap, BASE_WIDTH, TARGET_HEIGHT, BASE_WIDTH, TARGET_HEIGHT, color);
        False
          1. Executedraw_bitmap_scaled(frame, x - DESTROYED_BASE_WIDTH / 2, TARGET_TOP, destroyedBaseBitmap, DESTROYED_BASE_WIDTH, TARGET_HEIGHT, DESTROYED_BASE_WIDTH, TARGET_HEIGHT, color);
      4. Executechar label[12];
      5. Executesnprintf(label, sizeof(label), "%c %u", baseButtons[i], bases[i].ammo);
      6. Executedraw_text_centred(frame, x, 161, label, 1, color);
Read original function source
static void drawGround(uint16_t *frame)
{
    fill_rect(frame, 0, GROUND_Y, LCD_WIDTH, LCD_HEIGHT - GROUND_Y, COLOR_BLACK);
    draw_line(frame, 0, GROUND_Y, LCD_WIDTH - 1, GROUND_Y, COLOR_DARKORANGE);
    // All six surviving cities share the same artwork, aligned to the ground.
    for (unsigned i = 0; i < CITY_COUNT; ++i) {
        const int x = cityX[i] - CITY_WIDTH / 2;
        if (!cities[i]) {
            draw_bitmap_scaled(frame, x, TARGET_TOP, destroyedCityBitmap,
                               CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_DARKGRAY);
            continue;
        }
        draw_bitmap_scaled(frame, x, TARGET_TOP, cityBitmap,
                           CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_CYAN);
    }
    static const char baseButtons[BASE_COUNT] = {'D', 'A', 'B'};
    for (unsigned i = 0; i < BASE_COUNT; ++i) {
        const int x = baseX[i];
        const uint16_t color = bases[i].alive ? baseColors[i] : COLOR_DARKGRAY;
        if (bases[i].alive) {
            draw_bitmap_scaled(frame, x - BASE_WIDTH / 2, TARGET_TOP, baseBitmap,
                               BASE_WIDTH, TARGET_HEIGHT, BASE_WIDTH, TARGET_HEIGHT, color);
        } else {
            draw_bitmap_scaled(frame, x - DESTROYED_BASE_WIDTH / 2, TARGET_TOP, destroyedBaseBitmap,
                               DESTROYED_BASE_WIDTH, TARGET_HEIGHT,
                               DESTROYED_BASE_WIDTH, TARGET_HEIGHT, color);
        }
        char label[12];
        snprintf(label, sizeof(label), "%c %u", baseButtons[i], bases[i].ammo);
        draw_text_centred(frame, x, 161, label, 1, color);
    }
}
drawMissileCommand() · source line 684
void drawMissileCommand(uint16_t *frame)

Calls into this documentation:

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

    Continue after this decision.

  2. DecisiongameState == STATE_SCORES
    True
      1. ExecutedrawHighScoreEntry(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. DecisiongameState == STATE_MODE
    True
      1. Executeclear(frame, COLOR_BLACK);
      2. Executestarfield_draw(frame, &stars);
      3. Executedraw_text_centred(frame, 160, 22, "MISSILE COMMAND", 3, COLOR_CYAN);
      4. Executedraw_text_centred(frame, 160, 62, "SELECT MODE", 2, COLOR_LIGHTGRAY);
      5. Executedraw_text_centred(frame, 100, 96, "PLAY", 2, auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
      6. Executedraw_text_centred(frame, 220, 96, "AUTO", 2, auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
      7. Executedraw_text_centred(frame, 160, 145, "D/B/STICK CHANGE A/K START", 1, COLOR_LIGHTGRAY);
      8. Exit this functionreturn;
    False

    Continue after this decision.

  4. Executeclear(frame, COLOR_BLACK);
  5. Executestarfield_draw(frame, &stars);
  6. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

      1. Executeconst Missile *missile = &enemies[i];
      2. Decision!missile->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y);
      4. Executedraw_line(frame, (int)missile->startX, (int)missile->startY, x, y, COLOR_RED);
      5. Executefill_rect(frame, x - 1, y - 1, 3, 3, COLOR_WHITE);
  7. FOR loopunsigned i = 0; i < MAX_INTERCEPTORS; ++i

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

      1. Executeconst Missile *missile = &interceptors[i];
      2. Decision!missile->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y);
      4. Executedraw_line(frame, (int)missile->startX, (int)missile->startY, x, y, baseColors[missile->target]);
      5. Executefill_rect(frame, x - 1, y - 1, 2, 2, COLOR_WHITE);
  8. FOR loopunsigned i = 0; i < MAX_EXPLOSIONS; ++i

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

      1. Executeconst Explosion *explosion = &explosions[i];
      2. Decision!explosion->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst int x = (int)lroundf(explosion->x), y = (int)lroundf(explosion->y);
      4. Executeconst int radius = (int)explosion->radius;
      5. Executefill_circle(frame, x, y, radius, explosion->defensive ? COLOR_DARKORANGE : COLOR_DARKRED);
      6. Executedraw_ring(frame, x, y, radius, COLOR_YELLOW);
      7. Decisionradius > 2
        True
        1. Executefill_circle(frame, x, y, radius / 2, COLOR_ORANGE);
        False

        Continue after this decision.

      8. Decisionradius > 4
        True
        1. Executefill_circle(frame, x, y, radius / 4, COLOR_WHITE);
        False

        Continue after this decision.

  9. ExecutedrawGround(frame);
  10. FOR loopunsigned i = 0; i < MAX_INTERCEPTORS; ++i

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

      1. Executeconst Missile *missile = &interceptors[i];
      2. Decision!missile->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst int x = (int)lroundf(missile->targetX);
      4. Executeconst int y = (int)lroundf(missile->targetY);
      5. Executeconst uint16_t color = baseColors[missile->target];
      6. Executedraw_line(frame, x - 2, y - 2, x + 2, y + 2, color);
      7. Executedraw_line(frame, x - 2, y + 2, x + 2, y - 2, color);
  11. DecisiongameState == STATE_PLAYING
    True
      1. Executeconst int x = (int)lroundf(aimX), y = (int)lroundf(aimY);
      2. Executedraw_line(frame, x - 5, y, x - 2, y, COLOR_WHITE);
      3. Executedraw_line(frame, x + 2, y, x + 5, y, COLOR_WHITE);
      4. Executedraw_line(frame, x, y - 5, x, y - 2, COLOR_WHITE);
      5. Executedraw_line(frame, x, y + 2, x, y + 5, COLOR_WHITE);
    False

    Continue after this decision.

  12. Executefill_rect(frame, 0, 0, LCD_WIDTH, 13, COLOR_BLACK);
  13. Executechar text[40];
  14. Executesnprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
  15. Executedraw_text(frame, 3, 3, text, 1, COLOR_WHITE);
  16. Executesnprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
  17. Executedraw_text_centred(frame, 183, 3, text, 1, COLOR_YELLOW);
  18. Executesnprintf(text, sizeof(text), "CITIES %u", livingCities());
  19. Executedraw_text(frame, 266, 3, text, 1, COLOR_CYAN);
  20. DecisiongameState == STATE_READY
    True
      1. Executefill_rect(frame, 9, 22, 302, 109, COLOR_BLACK);
      2. Executedraw_text_centred(frame, 160, 25, "MISSILE COMMAND", 3, COLOR_CYAN);
      3. Executedraw_text_centred(frame, 160, 57, "DEFEND YOUR SIX CITIES", 1, COLOR_WHITE);
      4. Executedraw_text_centred(frame, 160, 73, "JOYSTICK AIM", 1, COLOR_WHITE);
      5. Executedraw_text_centred(frame, 160, 87, "D LEFT A CENTRE B RIGHT", 1, COLOR_YELLOW);
      6. Executedraw_text_centred(frame, 160, 123, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    False
    1. DecisiongameState == STATE_WAVE_CLEAR
      True
        1. Executefill_rect(frame, 37, 44, 246, 72, COLOR_BLACK);
        2. Executedraw_rect(frame, 37, 44, 246, 72, COLOR_DARKORANGE);
        3. Executedraw_text_centred(frame, 160, 50, "WAVE CLEAR", 2, COLOR_YELLOW);
        4. Executesnprintf(text, sizeof(text), "MISSILES %u X 5 +%u", waveMissilesRemaining, (unsigned)waveMissileAwarded);
        5. Executedraw_text_centred(frame, 160, 72, text, 1, COLOR_WHITE);
        6. DecisionwaveTallyElapsed >= WAVE_TALLY_MISSILES_MS
          True
            1. Executesnprintf(text, sizeof(text), "CITIES %u X 100 +%u", waveCitiesRemaining, (unsigned)waveCityAwarded);
            2. Executedraw_text_centred(frame, 160, 87, text, 1, COLOR_CYAN);
          False

          Continue after this decision.

        7. DecisionwaveTallyElapsed >= WAVE_TALLY_MISSILES_MS + WAVE_TALLY_CITIES_MS
          True
            1. Executesnprintf(text, sizeof(text), "WAVE BONUS +%u", (unsigned)waveBonus);
            2. Executedraw_text_centred(frame, 160, 102, text, 1, COLOR_YELLOW);
          False

          Continue after this decision.

      False
      1. DecisiongameState == STATE_FINISHED
        True
          1. Executedraw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
        False

        Continue after this decision.

  21. Decisionauto_mode
    True
      1. DecisiongameState == STATE_FINISHED
        True
          1. Executeconst uint32_t delay = aiRestartDelay();
          2. Executeconst uint32_t elapsed = lastUpdate - stateStarted;
          3. Executesnprintf(text, sizeof(text), "NEXT %u E SPEED A/K STOP", elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U));
        False
          1. Executesnprintf(text, sizeof(text), "AUTO %s E SPEED A/K STOP", aiSpeedShortText(ai_speed));
      2. Executefill_rect(frame, 50, 15, 220, 11, COLOR_BLACK);
      3. Executedraw_text_centred(frame, 160, 16, text, 1, COLOR_LIGHTGRAY);
    False

    Continue after this decision.

Read original function source
void drawMissileCommand(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (gameState == STATE_SCORES) {
        drawHighScoreEntry(frame);
        return;
    }
    if (gameState == STATE_MODE) {
        clear(frame, COLOR_BLACK);
        starfield_draw(frame, &stars);
        draw_text_centred(frame, 160, 22, "MISSILE COMMAND", 3, COLOR_CYAN);
        draw_text_centred(frame, 160, 62, "SELECT MODE", 2, COLOR_LIGHTGRAY);
        draw_text_centred(frame, 100, 96, "PLAY", 2, auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
        draw_text_centred(frame, 220, 96, "AUTO", 2, auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
        draw_text_centred(frame, 160, 145, "D/B/STICK CHANGE  A/K START", 1, COLOR_LIGHTGRAY);
        return;
    }
    clear(frame, COLOR_BLACK);
    starfield_draw(frame, &stars);
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        const Missile *missile = &enemies[i];
        if (!missile->active) continue;
        const int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y);
        draw_line(frame, (int)missile->startX, (int)missile->startY, x, y, COLOR_RED);
        fill_rect(frame, x - 1, y - 1, 3, 3, COLOR_WHITE);
    }
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        const Missile *missile = &interceptors[i];
        if (!missile->active) continue;
        const int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y);
        draw_line(frame, (int)missile->startX, (int)missile->startY, x, y, baseColors[missile->target]);
        fill_rect(frame, x - 1, y - 1, 2, 2, COLOR_WHITE);
    }
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        const Explosion *explosion = &explosions[i];
        if (!explosion->active) continue;
        const int x = (int)lroundf(explosion->x), y = (int)lroundf(explosion->y);
        const int radius = (int)explosion->radius;
        fill_circle(frame, x, y, radius, explosion->defensive ? COLOR_DARKORANGE : COLOR_DARKRED);
        draw_ring(frame, x, y, radius, COLOR_YELLOW);
        if (radius > 2) fill_circle(frame, x, y, radius / 2, COLOR_ORANGE);
        if (radius > 4) fill_circle(frame, x, y, radius / 4, COLOR_WHITE);
    }
    drawGround(frame);
    // Mark each in-flight interceptor's fixed destination, above existing blasts.
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        const Missile *missile = &interceptors[i];
        if (!missile->active) continue;
        const int x = (int)lroundf(missile->targetX);
        const int y = (int)lroundf(missile->targetY);
        const uint16_t color = baseColors[missile->target];
        draw_line(frame, x - 2, y - 2, x + 2, y + 2, color);
        draw_line(frame, x - 2, y + 2, x + 2, y - 2, color);
    }
    if (gameState == STATE_PLAYING) {
        const int x = (int)lroundf(aimX), y = (int)lroundf(aimY);
        draw_line(frame, x - 5, y, x - 2, y, COLOR_WHITE);
        draw_line(frame, x + 2, y, x + 5, y, COLOR_WHITE);
        draw_line(frame, x, y - 5, x, y - 2, COLOR_WHITE);
        draw_line(frame, x, y + 2, x, y + 5, COLOR_WHITE);
    }
    fill_rect(frame, 0, 0, LCD_WIDTH, 13, COLOR_BLACK);
    char text[40];
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    draw_text(frame, 3, 3, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
    draw_text_centred(frame, 183, 3, text, 1, COLOR_YELLOW);
    snprintf(text, sizeof(text), "CITIES %u", livingCities());
    draw_text(frame, 266, 3, text, 1, COLOR_CYAN);
    if (gameState == STATE_READY) {
        fill_rect(frame, 9, 22, 302, 109, COLOR_BLACK);
        draw_text_centred(frame, 160, 25, "MISSILE COMMAND", 3, COLOR_CYAN);
        draw_text_centred(frame, 160, 57, "DEFEND YOUR SIX CITIES", 1, COLOR_WHITE);
        draw_text_centred(frame, 160, 73, "JOYSTICK AIM", 1, COLOR_WHITE);
        draw_text_centred(frame, 160, 87, "D LEFT   A CENTRE   B RIGHT", 1, COLOR_YELLOW);
        draw_text_centred(frame, 160, 123, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    } else if (gameState == STATE_WAVE_CLEAR) {
        fill_rect(frame, 37, 44, 246, 72, COLOR_BLACK);
        draw_rect(frame, 37, 44, 246, 72, COLOR_DARKORANGE);
        draw_text_centred(frame, 160, 50, "WAVE CLEAR", 2, COLOR_YELLOW);
        snprintf(text, sizeof(text), "MISSILES %u X 5 +%u", waveMissilesRemaining,
                 (unsigned)waveMissileAwarded);
        draw_text_centred(frame, 160, 72, text, 1, COLOR_WHITE);
        if (waveTallyElapsed >= WAVE_TALLY_MISSILES_MS) {
            snprintf(text, sizeof(text), "CITIES %u X 100 +%u", waveCitiesRemaining,
                     (unsigned)waveCityAwarded);
            draw_text_centred(frame, 160, 87, text, 1, COLOR_CYAN);
        }
        if (waveTallyElapsed >= WAVE_TALLY_MISSILES_MS + WAVE_TALLY_CITIES_MS) {
            snprintf(text, sizeof(text), "WAVE BONUS +%u", (unsigned)waveBonus);
            draw_text_centred(frame, 160, 102, text, 1, COLOR_YELLOW);
        }
    } else if (gameState == STATE_FINISHED) {
        draw_text_centred(frame, 160, 78,
                           "GAME OVER", 2, COLOR_RED);
    }
    if (auto_mode) {
        if (gameState == STATE_FINISHED) {
            const uint32_t delay = aiRestartDelay();
            const uint32_t elapsed = lastUpdate - stateStarted;
            snprintf(text, sizeof(text), "NEXT %u  E SPEED  A/K STOP",
                     elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U));
        } else {
            snprintf(text, sizeof(text), "AUTO %s  E SPEED  A/K STOP", aiSpeedShortText(ai_speed));
        }
        fill_rect(frame, 50, 15, 220, 11, COLOR_BLACK);
        draw_text_centred(frame, 160, 16, text, 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 "missile_command.h"
#include "audio.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "gold.h"
#include "high_scores.h"
#include "ai_speed.h"
#include "mode_selection.h"
#include "starfield.h"
#include "esp_log.h"
#include "esp_random.h"
#include <math.h>
#include <stdio.h>
#include <string.h>

#define CITY_COUNT 6U
#define BASE_COUNT 3U
#define AMMO_PER_BASE 10U
#define MAX_ENEMIES 32U
#define MAX_INTERCEPTORS 32U
// At most 48 enemies and 30 interceptors can make one explosion each per wave.
#define MAX_EXPLOSIONS 80U
#define STEP_MS 10U
#define FIRE_DELAY_MS 250U
#define WAVE_TALLY_MISSILES_MS 1000U
#define WAVE_TALLY_CITIES_MS 1000U
#define WAVE_TALLY_TOTAL_MS 3000U
#define WAVE_CLEAR_MS (WAVE_TALLY_MISSILES_MS + WAVE_TALLY_CITIES_MS + WAVE_TALLY_TOTAL_MS)
#define GROUND_Y 158
#define TARGET_TOP 142
#define TARGET_HEIGHT 16
#define CITY_WIDTH 32
#define BASE_WIDTH 24
#define DESTROYED_BASE_WIDTH 32
#define BLAST_GROW_MS 450U
#define BLAST_HOLD_MS 250U
#define BLAST_SHRINK_MS 550U
#define BLAST_LIFE_MS (BLAST_GROW_MS + BLAST_HOLD_MS + BLAST_SHRINK_MS)
#define BLAST_RADIUS 20.0f
#define MISSILE_STAR_COUNT 45U

typedef enum {
    STATE_MODE, STATE_READY, STATE_PLAYING, STATE_WAVE_CLEAR, STATE_FINISHED, STATE_SCORES
} GameState;

typedef struct {
    float startX, startY, x, y, previousX, previousY;
    float targetX, targetY, vx, vy;
    unsigned target;
    bool active, arrived;
} Missile;

typedef struct {
    float x, y, radius;
    uint32_t age;
    bool active, defensive;
} Explosion;

typedef struct {
    unsigned ammo;
    uint32_t fireDelay;
    bool alive;
} Base;

static const int cityX[CITY_COUNT] = {51, 85, 119, 201, 235, 269};
static const int baseX[BASE_COUNT] = {18, 160, 302};
static const uint16_t baseColors[BASE_COUNT] = {COLOR_LIME, COLOR_CYAN, COLOR_YELLOW};

// Shared 32x16 city artwork: four bytes per row, most significant bit on the left.
static const uint8_t cityBitmap[64] = {
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x38, 0x00, 0x1C, 0xE0,
    0x28, 0x00, 0x1C, 0xA0,
    0x39, 0xC0, 0x14, 0xE0,
    0x39, 0xC0, 0x1C, 0xA0,
    0x39, 0x4E, 0x1C, 0xE7,
    0x29, 0xCA, 0x1C, 0xA7,
    0xF9, 0xCE, 0x14, 0xE5,
    0xB9, 0x4E, 0x3C, 0xE7,
    0xF9, 0xCE, 0xFC, 0xE5,
    0xFF, 0xCA, 0xB4, 0xFF,
    0xAD, 0xCE, 0xFC, 0xBB,
    0xFF, 0xFF, 0xAF, 0xED,
    0xFF, 0xFF, 0xFF, 0xFF
};

// Shared 32x16 destroyed city artwork, using the same row layout as cityBitmap.
static const uint8_t destroyedCityBitmap[64] = {
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x08, 0x00,
    0x00, 0x20, 0x0C, 0x00,
    0x00, 0x20, 0x54, 0x00,
    0x00, 0xAF, 0xA2, 0x00,
    0x00, 0x78, 0xF9, 0x40,
    0x00, 0xAF, 0xC3, 0x80,
    0x0F, 0x99, 0x05, 0x90,
    0x3F, 0x09, 0xD9, 0x44,
    0x68, 0xE6, 0x61, 0xF8,
    0xDF, 0x3E, 0xC5, 0x38,
    0xF1, 0xD3, 0x7E, 0x06
};

// Shared 24x16 base artwork: three bytes per row, most significant bit on the left.
static const uint8_t baseBitmap[48] = {
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x01, 0x99, 0x80,
    0x03, 0x99, 0xC0,
    0x03, 0x3C, 0xE0,
    0x07, 0x3C, 0x70,
    0x0E, 0x3C, 0x38,
    0x1C, 0x3C, 0x1C,
    0x38, 0xFF, 0x1E,
    0x79, 0xFF, 0x9F
};

// Shared 32x16 destroyed base artwork: four bytes per row, centred on the base.
static const uint8_t destroyedBaseBitmap[64] = {
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x40, 0x00, 0x00,
    0x01, 0xC0, 0x00, 0x00,
    0x01, 0x00, 0x00, 0x80,
    0x23, 0x00, 0x01, 0x40,
    0x66, 0xE0, 0x03, 0xF8,
    0xDF, 0x18, 0x07, 0xB8,
    0xF3, 0xE0, 0x03, 0xFE
};

// Game state owns every collision coordinate. No framebuffer is stored here.
static Missile enemies[MAX_ENEMIES], interceptors[MAX_INTERCEPTORS];
static Explosion explosions[MAX_EXPLOSIONS];
static Base bases[BASE_COUNT];
static bool cities[CITY_COUNT];
static GameState gameState;
static float aimX, aimY;
static uint32_t score, wave, lastUpdate, accumulator, stateStarted, spawnDelay;
static uint32_t waveBonus, waveMissileBonus, waveCityBonus;
static uint32_t waveMissileAwarded, waveCityAwarded, waveTallyElapsed;
static unsigned waveMissilesRemaining, waveCitiesRemaining;
static unsigned enemiesToSpawn;
static bool running, controlsReady, fireReady, auto_mode;
static uint8_t ai_speed;
static uint32_t ai_due;
static mode_selection_input_t mode_input;
static starfield_t stars;

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

static unsigned livingCities(void)
{
    unsigned count = 0;
    for (unsigned i = 0; i < CITY_COUNT; ++i) if (cities[i]) ++count;
    return count;
}

static unsigned difficulty(void)
{
    return wave <= 10U ? (unsigned)(wave - 1U) : 9U;
}

static void addPoints(uint32_t points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
}

static void updateWaveTally(uint32_t elapsed)
{
    waveTallyElapsed = elapsed;
    const uint32_t missileElapsed = elapsed < WAVE_TALLY_MISSILES_MS ? elapsed : WAVE_TALLY_MISSILES_MS;
    const uint32_t cityElapsed = elapsed <= WAVE_TALLY_MISSILES_MS ? 0U
        : elapsed - WAVE_TALLY_MISSILES_MS < WAVE_TALLY_CITIES_MS
            ? elapsed - WAVE_TALLY_MISSILES_MS : WAVE_TALLY_CITIES_MS;
    const uint32_t missiles = waveMissileBonus * missileElapsed / WAVE_TALLY_MISSILES_MS;
    const uint32_t cities = waveCityBonus * cityElapsed / WAVE_TALLY_CITIES_MS;

    if (missiles > waveMissileAwarded) addPoints(missiles - waveMissileAwarded);
    if (cities > waveCityAwarded) addPoints(cities - waveCityAwarded);
    waveMissileAwarded = missiles;
    waveCityAwarded = cities;
}

// Swept missile tip versus circle: fast motion cannot jump over a blast.
static bool segmentHitsCircle(float ax, float ay, float bx, float by,
                               float cx, float cy, float radius)
{
    const float dx = bx - ax, dy = by - ay;
    const float lengthSquared = dx * dx + dy * dy;
    const float fraction = lengthSquared > 0.0f
        ? clampValue(((cx - ax) * dx + (cy - ay) * dy) / lengthSquared, 0.0f, 1.0f)
        : 0.0f;
    const float distanceX = ax + fraction * dx - cx;
    const float distanceY = ay + fraction * dy - cy;
    return distanceX * distanceX + distanceY * distanceY <= radius * radius;
}

static bool clipAxis(float origin, float delta, float minimum, float maximum,
                      float *enter, float *leave)
{
    if (delta == 0.0f) return origin >= minimum && origin <= maximum;
    float first = (minimum - origin) / delta;
    float last = (maximum - origin) / delta;
    if (first > last) {
        const float swap = first;
        first = last;
        last = swap;
    }
    if (first > *enter) *enter = first;
    if (last < *leave) *leave = last;
    return *enter <= *leave;
}

static bool segmentHitsTarget(const Missile *missile, float centreX, float width)
{
    float enter = 0.0f, leave = 1.0f;
    return clipAxis(missile->previousX, missile->x - missile->previousX,
                    centreX - width * 0.5f, centreX + width * 0.5f, &enter, &leave) &&
           clipAxis(missile->previousY, missile->y - missile->previousY,
                    TARGET_TOP, GROUND_Y, &enter, &leave);
}

static void startExplosion(float x, float y, bool defensive)
{
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        if (explosions[i].active) continue;
        explosions[i] = (Explosion){
            .x = x, .y = y, .radius = 2.0f, .age = 0,
            .active = true, .defensive = defensive,
        };
        return;
    }
    ESP_LOGE("missile_command", "Explosion capacity exceeded");
}

static void updateExplosions(void)
{
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        Explosion *explosion = &explosions[i];
        if (!explosion->active) continue;
        explosion->age += STEP_MS;
        if (explosion->age >= BLAST_LIFE_MS) {
            explosion->active = false;
        } else if (explosion->age < BLAST_GROW_MS) {
            explosion->radius = 2.0f + (BLAST_RADIUS - 2.0f) * explosion->age / BLAST_GROW_MS;
        } else if (explosion->age < BLAST_GROW_MS + BLAST_HOLD_MS) {
            explosion->radius = BLAST_RADIUS;
        } else {
            explosion->radius = BLAST_RADIUS * (BLAST_LIFE_MS - explosion->age) / BLAST_SHRINK_MS;
        }
    }
}

static Missile makeMissile(float x, float y, float targetX, float targetY,
                            float speed, unsigned target)
{
    const float dx = targetX - x, dy = targetY - y;
    const float distance = sqrtf(dx * dx + dy * dy);
    return (Missile){
        .startX = x, .startY = y, .x = x, .y = y, .previousX = x, .previousY = y,
        .targetX = targetX, .targetY = targetY,
        .vx = distance > 0.0f ? dx * speed / distance : 0.0f,
        .vy = distance > 0.0f ? dy * speed / distance : 0.0f,
        .target = target, .active = true, .arrived = false,
    };
}

static void moveMissile(Missile *missile)
{
    missile->previousX = missile->x;
    missile->previousY = missile->y;
    const float dx = missile->targetX - missile->x;
    const float dy = missile->targetY - missile->y;
    const float stepX = missile->vx * STEP_MS / 1000.0f;
    const float stepY = missile->vy * STEP_MS / 1000.0f;
    if (dx * dx + dy * dy <= stepX * stepX + stepY * stepY) {
        missile->x = missile->targetX;
        missile->y = missile->targetY;
        missile->arrived = true;
    } else {
        missile->x += stepX;
        missile->y += stepY;
    }
}

static void spawnEnemy(void)
{
    unsigned targets[CITY_COUNT + BASE_COUNT], count = 0;
    for (unsigned i = 0; i < CITY_COUNT; ++i) if (cities[i]) targets[count++] = i;
    for (unsigned i = 0; i < BASE_COUNT; ++i) if (bases[i].alive) targets[count++] = CITY_COUNT + i;
    if (count == 0) return;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (enemies[i].active) continue;
        const unsigned target = targets[esp_random() % count];
        const float targetX = target < CITY_COUNT ? cityX[target] : baseX[target - CITY_COUNT];
        enemies[i] = makeMissile(5.0f + esp_random() % 310U, 14.0f,
                                 targetX, TARGET_TOP + TARGET_HEIGHT * 0.5f,
                                 16.0f + difficulty() * 2.5f, target);
        --enemiesToSpawn;
        spawnDelay = 900U - difficulty() * 55U + esp_random() % 301U;
        return;
    }
}

static void fireBase(unsigned base)
{
    if (!bases[base].alive || bases[base].ammo == 0 || bases[base].fireDelay != 0) return;
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        if (interceptors[i].active) continue;
        interceptors[i] = makeMissile(baseX[base], TARGET_TOP, aimX, aimY,
                                      base == 1U ? 180.0f : 140.0f, base);
        --bases[base].ammo;
        bases[base].fireDelay = FIRE_DELAY_MS;
        audioPlay(AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}

static bool defensiveBlastCovers(const Missile *enemy)
{
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        const Explosion *explosion = &explosions[i];
        if (!explosion->active || !explosion->defensive) continue;
        const float dx = enemy->x - explosion->x;
        const float dy = enemy->y - explosion->y;
        const float radius = explosion->radius + 8.0f;
        if (dx * dx + dy * dy <= radius * radius) return true;
    }
    return false;
}

static bool interceptorCovers(const Missile *enemy)
{
    const float coverage = BLAST_RADIUS + 8.0f;
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        const Missile *interceptor = &interceptors[i];
        if (!interceptor->active) continue;
        const float dx = enemy->x - interceptor->targetX;
        const float dy = enemy->y - interceptor->targetY;
        if (dx * dx + dy * dy <= coverage * coverage) return true;
    }
    return false;
}

static int closestAutoBase(float target_x)
{
    int best_base = -1;
    float best_distance = 0.0f;
    for (unsigned base = 0; base < BASE_COUNT; ++base) {
        if (!bases[base].alive || bases[base].ammo == 0U || bases[base].fireDelay != 0U) continue;
        const float distance = fabsf(target_x - (float)baseX[base]);
        if (best_base < 0 || distance < best_distance) {
            best_base = (int)base;
            best_distance = distance;
        }
    }
    return best_base;
}

static void autoFireAtThreat(void)
{
    const Missile *threat = NULL;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        const Missile *enemy = &enemies[i];
        if (!enemy->active || defensiveBlastCovers(enemy) || interceptorCovers(enemy)) continue;
        if (threat == NULL || enemy->y > threat->y) threat = enemy;
    }
    if (threat == NULL) return;

    const int base = closestAutoBase(threat->x);
    if (base < 0) return;
    const float interceptor_speed = base == 1 ? 180.0f : 140.0f;
    const float distance_x = threat->x - (float)baseX[base];
    const float distance_y = threat->y - (float)TARGET_TOP;
    const float travel_ms = sqrtf(distance_x * distance_x + distance_y * distance_y) * 1000.0f / interceptor_speed;
    aimX = clampValue(threat->x + threat->vx * travel_ms / 1000.0f, 5.0f, 314.0f);
    aimY = clampValue(threat->y + threat->vy * travel_ms / 1000.0f, 20.0f, 137.0f);
    fireBase((unsigned)base);
}

static void beginWave(void)
{
    memset(enemies, 0, sizeof(enemies));
    memset(interceptors, 0, sizeof(interceptors));
    memset(explosions, 0, sizeof(explosions));
    for (unsigned i = 0; i < BASE_COUNT; ++i) bases[i] = (Base){AMMO_PER_BASE, 0, true};
    enemiesToSpawn = 12U + difficulty() * 4U;
    spawnDelay = 1000U;
    accumulator = 0;
    fireReady = false;
    gameState = STATE_PLAYING;
    ESP_LOGI("missile_command", "WAVE %u: %u cities", (unsigned)wave, livingCities());
}

static void beginGame(uint32_t now_ms)
{
    score = waveBonus = waveMissileBonus = waveCityBonus = 0;
    waveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0;
    waveMissilesRemaining = waveCitiesRemaining = 0;
    wave = 1;
    for (unsigned i = 0; i < CITY_COUNT; ++i) cities[i] = true;
    aimX = 160.0f;
    aimY = 80.0f;
    controlsReady = false;
    lastUpdate = now_ms;
    beginWave();
    audioPlay(AUDIO_SFX_GAME_START);
}

static void finishGame(void)
{
    gameState = STATE_FINISHED;
    stateStarted = lastUpdate;
    audioPlay(AUDIO_SFX_GAME_OVER);
    ESP_LOGI("missile_command", "GAME OVER: SCORE %u, WAVE %u", (unsigned)score, (unsigned)wave);
    const uint32_t gold = score / 1000U;
    if (gold != 0 && !addUserGoldWon(gold)) {
        ESP_LOGE("missile_command", "Failed to save %u Gold", (unsigned)gold);
    }
}

static bool waveIsQuiet(void)
{
    if (enemiesToSpawn != 0) return false;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) return false;
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) if (interceptors[i].active) return false;
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) if (explosions[i].active) return false;
    return true;
}

static void resolveCollisions(void)
{
    // Resolve new chain reactions in this same step, regardless of array order.
    bool chainReaction;
    do {
        chainReaction = false;
        for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
            Missile *enemy = &enemies[i];
            if (!enemy->active) continue;
            for (unsigned j = 0; j < MAX_EXPLOSIONS; ++j) {
                const Explosion *explosion = &explosions[j];
                if (!explosion->active || !explosion->defensive) continue;
                if (!segmentHitsCircle(enemy->previousX, enemy->previousY, enemy->x, enemy->y,
                                        explosion->x, explosion->y, explosion->radius)) continue;
                enemy->active = false;
                addPoints(25U);
                startExplosion(enemy->x, enemy->y, true);
                audioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
                chainReaction = true;
                break;
            }
        }
    } while (chainReaction);

    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        Missile *enemy = &enemies[i];
        if (!enemy->active) continue;
        const bool city = enemy->target < CITY_COUNT;
        const unsigned index = city ? enemy->target : enemy->target - CITY_COUNT;
        if (!enemy->arrived && !segmentHitsTarget(enemy, city ? cityX[index] : baseX[index],
                                                  city ? CITY_WIDTH : BASE_WIDTH)) continue;
        enemy->active = false;
        if (city) cities[index] = false;
        else {
            bases[index].alive = false;
            bases[index].ammo = 0;
        }
        // Ground impacts are visual effects; they do not earn defensive chain hits.
        startExplosion(enemy->x, enemy->y, false);
        audioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
    }
}

static void simulateStep(float aimVelocityX, float aimVelocityY, const bool fire[BASE_COUNT])
{
    aimX = clampValue(aimX + aimVelocityX * STEP_MS / 1000.0f, 5.0f, 314.0f);
    aimY = clampValue(aimY + aimVelocityY * STEP_MS / 1000.0f, 20.0f, 137.0f);
    updateExplosions();
    for (unsigned i = 0; i < BASE_COUNT; ++i) {
        bases[i].fireDelay = bases[i].fireDelay > STEP_MS ? bases[i].fireDelay - STEP_MS : 0;
        if (fireReady && fire[i]) fireBase(i);
    }
    spawnDelay = spawnDelay > STEP_MS ? spawnDelay - STEP_MS : 0;
    if (enemiesToSpawn != 0 && spawnDelay == 0) spawnEnemy();

    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        Missile *missile = &interceptors[i];
        if (!missile->active) continue;
        moveMissile(missile);
        if (missile->arrived) {
            missile->active = false;
            startExplosion(missile->x, missile->y, true);
            audioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
        }
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) moveMissile(&enemies[i]);
    resolveCollisions();

    if (livingCities() == 0) {
        finishGame();
    } else if (waveIsQuiet()) {
        waveMissilesRemaining = 0;
        for (unsigned i = 0; i < BASE_COUNT; ++i) waveMissilesRemaining += bases[i].ammo;
        waveCitiesRemaining = livingCities();
        waveMissileBonus = waveMissilesRemaining * 5U;
        waveCityBonus = waveCitiesRemaining * 100U;
        waveBonus = waveMissileBonus + waveCityBonus;
        waveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0;
        gameState = STATE_WAVE_CLEAR;
        stateStarted = lastUpdate;
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
    }
}

static float axisVelocity(int offset)
{
    if (offset >= -350 && offset <= 350) return 0.0f;
    const float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f;
    return clampValue(adjusted / 1500.0f, -1.0f, 1.0f) * 140.0f;
}

void startMissileCommand(uint32_t now_ms)
{
    starfield_initialize(&stars, MISSILE_STAR_COUNT, 0, 0, LCD_WIDTH, GROUND_Y,
                         STARFIELD_GREYSCALE);
    running = true;
    auto_mode = false;
    ai_speed = AI_SPEED_DEFAULT_INDEX;
    modeSelectionReset(&mode_input);
    lastUpdate = now_ms;
    gameState = STATE_MODE;
    ESP_LOGI("missile_command", "START");
}

void stopMissileCommand(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("missile_command", "STOP");
}

void missileCommandAction(bool action, bool speed, uint32_t now_ms)
{
    if (!running) return;
    if (gameState == STATE_MODE) {
        if (action) {
            beginGame(now_ms);
            ai_due = now_ms + aiSpeedDelay(ai_speed);
        }
        return;
    }
    if (auto_mode) {
        if (speed) {
            cycleAISpeed(&ai_speed);
            ai_due = now_ms + aiSpeedDelay(ai_speed);
        }
        if (action) {
            auto_mode = false;
            modeSelectionReset(&mode_input);
            gameState = STATE_MODE;
        }
        return;
    }
    if (action && gameState == STATE_READY) beginGame(now_ms);
}

void updateMissileCommand(int x_offset, int y_offset, bool valid,
                          bool fire_left, bool fire_centre, bool fire_right,
                          uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - lastUpdate;
    lastUpdate = now_ms;
    starfield_update(&stars, elapsed);
    if (gameState == STATE_MODE) {
        if (modeSelectionNext(&mode_input, x_offset, y_offset, valid, fire_left, fire_right, now_ms)) {
            auto_mode = !auto_mode;
        }
        return;
    }
    if (gameState == STATE_FINISHED && auto_mode) {
        if (now_ms - stateStarted >= aiRestartDelay()) {
            beginGame(now_ms);
            ai_due = now_ms + aiSpeedDelay(ai_speed);
        }
        return;
    }
    if (gameState == STATE_FINISHED && now_ms - stateStarted >= 1000U) {
        startHighScoreEntry(HIGH_SCORE_MISSILE_COMMAND, score, now_ms);
        gameState = STATE_SCORES;
    }
    if (gameState == STATE_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, valid, now_ms)) gameState = STATE_READY;
        return;
    }
    if (gameState == STATE_WAVE_CLEAR) {
        updateWaveTally(now_ms - stateStarted);
        if (waveTallyElapsed >= WAVE_CLEAR_MS) {
            if (wave < UINT32_MAX) ++wave;
            beginWave();
        }
        return;
    }
    if (gameState != STATE_PLAYING) return;
    if (auto_mode && now_ms >= ai_due) {
        autoFireAtThreat();
        ai_due = now_ms + aiSpeedDelay(ai_speed);
    }
    if (!valid) controlsReady = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
        controlsReady = true;
    // Release the start/fire buttons before consuming ammunition in a new wave.
    if (!fire_left && !fire_centre && !fire_right) fireReady = true;
    float velocityX = !auto_mode && valid && controlsReady ? axisVelocity(x_offset) : 0.0f;
    float velocityY = !auto_mode && valid && controlsReady ? -axisVelocity(y_offset) : 0.0f;
    const float speed = sqrtf(velocityX * velocityX + velocityY * velocityY);
    if (speed > 140.0f) {
        velocityX *= 140.0f / speed;
        velocityY *= 140.0f / speed;
    }
    const bool fire[BASE_COUNT] = {auto_mode ? false : fire_left, auto_mode ? false : fire_centre,
                                   auto_mode ? false : fire_right};
    // Bound catch-up after a stall; the complete game can advance without drawing.
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
        accumulator -= STEP_MS;
        simulateStep(velocityX, velocityY, fire);
    }
}

static void drawGround(uint16_t *frame)
{
    fill_rect(frame, 0, GROUND_Y, LCD_WIDTH, LCD_HEIGHT - GROUND_Y, COLOR_BLACK);
    draw_line(frame, 0, GROUND_Y, LCD_WIDTH - 1, GROUND_Y, COLOR_DARKORANGE);
    // All six surviving cities share the same artwork, aligned to the ground.
    for (unsigned i = 0; i < CITY_COUNT; ++i) {
        const int x = cityX[i] - CITY_WIDTH / 2;
        if (!cities[i]) {
            draw_bitmap_scaled(frame, x, TARGET_TOP, destroyedCityBitmap,
                               CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_DARKGRAY);
            continue;
        }
        draw_bitmap_scaled(frame, x, TARGET_TOP, cityBitmap,
                           CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_CYAN);
    }
    static const char baseButtons[BASE_COUNT] = {'D', 'A', 'B'};
    for (unsigned i = 0; i < BASE_COUNT; ++i) {
        const int x = baseX[i];
        const uint16_t color = bases[i].alive ? baseColors[i] : COLOR_DARKGRAY;
        if (bases[i].alive) {
            draw_bitmap_scaled(frame, x - BASE_WIDTH / 2, TARGET_TOP, baseBitmap,
                               BASE_WIDTH, TARGET_HEIGHT, BASE_WIDTH, TARGET_HEIGHT, color);
        } else {
            draw_bitmap_scaled(frame, x - DESTROYED_BASE_WIDTH / 2, TARGET_TOP, destroyedBaseBitmap,
                               DESTROYED_BASE_WIDTH, TARGET_HEIGHT,
                               DESTROYED_BASE_WIDTH, TARGET_HEIGHT, color);
        }
        char label[12];
        snprintf(label, sizeof(label), "%c %u", baseButtons[i], bases[i].ammo);
        drawPet2001TextCentredTransparent(frame, x, 161, label, color, 1);
    }
}

void drawMissileCommand(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (gameState == STATE_SCORES) {
        drawHighScoreEntry(frame);
        return;
    }
    if (gameState == STATE_MODE) {
        clear(frame, COLOR_BLACK);
        starfield_draw(frame, &stars);
        drawPet2001TextCentredTransparent(frame, 160, 22, "MISSILE COMMAND", COLOR_CYAN, 2);
        drawPet2001TextCentredTransparent(frame, 160, 62, "SELECT MODE", COLOR_LIGHTGRAY, 2);
        drawPet2001TextCentredTransparent(frame, 100, 96, "PLAY", auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW, 2);
        drawPet2001TextCentredTransparent(frame, 220, 96, "AUTO", auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY, 2);
        drawPet2001TextCentredTransparent(frame, 160, 145, "D/B/STICK CHANGE  A/K START", COLOR_LIGHTGRAY, 1);
        return;
    }
    clear(frame, COLOR_BLACK);
    starfield_draw(frame, &stars);
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        const Missile *missile = &enemies[i];
        if (!missile->active) continue;
        const int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y);
        draw_line(frame, (int)missile->startX, (int)missile->startY, x, y, COLOR_RED);
        fill_rect(frame, x - 1, y - 1, 3, 3, COLOR_WHITE);
    }
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        const Missile *missile = &interceptors[i];
        if (!missile->active) continue;
        const int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y);
        draw_line(frame, (int)missile->startX, (int)missile->startY, x, y, baseColors[missile->target]);
        fill_rect(frame, x - 1, y - 1, 2, 2, COLOR_WHITE);
    }
    for (unsigned i = 0; i < MAX_EXPLOSIONS; ++i) {
        const Explosion *explosion = &explosions[i];
        if (!explosion->active) continue;
        const int x = (int)lroundf(explosion->x), y = (int)lroundf(explosion->y);
        const int radius = (int)explosion->radius;
        fill_circle(frame, x, y, radius, explosion->defensive ? COLOR_DARKORANGE : COLOR_DARKRED);
        draw_ring(frame, x, y, radius, COLOR_YELLOW);
        if (radius > 2) fill_circle(frame, x, y, radius / 2, COLOR_ORANGE);
        if (radius > 4) fill_circle(frame, x, y, radius / 4, COLOR_WHITE);
    }
    drawGround(frame);
    // Mark each in-flight interceptor's fixed destination, above existing blasts.
    for (unsigned i = 0; i < MAX_INTERCEPTORS; ++i) {
        const Missile *missile = &interceptors[i];
        if (!missile->active) continue;
        const int x = (int)lroundf(missile->targetX);
        const int y = (int)lroundf(missile->targetY);
        const uint16_t color = baseColors[missile->target];
        draw_line(frame, x - 2, y - 2, x + 2, y + 2, color);
        draw_line(frame, x - 2, y + 2, x + 2, y - 2, color);
    }
    if (gameState == STATE_PLAYING) {
        const int x = (int)lroundf(aimX), y = (int)lroundf(aimY);
        draw_line(frame, x - 5, y, x - 2, y, COLOR_WHITE);
        draw_line(frame, x + 2, y, x + 5, y, COLOR_WHITE);
        draw_line(frame, x, y - 5, x, y - 2, COLOR_WHITE);
        draw_line(frame, x, y + 2, x, y + 5, COLOR_WHITE);
    }
    fill_rect(frame, 0, 0, LCD_WIDTH, 13, COLOR_BLACK);
    char text[40];
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    drawPet2001TextTransparent(frame, 3, 3, text, COLOR_WHITE, 1);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
    drawPet2001TextCentredTransparent(frame, 183, 3, text, COLOR_YELLOW, 1);
    snprintf(text, sizeof(text), "CITIES %u", livingCities());
    drawPet2001TextTransparent(frame, 256, 3, text, COLOR_CYAN, 1);
    if (gameState == STATE_READY) {
        fill_rect(frame, 9, 22, 302, 109, COLOR_BLACK);
        drawPet2001TextCentredTransparent(frame, 160, 25, "MISSILE COMMAND", COLOR_CYAN, 2);
        drawPet2001TextCentredTransparent(frame, 160, 57, "DEFEND YOUR SIX CITIES", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 73, "JOYSTICK AIM", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 87, "D LEFT   A CENTRE   B RIGHT", COLOR_YELLOW, 1);
        drawPet2001TextCentredTransparent(frame, 160, 123, "BOOT BACK", COLOR_LIGHTGRAY, 1);
    } else if (gameState == STATE_WAVE_CLEAR) {
        fill_rect(frame, 37, 44, 246, 72, COLOR_BLACK);
        draw_rect(frame, 37, 44, 246, 72, COLOR_DARKORANGE);
        drawPet2001TextCentredTransparent(frame, 160, 50, "WAVE CLEAR", COLOR_YELLOW, 2);
        snprintf(text, sizeof(text), "MISSILES %u X 5 +%u", waveMissilesRemaining,
                 (unsigned)waveMissileAwarded);
        drawPet2001TextCentredTransparent(frame, 160, 72, text, COLOR_WHITE, 1);
        if (waveTallyElapsed >= WAVE_TALLY_MISSILES_MS) {
            snprintf(text, sizeof(text), "CITIES %u X 100 +%u", waveCitiesRemaining,
                     (unsigned)waveCityAwarded);
            drawPet2001TextCentredTransparent(frame, 160, 87, text, COLOR_CYAN, 1);
        }
        if (waveTallyElapsed >= WAVE_TALLY_MISSILES_MS + WAVE_TALLY_CITIES_MS) {
            snprintf(text, sizeof(text), "WAVE BONUS +%u", (unsigned)waveBonus);
            drawPet2001TextCentredTransparent(frame, 160, 102, text, COLOR_YELLOW, 1);
        }
    } else if (gameState == STATE_FINISHED) {
        drawPet2001TextCentredTransparent(frame, 160, 78, "GAME OVER", COLOR_RED, 2);
    }
    if (auto_mode) {
        if (gameState == STATE_FINISHED) {
            const uint32_t delay = aiRestartDelay();
            const uint32_t elapsed = lastUpdate - stateStarted;
            snprintf(text, sizeof(text), "NEXT %u  E SPEED  A/K STOP",
                     elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U));
        } else {
            snprintf(text, sizeof(text), "AUTO %s  E SPEED  A/K STOP", aiSpeedShortText(ai_speed));
        }
        fill_rect(frame, 44, 15, 232, 11, COLOR_BLACK);
        drawPet2001TextCentredTransparent(frame, 160, 16, text, COLOR_LIGHTGRAY, 1);
    }
}