← 都来码 / Dolaima← 例程索引c/app/starfall.c原文

c/app/starfall.c · 43775 字节 · 下载原文

/* starfall.c —— 《星陨》竖版太空射击(C 版),跑在手机端 VML 上
 * starfall.c — "Starfall", a vertical space shooter (C version), running on VML on mobile.
 *
 * 编译运行(手机 App 的 vml 工具):
 * Build and run (with the vml tool in the mobile app):
 *     vml run examples/c/starfall.c
 *
 * ## 操作:**单指拖动**,不用屏幕手柄
 * ## Controls: **single-finger drag**, no on-screen gamepad
 *
 * 手指按住屏幕任意处,飞船就朝那儿飞(有阻尼跟随,不是硬贴)。自动开火,不用按开火键。
 * Hold a finger anywhere on the screen and the ship flies toward it (damped follow, not glued).
 * Firing is automatic; there is no fire button.
 *
 * ⚠ **这不是随便选的**:屏幕手柄在宿主侧只有一个按键槽(`DrawWindowPage._padDownKey`),
 * ⚠ **This is not an arbitrary choice**: the on-screen gamepad has only one key slot on the host side
 *   (`DrawWindowPage._padDownKey`), and pressing a new key
 * 按新键会先替旧键补一条 KeyUp ⇒ **两个键同时按做不到**。写 mario.c 时就栽在这上面
 * first synthesizes a KeyUp for the old one ⇒ **holding two keys at once is impossible**.
 * mario.c already tripped over exactly this
 * ("跑动中起跳"做不出来,跳不过悬崖)。射击游戏天生要"边移动边开火",
 * ("jumping while running" could not be done, so the cliff was unjumpable). A shooter inherently needs
 * "move while firing",
 * 所以这里**彻底不用手柄、走触摸**,从根上避开那个限制。
 * so this program **drops the gamepad entirely and goes through touch**, avoiding that limit at the root.
 *
 * 顺带:屏幕上已有的东西不要在程序里再画一遍 —— 手柄区既然用不上,也就不用占那 140px。
 * Also: do not draw a second copy of what the screen already has — since the pad is unused, it need not take that 140px.
 *
 * ## 视觉手段(都在既有绘图接口里,没有新接口)
 * ## Visual techniques (all from existing draw APIs, no new interfaces)
 *
 * - **渐变**:`ui_2d_gradient(id, radial, 色A, 色B, 归一化几何)` + `ui_2d_rect_grad` / `ui_2d_circle_grad`。
 * - **Gradient**: `ui_2d_gradient(id, radial, colorA, colorB, normalizedGeometry)` + `ui_2d_rect_grad` / `ui_2d_circle_grad`.
 *   几何是**千分之一(0..1000)**的归一化值,不是像素 —— 传像素会让渐变塌成纯色。
 *   The geometry is a **thousandth (0..1000)** normalized value, not pixels — passing pixels collapses the gradient into a solid color.
 * - **发光**:没有 blur 之类的接口,用"**同色多层递减透明度 + 递增半径**"叠出来。
 * - **Glow**: there is no blur API, so it is stacked from **multiple layers of the same color with decreasing alpha and increasing radius**.
 * - **星空视差**:三层星星、速度与亮度各不同,营造纵深。
 * - **Star parallax**: three star layers with different speeds and brightness, giving depth.
 * - **粒子**:爆炸是 N 个小圆,半径与透明度随时间衰减。
 * - **Particles**: an explosion is N small circles whose radius and alpha decay over time.
 * - **多边形**:飞船/敌机都是 `ui_2d_polygon`,顶点在 C 里算好(见下面"为什么不拼路径字符串")。
 * - **Polygons**: ship and foes all use `ui_2d_polygon`, with vertices computed in C (see "why not build path strings" below).
 *
 * ## 为什么不拼 SVG 路径字符串(`ui_2d_path`)
 * ## Why not build SVG path strings (`ui_2d_path`)
 *
 * `ui_2d_path` 收的是**字符串**(`"M12 40 L..."`)。飞船每帧都在动,用它就得**每帧拼字符串**
 * `ui_2d_path` takes a **string** (`"M12 40 L..."`). The ship moves every frame, so using it would mean
 * **building a string every frame**
 * —— 而 C 里拼字符串要么 `sprintf`(~~本环境格式化路径有已知问题~~ **2026-10-04 复验已正常**),要么手写数字转字符串
 * — and string building in C is either `sprintf` (~~the format path has known problems in this environment~~ **re-checked fine 2026-10-04**)
 * or hand-writing number-to-string conversion
 * 再拼接,既慢又容易出界。多边形直接收 int 数组、顶点在 C 里算,零字符串操作。
 * followed by concatenation, which is slow and easy to overflow. A polygon takes the int array directly,
 * with vertices computed in C: zero string operations.
 * (静态不动的装饰图形才值得用 `ui_2d_path`。)
 * (Only static, motionless decoration is worth using `ui_2d_path` for.)
 *
 * ## 三条 C 前端硬约束(同 tetris.c / mario.c)
 * ## Three hard constraints of the C frontend (same as tetris.c / mario.c)
 *
 * 1. ⚠ `${}` 只认局部变量(**2026-10-04 复验仍成立**:全局量一个装载指令都不生成、参数静默丢)
 * 1. ⚠ `${}` recognizes local variables only (**still holds, re-checked 2026-10-04**: a global emits no load at all and the argument is silently dropped)
 * 2. 全局变量与全局 int 数组读写都正常。
 * 2. Global variables and global int arrays read and write normally.
 * 3. ⚠ `#define` 不支持反斜杠续行(~~此条已修好~~);⚠ **`ui_2d_polygon((int[]){…})` 复合字面量不支持**
 * 3. ⚠ `#define` does not support backslash line continuation (~~this one is now fixed~~); ⚠ **the `ui_2d_polygon((int[]){…})`
 *    compound literal is unsupported** (~~and fails silently~~)
 *    —— 顶点数组必须是**具名全局数组**。
 *    — the vertex array must be a **named global array**.
 *    ✅ **已修好(2026-10-04 复验)**:`#define` 续行可用(宏 = **5**、多行宏 = **42**、字符串续行 = **hello**);
 *    ✅ **fixed (re-checked 2026-10-04)**: `#define` continuation works (macro = **5**, multi-line macro = **42**, continued string literal = **hello**);
 *    复合字面量**仍是硬失败**(v0.96.183 起直接报语法错误,不再静默)。
 *    the compound literal is **still a hard failure** (since v0.96.183 it is a syntax error, no longer silent).
 */

