Defender

Source: src/defender.c · 435 lines · 24 functions.

Screen captures

Views of this part running on the Mini Arcade.

Defender game screen on the Mini Arcade
Defender showing the scrolling playfield and status display.

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

clampf() · source line 69
static float clampf(float value, float low, float high)
  1. Exit this functionreturn value < low ? low : value > high ? high : value;
Read original function source
static float clampf(float value, float low, float high)
{
    return value < low ? low : value > high ? high : value;
}
wrapWorld() · source line 74
static float wrapWorld(float x)
  1. WHILE loopx < 0.0f

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

    1. Executex += WORLD_WIDTH;
  2. WHILE loopx >= WORLD_WIDTH

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

    1. Executex -= WORLD_WIDTH;
  3. Exit this functionreturn x;
Read original function source
static float wrapWorld(float x)
{
    while (x < 0.0f) x += WORLD_WIDTH;
    while (x >= WORLD_WIDTH) x -= WORLD_WIDTH;
    return x;
}
worldDelta() · source line 81
static float worldDelta(float from, float to)
  1. Executefloat delta = to - from;
  2. Decisiondelta > WORLD_WIDTH / 2.0f
    True
    1. Executedelta -= WORLD_WIDTH;
    False

    Continue after this decision.

  3. Decisiondelta < -WORLD_WIDTH / 2.0f
    True
    1. Executedelta += WORLD_WIDTH;
    False

    Continue after this decision.

  4. Exit this functionreturn delta;
Read original function source
static float worldDelta(float from, float to)
{
    float delta = to - from;
    if (delta > WORLD_WIDTH / 2.0f) delta -= WORLD_WIDTH;
    if (delta < -WORLD_WIDTH / 2.0f) delta += WORLD_WIDTH;
    return delta;
}
addScore() · source line 89
static void addScore(uint32_t points)
  1. Executescore = score > UINT32_MAX - points ? UINT32_MAX : score + points;
  2. Executeconst uint8_t next_wave = (uint8_t)(score / 1500U + 1U);
  3. Decisionnext_wave > wave
    True
    1. Executewave = next_wave;
    False

    Continue after this decision.

Read original function source
static void addScore(uint32_t points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
    const uint8_t next_wave = (uint8_t)(score / 1500U + 1U);
    if (next_wave > wave) wave = next_wave;
}
groundHeight() · source line 96
static float groundHeight(float world_x)
  1. Executeconst float phase = world_x * 0.030f;
  2. Exit this functionreturn GROUND_Y - 9.0f * sinf(phase) - 5.0f * sinf(phase * 2.7f);
Read original function source
static float groundHeight(float world_x)
{
    const float phase = world_x * 0.030f;
    return GROUND_Y - 9.0f * sinf(phase) - 5.0f * sinf(phase * 2.7f);
}
nearestHuman() · source line 102
static int nearestHuman(float x)

Calls into this documentation:

  1. Executeint found = -1;
  2. Executefloat distance = WORLD_WIDTH;
  3. FOR loopunsigned i = 0; i < HUMANOID_COUNT; ++i

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

      1. Decisionhumans[i].state != HUMAN_GROUND
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst float candidate = fabsf(worldDelta(x, humans[i].x));
      3. Decisioncandidate < distance
        True
          1. Executedistance = candidate;
          2. Executefound = (int)i;
        False

        Continue after this decision.

  4. Exit this functionreturn found;
Read original function source
static int nearestHuman(float x)
{
    int found = -1;
    float distance = WORLD_WIDTH;
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        if (humans[i].state != HUMAN_GROUND) continue;
        const float candidate = fabsf(worldDelta(x, humans[i].x));
        if (candidate < distance) { distance = candidate; found = (int)i; }
    }
    return found;
}
spawnEnemy() · source line 114
static void spawnEnemy(EnemyType type)

Calls into this documentation:

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

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

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

        Continue after this decision.

      2. Executeconst float x = wrapWorld(player.x + (esp_random() % 2U ? 190.0f : -190.0f));
      3. Executeconst int human = type == LANDER ? nearestHuman(x) : -1;
      4. Executeenemies[i] = (Enemy){.object = {x, 28.0f + esp_random() % 55U, 0.0f, 0.0f, true}, .type = type, .human = human};
      5. Exit this functionreturn;
Read original function source
static void spawnEnemy(EnemyType type)
{
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (enemies[i].object.active) continue;
        const float x = wrapWorld(player.x + (esp_random() % 2U ? 190.0f : -190.0f));
        const int human = type == LANDER ? nearestHuman(x) : -1;
        enemies[i] = (Enemy){.object = {x, 28.0f + esp_random() % 55U, 0.0f, 0.0f, true},
                             .type = type, .human = human};
        return;
    }
}
resetWorld() · source line 126
static void resetWorld(void)

Calls into this documentation:

  1. Executememset(shots, 0, sizeof(shots));
  2. Executememset(shot_distance, 0, sizeof(shot_distance));
  3. Executememset(enemies, 0, sizeof(enemies));
  4. FOR loopunsigned i = 0; i < HUMANOID_COUNT; ++i

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

      1. Executehumans[i] = (Humanoid){(i + 1U) * WORLD_WIDTH / (HUMANOID_COUNT + 1U), 0.0f, HUMAN_GROUND};
      2. Executehumans[i].y = groundHeight(humans[i].x) - 8.0f;
  5. FOR loopunsigned i = 0; i < 4; ++i

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

    1. ExecutespawnEnemy(LANDER);
Read original function source
static void resetWorld(void)
{
    memset(shots, 0, sizeof(shots));
    memset(shot_distance, 0, sizeof(shot_distance));
    memset(enemies, 0, sizeof(enemies));
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        humans[i] = (Humanoid){(i + 1U) * WORLD_WIDTH / (HUMANOID_COUNT + 1U), 0.0f, HUMAN_GROUND};
        humans[i].y = groundHeight(humans[i].x) - 8.0f;
    }
    for (unsigned i = 0; i < 4; ++i) spawnEnemy(LANDER);
}
beginGame() · source line 138
static void beginGame(uint32_t now_ms)

Calls into this documentation:

  1. Executescore = 0;
  2. Executewave = 1;
  3. Executelives = 3;
  4. Executesmart_bombs = 3;
  5. Executefacing = 1;
  6. Executereverse_lag = 0;
  7. Executeplayer = (Object){WORLD_WIDTH / 2.0f, 75.0f, 0.0f, 0.0f, true};
  8. Executefire_delay = bomb_delay = 0;
  9. Executespawn_delay = 1000U;
  10. Executeaccumulator = 0;
  11. Executecontrols_ready = false;
  12. Executesmart_bomb_held = false;
  13. Executethrust_x_ms = thrust_y_ms = 0;
  14. Executethrust_x_direction = thrust_y_direction = 0;
  15. ExecuteresetWorld();
  16. Executestate = PLAYING;
  17. Executelast_update = now_ms;
Read original function source
static void beginGame(uint32_t now_ms)
{
    score = 0; wave = 1; lives = 3; smart_bombs = 3; facing = 1; reverse_lag = 0;
    player = (Object){WORLD_WIDTH / 2.0f, 75.0f, 0.0f, 0.0f, true};
    fire_delay = bomb_delay = 0; spawn_delay = 1000U; accumulator = 0;
    controls_ready = false; smart_bomb_held = false;
    thrust_x_ms = thrust_y_ms = 0; thrust_x_direction = thrust_y_direction = 0;
    resetWorld();
    state = PLAYING;
    last_update = now_ms;
}
hitPlayer() · source line 150
static void hitPlayer(void)

Calls into this documentation:

  1. Decision!player.active
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. ExecuteaudioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
  3. Decision--lives == 0
    True
      1. Executeplayer.active = false;
      2. Executestate = FINISHED;
      3. Executestate_started = last_update;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  4. Executeplayer.active = false;
  5. Executestate_started = last_update;
  6. Executememset(shots, 0, sizeof(shots));
Read original function source
static void hitPlayer(void)
{
    if (!player.active) return;
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (--lives == 0) { player.active = false; state = FINISHED; state_started = last_update; return; }
    player.active = false;
    state_started = last_update;
    memset(shots, 0, sizeof(shots));
}
fireShot() · source line 160
static void fireShot(void)

