Missile Command
Source: src/missile_command.c · 792 lines · 31 functions.
Screen captures
Views of this part running on the Mini Arcade.

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)
- Exit this function
return 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)
- Execute
unsigned count = 0; - FOR loop
unsigned i = 0; i < CITY_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Decision
cities[i]True- Execute
++count;
FalseContinue after this decision.
- Exit this function
return 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)
- Exit this function
return 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)
- Execute
score = 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:
- Execute
waveTallyElapsed = elapsed; - Execute
const uint32_t missileElapsed = elapsed < WAVE_TALLY_MISSILES_MS ? elapsed : WAVE_TALLY_MISSILES_MS; - Execute
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; - Execute
const uint32_t missiles = waveMissileBonus * missileElapsed / WAVE_TALLY_MISSILES_MS; - Execute
const uint32_t cities = waveCityBonus * cityElapsed / WAVE_TALLY_CITIES_MS; - Decision
missiles > waveMissileAwardedTrue- Execute
addPoints(missiles - waveMissileAwarded);
FalseContinue after this decision.
- Decision
cities > waveCityAwardedTrue- Execute
addPoints(cities - waveCityAwarded);
FalseContinue after this decision.
- Execute
waveMissileAwarded = missiles; - Execute
waveCityAwarded = 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:
- Execute
const float dx = bx - ax, dy = by - ay; - Execute
const float lengthSquared = dx * dx + dy * dy; - Execute
const float fraction = lengthSquared > 0.0f ? clampValue(((cx - ax) * dx + (cy - ay) * dy) / lengthSquared, 0.0f, 1.0f) : 0.0f; - Execute
const float distanceX = ax + fraction * dx - cx; - Execute
const float distanceY = ay + fraction * dy - cy; - Exit this function
return 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)- Decision
delta == 0.0fTrue- Exit this function
return origin >= minimum && origin <= maximum;
FalseContinue after this decision.
- Execute
float first = (minimum - origin) / delta; - Execute
float last = (maximum - origin) / delta; - Decision
first > lastTrue- Execute
const float swap = first; - Execute
first = last; - Execute
last = swap;
FalseContinue after this decision.
- Decision
first > *enterTrue- Execute
*enter = first;
FalseContinue after this decision.
- Decision
last < *leaveTrue- Execute
*leave = last;
FalseContinue after this decision.
- Exit this function
return *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:
- Execute
float enter = 0.0f, leave = 1.0f; - Exit this function
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);
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)
- FOR loop
unsigned i = 0; i < MAX_EXPLOSIONS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
explosions[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
explosions[i] = (Explosion){ .x = x, .y = y, .radius = 2.0f, .age = 0, .active = true, .defensive = defensive, }; - Exit this function
return;
- Execute
ESP_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)
- FOR loop
unsigned i = 0; i < MAX_EXPLOSIONS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Explosion *explosion = &explosions[i]; - Decision
!explosion->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
explosion->age += STEP_MS; - Decision
explosion->age >= BLAST_LIFE_MSTrue- Execute
explosion->active = false;
False- Decision
explosion->age < BLAST_GROW_MSTrue- Execute
explosion->radius = 2.0f + (BLAST_RADIUS - 2.0f) * explosion->age / BLAST_GROW_MS;
False- Decision
explosion->age < BLAST_GROW_MS + BLAST_HOLD_MSTrue- Execute
explosion->radius = BLAST_RADIUS;
False- Execute
explosion->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)- Execute
const float dx = targetX - x, dy = targetY - y; - Execute
const float distance = sqrtf(dx * dx + dy * dy); - Exit this function
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, };
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)
- Execute
missile->previousX = missile->x; - Execute
missile->previousY = missile->y; - Execute
const float dx = missile->targetX - missile->x; - Execute
const float dy = missile->targetY - missile->y; - Execute
const float stepX = missile->vx * STEP_MS / 1000.0f; - Execute
const float stepY = missile->vy * STEP_MS / 1000.0f; - Decision
dx * dx + dy * dy <= stepX * stepX + stepY * stepYTrue- Execute
missile->x = missile->targetX; - Execute
missile->y = missile->targetY; - Execute
missile->arrived = true;
False- Execute
missile->x += stepX; - Execute
missile->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:
- Execute
unsigned targets[CITY_COUNT + BASE_COUNT], count = 0; - FOR loop
unsigned i = 0; i < CITY_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Decision
cities[i]True- Execute
targets[count++] = i;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < BASE_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Decision
bases[i].aliveTrue- Execute
targets[count++] = CITY_COUNT + i;
FalseContinue after this decision.
- Decision
count == 0True- Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Decision
enemies[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const unsigned target = targets[esp_random() % count]; - Execute
const float targetX = target < CITY_COUNT ? cityX[target] : baseX[target - CITY_COUNT]; - Execute
enemies[i] = makeMissile(5.0f + esp_random() % 310U, 14.0f, targetX, TARGET_TOP + TARGET_HEIGHT * 0.5f, 16.0f + difficulty() * 2.5f, target); - Execute
--enemiesToSpawn; - Execute
spawnDelay = 900U - difficulty() * 55U + esp_random() % 301U; - Exit this function
return;
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:
- Decision
!bases[base].alive || bases[base].ammo == 0 || bases[base].fireDelay != 0True- Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_INTERCEPTORS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
interceptors[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
interceptors[i] = makeMissile(baseX[base], TARGET_TOP, aimX, aimY, base == 1U ? 180.0f : 140.0f, base); - Execute
--bases[base].ammo; - Execute
bases[base].fireDelay = FIRE_DELAY_MS; - Execute
audioPlay(AUDIO_SFX_PLAYER_SHOT); - Exit this function
return;
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)
- FOR loop
unsigned i = 0; i < MAX_EXPLOSIONS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Explosion *explosion = &explosions[i]; - Decision
!explosion->active || !explosion->defensiveTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float dx = enemy->x - explosion->x; - Execute
const float dy = enemy->y - explosion->y; - Execute
const float radius = explosion->radius + 8.0f; - Decision
dx * dx + dy * dy <= radius * radiusTrue- Exit this function
return true;
FalseContinue after this decision.
- Exit this function
return 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)
- Execute
const float coverage = BLAST_RADIUS + 8.0f; - FOR loop
unsigned i = 0; i < MAX_INTERCEPTORS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Missile *interceptor = &interceptors[i]; - Decision
!interceptor->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float dx = enemy->x - interceptor->targetX; - Execute
const float dy = enemy->y - interceptor->targetY; - Decision
dx * dx + dy * dy <= coverage * coverageTrue- Exit this function
return true;
FalseContinue after this decision.
- Exit this function
return 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)
- Execute
int best_base = -1; - Execute
float best_distance = 0.0f; - FOR loop
unsigned base = 0; base < BASE_COUNT; ++baseRepeat body while the loop condition permits; then continue below.
- Decision
!bases[base].alive || bases[base].ammo == 0U || bases[base].fireDelay != 0UTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float distance = fabsf(target_x - (float)baseX[base]); - Decision
best_base < 0 || distance < best_distanceTrue- Execute
best_base = (int)base; - Execute
best_distance = distance;
FalseContinue after this decision.
- Exit this function
return 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:
- Execute
const Missile *threat = NULL; - FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Missile *enemy = &enemies[i]; - Decision
!enemy->active || defensiveBlastCovers(enemy) || interceptorCovers(enemy)True- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
threat == NULL || enemy->y > threat->yTrue- Execute
threat = enemy;
FalseContinue after this decision.
- Decision
threat == NULLTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
const int base = closestAutoBase(threat->x); - Decision
base < 0True- Exit this function
return;
FalseContinue after this decision.
- Execute
const float interceptor_speed = base == 1 ? 180.0f : 140.0f; - Execute
const float distance_x = threat->x - (float)baseX[base]; - Execute
const float distance_y = threat->y - (float)TARGET_TOP; - Execute
const float travel_ms = sqrtf(distance_x * distance_x + distance_y * distance_y) * 1000.0f / interceptor_speed; - Execute
aimX = clampValue(threat->x + threat->vx * travel_ms / 1000.0f, 5.0f, 314.0f); - Execute
aimY = clampValue(threat->y + threat->vy * travel_ms / 1000.0f, 20.0f, 137.0f); - Execute
fireBase((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:
- Execute
memset(enemies, 0, sizeof(enemies)); - Execute
memset(interceptors, 0, sizeof(interceptors)); - Execute
memset(explosions, 0, sizeof(explosions)); - FOR loop
unsigned i = 0; i < BASE_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Execute
bases[i] = (Base){AMMO_PER_BASE, 0, true};
- Execute
enemiesToSpawn = 12U + difficulty() * 4U; - Execute
spawnDelay = 1000U; - Execute
accumulator = 0; - Execute
fireReady = false; - Execute
gameState = STATE_PLAYING; - Execute
ESP_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:
- Execute
score = waveBonus = waveMissileBonus = waveCityBonus = 0; - Execute
waveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0; - Execute
waveMissilesRemaining = waveCitiesRemaining = 0; - Execute
wave = 1; - FOR loop
unsigned i = 0; i < CITY_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Execute
cities[i] = true;
- Execute
aimX = 160.0f; - Execute
aimY = 80.0f; - Execute
controlsReady = false; - Execute
lastUpdate = now_ms; - Execute
beginWave(); - Execute
audioPlay(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:
- Execute
gameState = STATE_FINISHED; - Execute
stateStarted = lastUpdate; - Execute
audioPlay(AUDIO_SFX_GAME_OVER); - Execute
ESP_LOGI("missile_command", "GAME OVER: SCORE %u, WAVE %u", (unsigned)score, (unsigned)wave); - Execute
const uint32_t gold = score / 1000U; - Decision
gold != 0 && !addUserGoldWon(gold)True- Execute
ESP_LOGE("missile_command", "Failed to save %u Gold", (unsigned)gold);
FalseContinue 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)
- Decision
enemiesToSpawn != 0True- Exit this function
return false;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Decision
enemies[i].activeTrue- Exit this function
return false;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_INTERCEPTORS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
interceptors[i].activeTrue- Exit this function
return false;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_EXPLOSIONS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
explosions[i].activeTrue- Exit this function
return false;
FalseContinue after this decision.
- Exit this function
return 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:
- Execute
bool chainReaction; - DO / WHILE loop
chainReactionRun body, then test condition; repeat while true.
- Execute
chainReaction = false; - FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Missile *enemy = &enemies[i]; - Decision
!enemy->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- FOR loop
unsigned j = 0; j < MAX_EXPLOSIONS; ++jRepeat body while the loop condition permits; then continue below.
- Execute
const Explosion *explosion = &explosions[j]; - Decision
!explosion->active || !explosion->defensiveTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
!segmentHitsCircle(enemy->previousX, enemy->previousY, enemy->x, enemy->y, explosion->x, explosion->y, explosion->radius)True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
enemy->active = false; - Execute
addPoints(25U); - Execute
startExplosion(enemy->x, enemy->y, true); - Execute
audioPlay(AUDIO_SFX_MISSILE_EXPLOSION); - Execute
chainReaction = true; - Exit enclosing loop or switch
break;
- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Missile *enemy = &enemies[i]; - Decision
!enemy->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const bool city = enemy->target < CITY_COUNT; - Execute
const unsigned index = city ? enemy->target : enemy->target - CITY_COUNT; - Decision
!enemy->arrived && !segmentHitsTarget(enemy, city ? cityX[index] : baseX[index], city ? CITY_WIDTH : BASE_WIDTH)True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
enemy->active = false; - Decision
cityTrue- Execute
cities[index] = false;
False- Execute
bases[index].alive = false; - Execute
bases[index].ammo = 0;
- Execute
startExplosion(enemy->x, enemy->y, false); - Execute
audioPlay(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:
- Execute
aimX = clampValue(aimX + aimVelocityX * STEP_MS / 1000.0f, 5.0f, 314.0f); - Execute
aimY = clampValue(aimY + aimVelocityY * STEP_MS / 1000.0f, 20.0f, 137.0f); - Execute
updateExplosions(); - FOR loop
unsigned i = 0; i < BASE_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Execute
bases[i].fireDelay = bases[i].fireDelay > STEP_MS ? bases[i].fireDelay - STEP_MS : 0; - Decision
fireReady && fire[i]True- Execute
fireBase(i);
FalseContinue after this decision.
- Execute
spawnDelay = spawnDelay > STEP_MS ? spawnDelay - STEP_MS : 0; - Decision
enemiesToSpawn != 0 && spawnDelay == 0True- Execute
spawnEnemy();
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_INTERCEPTORS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Missile *missile = &interceptors[i]; - Decision
!missile->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
moveMissile(missile); - Decision
missile->arrivedTrue- Execute
missile->active = false; - Execute
startExplosion(missile->x, missile->y, true); - Execute
audioPlay(AUDIO_SFX_MISSILE_EXPLOSION);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Decision
enemies[i].activeTrue- Execute
moveMissile(&enemies[i]);
FalseContinue after this decision.
- Execute
resolveCollisions(); - Decision
livingCities() == 0True- Execute
finishGame();
False- Decision
waveIsQuiet()True- Execute
waveMissilesRemaining = 0; - FOR loop
unsigned i = 0; i < BASE_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Execute
waveMissilesRemaining += bases[i].ammo;
- Execute
waveCitiesRemaining = livingCities(); - Execute
waveMissileBonus = waveMissilesRemaining * 5U; - Execute
waveCityBonus = waveCitiesRemaining * 100U; - Execute
waveBonus = waveMissileBonus + waveCityBonus; - Execute
waveMissileAwarded = waveCityAwarded = waveTallyElapsed = 0; - Execute
gameState = STATE_WAVE_CLEAR; - Execute
stateStarted = lastUpdate; - Execute
audioPlay(AUDIO_SFX_WAVE_CLEAR);
FalseContinue 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:
- Decision
offset >= -350 && offset <= 350True- Exit this function
return 0.0f;
FalseContinue after this decision.
- Execute
const float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f; - Exit this function
return 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:
- Execute
starfield_initialize(&stars, MISSILE_STAR_COUNT, 0, 0, LCD_WIDTH, GROUND_Y, STARFIELD_GREYSCALE); - Execute
running = true; - Execute
auto_mode = false; - Execute
ai_speed = AI_SPEED_DEFAULT_INDEX; - Execute
modeSelectionReset(&mode_input); - Execute
lastUpdate = now_ms; - Execute
gameState = STATE_MODE; - Execute
ESP_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:
- Execute
cancelHighScoreEntry(); - Execute
running = false; - Execute
ESP_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:
- Decision
!runningTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState == STATE_MODETrue- Decision
actionTrue- Execute
beginGame(now_ms); - Execute
ai_due = now_ms + aiSpeedDelay(ai_speed);
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
auto_modeTrue- Decision
speedTrue- Execute
cycleAISpeed(&ai_speed); - Execute
ai_due = now_ms + aiSpeedDelay(ai_speed);
FalseContinue after this decision.
- Decision
actionTrue- Execute
auto_mode = false; - Execute
modeSelectionReset(&mode_input); - Execute
gameState = STATE_MODE;
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
action && gameState == STATE_READYTrue- Execute
beginGame(now_ms);
FalseContinue 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:
- Decision
!runningTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
const uint32_t elapsed = now_ms - lastUpdate; - Execute
lastUpdate = now_ms; - Execute
starfield_update(&stars, elapsed); - Decision
gameState == STATE_MODETrue- Decision
modeSelectionNext(&mode_input, x_offset, y_offset, valid, fire_left, fire_right, now_ms)True- Execute
auto_mode = !auto_mode;
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState == STATE_FINISHED && auto_modeTrue- Decision
now_ms - stateStarted >= aiRestartDelay()True- Execute
beginGame(now_ms); - Execute
ai_due = now_ms + aiSpeedDelay(ai_speed);
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState == STATE_FINISHED && now_ms - stateStarted >= 1000UTrue- Execute
startHighScoreEntry(HIGH_SCORE_MISSILE_COMMAND, score, now_ms); - Execute
gameState = STATE_SCORES;
FalseContinue after this decision.
- Decision
gameState == STATE_SCORESTrue- Decision
updateHighScoreEntry(x_offset, y_offset, valid, now_ms)True- Execute
gameState = STATE_READY;
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState == STATE_WAVE_CLEARTrue- Execute
updateWaveTally(now_ms - stateStarted); - Decision
waveTallyElapsed >= WAVE_CLEAR_MSTrue- Decision
wave < UINT32_MAXTrue- Execute
++wave;
FalseContinue after this decision.
- Execute
beginWave();
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState != STATE_PLAYINGTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
auto_mode && now_ms >= ai_dueTrue- Execute
autoFireAtThreat(); - Execute
ai_due = now_ms + aiSpeedDelay(ai_speed);
FalseContinue after this decision.
- Decision
!validTrue- Execute
controlsReady = false;
False- Decision
x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350True- Execute
controlsReady = true;
FalseContinue after this decision.
- Decision
!fire_left && !fire_centre && !fire_rightTrue- Execute
fireReady = true;
FalseContinue after this decision.
- Execute
float velocityX = !auto_mode && valid && controlsReady ? axisVelocity(x_offset) : 0.0f; - Execute
float velocityY = !auto_mode && valid && controlsReady ? -axisVelocity(y_offset) : 0.0f; - Execute
const float speed = sqrtf(velocityX * velocityX + velocityY * velocityY); - Decision
speed > 140.0fTrue- Execute
velocityX *= 140.0f / speed; - Execute
velocityY *= 140.0f / speed;
FalseContinue after this decision.
- Execute
const bool fire[BASE_COUNT] = {auto_mode ? false : fire_left, auto_mode ? false : fire_centre, auto_mode ? false : fire_right}; - Execute
accumulator += elapsed > 100U ? 100U : elapsed; - WHILE loop
accumulator >= STEP_MS && gameState == STATE_PLAYINGRepeat body while the loop condition permits; then continue below.
- Execute
accumulator -= STEP_MS; - Execute
simulateStep(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:
- Execute
fill_rect(frame, 0, GROUND_Y, LCD_WIDTH, LCD_HEIGHT - GROUND_Y, COLOR_BLACK); - Execute
draw_line(frame, 0, GROUND_Y, LCD_WIDTH - 1, GROUND_Y, COLOR_DARKORANGE); - FOR loop
unsigned i = 0; i < CITY_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const int x = cityX[i] - CITY_WIDTH / 2; - Decision
!cities[i]True- Execute
draw_bitmap_scaled(frame, x, TARGET_TOP, destroyedCityBitmap, CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_DARKGRAY); - Next loop iteration
continue;
FalseContinue after this decision.
- Execute
draw_bitmap_scaled(frame, x, TARGET_TOP, cityBitmap, CITY_WIDTH, TARGET_HEIGHT, CITY_WIDTH, TARGET_HEIGHT, COLOR_CYAN);
- Execute
static const char baseButtons[BASE_COUNT] = {'D', 'A', 'B'}; - FOR loop
unsigned i = 0; i < BASE_COUNT; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const int x = baseX[i]; - Execute
const uint16_t color = bases[i].alive ? baseColors[i] : COLOR_DARKGRAY; - Decision
bases[i].aliveTrue- Execute
draw_bitmap_scaled(frame, x - BASE_WIDTH / 2, TARGET_TOP, baseBitmap, BASE_WIDTH, TARGET_HEIGHT, BASE_WIDTH, TARGET_HEIGHT, color);
False- Execute
draw_bitmap_scaled(frame, x - DESTROYED_BASE_WIDTH / 2, TARGET_TOP, destroyedBaseBitmap, DESTROYED_BASE_WIDTH, TARGET_HEIGHT, DESTROYED_BASE_WIDTH, TARGET_HEIGHT, color);
- Execute
char label[12]; - Execute
snprintf(label, sizeof(label), "%c %u", baseButtons[i], bases[i].ammo); - Execute
draw_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:
- Decision
!running || frame == NULLTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState == STATE_SCORESTrue- Execute
drawHighScoreEntry(frame); - Exit this function
return;
FalseContinue after this decision.
- Decision
gameState == STATE_MODETrue- Execute
clear(frame, COLOR_BLACK); - Execute
starfield_draw(frame, &stars); - Execute
draw_text_centred(frame, 160, 22, "MISSILE COMMAND", 3, COLOR_CYAN); - Execute
draw_text_centred(frame, 160, 62, "SELECT MODE", 2, COLOR_LIGHTGRAY); - Execute
draw_text_centred(frame, 100, 96, "PLAY", 2, auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW); - Execute
draw_text_centred(frame, 220, 96, "AUTO", 2, auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY); - Execute
draw_text_centred(frame, 160, 145, "D/B/STICK CHANGE A/K START", 1, COLOR_LIGHTGRAY); - Exit this function
return;
FalseContinue after this decision.
- Execute
clear(frame, COLOR_BLACK); - Execute
starfield_draw(frame, &stars); - FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Missile *missile = &enemies[i]; - Decision
!missile->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y); - Execute
draw_line(frame, (int)missile->startX, (int)missile->startY, x, y, COLOR_RED); - Execute
fill_rect(frame, x - 1, y - 1, 3, 3, COLOR_WHITE);
- FOR loop
unsigned i = 0; i < MAX_INTERCEPTORS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Missile *missile = &interceptors[i]; - Decision
!missile->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int x = (int)lroundf(missile->x), y = (int)lroundf(missile->y); - Execute
draw_line(frame, (int)missile->startX, (int)missile->startY, x, y, baseColors[missile->target]); - Execute
fill_rect(frame, x - 1, y - 1, 2, 2, COLOR_WHITE);
- FOR loop
unsigned i = 0; i < MAX_EXPLOSIONS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Explosion *explosion = &explosions[i]; - Decision
!explosion->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int x = (int)lroundf(explosion->x), y = (int)lroundf(explosion->y); - Execute
const int radius = (int)explosion->radius; - Execute
fill_circle(frame, x, y, radius, explosion->defensive ? COLOR_DARKORANGE : COLOR_DARKRED); - Execute
draw_ring(frame, x, y, radius, COLOR_YELLOW); - Decision
radius > 2True- Execute
fill_circle(frame, x, y, radius / 2, COLOR_ORANGE);
FalseContinue after this decision.
- Decision
radius > 4True- Execute
fill_circle(frame, x, y, radius / 4, COLOR_WHITE);
FalseContinue after this decision.
- Execute
drawGround(frame); - FOR loop
unsigned i = 0; i < MAX_INTERCEPTORS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Missile *missile = &interceptors[i]; - Decision
!missile->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int x = (int)lroundf(missile->targetX); - Execute
const int y = (int)lroundf(missile->targetY); - Execute
const uint16_t color = baseColors[missile->target]; - Execute
draw_line(frame, x - 2, y - 2, x + 2, y + 2, color); - Execute
draw_line(frame, x - 2, y + 2, x + 2, y - 2, color);
- Decision
gameState == STATE_PLAYINGTrue- Execute
const int x = (int)lroundf(aimX), y = (int)lroundf(aimY); - Execute
draw_line(frame, x - 5, y, x - 2, y, COLOR_WHITE); - Execute
draw_line(frame, x + 2, y, x + 5, y, COLOR_WHITE); - Execute
draw_line(frame, x, y - 5, x, y - 2, COLOR_WHITE); - Execute
draw_line(frame, x, y + 2, x, y + 5, COLOR_WHITE);
FalseContinue after this decision.
- Execute
fill_rect(frame, 0, 0, LCD_WIDTH, 13, COLOR_BLACK); - Execute
char text[40]; - Execute
snprintf(text, sizeof(text), "SCORE %u", (unsigned)score); - Execute
draw_text(frame, 3, 3, text, 1, COLOR_WHITE); - Execute
snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave); - Execute
draw_text_centred(frame, 183, 3, text, 1, COLOR_YELLOW); - Execute
snprintf(text, sizeof(text), "CITIES %u", livingCities()); - Execute
draw_text(frame, 266, 3, text, 1, COLOR_CYAN); - Decision
gameState == STATE_READYTrue- Execute
fill_rect(frame, 9, 22, 302, 109, COLOR_BLACK); - Execute
draw_text_centred(frame, 160, 25, "MISSILE COMMAND", 3, COLOR_CYAN); - Execute
draw_text_centred(frame, 160, 57, "DEFEND YOUR SIX CITIES", 1, COLOR_WHITE); - Execute
draw_text_centred(frame, 160, 73, "JOYSTICK AIM", 1, COLOR_WHITE); - Execute
draw_text_centred(frame, 160, 87, "D LEFT A CENTRE B RIGHT", 1, COLOR_YELLOW); - Execute
draw_text_centred(frame, 160, 123, "BOOT BACK", 1, COLOR_LIGHTGRAY);
False- Decision
gameState == STATE_WAVE_CLEARTrue- Execute
fill_rect(frame, 37, 44, 246, 72, COLOR_BLACK); - Execute
draw_rect(frame, 37, 44, 246, 72, COLOR_DARKORANGE); - Execute
draw_text_centred(frame, 160, 50, "WAVE CLEAR", 2, COLOR_YELLOW); - Execute
snprintf(text, sizeof(text), "MISSILES %u X 5 +%u", waveMissilesRemaining, (unsigned)waveMissileAwarded); - Execute
draw_text_centred(frame, 160, 72, text, 1, COLOR_WHITE); - Decision
waveTallyElapsed >= WAVE_TALLY_MISSILES_MSTrue- Execute
snprintf(text, sizeof(text), "CITIES %u X 100 +%u", waveCitiesRemaining, (unsigned)waveCityAwarded); - Execute
draw_text_centred(frame, 160, 87, text, 1, COLOR_CYAN);
FalseContinue after this decision.
- Decision
waveTallyElapsed >= WAVE_TALLY_MISSILES_MS + WAVE_TALLY_CITIES_MSTrue- Execute
snprintf(text, sizeof(text), "WAVE BONUS +%u", (unsigned)waveBonus); - Execute
draw_text_centred(frame, 160, 102, text, 1, COLOR_YELLOW);
FalseContinue after this decision.
False- Decision
gameState == STATE_FINISHEDTrue- Execute
draw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
FalseContinue after this decision.
- Decision
auto_modeTrue- Decision
gameState == STATE_FINISHEDTrue- Execute
const uint32_t delay = aiRestartDelay(); - Execute
const uint32_t elapsed = lastUpdate - stateStarted; - Execute
snprintf(text, sizeof(text), "NEXT %u E SPEED A/K STOP", elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U));
False- Execute
snprintf(text, sizeof(text), "AUTO %s E SPEED A/K STOP", aiSpeedShortText(ai_speed));
- Execute
fill_rect(frame, 50, 15, 220, 11, COLOR_BLACK); - Execute
draw_text_centred(frame, 160, 16, text, 1, COLOR_LIGHTGRAY);
FalseContinue 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);
}
}