#include <vml_ui.h>
#include <stdlib.h>

/* ── 规模上限 ───────────────────────────────────────────── */
/* ── Size limits ─────────────────────────────────────────── */

#define MAXB  20        /* 我方子弹 */
                        /* Our bullets. */
#define MAXE  10        /* 敌机 */
                        /* Enemy craft. */
#define MAXP  70        /* 粒子 */
                        /* Particles. */
#define MAXS  54        /* 星星 */
                        /* Stars. */
#define TICK  33        /* 一拍 33ms ⇒ 约 30 拍/秒(逻辑节拍,与渲染帧率无关) */
                        /* One tick is 33ms => about 30 ticks/s (a logic beat, not the frame rate). */
#define STARS 3         /* 星空层数 */
                        /* Number of star layers. */

/* ── 配色(0xAARRGGBB,alpha 在前)────────────────────────── */
/* ── Palette (0xAARRGGBB, alpha first) ────────────────────── */

#define C_SPACE_TOP  0xFF070B1C
#define C_SPACE_BOT  0xFF1A2A5C
#define C_NEB_A      0xFF7A3FB0
#define C_NEB_B      0xFF2FA8C8
#define C_STAR       0xFFFFFFFF
#define C_STAR_DIM   0x66FFFFFF
#define C_STAR_MID   0xAAFFFFFF
#define C_HULL       0xFFE8F6FF
#define C_HULL_EDGE  0xFF5CE1FF
#define C_ENGINE     0xFFFF8A2B
#define C_ENGINE_HOT 0xFFFFE9A8
#define C_BULLET     0xFF5CE1FF
#define C_BULLET_HOT 0xFFEAFDFF
#define C_FOE        0xFFFF3D6E
#define C_FOE_EDGE   0xFFFFB199
#define C_FOE_ALT    0xFFFFC24B
#define C_GOLD       0xFFFFD34D
#define C_HUD_BG     0xCC060A18
#define C_TEXT       0xFFDCE8FF
#define C_DIM        0xFF8FA3C8
#define C_WARN       0xFFFF6B6B

/* 分层透明度(预置常量,避免运行时算 alpha 时溢出 int)*/
/* Layer alphas (preset constants, avoiding int overflow when computing alpha at runtime). */
#define A90  0xE6000000
#define A60  0x99000000
#define A40  0x66000000
#define A25  0x40000000
#define A12  0x1F000000

/* ── 正弦表(×1000,32 点一整圈)─────────────────────────────
 * ── Sine table (x1000, 32 points per full turn) ────────────
 * 不用 math.h:本仓对前端浮点语义踩过坑,整数表确定、可比对。
 * No math.h: this repo has been burned by frontend float semantics; an integer table is deterministic and comparable.
 */
int SIN[32] = {
        0,  195,  383,  556,  707,  831,  924,  981,
     1000,  981,  924,  831,  707,  556,  383,  195,
        0, -195, -383, -556, -707, -831, -924, -981,
    -1000, -981, -924, -831, -707, -556, -383, -195
};

int sin_i(int a) { return SIN[a & 31]; }
int cos_i(int a) { return SIN[(a + 8) & 31]; }   /* cos = sin 移 1/4 圈(8/32)*/
                                                 /* cos = sin shifted a quarter turn (8/32). */

/* ── 状态 ───────────────────────────────────────────────── */
/* ── State ──────────────────────────────────────────────── */

int W, H;                       /* 画布宽高 */
                                /* Canvas width and height. */
int g_lang;                     /* 界面语言:开局查一次(ui_scr_get_language 是 syscall,别每帧调) */
                                /* UI language: queried once at start (ui_scr_get_language is a syscall, don't call it each frame). */
int frame;
int acc;                        /* 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径 */
                                /* Motion wall-clock debt in ms -- steps are caught up from the wall clock (user's 2026-10-04 rule) */
int lastMs;                     /* 上次补步取的墙钟时刻(进主循环时初始化) */
                                /* wall-clock time of the last catch-up step (initialized when entering the main loop) */
int state;                      /* 0=玩 1=暂停 2=结束 */
                                /* 0 = playing, 1 = paused, 2 = over. */
int score, best, wave, lives, combo, comboTimer;
int shake;                      /* 震屏剩余帧数 */
                                /* Remaining screen-shake frames. */
int shx, shy;                   /* 本帧震屏偏移(只在绘制时加,不动游戏状态)*/
                                /* Screen-shake offset for this frame (added while drawing only; game state is untouched). */

int px, py;                     /* 飞船中心 */
                                /* Ship center. */
int tx, ty;                     /* 手指目标点 */
                                /* Finger target point. */
int touching;
int invuln;                     /* 无敌帧(复活后短暂) */
                                /* Invulnerability frames (brief, after respawning). */