Calls into this documentation:

  1. Decision!player.active || fire_delay != 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

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

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

    1. Decision!shots[i].active
      True
        1. Executeshots[i] = (Object){player.x + facing * 10.0f, player.y + 4.0f, facing * 250.0f, 0.0f, true};
        2. Executeshot_distance[i] = 0.0f;
        3. Executefire_delay = FIRE_DELAY_MS;
        4. ExecuteaudioPlay(AUDIO_SFX_PLAYER_SHOT);
        5. Exit this functionreturn;
      False

      Continue after this decision.

Read original function source
static void fireShot(void)
{
    if (!player.active || fire_delay != 0) return;
    for (unsigned i = 0; i < MAX_SHOTS; ++i) if (!shots[i].active) {
        shots[i] = (Object){player.x + facing * 10.0f, player.y + 4.0f, facing * 250.0f, 0.0f, true};
        shot_distance[i] = 0.0f;
        fire_delay = FIRE_DELAY_MS;
        audioPlay(AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}
releaseLanderHuman() · source line 172
static void releaseLanderHuman(Enemy *enemy)

Calls into this documentation:

  1. Decisionenemy->human < 0 || humans[enemy->human].state != HUMAN_ABDUCTED
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. ExecuteHumanoid *human = &humans[enemy->human];
  3. Executehuman->x = enemy->object.x;
  4. Executehuman->y = enemy->object.y + 10.0f;
  5. DecisiongroundHeight(human->x) - human->y > 16.0f
    True
    1. Executehuman->state = HUMAN_FALLING;
    False
      1. Executehuman->y = groundHeight(human->x) - 8.0f;
      2. Executehuman->state = HUMAN_GROUND;
  6. Executeenemy->human = -1;
Read original function source
static void releaseLanderHuman(Enemy *enemy)
{
    if (enemy->human < 0 || humans[enemy->human].state != HUMAN_ABDUCTED) return;
    Humanoid *human = &humans[enemy->human];
    human->x = enemy->object.x;
    human->y = enemy->object.y + 10.0f;
    if (groundHeight(human->x) - human->y > 16.0f) human->state = HUMAN_FALLING;
    else {
        human->y = groundHeight(human->x) - 8.0f;
        human->state = HUMAN_GROUND;
    }
    enemy->human = -1;
}
smartBomb() · source line 186
static void smartBomb(void)

Calls into this documentation:

  1. Decisionbomb_delay != 0 || smart_bombs == 0 || !player.active
    True
    1. Exit this functionreturn;
    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].object.active
      True
        1. Executeconst float x = screenX(enemies[i].object.x);
        2. Decisionx < -16.0f || x >= LCD_WIDTH + 16.0f
          True
          1. Next loop iterationcontinue;
          False

          Continue after this decision.

        3. ExecutereleaseLanderHuman(&enemies[i]);
        4. Executeenemies[i].object.active = false;
        5. ExecuteaddScore(enemies[i].type == MUTANT ? 250U : 150U);
      False

      Continue after this decision.

  3. Execute--smart_bombs;
  4. Executebomb_delay = SMART_BOMB_DELAY_MS;
  5. ExecuteaudioPlay(AUDIO_SFX_EXPLOSION);
Read original function source
static void smartBomb(void)
{
    if (bomb_delay != 0 || smart_bombs == 0 || !player.active) return;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].object.active) {
        const float x = screenX(enemies[i].object.x);
        if (x < -16.0f || x >= LCD_WIDTH + 16.0f) continue;
        releaseLanderHuman(&enemies[i]);
        enemies[i].object.active = false;
        addScore(enemies[i].type == MUTANT ? 250U : 150U);
    }
    --smart_bombs;
    bomb_delay = SMART_BOMB_DELAY_MS;
    audioPlay(AUDIO_SFX_EXPLOSION);
}
updateEnemy() · source line 201
static void updateEnemy(Enemy *enemy)

Calls into this documentation:

  1. ExecuteObject *object = &enemy->object;
  2. Decision!object->active
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeconst float dx = worldDelta(object->x, player.x);
  4. Decisionenemy->type == LANDER
    True
      1. Decisionenemy->human >= 0 && humans[enemy->human].state == HUMAN_GROUND
        True
          1. Executeconst float target_x = humans[enemy->human].x;
          2. Executeobject->x = wrapWorld(object->x + clampf(worldDelta(object->x, target_x), -35.0f, 35.0f) * STEP_MS / 1000.0f);
          3. Executeconst float target_y = humans[enemy->human].y - 8.0f;
          4. Executeobject->y += clampf(target_y - object->y, -24.0f, 24.0f) * STEP_MS / 1000.0f;
          5. Decisionfabsf(worldDelta(object->x, target_x)) < 5.0f && fabsf(object->y - target_y) < 5.0f
            True
              1. Executehumans[enemy->human].state = HUMAN_ABDUCTED;
            False

            Continue after this decision.

        False
        1. Decisionenemy->human >= 0 && humans[enemy->human].state == HUMAN_ABDUCTED
          True
            1. Executehumans[enemy->human].x = object->x;
            2. Executehumans[enemy->human].y = object->y + 10.0f;
            3. Executeobject->y -= (30.0f + wave * 2.0f) * STEP_MS / 1000.0f;
            4. Decisionobject->y < PLAY_TOP
              True
                1. Executehumans[enemy->human].state = HUMAN_LOST;
                2. Executeenemy->type = MUTANT;
                3. Executeenemy->human = -1;
                4. Executeobject->y = PLAY_TOP + 4;
              False

              Continue after this decision.

          False
            1. Executeobject->x = wrapWorld(object->x + (dx > 0 ? 20.0f : -20.0f) * STEP_MS / 1000.0f);
    False
      1. Executeconst float speed = enemy->type == MUTANT ? 58.0f + wave * 4.0f : 35.0f + wave * 2.0f;
      2. Executeobject->x = wrapWorld(object->x + clampf(dx, -speed, speed) * STEP_MS / 1000.0f);
      3. Executeobject->y += clampf(player.y - object->y, -speed * 0.45f, speed * 0.45f) * STEP_MS / 1000.0f;
  5. Decisionfabsf(dx) < 12.0f && fabsf(object->y - player.y) < 12.0f
    True
    1. ExecutehitPlayer();
    False

    Continue after this decision.

Read original function source
static void updateEnemy(Enemy *enemy)
{
    Object *object = &enemy->object;
    if (!object->active) return;
    const float dx = worldDelta(object->x, player.x);
    if (enemy->type == LANDER) {
        if (enemy->human >= 0 && humans[enemy->human].state == HUMAN_GROUND) {
            const float target_x = humans[enemy->human].x;
            object->x = wrapWorld(object->x + clampf(worldDelta(object->x, target_x), -35.0f, 35.0f) * STEP_MS / 1000.0f);
            const float target_y = humans[enemy->human].y - 8.0f;
            object->y += clampf(target_y - object->y, -24.0f, 24.0f) * STEP_MS / 1000.0f;
            if (fabsf(worldDelta(object->x, target_x)) < 5.0f && fabsf(object->y - target_y) < 5.0f) {
                humans[enemy->human].state = HUMAN_ABDUCTED;
            }
        } else if (enemy->human >= 0 && humans[enemy->human].state == HUMAN_ABDUCTED) {
            humans[enemy->human].x = object->x;
            humans[enemy->human].y = object->y + 10.0f;
            object->y -= (30.0f + wave * 2.0f) * STEP_MS / 1000.0f;
            if (object->y < PLAY_TOP) {
                humans[enemy->human].state = HUMAN_LOST;
                enemy->type = MUTANT;
                enemy->human = -1;
                object->y = PLAY_TOP + 4;
            }
        } else {
            object->x = wrapWorld(object->x + (dx > 0 ? 20.0f : -20.0f) * STEP_MS / 1000.0f);
        }
    } else {
        const float speed = enemy->type == MUTANT ? 58.0f + wave * 4.0f : 35.0f + wave * 2.0f;
        object->x = wrapWorld(object->x + clampf(dx, -speed, speed) * STEP_MS / 1000.0f);
        object->y += clampf(player.y - object->y, -speed * 0.45f, speed * 0.45f) * STEP_MS / 1000.0f;
    }
    if (fabsf(dx) < 12.0f && fabsf(object->y - player.y) < 12.0f) hitPlayer();
}
updateHumanRescue() · source line 237
static void updateHumanRescue(void)

