Tempest
Source: src/tempest.c · 431 lines · 20 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
nextLane() · source line 42
static uint8_t nextLane(uint8_t lane, int direction)
- Exit this function
return (uint8_t)((lane + direction + laneCount) % laneCount);
Read original function source
static uint8_t nextLane(uint8_t lane, int direction)
{
return (uint8_t)((lane + direction + laneCount) % laneCount);
}decrease() · source line 47
static uint32_t decrease(uint32_t value)
- Exit this function
return value > STEP_MS ? value - STEP_MS : 0;
Read original function source
static uint32_t decrease(uint32_t value)
{
return value > STEP_MS ? value - STEP_MS : 0;
}activeEnemies() · source line 52
static unsigned activeEnemies(void)
- Execute
unsigned count = 0; - FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
count += enemies[i].active;
- Exit this function
return count;
Read original function source
static unsigned activeEnemies(void)
{
unsigned count = 0;
for (unsigned i = 0; i < MAX_ENEMIES; ++i) count += enemies[i].active;
return count;
}destroyEnemy() · source line 59
static void destroyEnemy(unsigned index)
- Execute
const uint32_t points = enemies[index].flipper ? 150U : 100U; - Execute
score = score > UINT32_MAX - points ? UINT32_MAX : score + points; - Execute
enemies[index].active = false;
Read original function source
static void destroyEnemy(unsigned index)
{
const uint32_t points = enemies[index].flipper ? 150U : 100U;
score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
enemies[index].active = false;
}updateWeb() · source line 66
static void updateWeb(void)
- FOR loop
unsigned i = 0; i < laneCount; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const float angle = -PI / 2.0f + i * 2.0f * PI / laneCount; - Execute
outer[i] = (Point){108.0f + cosf(angle) * 91.0f, 88.0f + sinf(angle) * 64.0f}; - Execute
inner[i] = (Point){108.0f + cosf(angle) * 19.0f, 88.0f + sinf(angle) * 13.0f};
Read original function source
static void updateWeb(void)
{
for (unsigned i = 0; i < laneCount; ++i) {
const float angle = -PI / 2.0f + i * 2.0f * PI / laneCount;
outer[i] = (Point){108.0f + cosf(angle) * 91.0f, 88.0f + sinf(angle) * 64.0f};
inner[i] = (Point){108.0f + cosf(angle) * 19.0f, 88.0f + sinf(angle) * 13.0f};
}
}beginWave() · source line 75
static void beginWave(void)
Calls into this documentation:
- Execute
memset(enemies, 0, sizeof(enemies)); - Execute
memset(shots, 0, sizeof(shots)); - Execute
const uint8_t previousLanes = laneCount; - Decision
wave <= 8UTrue- Execute
laneCount = 7U + ((wave - 1U) / 2U) * 3U;
False- Decision
(wave & 1U) != 0True- Execute
laneCount = 8U + esp_random() % 9U;
FalseContinue after this decision.
- Execute
playerLane = (uint8_t)(((2U * playerLane + 1U) * laneCount) / (2U * previousLanes)); - Execute
updateWeb(); - Execute
remaining = 8U + (wave < 12U ? wave : 12U) * 2U; - Execute
spawnDue = 600U; - Execute
fireDue = moveDue = heldDirection = 0; - Execute
protection = 1800U; - Execute
zapReady = true; - Execute
gameState = STATE_PLAYING; - Execute
ESP_LOGI("tempest", "WAVE %u; LANES %u", (unsigned)wave, (unsigned)laneCount);
Read original function source
static void beginWave(void)
{
memset(enemies, 0, sizeof(enemies));
memset(shots, 0, sizeof(shots));
const uint8_t previousLanes = laneCount;
if (wave <= 8U) {
laneCount = 7U + ((wave - 1U) / 2U) * 3U;
} else if ((wave & 1U) != 0) {
// Keep each random web for two waves; repeated counts are allowed.
laneCount = 8U + esp_random() % 9U;
}
// Map the old lane centre onto the new web to preserve angular position.
playerLane = (uint8_t)(((2U * playerLane + 1U) * laneCount) / (2U * previousLanes));
updateWeb();
remaining = 8U + (wave < 12U ? wave : 12U) * 2U;
spawnDue = 600U;
fireDue = moveDue = heldDirection = 0;
protection = 1800U;
zapReady = true;
gameState = STATE_PLAYING;
ESP_LOGI("tempest", "WAVE %u; LANES %u", (unsigned)wave, (unsigned)laneCount);
}beginGame() · source line 98
static void beginGame(uint32_t now_ms)
Calls into this documentation:
- Execute
score = 0; - Execute
wave = 1; - Execute
lives = 3; - Execute
laneCount = MAX_LANES; - Execute
playerLane = 0; - Execute
accumulator = zapFlash = transition = 0; - Execute
lastUpdate = now_ms; - Execute
controlsReady = false; - Execute
beginWave(); - Execute
audioPlay(AUDIO_SFX_GAME_START);
Read original function source
static void beginGame(uint32_t now_ms)
{
score = 0;
wave = 1;
lives = 3;
laneCount = MAX_LANES;
playerLane = 0;
accumulator = zapFlash = transition = 0;
lastUpdate = now_ms;
controlsReady = false;
beginWave();
audioPlay(AUDIO_SFX_GAME_START);
}spawnEnemy() · source line 112
static void spawnEnemy(void)
- 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
enemies[i] = (Enemy){ .depth = 0.0f, .moveDue = 1000U + esp_random() % 800U, .lane = esp_random() % laneCount, .direction = esp_random() % 2 ? 1 : -1, .active = true, .flipper = wave >= 2U && esp_random() % 3 == 0, }; - Execute
--remaining; - Execute
spawnDue = 1100U - (wave < 10U ? wave : 10U) * 60U; - Exit this function
return;
Read original function source
static void spawnEnemy(void)
{
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
if (enemies[i].active) continue;
enemies[i] = (Enemy){
.depth = 0.0f,
.moveDue = 1000U + esp_random() % 800U,
.lane = esp_random() % laneCount,
.direction = esp_random() % 2 ? 1 : -1,
.active = true,
.flipper = wave >= 2U && esp_random() % 3 == 0,
};
--remaining;
spawnDue = 1100U - (wave < 10U ? wave : 10U) * 60U;
return;
}
}shoot() · source line 130
static void shoot(void)
Calls into this documentation:
- Decision
fireDueTrue- Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_SHOTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
shots[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
shots[i] = (Shot){1.0f, playerLane, true}; - Execute
fireDue = 140U; - Execute
audioPlay(AUDIO_SFX_PLAYER_SHOT); - Exit this function
return;
Read original function source
static void shoot(void)
{
if (fireDue) return;
for (unsigned i = 0; i < MAX_SHOTS; ++i) {
if (shots[i].active) continue;
shots[i] = (Shot){1.0f, playerLane, true};
fireDue = 140U;
audioPlay(AUDIO_SFX_PLAYER_SHOT);
return;
}
}loseLife() · source line 142
static void loseLife(void)
Calls into this documentation:
- Execute
--lives; - Execute
ESP_LOGI("tempest", "LIVES %u; SCORE %u", lives, (unsigned)score); - Execute
memset(enemies, 0, sizeof(enemies)); - Execute
memset(shots, 0, sizeof(shots)); - Execute
protection = 1800U; - Execute
controlsReady = false; - Execute
heldDirection = 0; - Decision
lives == 0True- Execute
gameState = STATE_FINISHED; - Execute
gameOverStarted = lastUpdate; - Execute
audioPlay(AUDIO_SFX_GAME_OVER); - Execute
const uint32_t gold = score / 1000U; - Decision
gold != 0True- Decision
addUserGoldWon(gold)True- Execute
ESP_LOGI("tempest", "GOLD WON: %u", (unsigned)gold);
False- Execute
ESP_LOGE("tempest", "Failed to save %u Gold", (unsigned)gold);
FalseContinue after this decision.
False- Execute
audioPlay(AUDIO_SFX_LIFE_LOST);
Read original function source
static void loseLife(void)
{
--lives; // lives minus 1
ESP_LOGI("tempest", "LIVES %u; SCORE %u", lives, (unsigned)score);
memset(enemies, 0, sizeof(enemies));
memset(shots, 0, sizeof(shots));
protection = 1800U;
controlsReady = false;
heldDirection = 0;
if (lives == 0) {
gameState = STATE_FINISHED;
gameOverStarted = lastUpdate;
audioPlay(AUDIO_SFX_GAME_OVER);
// Award whole thousands once, when the game ends.
const uint32_t gold = score / 1000U;
if (gold != 0) {
if (addUserGoldWon(gold)) {
ESP_LOGI("tempest", "GOLD WON: %u", (unsigned)gold);
} else {
ESP_LOGE("tempest", "Failed to save %u Gold", (unsigned)gold);
}
}
} else {
audioPlay(AUDIO_SFX_LIFE_LOST);
}
}simulateStep() · source line 169
static void simulateStep(int direction, bool fire)
Calls into this documentation:
- Execute
zapFlash = decrease(zapFlash); - Decision
gameState == STATE_NEXT_WAVETrue- Execute
transition = decrease(transition); - Decision
transition == 0True- 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 = decrease(protection); - Execute
spawnDue = decrease(spawnDue); - Execute
fireDue = decrease(fireDue); - Execute
moveDue = decrease(moveDue); - Decision
direction == 0True- Execute
heldDirection = 0;
False- Decision
heldDirection != direction || moveDue == 0True- Execute
playerLane = nextLane(playerLane, direction); - Execute
moveDue = heldDirection == direction ? 100U : 180U; - Execute
heldDirection = direction;
FalseContinue after this decision.
- Decision
remaining && spawnDue == 0True- Execute
spawnEnemy();
FalseContinue after this decision.
- Decision
fireTrue- Execute
shoot();
FalseContinue after this decision.
- Execute
const float speed = 0.075f + (wave < 15U ? wave : 15U) * 0.009f; - FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Enemy *enemy = &enemies[i]; - Decision
!enemy->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
enemy->moveDue = decrease(enemy->moveDue); - Decision
enemy->rimTrue- Decision
enemy->moveDue == 0True- Execute
enemy->lane = nextLane(enemy->lane, enemy->direction); - Execute
enemy->moveDue = 240U;
FalseContinue after this decision.
False- Execute
enemy->depth += speed * (STEP_MS / 1000.0f); - Decision
enemy->flipper && enemy->moveDue == 0True- Execute
enemy->lane = nextLane(enemy->lane, enemy->direction); - Execute
enemy->moveDue = 900U;
FalseContinue after this decision.
- Decision
enemy->depth >= 1.0fTrue- Execute
enemy->depth = 1.0f; - Execute
enemy->rim = true; - Execute
enemy->moveDue = 240U;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_SHOTS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Shot *shot = &shots[i]; - Decision
!shot->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float before = shot->depth; - Execute
shot->depth -= 1.8f * (STEP_MS / 1000.0f); - Execute
int target = -1; - Execute
float nearest = -1.0f; - FOR loop
unsigned j = 0; j < MAX_ENEMIES; ++jRepeat body while the loop condition permits; then continue below.
- Execute
const Enemy *enemy = &enemies[j]; - Decision
!enemy->active || enemy->lane != shot->laneTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
enemy->depth <= before + 0.035f && enemy->depth >= shot->depth - 0.035f && enemy->depth > nearestTrue- Execute
target = (int)j; - Execute
nearest = enemy->depth;
FalseContinue after this decision.
- Decision
target >= 0True- Execute
destroyEnemy((unsigned)target); - Execute
shot->active = false; - Execute
audioPlay(AUDIO_SFX_HIT);
False- Decision
shot->depth < 0.0fTrue- Execute
shot->active = false;
FalseContinue after this decision.
- Decision
protection == 0True- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Decision
enemies[i].active && enemies[i].rim && enemies[i].lane == playerLaneTrue- Execute
loseLife(); - Exit this function
return;
FalseContinue after this decision.
FalseContinue after this decision.
- Decision
remaining == 0 && activeEnemies() == 0True- Execute
gameState = STATE_NEXT_WAVE; - Execute
transition = 1500U; - Execute
memset(shots, 0, sizeof(shots)); - Execute
audioPlay(AUDIO_SFX_WAVE_CLEAR);
FalseContinue after this decision.
Read original function source
static void simulateStep(int direction, bool fire)
{
zapFlash = decrease(zapFlash);
if (gameState == STATE_NEXT_WAVE) {
transition = decrease(transition);
if (transition == 0) {
if (wave < UINT32_MAX) ++wave;
beginWave();
}
return;
}
protection = decrease(protection);
spawnDue = decrease(spawnDue);
fireDue = decrease(fireDue);
moveDue = decrease(moveDue);
if (direction == 0) heldDirection = 0;
else if (heldDirection != direction || moveDue == 0) {
playerLane = nextLane(playerLane, direction);
moveDue = heldDirection == direction ? 100U : 180U;
heldDirection = direction;
}
if (remaining && spawnDue == 0) spawnEnemy();
if (fire) shoot();
const float speed = 0.075f + (wave < 15U ? wave : 15U) * 0.009f;
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
Enemy *enemy = &enemies[i];
if (!enemy->active) continue;
enemy->moveDue = decrease(enemy->moveDue);
if (enemy->rim) {
if (enemy->moveDue == 0) {
enemy->lane = nextLane(enemy->lane, enemy->direction);
enemy->moveDue = 240U;
}
} else {
enemy->depth += speed * (STEP_MS / 1000.0f);
if (enemy->flipper && enemy->moveDue == 0) {
enemy->lane = nextLane(enemy->lane, enemy->direction);
enemy->moveDue = 900U;
}
if (enemy->depth >= 1.0f) {
enemy->depth = 1.0f;
enemy->rim = true;
enemy->moveDue = 240U;
}
}
}
// A shot hits the first enemy along its travelled segment, including the rim.
for (unsigned i = 0; i < MAX_SHOTS; ++i) {
Shot *shot = &shots[i];
if (!shot->active) continue;
const float before = shot->depth;
shot->depth -= 1.8f * (STEP_MS / 1000.0f);
int target = -1;
float nearest = -1.0f;
for (unsigned j = 0; j < MAX_ENEMIES; ++j) {
const Enemy *enemy = &enemies[j];
if (!enemy->active || enemy->lane != shot->lane) continue;
if (enemy->depth <= before + 0.035f && enemy->depth >= shot->depth - 0.035f &&
enemy->depth > nearest) { target = (int)j; nearest = enemy->depth; }
}
if (target >= 0) {
destroyEnemy((unsigned)target);
shot->active = false;
audioPlay(AUDIO_SFX_HIT);
}
else if (shot->depth < 0.0f) shot->active = false;
}
if (protection == 0) {
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
if (enemies[i].active && enemies[i].rim && enemies[i].lane == playerLane) {
loseLife();
return;
}
}
}
if (remaining == 0 && activeEnemies() == 0) {
gameState = STATE_NEXT_WAVE;
transition = 1500U;
memset(shots, 0, sizeof(shots));
audioPlay(AUDIO_SFX_WAVE_CLEAR);
}
}startTempest() · source line 252
void startTempest(uint32_t now_ms)
Calls into this documentation:
- Execute
running = true; - Execute
gameState = STATE_READY; - Execute
lastUpdate = now_ms; - Execute
laneCount = MAX_LANES; - Execute
updateWeb(); - Execute
ESP_LOGI("tempest", "START");
Read original function source
void startTempest(uint32_t now_ms)
{
running = true;
gameState = STATE_READY;
lastUpdate = now_ms;
laneCount = MAX_LANES;
updateWeb();
ESP_LOGI("tempest", "START");
}stopTempest() · source line 262
void stopTempest(void)
Calls into this documentation:
- Execute
cancelHighScoreEntry(); - Execute
running = false; - Execute
ESP_LOGI("tempest", "STOP");
Read original function source
void stopTempest(void)
{
cancelHighScoreEntry();
running = false;
ESP_LOGI("tempest", "STOP");
}tempestAction() · source line 269
void tempestAction(bool start, bool zap, uint32_t now_ms)
Calls into this documentation:
- Decision
!runningTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
start && gameState == STATE_READYTrue- Execute
beginGame(now_ms); - Exit this function
return;
FalseContinue after this decision.
- Decision
zap && zapReady && gameState == STATE_PLAYINGTrue- Execute
zapReady = false; - Execute
zapFlash = 250U; - FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Decision
enemies[i].activeTrue- Execute
destroyEnemy(i);
FalseContinue after this decision.
- Execute
audioPlay(AUDIO_SFX_EXPLOSION);
FalseContinue after this decision.
Read original function source
void tempestAction(bool start, bool zap, uint32_t now_ms)
{
if (!running) return;
if (start && gameState == STATE_READY) {
beginGame(now_ms);
return;
}
if (zap && zapReady && gameState == STATE_PLAYING) {
zapReady = false;
zapFlash = 250U;
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
if (enemies[i].active) destroyEnemy(i);
}
audioPlay(AUDIO_SFX_EXPLOSION);
}
}joystickLane() · source line 286
static uint8_t joystickLane(int x_offset, int y_offset)
Calls into this documentation:
- Execute
const float angle = atan2f(-(float)y_offset, (float)x_offset); - Execute
float bestDifference = 4.0f; - Execute
uint8_t selected = playerLane; - FOR loop
unsigned offset = 0; offset < laneCount; ++offsetRepeat body while the loop condition permits; then continue below.
- Execute
const uint8_t lane = (playerLane + offset) % laneCount; - Execute
const uint8_t next = nextLane(lane, 1); - Execute
const float x = (outer[lane].x + outer[next].x) * 0.5f - 108.0f; - Execute
const float y = (outer[lane].y + outer[next].y) * 0.5f - 88.0f; - Execute
float difference = fabsf(atan2f(y, x) - angle); - Decision
difference > PITrue- Execute
difference = 2.0f * PI - difference;
FalseContinue after this decision.
- Decision
difference + 0.00001f < bestDifferenceTrue- Execute
bestDifference = difference; - Execute
selected = lane;
FalseContinue after this decision.
- Exit this function
return selected;
Read original function source
static uint8_t joystickLane(int x_offset, int y_offset)
{
const float angle = atan2f(-(float)y_offset, (float)x_offset);
float bestDifference = 4.0f;
uint8_t selected = playerLane;
// Compare actual lane centres so diagonals also match the oval web.
// Start at the current lane to keep it when two directions tie.
for (unsigned offset = 0; offset < laneCount; ++offset) {
const uint8_t lane = (playerLane + offset) % laneCount;
const uint8_t next = nextLane(lane, 1);
const float x = (outer[lane].x + outer[next].x) * 0.5f - 108.0f;
const float y = (outer[lane].y + outer[next].y) * 0.5f - 88.0f;
float difference = fabsf(atan2f(y, x) - angle);
if (difference > PI) difference = 2.0f * PI - difference;
if (difference + 0.00001f < bestDifference) {
bestDifference = difference;
selected = lane;
}
}
return selected;
}updateTempest() · source line 308
void updateTempest(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; - Decision
gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000UTrue- Execute
startHighScoreEntry(HIGH_SCORE_TEMPEST, 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_PLAYING && gameState != STATE_NEXT_WAVETrue- 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 direction = (int)right - (int)left; - Execute
const bool stickPosition = !left && !right && valid && controlsReady && (x_offset > 600 || x_offset < -600 || y_offset > 600 || y_offset < -600); - Execute
uint8_t targetLane = stickPosition ? joystickLane(x_offset, y_offset) : playerLane; - Execute
uint8_t targetLaneCount = laneCount; - Execute
accumulator += elapsed > 100U ? 100U : elapsed; - WHILE loop
accumulator >= STEP_MS && (gameState == STATE_PLAYING || gameState == STATE_NEXT_WAVE)Repeat body while the loop condition permits; then continue below.
- Execute
accumulator -= STEP_MS; - Decision
stickPosition && controlsReady && gameState == STATE_PLAYINGTrue- Decision
targetLaneCount != laneCountTrue- Execute
targetLane = joystickLane(x_offset, y_offset); - Execute
targetLaneCount = laneCount;
FalseContinue after this decision.
- Execute
playerLane = targetLane;
FalseContinue after this decision.
- Execute
simulateStep(direction, fire);
Read original function source
void updateTempest(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;
if (gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000U) {
startHighScoreEntry(HIGH_SCORE_TEMPEST, 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_PLAYING && gameState != STATE_NEXT_WAVE) return;
if (!valid) controlsReady = false;
else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
controlsReady = true;
const int direction = (int)right - (int)left;
const bool stickPosition = !left && !right && valid && controlsReady &&
(x_offset > 600 || x_offset < -600 || y_offset > 600 || y_offset < -600);
uint8_t targetLane = stickPosition ? joystickLane(x_offset, y_offset) : playerLane;
uint8_t targetLaneCount = laneCount;
accumulator += elapsed > 100U ? 100U : elapsed;
while (accumulator >= STEP_MS && (gameState == STATE_PLAYING || gameState == STATE_NEXT_WAVE)) {
accumulator -= STEP_MS;
if (stickPosition && controlsReady && gameState == STATE_PLAYING) {
// A wave transition can rebuild the web during catch-up steps.
if (targetLaneCount != laneCount) {
targetLane = joystickLane(x_offset, y_offset);
targetLaneCount = laneCount;
}
playerLane = targetLane;
}
simulateStep(direction, fire);
}
}webPoint() · source line 346
static Point webPoint(unsigned edge, float depth)
- Exit this function
return (Point){inner[edge].x + (outer[edge].x - inner[edge].x) * depth, inner[edge].y + (outer[edge].y - inner[edge].y) * depth};
Read original function source
static Point webPoint(unsigned edge, float depth)
{
return (Point){inner[edge].x + (outer[edge].x - inner[edge].x) * depth,
inner[edge].y + (outer[edge].y - inner[edge].y) * depth};
}lanePoint() · source line 352
static Point lanePoint(uint8_t lane, float depth)
Calls into this documentation:
- Execute
const Point a = webPoint(lane, depth), b = webPoint(nextLane(lane, 1), depth); - Exit this function
return (Point){(a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f};
Read original function source
static Point lanePoint(uint8_t lane, float depth)
{
const Point a = webPoint(lane, depth), b = webPoint(nextLane(lane, 1), depth);
return (Point){(a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f};
}line() · source line 358
static void line(uint16_t *frame, Point a, Point b, uint16_t color)
Calls into this documentation:
- Execute
draw_line(frame, (int)lroundf(a.x), (int)lroundf(a.y), (int)lroundf(b.x), (int)lroundf(b.y), color);
Read original function source
static void line(uint16_t *frame, Point a, Point b, uint16_t color)
{
draw_line(frame, (int)lroundf(a.x), (int)lroundf(a.y), (int)lroundf(b.x), (int)lroundf(b.y), color);
}drawTempest() · source line 363
void drawTempest(uint16_t *frame)
Calls into this documentation:
- Decision
!runningTrue- 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
char text[48]; - Decision
gameState == STATE_READYTrue- Execute
draw_text_centred(frame, 160, 15, "TEMPEST", 3, 0x07ff); - Execute
draw_text_centred(frame, 160, 43, "POINT STICK TO POSITION SHIP", 1, 0xffff); - Execute
draw_text_centred(frame, 160, 57, "D/B ROTATE LEFT/RIGHT", 1, 0xffff); - Execute
draw_text_centred(frame, 160, 72, "HOLD A FIRE", 1, 0xffff); - Execute
draw_text_centred(frame, 160, 92, "E SUPERZAPPER ONCE PER WAVE", 1, 0xffe0); - Execute
draw_text_centred(frame, 160, 153, "BOOT BACK", 1, 0x7bef); - Exit this function
return;
FalseContinue after this decision.
- Execute
static const uint16_t colors[] = {0x039f, 0x781f, 0x03ef, COLOR_DARKBLUE}; - Execute
const uint16_t webColor = zapFlash ? 0xffff : colors[(wave - 1U) % 4U]; - FOR loop
unsigned i = 0; i < laneCount; ++iRepeat body while the loop condition permits; then continue below.
- Execute
line(frame, inner[i], outer[i], webColor); - FOR loop
unsigned ring = 0; ring <= 4; ++ringRepeat body while the loop condition permits; then continue below.
- Execute
line(frame, webPoint(i, ring / 4.0f), webPoint(nextLane(i, 1), ring / 4.0f), webColor);
- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Enemy *enemy = &enemies[i]; - Decision
!enemy->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const Point p = lanePoint(enemy->lane, enemy->depth); - Execute
const float radius = 2.0f + enemy->depth * 3.0f; - Execute
const uint16_t color = enemy->rim ? 0xffe0 : enemy->flipper ? 0xf81f : 0xf800; - Execute
const Point a = {p.x, p.y - radius}, b = {p.x + radius, p.y}; - Execute
const Point c = {p.x, p.y + radius}, d = {p.x - radius, p.y}; - Execute
line(frame, a, b, color); - Execute
line(frame, b, c, color); - Execute
line(frame, c, d, color); - Execute
line(frame, d, a, color); - Decision
enemy->flipperTrue- Execute
line(frame, a, c, color); - Execute
line(frame, b, d, color);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_SHOTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
shots[i].activeTrue- Execute
line(frame, lanePoint(shots[i].lane, shots[i].depth), lanePoint(shots[i].lane, fminf(1.0f, shots[i].depth + 0.08f)), 0xffff);
FalseContinue after this decision.
- Decision
gameState != STATE_FINISHED && (protection == 0 || (protection / 125U) % 2 == 0)True- Execute
const Point a = webPoint(playerLane, 1.06f), b = webPoint(nextLane(playerLane, 1), 1.06f); - Execute
const Point nose = lanePoint(playerLane, 0.90f), rear = lanePoint(playerLane, 1.13f); - Execute
line(frame, a, nose, 0xffe0); - Execute
line(frame, nose, b, 0xffe0); - Execute
line(frame, b, rear, 0xffe0); - Execute
line(frame, rear, a, 0xffe0);
FalseContinue after this decision.
- Execute
draw_text(frame, 218, 8, "TEMPEST", 2, 0x07ff); - Execute
draw_text(frame, 218, 35, "SCORE", 1, 0xffff); - Execute
snprintf(text, sizeof(text), "%u", (unsigned)score); - Execute
draw_text(frame, 218, 47, text, 1, 0xffff); - Execute
snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave); - Execute
draw_text(frame, 218, 67, text, 1, 0xffff); - Execute
snprintf(text, sizeof(text), "LIVES %u", lives); - Execute
draw_text(frame, 218, 84, text, 1, 0xffff); - Execute
draw_text(frame, 218, 105, zapReady ? "ZAP READY" : "ZAP USED", 1, 0xffe0); - Execute
draw_text(frame, 218, 127, "A FIRE E ZAP", 1, 0x7bef); - Execute
draw_text(frame, 218, 150, "BOOT BACK", 1, 0x7bef); - Decision
gameState == STATE_NEXT_WAVE || gameState == STATE_FINISHEDTrue- Execute
draw_text_centred(frame, 111, 90, gameState == STATE_FINISHED ? "GAME OVER" : "WAVE CLEAR", 2, COLOR_RED);
FalseContinue after this decision.
Read original function source
void drawTempest(uint16_t *frame)
{
if (!running) return;
if (gameState == STATE_SCORES) {
drawHighScoreEntry(frame);
return;
}
clear(frame, 0);
char text[48];
if (gameState == STATE_READY) {
draw_text_centred(frame, 160, 15, "TEMPEST", 3, 0x07ff);
draw_text_centred(frame, 160, 43, "POINT STICK TO POSITION SHIP", 1, 0xffff);
draw_text_centred(frame, 160, 57, "D/B ROTATE LEFT/RIGHT", 1, 0xffff);
draw_text_centred(frame, 160, 72, "HOLD A FIRE", 1, 0xffff);
draw_text_centred(frame, 160, 92, "E SUPERZAPPER ONCE PER WAVE", 1, 0xffe0);
draw_text_centred(frame, 160, 153, "BOOT BACK", 1, 0x7bef);
return;
}
static const uint16_t colors[] = {0x039f, 0x781f, 0x03ef, COLOR_DARKBLUE};
const uint16_t webColor = zapFlash ? 0xffff : colors[(wave - 1U) % 4U];
for (unsigned i = 0; i < laneCount; ++i) {
line(frame, inner[i], outer[i], webColor);
for (unsigned ring = 0; ring <= 4; ++ring)
line(frame, webPoint(i, ring / 4.0f), webPoint(nextLane(i, 1), ring / 4.0f), webColor);
}
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
const Enemy *enemy = &enemies[i];
if (!enemy->active) continue;
const Point p = lanePoint(enemy->lane, enemy->depth);
const float radius = 2.0f + enemy->depth * 3.0f;
const uint16_t color = enemy->rim ? 0xffe0 : enemy->flipper ? 0xf81f : 0xf800;
const Point a = {p.x, p.y - radius}, b = {p.x + radius, p.y};
const Point c = {p.x, p.y + radius}, d = {p.x - radius, p.y};
line(frame, a, b, color); line(frame, b, c, color);
line(frame, c, d, color); line(frame, d, a, color);
if (enemy->flipper) { line(frame, a, c, color); line(frame, b, d, color); }
}
for (unsigned i = 0; i < MAX_SHOTS; ++i) {
if (shots[i].active)
line(frame, lanePoint(shots[i].lane, shots[i].depth),
lanePoint(shots[i].lane, fminf(1.0f, shots[i].depth + 0.08f)), 0xffff);
}
if (gameState != STATE_FINISHED && (protection == 0 || (protection / 125U) % 2 == 0)) {
const Point a = webPoint(playerLane, 1.06f), b = webPoint(nextLane(playerLane, 1), 1.06f);
const Point nose = lanePoint(playerLane, 0.90f), rear = lanePoint(playerLane, 1.13f);
line(frame, a, nose, 0xffe0); line(frame, nose, b, 0xffe0);
line(frame, b, rear, 0xffe0); line(frame, rear, a, 0xffe0);
}
draw_text(frame, 218, 8, "TEMPEST", 2, 0x07ff);
draw_text(frame, 218, 35, "SCORE", 1, 0xffff);
snprintf(text, sizeof(text), "%u", (unsigned)score);
draw_text(frame, 218, 47, text, 1, 0xffff);
snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
draw_text(frame, 218, 67, text, 1, 0xffff);
snprintf(text, sizeof(text), "LIVES %u", lives);
draw_text(frame, 218, 84, text, 1, 0xffff);
draw_text(frame, 218, 105, zapReady ? "ZAP READY" : "ZAP USED", 1, 0xffe0);
draw_text(frame, 218, 127, "A FIRE E ZAP", 1, 0x7bef);
draw_text(frame, 218, 150, "BOOT BACK", 1, 0x7bef);
if (gameState == STATE_NEXT_WAVE || gameState == STATE_FINISHED) {
draw_text_centred(frame, 111, 90,
gameState == STATE_FINISHED ? "GAME OVER" : "WAVE CLEAR",
2, COLOR_RED);
}
}Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "tempest.h"
#include "audio.h"
#include "game_graphics.h"
#include "gold.h"
#include "high_scores.h"
#include "PET2001_font.h"
#include "esp_random.h"
#include "esp_log.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#define MAX_LANES 16
#define MAX_ENEMIES 24
#define MAX_SHOTS 8
#define STEP_MS 10U
#define PI 3.14159265f
typedef enum { STATE_READY, STATE_PLAYING, STATE_NEXT_WAVE, STATE_FINISHED, STATE_SCORES } GameState;
typedef struct {
float depth;
uint32_t moveDue;
uint8_t lane;
int8_t direction;
bool active, flipper, rim;
} Enemy;
typedef struct { float depth; uint8_t lane; bool active; } Shot;
typedef struct { float x, y; } Point;
static Enemy enemies[MAX_ENEMIES];
static Shot shots[MAX_SHOTS];
static Point outer[MAX_LANES], inner[MAX_LANES];
static uint8_t laneCount = MAX_LANES;
static GameState gameState;
static uint32_t score, wave, lastUpdate, accumulator, spawnDue, fireDue;
static uint32_t moveDue, protection, transition, zapFlash;
static uint32_t gameOverStarted;
static unsigned remaining;
static uint8_t playerLane, lives;
static int heldDirection;
static bool running, controlsReady, zapReady;
static uint8_t nextLane(uint8_t lane, int direction)
{
return (uint8_t)((lane + direction + laneCount) % laneCount);
}
static uint32_t decrease(uint32_t value)
{
return value > STEP_MS ? value - STEP_MS : 0;
}
static unsigned activeEnemies(void)
{
unsigned count = 0;
for (unsigned i = 0; i < MAX_ENEMIES; ++i) count += enemies[i].active;
return count;
}
static void destroyEnemy(unsigned index)
{
const uint32_t points = enemies[index].flipper ? 150U : 100U;
score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
enemies[index].active = false;
}
static void updateWeb(void)
{
for (unsigned i = 0; i < laneCount; ++i) {
const float angle = -PI / 2.0f + i * 2.0f * PI / laneCount;
outer[i] = (Point){108.0f + cosf(angle) * 91.0f, 88.0f + sinf(angle) * 64.0f};
inner[i] = (Point){108.0f + cosf(angle) * 19.0f, 88.0f + sinf(angle) * 13.0f};
}
}
static void beginWave(void)
{
memset(enemies, 0, sizeof(enemies));
memset(shots, 0, sizeof(shots));
const uint8_t previousLanes = laneCount;
if (wave <= 8U) {
laneCount = 7U + ((wave - 1U) / 2U) * 3U;
} else if ((wave & 1U) != 0) {
// Keep each random web for two waves; repeated counts are allowed.
laneCount = 8U + esp_random() % 9U;
}
// Map the old lane centre onto the new web to preserve angular position.
playerLane = (uint8_t)(((2U * playerLane + 1U) * laneCount) / (2U * previousLanes));
updateWeb();
remaining = 8U + (wave < 12U ? wave : 12U) * 2U;
spawnDue = 600U;
fireDue = moveDue = heldDirection = 0;
protection = 1800U;
zapReady = true;
gameState = STATE_PLAYING;
ESP_LOGI("tempest", "WAVE %u; LANES %u", (unsigned)wave, (unsigned)laneCount);
}
static void beginGame(uint32_t now_ms)
{
score = 0;
wave = 1;
lives = 3;
laneCount = MAX_LANES;
playerLane = 0;
accumulator = zapFlash = transition = 0;
lastUpdate = now_ms;
controlsReady = false;
beginWave();
audioPlay(AUDIO_SFX_GAME_START);
}
static void spawnEnemy(void)
{
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
if (enemies[i].active) continue;
enemies[i] = (Enemy){
.depth = 0.0f,
.moveDue = 1000U + esp_random() % 800U,
.lane = esp_random() % laneCount,
.direction = esp_random() % 2 ? 1 : -1,
.active = true,
.flipper = wave >= 2U && esp_random() % 3 == 0,
};
--remaining;
spawnDue = 1100U - (wave < 10U ? wave : 10U) * 60U;
return;
}
}
static void shoot(void)
{
if (fireDue) return;
for (unsigned i = 0; i < MAX_SHOTS; ++i) {
if (shots[i].active) continue;
shots[i] = (Shot){1.0f, playerLane, true};
fireDue = 140U;
audioPlay(AUDIO_SFX_PLAYER_SHOT);
return;
}
}
static void loseLife(void)
{
--lives; // lives minus 1
ESP_LOGI("tempest", "LIVES %u; SCORE %u", lives, (unsigned)score);
memset(enemies, 0, sizeof(enemies));
memset(shots, 0, sizeof(shots));
protection = 1800U;
controlsReady = false;
heldDirection = 0;
if (lives == 0) {
gameState = STATE_FINISHED;
gameOverStarted = lastUpdate;
audioPlay(AUDIO_SFX_GAME_OVER);
// Award whole thousands once, when the game ends.
const uint32_t gold = score / 1000U;
if (gold != 0) {
if (addUserGoldWon(gold)) {
ESP_LOGI("tempest", "GOLD WON: %u", (unsigned)gold);
} else {
ESP_LOGE("tempest", "Failed to save %u Gold", (unsigned)gold);
}
}
} else {
audioPlay(AUDIO_SFX_LIFE_LOST);
}
}
static void simulateStep(int direction, bool fire)
{
zapFlash = decrease(zapFlash);
if (gameState == STATE_NEXT_WAVE) {
transition = decrease(transition);
if (transition == 0) {
if (wave < UINT32_MAX) ++wave;
beginWave();
}
return;
}
protection = decrease(protection);
spawnDue = decrease(spawnDue);
fireDue = decrease(fireDue);
moveDue = decrease(moveDue);
if (direction == 0) heldDirection = 0;
else if (heldDirection != direction || moveDue == 0) {
playerLane = nextLane(playerLane, direction);
moveDue = heldDirection == direction ? 100U : 180U;
heldDirection = direction;
}
if (remaining && spawnDue == 0) spawnEnemy();
if (fire) shoot();
const float speed = 0.075f + (wave < 15U ? wave : 15U) * 0.009f;
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
Enemy *enemy = &enemies[i];
if (!enemy->active) continue;
enemy->moveDue = decrease(enemy->moveDue);
if (enemy->rim) {
if (enemy->moveDue == 0) {
enemy->lane = nextLane(enemy->lane, enemy->direction);
enemy->moveDue = 240U;
}
} else {
enemy->depth += speed * (STEP_MS / 1000.0f);
if (enemy->flipper && enemy->moveDue == 0) {
enemy->lane = nextLane(enemy->lane, enemy->direction);
enemy->moveDue = 900U;
}
if (enemy->depth >= 1.0f) {
enemy->depth = 1.0f;
enemy->rim = true;
enemy->moveDue = 240U;
}
}
}
// A shot hits the first enemy along its travelled segment, including the rim.
for (unsigned i = 0; i < MAX_SHOTS; ++i) {
Shot *shot = &shots[i];
if (!shot->active) continue;
const float before = shot->depth;
shot->depth -= 1.8f * (STEP_MS / 1000.0f);
int target = -1;
float nearest = -1.0f;
for (unsigned j = 0; j < MAX_ENEMIES; ++j) {
const Enemy *enemy = &enemies[j];
if (!enemy->active || enemy->lane != shot->lane) continue;
if (enemy->depth <= before + 0.035f && enemy->depth >= shot->depth - 0.035f &&
enemy->depth > nearest) { target = (int)j; nearest = enemy->depth; }
}
if (target >= 0) {
destroyEnemy((unsigned)target);
shot->active = false;
audioPlay(AUDIO_SFX_HIT);
}
else if (shot->depth < 0.0f) shot->active = false;
}
if (protection == 0) {
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
if (enemies[i].active && enemies[i].rim && enemies[i].lane == playerLane) {
loseLife();
return;
}
}
}
if (remaining == 0 && activeEnemies() == 0) {
gameState = STATE_NEXT_WAVE;
transition = 1500U;
memset(shots, 0, sizeof(shots));
audioPlay(AUDIO_SFX_WAVE_CLEAR);
}
}
void startTempest(uint32_t now_ms)
{
running = true;
gameState = STATE_READY;
lastUpdate = now_ms;
laneCount = MAX_LANES;
updateWeb();
ESP_LOGI("tempest", "START");
}
void stopTempest(void)
{
cancelHighScoreEntry();
running = false;
ESP_LOGI("tempest", "STOP");
}
void tempestAction(bool start, bool zap, uint32_t now_ms)
{
if (!running) return;
if (start && gameState == STATE_READY) {
beginGame(now_ms);
return;
}
if (zap && zapReady && gameState == STATE_PLAYING) {
zapReady = false;
zapFlash = 250U;
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
if (enemies[i].active) destroyEnemy(i);
}
audioPlay(AUDIO_SFX_EXPLOSION);
}
}
static uint8_t joystickLane(int x_offset, int y_offset)
{
const float angle = atan2f(-(float)y_offset, (float)x_offset);
float bestDifference = 4.0f;
uint8_t selected = playerLane;
// Compare actual lane centres so diagonals also match the oval web.
// Start at the current lane to keep it when two directions tie.
for (unsigned offset = 0; offset < laneCount; ++offset) {
const uint8_t lane = (playerLane + offset) % laneCount;
const uint8_t next = nextLane(lane, 1);
const float x = (outer[lane].x + outer[next].x) * 0.5f - 108.0f;
const float y = (outer[lane].y + outer[next].y) * 0.5f - 88.0f;
float difference = fabsf(atan2f(y, x) - angle);
if (difference > PI) difference = 2.0f * PI - difference;
if (difference + 0.00001f < bestDifference) {
bestDifference = difference;
selected = lane;
}
}
return selected;
}
void updateTempest(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;
if (gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000U) {
startHighScoreEntry(HIGH_SCORE_TEMPEST, 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_PLAYING && gameState != STATE_NEXT_WAVE) return;
if (!valid) controlsReady = false;
else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
controlsReady = true;
const int direction = (int)right - (int)left;
const bool stickPosition = !left && !right && valid && controlsReady &&
(x_offset > 600 || x_offset < -600 || y_offset > 600 || y_offset < -600);
uint8_t targetLane = stickPosition ? joystickLane(x_offset, y_offset) : playerLane;
uint8_t targetLaneCount = laneCount;
accumulator += elapsed > 100U ? 100U : elapsed;
while (accumulator >= STEP_MS && (gameState == STATE_PLAYING || gameState == STATE_NEXT_WAVE)) {
accumulator -= STEP_MS;
if (stickPosition && controlsReady && gameState == STATE_PLAYING) {
// A wave transition can rebuild the web during catch-up steps.
if (targetLaneCount != laneCount) {
targetLane = joystickLane(x_offset, y_offset);
targetLaneCount = laneCount;
}
playerLane = targetLane;
}
simulateStep(direction, fire);
}
}
static Point webPoint(unsigned edge, float depth)
{
return (Point){inner[edge].x + (outer[edge].x - inner[edge].x) * depth,
inner[edge].y + (outer[edge].y - inner[edge].y) * depth};
}
static Point lanePoint(uint8_t lane, float depth)
{
const Point a = webPoint(lane, depth), b = webPoint(nextLane(lane, 1), depth);
return (Point){(a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f};
}
static void line(uint16_t *frame, Point a, Point b, uint16_t color)
{
draw_line(frame, (int)lroundf(a.x), (int)lroundf(a.y), (int)lroundf(b.x), (int)lroundf(b.y), color);
}
void drawTempest(uint16_t *frame)
{
if (!running) return;
if (gameState == STATE_SCORES) {
drawHighScoreEntry(frame);
return;
}
clear(frame, 0);
char text[48];
if (gameState == STATE_READY) {
drawPet2001TextCentredTransparent(frame, 160, 15, "TEMPEST", 0x07ff, 3);
drawPet2001TextCentredTransparent(frame, 160, 43,
"POINT STICK TO POSITION SHIP", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, 160, 57,
"D/B ROTATE LEFT/RIGHT", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, 160, 72, "HOLD A FIRE", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, 160, 92,
"E SUPERZAPPER ONCE PER WAVE", 0xffe0, 1);
drawPet2001TextCentredTransparent(frame, 160, 153, "BOOT BACK", 0x7bef, 1);
return;
}
static const uint16_t colors[] = {0x039f, 0x781f, 0x03ef, COLOR_DARKBLUE};
const uint16_t webColor = zapFlash ? 0xffff : colors[(wave - 1U) % 4U];
for (unsigned i = 0; i < laneCount; ++i) {
line(frame, inner[i], outer[i], webColor);
for (unsigned ring = 0; ring <= 4; ++ring)
line(frame, webPoint(i, ring / 4.0f), webPoint(nextLane(i, 1), ring / 4.0f), webColor);
}
for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
const Enemy *enemy = &enemies[i];
if (!enemy->active) continue;
const Point p = lanePoint(enemy->lane, enemy->depth);
const float radius = 2.0f + enemy->depth * 3.0f;
const uint16_t color = enemy->rim ? 0xffe0 : enemy->flipper ? 0xf81f : 0xf800;
const Point a = {p.x, p.y - radius}, b = {p.x + radius, p.y};
const Point c = {p.x, p.y + radius}, d = {p.x - radius, p.y};
line(frame, a, b, color); line(frame, b, c, color);
line(frame, c, d, color); line(frame, d, a, color);
if (enemy->flipper) { line(frame, a, c, color); line(frame, b, d, color); }
}
for (unsigned i = 0; i < MAX_SHOTS; ++i) {
if (shots[i].active)
line(frame, lanePoint(shots[i].lane, shots[i].depth),
lanePoint(shots[i].lane, fminf(1.0f, shots[i].depth + 0.08f)), 0xffff);
}
if (gameState != STATE_FINISHED && (protection == 0 || (protection / 125U) % 2 == 0)) {
const Point a = webPoint(playerLane, 1.06f), b = webPoint(nextLane(playerLane, 1), 1.06f);
const Point nose = lanePoint(playerLane, 0.90f), rear = lanePoint(playerLane, 1.13f);
line(frame, a, nose, 0xffe0); line(frame, nose, b, 0xffe0);
line(frame, b, rear, 0xffe0); line(frame, rear, a, 0xffe0);
}
drawPet2001TextTransparent(frame, 208, 8, "TEMPEST", 0x07ff, 2);
drawPet2001TextTransparent(frame, 218, 35, "SCORE", 0xffff, 1);
snprintf(text, sizeof(text), "%u", (unsigned)score);
drawPet2001TextTransparent(frame, 218, 47, text, 0xffff, 1);
snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
drawPet2001TextTransparent(frame, 218, 67, text, 0xffff, 1);
snprintf(text, sizeof(text), "LIVES %u", lives);
drawPet2001TextTransparent(frame, 218, 84, text, 0xffff, 1);
drawPet2001TextTransparent(frame, 218, 105, zapReady ? "ZAP READY" : "ZAP USED", 0xffe0, 1);
drawPet2001TextTransparent(frame, 218, 127, "A FIRE E ZAP", 0x7bef, 1);
drawPet2001TextTransparent(frame, 218, 150, "BOOT BACK", 0x7bef, 1);
if (gameState == STATE_NEXT_WAVE || gameState == STATE_FINISHED) {
drawPet2001TextCentredTransparent(frame, 111, 90,
gameState == STATE_FINISHED ? "GAME OVER" : "WAVE CLEAR",
COLOR_RED, 2);
}
}