int fireCd;

int bx[MAXB], by[MAXB], balive[MAXB];
int ex_[MAXE], ey_[MAXE], eh_[MAXE], erot[MAXE], ekind[MAXE], ealive[MAXE], ecd[MAXE];
int ppx[MAXP], ppy[MAXP], pvx[MAXP], pvy[MAXP], plife[MAXP], pcol[MAXP], palive[MAXP];
int stx[MAXS], sty[MAXS], ssp[MAXS], slay[MAXS], ssz[MAXS];   /* +t=star,避开与局部 sx/sy 同名遮蔽 */
                                                              /* +t=star, to avoid shadowing by the local sx/sy. */

int shipPts[16];                /* 飞船多边形顶点(x,y 交替)—— 具名数组,不能用复合字面量 */
                                /* Ship polygon vertices (x,y alternating) — a named array; compound literals cannot be used. */
int foePts[16];                 /* 敌机多边形顶点 */
                                /* Foe polygon vertices. */
/* 飞船机身的**相对**顶点(以中心为原点,y 向上为负)。倾斜时按角度旋转后再加中心点 ——
 * **Relative** hull vertices (origin at the center, negative y points up). When tilting they are rotated by angle, then the center is added —
 * 写成相对量是因为旋转必须绕中心做,绝对坐标转起来会绕到屏幕原点去。
 * They are written relative because rotation must happen about the center; absolute coordinates would swing around the screen origin.
 */
int shipRel[14] = { 0,-18,  11,2,  4,0,  7,13,  -7,13,  -4,0,  -11,2 };

char nbuf[16];                  /* 数字转字符串暂存 */
                                /* Scratch buffer for number-to-string conversion. */

/* ── 小工具 ─────────────────────────────────────────────── */
/* ── Small helpers ──────────────────────────────────────── */

char* num_str(int v) {
    int i;
    int n;
    if (v < 0) { nbuf[0] = '-'; nbuf[1] = '0'; nbuf[2] = 0; return nbuf; }
    i = 0;
    if (v == 0) { nbuf[0] = '0'; nbuf[1] = 0; return nbuf; }
    while (v > 0) {
        nbuf[i] = '0' + (v % 10);
        i = i + 1;
        v = v / 10;
    }
    nbuf[i] = 0;
    n = 0;
    while (n < i / 2) {
        char t;
        t = nbuf[n];
        nbuf[n] = nbuf[i - 1 - n];
        nbuf[i - 1 - n] = t;
        n = n + 1;
    }
    return nbuf;
}

/* 发光:同色多层递减透明度 + 递增半径(没有 blur 接口,只能这么叠)*/
/* Glow: several layers of the same color with decreasing alpha and increasing radius (there is no blur API, so this is the only way). */
void glow(int cx, int cy, int r, int color) {
    ui_2d_circle(cx, cy, r, (color & 0x00FFFFFF) | A12, 1, 0);
    ui_2d_circle(cx, cy, (r * 2) / 3, (color & 0x00FFFFFF) | A25, 1, 0);
    ui_2d_circle(cx, cy, r / 3, (color & 0x00FFFFFF) | A60, 1, 0);
}

int rnd(int n) { if (n <= 0) return 0; return ui_math_rand(n); }

/* ── 生成 / 复位 ────────────────────────────────────────── */
/* ── Spawn / reset ──────────────────────────────────────── */

void spawn_particles(int x, int y, int count, int color, int power) {
    int i;
    int k;
    i = 0;
    while (i < count) {
        k = 0;
        while (k < MAXP) {
            if (palive[k] == 0) {
                palive[k] = 1;
                ppx[k] = x;
                ppy[k] = y;
                pvx[k] = rnd(2 * power + 1) - power;
                pvy[k] = rnd(2 * power + 1) - power;
                plife[k] = 14 + rnd(14);
                pcol[k] = color;
                break;
            }
            k = k + 1;
        }
        i = i + 1;
    }
}

void spawn_foe(void) {
    int i;
    i = 0;
    while (i < MAXE) {
        if (ealive[i] == 0) {
            ealive[i] = 1;
            ex_[i] = 30 + rnd(W - 60);
            ey_[i] = -40;
            eh_[i] = 18 + rnd(10);
            erot[i] = rnd(32);
            ekind[i] = rnd(3);              /* 0/1 直线 2 蛇形 */
                                            /* 0/1 = straight line, 2 = snaking. */
            ecd[i] = 0;
            return;
        }
        i = i + 1;
    }
}

void reset_game(void) {
    int i;
    score = 0;
    wave = 1;
    lives = 3;
    combo = 0;
    comboTimer = 0;
    shake = 0;
    state = 0;
    frame = 0;
    px = W / 2;
    py = H - 120;
    tx = px;
    ty = py;
    touching = 0;
    invuln = 90;
    fireCd = 0;
    i = 0;
    while (i < MAXB) { balive[i] = 0; i = i + 1; }
    i = 0;
    while (i < MAXE) { ealive[i] = 0; i = i + 1; }
    i = 0;
    while (i < MAXP) { palive[i] = 0; i = i + 1; }
    i = 0;
    while (i < MAXS) {
        slay[i] = i % STARS;
        stx[i] = rnd(W);
        sty[i] = rnd(H);
        ssp[i] = 1 + slay[i] * 2;           /* 越远的层越慢 */
                                            /* The more distant the layer, the slower it drifts. */
        ssz[i] = 1 + slay[i];
        i = i + 1;
    }
}