Calls into this documentation:

  1. Executeint carried = -1;
  2. FOR loopunsigned i = 0; i < HUMANOID_COUNT; ++i

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

      1. Decisionhumans[i].state == HUMAN_CARRIED
        True
          1. Executecarried = (int)i;
          2. Exit enclosing loop or switchbreak;
        False

        Continue after this decision.

  3. Decisioncarried < 0
    True
      1. FOR loopunsigned i = 0; i < HUMANOID_COUNT; ++i

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

          1. Decision(humans[i].state != HUMAN_ABDUCTED && humans[i].state != HUMAN_FALLING) || fabsf(worldDelta(player.x, humans[i].x)) >= 10.0f || fabsf((player.y + PLAYER_H) - humans[i].y) >= 12.0f
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

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

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

              1. Decisionenemies[e].human == (int)i
                True
                1. Executeenemies[e].human = -1;
                False

                Continue after this decision.

          3. Executehumans[i].state = HUMAN_CARRIED;
          4. Executecarried = (int)i;
          5. Exit enclosing loop or switchbreak;
    False

    Continue after this decision.

  4. Decisioncarried < 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  5. ExecuteHumanoid *human = &humans[carried];
  6. Executehuman->x = player.x;
  7. Executehuman->y = player.y + PLAYER_H;
  8. Decisionhuman->y >= groundHeight(player.x) - 1.0f
    True
      1. Executehuman->y = groundHeight(player.x) - 8.0f;
      2. Executehuman->state = HUMAN_GROUND;
      3. ExecuteaddScore(500U);
    False

    Continue after this decision.

Read original function source
static void updateHumanRescue(void)
{
    int carried = -1;
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        if (humans[i].state == HUMAN_CARRIED) { carried = (int)i; break; }
    }

    /* Catch an abducted human by flying beneath it. Only one can be carried. */
    if (carried < 0) {
        for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
            if ((humans[i].state != HUMAN_ABDUCTED && humans[i].state != HUMAN_FALLING) ||
                fabsf(worldDelta(player.x, humans[i].x)) >= 10.0f ||
                fabsf((player.y + PLAYER_H) - humans[i].y) >= 12.0f) continue;

            for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
                if (enemies[e].human == (int)i) enemies[e].human = -1;
            }
            humans[i].state = HUMAN_CARRIED;
            carried = (int)i;
            break;
        }
    }

    if (carried < 0) return;
    Humanoid *human = &humans[carried];
    human->x = player.x;
    human->y = player.y + PLAYER_H;

    /* Landing near the terrain safely returns the human and awards rescue points. */
    if (human->y >= groundHeight(player.x) - 1.0f) {
        human->y = groundHeight(player.x) - 8.0f;
        human->state = HUMAN_GROUND;
        addScore(500U);
    }
}
updateFallingHumans() · source line 273
static void updateFallingHumans(void)

Calls into this documentation:

  1. FOR loopunsigned i = 0; i < HUMANOID_COUNT; ++i

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

      1. ExecuteHumanoid *human = &humans[i];
      2. Decisionhuman->state != HUMAN_FALLING
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executehuman->y += 75.0f * STEP_MS / 1000.0f;
      4. Decisionhuman->y >= groundHeight(human->x) - 8.0f
        True
          1. Executehuman->y = groundHeight(human->x) - 8.0f;
          2. Executehuman->state = HUMAN_GROUND;
        False

        Continue after this decision.

Read original function source
static void updateFallingHumans(void)
{
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        Humanoid *human = &humans[i];
        if (human->state != HUMAN_FALLING) continue;
        human->y += 75.0f * STEP_MS / 1000.0f;
        if (human->y >= groundHeight(human->x) - 8.0f) {
            human->y = groundHeight(human->x) - 8.0f;
            human->state = HUMAN_GROUND;
        }
    }
}
updateThrustRamp() · source line 286
static uint16_t updateThrustRamp(float input, int *direction, uint16_t held_ms)
  1. Executeconst int new_direction = input > 0.15f ? 1 : input < -0.15f ? -1 : 0;
  2. Decisionnew_direction == 0
    True
      1. Execute*direction = 0;
      2. Exit this functionreturn 0;
    False

    Continue after this decision.

  3. Decisionnew_direction != *direction
    True
      1. Execute*direction = new_direction;
      2. Exit this functionreturn STEP_MS;
    False

    Continue after this decision.

  4. Exit this functionreturn held_ms + STEP_MS > THRUST_RAMP_MS ? THRUST_RAMP_MS : held_ms + STEP_MS;
Read original function source
static uint16_t updateThrustRamp(float input, int *direction, uint16_t held_ms)
{
    const int new_direction = input > 0.15f ? 1 : input < -0.15f ? -1 : 0;
    if (new_direction == 0) { *direction = 0; return 0; }
    if (new_direction != *direction) { *direction = new_direction; return STEP_MS; }
    return held_ms + STEP_MS > THRUST_RAMP_MS ? THRUST_RAMP_MS : held_ms + STEP_MS;
}
stepGame() · source line 294
static void stepGame(float input_x, float input_y, bool fire, bool smart_bomb)

Calls into this documentation:

  1. Executefire_delay = fire_delay > STEP_MS ? fire_delay - STEP_MS : 0;
  2. Executebomb_delay = bomb_delay > STEP_MS ? bomb_delay - STEP_MS : 0;
  3. Executereverse_lag = reverse_lag > STEP_MS ? reverse_lag - STEP_MS : 0;
  4. Decision!player.active
    True
      1. Decisionlast_update - state_started >= RESPAWN_MS
        True
        1. Executeplayer = (Object){WORLD_WIDTH / 2.0f, 75.0f, 0, 0, true};
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executethrust_x_ms = updateThrustRamp(input_x, &thrust_x_direction, thrust_x_ms);
  6. Executethrust_y_ms = updateThrustRamp(input_y, &thrust_y_direction, thrust_y_ms);
  7. Executeconst float x_acceleration = reverse_lag ? 0.0f : 100.0f + 180.0f * (float)thrust_x_ms / THRUST_RAMP_MS;
  8. Executeconst float y_acceleration = reverse_lag ? 0.0f : 100.0f + 180.0f * (float)thrust_y_ms / THRUST_RAMP_MS;
  9. Executeplayer.vx += input_x * x_acceleration * STEP_MS / 1000.0f;
  10. Executeplayer.vy += input_y * y_acceleration * STEP_MS / 1000.0f;
  11. Executeplayer.vx *= 0.975f;
  12. Executeplayer.vy *= 0.975f;
  13. Executeplayer.vx = clampf(player.vx, -125.0f, 125.0f);
  14. Executeplayer.vy = clampf(player.vy, -95.0f, 95.0f);
  15. Executeplayer.x = wrapWorld(player.x + player.vx * STEP_MS / 1000.0f);
  16. Executeplayer.y = clampf(player.y + player.vy * STEP_MS / 1000.0f, PLAY_TOP + 4.0f, groundHeight(player.x) - PLAYER_H);
  17. Executecamera_x = player.x - LCD_WIDTH / 2.0f;
  18. ExecuteupdateFallingHumans();
  19. ExecuteupdateHumanRescue();
  20. Decisionfire
    True
    1. ExecutefireShot();
    False

    Continue after this decision.

  21. Decisionsmart_bomb
    True
    1. ExecutesmartBomb();
    False

    Continue after this decision.

  22. FOR loopunsigned i = 0; i < MAX_SHOTS; ++i

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

      1. ExecuteObject *shot = &shots[i];
      2. Decision!shot->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeshot->x = wrapWorld(shot->x + shot->vx * STEP_MS / 1000.0f);
      4. Executeshot_distance[i] += fabsf(shot->vx) * STEP_MS / 1000.0f;
      5. Decisionshot_distance[i] > 320.0f
        True
          1. Executeshot->active = false;
          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      6. FOR loopunsigned e = 0; e < MAX_ENEMIES; ++e

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

          1. ExecuteEnemy *enemy = &enemies[e];
          2. Decisionenemy->object.active && fabsf(worldDelta(shot->x, enemy->object.x)) < 10.0f && fabsf(shot->y - enemy->object.y) < 9.0f
            True
              1. ExecutereleaseLanderHuman(enemy);
              2. Executeenemy->object.active = false;
              3. Executeshot->active = false;
              4. ExecuteaddScore(enemy->type == MUTANT ? 250U : enemy->type == BOMBER ? 150U : 150U);
              5. ExecuteaudioPlay(AUDIO_SFX_HIT);
              6. Exit enclosing loop or switchbreak;
            False

            Continue after this decision.

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

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

    1. ExecuteupdateEnemy(&enemies[i]);
  24. Executespawn_delay = spawn_delay > STEP_MS ? spawn_delay - STEP_MS : 0;
  25. Decisionspawn_delay == 0
    True
      1. ExecutespawnEnemy((esp_random() % 8U) == 0 ? BOMBER : LANDER);
      2. Executespawn_delay = 900U - (wave > 5U ? 250U : wave * 50U);
    False

    Continue after this decision.

