Invaders
Source: src/invaders.c · 695 lines · 24 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
reduceTimer() · source line 146
static uint32_t reduceTimer(uint32_t timer)
- Exit this function
return timer > STEP_MS ? timer - STEP_MS : 0;
Read original function source
static uint32_t reduceTimer(uint32_t timer)
{
return timer > STEP_MS ? timer - STEP_MS : 0;
}overlaps() · source line 151
static bool overlaps(float ax, float ay, float aw, float ah,
float bx, float by, float bw, float bh)- Exit this function
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
Read original function source
static bool overlaps(float ax, float ay, float aw, float ah,
float bx, float by, float bw, float bh)
{
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}addPoints() · source line 157
static void addPoints(InvaderType type)
- Execute
const uint32_t points = artwork[type].points; - Execute
score = score > UINT32_MAX - points ? UINT32_MAX : score + points; - WHILE loop
extra_lives_awarded < sizeof(extra_life_scores) / sizeof(extra_life_scores[0]) && score >= extra_life_scores[extra_lives_awarded]Repeat body while the loop condition permits; then continue below.
- Execute
++lives; - Execute
ESP_LOGI("invaders", "EXTRA LIFE: %u POINTS, %u LIVES", (unsigned)extra_life_scores[extra_lives_awarded], (unsigned)lives); - Execute
++extra_lives_awarded;
Read original function source
static void addPoints(InvaderType type)
{
const uint32_t points = artwork[type].points;
score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
while (extra_lives_awarded < sizeof(extra_life_scores) / sizeof(extra_life_scores[0]) &&
score >= extra_life_scores[extra_lives_awarded]) {
++lives;
ESP_LOGI("invaders", "EXTRA LIFE: %u POINTS, %u LIVES",
(unsigned)extra_life_scores[extra_lives_awarded], (unsigned)lives);
++extra_lives_awarded;
}
}startHitEffect() · source line 170
static void startHitEffect(int x, int y, InvaderType type)
- FOR loop
unsigned i = 0; i < MAX_HIT_EFFECTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
hitEffects[i].remaining != 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
hitEffects[i] = (HitEffect){x, y, artwork[type].color, HIT_FRAME_MS * 2U}; - Exit this function
return;
- Execute
ESP_LOGW("invaders", "No free hit effect slot");
Read original function source
static void startHitEffect(int x, int y, InvaderType type)
{
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
if (hitEffects[i].remaining != 0) continue;
hitEffects[i] = (HitEffect){x, y, artwork[type].color, HIT_FRAME_MS * 2U};
return;
}
ESP_LOGW("invaders", "No free hit effect slot");
}updateHitEffects() · source line 180
static void updateHitEffects(uint32_t elapsed)
- FOR loop
unsigned i = 0; i < MAX_HIT_EFFECTS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
HitEffect *effect = &hitEffects[i]; - Execute
effect->remaining = effect->remaining > elapsed ? effect->remaining - elapsed : 0;
Read original function source
static void updateHitEffects(uint32_t elapsed)
{
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
HitEffect *effect = &hitEffects[i];
effect->remaining = effect->remaining > elapsed ? effect->remaining - elapsed : 0;
}
}hitEffectsActive() · source line 188
static bool hitEffectsActive(void)
- FOR loop
unsigned i = 0; i < MAX_HIT_EFFECTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
hitEffects[i].remaining != 0True- Exit this function
return true;
FalseContinue after this decision.
- Exit this function
return false;
Read original function source
static bool hitEffectsActive(void)
{
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
if (hitEffects[i].remaining != 0) return true;
}
return false;
}scheduleMothership() · source line 196
static void scheduleMothership(void)
- Execute
mothershipActive = false; - Execute
mothershipDelay = 8000U + esp_random() % 6001U; - Execute
mothershipFrame = mothershipElapsed = 0;
Read original function source
static void scheduleMothership(void)
{
mothershipActive = false;
mothershipDelay = 8000U + esp_random() % 6001U;
mothershipFrame = mothershipElapsed = 0;
}beginWave() · source line 203
static void beginWave(void)
Calls into this documentation:
- Execute
memset(hitEffects, 0, sizeof(hitEffects)); - FOR loop
unsigned row = 0; row < INVADER_ROWS; ++rowRepeat body while the loop condition permits; then continue below.
- FOR loop
unsigned column = 0; column < INVADER_COLUMNS; ++columnRepeat body while the loop condition permits; then continue below.
- Execute
alive[row][column] = true;
- Execute
remaining = INVADER_ROWS * INVADER_COLUMNS; - Execute
formationX = PLAY_LEFT + (PLAY_WIDTH - ((INVADER_COLUMNS - 1) * COLUMN_SPACING + INVADER_SIZE)) / 2; - Execute
formationY = FORMATION_TOP; - Execute
marchDirection = 1; - Execute
marchFrame = marchElapsed = 0; - Execute
playerBullet.active = false; - Execute
memset(enemyBullets, 0, sizeof(enemyBullets)); - Execute
enemyFireDelay = 1000U; - Execute
fireDelay = 0; - Execute
autoDodgeUntil = 0U; - Execute
autoDodgeDirection = 0; - Execute
autoIgnoredBullet = -1; - Execute
scheduleMothership(); - Execute
ESP_LOGI("invaders", "WAVE %u", (unsigned)wave);
Read original function source
static void beginWave(void)
{
memset(hitEffects, 0, sizeof(hitEffects));
for (unsigned row = 0; row < INVADER_ROWS; ++row) {
for (unsigned column = 0; column < INVADER_COLUMNS; ++column) {
alive[row][column] = true;
}
}
remaining = INVADER_ROWS * INVADER_COLUMNS;
formationX = PLAY_LEFT + (PLAY_WIDTH - ((INVADER_COLUMNS - 1) * COLUMN_SPACING + INVADER_SIZE)) / 2;
formationY = FORMATION_TOP;
marchDirection = 1;
marchFrame = marchElapsed = 0;
playerBullet.active = false;
memset(enemyBullets, 0, sizeof(enemyBullets));
enemyFireDelay = 1000U;
fireDelay = 0;
autoDodgeUntil = 0U;
autoDodgeDirection = 0;
autoIgnoredBullet = -1;
scheduleMothership();
ESP_LOGI("invaders", "WAVE %u", (unsigned)wave);
}beginGame() · source line 227
static void beginGame(uint32_t now_ms)
Calls into this documentation:
- Execute
score = 0; - Execute
wave = 1; - Execute
lives = gameConfigInvadersStartingLives(); - Execute
extra_lives_awarded = 0U; - Execute
cannonX = PLAY_LEFT + (PLAY_WIDTH - CANNON_WIDTH) / 2.0f; - Execute
protection = PROTECTION_MS; - Execute
controlsReady = false; - Execute
lastUpdate = now_ms; - Execute
accumulator = 0; - Execute
gameState = STATE_PLAYING; - Execute
beginWave(); - Execute
audioPlay(AUDIO_SFX_GAME_START);
Read original function source
static void beginGame(uint32_t now_ms)
{
score = 0;
wave = 1;
lives = gameConfigInvadersStartingLives();
extra_lives_awarded = 0U;
cannonX = PLAY_LEFT + (PLAY_WIDTH - CANNON_WIDTH) / 2.0f;
protection = PROTECTION_MS;
controlsReady = false;
lastUpdate = now_ms;
accumulator = 0;
gameState = STATE_PLAYING;
beginWave();
audioPlay(AUDIO_SFX_GAME_START);
}finishGame() · source line 243
static void finishGame(void)
Calls into this documentation:
- Decision
gameState != STATE_PLAYINGTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
gameState = STATE_FINISHED; - Execute
gameOverStarted = lastUpdate; - Execute
audioPlay(AUDIO_SFX_GAME_OVER); - Execute
ESP_LOGI("invaders", "GAME OVER: SCORE %u, WAVE %u", (unsigned)score, (unsigned)wave); - Execute
const uint32_t gold = score / 1000U; - Decision
gold != 0True- Decision
(autoMode ? addAIGold(gold) : addUserGoldWon(gold))True- Execute
ESP_LOGI("invaders", "GOLD WON: %u", (unsigned)gold);
False- Execute
ESP_LOGE("invaders", "Failed to save %u Gold", (unsigned)gold);
FalseContinue after this decision.
Read original function source
static void finishGame(void)
{
if (gameState != STATE_PLAYING) return;
gameState = STATE_FINISHED;
gameOverStarted = lastUpdate;
audioPlay(AUDIO_SFX_GAME_OVER);
ESP_LOGI("invaders", "GAME OVER: SCORE %u, WAVE %u", (unsigned)score, (unsigned)wave);
// Award whole thousands once, when the game ends.
const uint32_t gold = score / 1000U;
if (gold != 0) {
if ((autoMode ? addAIGold(gold) : addUserGoldWon(gold))) {
ESP_LOGI("invaders", "GOLD WON: %u", (unsigned)gold);
} else {
ESP_LOGE("invaders", "Failed to save %u Gold", (unsigned)gold);
}
}
}autoDodgeMovement() · source line 261
static int autoDodgeMovement(uint32_t now_ms)
- Decision
autoDodgeDirection != 0 && (int32_t)(now_ms - autoDodgeUntil) < 0True- Exit this function
return autoDodgeDirection;
FalseContinue after this decision.
- Execute
autoDodgeDirection = 0; - Decision
autoIgnoredBullet >= 0True- Execute
const Bullet *ignored = &enemyBullets[autoIgnoredBullet]; - Decision
!ignored->active || ignored->y < CANNON_Y - AUTO_DODGE_TRIGGER_Y || ignored->y > CANNON_Y + CANNON_HEIGHTTrue- Execute
autoIgnoredBullet = -1;
FalseContinue after this decision.
FalseContinue after this decision.
- Execute
const int cannonCentre = (int)cannonX + CANNON_WIDTH / 2; - FOR loop
unsigned i = 0; i < MAX_ENEMY_BULLETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Bullet *bullet = &enemyBullets[i]; - Decision
!bullet->active || bullet->y < CANNON_Y - AUTO_DODGE_TRIGGER_Y || bullet->y > CANNON_Y + CANNON_HEIGHTTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
(int)i == autoIgnoredBullet || bullet->x < cannonX - 8.0f || bullet->x > cannonX + CANNON_WIDTH + 8.0fTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
esp_random() % 100U >= AUTO_DODGE_CHANCE_PERCENTTrue- Execute
autoIgnoredBullet = (int)i; - Exit this function
return 0;
FalseContinue after this decision.
- Execute
autoDodgeDirection = bullet->x < cannonCentre ? 1 : -1; - Decision
autoDodgeDirection < 0 && cannonX <= PLAY_LEFT + 8True- Execute
autoDodgeDirection = 1;
FalseContinue after this decision.
- Decision
autoDodgeDirection > 0 && cannonX >= PLAY_RIGHT - CANNON_WIDTH - 8True- Execute
autoDodgeDirection = -1;
FalseContinue after this decision.
- Execute
autoDodgeUntil = now_ms + AUTO_DODGE_MS; - Exit this function
return autoDodgeDirection;
- Exit this function
return 0;
Read original function source
static int autoDodgeMovement(uint32_t now_ms)
{
if (autoDodgeDirection != 0 && (int32_t)(now_ms - autoDodgeUntil) < 0) {
return autoDodgeDirection;
}
autoDodgeDirection = 0;
if (autoIgnoredBullet >= 0) {
const Bullet *ignored = &enemyBullets[autoIgnoredBullet];
if (!ignored->active || ignored->y < CANNON_Y - AUTO_DODGE_TRIGGER_Y ||
ignored->y > CANNON_Y + CANNON_HEIGHT) {
autoIgnoredBullet = -1;
}
}
const int cannonCentre = (int)cannonX + CANNON_WIDTH / 2;
for (unsigned i = 0; i < MAX_ENEMY_BULLETS; ++i) {
const Bullet *bullet = &enemyBullets[i];
if (!bullet->active || bullet->y < CANNON_Y - AUTO_DODGE_TRIGGER_Y ||
bullet->y > CANNON_Y + CANNON_HEIGHT) continue;
if ((int)i == autoIgnoredBullet ||
bullet->x < cannonX - 8.0f || bullet->x > cannonX + CANNON_WIDTH + 8.0f) continue;
if (esp_random() % 100U >= AUTO_DODGE_CHANCE_PERCENT) {
autoIgnoredBullet = (int)i;
return 0;
}
autoDodgeDirection = bullet->x < cannonCentre ? 1 : -1;
if (autoDodgeDirection < 0 && cannonX <= PLAY_LEFT + 8) autoDodgeDirection = 1;
if (autoDodgeDirection > 0 && cannonX >= PLAY_RIGHT - CANNON_WIDTH - 8) autoDodgeDirection = -1;
autoDodgeUntil = now_ms + AUTO_DODGE_MS;
return autoDodgeDirection;
}
return 0;
}autoMovement() · source line 297
static int autoMovement(uint32_t now_ms)
Calls into this documentation:
- Execute
const int dodgeMovement = autoDodgeMovement(now_ms); - Decision
dodgeMovement != 0True- Exit this function
return dodgeMovement;
FalseContinue after this decision.
- Execute
const int cannonCentre = (int)cannonX + CANNON_WIDTH / 2; - Execute
int targetX = cannonCentre; - Execute
int bestDistance = PLAY_RIGHT - PLAY_LEFT; - FOR loop
int column = 0; column < INVADER_COLUMNS; ++columnRepeat body while the loop condition permits; then continue below.
- Execute
int lowestRow = -1; - FOR loop
int row = INVADER_ROWS - 1; row >= 0; --rowRepeat body while the loop condition permits; then continue below.
- Decision
alive[row][column]True- Execute
lowestRow = row; - Exit enclosing loop or switch
break;
FalseContinue after this decision.
- Decision
lowestRow < 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int invaderX = formationX + column * COLUMN_SPACING + INVADER_SIZE / 2; - Execute
const int distance = invaderX > cannonCentre ? invaderX - cannonCentre : cannonCentre - invaderX; - Decision
distance < bestDistanceTrue- Execute
bestDistance = distance; - Execute
targetX = invaderX;
FalseContinue after this decision.
- Exit this function
return targetX > cannonCentre + 2 ? 1 : targetX < cannonCentre - 2 ? -1 : 0;
Read original function source
static int autoMovement(uint32_t now_ms)
{
const int dodgeMovement = autoDodgeMovement(now_ms);
if (dodgeMovement != 0) return dodgeMovement;
const int cannonCentre = (int)cannonX + CANNON_WIDTH / 2;
int targetX = cannonCentre;
int bestDistance = PLAY_RIGHT - PLAY_LEFT;
for (int column = 0; column < INVADER_COLUMNS; ++column) {
int lowestRow = -1;
for (int row = INVADER_ROWS - 1; row >= 0; --row) {
if (alive[row][column]) {
lowestRow = row;
break;
}
}
if (lowestRow < 0) continue;
const int invaderX = formationX + column * COLUMN_SPACING + INVADER_SIZE / 2;
const int distance = invaderX > cannonCentre ? invaderX - cannonCentre : cannonCentre - invaderX;
if (distance < bestDistance) {
bestDistance = distance;
targetX = invaderX;
}
}
return targetX > cannonCentre + 2 ? 1 : targetX < cannonCentre - 2 ? -1 : 0;
}marchFormation() · source line 325
static void marchFormation(void)
Calls into this documentation:
- Execute
int left = PLAY_RIGHT, right = PLAY_LEFT; - FOR loop
int row = 0; row < INVADER_ROWS; ++rowRepeat body while the loop condition permits; then continue below.
- FOR loop
int column = 0; column < INVADER_COLUMNS; ++columnRepeat body while the loop condition permits; then continue below.
- Decision
!alive[row][column]True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int x = formationX + column * COLUMN_SPACING; - Decision
x < leftTrue- Execute
left = x;
FalseContinue after this decision.
- Decision
x + INVADER_SIZE > rightTrue- Execute
right = x + INVADER_SIZE;
FalseContinue after this decision.
- Decision
left + marchDirection * 4 < PLAY_LEFT + 3 || right + marchDirection * 4 > PLAY_RIGHT - 3True- Execute
formationY += 4; - Execute
marchDirection = -marchDirection;
False- Execute
formationX += marchDirection * 4;
- Execute
marchFrame ^= 1U; - FOR loop
int row = 0; row < INVADER_ROWS; ++rowRepeat body while the loop condition permits; then continue below.
- Decision
formationY + row * ROW_SPACING + INVADER_SIZE < CANNON_YTrue- Next loop iteration
continue;
FalseContinue after this decision.
- FOR loop
int column = 0; column < INVADER_COLUMNS; ++columnRepeat body while the loop condition permits; then continue below.
- Decision
alive[row][column]True- Execute
finishGame(); - Exit this function
return;
FalseContinue after this decision.
Read original function source
static void marchFormation(void)
{
int left = PLAY_RIGHT, right = PLAY_LEFT;
for (int row = 0; row < INVADER_ROWS; ++row) {
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (!alive[row][column]) continue;
const int x = formationX + column * COLUMN_SPACING;
if (x < left) left = x;
if (x + INVADER_SIZE > right) right = x + INVADER_SIZE;
}
}
if (left + marchDirection * 4 < PLAY_LEFT + 3 ||
right + marchDirection * 4 > PLAY_RIGHT - 3) {
formationY += 4;
marchDirection = -marchDirection;
} else {
formationX += marchDirection * 4;
}
// The whole formation changes pose once per marching step, including drops.
marchFrame ^= 1U;
for (int row = 0; row < INVADER_ROWS; ++row) {
if (formationY + row * ROW_SPACING + INVADER_SIZE < CANNON_Y) continue;
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (alive[row][column]) {
finishGame();
return;
}
}
}
}fireEnemyBullet() · source line 356
static void fireEnemyBullet(void)
Calls into this documentation:
- FOR loop
unsigned slot = 0; slot < MAX_ENEMY_BULLETS; ++slotRepeat body while the loop condition permits; then continue below.
- Decision
enemyBullets[slot].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const unsigned first = esp_random() % INVADER_COLUMNS; - FOR loop
unsigned offset = 0; offset < INVADER_COLUMNS; ++offsetRepeat body while the loop condition permits; then continue below.
- Execute
const unsigned column = (first + offset) % INVADER_COLUMNS; - FOR loop
int row = INVADER_ROWS - 1; row >= 0; --rowRepeat body while the loop condition permits; then continue below.
- Decision
!alive[row][column]True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
enemyBullets[slot] = (Bullet){ .x = formationX + (int)column * COLUMN_SPACING + INVADER_SIZE / 2 - 1, .y = formationY + row * ROW_SPACING + INVADER_SIZE, .active = true, }; - Execute
audioPlay(AUDIO_SFX_ENEMY_SHOT); - Exit this function
return;
- Exit this function
return;
Read original function source
static void fireEnemyBullet(void)
{
for (unsigned slot = 0; slot < MAX_ENEMY_BULLETS; ++slot) {
if (enemyBullets[slot].active) continue;
const unsigned first = esp_random() % INVADER_COLUMNS;
for (unsigned offset = 0; offset < INVADER_COLUMNS; ++offset) {
const unsigned column = (first + offset) % INVADER_COLUMNS;
// Only the lowest surviving invader in a column fires.
for (int row = INVADER_ROWS - 1; row >= 0; --row) {
if (!alive[row][column]) continue;
enemyBullets[slot] = (Bullet){
.x = formationX + (int)column * COLUMN_SPACING + INVADER_SIZE / 2 - 1,
.y = formationY + row * ROW_SPACING + INVADER_SIZE,
.active = true,
};
audioPlay(AUDIO_SFX_ENEMY_SHOT);
return;
}
}
return;
}
}updateMothership() · source line 379
static void updateMothership(void)
Calls into this documentation:
- Decision
!mothershipActiveTrue- Execute
mothershipDelay = reduceTimer(mothershipDelay); - Decision
mothershipDelay != 0True- Exit this function
return;
FalseContinue after this decision.
- Execute
mothershipDirection = (esp_random() & 1U) != 0 ? 1 : -1; - Execute
mothershipX = mothershipDirection > 0 ? PLAY_LEFT - INVADER_SIZE : PLAY_RIGHT; - Execute
mothershipActive = true; - Execute
mothershipFrame = mothershipElapsed = 0;
FalseContinue after this decision.
- Execute
mothershipX += mothershipDirection * 28.0f * STEP_MS / 1000.0f; - Execute
mothershipElapsed += STEP_MS; - Decision
mothershipElapsed >= 200UTrue- Execute
mothershipElapsed -= 200U; - Execute
mothershipFrame ^= 1U;
FalseContinue after this decision.
- Decision
(mothershipDirection > 0 && mothershipX >= PLAY_RIGHT) || (mothershipDirection < 0 && mothershipX + INVADER_SIZE <= PLAY_LEFT)True- Execute
scheduleMothership();
FalseContinue after this decision.
Read original function source
static void updateMothership(void)
{
if (!mothershipActive) {
mothershipDelay = reduceTimer(mothershipDelay);
if (mothershipDelay != 0) return;
mothershipDirection = (esp_random() & 1U) != 0 ? 1 : -1;
mothershipX = mothershipDirection > 0 ? PLAY_LEFT - INVADER_SIZE : PLAY_RIGHT;
mothershipActive = true;
mothershipFrame = mothershipElapsed = 0;
}
mothershipX += mothershipDirection * 28.0f * STEP_MS / 1000.0f;
mothershipElapsed += STEP_MS;
if (mothershipElapsed >= 200U) {
mothershipElapsed -= 200U;
mothershipFrame ^= 1U;
}
if ((mothershipDirection > 0 && mothershipX >= PLAY_RIGHT) ||
(mothershipDirection < 0 && mothershipX + INVADER_SIZE <= PLAY_LEFT)) {
scheduleMothership();
}
}updatePlayerBullet() · source line 401
static void updatePlayerBullet(void)
Calls into this documentation:
- Decision
!playerBullet.activeTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
playerBullet.y -= 220.0f * STEP_MS / 1000.0f; - FOR loop
int row = INVADER_ROWS - 1; row >= 0; --rowRepeat body while the loop condition permits; then continue below.
- FOR loop
int column = 0; column < INVADER_COLUMNS; ++columnRepeat body while the loop condition permits; then continue below.
- Decision
!alive[row][column]True- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
overlaps(playerBullet.x, playerBullet.y, 2, 5, formationX + column * COLUMN_SPACING, formationY + row * ROW_SPACING, INVADER_SIZE, INVADER_SIZE)True- Execute
alive[row][column] = false; - Execute
--remaining; - Execute
playerBullet.active = false; - Execute
addPoints(rowTypes[row]); - Execute
startHitEffect(formationX + column * COLUMN_SPACING, formationY + row * ROW_SPACING, rowTypes[row]); - Execute
audioPlay(AUDIO_SFX_HIT); - Decision
remaining == 0UTrue- Execute
audioPlay(AUDIO_SFX_WAVE_CLEAR);
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
mothershipActive && overlaps(playerBullet.x, playerBullet.y, 2, 5, mothershipX, MOTHERSHIP_Y, INVADER_SIZE, INVADER_SIZE)True- Execute
playerBullet.active = false; - Execute
addPoints(TYPE_4); - Execute
startHitEffect((int)mothershipX, MOTHERSHIP_Y, TYPE_4); - Execute
scheduleMothership(); - Execute
audioPlay(AUDIO_SFX_HIT); - Execute
ESP_LOGI("invaders", "MOTHERSHIP HIT: 50 POINTS");
False- Decision
playerBullet.y + 5 <= PLAY_TOPTrue- Execute
playerBullet.active = false;
FalseContinue after this decision.
Read original function source
static void updatePlayerBullet(void)
{
if (!playerBullet.active) return;
playerBullet.y -= 220.0f * STEP_MS / 1000.0f;
// Rectangular sprite bounds are used for hits, independent of artwork bits.
// Check bottom rows first so a shot hits the nearest invader in its path.
for (int row = INVADER_ROWS - 1; row >= 0; --row) {
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (!alive[row][column]) continue;
if (overlaps(playerBullet.x, playerBullet.y, 2, 5,
formationX + column * COLUMN_SPACING,
formationY + row * ROW_SPACING, INVADER_SIZE, INVADER_SIZE)) {
alive[row][column] = false;
--remaining;
playerBullet.active = false;
addPoints(rowTypes[row]);
startHitEffect(formationX + column * COLUMN_SPACING,
formationY + row * ROW_SPACING, rowTypes[row]);
audioPlay(AUDIO_SFX_HIT);
if (remaining == 0U) audioPlay(AUDIO_SFX_WAVE_CLEAR);
return;
}
}
}
if (mothershipActive && overlaps(playerBullet.x, playerBullet.y, 2, 5,
mothershipX, MOTHERSHIP_Y, INVADER_SIZE, INVADER_SIZE)) {
playerBullet.active = false;
addPoints(TYPE_4);
startHitEffect((int)mothershipX, MOTHERSHIP_Y, TYPE_4);
scheduleMothership();
audioPlay(AUDIO_SFX_HIT);
ESP_LOGI("invaders", "MOTHERSHIP HIT: 50 POINTS");
} else if (playerBullet.y + 5 <= PLAY_TOP) {
playerBullet.active = false;
}
}simulateStep() · source line 438
static void simulateStep(int movement, bool fire)
Calls into this documentation:
- Decision
remaining == 0True- Decision
!hitEffectsActive()True- Decision
wave < UINT32_MAXTrue- Execute
++wave;
FalseContinue after this decision.
- Execute
beginWave();
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Execute
protection = reduceTimer(protection); - Execute
fireDelay = reduceTimer(fireDelay); - Execute
cannonX += movement * 125.0f * STEP_MS / 1000.0f; - Decision
cannonX < PLAY_LEFT + 1True- Execute
cannonX = PLAY_LEFT + 1;
FalseContinue after this decision.
- Decision
cannonX > PLAY_RIGHT - CANNON_WIDTH - 1True- Execute
cannonX = PLAY_RIGHT - CANNON_WIDTH - 1;
FalseContinue after this decision.
- Decision
fire && fireDelay == 0 && !playerBullet.activeTrue- Execute
playerBullet = (Bullet){cannonX + CANNON_WIDTH / 2 - 1, CANNON_Y - 5, true}; - Execute
fireDelay = FIRE_DELAY_MS; - Execute
audioPlay(AUDIO_SFX_PLAYER_SHOT);
FalseContinue after this decision.
- Execute
updateMothership(); - Execute
updatePlayerBullet(); - Decision
remaining == 0True- Execute
memset(enemyBullets, 0, sizeof(enemyBullets)); - Exit this function
return;
FalseContinue after this decision.
- Execute
const uint32_t difficulty = wave < 9U ? wave - 1U : 8U; - Execute
const uint32_t base_interval = 120U + remaining * 30U; - Execute
const uint32_t normal_march_interval = base_interval > 80U + difficulty * 20U ? base_interval - difficulty * 20U : 80U; - Execute
const uint32_t march_interval = remaining == 1U ? 40U : normal_march_interval; - Execute
marchElapsed += STEP_MS; - Decision
marchElapsed >= march_intervalTrue- Execute
marchElapsed -= march_interval; - Execute
marchFormation(); - Decision
gameState != STATE_PLAYINGTrue- Exit this function
return;
FalseContinue after this decision.
FalseContinue after this decision.
- Execute
enemyFireDelay = reduceTimer(enemyFireDelay); - Decision
enemyFireDelay == 0True- Execute
fireEnemyBullet(); - Execute
enemyFireDelay = 900U - difficulty * 50U + esp_random() % 301U;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ENEMY_BULLETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Bullet *bullet = &enemyBullets[i]; - Decision
!bullet->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
bullet->y += (60.0f + difficulty * 4.0f) * STEP_MS / 1000.0f; - Decision
bullet->y >= PLAY_BOTTOMTrue- Execute
bullet->active = false;
False- Decision
overlaps(bullet->x, bullet->y, 2, 5, cannonX, CANNON_Y, CANNON_WIDTH, CANNON_HEIGHT)True- Execute
bullet->active = false; - Decision
protection != 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
--lives; - Execute
ESP_LOGI("invaders", "PLAYER HIT: %u LIVES", (unsigned)lives); - Execute
audioPlay(AUDIO_SFX_PLAYER_EXPLOSION); - Decision
lives == 0True- Execute
finishGame(); - Exit this function
return;
FalseContinue after this decision.
- Execute
cannonX = PLAY_LEFT + (PLAY_WIDTH - CANNON_WIDTH) / 2.0f; - Execute
protection = PROTECTION_MS; - Execute
controlsReady = false; - Execute
playerBullet.active = false; - Execute
memset(enemyBullets, 0, sizeof(enemyBullets)); - Exit this function
return;
FalseContinue after this decision.
Read original function source
static void simulateStep(int movement, bool fire)
{
// Pause the cleared wave until every explosion has played both frames.
if (remaining == 0) {
if (!hitEffectsActive()) {
if (wave < UINT32_MAX) ++wave;
beginWave();
}
return;
}
protection = reduceTimer(protection);
fireDelay = reduceTimer(fireDelay);
cannonX += movement * 125.0f * STEP_MS / 1000.0f;
if (cannonX < PLAY_LEFT + 1) cannonX = PLAY_LEFT + 1;
if (cannonX > PLAY_RIGHT - CANNON_WIDTH - 1) cannonX = PLAY_RIGHT - CANNON_WIDTH - 1;
if (fire && fireDelay == 0 && !playerBullet.active) {
playerBullet = (Bullet){cannonX + CANNON_WIDTH / 2 - 1, CANNON_Y - 5, true};
fireDelay = FIRE_DELAY_MS;
audioPlay(AUDIO_SFX_PLAYER_SHOT);
}
updateMothership();
updatePlayerBullet();
if (remaining == 0) {
memset(enemyBullets, 0, sizeof(enemyBullets));
return;
}
// Wave difficulty affects marching, enemy firing cadence, and bullet speed.
const uint32_t difficulty = wave < 9U ? wave - 1U : 8U;
// Each destroyed invader removes 30 ms from the march interval, so the
// formation marches faster as the wave is cleared.
const uint32_t base_interval = 120U + remaining * 30U;
const uint32_t normal_march_interval = base_interval > 80U + difficulty * 20U
? base_interval - difficulty * 20U : 80U;
// The final survivor gets a deliberate Crazy speed, independent of wave.
const uint32_t march_interval = remaining == 1U ? 40U : normal_march_interval;
marchElapsed += STEP_MS;
if (marchElapsed >= march_interval) {
marchElapsed -= march_interval;
marchFormation();
if (gameState != STATE_PLAYING) return;
}
enemyFireDelay = reduceTimer(enemyFireDelay);
if (enemyFireDelay == 0) {
fireEnemyBullet();
// Higher waves shorten the delay between enemy shots; the random term
// keeps the firing pattern from becoming perfectly regular.
enemyFireDelay = 900U - difficulty * 50U + esp_random() % 301U;
}
for (unsigned i = 0; i < MAX_ENEMY_BULLETS; ++i) {
Bullet *bullet = &enemyBullets[i];
if (!bullet->active) continue;
// Enemy bullet travel speed increases by wave, not by each shot.
bullet->y += (60.0f + difficulty * 4.0f) * STEP_MS / 1000.0f;
if (bullet->y >= PLAY_BOTTOM) {
bullet->active = false;
} else if (overlaps(bullet->x, bullet->y, 2, 5,
cannonX, CANNON_Y, CANNON_WIDTH, CANNON_HEIGHT)) {
bullet->active = false;
if (protection != 0) continue;
--lives;
ESP_LOGI("invaders", "PLAYER HIT: %u LIVES", (unsigned)lives);
audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
if (lives == 0) {
finishGame();
return;
}
cannonX = PLAY_LEFT + (PLAY_WIDTH - CANNON_WIDTH) / 2.0f;
protection = PROTECTION_MS;
controlsReady = false;
playerBullet.active = false;
memset(enemyBullets, 0, sizeof(enemyBullets));
return;
}
}
}startInvaders() · source line 517
void startInvaders(uint32_t now_ms)
Calls into this documentation:
- Execute
starfield_initialize(&stars, 45, PLAY_LEFT, PLAY_TOP, PLAY_WIDTH, PLAY_BOTTOM - PLAY_TOP, STARFIELD_GREYSCALE); - Execute
running = true; - Execute
gameState = STATE_READY; - Execute
lastUpdate = now_ms; - Execute
accumulator = score = 0; - Execute
wave = 1; - Execute
lives = 3; - Execute
extra_lives_awarded = 0U; - Execute
memset(hitEffects, 0, sizeof(hitEffects)); - Execute
controlsReady = false; - Execute
autoMode = false; - Execute
modeSelectionReset(&modeInput); - Execute
ESP_LOGI("invaders", "START");
Read original function source
void startInvaders(uint32_t now_ms)
{
starfield_initialize(&stars, 45, PLAY_LEFT, PLAY_TOP, PLAY_WIDTH,
PLAY_BOTTOM - PLAY_TOP, STARFIELD_GREYSCALE);
running = true;
gameState = STATE_READY;
lastUpdate = now_ms;
accumulator = score = 0;
wave = 1;
lives = 3;
extra_lives_awarded = 0U;
memset(hitEffects, 0, sizeof(hitEffects));
controlsReady = false;
autoMode = false;
modeSelectionReset(&modeInput);
ESP_LOGI("invaders", "START");
}stopInvaders() · source line 535
void stopInvaders(void)
Calls into this documentation:
- Execute
cancelHighScoreEntry(); - Execute
running = false; - Execute
memset(hitEffects, 0, sizeof(hitEffects)); - Execute
ESP_LOGI("invaders", "STOP");
Read original function source
void stopInvaders(void)
{
cancelHighScoreEntry();
running = false;
memset(hitEffects, 0, sizeof(hitEffects));
ESP_LOGI("invaders", "STOP");
}invadersAction() · source line 543
void invadersAction(bool start, bool stop_auto, uint32_t now_ms)
Calls into this documentation:
- Decision
running && start && gameState == STATE_READYTrue- Execute
ESP_LOGI("invaders", "START: %s", autoMode ? "AUTO" : "PLAY"); - Execute
beginGame(now_ms);
False- Decision
running && stop_auto && autoMode && gameState == STATE_PLAYINGTrue- Execute
autoMode = false; - Execute
modeSelectionReset(&modeInput); - Execute
gameState = STATE_READY;
FalseContinue after this decision.
Read original function source
void invadersAction(bool start, bool stop_auto, uint32_t now_ms)
{
if (running && start && gameState == STATE_READY) {
ESP_LOGI("invaders", "START: %s", autoMode ? "AUTO" : "PLAY");
beginGame(now_ms);
} else if (running && stop_auto && autoMode && gameState == STATE_PLAYING) {
autoMode = false;
modeSelectionReset(&modeInput);
gameState = STATE_READY;
}
}updateInvaders() · source line 555
void updateInvaders(int x_offset, int y_offset, bool valid, bool left, bool right,
bool fire, 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
updateHitEffects(elapsed); - Execute
starfield_update(&stars, elapsed); - Decision
gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000UTrue- Decision
autoModeTrue- Execute
beginGame(now_ms);
False- Execute
startHighScoreEntry(HIGH_SCORE_INVADERS, 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_READYTrue- Decision
modeSelectionNext(&modeInput, x_offset, y_offset, valid, left, right, now_ms)True- Execute
autoMode = !autoMode;
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
!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.
- Execute
const int movement = left || right ? (int)right - (int)left : valid && controlsReady ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0; - Execute
const int automatedMovement = autoMode ? autoMovement(now_ms) : 0; - 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(autoMode ? automatedMovement : (left || right || controlsReady) ? movement : 0, autoMode || fire);
Read original function source
void updateInvaders(int x_offset, int y_offset, bool valid, bool left, bool right,
bool fire, uint32_t now_ms)
{
if (!running) return;
const uint32_t elapsed = now_ms - lastUpdate;
lastUpdate = now_ms;
// Effects also expire on the game-over screen. Update before new hits are
// created so a new explosion gets its full first frame on the next draw.
updateHitEffects(elapsed);
starfield_update(&stars, elapsed);
if (gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000U) {
if (autoMode) beginGame(now_ms);
else {
startHighScoreEntry(HIGH_SCORE_INVADERS, 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_READY) {
if (modeSelectionNext(&modeInput, x_offset, y_offset, valid, left, right, now_ms)) {
autoMode = !autoMode;
}
return;
}
if (gameState != STATE_PLAYING) return;
if (!valid) controlsReady = false;
else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
controlsReady = true;
const int movement = left || right ? (int)right - (int)left
: valid && controlsReady ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0;
const int automatedMovement = autoMode ? autoMovement(now_ms) : 0;
// Bound catch-up after a stall; collisions are checked every 10 ms.
accumulator += elapsed > 100U ? 100U : elapsed;
while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
accumulator -= STEP_MS;
simulateStep(autoMode ? automatedMovement : (left || right || controlsReady) ? movement : 0,
autoMode || fire);
}
}drawInvader() · source line 598
static void drawInvader(uint16_t *frame, InvaderType type, unsigned pose, int x, int y)
Calls into this documentation:
- Execute
draw_bitmap_scaled(frame, x, y, artwork[type].frames[pose], 8, 8, INVADER_SIZE, INVADER_SIZE, artwork[type].color);
Read original function source
static void drawInvader(uint16_t *frame, InvaderType type, unsigned pose, int x, int y)
{
draw_bitmap_scaled(frame, x, y, artwork[type].frames[pose], 8, 8,
INVADER_SIZE, INVADER_SIZE, artwork[type].color);
}drawInvadersLayout() · source line 604
static void drawInvadersLayout(uint16_t *frame)
Calls into this documentation:
- Execute
const int text_x = PLAY_RIGHT + 7; - Execute
const uint16_t border = 0x7bef; - Execute
const uint16_t cyan = RGB565(50, 205, 255); - Execute
char value[16]; - Execute
fill_rect(frame, PLAY_RIGHT, 0, LCD_WIDTH - PLAY_RIGHT, LCD_HEIGHT, 0); - Execute
draw_rect(frame, 0, 0, LCD_WIDTH, LCD_HEIGHT, border); - Execute
fill_rect(frame, PLAY_RIGHT, 0, 1, LCD_HEIGHT, border); - Execute
draw_text(frame, text_x, 12, "SCORE", 1, cyan); - Execute
snprintf(value, sizeof(value), "%u", (unsigned)score); - Execute
draw_text(frame, text_x, 28, value, 1, 0xffff); - Execute
draw_text(frame, text_x, 61, "LIVES", 1, cyan); - Execute
snprintf(value, sizeof(value), "%u", (unsigned)lives); - Execute
draw_text(frame, text_x, 77, value, 2, RGB565(70, 245, 130)); - Execute
draw_text(frame, text_x, 112, "WAVE", 1, cyan); - Execute
snprintf(value, sizeof(value), "%u", (unsigned)wave); - Execute
draw_text(frame, text_x, 128, value, 1, 0xffff); - Decision
autoModeTrue- Execute
draw_text(frame, text_x, 145, "AUTO", 1, COLOR_YELLOW);
FalseContinue after this decision.
Read original function source
static void drawInvadersLayout(uint16_t *frame)
{
const int text_x = PLAY_RIGHT + 7;
const uint16_t border = 0x7bef;
const uint16_t cyan = RGB565(50, 205, 255);
char value[16];
// Draw last to mask sprites entering/exiting at the partition and protect
// all four outer edges. PLAY_RIGHT and PLAY_BOTTOM are exclusive bounds.
fill_rect(frame, PLAY_RIGHT, 0, LCD_WIDTH - PLAY_RIGHT, LCD_HEIGHT, 0);
draw_rect(frame, 0, 0, LCD_WIDTH, LCD_HEIGHT, border);
fill_rect(frame, PLAY_RIGHT, 0, 1, LCD_HEIGHT, border);
draw_text(frame, text_x, 12, "SCORE", 1, cyan);
snprintf(value, sizeof(value), "%u", (unsigned)score);
draw_text(frame, text_x, 28, value, 1, 0xffff);
draw_text(frame, text_x, 61, "LIVES", 1, cyan);
snprintf(value, sizeof(value), "%u", (unsigned)lives);
draw_text(frame, text_x, 77, value, 2, RGB565(70, 245, 130));
draw_text(frame, text_x, 112, "WAVE", 1, cyan);
snprintf(value, sizeof(value), "%u", (unsigned)wave);
draw_text(frame, text_x, 128, value, 1, 0xffff);
if (autoMode) draw_text(frame, text_x, 145, "AUTO", 1, COLOR_YELLOW);
}drawInvaders() · source line 628
void drawInvaders(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.
- Execute
clear(frame, 0); - Execute
starfield_draw(frame, &stars); - Decision
gameState == STATE_READYTrue- Execute
draw_text_centred(frame, PLAY_CENTRE, 12, "INVADERS", 3, 0xffff); - Execute
draw_text_centred(frame, 66, 51, "PLAY", 2, autoMode ? COLOR_LIGHTGRAY : COLOR_YELLOW); - Execute
draw_text_centred(frame, 186, 51, "AUTO", 2, autoMode ? COLOR_YELLOW : COLOR_LIGHTGRAY); - Execute
draw_text_centred(frame, PLAY_CENTRE, 91, "D/B/STICK CHANGE A/K START", 1, 0xffff); - Execute
draw_text_centred(frame, PLAY_CENTRE, 107, "D/B/STICK LEFT/RIGHT MOVE", 1, 0xffff); - Execute
draw_text_centred(frame, PLAY_CENTRE, 123, "HOLD A FIRE", 1, 0xffff); - Execute
draw_text_centred(frame, PLAY_CENTRE, 139, "EXTRA LIFE: 1500 4000 7000", 1, COLOR_SKYBLUE); - Execute
draw_text_centred(frame, PLAY_CENTRE, 156, "BOOT BACK", 1, 0x7bef); - Execute
drawInvadersLayout(frame); - Exit this function
return;
FalseContinue after this decision.
- FOR loop
int row = 0; row < INVADER_ROWS; ++rowRepeat body while the loop condition permits; then continue below.
- FOR loop
int column = 0; column < INVADER_COLUMNS; ++columnRepeat body while the loop condition permits; then continue below.
- Decision
alive[row][column]True- Execute
drawInvader(frame, rowTypes[row], marchFrame, formationX + column * COLUMN_SPACING, formationY + row * ROW_SPACING);
FalseContinue after this decision.
- Decision
mothershipActiveTrue- Execute
drawInvader(frame, TYPE_4, mothershipFrame, (int)mothershipX, MOTHERSHIP_Y);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_HIT_EFFECTS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const HitEffect *effect = &hitEffects[i]; - Decision
effect->remaining == 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const unsigned pose = effect->remaining > HIT_FRAME_MS ? 0U : 1U; - Execute
draw_bitmap_scaled(frame, effect->x, effect->y, invader_hit_frames[pose], 8, 8, INVADER_SIZE, INVADER_SIZE, effect->color);
- Decision
playerBullet.activeTrue- Execute
fill_rect(frame, (int)playerBullet.x, (int)playerBullet.y, 2, 5, 0xffff);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ENEMY_BULLETS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
enemyBullets[i].activeTrue- Execute
fill_rect(frame, (int)enemyBullets[i].x, (int)enemyBullets[i].y, 2, 5, RGB565(255, 80, 100));
FalseContinue after this decision.
- Decision
gameState == STATE_PLAYING && (protection == 0 || (protection / 125U) % 2U == 0)True- Execute
const int x = (int)cannonX; - Execute
const uint16_t green = RGB565(70, 245, 130); - Execute
fill_rect(frame, x, CANNON_Y + 5, CANNON_WIDTH, 5, green); - Execute
fill_rect(frame, x + 3, CANNON_Y + 3, CANNON_WIDTH - 6, 3, green); - Execute
fill_rect(frame, x + 8, CANNON_Y, 2, 4, green);
FalseContinue after this decision.
- Decision
gameState == STATE_FINISHEDTrue- Execute
draw_text_centred(frame, PLAY_CENTRE, 78, "GAME OVER", 2, COLOR_RED);
FalseContinue after this decision.
- Execute
drawInvadersLayout(frame);
Read original function source
void drawInvaders(uint16_t *frame)
{
if (!running || frame == NULL) return;
if (gameState == STATE_SCORES) {
drawHighScoreEntry(frame);
return;
}
// Repaint the background so moving sprites and changing legs leave no trails.
clear(frame, 0);
starfield_draw(frame, &stars);
if (gameState == STATE_READY) {
draw_text_centred(frame, PLAY_CENTRE, 12, "INVADERS", 3, 0xffff);
draw_text_centred(frame, 66, 51, "PLAY", 2, autoMode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
draw_text_centred(frame, 186, 51, "AUTO", 2, autoMode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
draw_text_centred(frame, PLAY_CENTRE, 91, "D/B/STICK CHANGE A/K START", 1, 0xffff);
draw_text_centred(frame, PLAY_CENTRE, 107, "D/B/STICK LEFT/RIGHT MOVE", 1, 0xffff);
draw_text_centred(frame, PLAY_CENTRE, 123, "HOLD A FIRE", 1, 0xffff);
draw_text_centred(frame, PLAY_CENTRE, 139, "EXTRA LIFE: 1500 4000 7000", 1, COLOR_SKYBLUE);
draw_text_centred(frame, PLAY_CENTRE, 156, "BOOT BACK", 1, 0x7bef);
drawInvadersLayout(frame);
return;
}
for (int row = 0; row < INVADER_ROWS; ++row) {
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (alive[row][column]) {
drawInvader(frame, rowTypes[row], marchFrame,
formationX + column * COLUMN_SPACING, formationY + row * ROW_SPACING);
}
}
}
if (mothershipActive) {
drawInvader(frame, TYPE_4, mothershipFrame, (int)mothershipX, MOTHERSHIP_Y);
}
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
const HitEffect *effect = &hitEffects[i];
if (effect->remaining == 0) continue;
const unsigned pose = effect->remaining > HIT_FRAME_MS ? 0U : 1U;
draw_bitmap_scaled(frame, effect->x, effect->y, invader_hit_frames[pose], 8, 8,
INVADER_SIZE, INVADER_SIZE, effect->color);
}
if (playerBullet.active) {
fill_rect(frame, (int)playerBullet.x, (int)playerBullet.y, 2, 5, 0xffff);
}
for (unsigned i = 0; i < MAX_ENEMY_BULLETS; ++i) {
if (enemyBullets[i].active) {
fill_rect(frame, (int)enemyBullets[i].x, (int)enemyBullets[i].y,
2, 5, RGB565(255, 80, 100));
}
}
if (gameState == STATE_PLAYING && (protection == 0 || (protection / 125U) % 2U == 0)) {
const int x = (int)cannonX;
const uint16_t green = RGB565(70, 245, 130);
fill_rect(frame, x, CANNON_Y + 5, CANNON_WIDTH, 5, green);
fill_rect(frame, x + 3, CANNON_Y + 3, CANNON_WIDTH - 6, 3, green);
fill_rect(frame, x + 8, CANNON_Y, 2, 4, green);
}
if (gameState == STATE_FINISHED) {
draw_text_centred(frame, PLAY_CENTRE, 78, "GAME OVER", 2, COLOR_RED);
}
drawInvadersLayout(frame);
}Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "invaders.h"
#include "audio.h"
#include "game_graphics.h"
#include "game_config.h"
#include "gold.h"
#include "high_scores.h"
#include "mode_selection.h"
#include "PET2001_font.h"
#include "starfield.h"
#include "esp_random.h"
#include "esp_log.h"
#include <stdio.h>
#include <string.h>
#define INVADER_ROWS 5
#define INVADER_COLUMNS 8
#define INVADER_SIZE 16
#define COLUMN_SPACING 24
#define ROW_SPACING 18
#define PLAY_LEFT 1
#define PLAY_RIGHT 248
#define PLAY_TOP 1
#define PLAY_BOTTOM (LCD_HEIGHT - 1)
#define PLAY_WIDTH (PLAY_RIGHT - PLAY_LEFT)
#define PLAY_CENTRE (PLAY_LEFT + PLAY_WIDTH / 2)
#define FORMATION_TOP 24
#define MOTHERSHIP_Y 2
#define CANNON_Y 156
#define CANNON_WIDTH 18
#define CANNON_HEIGHT 10
#define MAX_ENEMY_BULLETS 3
#define STEP_MS 10U
#define FIRE_DELAY_MS 200U
#define PROTECTION_MS 2000U
#define HIT_FRAME_MS 100U
#define MAX_HIT_EFFECTS 4
#define AUTO_DODGE_TRIGGER_Y 42.0f
#define AUTO_DODGE_CHANCE_PERCENT 65U
#define AUTO_DODGE_MS 180U
// INVADER ARTWORK: edit each type independently here.
// Every type has two frames. Each frame has eight bytes, one per row from top
// to bottom. Bit 7 is the leftmost pixel; 1 draws and 0 preserves the background.
// These native 8x8 images are drawn at 16x16. Change the byte values to create
// your artwork; keep both frames and all eight rows in each frame.
// Native 16x16/32x32 artwork will also need updated source dimensions and array
// lengths when introduced. The shared renderer already supports those sizes.
// Invader 1: bottom two formation rows, 5 points per invader.
static const uint8_t invader_1_frames[2][8] = {
// Frame 1
{0x42, 0x3C, 0xFF, 0x99, 0xFF, 0x24, 0x42, 0x24},
// Frame 2
{0x18, 0x3C, 0xFF, 0x99, 0xFF, 0x24, 0x42, 0x81}
};
// Invader 2: middle two formation rows, 15 points per invader.
static const uint8_t invader_2_frames[2][8] = {
// Frame 1
// {0x99, 0xBD, 0xA5, 0xFF, 0x3C, 0x3C, 0x24, 0x42},
{0x18, 0x3C, 0x24, 0x3C, 0xFF, 0x99, 0x24, 0x42},
// Frame 2
// {0x42, 0x3C, 0x24, 0xFF, 0xBD, 0xBD, 0xA5, 0x24}
{0x18, 0x3C, 0x24, 0xBD, 0xFF, 0x18, 0x24, 0x24}
};
// Invader 3: top formation row, 25 points per invader.
static const uint8_t invader_3_frames[2][8] = {
// Frame 1
{0x18, 0x18, 0x3C, 0x5A, 0x7E, 0x18, 0x18, 0x24},
// Frame 2
{0x00, 0x3C, 0x3C, 0x5A, 0x7E, 0x18, 0x24, 0x42}
};
// Invader 4: mothership, 50 points. Crosses the separate top lane in either
// direction and alternates its two frames every 200 ms while visible.
static const uint8_t invader_4_frames[2][8] = {
// Frame 1
{0x00, 0x18, 0x3E, 0x6B, 0x55, 0x7F, 0x49, 0x24},
// Frame 2
{0x18, 0x3C, 0xFF, 0x99, 0xFF, 0x24, 0x42, 0x81}
};
// Enemy hit animation: frame 1 for 100 ms, frame 2 for 100 ms, then clear.
// Drawn at 16x16 in the destroyed enemy's color, at its position when hit.
static const uint8_t invader_hit_frames[2][8] = {
// Frame 1
// {0x81, 0x24, 0x42, 0x18, 0x18, 0x42, 0x24, 0x81},
{0x81, 0x5A, 0x24, 0x5A, 0x5A, 0x24, 0x5A, 0x81},
// Frame 2
// {0x00, 0x18, 0x3E, 0x6B, 0x55, 0x7F, 0x24, 0x48}
{0x99, 0x24, 0x42, 0x81, 0x81, 0x42, 0x24, 0x99}
};
typedef enum { TYPE_1, TYPE_2, TYPE_3, TYPE_4 } InvaderType;
typedef struct {
const uint8_t (*frames)[8];
uint16_t points, color;
} InvaderArtwork;
// Type definitions: artwork, points, drawing color.
static const InvaderArtwork artwork[] = {
{invader_1_frames, 5, RGB565(70, 245, 130)}, // Bottom two rows: 5 points.
{invader_2_frames, 15, RGB565(50, 205, 255)}, // Middle two rows: 15 points.
{invader_3_frames, 25, RGB565(255, 225, 70)}, // Top formation row: 25 points.
{invader_4_frames, 50, RGB565(255, 80, 100)}, // Mothership: 50 points.
};
// Formation rows from top to bottom: 3, 2, 2, 1, 1. Eight invaders per row.
static const InvaderType rowTypes[INVADER_ROWS] = {
TYPE_3, TYPE_2, TYPE_2, TYPE_1, TYPE_1
};
typedef enum { STATE_READY, STATE_PLAYING, STATE_FINISHED, STATE_SCORES } GameState;
typedef struct {
float x, y;
bool active;
} Bullet;
typedef struct {
int x, y;
uint16_t color;
uint32_t remaining;
} HitEffect;
// Fixed storage only; drawing reuses the caller's existing framebuffer.
static bool alive[INVADER_ROWS][INVADER_COLUMNS];
static Bullet playerBullet, enemyBullets[MAX_ENEMY_BULLETS];
// Separate slots preserve overlapping explosions; one player shot is active at a time.
static HitEffect hitEffects[MAX_HIT_EFFECTS];
static GameState gameState;
static float cannonX, mothershipX;
static int formationX, formationY, marchDirection, mothershipDirection;
static unsigned remaining, marchFrame, mothershipFrame;
static uint32_t score, wave, lastUpdate, accumulator, marchElapsed;
static uint32_t gameOverStarted;
static uint32_t fireDelay, enemyFireDelay, protection, mothershipDelay, mothershipElapsed;
static uint32_t autoDodgeUntil;
static uint8_t lives, extra_lives_awarded;
static int autoDodgeDirection, autoIgnoredBullet;
static bool running, controlsReady, mothershipActive, autoMode;
static mode_selection_input_t modeInput;
static starfield_t stars;
static const uint32_t extra_life_scores[] = { 1500U, 4000U, 7000U };
static uint32_t reduceTimer(uint32_t timer)
{
return timer > STEP_MS ? timer - STEP_MS : 0;
}
static bool overlaps(float ax, float ay, float aw, float ah,
float bx, float by, float bw, float bh)
{
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
static void addPoints(InvaderType type)
{
const uint32_t points = artwork[type].points;
score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
while (extra_lives_awarded < sizeof(extra_life_scores) / sizeof(extra_life_scores[0]) &&
score >= extra_life_scores[extra_lives_awarded]) {
++lives;
ESP_LOGI("invaders", "EXTRA LIFE: %u POINTS, %u LIVES",
(unsigned)extra_life_scores[extra_lives_awarded], (unsigned)lives);
++extra_lives_awarded;
}
}
static void startHitEffect(int x, int y, InvaderType type)
{
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
if (hitEffects[i].remaining != 0) continue;
hitEffects[i] = (HitEffect){x, y, artwork[type].color, HIT_FRAME_MS * 2U};
return;
}
ESP_LOGW("invaders", "No free hit effect slot");
}
static void updateHitEffects(uint32_t elapsed)
{
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
HitEffect *effect = &hitEffects[i];
effect->remaining = effect->remaining > elapsed ? effect->remaining - elapsed : 0;
}
}
static bool hitEffectsActive(void)
{
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
if (hitEffects[i].remaining != 0) return true;
}
return false;
}
static void scheduleMothership(void)
{
mothershipActive = false;
mothershipDelay = 8000U + esp_random() % 6001U;
mothershipFrame = mothershipElapsed = 0;
}
static void beginWave(void)
{
memset(hitEffects, 0, sizeof(hitEffects));
for (unsigned row = 0; row < INVADER_ROWS; ++row) {
for (unsigned column = 0; column < INVADER_COLUMNS; ++column) {
alive[row][column] = true;
}
}
remaining = INVADER_ROWS * INVADER_COLUMNS;
formationX = PLAY_LEFT + (PLAY_WIDTH - ((INVADER_COLUMNS - 1) * COLUMN_SPACING + INVADER_SIZE)) / 2;
formationY = FORMATION_TOP;
marchDirection = 1;
marchFrame = marchElapsed = 0;
playerBullet.active = false;
memset(enemyBullets, 0, sizeof(enemyBullets));
enemyFireDelay = 1000U;
fireDelay = 0;
autoDodgeUntil = 0U;
autoDodgeDirection = 0;
autoIgnoredBullet = -1;
scheduleMothership();
ESP_LOGI("invaders", "WAVE %u", (unsigned)wave);
}
static void beginGame(uint32_t now_ms)
{
score = 0;
wave = 1;
lives = gameConfigInvadersStartingLives();
extra_lives_awarded = 0U;
cannonX = PLAY_LEFT + (PLAY_WIDTH - CANNON_WIDTH) / 2.0f;
protection = PROTECTION_MS;
controlsReady = false;
lastUpdate = now_ms;
accumulator = 0;
gameState = STATE_PLAYING;
beginWave();
audioPlay(AUDIO_SFX_GAME_START);
}
static void finishGame(void)
{
if (gameState != STATE_PLAYING) return;
gameState = STATE_FINISHED;
gameOverStarted = lastUpdate;
audioPlay(AUDIO_SFX_GAME_OVER);
ESP_LOGI("invaders", "GAME OVER: SCORE %u, WAVE %u", (unsigned)score, (unsigned)wave);
// Award whole thousands once, when the game ends.
const uint32_t gold = score / 1000U;
if (gold != 0) {
if ((autoMode ? addAIGold(gold) : addUserGoldWon(gold))) {
ESP_LOGI("invaders", "GOLD WON: %u", (unsigned)gold);
} else {
ESP_LOGE("invaders", "Failed to save %u Gold", (unsigned)gold);
}
}
}
static int autoDodgeMovement(uint32_t now_ms)
{
if (autoDodgeDirection != 0 && (int32_t)(now_ms - autoDodgeUntil) < 0) {
return autoDodgeDirection;
}
autoDodgeDirection = 0;
if (autoIgnoredBullet >= 0) {
const Bullet *ignored = &enemyBullets[autoIgnoredBullet];
if (!ignored->active || ignored->y < CANNON_Y - AUTO_DODGE_TRIGGER_Y ||
ignored->y > CANNON_Y + CANNON_HEIGHT) {
autoIgnoredBullet = -1;
}
}
const int cannonCentre = (int)cannonX + CANNON_WIDTH / 2;
for (unsigned i = 0; i < MAX_ENEMY_BULLETS; ++i) {
const Bullet *bullet = &enemyBullets[i];
if (!bullet->active || bullet->y < CANNON_Y - AUTO_DODGE_TRIGGER_Y ||
bullet->y > CANNON_Y + CANNON_HEIGHT) continue;
if ((int)i == autoIgnoredBullet ||
bullet->x < cannonX - 8.0f || bullet->x > cannonX + CANNON_WIDTH + 8.0f) continue;
if (esp_random() % 100U >= AUTO_DODGE_CHANCE_PERCENT) {
autoIgnoredBullet = (int)i;
return 0;
}
autoDodgeDirection = bullet->x < cannonCentre ? 1 : -1;
if (autoDodgeDirection < 0 && cannonX <= PLAY_LEFT + 8) autoDodgeDirection = 1;
if (autoDodgeDirection > 0 && cannonX >= PLAY_RIGHT - CANNON_WIDTH - 8) autoDodgeDirection = -1;
autoDodgeUntil = now_ms + AUTO_DODGE_MS;
return autoDodgeDirection;
}
return 0;
}
static int autoMovement(uint32_t now_ms)
{
const int dodgeMovement = autoDodgeMovement(now_ms);
if (dodgeMovement != 0) return dodgeMovement;
const int cannonCentre = (int)cannonX + CANNON_WIDTH / 2;
int targetX = cannonCentre;
int bestDistance = PLAY_RIGHT - PLAY_LEFT;
for (int column = 0; column < INVADER_COLUMNS; ++column) {
int lowestRow = -1;
for (int row = INVADER_ROWS - 1; row >= 0; --row) {
if (alive[row][column]) {
lowestRow = row;
break;
}
}
if (lowestRow < 0) continue;
const int invaderX = formationX + column * COLUMN_SPACING + INVADER_SIZE / 2;
const int distance = invaderX > cannonCentre ? invaderX - cannonCentre : cannonCentre - invaderX;
if (distance < bestDistance) {
bestDistance = distance;
targetX = invaderX;
}
}
return targetX > cannonCentre + 2 ? 1 : targetX < cannonCentre - 2 ? -1 : 0;
}
static void marchFormation(void)
{
int left = PLAY_RIGHT, right = PLAY_LEFT;
for (int row = 0; row < INVADER_ROWS; ++row) {
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (!alive[row][column]) continue;
const int x = formationX + column * COLUMN_SPACING;
if (x < left) left = x;
if (x + INVADER_SIZE > right) right = x + INVADER_SIZE;
}
}
if (left + marchDirection * 4 < PLAY_LEFT + 3 ||
right + marchDirection * 4 > PLAY_RIGHT - 3) {
formationY += 4;
marchDirection = -marchDirection;
} else {
formationX += marchDirection * 4;
}
// The whole formation changes pose once per marching step, including drops.
marchFrame ^= 1U;
for (int row = 0; row < INVADER_ROWS; ++row) {
if (formationY + row * ROW_SPACING + INVADER_SIZE < CANNON_Y) continue;
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (alive[row][column]) {
finishGame();
return;
}
}
}
}
static void fireEnemyBullet(void)
{
for (unsigned slot = 0; slot < MAX_ENEMY_BULLETS; ++slot) {
if (enemyBullets[slot].active) continue;
const unsigned first = esp_random() % INVADER_COLUMNS;
for (unsigned offset = 0; offset < INVADER_COLUMNS; ++offset) {
const unsigned column = (first + offset) % INVADER_COLUMNS;
// Only the lowest surviving invader in a column fires.
for (int row = INVADER_ROWS - 1; row >= 0; --row) {
if (!alive[row][column]) continue;
enemyBullets[slot] = (Bullet){
.x = formationX + (int)column * COLUMN_SPACING + INVADER_SIZE / 2 - 1,
.y = formationY + row * ROW_SPACING + INVADER_SIZE,
.active = true,
};
audioPlay(AUDIO_SFX_ENEMY_SHOT);
return;
}
}
return;
}
}
static void updateMothership(void)
{
if (!mothershipActive) {
mothershipDelay = reduceTimer(mothershipDelay);
if (mothershipDelay != 0) return;
mothershipDirection = (esp_random() & 1U) != 0 ? 1 : -1;
mothershipX = mothershipDirection > 0 ? PLAY_LEFT - INVADER_SIZE : PLAY_RIGHT;
mothershipActive = true;
mothershipFrame = mothershipElapsed = 0;
}
mothershipX += mothershipDirection * 28.0f * STEP_MS / 1000.0f;
mothershipElapsed += STEP_MS;
if (mothershipElapsed >= 200U) {
mothershipElapsed -= 200U;
mothershipFrame ^= 1U;
}
if ((mothershipDirection > 0 && mothershipX >= PLAY_RIGHT) ||
(mothershipDirection < 0 && mothershipX + INVADER_SIZE <= PLAY_LEFT)) {
scheduleMothership();
}
}
static void updatePlayerBullet(void)
{
if (!playerBullet.active) return;
playerBullet.y -= 220.0f * STEP_MS / 1000.0f;
// Rectangular sprite bounds are used for hits, independent of artwork bits.
// Check bottom rows first so a shot hits the nearest invader in its path.
for (int row = INVADER_ROWS - 1; row >= 0; --row) {
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (!alive[row][column]) continue;
if (overlaps(playerBullet.x, playerBullet.y, 2, 5,
formationX + column * COLUMN_SPACING,
formationY + row * ROW_SPACING, INVADER_SIZE, INVADER_SIZE)) {
alive[row][column] = false;
--remaining;
playerBullet.active = false;
addPoints(rowTypes[row]);
startHitEffect(formationX + column * COLUMN_SPACING,
formationY + row * ROW_SPACING, rowTypes[row]);
audioPlay(AUDIO_SFX_HIT);
if (remaining == 0U) audioPlay(AUDIO_SFX_WAVE_CLEAR);
return;
}
}
}
if (mothershipActive && overlaps(playerBullet.x, playerBullet.y, 2, 5,
mothershipX, MOTHERSHIP_Y, INVADER_SIZE, INVADER_SIZE)) {
playerBullet.active = false;
addPoints(TYPE_4);
startHitEffect((int)mothershipX, MOTHERSHIP_Y, TYPE_4);
scheduleMothership();
audioPlay(AUDIO_SFX_HIT);
ESP_LOGI("invaders", "MOTHERSHIP HIT: 50 POINTS");
} else if (playerBullet.y + 5 <= PLAY_TOP) {
playerBullet.active = false;
}
}
static void simulateStep(int movement, bool fire)
{
// Pause the cleared wave until every explosion has played both frames.
if (remaining == 0) {
if (!hitEffectsActive()) {
if (wave < UINT32_MAX) ++wave;
beginWave();
}
return;
}
protection = reduceTimer(protection);
fireDelay = reduceTimer(fireDelay);
cannonX += movement * 125.0f * STEP_MS / 1000.0f;
if (cannonX < PLAY_LEFT + 1) cannonX = PLAY_LEFT + 1;
if (cannonX > PLAY_RIGHT - CANNON_WIDTH - 1) cannonX = PLAY_RIGHT - CANNON_WIDTH - 1;
if (fire && fireDelay == 0 && !playerBullet.active) {
playerBullet = (Bullet){cannonX + CANNON_WIDTH / 2 - 1, CANNON_Y - 5, true};
fireDelay = FIRE_DELAY_MS;
audioPlay(AUDIO_SFX_PLAYER_SHOT);
}
updateMothership();
updatePlayerBullet();
if (remaining == 0) {
memset(enemyBullets, 0, sizeof(enemyBullets));
return;
}
// Wave difficulty affects marching, enemy firing cadence, and bullet speed.
const uint32_t difficulty = wave < 9U ? wave - 1U : 8U;
// Each destroyed invader removes 30 ms from the march interval, so the
// formation marches faster as the wave is cleared.
const uint32_t base_interval = 120U + remaining * 30U;
const uint32_t normal_march_interval = base_interval > 80U + difficulty * 20U
? base_interval - difficulty * 20U : 80U;
// The final survivor gets a deliberate Crazy speed, independent of wave.
const uint32_t march_interval = remaining == 1U ? 40U : normal_march_interval;
marchElapsed += STEP_MS;
if (marchElapsed >= march_interval) {
marchElapsed -= march_interval;
marchFormation();
if (gameState != STATE_PLAYING) return;
}
enemyFireDelay = reduceTimer(enemyFireDelay);
if (enemyFireDelay == 0) {
fireEnemyBullet();
// Higher waves shorten the delay between enemy shots; the random term
// keeps the firing pattern from becoming perfectly regular.
enemyFireDelay = 900U - difficulty * 50U + esp_random() % 301U;
}
for (unsigned i = 0; i < MAX_ENEMY_BULLETS; ++i) {
Bullet *bullet = &enemyBullets[i];
if (!bullet->active) continue;
// Enemy bullet travel speed increases by wave, not by each shot.
bullet->y += (60.0f + difficulty * 4.0f) * STEP_MS / 1000.0f;
if (bullet->y >= PLAY_BOTTOM) {
bullet->active = false;
} else if (overlaps(bullet->x, bullet->y, 2, 5,
cannonX, CANNON_Y, CANNON_WIDTH, CANNON_HEIGHT)) {
bullet->active = false;
if (protection != 0) continue;
--lives;
ESP_LOGI("invaders", "PLAYER HIT: %u LIVES", (unsigned)lives);
audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
if (lives == 0) {
finishGame();
return;
}
cannonX = PLAY_LEFT + (PLAY_WIDTH - CANNON_WIDTH) / 2.0f;
protection = PROTECTION_MS;
controlsReady = false;
playerBullet.active = false;
memset(enemyBullets, 0, sizeof(enemyBullets));
return;
}
}
}
void startInvaders(uint32_t now_ms)
{
starfield_initialize(&stars, 45, PLAY_LEFT, PLAY_TOP, PLAY_WIDTH,
PLAY_BOTTOM - PLAY_TOP, STARFIELD_GREYSCALE);
running = true;
gameState = STATE_READY;
lastUpdate = now_ms;
accumulator = score = 0;
wave = 1;
lives = 3;
extra_lives_awarded = 0U;
memset(hitEffects, 0, sizeof(hitEffects));
controlsReady = false;
autoMode = false;
modeSelectionReset(&modeInput);
ESP_LOGI("invaders", "START");
}
void stopInvaders(void)
{
cancelHighScoreEntry();
running = false;
memset(hitEffects, 0, sizeof(hitEffects));
ESP_LOGI("invaders", "STOP");
}
void invadersAction(bool start, bool stop_auto, uint32_t now_ms)
{
if (running && start && gameState == STATE_READY) {
ESP_LOGI("invaders", "START: %s", autoMode ? "AUTO" : "PLAY");
beginGame(now_ms);
} else if (running && stop_auto && autoMode && gameState == STATE_PLAYING) {
autoMode = false;
modeSelectionReset(&modeInput);
gameState = STATE_READY;
}
}
void updateInvaders(int x_offset, int y_offset, bool valid, bool left, bool right,
bool fire, uint32_t now_ms)
{
if (!running) return;
const uint32_t elapsed = now_ms - lastUpdate;
lastUpdate = now_ms;
// Effects also expire on the game-over screen. Update before new hits are
// created so a new explosion gets its full first frame on the next draw.
updateHitEffects(elapsed);
starfield_update(&stars, elapsed);
if (gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000U) {
if (autoMode) beginGame(now_ms);
else {
startHighScoreEntry(HIGH_SCORE_INVADERS, 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_READY) {
if (modeSelectionNext(&modeInput, x_offset, y_offset, valid, left, right, now_ms)) {
autoMode = !autoMode;
}
return;
}
if (gameState != STATE_PLAYING) return;
if (!valid) controlsReady = false;
else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
controlsReady = true;
const int movement = left || right ? (int)right - (int)left
: valid && controlsReady ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0;
const int automatedMovement = autoMode ? autoMovement(now_ms) : 0;
// Bound catch-up after a stall; collisions are checked every 10 ms.
accumulator += elapsed > 100U ? 100U : elapsed;
while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
accumulator -= STEP_MS;
simulateStep(autoMode ? automatedMovement : (left || right || controlsReady) ? movement : 0,
autoMode || fire);
}
}
static void drawInvader(uint16_t *frame, InvaderType type, unsigned pose, int x, int y)
{
draw_bitmap_scaled(frame, x, y, artwork[type].frames[pose], 8, 8,
INVADER_SIZE, INVADER_SIZE, artwork[type].color);
}
static void drawInvadersLayout(uint16_t *frame)
{
const int text_x = PLAY_RIGHT + 7;
const uint16_t border = 0x7bef;
const uint16_t cyan = RGB565(50, 205, 255);
char value[16];
// Draw last to mask sprites entering/exiting at the partition and protect
// all four outer edges. PLAY_RIGHT and PLAY_BOTTOM are exclusive bounds.
fill_rect(frame, PLAY_RIGHT, 0, LCD_WIDTH - PLAY_RIGHT, LCD_HEIGHT, 0);
draw_rect(frame, 0, 0, LCD_WIDTH, LCD_HEIGHT, border);
fill_rect(frame, PLAY_RIGHT, 0, 1, LCD_HEIGHT, border);
drawPet2001TextTransparent(frame, text_x, 12, "SCORE", cyan, 1);
snprintf(value, sizeof(value), "%u", (unsigned)score);
drawPet2001TextTransparent(frame, text_x, 28, value, 0xffff, 1);
drawPet2001TextTransparent(frame, text_x, 61, "LIVES", cyan, 1);
snprintf(value, sizeof(value), "%u", (unsigned)lives);
drawPet2001TextTransparent(frame, text_x, 77, value, RGB565(70, 245, 130), 2);
drawPet2001TextTransparent(frame, text_x, 112, "WAVE", cyan, 1);
snprintf(value, sizeof(value), "%u", (unsigned)wave);
drawPet2001TextTransparent(frame, text_x, 128, value, 0xffff, 1);
if (autoMode) drawPet2001TextTransparent(frame, text_x, 145, "AUTO", COLOR_YELLOW, 1);
}
void drawInvaders(uint16_t *frame)
{
if (!running || frame == NULL) return;
if (gameState == STATE_SCORES) {
drawHighScoreEntry(frame);
return;
}
// Repaint the background so moving sprites and changing legs leave no trails.
clear(frame, 0);
starfield_draw(frame, &stars);
if (gameState == STATE_READY) {
drawPet2001TextCentredTransparent(frame, PLAY_CENTRE, 12, "INVADERS", 0xffff, 3);
drawPet2001TextCentredTransparent(frame, 66, 51, "PLAY",
autoMode ? COLOR_LIGHTGRAY : COLOR_YELLOW, 2);
drawPet2001TextCentredTransparent(frame, 186, 51, "AUTO",
autoMode ? COLOR_YELLOW : COLOR_LIGHTGRAY, 2);
drawPet2001TextCentredTransparent(frame, PLAY_CENTRE, 91,
"D/B/STICK CHANGE A/K START", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, PLAY_CENTRE, 107,
"D/B/STICK LEFT/RIGHT MOVE", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, PLAY_CENTRE, 123, "HOLD A FIRE", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, PLAY_CENTRE, 139,
"EXTRA LIFE: 1500 4000 7000", COLOR_SKYBLUE, 1);
drawPet2001TextCentredTransparent(frame, PLAY_CENTRE, 156, "BOOT BACK", 0x7bef, 1);
drawInvadersLayout(frame);
return;
}
for (int row = 0; row < INVADER_ROWS; ++row) {
for (int column = 0; column < INVADER_COLUMNS; ++column) {
if (alive[row][column]) {
drawInvader(frame, rowTypes[row], marchFrame,
formationX + column * COLUMN_SPACING, formationY + row * ROW_SPACING);
}
}
}
if (mothershipActive) {
drawInvader(frame, TYPE_4, mothershipFrame, (int)mothershipX, MOTHERSHIP_Y);
}
for (unsigned i = 0; i < MAX_HIT_EFFECTS; ++i) {
const HitEffect *effect = &hitEffects[i];
if (effect->remaining == 0) continue;
const unsigned pose = effect->remaining > HIT_FRAME_MS ? 0U : 1U;
draw_bitmap_scaled(frame, effect->x, effect->y, invader_hit_frames[pose], 8, 8,
INVADER_SIZE, INVADER_SIZE, effect->color);
}
if (playerBullet.active) {
fill_rect(frame, (int)playerBullet.x, (int)playerBullet.y, 2, 5, 0xffff);
}
for (unsigned i = 0; i < MAX_ENEMY_BULLETS; ++i) {
if (enemyBullets[i].active) {
fill_rect(frame, (int)enemyBullets[i].x, (int)enemyBullets[i].y,
2, 5, RGB565(255, 80, 100));
}
}
if (gameState == STATE_PLAYING && (protection == 0 || (protection / 125U) % 2U == 0)) {
const int x = (int)cannonX;
const uint16_t green = RGB565(70, 245, 130);
fill_rect(frame, x, CANNON_Y + 5, CANNON_WIDTH, 5, green);
fill_rect(frame, x + 3, CANNON_Y + 3, CANNON_WIDTH - 6, 3, green);
fill_rect(frame, x + 8, CANNON_Y, 2, 4, green);
}
if (gameState == STATE_FINISHED) {
drawPet2001TextCentredTransparent(frame, PLAY_CENTRE, 78, "GAME OVER", COLOR_RED, 2);
}
drawInvadersLayout(frame);
}