/* ── 音效:**用 ui_audio_beep 单音**(v0.96.509 统一换回来) ────────────
 * ── Sound: **single-tone ui_audio_beep** (switched back uniformly in v0.96.509) ──
 *
 * ⚠⚠ 这些音一度走共享库的音序器(`ui_sfx_add`),**真机上破音**,全部换回来了。
 * ⚠⚠ These sounds once went through the shared-library sequencer (`ui_sfx_add`) and
 * **clipped on real devices**, so they were all switched back.
 *   破音的是**这里配的音**:爆炸是**三个声部同时响**(48/54/48 一起轰),多声部混音
 *   What clipped was **the sounds configured here**: the explosion fired **three voices at once**
 *   (48/54/48 together), and mixing multiple voices
 *   一叠加就顶到削波;被撞的末音还拖了 12 拍(一拍 33ms ≈ 400ms)。
 *   stacked straight into clipping; the tail of the hit sound also dragged 12 ticks (33ms per tick).
 *   `ui_audio_beep` 是**单通道**的(后一个音掐掉前一个)⇒ 一个事件永远只有一个音在响,
 *   `ui_audio_beep` is **single-channel** (a new tone cuts off the previous one) ⇒ only one tone sounds per event,
 *   **结构上不可能削波、也不会长音叠加**。代价是没有音色。
 *   so **clipping and long-tone stacking are structurally impossible**. The price is no timbre.
 *
 * ⚠ 频率沿用原来那版的**首音**(射击最高最轻、爆炸最低),时长取整块时长、封顶 320ms。
 * ⚠ Frequencies keep the **first tone** of the previous version (shoot highest and lightest, boom lowest);
 * the duration is the whole block's length, capped at 320ms.
 *   低音不低于 C3(131Hz) —— 手机外放在 200Hz 以下衰减很快,玩家听着像没响。
 *   Bass never drops below C3 (131Hz) — phone speakers roll off fast under 200Hz, so players hear nothing.
 */

void sfx_shoot(void) { ui_audio_beep(1568, 33); }      /* 射击:最高频的动作,最轻最短 */
                                                 /* Shoot: the highest-pitched action, lightest and shortest. */
void sfx_boom(void) { ui_audio_beep(131, 198); }       /* 爆炸:一声闷响(最低) */
                                                 /* Boom: a dull thud (the lowest). */
void sfx_hit(void) {                             /* 被撞:低沉、长 */
                                                 /* Hit: low and long. */
    ui_audio_beep(196, 320);
    ui_vibrate_simple(140, 0);
}
void sfx_wave(void) { ui_audio_beep(659, 320); }       /* 过一波:中高、长 */
                                                 /* Wave cleared: mid-high and long. */

/* ── 每拍 ───────────────────────────────────────────────── */
/* ── Per tick ───────────────────────────────────────────── */

void fire(void) {
    int i;
    i = 0;
    while (i < MAXB) {
        if (balive[i] == 0) {
            balive[i] = 1;
            bx[i] = px;
            by[i] = py - 18;
            sfx_shoot();
            return;
        }
        i = i + 1;
    }
}

void hurt_player(void) {
    if (invuln > 0) return;
    lives = lives - 1;
    combo = 0;
    shake = 12;
    sfx_hit();
    spawn_particles(px, py, 26, C_FOE, 4);
    if (lives <= 0) { state = 2; return; }
    invuln = 90;
    px = W / 2;
    py = H - 120;
    tx = px;
    ty = py;
}