Read original function source
static void stepGame(float input_x, float input_y, bool fire, bool smart_bomb)
{
    fire_delay = fire_delay > STEP_MS ? fire_delay - STEP_MS : 0;
    bomb_delay = bomb_delay > STEP_MS ? bomb_delay - STEP_MS : 0;
    reverse_lag = reverse_lag > STEP_MS ? reverse_lag - STEP_MS : 0;
    if (!player.active) { if (last_update - state_started >= RESPAWN_MS) player = (Object){WORLD_WIDTH / 2.0f, 75.0f, 0, 0, true}; return; }
    thrust_x_ms = updateThrustRamp(input_x, &thrust_x_direction, thrust_x_ms);
    thrust_y_ms = updateThrustRamp(input_y, &thrust_y_direction, thrust_y_ms);
    const float x_acceleration = reverse_lag ? 0.0f : 100.0f + 180.0f * (float)thrust_x_ms / THRUST_RAMP_MS;
    const float y_acceleration = reverse_lag ? 0.0f : 100.0f + 180.0f * (float)thrust_y_ms / THRUST_RAMP_MS;
    player.vx += input_x * x_acceleration * STEP_MS / 1000.0f;
    player.vy += input_y * y_acceleration * STEP_MS / 1000.0f;
    player.vx *= 0.975f; player.vy *= 0.975f;
    player.vx = clampf(player.vx, -125.0f, 125.0f);
    player.vy = clampf(player.vy, -95.0f, 95.0f);
    player.x = wrapWorld(player.x + player.vx * STEP_MS / 1000.0f);
    player.y = clampf(player.y + player.vy * STEP_MS / 1000.0f, PLAY_TOP + 4.0f, groundHeight(player.x) - PLAYER_H);
    camera_x = player.x - LCD_WIDTH / 2.0f;
    updateFallingHumans();
    updateHumanRescue();
    if (fire) fireShot();
    if (smart_bomb) smartBomb();
    for (unsigned i = 0; i < MAX_SHOTS; ++i) {
        Object *shot = &shots[i]; if (!shot->active) continue;
        shot->x = wrapWorld(shot->x + shot->vx * STEP_MS / 1000.0f);
        shot_distance[i] += fabsf(shot->vx) * STEP_MS / 1000.0f;
        if (shot_distance[i] > 320.0f) { shot->active = false; continue; }
        for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
            Enemy *enemy = &enemies[e];
            if (enemy->object.active && fabsf(worldDelta(shot->x, enemy->object.x)) < 10.0f && fabsf(shot->y - enemy->object.y) < 9.0f) {
                releaseLanderHuman(enemy);
                enemy->object.active = false; shot->active = false;
                addScore(enemy->type == MUTANT ? 250U : enemy->type == BOMBER ? 150U : 150U);
                audioPlay(AUDIO_SFX_HIT);
                break;
            }
        }
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) updateEnemy(&enemies[i]);
    spawn_delay = spawn_delay > STEP_MS ? spawn_delay - STEP_MS : 0;
    if (spawn_delay == 0) { spawnEnemy((esp_random() % 8U) == 0 ? BOMBER : LANDER); spawn_delay = 900U - (wave > 5U ? 250U : wave * 50U); }
}
startDefender() · source line 337
void startDefender(uint32_t now_ms)

Calls into this documentation:

  1. Executerunning = true;
  2. ExecutebeginGame(now_ms);
  3. Executestate = READY;
Read original function source
void startDefender(uint32_t now_ms) { running = true; beginGame(now_ms); state = READY; }
stopDefender() · source line 338
void stopDefender(void)

Calls into this documentation:

  1. ExecutecancelHighScoreEntry();
  2. Executerunning = false;
Read original function source
void stopDefender(void) { cancelHighScoreEntry(); running = false; }
defenderAction() · source line 339
void defenderAction(bool start, bool reverse, uint32_t now_ms)

Calls into this documentation:

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

    Continue after this decision.

  2. Decisionstart && state == READY
    True
      1. ExecutebeginGame(now_ms);
      2. ExecuteaudioPlay(AUDIO_SFX_GAME_START);
    False

    Continue after this decision.

  3. Decisionreverse && state == PLAYING && player.active
    True
      1. Executefacing = -facing;
      2. Executeplayer.vx *= 0.30f;
      3. Executereverse_lag = REVERSE_LAG_MS;
      4. Executethrust_x_ms = thrust_y_ms = 0;
      5. Executethrust_x_direction = thrust_y_direction = 0;
    False

    Continue after this decision.

Read original function source
void defenderAction(bool start, bool reverse, uint32_t now_ms)
{
    if (!running) return;
    if (start && state == READY) {
        beginGame(now_ms);
        audioPlay(AUDIO_SFX_GAME_START);
    }
    if (reverse && state == PLAYING && player.active) {
        facing = -facing; player.vx *= 0.30f; reverse_lag = REVERSE_LAG_MS;
        thrust_x_ms = thrust_y_ms = 0; thrust_x_direction = thrust_y_direction = 0;
    }
}
updateDefender() · source line 352
void updateDefender(int x_offset, int y_offset, bool valid, bool fire, bool smart_bomb, uint32_t now_ms)

Calls into this documentation:

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

    Continue after this decision.

  2. Executeconst uint32_t elapsed = now_ms - last_update;
  3. Executelast_update = now_ms;
  4. Decisionstate == FINISHED && now_ms - state_started >= 1000U
    True
      1. ExecutestartHighScoreEntry(HIGH_SCORE_DEFENDER, score, now_ms);
      2. Executestate = SCORES;
    False

    Continue after this decision.

  5. Decisionstate == SCORES
    True
      1. DecisionupdateHighScoreEntry(x_offset, y_offset, valid, now_ms)
        True
        1. Executestate = READY;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisionstate != PLAYING
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

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

      Continue after this decision.

  8. Executeconst bool smart_bomb_pressed = smart_bomb && !smart_bomb_held;
  9. Executesmart_bomb_held = smart_bomb;
  10. Executeconst float input_x = valid && controls_ready ? clampf((float)x_offset / 1600.0f, -1.0f, 1.0f) : 0.0f;
  11. Executeconst float input_y = valid && controls_ready ? clampf((float)-y_offset / 1600.0f, -1.0f, 1.0f) : 0.0f;
  12. Executeaccumulator += elapsed > 100U ? 100U : elapsed;
  13. WHILE loopaccumulator >= STEP_MS && state == PLAYING

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

      1. Executeaccumulator -= STEP_MS;
      2. ExecutestepGame(input_x, input_y, fire, smart_bomb_pressed);
