mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-05 04:27:40 +00:00
523 lines
17 KiB
C
523 lines
17 KiB
C
/* Copyright 2026 Armel F4HWN
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*/
|
|
|
|
/*
|
|
* Space Impact-inspired scrolling shooter for the 128x64 monochrome LCD.
|
|
*
|
|
* The keypad reports a single key at a time, so the gun fires automatically
|
|
* and the keys are free for movement.
|
|
*
|
|
* Controls: 2/8 or UP/DOWN move the ship, 5/MENU launches a piercing
|
|
* missile, F pauses and EXIT quits.
|
|
*
|
|
* Texts, tables and artwork live in the app's read-only assets (built by
|
|
* gen_assets.py), not in the 4 KiB overlay: sprites are fetched from external
|
|
* flash as they are drawn and the title screen is streamed straight into the
|
|
* frame buffer.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include "../app_api.h"
|
|
#include "spaceimpact_assets.h" /* generated by gen_assets.py */
|
|
|
|
#define W 128
|
|
#define TICK_MS 40u
|
|
#define TOP 9 /* first playfield row, below the HUD */
|
|
#define PX 6 /* the ship only moves vertically */
|
|
#define Y_MAX 54 /* lowest sprite row, just above the hills */
|
|
#define N_ENEMY 6u
|
|
#define N_SHOT 5u
|
|
#define N_HOSTILE 6u
|
|
#define LEVEL_LEN 700u /* ticks of waves before the boss */
|
|
#define BOOM 10u /* explosion length in ticks */
|
|
|
|
enum { ST_PLAY, ST_OVER, ST_TITLE };
|
|
/* Countdowns, all decremented once per tick. */
|
|
enum { CD_FIRE, CD_HOSTILE, CD_INVUL, CD_BANNER, CD_WAIT, CD_COUNT };
|
|
|
|
/* Rows never go negative (TOP..63), so they are unsigned bytes, which Thumb-1
|
|
* loads with an immediate offset (signed ones need an extra step). */
|
|
typedef struct { int16_t x; uint8_t y; int8_t vx, vy; uint8_t live; } shot_t;
|
|
/* state: 0 free, 1 alive, >1 exploding while the counter decreases. */
|
|
typedef struct { int16_t x; uint8_t y, base, type, hp, state, phase; } enemy_t;
|
|
|
|
/* All the state in one struct: Thumb-1 code then reaches every field from a
|
|
* single base address (one literal per function instead of one per global),
|
|
* byte fields first so their offsets fit ldrb's 0..31 immediate. */
|
|
struct globals {
|
|
uint8_t prevKey;
|
|
/* NEW_GAME's values, read whole by new_game(): same order and sizes */
|
|
uint8_t mode;
|
|
bool paused;
|
|
uint8_t kills, py, lives, level, missiles, spawnCd;
|
|
uint8_t cd[CD_COUNT];
|
|
uint8_t bossY, bossHp, bossMax;
|
|
int8_t bossDir;
|
|
bool running, boss, saver;
|
|
uint16_t distance;
|
|
uint16_t frame; /* 16-bit so the slow star layers wrap without a jump */
|
|
int32_t bossX; /* a word: loaded without a sign extension */
|
|
uint32_t score;
|
|
const app_api_t *api;
|
|
/* contiguous: new_game() clears the three pools at once */
|
|
enemy_t enemies[N_ENEMY];
|
|
shot_t shots[N_SHOT], hostile[N_HOSTILE];
|
|
};
|
|
static struct globals g;
|
|
#define A (g.api)
|
|
|
|
_Static_assert(ST_PLAY == 0, "NEW_GAME starts a game in ST_PLAY");
|
|
_Static_assert(offsetof(struct globals, cd) + CD_COUNT - offsetof(struct globals, mode)
|
|
== NEW_GAME_LEN, "new-game fields do not match NEW_GAME");
|
|
_Static_assert(offsetof(struct globals, saver) < 32u, "byte fields out of ldrb range");
|
|
|
|
void *memset(void *dst, int value, size_t size)
|
|
{
|
|
uint8_t *p = dst;
|
|
while (size--) *p++ = (uint8_t)value;
|
|
return dst;
|
|
}
|
|
|
|
static uint8_t asset_u8(uint16_t off)
|
|
{
|
|
uint8_t v;
|
|
A->asset_read(off, &v, 1);
|
|
return v;
|
|
}
|
|
|
|
static uint32_t rnd(void)
|
|
{
|
|
return A->rand32() >> 8; /* resident PRNG, seeded by the loader */
|
|
}
|
|
|
|
static void pixel(int x, int y)
|
|
{
|
|
if ((unsigned)x >= W || (unsigned)y >= 64u) return;
|
|
uint8_t *p = y < 8 ? &A->status_line[x] : &A->fb[(y >> 3) - 1][x];
|
|
*p |= (uint8_t)(1u << (y & 7));
|
|
}
|
|
|
|
/* Draw a column-major sprite (up to 8 rows high, LSB = top, w <= 16) read
|
|
* from the assets at offset art. */
|
|
static void blit(int x, int y, uint16_t art, uint8_t w)
|
|
{
|
|
uint8_t cols[16];
|
|
const uint8_t *col = cols;
|
|
A->asset_read(art, cols, w);
|
|
for (; w; w--, x++) {
|
|
uint8_t v = *col++;
|
|
for (int yy = y; v; v >>= 1, yy++)
|
|
if (v & 1u) pixel(x, yy);
|
|
}
|
|
}
|
|
|
|
/* A centred bold text from the assets on LCD line `line`; a non-zero digit
|
|
* replaces its 7th character ("LEVEL n"). */
|
|
static void say(uint16_t off, uint8_t line, char digit)
|
|
{
|
|
char buf[TEXT_MAX];
|
|
A->asset_read(off, buf, TEXT_MAX);
|
|
if (digit) buf[6] = digit;
|
|
A->print_bold(buf, 0, 127, line);
|
|
}
|
|
|
|
static bool overlap(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh)
|
|
{
|
|
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
|
}
|
|
|
|
static bool hits_player(int x, int y, int w, int h)
|
|
{
|
|
return g.mode == ST_PLAY && overlap(x, y, w, h, PX + 1, g.py + 1, 8, 5);
|
|
}
|
|
|
|
static int aim(int y)
|
|
{
|
|
return y < g.py - 1 ? 1 : (y > g.py + 7 ? -1 : 0);
|
|
}
|
|
|
|
/* `width` digits of value (at most 5, no division) in tiny type on the HUD. */
|
|
static void hud_number(uint32_t value, uint8_t width, uint8_t x)
|
|
{
|
|
uint16_t place[5];
|
|
char buf[6];
|
|
A->asset_read(PLACE, place, sizeof(place));
|
|
const uint8_t first = (uint8_t)(5u - width);
|
|
for (uint8_t i = 0; i < width; i++) {
|
|
uint8_t digit = 0;
|
|
while (value >= place[first + i]) {
|
|
value -= place[first + i];
|
|
digit++;
|
|
}
|
|
buf[i] = (char)('0' + digit);
|
|
}
|
|
buf[width] = 0;
|
|
A->print_tiny(buf, x, 1, true, true);
|
|
}
|
|
|
|
static shot_t *free_shot(shot_t *pool, uint8_t count)
|
|
{
|
|
for (; count; count--, pool++)
|
|
if (!pool->live) return pool;
|
|
return NULL;
|
|
}
|
|
|
|
/* A player shot (kind 1) or missile (kind 2) from the ship's nose row. */
|
|
static bool fire(int x, uint8_t kind)
|
|
{
|
|
shot_t *s = free_shot(g.shots, N_SHOT);
|
|
if (!s) return false;
|
|
s->x = (int16_t)x; s->y = (uint8_t)(g.py + 3); s->live = kind;
|
|
return true;
|
|
}
|
|
|
|
static void fire_hostile(int x, int y, int vx, int vy)
|
|
{
|
|
shot_t *s = free_shot(g.hostile, N_HOSTILE);
|
|
if (!s) return;
|
|
s->x = (int16_t)x; s->y = (uint8_t)y;
|
|
s->vx = (int8_t)vx; s->vy = (int8_t)vy; s->live = 1u;
|
|
}
|
|
|
|
/* Spawn an explosion, `delay` ticks later; recycles the last slot if full. */
|
|
static void boom(int x, int y, uint8_t delay)
|
|
{
|
|
enemy_t *e = g.enemies;
|
|
while (e->state && e < &g.enemies[N_ENEMY - 1u]) e++;
|
|
e->x = (int16_t)x; e->y = (uint8_t)y; e->state = (uint8_t)(BOOM + delay);
|
|
}
|
|
|
|
static void start_level(void)
|
|
{
|
|
g.distance = 0; g.cd[CD_BANNER] = 50u; g.spawnCd = 0; g.boss = false;
|
|
}
|
|
|
|
static void new_game(void)
|
|
{
|
|
memset((uint8_t *)&g + offsetof(struct globals, enemies), 0,
|
|
offsetof(struct globals, hostile) + sizeof(g.hostile)
|
|
- offsetof(struct globals, enemies)); /* the three pools */
|
|
A->asset_read(NEW_GAME, &g.mode, NEW_GAME_LEN);
|
|
g.score = 0;
|
|
start_level();
|
|
}
|
|
|
|
static void player_hit(void)
|
|
{
|
|
if (g.cd[CD_INVUL] || g.mode != ST_PLAY) return;
|
|
boom(PX + 1, g.py, 0);
|
|
A->play_tone(200u, 60u);
|
|
memset(g.hostile, 0, sizeof(g.hostile));
|
|
if (--g.lives == 0u) { g.mode = ST_OVER; g.cd[CD_WAIT] = 30u; return; }
|
|
g.cd[CD_INVUL] = 60u; g.py = 30u;
|
|
}
|
|
|
|
static void kill_enemy(enemy_t *e)
|
|
{
|
|
e->state = BOOM;
|
|
g.score += asset_u8(POINTS + e->type);
|
|
if ((++g.kills & 15u) == 0u && g.missiles < 9u) g.missiles++;
|
|
}
|
|
|
|
static void kill_boss(void)
|
|
{
|
|
g.boss = false;
|
|
g.score += 500u * g.level;
|
|
for (uint8_t i = 0; i < N_ENEMY; i++) { /* bursts all over the hull */
|
|
const uint32_t r = rnd();
|
|
boom(g.bossX + (int)(r & 7u), g.bossY + (int)((r >> 4) & 7u), (uint8_t)(i * 3u));
|
|
}
|
|
A->play_tone(700u, 150u);
|
|
memset(g.hostile, 0, sizeof(g.hostile));
|
|
if (g.level < 9u) g.level++;
|
|
if (g.lives < 5u) g.lives++;
|
|
if (g.missiles < 9u) g.missiles++;
|
|
start_level();
|
|
}
|
|
|
|
static void spawn_wave(void)
|
|
{
|
|
const uint32_t r = rnd();
|
|
const uint8_t type = (uint8_t)(r & 3u);
|
|
const uint8_t y = (uint8_t)(TOP + 2 + ((r >> 8) & 31u) + ((r >> 16) & 15u));
|
|
uint8_t n = type == 2u ? 1u : 3u;
|
|
for (uint8_t i = 0, k = 0; i < N_ENEMY && k < n; i++) {
|
|
enemy_t *e = &g.enemies[i];
|
|
if (e->state) continue;
|
|
e->x = (int16_t)(W + k * 12u); e->y = e->base = y;
|
|
e->type = type; e->hp = type == 2u ? 3u : 1u;
|
|
e->state = 1u; e->phase = (uint8_t)(k * 10u);
|
|
k++;
|
|
}
|
|
}
|
|
|
|
static bool enemies_alive(void)
|
|
{
|
|
for (uint8_t i = 0; i < N_ENEMY; i++) if (g.enemies[i].state == 1u) return true;
|
|
return false;
|
|
}
|
|
|
|
static void update_enemies(void)
|
|
{
|
|
for (uint8_t i = 0; i < N_ENEMY; i++) {
|
|
enemy_t *e = &g.enemies[i];
|
|
if (!e->state) continue;
|
|
if (e->state > 1u) { if (--e->state == 1u) e->state = 0; continue; }
|
|
|
|
const uint8_t t = e->type;
|
|
int y = e->y;
|
|
e->phase++;
|
|
if (t != 2u || (g.frame & 1u)) e->x -= asset_u8(SPEED + t); /* saucer: half speed */
|
|
if (t == 0u) {
|
|
const uint8_t p = (e->phase >> 1) & 31u;
|
|
y = e->base - 8 + (int)(p < 16u ? p : 31u - p); /* triangle wave */
|
|
} else if (t == 3u && (g.frame & 1u)) {
|
|
y += aim(y);
|
|
}
|
|
if (y < TOP) y = TOP;
|
|
if (y > Y_MAX) y = Y_MAX;
|
|
e->y = (uint8_t)y;
|
|
|
|
if (e->x < -8) { e->state = 0; continue; }
|
|
if (hits_player(e->x, e->y, 8, 7)) { kill_enemy(e); player_hit(); continue; }
|
|
if (!g.cd[CD_HOSTILE] && e->x < 118 && e->x > PX + 24) {
|
|
fire_hostile(e->x - 1, y + 3, g.level > 4u ? -3 : -2, aim(y));
|
|
g.cd[CD_HOSTILE] = (uint8_t)(38u - g.level * 3u);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void update_boss(void)
|
|
{
|
|
if (!g.boss) return;
|
|
if (g.bossX > 108) {
|
|
g.bossX--;
|
|
} else if ((g.frame & 1u) || g.level > 3u) {
|
|
g.bossY = (uint8_t)(g.bossY + g.bossDir);
|
|
if (g.bossY <= TOP + 1 || g.bossY >= Y_MAX - 9) g.bossDir = (int8_t)-g.bossDir;
|
|
}
|
|
if (!g.cd[CD_HOSTILE] && g.bossX < 116) {
|
|
for (int vy = -1; vy <= 1; vy++)
|
|
fire_hostile(g.bossX, g.bossY + 9, -2, vy);
|
|
g.cd[CD_HOSTILE] = (uint8_t)(28u - g.level * 2u);
|
|
}
|
|
if (hits_player(g.bossX + 2, g.bossY, 14, 16)) player_hit();
|
|
}
|
|
|
|
static void update_shots(void)
|
|
{
|
|
for (uint8_t i = 0; i < N_SHOT; i++) {
|
|
shot_t *s = &g.shots[i];
|
|
if (!s->live) continue;
|
|
const bool missile = s->live == 2u;
|
|
s->x += missile ? 3 : 4;
|
|
if (s->x >= W) { s->live = 0; continue; }
|
|
if (g.boss && overlap(s->x, s->y, 6, 1, g.bossX + 1, g.bossY, 15, 16)) {
|
|
const uint8_t dmg = missile ? 6u : 1u;
|
|
s->live = 0;
|
|
if (g.bossHp > dmg) g.bossHp -= dmg; else kill_boss();
|
|
continue;
|
|
}
|
|
for (uint8_t j = 0; j < N_ENEMY; j++) {
|
|
enemy_t *e = &g.enemies[j];
|
|
if (e->state != 1u || !overlap(s->x, s->y, 6, 1, e->x, e->y, 8, 7)) continue;
|
|
if (missile || --e->hp == 0u) kill_enemy(e); /* missiles pierce */
|
|
if (!missile) { s->live = 0; break; }
|
|
}
|
|
}
|
|
|
|
for (uint8_t i = 0; i < N_HOSTILE; i++) {
|
|
shot_t *s = &g.hostile[i];
|
|
if (!s->live) continue;
|
|
s->x += s->vx; s->y = (uint8_t)(s->y + s->vy);
|
|
if (s->x < -2 || s->y < TOP || s->y > 62) { s->live = 0; continue; }
|
|
if (hits_player(s->x - 1, s->y - 1, 3, 3)) { s->live = 0; player_hit(); }
|
|
}
|
|
}
|
|
|
|
static void tick(void)
|
|
{
|
|
g.frame++;
|
|
for (uint8_t i = 0; i < CD_COUNT; i++)
|
|
if (g.cd[i]) g.cd[i]--;
|
|
|
|
if (g.mode == ST_PLAY && !g.cd[CD_FIRE] && fire(PX + 9, 1u))
|
|
g.cd[CD_FIRE] = 5u;
|
|
|
|
if (!g.boss && !g.cd[CD_BANNER]) {
|
|
if (g.distance < LEVEL_LEN) {
|
|
g.distance++;
|
|
if (g.spawnCd) g.spawnCd--;
|
|
else { spawn_wave(); g.spawnCd = (uint8_t)(52u - g.level * 3u + (rnd() & 31u)); }
|
|
} else if (!enemies_alive()) {
|
|
g.boss = true; g.bossX = W + 4; g.bossY = 24; g.bossDir = 1;
|
|
g.bossHp = g.bossMax = (uint8_t)(20u + g.level * 6u);
|
|
g.cd[CD_HOSTILE] = 30u;
|
|
}
|
|
}
|
|
update_enemies();
|
|
update_boss();
|
|
update_shots();
|
|
}
|
|
|
|
static void draw_hud(void)
|
|
{
|
|
for (uint8_t i = 0; i < g.lives; i++) blit(1 + i * 6, 1, SPR_HEART, 5);
|
|
blit(33, 1, SPR_MISS, 8);
|
|
hud_number(g.missiles, 1, 43);
|
|
hud_number(g.score > 99999u ? 99999u : g.score, 5, 107);
|
|
for (uint8_t x = 0; x < W; x += 2u) pixel(x, 8);
|
|
if (g.boss) {
|
|
A->draw_rect((app_fb_t)A->status_line, 49, 1, 90, 5, true);
|
|
for (uint8_t x = 0; x < 38u; x++)
|
|
if ((uint16_t)x * g.bossMax < 38u * g.bossHp) pixel(51 + x, 3);
|
|
}
|
|
}
|
|
|
|
/* Three-layer starfield computed from the frame counter (no RAM): 8 far
|
|
* stars at 1/4 px per tick, 4 at 1/2 and 4 near streaks at 1 px. Each star
|
|
* gets a new pseudo-random height every time it wraps around. */
|
|
static void draw_stars(void)
|
|
{
|
|
for (uint8_t i = 0; i < 16u; i++) {
|
|
const uint8_t sh = (uint8_t)((0x1Au >> ((i & 3u) * 2u)) & 3u); /* 2,2,1,0 */
|
|
const uint16_t pos = (uint16_t)(i * 53u + (g.frame >> sh));
|
|
const uint8_t h = (uint8_t)((pos >> 7) * 29u + i * 71u);
|
|
const int x = (int)(127u - (pos & 127u));
|
|
const int y = (int)(TOP + 2 + ((h * 40u) >> 8));
|
|
pixel(x, y);
|
|
if (!sh) pixel(x + 1, y);
|
|
}
|
|
}
|
|
|
|
/* Hilly ground: the ridge is the sum of two triangle waves whose periods
|
|
* divide 256, so it wraps seamlessly with the low byte of frame. Their
|
|
* steps fall on different columns, so a 1-pixel line stays continuous. */
|
|
static void draw_ground(void)
|
|
{
|
|
for (uint8_t x = 0; x < W; x++) {
|
|
const uint8_t wx = (uint8_t)(x + g.frame);
|
|
const uint8_t a = wx & 127u, b = (wx + 2u) & 31u; /* offset: steps never coincide */
|
|
const uint8_t y = (uint8_t)(62u - ((a < 64u ? a : 127u - a) >> 4)
|
|
- ((b < 16u ? b : 31u - b) >> 2));
|
|
const uint8_t h = (uint8_t)(wx * 37u); /* cheap per-column hash */
|
|
pixel(x, y);
|
|
if (wx & 1u) pixel(x, y + 3); /* dotted stratum */
|
|
if (h < 40u) pixel(x, y + 5 + (h & 1u)); /* scattered pebbles */
|
|
}
|
|
}
|
|
|
|
static void draw(void)
|
|
{
|
|
A->display_clear(); A->status_clear();
|
|
draw_stars();
|
|
draw_ground();
|
|
|
|
for (uint8_t i = 0; i < N_ENEMY; i++) {
|
|
const enemy_t *e = &g.enemies[i];
|
|
if (e->state == 1u && !(e->type == 2u && e->hp < 3u && (g.frame & 2u))) /* hurt saucer blinks */
|
|
blit(e->x, e->y, SPR_ENEMY + e->type * 8u, 8);
|
|
else if (e->state > 1u && e->state <= BOOM)
|
|
blit(e->x, e->y, SPR_BOOM + (e->state > 5u ? 0u : 8u), 8);
|
|
}
|
|
if (g.boss) {
|
|
const uint16_t spr = SPR_BOSS + ((g.level & 1u) ? 0u : 32u); /* alternate bosses */
|
|
blit(g.bossX, g.bossY, spr, 16);
|
|
blit(g.bossX, g.bossY + 8, spr + 16u, 16);
|
|
}
|
|
if (g.mode == ST_PLAY && !(g.cd[CD_INVUL] & 2u)) blit(PX, g.py, SPR_SHIP, 10);
|
|
|
|
for (uint8_t i = 0; i < N_SHOT; i++) {
|
|
const shot_t *s = &g.shots[i];
|
|
if (s->live == 2u) blit(s->x, s->y - 2, SPR_MISS, 8);
|
|
else if (s->live) for (uint8_t k = 0; k < 4u; k++) pixel(s->x + k, s->y);
|
|
}
|
|
for (uint8_t i = 0; i < N_HOSTILE; i++) {
|
|
const shot_t *s = &g.hostile[i];
|
|
if (!s->live) continue;
|
|
pixel(s->x, s->y - 1); pixel(s->x, s->y + 1);
|
|
pixel(s->x - 1, s->y); pixel(s->x, s->y); pixel(s->x + 1, s->y);
|
|
}
|
|
|
|
draw_hud();
|
|
if (g.mode == ST_OVER)
|
|
say(T_GAME_OVER, 3u, 0);
|
|
else if (g.paused)
|
|
say(T_PAUSE, 3u, 0);
|
|
else if (g.cd[CD_BANNER] > 10u)
|
|
say(T_LEVEL, 3u, (char)('0' + g.level)); /* "LEVEL n" */
|
|
}
|
|
|
|
/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers
|
|
* every frame, the starfield scrolls over it and the prompt blinks. */
|
|
static void draw_title(void)
|
|
{
|
|
g.frame++;
|
|
A->asset_read(ART_TITLE, A->status_line, W);
|
|
A->asset_read(ART_TITLE + W, A->fb[0], 7u * W);
|
|
draw_stars();
|
|
if (g.frame & 16u)
|
|
say(T_PRESS, 6u, 0);
|
|
}
|
|
|
|
static void poll_key(void)
|
|
{
|
|
uint8_t key = A->get_key();
|
|
if (key == APP_KEY_SAVER) { g.saver = true; return; } /* freeze behind the saver */
|
|
if (key == APP_KEY_WAKE || key == APP_KEY_PTT) { g.saver = false; key = APP_KEY_INVALID; }
|
|
const bool press = key != g.prevKey;
|
|
const bool action = key == APP_KEY_5 || key == APP_KEY_MENU;
|
|
g.prevKey = key;
|
|
if (key == APP_KEY_INVALID) return;
|
|
if (press && key == APP_KEY_EXIT) { g.running = false; return; }
|
|
|
|
if (g.mode != ST_PLAY) {
|
|
if (press && key == APP_KEY_MENU && !g.cd[CD_WAIT]) new_game();
|
|
return;
|
|
}
|
|
if (press && key == APP_KEY_F) { g.paused = !g.paused; return; }
|
|
if (g.paused) return;
|
|
if (action) {
|
|
if (press && g.missiles && fire(PX + 4, 2u))
|
|
g.missiles--;
|
|
} else { /* 2/8, UP/DOWN; 0 for any other key */
|
|
const int dy = key == APP_KEY_2 ? -2 : key == APP_KEY_8 ? 2 : -2 * A->nav_dir(key);
|
|
int ny = g.py + dy;
|
|
if (ny < TOP) ny = TOP;
|
|
if (ny > Y_MAX) ny = Y_MAX;
|
|
g.py = (uint8_t)ny;
|
|
}
|
|
}
|
|
|
|
__attribute__((section(".text.entry"), used))
|
|
void app_main(const app_api_t *api)
|
|
{
|
|
A = api;
|
|
g.running = true; /* the rest of g starts zeroed (.bss) */
|
|
g.prevKey = A->get_key();
|
|
A->led(false); A->backlight_on();
|
|
g.mode = ST_TITLE; /* MENU starts a game, like after GAME OVER */
|
|
|
|
while (g.running) {
|
|
poll_key();
|
|
if (!g.running) break;
|
|
if (!g.saver) { /* the saver freezes the game */
|
|
if (g.mode == ST_TITLE) draw_title();
|
|
else { if (!g.paused) tick(); draw(); }
|
|
A->blit_status(); A->blit_full();
|
|
}
|
|
A->backlight_update();
|
|
A->delay_ms(TICK_MS);
|
|
}
|
|
|
|
A->led(false);
|
|
}
|