void tick(void) {
    int i;
    int k;
    int dx;
    int dy;
    int hitdx;
    int hity;

    if (state != 0) return;
    frame = frame + 1;
    if (shake > 0) shake = shake - 1;
    if (invuln > 0) invuln = invuln - 1;
    if (comboTimer > 0) { comboTimer = comboTimer - 1; if (comboTimer == 0) combo = 0; }

    /* ── 飞船跟随手指(阻尼,不是硬贴)── */
    /* ── Ship follows the finger (damped, not glued) ── */
    dx = tx - px;
    dy = ty - py;
    px = px + dx / 5;
    py = py + dy / 5;
    if (px < 16) px = 16;
    if (px > W - 16) px = W - 16;
    if (py < 50) py = 50;
    if (py > H - 30) py = H - 30;

    /* ── 自动开火 ── */
    /* ── Auto fire ── */
    fireCd = fireCd - 1;
    if (fireCd <= 0) { fire(); fireCd = 7; }

    /* ── 子弹 ── */
    /* ── Bullets ── */
    i = 0;
    while (i < MAXB) {
        if (balive[i] != 0) {
            by[i] = by[i] - 13;
            if (by[i] < -20) balive[i] = 0;
        }
        i = i + 1;
    }

    /* ── 敌机 ── */
    /* ── Foes ── */
    i = 0;
    while (i < MAXE) {
        if (ealive[i] != 0) {
            ey_[i] = ey_[i] + 2 + wave / 3;
            if (ekind[i] == 2) ex_[i] = ex_[i] + sin_i(frame / 3 + i * 7) / 3;
            erot[i] = erot[i] + 1;
            if (ex_[i] < 20) ex_[i] = 20;
            if (ex_[i] > W - 20) ex_[i] = W - 20;
            /* 漏过去:扣连击 */
            /* Slipped past: the combo is lost. */
            if (ey_[i] > H + 30) { ealive[i] = 0; combo = 0; }

            /* 与我方子弹 */
            /* Against our bullets. */
            k = 0;
            while (k < MAXB) {
                if (balive[k] != 0) {
                    hitdx = bx[k] - ex_[i];
                    hity = by[k] - ey_[i];
                    if (hitdx < 0) hitdx = -hitdx;
                    if (hity < 0) hity = -hity;
                    if (hitdx < eh_[i] + 6 && hity < eh_[i] + 6) {
                        balive[k] = 0;
                        ealive[i] = 0;
                        combo = combo + 1;
                        comboTimer = 90;
                        score = score + 10 + combo * 2;
                        shake = 4;
                        sfx_boom();
                        spawn_particles(ex_[i], ey_[i], 14,
                                        ekind[i] == 2 ? C_FOE_ALT : C_FOE, 3);
                        break;
                    }
                }
                k = k + 1;
            }

            /* 与飞船 */
            /* Against the ship. */
            if (ealive[i] != 0) {
                hitdx = px - ex_[i];
                hity = py - ey_[i];
                if (hitdx < 0) hitdx = -hitdx;
                if (hity < 0) hity = -hity;
                if (hitdx < eh_[i] + 10 && hity < eh_[i] + 10) {
                    ealive[i] = 0;
                    spawn_particles(ex_[i], ey_[i], 12, C_FOE, 3);
                    hurt_player();
                    if (state != 0) return;
                }
            }
        }
        i = i + 1;
    }

    /* ── 粒子 ── */
    /* ── Particles ── */
    i = 0;
    while (i < MAXP) {
        if (palive[i] != 0) {
            ppx[i] = ppx[i] + pvx[i];
            ppy[i] = ppy[i] + pvy[i];
            pvx[i] = (pvx[i] * 9) / 10;
            pvy[i] = (pvy[i] * 9) / 10;
            plife[i] = plife[i] - 1;
            if (plife[i] <= 0) palive[i] = 0;
        }
        i = i + 1;
    }

    /* ── 星空 ── */
    /* ── Starfield ── */
    i = 0;
    while (i < MAXS) {
        sty[i] = sty[i] + ssp[i];
        if (sty[i] > H) { sty[i] = -2; stx[i] = rnd(W); }
        i = i + 1;
    }

    /* ── 波次 ── */
    /* ── Waves ── */
    /* 出敌节奏:55 拍(≈1.8 秒)一个;第 2 波起再补一个错开的 ——
     * Spawn cadence: one foe every 55 ticks (about 1.8 seconds); from wave 2 on a second, staggered one is added —
     * 原先是 90 拍一个,实测 16 秒才 5 个,画面太冷清。
     * it used to be one every 90 ticks, which measured only 5 in 16 seconds — too empty a screen.
     */
    if (frame % 55 == 0) {
        spawn_foe();
        if (wave >= 2 && (frame % 110) == 55) spawn_foe();
        if (frame % 450 == 0) { wave = wave + 1; sfx_wave(); }
    }
}

/* ── 绘制 ───────────────────────────────────────────────── */
/* ── Drawing ────────────────────────────────────────────── */

void draw_space(void) {
    /* ⚠⚠ **`ui_2d_clear` 不是"可选的美化",它是"重置这一帧"的唯一入口。**
     * ⚠⚠ **`ui_2d_clear` is not optional decoration; it is the only way to "reset this frame".**
     *
     * 宿主那边的语义是:所有绘制(含 `ui_2d_gradient` 这条 DSL 指令)都往同一个图元表里
     * On the host side the semantics are: every draw call (including the `ui_2d_gradient` DSL instruction)
     * *appends* to one shared figure list,
     * **追加**,只有 `ui_2d_clear` 会把它清空(`VmlScene.Clear` → `_figures.Clear()`)。
     * and only `ui_2d_clear` empties it (`VmlScene.Clear` → `_figures.Clear()`).
     * 漏了它 ⇒ **每帧都叠在上一帧上面**:画面糊成一团("看不清"),
     * Miss it ⇒ **each frame is painted on top of the previous one**: the picture smears into a blur
     * ("can't see anything"),
     * 且图元表无限增长(一秒 170 个 → 十秒五万个)直到撑爆("闪退")。
     * and the figure list grows without bound (170 per second → fifty thousand in ten seconds) until it blows up ("crash").
     *
     * 本文件第一版就是这么错的:把"清屏"换成了"画渐变背景",于是漏掉了清屏本身。
     * The first version of this file made exactly that mistake: it replaced "clear the screen" with
     * "draw a gradient background" and so lost the clear itself.
     * 渐变铺满全屏**看起来**像清了底,但那是"盖上去"不是"重置"。
     * A full-screen gradient **looks** like it cleared the background, but that is "covering", not "resetting".
     */
    ui_2d_clear(C_SPACE_TOP);

    /* 背景:纵向渐变(几何是千分之一的归一化值:从上到下)*/
    /* Background: a vertical gradient (geometry is thousandth-normalized: top to bottom). */
    ui_2d_gradient("bg", 0, C_SPACE_TOP, C_SPACE_BOT, 0, 0, 0, 1000);
    ui_2d_rect_grad(0, 0, W, H, "bg", 0);

    /* 星云:两团径向渐变(随帧极缓慢"呼吸",让静止画面也有生气)。
     * Nebulae: two radial gradients (breathing very slowly with the frame count, so a still picture still feels alive).
     *
     * ⚠⚠ **渐变几何是「千分之一」的归一化值(0..1000),不是像素!**
     * ⚠⚠ **Gradient geometry is a "thousandth" normalized value (0..1000), not pixels!**
     * 第一版按像素传了 `W/3, H/4, W/2`(如 90,114,180)—— 宿主把 180 读成
     * The first version passed pixels: `W/3, H/4, W/2` (e.g. 90,114,180) — the host read 180 as
     * "半径 = 包围盒的 18%",于是焦点缩在左上角一小块、**其余全是 ColorB**,
     * "radius = 18% of the bounding box", so the focus shrank into a small patch in the top-left corner
     * and **everything else was ColorB**,
     * 屏幕上就是**一个硬边深色圆盘**(用户实测「看不清是什么」)。
     * leaving **a hard-edged dark disc** on screen (the user measured this and said "can't tell what it is").
     * 径向居中只有一种正确写法:**500,500,500**(位置由 ui_2d_circle_grad 的 cx,cy 给)。
     * There is only one correct way to center a radial: **500,500,500** (the position comes from cx,cy of ui_2d_circle_grad).
     */
    ui_2d_gradient("neb1", 1, (C_NEB_A & 0x00FFFFFF) | A60, (C_SPACE_TOP & 0x00FFFFFF) | A90,
                500, 500, 500, 0);
    ui_2d_circle_grad(W / 3, H / 4 + sin_i(frame / 16) * 8, W / 2, "neb1");
    ui_2d_gradient("neb2", 1, (C_NEB_B & 0x00FFFFFF) | A40, (C_SPACE_TOP & 0x00FFFFFF) | A90,
                500, 500, 500, 0);
    ui_2d_circle_grad((W * 3) / 4, (H * 2) / 3 + cos_i(frame / 20) * 10, W / 2, "neb2");
}