Read original function source
void updateDefender(int x_offset, int y_offset, bool valid, bool fire, bool smart_bomb, uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update; last_update = now_ms;
    if (state == FINISHED && now_ms - state_started >= 1000U) { startHighScoreEntry(HIGH_SCORE_DEFENDER, score, now_ms); state = SCORES; }
    if (state == SCORES) { if (updateHighScoreEntry(x_offset, y_offset, valid, now_ms)) state = READY; return; }
    if (state != PLAYING) return;
    if (!valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350) controls_ready = true;
    const bool smart_bomb_pressed = smart_bomb && !smart_bomb_held;
    smart_bomb_held = smart_bomb;
    const float input_x = valid && controls_ready ? clampf((float)x_offset / 1600.0f, -1.0f, 1.0f) : 0.0f;
    const float input_y = valid && controls_ready ? clampf((float)-y_offset / 1600.0f, -1.0f, 1.0f) : 0.0f;
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == PLAYING) { accumulator -= STEP_MS; stepGame(input_x, input_y, fire, smart_bomb_pressed); }
}
screenX() · source line 369
static float screenX(float world_x)

Calls into this documentation:

  1. Executefloat x = worldDelta(camera_x, world_x);
  2. Decisionx < -20.0f
    True
    1. Executex += WORLD_WIDTH;
    False

    Continue after this decision.

  3. Exit this functionreturn x;
Read original function source
static float screenX(float world_x)
{
    float x = worldDelta(camera_x, world_x);
    if (x < -20.0f) x += WORLD_WIDTH;
    return x;
}
drawDefender() · source line 376
void drawDefender(uint16_t *frame)