void draw_stars(void) {
    int i;
    int b;
    i = 0;
    while (i < MAXS) {
        /* 越近的层越亮越大;最远那层还会轻微闪烁 */
        /* The nearer the layer, the brighter and bigger; the farthest layer also twinkles faintly. */
        if (slay[i] == 0) b = C_STAR_DIM;
        else if (slay[i] == 1) b = C_STAR_MID;
        else b = C_STAR;
        if (slay[i] == 0 && ((frame / 8 + i) & 1) == 0) b = (C_STAR & 0x00FFFFFF) | A25;
        ui_2d_circle(stx[i], sty[i], ssz[i], b, 1, 0);
        i = i + 1;
    }
}

void draw_ship(void) {
    int cx;
    int cy;
    int i;
    int tilt;
    int c;
    int s;
    int rx;
    int ry;
    /* 无敌时闪烁:不画(不是画成半透明 —— 圆接口没有统一的 alpha 混色保证)*/
    /* Blink while invulnerable: skip drawing entirely (rather than drawing semi-transparent — the circle API gives no uniform alpha blending guarantee). */
    if (invuln > 0 && ((invuln / 4) & 1) == 1) return;
    cx = px + shx;      /* 震屏只在**画**的时候加偏移 */
                        /* The screen-shake offset is added only while **drawing**. */
    cy = py + shy;

    /* 机身随横向速度倾斜 —— 一点"juice",让移动有重量感。
     * The hull tilts with horizontal velocity — a bit of "juice" so movement has weight.
     * 角度单位与正弦表同制(32 格一整圈,1 格 = 11.25°),限幅 ±2 格 ≈ ±22°。
     * The angle unit matches the sine table (32 steps per full turn, 1 step = 11.25°); clamped to ±2 steps, about ±22°.
     */
    tilt = tx - px;
    if (tilt > 5) tilt = 5;
    if (tilt < -5) tilt = -5;
    tilt = tilt;                      /* 1px 偏移 ≈ 1 格角度,够明显又不夸张 */
                                      /* A 1px offset is about 1 angle step: noticeable without being silly. */
    c = cos_i(tilt);
    s = sin_i(tilt);

    /* 引擎焰:跟着机身一起转,长度随帧抖动 */
    /* Engine flame: rotates with the hull, its length jittering with the frame count. */
    ry = 10;
    shipPts[0] = cx + ((-5) * c - ry * s) / 1000;
    shipPts[1] = cy + ((-5) * s + ry * c) / 1000;
    shipPts[2] = cx + (5 * c - ry * s) / 1000;
    shipPts[3] = cy + (5 * s + ry * c) / 1000;
    ry = 24 + sin_i(frame) / 60;
    shipPts[4] = cx + (0 * c - ry * s) / 1000;
    shipPts[5] = cy + (0 * s + ry * c) / 1000;
    glow(cx, cy + 16, 10, C_ENGINE);
    ui_2d_polygon(shipPts, 3, C_ENGINE_HOT, C_ENGINE, 1, "");

    /* 机身:相对顶点按 tilt 旋转后落到画布上 */
    /* Hull: the relative vertices are rotated by tilt and then placed on the canvas. */
    i = 0;
    while (i < 7) {
        rx = shipRel[i * 2];
        ry = shipRel[i * 2 + 1];
        shipPts[i * 2]     = cx + (rx * c - ry * s) / 1000;
        shipPts[i * 2 + 1] = cy + (rx * s + ry * c) / 1000;
        i = i + 1;
    }
    glow(cx, cy, 16, C_HULL_EDGE);
    ui_2d_polygon(shipPts, 7, C_HULL, C_HULL_EDGE, 2, "");

    /* 座舱 */
    /* Cockpit. */
    ui_2d_circle(cx + (0 * c - (-4) * s) / 1000, cy + (0 * s + (-4) * c) / 1000, 3, C_HULL_EDGE, 1, 0);
}

void draw_foes(void) {
    int i;
    int fx;
    int fy;
    int k;
    int a;
    int r;
    int col;
    i = 0;
    while (i < MAXE) {
        if (ealive[i] != 0) {
            r = eh_[i];
            fx = ex_[i] + shx;
            fy = ey_[i] + shy;
            /* 八边形:顶点由旋转角算出来(不拼字符串,见文件头那段)*/
            /* Octagon: vertices are computed from the rotation angle (no string building; see the header). */
            k = 0;
            while (k < 8) {
                a = erot[i] + k * 4;          /* 45° = 4/32 圈 */
                                              /* 45 degrees = 4/32 of a turn. */
                foePts[k * 2] = fx + (r * cos_i(a)) / 1000;
                foePts[k * 2 + 1] = fy + (r * sin_i(a)) / 1000;
                k = k + 1;
            }
            col = ekind[i] == 2 ? C_FOE_ALT : C_FOE;
            glow(fx, fy, r, col);
            ui_2d_polygon(foePts, 8, (col & 0x00FFFFFF) | A90, C_FOE_EDGE, 2, "");
            /* 核心 */
            /* Core. */
            ui_2d_circle(fx, fy, r / 3, (col & 0x00FFFFFF) | A90, 1, 0);
            ui_2d_circle(fx, fy, r / 5, C_HULL, 1, 0);
        }
        i = i + 1;
    }
}