Calls into this documentation:

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

    Continue after this decision.

  2. Decisionstate == SCORES
    True
      1. ExecutedrawHighScoreEntry(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeclear(frame, COLOR_BLACK);
  4. FOR loopint x = 0; x < LCD_WIDTH; ++x

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

      1. Executeconst int ground = (int)groundHeight(camera_x + x);
      2. Executefill_rect(frame, x, ground, 1, LCD_HEIGHT - ground, COLOR_DARKGREEN);
      3. Executefill_rect(frame, x, ground, 1, 2, COLOR_LIME);
  5. FOR loopunsigned i = 0; i < HUMANOID_COUNT; ++i

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

      1. Decisionhumans[i].state == HUMAN_LOST
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst int x = (int)screenX(humans[i].x);
      3. Executeconst uint16_t colour = humans[i].state == HUMAN_CARRIED ? COLOR_CYAN : humans[i].state == HUMAN_FALLING ? COLOR_WHITE : humans[i].state == HUMAN_ABDUCTED ? COLOR_ORANGE : COLOR_YELLOW;
      4. Decisionx >= -8 && x < LCD_WIDTH
        True
        1. Executedraw_bitmap_scaled(frame, x - 4, (int)humans[i].y, defender_humanoid_bitmap, 8, 8, 8, 10, colour);
        False

        Continue after this decision.

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

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

    1. Decisionenemies[i].object.active
      True
        1. Executeconst Enemy *enemy = &enemies[i];
        2. Executeconst int x = (int)screenX(enemy->object.x);
        3. Executeconst uint8_t *bitmap = enemy->type == LANDER ? defender_lander_bitmap : enemy->type == MUTANT ? defender_mutant_bitmap : defender_bomber_bitmap;
        4. Executeconst uint16_t colour = enemy->type == LANDER ? COLOR_MAGENTA : enemy->type == MUTANT ? COLOR_RED : COLOR_ORANGE;
        5. Executedraw_bitmap_scaled(frame, x - 8, (int)enemy->object.y, bitmap, 8, 8, 16, 12, colour);
      False

      Continue after this decision.

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

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

    1. Decisionshots[i].active
      True
        1. Executeconst int x = (int)screenX(shots[i].x);
        2. Executeconst int y = (int)shots[i].y + 4;
        3. Executeconst int direction = shots[i].vx < 0.0f ? -1 : 1;
        4. Decisionx < -18 || x >= LCD_WIDTH + 18
          True
          1. Next loop iterationcontinue;
          False

          Continue after this decision.

        5. Executedraw_line(frame, x - direction * 31, y, x - direction * 4, y, COLOR_DARKCYAN);
        6. Executefill_rect(frame, x - (direction < 0 ? 4 : 0), y - 1, 5, 3, COLOR_WHITE);
        7. Executefill_rect(frame, x - direction * 36, y, 2, 1, COLOR_CYAN);
        8. Executefill_rect(frame, x - direction * 44, y, 2, 1, COLOR_BLUE);
      False

      Continue after this decision.

  8. Decisionplayer.active
    True
    1. Executedraw_bitmap_scaled(frame, 152, (int)player.y, facing > 0 ? defender_ship_right : defender_ship_left, 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    False

    Continue after this decision.

  9. Executefill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
  10. FOR loopunsigned i = 0; i < HUMANOID_COUNT; ++i

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

      1. Executeconst int x = (int)(humans[i].x * LCD_WIDTH / WORLD_WIDTH);
      2. Executeconst uint16_t colour = humans[i].state == HUMAN_LOST ? COLOR_RED : humans[i].state == HUMAN_CARRIED ? COLOR_CYAN : humans[i].state == HUMAN_FALLING ? COLOR_WHITE : humans[i].state == HUMAN_ABDUCTED ? COLOR_ORANGE : COLOR_YELLOW;
      3. Executefill_rect(frame, x, 2, 2, 4, colour);
  11. Executechar text[32];
  12. Executesnprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
  13. Executedraw_text(frame, 3, 7, text, 1, COLOR_WHITE);
  14. Executesnprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
  15. Executedraw_text_centred(frame, 164, 7, text, 1, COLOR_YELLOW);
  16. Executesnprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
  17. Executedraw_text(frame, 205, 7, text, 1, COLOR_CYAN);
  18. Executesnprintf(text, sizeof(text), "BOMB %u", (unsigned)smart_bombs);
  19. Executedraw_text(frame, 264, 7, text, 1, COLOR_ORANGE);
  20. Decisionstate == READY
    True
      1. Executeclear(frame, COLOR_BLACK);
      2. Executedraw_text_centred(frame, 160, 21, "DEFENDER", 3, COLOR_CYAN);
      3. Executedraw_text_centred(frame, 160, 59, "RESCUE THE HUMANOIDS", 1, COLOR_WHITE);
      4. Executedraw_text_centred(frame, 160, 76, "STICK FLY A FIRE", 1, COLOR_YELLOW);
      5. Executedraw_text_centred(frame, 160, 92, "C REVERSE B SMART BOMB", 1, COLOR_ORANGE);
      6. Executedraw_text_centred(frame, 160, 140, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    False
    1. Decisionstate == FINISHED
      True
      1. Executedraw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
      False

      Continue after this decision.

Read original function source
void drawDefender(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == SCORES) { drawHighScoreEntry(frame); return; }
    clear(frame, COLOR_BLACK);
    for (int x = 0; x < LCD_WIDTH; ++x) {
        const int ground = (int)groundHeight(camera_x + x);
        fill_rect(frame, x, ground, 1, LCD_HEIGHT - ground, COLOR_DARKGREEN);
        fill_rect(frame, x, ground, 1, 2, COLOR_LIME);
    }
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        if (humans[i].state == HUMAN_LOST) continue;
        const int x = (int)screenX(humans[i].x);
        const uint16_t colour = humans[i].state == HUMAN_CARRIED ? COLOR_CYAN :
            humans[i].state == HUMAN_FALLING ? COLOR_WHITE :
            humans[i].state == HUMAN_ABDUCTED ? COLOR_ORANGE : COLOR_YELLOW;
        if (x >= -8 && x < LCD_WIDTH) draw_bitmap_scaled(frame, x - 4, (int)humans[i].y,
            defender_humanoid_bitmap, 8, 8, 8, 10, colour);
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].object.active) {
        const Enemy *enemy = &enemies[i]; const int x = (int)screenX(enemy->object.x);
        const uint8_t *bitmap = enemy->type == LANDER ? defender_lander_bitmap : enemy->type == MUTANT ? defender_mutant_bitmap : defender_bomber_bitmap;
        const uint16_t colour = enemy->type == LANDER ? COLOR_MAGENTA : enemy->type == MUTANT ? COLOR_RED : COLOR_ORANGE;
        draw_bitmap_scaled(frame, x - 8, (int)enemy->object.y, bitmap, 8, 8, 16, 12, colour);
    }
    // Laser bolts travel horizontally: a bright core with a short dotted cyan trail.
    for (unsigned i = 0; i < MAX_SHOTS; ++i) if (shots[i].active) {
        const int x = (int)screenX(shots[i].x);
        const int y = (int)shots[i].y + 4;
        const int direction = shots[i].vx < 0.0f ? -1 : 1;
        if (x < -18 || x >= LCD_WIDTH + 18) continue;
        draw_line(frame, x - direction * 31, y, x - direction * 4, y, COLOR_DARKCYAN);
        fill_rect(frame, x - (direction < 0 ? 4 : 0), y - 1, 5, 3, COLOR_WHITE);
        fill_rect(frame, x - direction * 36, y, 2, 1, COLOR_CYAN);
        fill_rect(frame, x - direction * 44, y, 2, 1, COLOR_BLUE);
    }
    if (player.active) draw_bitmap_scaled(frame, 152, (int)player.y, facing > 0 ? defender_ship_right : defender_ship_left, 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    fill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        const int x = (int)(humans[i].x * LCD_WIDTH / WORLD_WIDTH);
        const uint16_t colour = humans[i].state == HUMAN_LOST ? COLOR_RED :
            humans[i].state == HUMAN_CARRIED ? COLOR_CYAN :
            humans[i].state == HUMAN_FALLING ? COLOR_WHITE :
            humans[i].state == HUMAN_ABDUCTED ? COLOR_ORANGE : COLOR_YELLOW;
        fill_rect(frame, x, 2, 2, 4, colour);
    }
    char text[32]; snprintf(text, sizeof(text), "SCORE %u", (unsigned)score); draw_text(frame, 3, 7, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives); draw_text_centred(frame, 164, 7, text, 1, COLOR_YELLOW);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave); draw_text(frame, 205, 7, text, 1, COLOR_CYAN);
    snprintf(text, sizeof(text), "BOMB %u", (unsigned)smart_bombs); draw_text(frame, 264, 7, text, 1, COLOR_ORANGE);
    if (state == READY) {
        clear(frame, COLOR_BLACK);
        draw_text_centred(frame, 160, 21, "DEFENDER", 3, COLOR_CYAN);
        draw_text_centred(frame, 160, 59, "RESCUE THE HUMANOIDS", 1, COLOR_WHITE);
        draw_text_centred(frame, 160, 76, "STICK FLY  A FIRE", 1, COLOR_YELLOW);
        draw_text_centred(frame, 160, 92, "C REVERSE  B SMART BOMB", 1, COLOR_ORANGE);
        draw_text_centred(frame, 160, 140, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    } else if (state == FINISHED) draw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "defender.h"
#include "audio.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "high_scores.h"
#include "esp_random.h"
#include <math.h>
#include <stdio.h>
#include <string.h>

#define STEP_MS 10U
#define WORLD_WIDTH 960.0f
#define PLAY_TOP 15
#define GROUND_Y 145
#define PLAYER_W 16
#define PLAYER_H 10
#define HUMANOID_COUNT 6
#define MAX_ENEMIES 10
#define MAX_SHOTS 5
#define FIRE_DELAY_MS 115U
#define SMART_BOMB_DELAY_MS 900U
#define REVERSE_LAG_MS 180U
#define RESPAWN_MS 1000U
#define THRUST_RAMP_MS 900U

// DEFENDER ARTWORK: each byte is one 8-pixel row, bit 7 is the left edge.
// Replace these arrays to change the ship, civilians, and enemy pictures.
static const uint8_t defender_ship_right[8] = {
    0x00, 0x00, 0xC0, 0xFC, 0x77, 0xFF, 0x00, 0x00,
};
static const uint8_t defender_ship_left[8] = {
    0x00, 0x00, 0x03, 0x7F, 0xEE, 0xFF, 0x08, 0x00,
};
static const uint8_t defender_humanoid_bitmap[8] = {
    0x18, 0x18, 0x7E, 0x7E, 0x5A, 0x18, 0x18, 0x18,
};
static const uint8_t defender_lander_bitmap[8] = {
    0x18, 0x3C, 0x7E, 0xDB, 0xFF, 0x24, 0x42, 0x00,
};
static const uint8_t defender_mutant_bitmap[8] = {
    0x42, 0xA5, 0x5A, 0xFF, 0x5A, 0xA5, 0x42, 0x00,
};
static const uint8_t defender_bomber_bitmap[8] = {
    0x00, 0x24, 0x7E, 0xFF, 0xFF, 0x7E, 0x24, 0x00,
};

typedef enum { READY, PLAYING, FINISHED, SCORES } State;
typedef enum { LANDER, MUTANT, BOMBER } EnemyType;
typedef enum { HUMAN_GROUND, HUMAN_ABDUCTED, HUMAN_FALLING, HUMAN_CARRIED, HUMAN_LOST } HumanState;
typedef struct { float x, y, vx, vy; bool active; } Object;
typedef struct { float x, y; HumanState state; } Humanoid;
typedef struct { Object object; EnemyType type; int human; } Enemy;

static State state;
static Object player, shots[MAX_SHOTS];
static float shot_distance[MAX_SHOTS];
static Humanoid humans[HUMANOID_COUNT];
static Enemy enemies[MAX_ENEMIES];
static float camera_x;
static uint32_t score, last_update, accumulator, fire_delay, bomb_delay, state_started, spawn_delay;
static uint8_t lives, wave, smart_bombs;
static int facing;
static bool running, controls_ready, smart_bomb_held;
static uint32_t reverse_lag;
static uint16_t thrust_x_ms, thrust_y_ms;
static int thrust_x_direction, thrust_y_direction;

static float screenX(float world_x);

static float clampf(float value, float low, float high)
{
    return value < low ? low : value > high ? high : value;
}

static float wrapWorld(float x)
{
    while (x < 0.0f) x += WORLD_WIDTH;
    while (x >= WORLD_WIDTH) x -= WORLD_WIDTH;
    return x;
}

static float worldDelta(float from, float to)
{
    float delta = to - from;
    if (delta > WORLD_WIDTH / 2.0f) delta -= WORLD_WIDTH;
    if (delta < -WORLD_WIDTH / 2.0f) delta += WORLD_WIDTH;
    return delta;
}

static void addScore(uint32_t points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
    const uint8_t next_wave = (uint8_t)(score / 1500U + 1U);
    if (next_wave > wave) wave = next_wave;
}

static float groundHeight(float world_x)
{
    const float phase = world_x * 0.030f;
    return GROUND_Y - 9.0f * sinf(phase) - 5.0f * sinf(phase * 2.7f);
}

static int nearestHuman(float x)
{
    int found = -1;
    float distance = WORLD_WIDTH;
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        if (humans[i].state != HUMAN_GROUND) continue;
        const float candidate = fabsf(worldDelta(x, humans[i].x));
        if (candidate < distance) { distance = candidate; found = (int)i; }
    }
    return found;
}

static void spawnEnemy(EnemyType type)
{
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (enemies[i].object.active) continue;
        const float x = wrapWorld(player.x + (esp_random() % 2U ? 190.0f : -190.0f));
        const int human = type == LANDER ? nearestHuman(x) : -1;
        enemies[i] = (Enemy){.object = {x, 28.0f + esp_random() % 55U, 0.0f, 0.0f, true},
                             .type = type, .human = human};
        return;
    }
}

static void resetWorld(void)
{
    memset(shots, 0, sizeof(shots));
    memset(shot_distance, 0, sizeof(shot_distance));
    memset(enemies, 0, sizeof(enemies));
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        humans[i] = (Humanoid){(i + 1U) * WORLD_WIDTH / (HUMANOID_COUNT + 1U), 0.0f, HUMAN_GROUND};
        humans[i].y = groundHeight(humans[i].x) - 8.0f;
    }
    for (unsigned i = 0; i < 4; ++i) spawnEnemy(LANDER);
}

static void beginGame(uint32_t now_ms)
{
    score = 0; wave = 1; lives = 3; smart_bombs = 3; facing = 1; reverse_lag = 0;
    player = (Object){WORLD_WIDTH / 2.0f, 75.0f, 0.0f, 0.0f, true};
    fire_delay = bomb_delay = 0; spawn_delay = 1000U; accumulator = 0;
    controls_ready = false; smart_bomb_held = false;
    thrust_x_ms = thrust_y_ms = 0; thrust_x_direction = thrust_y_direction = 0;
    resetWorld();
    state = PLAYING;
    last_update = now_ms;
}

static void hitPlayer(void)
{
    if (!player.active) return;
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (--lives == 0) { player.active = false; state = FINISHED; state_started = last_update; return; }
    player.active = false;
    state_started = last_update;
    memset(shots, 0, sizeof(shots));
}

static void fireShot(void)
{
    if (!player.active || fire_delay != 0) return;
    for (unsigned i = 0; i < MAX_SHOTS; ++i) if (!shots[i].active) {
        shots[i] = (Object){player.x + facing * 10.0f, player.y + 4.0f, facing * 250.0f, 0.0f, true};
        shot_distance[i] = 0.0f;
        fire_delay = FIRE_DELAY_MS;
        audioPlay(AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}

static void releaseLanderHuman(Enemy *enemy)
{
    if (enemy->human < 0 || humans[enemy->human].state != HUMAN_ABDUCTED) return;
    Humanoid *human = &humans[enemy->human];
    human->x = enemy->object.x;
    human->y = enemy->object.y + 10.0f;
    if (groundHeight(human->x) - human->y > 16.0f) human->state = HUMAN_FALLING;
    else {
        human->y = groundHeight(human->x) - 8.0f;
        human->state = HUMAN_GROUND;
    }
    enemy->human = -1;
}

static void smartBomb(void)
{
    if (bomb_delay != 0 || smart_bombs == 0 || !player.active) return;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].object.active) {
        const float x = screenX(enemies[i].object.x);
        if (x < -16.0f || x >= LCD_WIDTH + 16.0f) continue;
        releaseLanderHuman(&enemies[i]);
        enemies[i].object.active = false;
        addScore(enemies[i].type == MUTANT ? 250U : 150U);
    }
    --smart_bombs;
    bomb_delay = SMART_BOMB_DELAY_MS;
    audioPlay(AUDIO_SFX_EXPLOSION);
}

static void updateEnemy(Enemy *enemy)
{
    Object *object = &enemy->object;
    if (!object->active) return;
    const float dx = worldDelta(object->x, player.x);
    if (enemy->type == LANDER) {
        if (enemy->human >= 0 && humans[enemy->human].state == HUMAN_GROUND) {
            const float target_x = humans[enemy->human].x;
            object->x = wrapWorld(object->x + clampf(worldDelta(object->x, target_x), -35.0f, 35.0f) * STEP_MS / 1000.0f);
            const float target_y = humans[enemy->human].y - 8.0f;
            object->y += clampf(target_y - object->y, -24.0f, 24.0f) * STEP_MS / 1000.0f;
            if (fabsf(worldDelta(object->x, target_x)) < 5.0f && fabsf(object->y - target_y) < 5.0f) {
                humans[enemy->human].state = HUMAN_ABDUCTED;
            }
        } else if (enemy->human >= 0 && humans[enemy->human].state == HUMAN_ABDUCTED) {
            humans[enemy->human].x = object->x;
            humans[enemy->human].y = object->y + 10.0f;
            object->y -= (30.0f + wave * 2.0f) * STEP_MS / 1000.0f;
            if (object->y < PLAY_TOP) {
                humans[enemy->human].state = HUMAN_LOST;
                enemy->type = MUTANT;
                enemy->human = -1;
                object->y = PLAY_TOP + 4;
            }
        } else {
            object->x = wrapWorld(object->x + (dx > 0 ? 20.0f : -20.0f) * STEP_MS / 1000.0f);
        }
    } else {
        const float speed = enemy->type == MUTANT ? 58.0f + wave * 4.0f : 35.0f + wave * 2.0f;
        object->x = wrapWorld(object->x + clampf(dx, -speed, speed) * STEP_MS / 1000.0f);
        object->y += clampf(player.y - object->y, -speed * 0.45f, speed * 0.45f) * STEP_MS / 1000.0f;
    }
    if (fabsf(dx) < 12.0f && fabsf(object->y - player.y) < 12.0f) hitPlayer();
}

/* A rescued human follows the ship until it is returned to solid ground. */
static void updateHumanRescue(void)
{
    int carried = -1;
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        if (humans[i].state == HUMAN_CARRIED) { carried = (int)i; break; }
    }

    /* Catch an abducted human by flying beneath it. Only one can be carried. */
    if (carried < 0) {
        for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
            if ((humans[i].state != HUMAN_ABDUCTED && humans[i].state != HUMAN_FALLING) ||
                fabsf(worldDelta(player.x, humans[i].x)) >= 10.0f ||
                fabsf((player.y + PLAYER_H) - humans[i].y) >= 12.0f) continue;

            for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
                if (enemies[e].human == (int)i) enemies[e].human = -1;
            }
            humans[i].state = HUMAN_CARRIED;
            carried = (int)i;
            break;
        }
    }

    if (carried < 0) return;
    Humanoid *human = &humans[carried];
    human->x = player.x;
    human->y = player.y + PLAYER_H;

    /* Landing near the terrain safely returns the human and awards rescue points. */
    if (human->y >= groundHeight(player.x) - 1.0f) {
        human->y = groundHeight(player.x) - 8.0f;
        human->state = HUMAN_GROUND;
        addScore(500U);
    }
}

static void updateFallingHumans(void)
{
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        Humanoid *human = &humans[i];
        if (human->state != HUMAN_FALLING) continue;
        human->y += 75.0f * STEP_MS / 1000.0f;
        if (human->y >= groundHeight(human->x) - 8.0f) {
            human->y = groundHeight(human->x) - 8.0f;
            human->state = HUMAN_GROUND;
        }
    }
}

static uint16_t updateThrustRamp(float input, int *direction, uint16_t held_ms)
{
    const int new_direction = input > 0.15f ? 1 : input < -0.15f ? -1 : 0;
    if (new_direction == 0) { *direction = 0; return 0; }
    if (new_direction != *direction) { *direction = new_direction; return STEP_MS; }
    return held_ms + STEP_MS > THRUST_RAMP_MS ? THRUST_RAMP_MS : held_ms + STEP_MS;
}