void draw_bullets(void) {
    int i;
    i = 0;
    while (i < MAXB) {
        if (balive[i] != 0) {
            glow(bx[i] + shx, by[i] + shy, 7, C_BULLET);
            ui_2d_rect(bx[i] + shx - 2, by[i] + shy - 9, 4, 16, C_BULLET, 1, 0, 2);
            ui_2d_rect(bx[i] + shx - 1, by[i] + shy - 7, 2, 12, C_BULLET_HOT, 1, 0, 1);
        }
        i = i + 1;
    }
}

void draw_particles(void) {
    int i;
    int r;
    int a;
    i = 0;
    while (i < MAXP) {
        if (palive[i] != 0) {
            r = plife[i] / 3;
            if (r < 1) r = 1;
            if (plife[i] > 18) a = A90;
            else if (plife[i] > 10) a = A60;
            else if (plife[i] > 5) a = A40;
            else a = A25;
            ui_2d_circle(ppx[i] + shx, ppy[i] + shy, r, (pcol[i] & 0x00FFFFFF) | a, 1, 0);
        }
        i = i + 1;
    }
}

/* 命数用的小飞船图标(独立函数:几何与主飞船不同,别混用同一张顶点表)*/
/* The little ship icon used for lives (a separate function: its geometry differs from the main ship, so do not share one vertex array). */
void ui_polygon_small(int cx, int cy) {
    int a[12];
    a[0] = cx;      a[1] = cy - 6;
    a[2] = cx + 5;  a[3] = cy + 2;
    a[4] = cx + 2;  a[5] = cy + 1;
    a[6] = cx + 3;  a[7] = cy + 5;
    a[8] = cx - 3;  a[9] = cy + 5;
    a[10] = cx - 2; a[11] = cy + 1;
    ui_2d_polygon(a, 6, C_HULL, C_HULL_EDGE, 1, "");
}

void draw_hud(void) {
    int i;
    ui_2d_rect(0, 0, W, 34, C_HUD_BG, 1, 0, 0);
    ui_text_set_font(13, VML_FONT_BOLD, C_TEXT, VML_ANCHOR_LEFT);
    ui_2d_text_cur(10, 9, num_str(score));
    ui_text_set_font(10, 0, C_DIM, VML_ANCHOR_LEFT);
    ui_2d_text_cur(10, 23, g_lang == 0 ? "分数" : "Score");

    ui_text_set_font(12, VML_FONT_BOLD, C_GOLD, VML_ANCHOR_CENTER);
    /* ⚠ "第" 是 1 个全角(12px 宽,居中后占 ±6),而 "Level" 是 30px 宽(±15)。
     * ⚠ The Chinese label is a single full-width glyph (12px wide, so ±6 when centered), while "Level" is 30px wide (±15).
       数字的起点 W/2+22 对两者都还留着空隙,不必挪。 */
    /* The number starts at W/2+22, which still leaves a gap for both, so no adjustment is needed. */
    ui_2d_text_cur(W / 2, 9, g_lang == 0 ? "第" : "Level");
    ui_text_set_font(12, VML_FONT_BOLD, C_TEXT, VML_ANCHOR_CENTER);
    ui_2d_text_cur(W / 2 + 22, 9, num_str(wave));
    if (combo > 1) {
        ui_text_set_font(10, VML_FONT_BOLD, C_GOLD, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, 24, g_lang == 0 ? "连击" : "Combo");
    }

    /* 命:画成小飞船图标,比数字直观 */
    /* Lives: drawn as little ship icons, which reads better than a number. */
    i = 0;
    while (i < lives && i < 5) {
        ui_polygon_small(W - 22 - i * 20, 17);
        i = i + 1;
    }

    if (state == 1) {
        ui_2d_rect(0, H / 2 - 34, W, 68, 0xCC000000, 1, 0, 0);
        ui_text_set_font(22, VML_FONT_BOLD, C_TEXT, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 - 22, g_lang == 0 ? "暂停" : "Paused");
        ui_text_set_font(12, 0, C_DIM, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 + 8, g_lang == 0 ? "按 START 继续" : "Press START to resume");
    }
    if (state == 2) {
        ui_2d_rect(0, H / 2 - 60, W, 120, 0xCC000000, 1, 0, 0);
        ui_text_set_font(24, VML_FONT_BOLD, C_WARN, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 - 40, g_lang == 0 ? "舰体损毁" : "Hull destroyed");
        ui_text_set_font(14, 0, C_TEXT, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 - 4, g_lang == 0 ? "最终分数" : "Final score");
        ui_text_set_font(20, VML_FONT_BOLD, C_GOLD, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 + 22, num_str(score));
        ui_text_set_font(12, 0, C_DIM, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 + 50, g_lang == 0 ? "按 START 重新出击" : "Press START to fly again");
    }
}

void draw_all(void) {
    /* 震屏:只算一个偏移量,由各实体绘制函数在**画**的时候加上。
     * Screen shake: only an offset is computed here; each entity's draw function adds it while **drawing**.
     * ⚠ 不要在这里改 px/py 再改回来 —— 绘制期改游戏状态,一旦重入或中途返回,
     * ⚠ Do not modify px/py here and change them back — mutating game state during drawing means that
     * if the call re-enters or returns early,
     *   状态就留在偏移过的位置上(这是"看着偶然、其实必错"的一类写法)。
     * the state is left at the offset position (this is the "looks incidental, is actually always wrong" style of code).
     */
    shx = 0;
    shy = 0;
    if (shake > 0) { shx = sin_i(frame * 5) / 90; shy = cos_i(frame * 7) / 90; }

    draw_space();
    draw_stars();
    draw_bullets();
    draw_foes();
    draw_particles();
    draw_ship();
    draw_hud();
    ui_2d_present();
}

/* ── 输入 ───────────────────────────────────────────────── */
/* ── Input ──────────────────────────────────────────────── */

void on_key(int k) {
    if (k == VML_KEY_SELECT || k == VML_KEY_PAUSE) {
        if (state == 0) state = 1;
        else if (state == 1) state = 0;
        draw_all();
        return;
    }
    if (k == VML_KEY_ENTER) {
        reset_game();
        draw_all();
        return;
    }
}

/* ── 主循环 ─────────────────────────────────────────────── */
/* ── Main loop ──────────────────────────────────────────── */

int main(void) {
    int msg[4];
    int t;
    int sw;
    int sh;
    int i;

    g_lang = ui_scr_get_language();
    ui_win_open(g_lang == 0 ? "星陨" : "Starfall", 0, 0);
    sw = ui_scr_get_w();
    sh = ui_scr_get_h();
    if (sw <= 0) sw = 360;
    if (sh <= 0) sh = 620;
    ui_scr_set_keep_on(1);

    W = sw;
    H = sh;

    /* 全部初始化为"空",免得 reset 之前就画到未初始化的状态上 */
    /* Initialize everything to "empty", so nothing is drawn onto uninitialized state before reset. */
    i = 0;
    while (i < MAXB) { balive[i] = 0; i = i + 1; }
    i = 0;
    while (i < MAXE) { ealive[i] = 0; i = i + 1; }
    i = 0;
    while (i < MAXP) { palive[i] = 0; i = i + 1; }

    reset_game();
    draw_all();

    ui_timer_set(TICK, 0);

    /* ⚠ 中文这条**整句写在同一行**里:跨行的话,第二段的字面量会落在没有 `g_lang == 0`
     * ⚠ The Chinese message keeps **the whole sentence on one line**: split across lines, the second literal
     * would land on a line without `g_lang == 0`
       的那一行上(语言审计脚本按行认),而且相邻字面量拼接本来就只是 C 的语法糖。 */
    /* (the language audit script works line by line), and adjacent literal concatenation is just C syntactic sugar anyway. */
    if (g_lang == 0) ui_dlg_msg("星陨", "单指按住屏幕任意处拖动,飞船会跟过来;自动开火。打掉敌机攒连击,别被撞到。START 重开、SELECT 暂停。", VML_DLG_INFO);
    else             ui_dlg_msg("Starfall", "Drag anywhere to move; the ship fires by itself. Kill foes to build combo, don't get hit. START = restart, SELECT = pause.", VML_DLG_INFO);

    /* 墙钟起点:进主循环时取(不在模块级/全局初始化里调 syscall) */
    /* wall-clock origin: sampled when entering the main loop (no syscall at module/global-init level) */
    acc = 0;
    lastMs = ui_time_get_ms();

    while (ui_win_is_closed() == 0) {
        /* 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。
         * how many steps this frame takes: **driven by the wall-clock deficit** (timer resolution differs per platform => never treat "ticks received" as time).
         * 用户 2026-10-04 口径:所有游戏时钟取系统毫秒数。
         * user's 2026-10-04 rule: all game clocks read system milliseconds. */
        {
            int now2 = ui_time_get_ms();
            acc = acc + (now2 - lastMs);
            lastMs = now2;
            if (acc > TICK * 8) acc = 0;        /* 卡太久别一次补一大串(防瞬移) */
                                                /* do not catch up in one huge burst after a long stall (prevents teleporting) */
            if (state != 0) acc = 0;            /* 暂停/结束不累积 */
                                                /* no accumulation while paused/finished */
            if (acc >= TICK) {
                while (acc >= TICK) { acc = acc - TICK; tick(); }
                draw_all();
            }
        }

        t = ui_msg_wait(msg, 0);
        if (t == 0) continue;
        if (t == VML_MSG_WINDOWCLOSE) break;

        if (t == VML_MSG_TIMER) {
            /* 定时器**只负责唤醒**(推进在上面按墙钟欠账走) */
            /* the timer **only wakes us up** (advancement happens above, driven by the wall-clock deficit) */
            continue;
        }

        /* 触摸:按下/移动都更新目标点,抬起则停在原地 */
        /* Touch: both down and move update the target point; up leaves it where it is. */
        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
            touching = 1;
            tx = msg[1];
            ty = msg[2];
            continue;
        }
        if (t == VML_MSG_TOUCHMOVE || t == VML_MSG_MOUSEMOVE) {
            if (touching != 0) { tx = msg[1]; ty = msg[2]; }
            continue;
        }
        if (t == VML_MSG_TOUCHUP || t == VML_MSG_MOUSEUP) {
            touching = 0;
            continue;
        }

        if (t == VML_MSG_KEYDOWN) {
            if (msg[1] == VML_KEY_ESCAPE) break;
            on_key(msg[1]);
            continue;
        }
    }

    ui_scr_set_keep_on(0);
    ui_sfx_panic();
    ui_win_close();
    return 0;
}