static void stepGame(float input_x, float input_y, bool fire, bool smart_bomb)
{
    fire_delay = fire_delay > STEP_MS ? fire_delay - STEP_MS : 0;
    bomb_delay = bomb_delay > STEP_MS ? bomb_delay - STEP_MS : 0;
    reverse_lag = reverse_lag > STEP_MS ? reverse_lag - STEP_MS : 0;
    if (!player.active) { if (last_update - state_started >= RESPAWN_MS) player = (Object){WORLD_WIDTH / 2.0f, 75.0f, 0, 0, true}; return; }
    thrust_x_ms = updateThrustRamp(input_x, &thrust_x_direction, thrust_x_ms);
    thrust_y_ms = updateThrustRamp(input_y, &thrust_y_direction, thrust_y_ms);
    const float x_acceleration = reverse_lag ? 0.0f : 100.0f + 180.0f * (float)thrust_x_ms / THRUST_RAMP_MS;
    const float y_acceleration = reverse_lag ? 0.0f : 100.0f + 180.0f * (float)thrust_y_ms / THRUST_RAMP_MS;
    player.vx += input_x * x_acceleration * STEP_MS / 1000.0f;
    player.vy += input_y * y_acceleration * STEP_MS / 1000.0f;
    player.vx *= 0.975f; player.vy *= 0.975f;
    player.vx = clampf(player.vx, -125.0f, 125.0f);
    player.vy = clampf(player.vy, -95.0f, 95.0f);
    player.x = wrapWorld(player.x + player.vx * STEP_MS / 1000.0f);
    player.y = clampf(player.y + player.vy * STEP_MS / 1000.0f, PLAY_TOP + 4.0f, groundHeight(player.x) - PLAYER_H);
    camera_x = player.x - LCD_WIDTH / 2.0f;
    updateFallingHumans();
    updateHumanRescue();
    if (fire) fireShot();
    if (smart_bomb) smartBomb();
    for (unsigned i = 0; i < MAX_SHOTS; ++i) {
        Object *shot = &shots[i]; if (!shot->active) continue;
        shot->x = wrapWorld(shot->x + shot->vx * STEP_MS / 1000.0f);
        shot_distance[i] += fabsf(shot->vx) * STEP_MS / 1000.0f;
        if (shot_distance[i] > 320.0f) { shot->active = false; continue; }
        for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
            Enemy *enemy = &enemies[e];
            if (enemy->object.active && fabsf(worldDelta(shot->x, enemy->object.x)) < 10.0f && fabsf(shot->y - enemy->object.y) < 9.0f) {
                releaseLanderHuman(enemy);
                enemy->object.active = false; shot->active = false;
                addScore(enemy->type == MUTANT ? 250U : enemy->type == BOMBER ? 150U : 150U);
                audioPlay(AUDIO_SFX_HIT);
                break;
            }
        }
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) updateEnemy(&enemies[i]);
    spawn_delay = spawn_delay > STEP_MS ? spawn_delay - STEP_MS : 0;
    if (spawn_delay == 0) { spawnEnemy((esp_random() % 8U) == 0 ? BOMBER : LANDER); spawn_delay = 900U - (wave > 5U ? 250U : wave * 50U); }
}

void startDefender(uint32_t now_ms) { running = true; beginGame(now_ms); state = READY; }
void stopDefender(void) { cancelHighScoreEntry(); running = false; }
void defenderAction(bool start, bool reverse, uint32_t now_ms)
{
    if (!running) return;
    if (start && state == READY) {
        beginGame(now_ms);
        audioPlay(AUDIO_SFX_GAME_START);
    }
    if (reverse && state == PLAYING && player.active) {
        facing = -facing; player.vx *= 0.30f; reverse_lag = REVERSE_LAG_MS;
        thrust_x_ms = thrust_y_ms = 0; thrust_x_direction = thrust_y_direction = 0;
    }
}

void updateDefender(int x_offset, int y_offset, bool valid, bool fire, bool smart_bomb, uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update; last_update = now_ms;
    if (state == FINISHED && now_ms - state_started >= 1000U) { startHighScoreEntry(HIGH_SCORE_DEFENDER, score, now_ms); state = SCORES; }
    if (state == SCORES) { if (updateHighScoreEntry(x_offset, y_offset, valid, now_ms)) state = READY; return; }
    if (state != PLAYING) return;
    if (!valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350) controls_ready = true;
    const bool smart_bomb_pressed = smart_bomb && !smart_bomb_held;
    smart_bomb_held = smart_bomb;
    const float input_x = valid && controls_ready ? clampf((float)x_offset / 1600.0f, -1.0f, 1.0f) : 0.0f;
    const float input_y = valid && controls_ready ? clampf((float)-y_offset / 1600.0f, -1.0f, 1.0f) : 0.0f;
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == PLAYING) { accumulator -= STEP_MS; stepGame(input_x, input_y, fire, smart_bomb_pressed); }
}

static float screenX(float world_x)
{
    float x = worldDelta(camera_x, world_x);
    if (x < -20.0f) x += WORLD_WIDTH;
    return x;
}

void drawDefender(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == SCORES) { drawHighScoreEntry(frame); return; }
    clear(frame, COLOR_BLACK);
    for (int x = 0; x < LCD_WIDTH; ++x) {
        const int ground = (int)groundHeight(camera_x + x);
        fill_rect(frame, x, ground, 1, LCD_HEIGHT - ground, COLOR_DARKGREEN);
        fill_rect(frame, x, ground, 1, 2, COLOR_LIME);
    }
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        if (humans[i].state == HUMAN_LOST) continue;
        const int x = (int)screenX(humans[i].x);
        const uint16_t colour = humans[i].state == HUMAN_CARRIED ? COLOR_CYAN :
            humans[i].state == HUMAN_FALLING ? COLOR_WHITE :
            humans[i].state == HUMAN_ABDUCTED ? COLOR_ORANGE : COLOR_YELLOW;
        if (x >= -8 && x < LCD_WIDTH) draw_bitmap_scaled(frame, x - 4, (int)humans[i].y,
            defender_humanoid_bitmap, 8, 8, 8, 10, colour);
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].object.active) {
        const Enemy *enemy = &enemies[i]; const int x = (int)screenX(enemy->object.x);
        const uint8_t *bitmap = enemy->type == LANDER ? defender_lander_bitmap : enemy->type == MUTANT ? defender_mutant_bitmap : defender_bomber_bitmap;
        const uint16_t colour = enemy->type == LANDER ? COLOR_MAGENTA : enemy->type == MUTANT ? COLOR_RED : COLOR_ORANGE;
        draw_bitmap_scaled(frame, x - 8, (int)enemy->object.y, bitmap, 8, 8, 16, 12, colour);
    }
    // Laser bolts travel horizontally: a bright core with a short dotted cyan trail.
    for (unsigned i = 0; i < MAX_SHOTS; ++i) if (shots[i].active) {
        const int x = (int)screenX(shots[i].x);
        const int y = (int)shots[i].y + 4;
        const int direction = shots[i].vx < 0.0f ? -1 : 1;
        if (x < -18 || x >= LCD_WIDTH + 18) continue;
        draw_line(frame, x - direction * 31, y, x - direction * 4, y, COLOR_DARKCYAN);
        fill_rect(frame, x - (direction < 0 ? 4 : 0), y - 1, 5, 3, COLOR_WHITE);
        fill_rect(frame, x - direction * 36, y, 2, 1, COLOR_CYAN);
        fill_rect(frame, x - direction * 44, y, 2, 1, COLOR_BLUE);
    }
    if (player.active) draw_bitmap_scaled(frame, 152, (int)player.y, facing > 0 ? defender_ship_right : defender_ship_left, 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    fill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
    for (unsigned i = 0; i < HUMANOID_COUNT; ++i) {
        const int x = (int)(humans[i].x * LCD_WIDTH / WORLD_WIDTH);
        const uint16_t colour = humans[i].state == HUMAN_LOST ? COLOR_RED :
            humans[i].state == HUMAN_CARRIED ? COLOR_CYAN :
            humans[i].state == HUMAN_FALLING ? COLOR_WHITE :
            humans[i].state == HUMAN_ABDUCTED ? COLOR_ORANGE : COLOR_YELLOW;
        fill_rect(frame, x, 2, 2, 4, colour);
    }
    char text[32]; snprintf(text, sizeof(text), "SCORE %u", (unsigned)score); drawPet2001TextTransparent(frame, 3, 7, text, COLOR_WHITE, 1);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives); drawPet2001TextCentredTransparent(frame, 164, 7, text, COLOR_YELLOW, 1);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave); drawPet2001TextTransparent(frame, 205, 7, text, COLOR_CYAN, 1);
    snprintf(text, sizeof(text), "BOMB %u", (unsigned)smart_bombs); drawPet2001TextTransparent(frame, 264, 7, text, COLOR_ORANGE, 1);
    if (state == READY) {
        clear(frame, COLOR_BLACK);
        drawPet2001TextCentredTransparent(frame, 160, 21, "DEFENDER", COLOR_CYAN, 3);
        drawPet2001TextCentredTransparent(frame, 160, 59, "RESCUE THE HUMANOIDS", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 76, "STICK FLY  A FIRE", COLOR_YELLOW, 1);
        drawPet2001TextCentredTransparent(frame, 160, 92, "C REVERSE  B SMART BOMB", COLOR_ORANGE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 140, "BOOT BACK", COLOR_LIGHTGRAY, 1);
    } else if (state == FINISHED) drawPet2001TextCentredTransparent(frame, 160, 78, "GAME OVER", COLOR_RED, 2);
}