← 都来码 / Dolaima← 例程索引cpp/app/1945.cpp原文

cpp/app/1945.cpp · 129842 字节 · 下载原文

// 1945 —— 竖版卷轴射击(用 C++ 写)

// 1945 — a vertical scrolling shooter (written in C++)

//

// 与同目录的 `gorilla.cpp` 同一条路数:**绘图与输入只走 `ui_*`**,

// The same approach as `gorilla.cpp` in this directory: **drawing and input go through `ui_*` only**,

// 不要任何变换接口(这个 VM 没有 `ui_push` / `ui_translate` / `ui_rotate` —— 要整体移动就把偏移算进坐标)。

// no transform interface at all (this VM has no `ui_push` / `ui_translate` / `ui_rotate` — to move something as a whole you fold the offset into its coordinates).

// 全触摸、竖屏、自动开火,键盘只作兜底。

// All touch, portrait, auto fire, with the keyboard only as a fallback.

//

// ## 玩法

// ## How it plays

//

//   手指按住拖动 = 移动飞机(**相对拖动**:飞机跟着手指的位移走,

//   Press and drag to move the craft (**relative dragging**: the craft follows how far the finger moved,

//   不会瞬移到指尖底下);开火是**自动**的,不需要开火键。

//   it does not teleport under the fingertip); firing is **automatic** and needs no fire button.

//   键盘兜底:方向键移动、回车/空格立即补一轮子弹、ESC 退出。

//   Keyboard fallback: arrow keys move, Enter/Space fires an extra volley at once, ESC quits.

//

//   一共 6 波:**每 2 波一个 BOSS**(第 2 / 4 / 6 波),三种 BOSS 按顺序轮换

//   Six waves in all: **one BOSS every 2 waves** (waves 2 / 4 / 6), cycling through three BOSS kinds

//   (扇弹型 → 激光型 → 追踪弹幕型),血量与弹速逐轮上调。打完第 6 波那个 BOSS 才通关。

//   (fan → laser → homing barrage), with health and bullet speed stepping up each round. Only killing the wave-6 BOSS clears the game.

//   自机有 **HP 条**(100 点)+ 3 条命:HP 归零掉一条命、原地满血复活并短暂无敌;

//   The craft has an **HP bar** (100 points) plus 3 lives: at zero HP it loses a life, revives in place at full HP and is briefly invincible;

//   命尽才进结束画面,点屏重开。

//   only running out of lives shows the end screen, and a tap restarts.

//

// ## 难度曲线(有封顶,不是"后面一坨")

// ## Difficulty curve (capped, not "a wall at the end")

//

//   本波数量 = 8 + 波次×4,**上限 40**;出现间隔 = 34 − 波次×3 拍,**下限 10 拍**;

//   Enemies per wave = 8 + wave×4 capped at **40**; spawn gap = 34 − wave×3 ticks with a floor of **10**;

//   同屏敌机 = 4 + 波次,**上限 12**;同屏敌弹 = 14 + 波次×6,**上限 48**(池子 64,给 BOSS 弹幕留余量)。

//   on-screen enemies = 4 + wave capped at **12**; on-screen enemy bullets = 14 + wave×6 capped at **48** (pool 64, leaving room for the BOSS barrage).

//   种类按波次解锁:第 1 波只有侦察机,第 3 波起加横扫/斜插/蛇行,第 5 波起加炮台/自杀机。

//   Types unlock by wave: wave 1 has scouts only, wave 3 adds sweepers/divers/snakes, wave 5 adds turrets and kamikazes.

//   第 6 波之后这些量全部封顶,难度改由 BOSS(血量 / 弹速 / 激光时长)承担。

//   After wave 6 all of those are capped and the difficulty moves entirely to the BOSS (health / bullet speed / laser duration).

//

// ## 为什么池子是**普通 int 数组**(以及那两条"前端缺陷"的下场)

// ## Why the pools are **plain int arrays** (and what became of those two "front-end defects")

//

// 这份例程最初是"每个机种一个类、池子是 `Enemy gEnemies[16]`"写的 —— **跑不起来**,

// This example was first written as "one class per craft type, the pool being `Enemy gEnemies[16]`" — **it did not run**,

// 当时的结论是"C++ 前端有两条缺陷",并据此把池子改成了现在的形状。

// the conclusion at the time was "this C++ front end has two defects", and the pools were reshaped accordingly.

// **2026-09-30 把这两条各做成一个能独立编的最小复现,结论是一条真、一条假。**

// **On 2026-09-30 each of the two was minimised into a standalone repro; one turned out real, the other false.**

//

//   ① **静态对象的构造函数不跑** —— **真缺陷,已修**(C++ 前端 F49 / OPEN #24)。

//   ① **Constructors do not run for static objects** — **a real defect, now fixed** (C++ front end F49 / OPEN #24).

//      最小复现是"把初值写进构造函数、然后**只读不写**":`static Foo G;` 之后 `G.a` 恒为 0。

//      The minimal repro is "write the values in a constructor, then **read without ever writing**": after `static Foo G;`, `G.a` is always 0.

//      ⚠ 它难查的地方正是"**先写再读一切正常**" —— 那条路不经过构造函数,

//      ⚠ What makes it hard to catch is precisely that "**write first, then read**" works fine — that path never touches the constructor,

//        所以当初探针里顺手加的一句 `Set()` 就把它盖过去了。

//        which is exactly how a casual `Set()` in an early probe papered over it.

//      绕过(仍然有效,也仍然推荐):初值靠显式的 `Reset()` 写、由 `main` 开头调 —— 见 `Player::Reset()`。

//      The workaround (still valid, still recommended): write the initial values in an explicit `Reset()` called at the top of `main` — see `Player::Reset()`.

//   ② **带构造函数(或干脆没有方法)的类做数组元素寻址是错的** —— **查证后不是缺陷**

//   ② **A class with a constructor (or with no methods at all) is addressed wrongly as an array element** — **investigated, not a defect**

//      (C++ 前端 F50,与那份台账的 OPEN #16 是同一类假警报)。

//      (C++ front end F50, the same kind of false alarm as OPEN #16 in that ledger).

//      四种形态全部正常:带构造函数的全局数组 / 无方法的全局数组 / 整池清 0 之后读回 /

//      All four shapes behave correctly: a global array of a class with a constructor / of a class with no methods / the pool cleared to 0 and read back /

//      **直接**写字段(`arr[i].f4 = 4;`)与走方法写读回来一致。

//      a **direct** field write (`arr[i].f4 = 4;`) against a method write, read back identically.

//      当初的假警报来自 F43(`arr[i] = T(…)` 只写半个对象)与 F38(全局对象只分配 1 个 word)——

//      The original false alarm came from F43 (`arr[i] = T(…)` writing only half an object) and F38 (a global object allocated only 1 word) —

//      那两条的症状与它**一模一样**,差别只在"是不是**每一个**元素都坏"。

//      their symptoms are **identical**; the only difference is whether **every** element is broken.

//      **2026-10-04 复验:两条都实测通过。** ① `scripts/vml-cpp-probe/cases/f49_static_object_ctor.cpp` 打 `ALL PASS`(65661 条指令);

//      **Re-measured 2026-10-04: both pass.** ① `scripts/vml-cpp-probe/cases/f49_static_object_ctor.cpp` prints `ALL PASS` (65,661 instructions);

//      ② `f50_class_array_pool_addressing.cpp` 打 `ALL PASS`(66117 条指令),F38 的 `f38_global_object_alloc.cpp` 也打 `ALL PASS`(65655 条指令)。

//      ② `f50_class_array_pool_addressing.cpp` prints `ALL PASS` (66,117 instructions) and F38's `f38_global_object_alloc.cpp` prints `ALL PASS` (65,655 instructions).

//

// ⇒ 池子保持现在的形状**不是因为缺陷**,而是因为这个形状**已经跑通、且与同目录的 `gorilla.cpp` 同构**。

// ⇒ The pools keep their present shape **not because of a defect** but because that shape **already works and matches `gorilla.cpp` in this directory**.

//   想把它们改回对象数组的话,先跑 `scripts/vml-cpp-probe/cases/f50_class_array_pool_addressing.cpp`。

//   Before turning them back into arrays of objects, run `scripts/vml-cpp-probe/cases/f50_class_array_pool_addressing.cpp`.

// 所以这里的池子一律用**文件级 `int` 数组**(`enX[]` / `pbY[]` / `ebVX[]` …),逻辑用自由函数写;

// So the pools here are all **file-level `int` arrays** (`enX[]` / `pbY[]` / `ebVX[]` …) with the logic written as free functions;

// 对象只留给**自机**(`class Player`,单个全局对象 + 方法),初值一律走 `Reset()`。

// the only object is **the player craft** (`class Player`, one global object plus methods), and its initial values all come from `Reset()`.

//

// ⚠ 本程序自身的写法约定(**不是**前端缺陷,只是为了少踩坑、也让生成的指令好读):

// ⚠ This program's own conventions (**not** front-end defects, just ways to step on fewer rakes and to keep the generated code readable):

//   **局部变量一律在函数开头声明**;**不用指针形参 / 引用 / 默认参数 / 模板 / STL / `new`** ——

//   **every local variable is declared at the top of its function**; **no pointer parameters, references, default arguments, templates, STL or `new`** —

//   要"返回两个值"就用文件级暂存变量(见 `AimAt` 写 `gAimVX` / `gAimVY` 那处)。

//   to "return two values", use file-level scratch variables (see `AimAt` writing `gAimVX` / `gAimVY`).

//

// ## 图块句柄的固定顺序(写测试的人会用到)

// ## The fixed order of the block handles (useful when writing tests)

//

// `ui_2d_create_block` 是**逐个分配**句柄的,而 `--trace-draw` 里画块只记 `blkdraw handle=N`

// `ui_2d_create_block` hands out handles **one by one**, and `--trace-draw` records a block draw as just `blkdraw handle=N`

// ⇒ 句柄号与机型的对应关系由 `MakeBlocks()` 的**建立顺序**唯一确定,便于数"哪一种出现过":

// ⇒ so the handle-to-craft mapping is fixed by the **order** in `MakeBlocks()` and makes it easy to count "which type showed up":

//   1 自机 / 2–7 敌机 0–5 / 8–10 三种 BOSS / 11 自机子弹 / 12–17 敌弹六种。

//   1 the player / 2–7 enemy types 0–5 / 8–10 the three BOSSes / 11 the player bullet / 12–17 the six enemy bullets.

//

// ## 跑法

// ## How to run

//

//   手机:`vml run examples/cpp/1945.cpp`

//   Phone: `vml run examples/cpp/1945.cpp`

//   桌面:`vmlcli Examples/cpp/1945.cpp --screen 411x726 --timeout 8 --frame /tmp/1945.png`

//   Desktop: `vmlcli Examples/cpp/1945.cpp --screen 411x726 --timeout 8 --frame /tmp/1945.png`


#include <vml_ui.h>

// ════════════════════════════════════════════════════════════════════

// 参数

// Tunables

// ════════════════════════════════════════════════════════════════════


#define TICK_MS     33      // 逻辑节拍(≈30 拍/秒,与 gorilla 同一档)—— ⚠ 拍 ≠ 帧

// Logic tick (about 30 ticks/s, the same step gorilla uses) -- ⚠ a tick is not a frame

#define ENEMY_MAX   24      // 敌机池(含 BOSS;同屏硬上限 12,池子留余量给"退出屏幕前那一批")

// Enemy pool (BOSSes included; the on-screen cap is 12, and the pool leaves room for the batch still leaving the screen)

#define PBUL_MAX    40      // 自机子弹池(W4 五向、11 拍一轮 ⇒ 峰值约 17 发)

// Player bullet pool (5-way at W4 one volley every 11 ticks ⇒ a peak of about 17 in flight)

#define EBUL_MAX    64      // 敌弹池(同屏上限 48,余量给 BOSS 弹幕)

// Enemy bullet pool (48 on screen, with room to spare for the BOSS barrage)

#define BOOM_MAX    18      // 爆炸池

// Explosion pool

#define ITEM_MAX    8       // 掉落物池(**同屏最多 4 个**,见 `ITEM_LIVE_MAX`)

// Item pool (**at most 4 on screen**, see `ITEM_LIVE_MAX`)

#define STAR_MAX    36      // 星空点数

// Number of star points

#define CLOUD_MAX   6       // 远景云带条数

// Number of cloud bands


#define WAVE_LAST   6       // 最后一波(打完它的 BOSS 即通关)

// The last wave (killing its BOSS clears the game)

#define BOSS_EVERY  2       // 每 2 波出一个 BOSS

// A BOSS every 2 waves

#define BOSS_KIND0  6       // 敌机池里 BOSS 的类型起点:6 / 7 / 8 = 扇弹 / 激光 / 追踪弹幕

// Where the BOSS types start in the enemy pool: 6 / 7 / 8 = fan / laser / homing barrage

#define BOSS_KINDS  3       // 三种 BOSS

// Three BOSS kinds


#define COUNT_BASE  8       // 本波敌机数 = COUNT_BASE + 波次 × COUNT_STEP

// Enemies per wave = COUNT_BASE + wave × COUNT_STEP

#define COUNT_STEP  4
#define COUNT_CAP   40      // 每波数量上限

// Cap on enemies per wave

#define GAP_BASE    34      // 出现间隔 = GAP_BASE − 波次 × GAP_STEP 拍

// Spawn gap = GAP_BASE − wave × GAP_STEP ticks

#define GAP_STEP    3
#define GAP_MIN     10      // 出现间隔下限(10 拍 ≈ 0.33 秒一架)

// Floor on the spawn gap (10 ticks is about one enemy every 0.33 seconds)

#define ONSCR_BASE  4       // 同屏敌机 = ONSCR_BASE + 波次

// On-screen enemies = ONSCR_BASE + wave

#define ONSCR_CAP   12      // 同屏敌机硬上限 —— ⚠ 别为了"密"放开它,每帧图元数会顶破 400

// Hard cap on on-screen enemies — ⚠ do not relax it for density, or the per-frame primitive count will blow past 400

#define EBCAP_BASE  14      // 同屏敌弹 = EBCAP_BASE + 波次 × EBCAP_STEP

// On-screen enemy bullets = EBCAP_BASE + wave × EBCAP_STEP

#define EBCAP_STEP  6
#define EBCAP_CAP   48      // 同屏敌弹上限(池子 64)

// Cap on on-screen enemy bullets (pool 64)


#define HP_MAX      100     // 自机满血

// The craft's full health

#define HP_PACK     45      // 血包回血量

// Health restored by one health pack

#define DMG_SMALL   8       // 小弹 / 快弹的伤害

// Damage from a small or fast bullet

#define DMG_BIG     16      // 大弹 / 瞄准弹 / 环弹 / 追踪弹的伤害

// Damage from a big, aimed, ring or homing bullet

#define DMG_RAM     20      // 撞击(敌机撞到自机)的伤害

// Damage from being rammed by an enemy

#define DMG_LASER   6       // 激光**每次结算**的伤害(结算节流见 `LASER_HIT_CD`)

// Damage per laser **damage event** (the event throttle is `LASER_HIT_CD`)

#define LASER_HIT_CD 6      // 同一道激光两次结算之间至少隔几拍

// Minimum ticks between two damage events from the same laser

#define SHIELD_TICKS 90     // 护盾 S:3 秒无敌(≈90 拍)

// Shield pickup: 3 seconds of invincibility (about 90 ticks)

#define INV_TICKS   45      // 掉命复活后的无敌拍数(45 × 33ms ≈ 1.5 秒)

// Invincible ticks after losing a life (45 × 33ms ≈ 1.5 seconds)

#define PLAYER_LIVE 3       // 初始命数

// Starting lives

#define WLEV_MAX    4       // 火力等级上限

// Maximum weapon level

#define ITEM_LIVE_MAX 4     // 同屏掉落物上限(密度上来之后靠它防糊屏)

// Cap on simultaneous items (this is what keeps the screen readable once the density climbs)


#define BOSS_HP     50      // BOSS 基础血量(第 1 个 BOSS)

// Base BOSS health (the first BOSS)

#define BOSS_HP_STEP 25     // 每个 BOSS 递增的血量(50 / 75 / 100)

// Health added per BOSS round (50 / 75 / 100)

#define BOSS_FAN_N  5       // 扇形弹幕的**发数**(奇数才好以正下方为中心对称铺开)

// Number of shots in the fan barrage (odd so it spreads symmetrically about straight down)

#define BOSS_FAN_D  25      // 扇形相邻两发的**角度差**(度)⇒ 总张角 = (N-1)×D

// Angle between neighbouring fan shots in degrees ⇒ the total spread is (N-1)×D

#define BOSS_FAN_V  4       // 扇形弹速(比普通敌弹快一点,但仍要慢到看得清、躲得开)

// Fan bullet speed (a little faster than an ordinary enemy bullet, but still slow enough to read and dodge)

#define BOSS_FIRE_T 80      // BOSS 两轮弹幕之间隔几拍(80 × 33ms ≈ 2.6 秒)

// Ticks between two BOSS barrages (80 × 33ms ≈ 2.6 seconds)

#define BOSS_IDLE_T 60      // 激光型 BOSS 两轮激光之间的空闲拍数

// Idle ticks between two laser cycles for the laser BOSS

#define RING_N      16      // 环弹一圈多少发

// Shots in one ring barrage

#define RING_SPD    3       // 环弹基础弹速

// Base ring bullet speed

#define LASER_W     26      // 激光柱宽度(伤害判定也用这个数)

// Laser column width (the damage test uses the same number)

#define LASER_WARN_T 36     // 激光充能预警拍数(36 × 33ms ≈ 1.2 秒)

// Laser charge warning ticks (36 × 33ms ≈ 1.2 seconds)

#define LASER_FIRE_T 45     // 激光持续拍数(45 × 33ms ≈ 1.5 秒)

// Laser firing ticks (45 × 33ms ≈ 1.5 seconds)


#define ST_PLAY     0       // 正在玩

// Playing

#define ST_OVER     1       // 命尽(结束画面)

// Out of lives (end screen)

#define ST_CLEAR    2       // 通关

// Cleared


// 敌机种类编号

// Enemy type numbers

#define ET_SCOUT    0       // 侦察机:直下、偶尔单发

// Scout: straight down, the occasional single shot

#define ET_SWEEP    1       // 横扫机:左右横扫、单发

// Sweeper: sweeps across, single shot

#define ET_DIVE     2       // 斜插机:斜线、不开火

// Diver: a diagonal line, does not fire

#define ET_SNAKE    3       // 蛇行机:正弦蛇行、三连发

// Snake: sine wave descent, three-shot bursts

#define ET_TURRET   4       // 炮台机:缓降后停住、**瞄准自机**开火

// Turret: descends then stops, firing **aimed at the craft**

#define ET_KAMI     5       // 自杀机:朝自机俯冲、不开火(撞你)

// Kamikaze: dives at the craft and does not fire (it rams you)


// 敌弹种类编号

// Enemy bullet kind numbers

#define EK_SMALL    1       // 小弹(慢)

// Small bullet (slow)

#define EK_BIG      2       // BOSS 大弹

// The BOSS's big bullet

#define EK_FAST     3       // 快弹

// Fast bullet

#define EK_AIM      4       // 瞄准弹(朝自机当前位置)

// Aimed bullet (towards where the craft is now)

#define EK_RING     5       // 环弹(一圈 N 发)

// Ring bullet (N shots in a circle)

#define EK_HOME     6       // 追踪弹(慢速追踪,躲得开)

// Homing bullet (slow tracking, dodgeable)


// ── 配色 ──

// Colours

#define C_SPACE     0xFF050A18   // 太空底色

// Space background

#define C_HUD_BG    0xB0101828   // HUD 底板(半透明)

// HUD plate (semi-transparent)

#define C_TEXT      0xFFE8F0FF
#define C_TEXT_DIM  0xFF8FA0C0
#define C_GOLD      0xFFFFD060
#define C_WARN      0xFFFF6060
#define C_BAR_BG    0x80000000
#define C_BAR_FILL  0xFFFF4040
#define C_SHIELD    0xFF7FE8FF
#define C_HP_HI     0xFF54E070   // 血条:余量 > 60%

// Health bar: above 60%

#define C_HP_MID    0xFFF0D040   // 血条:30% ~ 60%

// Health bar: 30% to 60%

#define C_HP_LOW    0xFFFF5050   // 血条:< 30%

// Health bar: below 30%

#define C_TROPHY    0xFFF0C040   // 奖杯主体(金)

// Trophy body (gold)

#define C_TROPHY_DK 0xFF9A7418   // 奖杯底座与杯耳(暗金)

// Trophy base and handles (dark gold)

#define C_STONE     0xFF9AA2AE   // 墓碑石身(浅灰)

// Tombstone body (light grey)

#define C_STONE_DK  0xFF6E7684   // 墓碑内嵌面板

// The tombstone's inset panel

#define C_STONE_ENG 0xFF4A525E   // 碑面刻痕(十字)

// The engraving on the face (a cross)

#define C_GROUND    0xFF2A2E38   // 墓碑脚下的土堆

// The mound of earth at the tombstone's foot


// ── 图块句柄 ──

// Block handles

static int gPlayerBid;      // 1 自机

// 1 the player craft

static int gEnemyBid[6];    // 2–7 敌机 0..5(侦察 / 横扫 / 斜插 / 蛇行 / 炮台 / 自杀)

// 2–7 enemy types 0..5 (scout / sweeper / diver / snake / turret / kamikaze)

static int gBossBid[BOSS_KINDS];  // 8–10 三种 BOSS(扇弹 / 激光 / 追踪弹幕)

// 8–10 the three BOSSes (fan / laser / homing barrage)

static int gPBulBid;        // 11 自机子弹

// 11 the player bullet

static int gEBulBid;        // 12 敌弹(小)

// 12 enemy bullet (small)

static int gEBulFastBid;    // 13 敌弹(快)

// 13 enemy bullet (fast)

static int gEBulAimBid;     // 14 敌弹(瞄准)

// 14 enemy bullet (aimed)

static int gEBulRingBid;    // 15 敌弹(环)

// 15 enemy bullet (ring)

static int gEBulHomeBid;    // 16 敌弹(追踪)

// 16 enemy bullet (homing)

static int gEBulBigBid;     // 17 敌弹(BOSS 的大弹)

// 17 enemy bullet (the BOSS's big one)


// ════════════════════════════════════════════════════════════════════

// 全局状态

// Global state

// ════════════════════════════════════════════════════════════════════


static int gLang;           // 界面语言:开局查一次(`ui_scr_get_language` 是 syscall,别每帧调)

// UI language: looked up once at start (`ui_scr_get_language` is a syscall, do not call it every frame)

static int gSw;
static int gSh;
static int gTick;           // 帧计数(动画与节拍都用它)

// Frame counter (used for animations and cadence)

static int gScore;
static int gHi;             // 最高分

// High score

static int gState;
static int gWave;           // 波次,**1 起**(1..WAVE_LAST)

// Wave number, **1-based** (1..WAVE_LAST)

static int gSpawned;        // 本波已经放出来的敌机数

// Enemies already spawned in this wave

static int gSpawnT;         // 距下一架敌机的拍数

// Ticks until the next enemy spawns

static int gSpawnGap;
static int gWaveTotal;      // 本波一共几架

// How many enemies this wave has in total

static int gWaveDelay;      // 本波清空之后的过场拍数

// Transition ticks after the wave is cleared

static int gOnScreen;       // 本波的同屏敌机上限

// On-screen enemy cap for this wave

static int gEbCap;          // 本波的同屏敌弹上限

// On-screen enemy bullet cap for this wave

static int gBeepCd;         // 音效互斥:避免同拍连响把前面那声掐掉

// Sound guard: stops several beeps in one tick from cutting each other off

static int gBannerT;        // 波次提示还显示几拍

// How many ticks the wave banner stays up

static int gFlash;          // 被击中时的全屏闪白(0 = 不闪)

// Full-screen white flash on being hit (0 = no flash)

static int gDragOn;         // 手指是否按着

// Whether a finger is down

static int gDragX;
static int gDragY;
static int gBossShown;      // 本波 BOSS 是否已经出现过(给 HUD 标签用)

// Whether this wave's BOSS has already appeared (used by the HUD label)


// ════════════════════════════════════════════════════════════════════

// 小工具

// Small helpers

// ════════════════════════════════════════════════════════════════════


// 整数 → 字符串(不引 std::string:这个前端没有 STL)

// Integer → string (no std::string: this front end has no STL)

// ⚠ 返回的是**同一个静态缓冲** ⇒ 一次 `ui_2d_text` 调用里只能出现一次 `numstr()`,

// ⚠ It returns **one shared static buffer** ⇒ only one `numstr()` may appear inside a single `ui_2d_text` call,

//   否则后一次会把前一次的内容覆盖掉(宿主是否当场拷贝它没有保证)。

//   otherwise the later one overwrites the earlier one (the host gives no guarantee that it copies on the spot).

static char g_num[12];

char* numstr(int v)
{
    int n = 0;
    int m = 0;
    char tmp[12];
    if (v < 0) { g_num[0] = '-'; v = -v; n = 1; }
    if (v == 0) { g_num[n] = '0'; g_num[n + 1] = 0; return g_num; }
    while (v > 0) { tmp[m] = (char)('0' + v % 10); v = v / 10; m = m + 1; }
    while (m > 0) { m = m - 1; g_num[n] = tmp[m]; n = n + 1; }
    g_num[n] = 0;
    return g_num;
}

// 火力等级 → "W1"…"W4"(同样是**共享缓冲**,别和 `numstr` 混在一次 `ui_2d_text` 里)

// Weapon level → "W1"…"W4" (also a **shared buffer**; do not mix it into one `ui_2d_text` call with `numstr`)

static char g_wstr[4];

char* wstr(int lv)
{
    g_wstr[0] = 'W';
    g_wstr[1] = (char)('0' + lv);
    g_wstr[2] = 0;
    return g_wstr;
}

// 十进制字符串 → 整数(只认前缀数字,读存档用)

// Decimal string → integer (prefix digits only; used to read the save)

int myatoi(char* s)
{
    int v = 0;
    int i = 0;
    while (s[i] >= '0' && s[i] <= '9')
    {
        v = v * 10 + (s[i] - '0');
        i = i + 1;
    }
    return v;
}

int imax(int a, int b)
{
    if (a > b) { return a; }
    return b;
}

int imin(int a, int b)
{
    if (a < b) { return a; }
    return b;
}

int clampi(int v, int lo, int hi)
{
    if (v < lo) { return lo; }
    if (v > hi) { return hi; }
    return v;
}

int iabsv(int v)
{
    if (v < 0) { return -v; }
    return v;
}

// 整数正弦(Bhaskara I 近似):角度 -180..180 度 → -1000..1000

// Integer sine (Bhaskara I approximation): an angle of -180..180 degrees → -1000..1000

// ⚠ 只用整数:这个 VM 的浮点是另一套寄存器(F/D),而弹幕方向只需要千分之一的精度。

// ⚠ Integers only: this VM's floating point lives in a separate register bank (F/D), and a barrage direction only needs thousandth precision.

int isin(int deg)
{
    int a;
    int s;
    if (deg < -180) { deg = deg + 360; }
    if (deg > 180) { deg = deg - 360; }
    s = 1;
    if (deg < 0) { s = -1; deg = -deg; }
    a = deg * (180 - deg);
    if (a < 0) { a = 0; }
    return s * (4 * a * 1000) / (40500 - a);
}

// 余弦 = 正弦平移 90 度(省一张表、也省一处近似误差)

// Cosine is the sine shifted by 90 degrees (saves a table and one source of approximation error)

int icos(int deg)
{
    return isin(deg + 90);
}

// 圆与圆的粗略相交(先按轴分离筛掉大部分,再算平方距离)

// Rough circle-versus-circle overlap (reject most pairs by axis separation first, then compare squared distance)

int hitCirc(int ax, int ay, int ar, int bx, int by, int br)
{
    int dx;
    int dy;
    int rr;
    dx = iabsv(ax - bx);
    dy = iabsv(ay - by);
    rr = ar + br;
    if (dx > rr) { return 0; }
    if (dy > rr) { return 0; }
    if (dx * dx + dy * dy <= rr * rr) { return 1; }
    return 0;
}

// 瞄准:把 (sx,sy) → (tx,ty) 的方向算成速度,结果写进**文件级暂存** `gAimVX` / `gAimVY`

// Aiming: turn the direction (sx,sy) → (tx,ty) into a velocity, written into the **file-level scratch** `gAimVX` / `gAimVY`

// ⚠ 不用指针形参(见文件头)⇒ 只能这样"返回两个值"。**调用完立刻用**,中间别再调一次。

// ⚠ No pointer parameters here (see the file header) ⇒ this is the only way to "return two values". **Use them right away**, do not call this again in between.

// ⚠ 用 L1(分量绝对值之和)归一化:`spd` 是"两个分量加起来"的速度,斜着飞时每轴都慢下来,

// ⚠ Normalised by L1 (the sum of the absolute components): `spd` is the speed with both components added, so each axis slows down on a diagonal,

//   不会出现"横着飞时快得离谱"或"垂直分量算成 0 卡在天上"这两种整数除法的老毛病。

//   which avoids the two classic integer-division bugs: "absurdly fast when flying sideways" and "the vertical component rounds to 0 and it hangs in the sky".

static int gAimVX;
static int gAimVY;

void AimAt(int sx, int sy, int tx, int ty, int spd)
{
    int dx;
    int dy;
    int d;
    dx = tx - sx;
    dy = ty - sy;
    d = iabsv(dx) + iabsv(dy);
    if (d < 1) { d = 1; }
    gAimVX = spd * dx / d;
    gAimVY = spd * dy / d;
    if (gAimVY < 1) { gAimVY = 1; }
}

// ════════════════════════════════════════════════════════════════════

// 音效

// Sound effects

//

// ⚠ `ui_audio_beep` 是**单通道**的(后一个音掐掉前一个)⇒ 绝不做"连发枪声",

// ⚠ `ui_audio_beep` is **single-channel** (a new tone cuts the previous one) ⇒ never make a "machine-gun" sound,

//   音效只留给**击中 / 击落 / 被击中 / BOSS 阵亡 / 结束 / 换波 / 拾取 / 激光**这几个事件。

//   and save sound for the events **hit / shot down / player hurt / BOSS destroyed / game over / wave change / pickup / laser**.

// ⚠ `gBeepCd` 是"同拍互斥":一轮打掉三架敌机时只响最响的那一声,否则三声叠在一起

// ⚠ `gBeepCd` is a same-tick guard: killing three enemies at once plays only the loudest tone, instead of three

//   结果是三声全被掐到只剩最后一声(听起来像"少响了两次")。

//   tones cutting each other down to the last one (which sounds like "two of them never played").

// ⚠ 拾取音**按单通道纪律**:一次事件一声,绝不做"连拾连响"(连着捡两个就是两声,第 2 声会掐掉第 1 声)。

// ⚠ The pickup sound follows the **single-channel discipline**: one event, one tone, never a "pickup rattle" (two pickups in a row means the second cuts off the first).

// ════════════════════════════════════════════════════════════════════


void SfxWave()
{
    ui_audio_beep(660, 90);
    gBeepCd = 4;
}

void SfxHit()
{
    if (gBeepCd > 0) { return; }
    ui_audio_beep(988, 30);
    gBeepCd = 3;
}

void SfxKill()
{
    if (gBeepCd > 1) { return; }
    ui_audio_beep(392, 70);
    gBeepCd = 3;
}

// 重型机被打掉:低一档、长一点,与轻型机**听得出区别**

// A heavy craft going down: a lower, longer tone that is **audibly different** from a light one

void SfxKillHeavy()
{
    if (gBeepCd > 1) { return; }
    ui_audio_beep(196, 150);
    gBeepCd = 6;
}

// 拾取掉落物(单通道:一次一声)

// Picking up an item (single channel: one event, one tone)

void SfxPick()
{
    if (gBeepCd > 0) { return; }
    ui_audio_beep(1318, 55);
    gBeepCd = 3;
}

// 激光充能 / 发射:两个**必响**的预警音(玩家要靠它们判断"该躲了")

// Laser charge and fire: two **always audible** cues (the player relies on them to know it is time to dodge)

void SfxLaserWarn()
{
    ui_audio_beep(240, 150);
    gBeepCd = 14;
}

void SfxLaserFire()
{
    ui_audio_beep(1200, 260);
    gBeepCd = 18;
    ui_vibrate_simple(70, 0);
}

// 玩家被击中:**强制响**(这是最重要的反馈,不能被互斥吃掉),并震动一下

// The player is hit: **always plays** (this is the most important feedback and must not be eaten by the guard), plus a vibration

void SfxHurt()
{
    ui_audio_beep(165, 280);
    gBeepCd = 8;
    ui_vibrate_simple(60, 0);
}

void SfxBossDown()
{
    ui_audio_beep(110, 420);
    gBeepCd = 14;
    ui_vibrate_simple(220, 0);
}

void SfxOver()
{
    ui_audio_beep(131, 480);
    gBeepCd = 14;
}

// ════════════════════════════════════════════════════════════════════

// 自机(**唯一的对象**:单个全局对象 + 方法,这一档在前端里是验过的)

// The player craft (**the only object**: one global object plus methods, a case verified in this front end)

// ════════════════════════════════════════════════════════════════════


class Player
{
public:
    int x;
    int y;
    int r;          // 碰撞半径(比图形略小:擦着弹幕过也算躲开,手感才不憋屈)

    // Collision radius (slightly smaller than the art: grazing the barrage should still count as a dodge, or it feels unfair)

    int lives;
    int hp;         // 当前血量(0 就掉一条命,见 `HurtPlayer`)

    // Current health (at 0 a life is lost, see `HurtPlayer`)

    int wlev;       // 火力等级 1..WLEV_MAX

    // Weapon level 1..WLEV_MAX

    int invT;       // 无敌剩余拍数(掉命复活 / 护盾 S)

    // Remaining invincible ticks (life-loss revival or a shield pickup)

    int fireT;      // 距下一轮自动开火

    // Ticks until the next auto volley


    // ⚠ **没有构造函数**:静态对象的构造函数在这个前端不跑(见文件头 ①),

    // ⚠ **No constructor**: constructors do not run for static objects in this front end (see point ① in the file header),

    //   初值一律靠 `Reset()` 显式写。`main` 里开局就会调它。

    //   so every initial value is written explicitly by `Reset()`, which `main` calls at startup.

    void Reset()
    {
        x = gSw / 2;
        y = gSh * 72 / 100;
        r = 11;
        lives = PLAYER_LIVE;
        hp = HP_MAX;
        wlev = 1;
        // 开局也给一段无敌:免得"一出来就被子弹糊脸"

        // Start with some invincibility too, so you are not pasted by bullets the moment you appear

        invT = 60;
        fireT = 6;
    }

    void Clamp()
    {
        x = clampi(x, 20, gSw - 20);
        y = clampi(y, gSh * 42 / 100, gSh - 26);
    }

    // 触摸拖动:按**位移**移动(相对拖动),不是"瞬移到指尖底下"

    // Touch dragging: move by the **displacement** (relative dragging), not "teleport under the fingertip"

    void MoveBy(int dx, int dy)
    {
        x = x + dx;
        y = y + dy;
        Clamp();
    }

    void TickTimers()
    {
        if (invT > 0) { invT = invT - 1; }
        if (fireT > 0) { fireT = fireT - 1; }
    }

    void Step()
    {
        int spd;
        int kx;
        int ky;
        TickTimers();
        // 键盘兜底:方向键按住就持续移动(触摸拖动在 `TouchMove` 里直接算位移)

        // Keyboard fallback: holding an arrow key moves continuously (touch dragging works out its displacement in `TouchMove`)

        spd = 6;
        kx = 0;
        ky = 0;
        if (ui_key_is_down(VML_KEY_LEFT) != 0) { kx = kx - spd; }
        if (ui_key_is_down(VML_KEY_RIGHT) != 0) { kx = kx + spd; }
        if (ui_key_is_down(VML_KEY_UP) != 0) { ky = ky - spd; }
        if (ui_key_is_down(VML_KEY_DOWN) != 0) { ky = ky + spd; }
        x = x + kx;
        y = y + ky;
        Clamp();
    }

    // 无敌时**隔拍闪一下**而不是不画:完全不画会让玩家找不到自己的飞机

    // While invincible, **blink every other tick** rather than not drawing at all: drawing nothing would leave the player unable to find their craft

    int Visible()
    {
        if (invT > 0 && (gTick / 3) % 2 == 0) { return 0; }
        return 1;
    }

    void Draw()
    {
        int fy;
        if (Visible() == 0) { return; }
        // 尾焰:**动态的**(每拍长度不同),不进图块

        // The engine flame is **dynamic** (a different length each tick) and is not part of the block

        fy = 14;
        if (gTick % 2 == 0) { fy = 20; }
        ui_2d_rect(x - 6, y + 13, 4, fy, 0xFFFFC060, 1, 0, 2);
        ui_2d_rect(x + 2, y + 13, 4, fy, 0xFFFFC060, 1, 0, 2);
        if (gPlayerBid > 0)
        {
            ui_2d_draw_block(gPlayerBid, x, y, 1000, 1000, 0);
        }
        else
        {
            ui_2d_rect(x - 14, y - 4, 28, 10, 0xFF2E7BD6, 1, 0, 3);
            ui_2d_rect(x - 4, y - 14, 8, 26, 0xFF9ED2FF, 1, 0, 3);
        }
        // 火力等级 ≥ 3 时在翼下挂两个小炮舱:**升级看得见**(不用去读 HUD 才知道变强了)

        // From weapon level 3 the craft carries two small pods under the wings: **the upgrade is visible** (you do not have to read the HUD to know you got stronger)

        if (wlev >= 3)
        {
            ui_2d_rect(x - 20, y + 1, 7, 12, 0xFFFFB040, 1, 0, 2);
            ui_2d_rect(x + 13, y + 1, 7, 12, 0xFFFFB040, 1, 0, 2);
        }
        // 无敌时加一圈护盾环,让"还能再挨几下"这件事看得见

        // While invincible, add a shield ring so "you can take a few more hits" is visible

        if (invT > 0)
        {
            ui_2d_circle(x, y, 21, C_SHIELD, 0, 2);
        }
    }
};

static Player gPlayer;

// ════════════════════════════════════════════════════════════════════

// 敌机池(**普通 int 数组**,见文件头那两条前端缺陷)

// Enemy pool (**plain int arrays**, see the two front-end defects in the file header)

//

// 六种普通机 + 三种 BOSS 共用一张池子,用 `enType[]` 分流:

// Six ordinary craft plus three BOSSes share one pool and branch on `enType[]`:

//   0 侦察机(直下、偶尔单发)        1 横扫机(左右掠过、单发)

//   0 scout (straight down, occasional single shot)  1 sweeper (skims in from the sides, single shot)

//   2 斜插机(斜线、不开火)          3 蛇行机(正弦下行、三连发)

//   2 diver (a diagonal line, no fire)               3 snake (sine descent, three-shot bursts)

//   4 炮台机(缓降后停住、瞄准开火)  5 自杀机(朝自机俯冲、撞你)

//   4 turret (descends then stops, aimed fire)       5 kamikaze (dives at the craft and rams it)

//   6 / 7 / 8 = 扇弹型 / 激光型 / 追踪弹幕型 BOSS

//   6 / 7 / 8 = fan / laser / homing-barrage BOSS

// ════════════════════════════════════════════════════════════════════


static int enX[ENEMY_MAX];
static int enY[ENEMY_MAX];
static int enVX[ENEMY_MAX];
static int enVY[ENEMY_MAX];
static int enHP[ENEMY_MAX];
static int enHPMax[ENEMY_MAX];
static int enType[ENEMY_MAX];
static int enFireT[ENEMY_MAX];
static int enT[ENEMY_MAX];
static int enLive[ENEMY_MAX];
static int enR[ENEMY_MAX];
static int enSc[ENEMY_MAX];
static int enFlash[ENEMY_MAX];
static int enBX[ENEMY_MAX];     // 蛇行机的**基准横坐标**(正弦绕它摆)

// The snake's **base x** (its sine swings about this)

static int enLPh[ENEMY_MAX];    // 激光型 BOSS 的阶段:0 空闲 / 1 充能 / 2 发射

// The laser BOSS's phase: 0 idle / 1 charging / 2 firing

static int enLT[ENEMY_MAX];     // 当前阶段的剩余拍数

// Ticks left in the current phase

static int enLCd[ENEMY_MAX];    // 激光伤害节流("同一道激光"至少隔 LASER_HIT_CD 拍才再扣一次)

// Laser damage throttle (the same laser only bites again after at least LASER_HIT_CD ticks)


int IsBossType(int t)
{
    if (t >= BOSS_KIND0) { return 1; }
    return 0;
}

// 第 k 个 BOSS 的血量 / 弹速加成 / 激光时长 —— **逐轮上调**,这是"封顶之后唯一的难度来源"

// Health / bullet speed bonus / laser duration of the k-th BOSS — **stepped up each round**, which is "the only difficulty left once the density is capped"

int BossHpFor(int k)
{
    return BOSS_HP + k * BOSS_HP_STEP;
}

int BossSpdFor(int k)
{
    return k;
}

int BossLaserFor(int k)
{
    return LASER_FIRE_T + k * 12;
}

int BossFireGap(int k)
{
    if (k == 0) { return BOSS_FIRE_T; }
    if (k == 1) { return 90; }
    return 95;
}

// 当前是第几个 BOSS(0 起):波次 2 / 4 / 6 → 0 / 1 / 2

// Which BOSS this is (0-based): waves 2 / 4 / 6 → 0 / 1 / 2

int BossRound()
{
    int k;
    k = gWave / BOSS_EVERY - 1;
    return clampi(k, 0, BOSS_KINDS - 1);
}

void EnemySpawn(int i, int pt, int px, int py, int pvx, int pvy)
{
    int k;
    int d;
    int dx;
    int dy;
    enType[i] = pt;
    enX[i] = px;
    enY[i] = py;
    enVX[i] = pvx;
    enVY[i] = pvy;
    enBX[i] = px;
    enT[i] = 0;
    enFlash[i] = 0;
    enLive[i] = 1;
    enLPh[i] = 0;
    enLCd[i] = 0;
    // 数值按机型定,**与出场位置无关** ⇒ 同一机型不管从哪冒出来手感一致

    // The numbers are set by craft type, **not by where it enters** ⇒ the same type feels the same no matter where it appears

    if (pt == ET_SCOUT)
    {
        enHP[i] = 1; enHPMax[i] = 1; enR[i] = 13; enSc[i] = 100; enFireT[i] = 55 + ui_math_rand(40);
    }
    if (pt == ET_SWEEP)
    {
        enHP[i] = 2; enHPMax[i] = 2; enR[i] = 14; enSc[i] = 150; enFireT[i] = 40 + ui_math_rand(40);
    }
    if (pt == ET_DIVE)
    {
        enHP[i] = 1; enHPMax[i] = 1; enR[i] = 13; enSc[i] = 200; enFireT[i] = 9999;
    }
    if (pt == ET_SNAKE)
    {
        enHP[i] = 2; enHPMax[i] = 2; enR[i] = 14; enSc[i] = 220; enFireT[i] = 60 + ui_math_rand(30);
    }
    if (pt == ET_TURRET)
    {
        enHP[i] = 4; enHPMax[i] = 4; enR[i] = 16; enSc[i] = 300; enFireT[i] = 45 + ui_math_rand(25);
    }
    if (pt == ET_KAMI)
    {
        enHP[i] = 1; enHPMax[i] = 1; enR[i] = 12; enSc[i] = 180; enFireT[i] = 9999;
        // 自杀机:**出生那一刻**算好朝自机的方向,之后走直线 ⇒ 玩家只要侧移就能躲开

        // Kamikaze: the heading towards the craft is worked out **the moment it spawns**, then it flies straight ⇒ sidestepping dodges it

        AimAt(px, py, gPlayer.x, gPlayer.y, 7);
        dx = gAimVX;
        dy = gAimVY;
        d = iabsv(dx) + iabsv(dy);
        if (d < 1) { d = 1; }
        enVX[i] = 7 * dx / d;
        enVY[i] = 7 * dy / d;
        if (enVY[i] < 1) { enVY[i] = 1; }
    }
    if (IsBossType(pt) != 0)
    {
        k = pt - BOSS_KIND0;
        enHP[i] = BossHpFor(k);
        enHPMax[i] = enHP[i];
        enR[i] = 50;
        enSc[i] = 2000 + k * 1000;
        enFireT[i] = BOSS_FIRE_T;
        enLT[i] = BOSS_IDLE_T;
        enFireT[i] = BossFireGap(k);
    }
}

// 普通机的位移。**分开写**是为了让 `EnemyStep` 的收尾(出界回收)对每种机型都跑一遍。

// The movement of an ordinary craft. It is **split out** so that the exit check at the end of `EnemyStep` runs for every type alike.

void EnemyMove(int i)
{
    int amp;
    if (enType[i] == ET_SNAKE)
    {
        // 蛇行:以出生点为中心正弦摆动 + 匀速下行

        // Snake: a sine swing about its spawn point plus a steady descent

        amp = gSw / 6;
        enX[i] = enBX[i] + amp * isin((enT[i] * 5) % 360) / 1000;
        enX[i] = clampi(enX[i], 16, gSw - 16);
        enY[i] = enY[i] + enVY[i];
        return;
    }
    if (enType[i] == ET_TURRET)
    {
        // 炮台机:缓降到屏幕上部就**停住**(停住之后它就是个固定火力点,逼玩家去拆)

        // Turret: it descends and then **stops** in the upper part of the screen (once stopped it is a fixed gun emplacement the player has to go and remove)

        if (enY[i] < gSh * 26 / 100) { enY[i] = enY[i] + enVY[i]; }
        else { enY[i] = gSh * 26 / 100; }
        return;
    }
    enX[i] = enX[i] + enVX[i];
    enY[i] = enY[i] + enVY[i];
}

// 激光型 BOSS 的状态机:空闲巡游 → 充能预警 → 发射横移 → 回到空闲

// The laser BOSS's state machine: idle patrol → charge warning → firing sweep → back to idle

void BossLaserStep(int i)
{
    int k;
    k = BossRound();
    if (enLPh[i] == 0)
    {
        if (enVX[i] == 0) { enVX[i] = 2; }
        enX[i] = enX[i] + enVX[i];
        if (enX[i] < gSw / 5) { enVX[i] = iabsv(enVX[i]); }
        if (enX[i] > gSw * 4 / 5) { enVX[i] = -iabsv(enVX[i]); }
        enLT[i] = enLT[i] - 1;
        if (enLT[i] <= 0)
        {
            // 充能:**停住**并亮起预警线(停在哪儿就射哪儿 ⇒ 预警线是真的)

            // Charge: **stop moving** and light the warning line (it fires where it stopped, so the warning line is the truth)

            enLPh[i] = 1;
            enLT[i] = LASER_WARN_T;
            SfxLaserWarn();
        }
        return;
    }
    if (enLPh[i] == 1)
    {
        enLT[i] = enLT[i] - 1;
        if (enLT[i] <= 0)
        {
            enLPh[i] = 2;
            enLT[i] = BossLaserFor(k);
            enVX[i] = 1;
            SfxLaserFire();
        }
        return;
    }
    // 发射:缓慢左右横移(速度 1 px/拍 ⇒ 1.5 秒挪不到 50px,躲得开)

    // Firing: a slow left-right sweep (1 px per tick ⇒ under 50px in 1.5 seconds, easy to dodge)

    enX[i] = enX[i] + enVX[i];
    if (enX[i] < gSw / 5) { enVX[i] = iabsv(enVX[i]); }
    if (enX[i] > gSw * 4 / 5) { enVX[i] = -iabsv(enVX[i]); }
    enLT[i] = enLT[i] - 1;
    if (enLT[i] <= 0)
    {
        enLPh[i] = 0;
        enLT[i] = BOSS_IDLE_T;
        enVX[i] = 2;
    }
}

void BossStep(int i)
{
    int k;
    k = enType[i] - BOSS_KIND0;
    // 所有 BOSS 都先降到悬停高度再开始干别的

    // Every BOSS first descends to its hover height before doing anything else

    if (enY[i] < gSh * 18 / 100)
    {
        enY[i] = enY[i] + 2;
        return;
    }
    enY[i] = gSh * 18 / 100;
    if (enLCd[i] > 0) { enLCd[i] = enLCd[i] - 1; }
    if (k == 1) { BossLaserStep(i); return; }
    // 扇弹型 / 追踪弹幕型:悬停 + 左右巡游

    // Fan and homing-barrage BOSSes: hover and patrol left and right

    if (enVX[i] == 0) { enVX[i] = 2; }
    enX[i] = enX[i] + enVX[i];
    if (enX[i] < gSw / 5) { enVX[i] = iabsv(enVX[i]); }
    if (enX[i] > gSw * 4 / 5) { enVX[i] = -iabsv(enVX[i]); }
}

void EnemyStep(int i)
{
    enT[i] = enT[i] + 1;
    if (enFlash[i] > 0) { enFlash[i] = enFlash[i] - 1; }
    if (IsBossType(enType[i]) != 0) { BossStep(i); return; }
    EnemyMove(i);
    // 出界即回收(不是"打掉了"):漏掉的敌机不给分,也不扣命

    // Off-screen means recycled (not "shot down"): a missed enemy scores nothing and costs nothing

    if (enY[i] > gSh + 50) { enLive[i] = 0; }
    if (enX[i] < -70 && enVX[i] < 0) { enLive[i] = 0; }
    if (enX[i] > gSw + 70 && enVX[i] > 0) { enLive[i] = 0; }
}

int EnemyBlockId(int t)
{
    if (IsBossType(t) != 0) { return gBossBid[t - BOSS_KIND0]; }
    return gEnemyBid[t];
}

void EnemyDraw(int i)
{
    int bid;
    int t;
    t = enType[i];
    bid = EnemyBlockId(t);
    if (bid > 0)
    {
        ui_2d_draw_block(bid, enX[i], enY[i], 1000, 1000, 0);
    }
    else
    {
        // 图块没建起来时的简版(真窗口下不会走到)

        // The simple version when the block was not created (unreachable with a real window)

        ui_2d_ellipse(enX[i], enY[i], 13, 11, 0xFFE0533C, 1, 0);
    }
    // 被打中时罩一层白:**不进图块**(图块的内容不可变,闪白只能另画)

    // A white overlay when hit: **not part of the block** (a block's content is immutable, so the flash has to be drawn separately)

    if (enFlash[i] > 0)
    {
        ui_2d_circle(enX[i], enY[i], enR[i], 0x80FFFFFF, 1, 0);
    }
}

// ════════════════════════════════════════════════════════════════════

// 子弹池(自机 / 敌机各一张)

// Bullet pools (one for the player, one for the enemies)

// ════════════════════════════════════════════════════════════════════


static int pbX[PBUL_MAX];
static int pbY[PBUL_MAX];
static int pbVX[PBUL_MAX];      // 斜射用的横向速度(0 = 直上)

// The sideways velocity used by angled shots (0 means straight up)

static int pbD[PBUL_MAX];       // 单发伤害(随火力等级递减,见 `PlayerFire`)

// Damage per shot (it drops as the weapon level rises, see `PlayerFire`)

static int pbLive[PBUL_MAX];
static int pbR[PBUL_MAX];

static int ebX[EBUL_MAX];
static int ebY[EBUL_MAX];
static int ebVX[EBUL_MAX];
static int ebVY[EBUL_MAX];
static int ebLive[EBUL_MAX];
static int ebKind[EBUL_MAX];    // EK_* 六种,见文件头

// One of the six EK_* kinds, see the file header

static int ebT[EBUL_MAX];       // 存活拍数(追踪弹靠它做"每隔几拍重新瞄准")

// Age in ticks (the homing bullet uses it to re-aim every few ticks)

static int ebR[EBUL_MAX];

static int bmX[BOOM_MAX];
static int bmY[BOOM_MAX];
static int bmT[BOOM_MAX];
static int bmMax[BOOM_MAX];
static int bmSize[BOOM_MAX];
static int bmLive[BOOM_MAX];
static int bmCol[BOOM_MAX];

// ── 掉落物池 ──

// Item pool

static int itX[ITEM_MAX];
static int itY[ITEM_MAX];
static int itK[ITEM_MAX];       // 0 = 火力包 W / 1 = 血包 H / 2 = 护盾 S

// 0 = weapon pack W / 1 = health pack H / 2 = shield S

static int itT[ITEM_MAX];
static int itLive[ITEM_MAX];

// ════════════════════════════════════════════════════════════════════

// 滚动背景

// Scrolling background

//

// 三层不同速度(1 / 4 / 7 px 每拍)+ 一层云带(9 px 每拍)——「在往前飞」靠的是

// Three layers at different speeds (1 / 4 / 7 px per tick) plus a band of clouds (9 px per tick) — "flying forward" comes

// 层与层之间的**速度差**,不是任何单一层的绝对速度。

// from the **speed difference between layers**, not from the absolute speed of any one of them.

// ════════════════════════════════════════════════════════════════════


static int starX[STAR_MAX];
static int starY[STAR_MAX];
static int starL[STAR_MAX];     // 层号 0/1/2

// Layer number, 0/1/2

static int starSpd[STAR_MAX];
static int cloudX[CLOUD_MAX];
static int cloudY[CLOUD_MAX];
static int cloudW[CLOUD_MAX];

void BackInit()
{
    int i;
    int r;
    i = 0;
    while (i < STAR_MAX)
    {
        starX[i] = ui_math_rand(gSw);
        starY[i] = ui_math_rand(gSh);
        r = ui_math_rand(10);
        starL[i] = 0;
        if (r >= 5) { starL[i] = 1; }
        if (r >= 8) { starL[i] = 2; }
        starSpd[i] = 1;
        if (starL[i] == 1) { starSpd[i] = 4; }
        if (starL[i] == 2) { starSpd[i] = 7; }
        i = i + 1;
    }

    i = 0;
    while (i < CLOUD_MAX)
    {
        cloudW[i] = 70 + ui_math_rand(110);
        cloudX[i] = ui_math_rand(gSw) - cloudW[i] / 2;
        cloudY[i] = ui_math_rand(gSh);
        i = i + 1;
    }
}

// 背景**一直**在滚:结束画面上也滚 —— 停住会让画面看起来像卡死了

// The background **always** scrolls, even on the end screen — freezing it would make the picture look hung

void BackTick()
{
    int i;
    i = 0;
    while (i < STAR_MAX)
    {
        starY[i] = starY[i] + starSpd[i];
        if (starY[i] > gSh)
        {
            starY[i] = -10;
            starX[i] = ui_math_rand(gSw);
        }
        i = i + 1;
    }
    i = 0;
    while (i < CLOUD_MAX)
    {
        cloudY[i] = cloudY[i] + 9;
        if (cloudY[i] > gSh + 20)
        {
            cloudY[i] = -30;
            cloudW[i] = 70 + ui_math_rand(110);
            cloudX[i] = ui_math_rand(gSw) - cloudW[i] / 2;
        }
        i = i + 1;
    }
}

void BackDraw()
{
    int i;
    // 云带先画(最远),再画星星 —— 星星在云之上才看得出"云在下面飘"

    // Clouds first (farthest), stars second — stars above the clouds is what makes the clouds read as drifting below

    i = 0;
    while (i < CLOUD_MAX)
    {
        ui_2d_rect(cloudX[i], cloudY[i], cloudW[i], 12, 0x2A6FA8DC, 1, 0, 6);
        ui_2d_rect(cloudX[i] + cloudW[i] / 4, cloudY[i] + 3, cloudW[i] / 2, 6, 0x1AFFFFFF, 1, 0, 3);
        i = i + 1;
    }
    i = 0;
    while (i < STAR_MAX)
    {
        if (starL[i] == 0)
        {
            ui_2d_rect(starX[i], starY[i], 2, 2, 0xFF3A4A6A, 1, 0, 0);
        }
        if (starL[i] == 1)
        {
            ui_2d_rect(starX[i], starY[i], 2, 3, 0xFF8FA8D0, 1, 0, 0);
        }
        if (starL[i] == 2)
        {
            // 最近的一层画成**拉长的条纹**:这一层速度最快,"拖线"正是速度感的来源

            // The nearest layer is drawn as **elongated streaks**: it moves fastest, and the smear is exactly where the sense of speed comes from

            ui_2d_rect(starX[i], starY[i], 2, 10, 0xFFBFD8FF, 1, 0, 1);
        }
        i = i + 1;
    }
}

// ════════════════════════════════════════════════════════════════════

// 池子操作

// Pool operations

// ════════════════════════════════════════════════════════════════════


int FreePBul()
{
    int i;
    i = 0;
    while (i < PBUL_MAX)
    {
        if (pbLive[i] == 0) { return i; }
        i = i + 1;
    }
    return -1;
}

int FreeEBul()
{
    int i;
    i = 0;
    while (i < EBUL_MAX)
    {
        if (ebLive[i] == 0) { return i; }
        i = i + 1;
    }
    return -1;
}

// 同屏还活着几发敌弹("同屏敌弹上限"靠它把关;加法是 O(池子) 的,但每拍最多几十次,量过不成负担)

// How many enemy bullets are alive (the on-screen bullet cap leans on this; it is O(pool) per call, but there are at most a few dozen calls a tick, measured to be no burden)

int LiveEBul()
{
    int i;
    int n;
    n = 0;
    i = 0;
    while (i < EBUL_MAX)
    {
        if (ebLive[i] != 0) { n = n + 1; }
        i = i + 1;
    }
    return n;
}

int FreeBoom()
{
    int i;
    i = 0;
    while (i < BOOM_MAX)
    {
        if (bmLive[i] == 0) { return i; }
        i = i + 1;
    }
    return -1;
}

int FreeEnemy()
{
    int i;
    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] == 0) { return i; }
        i = i + 1;
    }
    return -1;
}

int AliveEnemies()
{
    int i;
    int n;
    n = 0;
    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] != 0) { n = n + 1; }
        i = i + 1;
    }
    return n;
}

int FreeItem()
{
    int i;
    i = 0;
    while (i < ITEM_MAX)
    {
        if (itLive[i] == 0) { return i; }
        i = i + 1;
    }
    return -1;
}

int LiveItems()
{
    int i;
    int n;
    n = 0;
    i = 0;
    while (i < ITEM_MAX)
    {
        if (itLive[i] != 0) { n = n + 1; }
        i = i + 1;
    }
    return n;
}

void AddBoom(int x, int y, int size, int col)
{
    int i;
    i = FreeBoom();
    if (i < 0) { return; }
    bmX[i] = x;
    bmY[i] = y;
    bmT[i] = 0;
    bmSize[i] = size;
    bmCol[i] = col;
    bmMax[i] = 6 + size / 3;
    bmLive[i] = 1;
}

// 敌弹的**单发伤害**(小弹/快弹轻,其余重)

// The **damage per shot** of an enemy bullet (small and fast are light, the rest are heavy)

int EBulDamage(int kind)
{
    if (kind == EK_SMALL) { return DMG_SMALL; }
    if (kind == EK_FAST) { return DMG_SMALL; }
    return DMG_BIG;
}

int EBulR(int kind)
{
    if (kind == EK_SMALL) { return 5; }
    if (kind == EK_FAST) { return 5; }
    if (kind == EK_AIM) { return 7; }
    if (kind == EK_RING) { return 7; }
    if (kind == EK_HOME) { return 6; }
    return 7;
}

// 加一发敌弹。**同屏上限**在这里把关(超了就直接丢弃这一发):

// Add one enemy bullet. The **on-screen cap** is enforced here (once it is reached the shot is simply dropped):

// 上限按波次来(`gEbCap`),封顶 48 —— 密度最高的那一波也不会把弹幕糊成一面墙。

// the cap follows the wave (`gEbCap`) and tops out at 48 — even the densest wave cannot turn the barrage into a wall.

void AddEBul(int x, int y, int vx, int vy, int kind)
{
    int i;
    if (LiveEBul() >= gEbCap) { return; }
    i = FreeEBul();
    if (i < 0) { return; }
    ebX[i] = x;
    ebY[i] = y;
    ebVX[i] = vx;
    ebVY[i] = vy;
    ebKind[i] = kind;
    ebR[i] = EBulR(kind);
    ebT[i] = 0;
    ebLive[i] = 1;
}

void AddPBul(int x, int y, int vx, int dmg)
{
    int i;
    i = FreePBul();
    if (i < 0) { return; }
    pbX[i] = x;
    pbY[i] = y;
    pbVX[i] = vx;
    pbD[i] = dmg;
    pbR[i] = 4;
    pbLive[i] = 1;
}

// 当前火力等级的**开火间隔**(拍)。W4 略降射速,配合"单发伤害递减"把总量压住

// The **fire interval** in ticks for the current weapon level. W4 fires slightly slower, which together with the falling per-shot damage keeps the total in check

int FireGap()
{
    if (gPlayer.wlev >= WLEV_MAX) { return 11; }
    if (gPlayer.wlev == 3) { return 9; }
    return 10;
}

// 自机开火:按火力等级 1/2/3/5 发。**不做音效** —— 一秒 3 轮,响了就是噪音(见 `ui_audio_beep` 的单通道说明)

// The player fires 1/2/3/5 shots by weapon level. **No sound effect** — 3 volleys a second would be noise (see the single-channel note on `ui_audio_beep`)

// ⚠ 伤害**不是**随发数线性翻倍:W1 单发 3 点、W2 两发各 2 点、W3 三发各 2 点、W4 五发各 2 点 + 射速降到 11 拍。

// ⚠ Damage does **not** scale linearly with the shot count: W1 is one shot of 3, W2 is two of 2, W3 is three of 2, W4 is five of 2 with the rate dropped to 11 ticks.

//   每秒总伤害 0.30 / 0.40 / 0.67 / 0.91 —— 单调但远低于"发数翻倍"。

//   Damage per second is 0.30 / 0.40 / 0.67 / 0.91 — monotonic but far below "the shot count doubled".

void PlayerFire()
{
    int lv;
    int ang;
    int vx;
    lv = gPlayer.wlev;
    if (lv <= 1)
    {
        AddPBul(gPlayer.x, gPlayer.y - 18, 0, 3);
        return;
    }
    if (lv == 2)
    {
        AddPBul(gPlayer.x - 9, gPlayer.y - 16, 0, 2);
        AddPBul(gPlayer.x + 9, gPlayer.y - 16, 0, 2);
        return;
    }
    if (lv == 3)
    {
        AddPBul(gPlayer.x, gPlayer.y - 20, 0, 2);
        ang = 12;
        vx = 3 * isin(ang) / 1000;
        AddPBul(gPlayer.x - 12, gPlayer.y - 12, -3, 2);
        AddPBul(gPlayer.x + 12, gPlayer.y - 12, 3, 2);
        return;
    }
    // W4:五向(中间一发直上,两侧各一发小角度、再加一发大角度)

    // W4: five-way (one straight up, then a small angle each side, then a wide angle each side)

    AddPBul(gPlayer.x, gPlayer.y - 20, 0, 2);
    AddPBul(gPlayer.x - 10, gPlayer.y - 16, -2, 2);
    AddPBul(gPlayer.x + 10, gPlayer.y - 16, 2, 2);
    AddPBul(gPlayer.x - 16, gPlayer.y - 10, -5, 2);
    AddPBul(gPlayer.x + 16, gPlayer.y - 10, 5, 2);
}

// ════════════════════════════════════════════════════════════════════

// 掉落物

// Items

//

// 被击落的敌机**有概率**掉落,概率按**机种权重**给(重型机高、自杀机低),

// A shot-down craft **may** drop one; the chance is weighted by **craft type** (heavy craft high, kamikaze low),

// ⚠ **不按数量线性给** —— 同屏最多 `ITEM_LIVE_MAX` 个,密度最高的那一波也不会把屏幕铺满图标。

// ⚠ and **not linearly by count** — at most `ITEM_LIVE_MAX` at a time, so even the densest wave cannot carpet the screen with icons.

// ════════════════════════════════════════════════════════════════════


int DropChance(int t)
{
    if (t == ET_SCOUT) { return 8; }
    if (t == ET_SWEEP) { return 14; }
    if (t == ET_DIVE) { return 8; }
    if (t == ET_SNAKE) { return 18; }
    if (t == ET_TURRET) { return 45; }
    return 5;
}

void DropItem(int x, int y)
{
    int i;
    int r;
    if (LiveItems() >= ITEM_LIVE_MAX) { return; }
    i = FreeItem();
    if (i < 0) { return; }
    r = ui_math_rand(100);
    itK[i] = 0;
    if (r >= 40) { itK[i] = 1; }
    if (r >= 75) { itK[i] = 2; }
    itX[i] = x;
    itY[i] = y;
    itT[i] = 0;
    itLive[i] = 1;
}

void TakeItem(int i)
{
    if (itK[i] == 0)
    {
        if (gPlayer.wlev < WLEV_MAX) { gPlayer.wlev = gPlayer.wlev + 1; }
        else { gScore = gScore + 300; }
    }
    if (itK[i] == 1)
    {
        gPlayer.hp = gPlayer.hp + HP_PACK;
        if (gPlayer.hp > HP_MAX) { gPlayer.hp = HP_MAX; }
    }
    if (itK[i] == 2)
    {
        if (gPlayer.invT < SHIELD_TICKS) { gPlayer.invT = SHIELD_TICKS; }
    }
    SfxPick();
}

// 掉落物**缓慢下落**、碰到自机即生效、没接到就飘出屏幕消失

// Items **drift down slowly**, take effect the moment they touch the craft, and vanish off the bottom if you miss them

void ItemsTick()
{
    int i;
    i = 0;
    while (i < ITEM_MAX)
    {
        if (itLive[i] != 0)
        {
            itT[i] = itT[i] + 1;
            itY[i] = itY[i] + 2;
            if (itY[i] > gSh + 24) { itLive[i] = 0; }
            else
            {
                // 判定半径给到 20(比图形大一圈):掉落物是"送到嘴边"的东西,

                // The pickup radius is 20 (a size larger than the art): items are meant to be **eaten out of your hand**,

                // 擦着机翼过去的必须算接到 —— 按 `r=15` 量过一次,实测差 4px 就漏掉,玩家会觉得"明明碰到了"。

                // and one passing the wingtip has to count — measured once at `r=15` it missed by 4px, and that reads to the player as "it clearly touched me".

                if (hitCirc(itX[i], itY[i], 20, gPlayer.x, gPlayer.y, gPlayer.r) != 0)
                {
                    TakeItem(i);
                    itLive[i] = 0;
                }
            }
        }
        i = i + 1;
    }
}

void ItemDraw(int i)
{
    int x;
    int y;
    x = itX[i];
    y = itY[i];
    if (itK[i] == 0)
    {
        // 火力包 W:金色方牌 + 白色竖条(像一发子弹)

        // Weapon pack W: a gold plate with a white vertical bar (it reads as a bullet)

        ui_2d_rect(x - 10, y - 10, 20, 20, 0xFFFFC030, 1, 0, 4);
        ui_2d_rect(x - 3, y - 7, 6, 14, 0xFFFFFFFF, 1, 0, 2);
    }
    if (itK[i] == 1)
    {
        // 血包 H:绿色方牌 + 白十字

        // Health pack H: a green plate with a white cross

        ui_2d_rect(x - 10, y - 10, 20, 20, 0xFF40D060, 1, 0, 4);
        ui_2d_rect(x - 8, y - 2, 16, 5, 0xFFFFFFFF, 1, 0, 1);
        ui_2d_rect(x - 2, y - 8, 5, 16, 0xFFFFFFFF, 1, 0, 1);
    }
    if (itK[i] == 2)
    {
        // 护盾 S:青色方牌 + 白环

        // Shield S: a cyan plate with a white ring

        ui_2d_rect(x - 10, y - 10, 20, 20, 0xFF30C8E0, 1, 0, 4);
        ui_2d_circle(x, y, 6, 0xFFFFFFFF, 0, 2);
    }
    // 字母标签(W/H/S 三种语言里都一样,不做中英两套)

    // The letter label (W/H/S is the same in every language, so it needs no Chinese/English pair)

    if (itK[i] == 0) { ui_2d_text(x, y + 5, "W", 0xFF201000, 13, VML_ANCHOR_CENTER); }
    if (itK[i] == 1) { ui_2d_text(x, y + 5, "H", 0xFF002000, 13, VML_ANCHOR_CENTER); }
    if (itK[i] == 2) { ui_2d_text(x, y + 5, "S", 0xFF002028, 13, VML_ANCHOR_CENTER); }
}

// ════════════════════════════════════════════════════════════════════

// 波次与难度曲线

// Waves and the difficulty curve

//

// 全部是**有封顶**的线性式,四个量各有一个上限(见文件头的曲线表)。

// Every formula is linear **with a cap**, and each of the four quantities has its own ceiling (see the curve table in the file header).

// ⚠ 第 6 波之后这些量**全都封顶**,难度改由 BOSS 承担(血量 / 弹速 / 激光时长逐轮上调)——

// ⚠ After wave 6 they are **all capped** and the difficulty moves to the BOSS (health / bullet speed / laser duration step up each round) —

//   不这么做的话后期会从"难"滑向"不可解"。

//   without that, the late game slides from "hard" to "unsolvable".

// ════════════════════════════════════════════════════════════════════


int IsBossWave(int w)
{
    if (w <= 0) { return 0; }
    return (w % BOSS_EVERY) == 0;
}

int WaveEnemyCount(int w)
{
    int n;
    n = COUNT_BASE + w * COUNT_STEP;
    return imin(n, COUNT_CAP);
}

int WaveSpawnGap(int w)
{
    int g;
    g = GAP_BASE - w * GAP_STEP;
    return imax(g, GAP_MIN);
}

int WaveOnScreen(int w)
{
    int n;
    n = ONSCR_BASE + w;
    return imin(n, ONSCR_CAP);
}

int WaveEBulCap(int w)
{
    int n;
    n = EBCAP_BASE + w * EBCAP_STEP;
    return imin(n, EBCAP_CAP);
}

// 本波允许出现的**最高机种编号**

// The **highest enemy type number** allowed in this wave

// ⚠ 解锁是"一课一课教":第 1 波只有侦察机;横扫机从第 2 波起、斜插/蛇行从第 3 波起、

// ⚠ The unlocking teaches one lesson at a time: wave 1 has scouts only; sweepers from wave 2, divers and snakes from wave 3,

//   炮台从第 4 波起、自杀机从第 5 波起。**偶数波是 BOSS 波**,所以实际是在第 3 / 5 波各见一次新面孔。

//   turrets from wave 4 and kamikazes from wave 5. **Even waves are BOSS waves**, so the new faces really show up on waves 3 and 5.

int WaveMaxType(int w)
{
    if (w <= 1) { return ET_SCOUT; }
    if (w <= 3) { return ET_SNAKE; }
    return ET_KAMI;
}

// 按波次掷一个机种:**越靠后的机种越少**,免得一屏全是炮台机

// Roll a craft type for this wave: **later types are rarer**, so the screen is not all turrets

// ⚠ 权重表是"第 3 波起生效"的那一档;第 1 波被 `WaveMaxType` 压成只有侦察机,走不到这里。

// ⚠ The weight table is the one that applies from wave 3 on; wave 1 is clamped to scouts by `WaveMaxType` and never reaches the tail choices.

int PickEnemyType(int w)
{
    int r;
    int mx;
    mx = WaveMaxType(w);
    if (mx <= ET_SCOUT) { return ET_SCOUT; }
    r = ui_math_rand(100);
    if (mx == ET_SNAKE)
    {
        if (r < 45) { return ET_SCOUT; }
        if (r < 75) { return ET_SWEEP; }
        if (r < 90) { return ET_DIVE; }
        return ET_SNAKE;
    }
    if (r < 26) { return ET_SCOUT; }
    if (r < 48) { return ET_SWEEP; }
    if (r < 62) { return ET_DIVE; }
    if (r < 78) { return ET_SNAKE; }
    if (r < 90) { return ET_TURRET; }
    return ET_KAMI;
}

void WaveStart(int w)
{
    gWave = w;
    gSpawned = 0;
    gWaveDelay = 0;
    gBannerT = 70;
    gBossShown = 0;
    if (IsBossWave(w) != 0)
    {
        gWaveTotal = 1;
        gSpawnGap = 1;
        gSpawnT = 30;
        gOnScreen = ONSCR_CAP;
    }
    else
    {
        gWaveTotal = WaveEnemyCount(w);
        gSpawnGap = WaveSpawnGap(w);
        gSpawnT = 20;
        gOnScreen = WaveOnScreen(w);
    }
    gEbCap = WaveEBulCap(w);
    SfxWave();
}

// 放出这一波的第 n 架敌机。**出场位置只跟机种有关**(同一种机型不管在哪一波都从同一个地方来),

// Spawns enemy number n of this wave. **The entry position depends only on the type** (the same type always enters the same way, whatever the wave),

// 波与波的差别因此全落在"数量 / 间隔 / 种类配比"这三个可量的量上。

// so the difference between waves rests entirely on the three measurable quantities: count, gap and type mix.

void SpawnWaveEnemy(int n)
{
    int i;
    int t;
    int col;
    int row;
    int px;
    i = FreeEnemy();
    if (i < 0) { return; }
    if (IsBossWave(gWave) != 0)
    {
        // BOSS:从正上方慢慢降到悬停高度(`BossStep` 里判 `y < 18%`),三种按波次轮换

        // BOSS: descends from straight above to its hover height (`BossStep` tests `y < 18%`), cycling through the three kinds by wave

        EnemySpawn(i, BOSS_KIND0 + BossRound(), gSw / 2, -60, 2, 2);
        gBossShown = 1;
        return;
    }
    t = PickEnemyType(gWave);
    if (t == ET_SCOUT)
    {
        // 侦察机:三列纵队一齐压下来,路线可预测

        // Scout: three columns pressing down together on a predictable path

        col = n % 3;
        row = n / 3;
        px = gSw * (col + 1) / 4;
        EnemySpawn(i, ET_SCOUT, px, -40 - (row % 3) * 56, 0, 3);
        return;
    }
    if (t == ET_SWEEP)
    {
        // 横扫机:从左右两侧轮流掠过,逼玩家在屏幕两头来回跑

        // Sweeper: alternating passes from the left and right, forcing the player to run between the two ends

        row = n / 2;
        if (n % 2 == 0)
        {
            EnemySpawn(i, ET_SWEEP, -34, gSh * 12 / 100 + (row % 4) * 46, 3, 1);
        }
        else
        {
            EnemySpawn(i, ET_SWEEP, gSw + 34, gSh * 12 / 100 + (row % 4) * 46, -3, 1);
        }
        return;
    }
    if (t == ET_DIVE)
    {
        // 斜插机:从上方斜着扎进来,落点分散、最难预判

        // Diver: stabs in at an angle from above with scattered arrival points, the hardest to predict

        px = gSw / 6 + ui_math_rand(gSw * 2 / 3);
        if (n % 2 == 0)
        {
            EnemySpawn(i, ET_DIVE, px, -40 - (n % 3) * 44, 2, 4);
        }
        else
        {
            EnemySpawn(i, ET_DIVE, px, -40 - (n % 3) * 44, -2, 4);
        }
        return;
    }
    if (t == ET_SNAKE)
    {
        // 蛇行机:从上方蛇行而下,横摆幅度是屏宽的 1/6 ⇒ 总能摆回来,不会自己摆出屏幕

        // Snake: comes down in a sine from above; the swing is a sixth of the screen width ⇒ it always swings back and never walks itself off screen

        px = gSw / 6 + ui_math_rand(gSw * 2 / 3);
        EnemySpawn(i, ET_SNAKE, px, -40 - (n % 3) * 40, 0, 3);
        return;
    }
    if (t == ET_TURRET)
    {
        // 炮台机:慢悠悠降下来就钉在那儿(**不怕你拖时间**,只会一直瞄准)

        // Turret: drifts down and then nails itself in place (**stalling does not help**, it just keeps aiming)

        px = gSw / 5 + ui_math_rand(gSw * 3 / 5);
        EnemySpawn(i, ET_TURRET, px, -46, 0, 1);
        return;
    }
    // 自杀机:从上方朝自机俯冲

    // Kamikaze: dives at the craft from above

    px = ui_math_rand(gSw - 80) + 40;
    EnemySpawn(i, ET_KAMI, px, -40, 0, 4);
}

// 敌机开火

// Enemy fire

void BossFire(int i)
{
    int k;
    int j;
    int ang;
    int spd;
    int vx;
    int vy;
    k = enType[i] - BOSS_KIND0;
    // 还没降到悬停高度就别开火(屏幕上打不到它)

    // Do not fire before it has descended to the hover height (you cannot reach it on screen)

    if (enY[i] < gSh * 18 / 100) { return; }
    if (k == 1)
    {
        // 激光型:充能 / 发射阶段**一发别的弹都不发**(这是"激光期间能安心躲"的前提)

        // Laser BOSS: during charging and firing it **fires nothing else at all** (that is what makes the laser phase a readable dodge)

        if (enLPh[i] != 0) { return; }
        // 空闲期给一轮三发小扇形,免得它站着挨打

        // During the idle phase it throws a three-shot spread so it is not a sitting duck

        spd = 3 + BossSpdFor(k);
        j = 0;
        while (j < 3)
        {
            ang = -12 + j * 12;
            vx = spd * isin(ang) / 1000;
            vy = spd * icos(ang) / 1000;
            if (vy < 1) { vy = 1; }
            AddEBul(enX[i], enY[i] + 34, vx, vy, EK_BIG);
            j = j + 1;
        }
        return;
    }
    if (k == 2)
    {
        // 追踪弹幕型:一圈 16 发环弹 + 每轮 2 发**慢速追踪**弹(追踪速度只有环弹的一半 ⇒ 躲得开)

        // Homing-barrage BOSS: a 16-shot ring plus 2 **slow homing** shots per cycle (the homing speed is half the ring's ⇒ dodgeable)

        // ⚠ **追踪弹先发**:`AddEBul` 撞上同屏上限会直接丢弃这一发,而一圈 16 发正好能把上限吃满 ——

        // ⚠ **The homing shots go first**: `AddEBul` simply drops a shot once the on-screen cap is reached, and a 16-shot ring can eat the whole cap —

        //   顺序反过来的话,"追踪"这个主威胁会被自己的环弹挤掉(实测跑一次就能看到追踪弹整轮消失)。

        //   in the other order the homing shots, which are the real threat, get squeezed out by the ring's own bullets (a single run shows whole volleys of homing shots vanishing).

        j = 0;
        while (j < 2)
        {
            AimAt(enX[i] + (j * 2 - 1) * 34, enY[i] + 24, gPlayer.x, gPlayer.y, 4);
            vx = gAimVX;
            vy = gAimVY;
            AddEBul(enX[i] + (j * 2 - 1) * 34, enY[i] + 24, vx, vy, EK_HOME);
            j = j + 1;
        }
        spd = RING_SPD + BossSpdFor(k);
        j = 0;
        while (j < RING_N)
        {
            ang = j * 360 / RING_N;
            vx = spd * isin(ang) / 1000;
            vy = spd * icos(ang) / 1000;
            AddEBul(enX[i], enY[i] + 20, vx, vy, EK_RING);
            j = j + 1;
        }
        return;
    }
    // 扇弹型:5 发,从 -50 度扫到 +50 度 —— 慢到能躲,**但弹与弹之间有缝**。

    // Fan BOSS: 5 shots sweeping from -50 to +50 degrees — slow enough to dodge, **with gaps between the shots**.

    // ⚠ 张角与发数是**配着调**的:原来 7 发 × 20 度 = ±60 度、速度 5,实测把整屏铺满,

    // ⚠ The spread and the shot count are **tuned together**: it used to be 7 shots × 20 degrees = ±60 degrees at speed 5, which measured out to covering the whole screen,

    //   站在哪都得连续闪避十来秒,等于"没有可站的地方" —— 那不是难,那是不可解。

    //   so wherever you stood you had to dodge continuously for a dozen seconds — not hard, unsolvable.

    //   现在 5 发 × 25 度 = ±50 度:外沿那两发飞到底也留得出空档。

    //   Now 5 shots × 25 degrees = ±50 degrees: the two outermost shots leave a clear gap by the time they reach the bottom.

    spd = BOSS_FAN_V + BossSpdFor(k);
    j = 0;
    while (j < BOSS_FAN_N)
    {
        ang = -((BOSS_FAN_N - 1) / 2) * BOSS_FAN_D + j * BOSS_FAN_D;
        vx = spd * isin(ang) / 1000;
        vy = spd * icos(ang) / 1000;
        if (vy < 1) { vy = 1; }
        AddEBul(enX[i], enY[i] + 30, vx, vy, EK_BIG);
        j = j + 1;
    }
}

void EnemyFire(int i)
{
    int j;
    int ang;
    int vx;
    int vy;
    // 还没进场就别开火(屏幕上打不到它)

    // Do not fire before entering the screen (you cannot reach it on screen)

    if (enY[i] < 0) { return; }
    if (IsBossType(enType[i]) != 0) { BossFire(i); return; }
    if (enType[i] == ET_SNAKE)
    {
        // 蛇行机:三连发(中间一发直下,两侧各偏 14 度)

        // Snake: a three-shot burst (one straight down, one 14 degrees either side)

        j = 0;
        while (j < 3)
        {
            ang = -14 + j * 14;
            vx = 3 * isin(ang) / 1000;
            vy = 3 * icos(ang) / 1000;
            if (vy < 1) { vy = 1; }
            AddEBul(enX[i], enY[i] + 14, vx, vy, EK_SMALL);
            j = j + 1;
        }
        return;
    }
    if (enType[i] == ET_TURRET)
    {
        // 炮台机:**瞄准自机当前位置**(不是"偏一点" —— 它不动,偏一点就永远打不着)

        // Turret: **aimed at the craft's current position** (not "leaning a little" — it does not move, and a lean would never hit anything)

        AimAt(enX[i], enY[i] + 18, gPlayer.x, gPlayer.y, 6);
        vx = gAimVX;
        vy = gAimVY;
        AddEBul(enX[i], enY[i] + 18, vx, vy, EK_AIM);
        return;
    }
    // 其余机种:单发,略微朝着玩家偏一点(不是精确瞄准 —— 精确瞄准会变成"必中")

    // The rest: a single shot leaning slightly toward the player (not exact aiming — exact aiming would become "unavoidable")

    vx = 0;
    if (gPlayer.x < enX[i] - 30) { vx = -1; }
    if (gPlayer.x > enX[i] + 30) { vx = 1; }
    if (enType[i] == ET_SWEEP)
    {
        // 横扫机打的是**快弹**:它自己就是横向高速的机型,配慢弹会让人误判"这波很温和"。

        // The sweeper fires **fast bullets**: it is itself a fast sideways craft, and pairing it with slow shots would mislead the player into reading the wave as gentle.

        // 弹速 7(慢弹是 4)⇒ 同样看得见,但留给玩家的反应时间少一半。

        // The speed is 7 against the slow bullet's 4 ⇒ just as visible, but with half the reaction time.

        vx = (enVX[i] * 2) / 3;
        AddEBul(enX[i], enY[i] + 14, vx, 7, EK_FAST);
        return;
    }
    AddEBul(enX[i], enY[i] + 14, vx, 4, EK_SMALL);
}

void EnemiesTick()
{
    int i;
    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] != 0)
        {
            EnemyStep(i);
            if (enLive[i] != 0)
            {
                if (enFireT[i] > 0) { enFireT[i] = enFireT[i] - 1; }
                if (enFireT[i] <= 0)
                {
                    EnemyFire(i);
                    if (enLive[i] != 0)
                    {
                        if (IsBossType(enType[i]) != 0)
                        {
                            enFireT[i] = BossFireGap(enType[i] - BOSS_KIND0);
                        }
                        else { enFireT[i] = 90 + ui_math_rand(60); }
                    }
                }
            }
        }
        i = i + 1;
    }
}

// 波次的推进:**放满同屏上限之后要等**,放完 + 清空 + 过场三件都齐了才换下一波

// Wave progression: **once the on-screen cap is reached the spawner waits**, and only when spawned + cleared + the transition are all done does the next wave start

void WaveTick()
{
    int i;
    if (gSpawned < gWaveTotal)
    {
        if (AliveEnemies() >= gOnScreen) { return; }
        if (gSpawnT > 0) { gSpawnT = gSpawnT - 1; }
        if (gSpawnT <= 0)
        {
            SpawnWaveEnemy(gSpawned);
            gSpawned = gSpawned + 1;
            gSpawnT = gSpawnGap;
        }
        return;
    }
    if (AliveEnemies() > 0)
    {
        gWaveDelay = 0;
        return;
    }
    gWaveDelay = gWaveDelay + 1;
    if (gWaveDelay < 45) { return; }
    i = gWave + 1;
    if (i > WAVE_LAST) { i = WAVE_LAST; }
    WaveStart(i);
}

// ════════════════════════════════════════════════════════════════════

// 碰撞与伤害

// Collisions and damage

// ════════════════════════════════════════════════════════════════════


// 自机掉血。**两种结局分开写**:还有血 = 只是挨了一下;血量归零 = 掉一条命。

// The craft loses health. **The two outcomes are written separately**: health left means it just took a hit; zero health means a life is lost.

// ⚠ 无敌只在**掉命复活**与**护盾 S** 时给 ⇒ 挨一下不无敌。这是有意的:有了 100 点血,

// ⚠ Invincibility is granted only on a **life-loss revival** and by the **shield S** ⇒ taking a scratch does not make you invincible. That is deliberate: with 100 health,

//   再给"每挨一下无敌 1.5 秒"会把弹幕的威胁整个抹掉;而掉命复活不给无敌才是真的没法玩。

//   adding "1.5 seconds of invincibility per scratch" would erase the threat of the barrage entirely, while a revival without invincibility is genuinely unplayable.

void HurtPlayer(int dmg)
{
    int i;
    if (gPlayer.invT > 0) { return; }
    gPlayer.hp = gPlayer.hp - dmg;
    if (gPlayer.hp > 0)
    {
        gFlash = 3;
        SfxHit();
        return;
    }
    // 掉一条命:原地满血复活 + 无敌

    // A life is lost: revive in place at full health, and invincible

    gPlayer.hp = HP_MAX;
    gPlayer.lives = gPlayer.lives - 1;
    gPlayer.invT = INV_TICKS;
    gFlash = 6;
    AddBoom(gPlayer.x, gPlayer.y, 16, 0xFFFFC060);
    SfxHurt();
    // 掉命时**清掉身边的敌弹**:不清的话无敌一结束就被原地糊死,等于白给一条命

    // On losing a life, **clear the nearby enemy bullets**: otherwise the moment invincibility ends you are pasted on the spot, wasting the life

    i = 0;
    while (i < EBUL_MAX)
    {
        if (ebLive[i] != 0)
        {
            if (iabsv(ebX[i] - gPlayer.x) < 70 && iabsv(ebY[i] - gPlayer.y) < 90)
            {
                ebLive[i] = 0;
            }
        }
        i = i + 1;
    }
    if (gPlayer.lives <= 0)
    {
        gPlayer.lives = 0;
        gState = ST_OVER;
        gHi = imax(gHi, gScore);
        AddBoom(gPlayer.x, gPlayer.y, 24, 0xFFFFF0B0);
        SfxOver();
    }
}

void KillEnemy(int i)
{
    int x;
    int y;
    int sc;
    int type;
    int hp;
    x = enX[i];
    y = enY[i];
    sc = enSc[i];
    type = enType[i];
    hp = enHPMax[i];
    enLive[i] = 0;
    gScore = gScore + sc;
    if (IsBossType(type) != 0)
    {
        // BOSS:多点开花;**最后一个(第 6 波的)才通关**,前面几个只是换下一波

        // BOSS: a cluster of explosions; **only the last one (wave 6) clears the game**, the earlier ones just lead to the next wave

        AddBoom(x, y, 34, 0xFFFFE060);
        AddBoom(x - 34, y + 16, 22, 0xFFFF8030);
        AddBoom(x + 34, y + 16, 22, 0xFFFF8030);
        AddBoom(x, y - 26, 20, 0xFFFF8030);
        DropItem(x - 30, y);
        DropItem(x, y + 10);
        DropItem(x + 30, y);
        SfxBossDown();
        if (gWave >= WAVE_LAST)
        {
            gState = ST_CLEAR;
            gHi = imax(gHi, gScore);
        }
        return;
    }
    AddBoom(x, y, 14, 0xFFFF9040);
    // 掉落:**按机种权重**掷一次(重型机高、自杀机低),见 `DropChance`

    // Drop: **one weighted roll by craft type** (heavy high, kamikaze low), see `DropChance`

    if (ui_math_rand(100) < DropChance(type)) { DropItem(x, y); }
    // 重型机(血量 ≥ 4 的)换一个低沉的音效,**听得出"这次拆掉的是大家伙"**

    // Heavy craft (health 4 or more) get a lower tone, so **you can hear that the thing you removed was a big one**

    if (hp >= 4) { AddBoom(x - 12, y + 8, 10, 0xFFFFB060); SfxKillHeavy(); }
    else { SfxKill(); }
}

void DamageEnemy(int i, int dmg)
{
    enHP[i] = enHP[i] - dmg;
    enFlash[i] = 3;
    if (enHP[i] <= 0) { KillEnemy(i); return; }
    SfxHit();
}

// 激光伤害:只在**发射阶段**、只对**柱子里**的玩家、且**同一道激光至少隔 LASER_HIT_CD 拍**才结算一次

// Laser damage: only during the **firing phase**, only for a player **inside the column**, and only **once per LASER_HIT_CD ticks for the same laser**

void LaserHit(int i)
{
    int halfw;
    if (enLPh[i] != 2) { return; }
    if (enLCd[i] > 0) { return; }
    if (gPlayer.invT > 0) { return; }
    if (gPlayer.y < enY[i]) { return; }
    halfw = LASER_W / 2 + gPlayer.r;
    if (iabsv(gPlayer.x - enX[i]) > halfw) { return; }
    enLCd[i] = LASER_HIT_CD;
    HurtPlayer(DMG_LASER);
}

void Collisions()
{
    int i;
    int j;
    // 自机子弹 → 敌机(伤害取自**这一发子弹**的 `pbD`,不是固定 1)

    // Player bullets → enemies (the damage comes from **this bullet's** `pbD`, not a fixed 1)

    i = 0;
    while (i < PBUL_MAX)
    {
        if (pbLive[i] != 0)
        {
            j = 0;
            while (j < ENEMY_MAX)
            {
                if (pbLive[i] != 0 && enLive[j] != 0)
                {
                    if (hitCirc(pbX[i], pbY[i], pbR[i], enX[j], enY[j], enR[j]) != 0)
                    {
                        pbLive[i] = 0;
                        DamageEnemy(j, pbD[i]);
                    }
                }
                j = j + 1;
            }
        }
        i = i + 1;
    }
    // 敌弹 → 自机

    // Enemy bullets → the player

    i = 0;
    while (i < EBUL_MAX)
    {
        if (ebLive[i] != 0)
        {
            if (gPlayer.invT == 0)
            {
                if (hitCirc(ebX[i], ebY[i], ebR[i], gPlayer.x, gPlayer.y, gPlayer.r) != 0)
                {
                    ebLive[i] = 0;
                    HurtPlayer(EBulDamage(ebKind[i]));
                }
            }
        }
        i = i + 1;
    }
    // BOSS 激光 → 自机(按拍持续扣,但有节流)

    // The BOSS laser → the player (it bites every tick, but throttled)

    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] != 0 && enType[i] == BOSS_KIND0 + 1) { LaserHit(i); }
        i = i + 1;
    }
    // 撞机 → 双方都掉血(玩家掉 20 点、敌机掉一大截血)。**不这么做**的话"贴脸"就是最强战术

    // Ramming → both take damage (the player loses 20 and the enemy loses a big chunk). **Without this**, hugging an enemy would be the strongest tactic

    j = 0;
    while (j < ENEMY_MAX)
    {
        if (enLive[j] != 0)
        {
            if (hitCirc(enX[j], enY[j], enR[j], gPlayer.x, gPlayer.y, gPlayer.r) != 0)
            {
                if (gPlayer.invT == 0) { HurtPlayer(DMG_RAM); }
                if (enLive[j] != 0 && IsBossType(enType[j]) == 0) { DamageEnemy(j, 2); }
            }
        }
        j = j + 1;
    }
}

// ════════════════════════════════════════════════════════════════════

// 一局的生命周期

// Life cycle of one round

// ════════════════════════════════════════════════════════════════════


void PoolsClear()
{
    int i;
    i = 0;
    while (i < ENEMY_MAX) { enLive[i] = 0; i = i + 1; }
    i = 0;
    while (i < PBUL_MAX) { pbLive[i] = 0; i = i + 1; }
    i = 0;
    while (i < EBUL_MAX) { ebLive[i] = 0; i = i + 1; }
    i = 0;
    while (i < BOOM_MAX) { bmLive[i] = 0; i = i + 1; }
    i = 0;
    while (i < ITEM_MAX) { itLive[i] = 0; i = i + 1; }
}

void GameReset()
{
    gScore = 0;
    gTick = 0;
    gState = ST_PLAY;
    gFlash = 0;
    gBannerT = 70;
    gDragOn = 0;
    gBeepCd = 0;
    PoolsClear();
    gPlayer.Reset();
    BackInit();
    WaveStart(1);
}

// 一拍逻辑。**背景先走**(结束画面上也走),再走游戏本身

// One tick of logic. **The background moves first** (even on the end screen), then the game itself

void Tick()
{
    int i;
    gTick = gTick + 1;
    if (gBeepCd > 0) { gBeepCd = gBeepCd - 1; }
    if (gFlash > 0) { gFlash = gFlash - 1; }
    if (gBannerT > 0) { gBannerT = gBannerT - 1; }

    BackTick();

    i = 0;
    while (i < BOOM_MAX)
    {
        if (bmLive[i] != 0)
        {
            bmT[i] = bmT[i] + 1;
            if (bmT[i] >= bmMax[i]) { bmLive[i] = 0; }
        }
        i = i + 1;
    }

    if (gState != ST_PLAY) { return; }

    gPlayer.Step();

    // 自动开火:**按火力等级定间隔**(W1/W2 十拍、W3 九拍、W4 十一拍)

    // Auto fire: **the interval follows the weapon level** (10 ticks at W1/W2, 9 at W3, 11 at W4)

    if (gPlayer.fireT <= 0)
    {
        PlayerFire();
        gPlayer.fireT = FireGap();
    }

    i = 0;
    while (i < PBUL_MAX)
    {
        if (pbLive[i] != 0)
        {
            pbY[i] = pbY[i] - 14;
            pbX[i] = pbX[i] + pbVX[i];
            if (pbY[i] < -30) { pbLive[i] = 0; }
            if (pbX[i] < -20 || pbX[i] > gSw + 20) { pbLive[i] = 0; }
        }
        i = i + 1;
    }

    i = 0;
    while (i < EBUL_MAX)
    {
        if (ebLive[i] != 0)
        {
            ebT[i] = ebT[i] + 1;
            // 追踪弹:**每 10 拍**重新朝自机瞄一次(慢速追踪 ⇒ 玩家加速跑开或侧移就能甩掉)

            // Homing bullets: **every 10 ticks** they re-aim at the craft (slow tracking ⇒ the player can outrun or sidestep them)

            if (ebKind[i] == EK_HOME)
            {
                if (ebT[i] % 10 == 0)
                {
                    AimAt(ebX[i], ebY[i], gPlayer.x, gPlayer.y, 4);
                    ebVX[i] = gAimVX;
                    ebVY[i] = gAimVY;
                }
                // 追踪弹有个寿命上限:否则它会在屏幕里绕圈绕到天荒地老

                // Homing bullets have a lifetime cap; otherwise they circle the screen forever

                if (ebT[i] > 260) { ebLive[i] = 0; }
            }
            ebX[i] = ebX[i] + ebVX[i];
            ebY[i] = ebY[i] + ebVY[i];
            if (ebLive[i] != 0)
            {
                if (ebY[i] > gSh + 30 || ebY[i] < -60 || ebX[i] < -40 || ebX[i] > gSw + 40)
                {
                    ebLive[i] = 0;
                }
            }
        }
        i = i + 1;
    }

    EnemiesTick();
    Collisions();
    ItemsTick();
    WaveTick();
}

// ════════════════════════════════════════════════════════════════════

// 绘制

// Drawing

// ════════════════════════════════════════════════════════════════════


// 血条颜色随余量换档:绿 → 黄 → 红(阈值 60% / 30%)

// The health bar changes colour by how much is left: green → yellow → red (thresholds 60% and 30%)

int HpColor(int hp)
{
    if (hp > HP_MAX * 60 / 100) { return C_HP_HI; }
    if (hp > HP_MAX * 30 / 100) { return C_HP_MID; }
    return C_HP_LOW;
}

void DrawHud()
{
    int i;
    int x;
    int hp;
    int bw;
    // 顶栏底板

    // The top plate

    ui_2d_rect(0, 0, gSw, 68, C_HUD_BG, 1, 0, 0);

    // 左上:**血条** + 血量数字(血条颜色随余量换档,一眼看得出还剩多少)

    // Top left: the **health bar** plus the number (its colour shifts with the amount left, so the state reads at a glance)

    hp = gPlayer.hp;
    ui_2d_rect(8, 12, 120, 14, C_BAR_BG, 1, 0, 3);
    if (hp > 0)
    {
        ui_2d_rect(10, 14, 116 * hp / HP_MAX, 10, HpColor(hp), 1, 0, 2);
    }
    ui_2d_text(134, 25, numstr(hp), HpColor(hp), 14, VML_ANCHOR_LEFT);

    // 中间:得分

    // Centre: the score

    ui_2d_text(gSw / 2, 16, gLang == 0 ? "得分" : "SCORE", C_TEXT_DIM, 10, VML_ANCHOR_CENTER);
    ui_2d_text(gSw / 2, 34, numstr(gScore), C_TEXT, 16, VML_ANCHOR_CENTER);

    // 左下:火力等级(W1..W4)

    // Bottom left: the weapon level (W1..W4)

    ui_2d_text(8, 58, gLang == 0 ? "火力" : "POW", C_TEXT_DIM, 11, VML_ANCHOR_LEFT);
    ui_2d_text(46, 59, wstr(gPlayer.wlev), C_GOLD, 14, VML_ANCHOR_LEFT);

    // 右下:最高分

    // Bottom right: the high score

    ui_2d_text(gSw - 8, 46, gLang == 0 ? "最高" : "BEST", C_TEXT_DIM, 10, VML_ANCHOR_RIGHT);
    ui_2d_text(gSw - 8, 60, numstr(gHi), C_GOLD, 13, VML_ANCHOR_RIGHT);

    // 右上角:剩余命数(用小飞机图标,比"×3"更一眼看得出)

    // Top right: lives left (drawn as small craft icons, which read faster than "×3")

    i = 0;
    while (i < gPlayer.lives && i < 5)
    {
        x = gSw - 14 - i * 17;
        ui_2d_rect(x - 7, 15, 14, 4, 0xFF4FA8E0, 1, 0, 1);
        ui_2d_rect(x - 2, 9, 4, 16, 0xFFBEE4FF, 1, 0, 1);
        i = i + 1;
    }

    // 波次提示(大字横幅,几拍后自己消失)

    // The wave banner (large text that fades out on its own after a few ticks)

    if (gBannerT > 0 && gState == ST_PLAY)
    {
        if (IsBossWave(gWave) != 0)
        {
            ui_2d_text(gSw / 2, gSh * 40 / 100, gLang == 0 ? "警 报 ! B O S S" : "W A R N I N G  -  B O S S", C_WARN, 20, VML_ANCHOR_CENTER);
        }
        else
        {
            ui_2d_text(gSw / 2, gSh * 40 / 100, gLang == 0 ? "第" : "WAVE", C_GOLD, 20, VML_ANCHOR_CENTER);
            ui_2d_text(gSw / 2, gSh * 40 / 100 + 26, numstr(gWave), C_GOLD, 26, VML_ANCHOR_CENTER);
            ui_2d_text(gSw / 2, gSh * 40 / 100 + 52, gLang == 0 ? "共 6 波" : "OF 6", C_TEXT_DIM, 12, VML_ANCHOR_CENTER);
        }
    }

    // BOSS 血条:**只在 BOSS 在场时出现**,位置压在顶栏下沿

    // The BOSS health bar: **only while the BOSS is present**, tucked under the top plate

    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] != 0 && IsBossType(enType[i]) != 0)
        {
            hp = enHP[i] * 100 / enHPMax[i];
            bw = gSw - 48;
            ui_2d_rect(24, 76, bw, 10, C_BAR_BG, 1, 0, 3);
            if (hp > 0)
            {
                ui_2d_rect(26, 78, (bw - 4) * hp / 100, 6, C_BAR_FILL, 1, 0, 2);
            }
            ui_2d_text(gSw / 2, 99, "BOSS", C_WARN, 11, VML_ANCHOR_CENTER);
        }
        i = i + 1;
    }
}

// 三种 BOSS 的**名字**(打的是哪一个要看得出来)

// The **names** of the three BOSSes (which one you are fighting should be readable)

char* BossName(int k)
{
    if (k == 0) { return gLang == 0 ? "扇弹型" : "FAN"; }
    if (k == 1) { return gLang == 0 ? "激光型" : "LASER"; }
    return gLang == 0 ? "追踪弹幕型" : "HOMING";
}

void DrawBossTag()
{
    int i;
    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] != 0 && IsBossType(enType[i]) != 0)
        {
            ui_2d_text(gSw / 2, 112, BossName(enType[i] - BOSS_KIND0), C_TEXT_DIM, 11, VML_ANCHOR_CENTER);
        }
        i = i + 1;
    }
}

// 结束画面的**图形标志物**:通关画大奖杯、命尽画墓碑。

// The **graphic marker** on the end screen: a big trophy when cleared, a tombstone when out of lives.

//

// ⚠ **摆位**:都放在 `屏高 23%` 处 —— 也就是**大标题(38%)的上方**那一段留白里。

// ⚠ **Placement**: both sit at **23% of the screen height**, i.e. in the blank band **above the big title (38%)**.

//   为什么不用"得分区下方":那里已经排了 61% 的得分、52% 的波次、82% 的"点击重新开始"三块,

//   Why not "below the score area": that region already holds three blocks — the score at 61%, the wave at 52% and "tap to play again" at 82%,

//   再塞一个 130px 高的家伙进去就得改那三块的坐标,而那些坐标是**旧路径的一部分**,能不动就不动。

//   and squeezing a 130px-tall object in would force those three to move, and those coordinates are **part of the old path**, so leave them alone if at all possible.

//   23% 这一段本来是空的(顶栏只到 68px ≈ 9%),放这里既不挤,又正好在标题上一眼看到。

//   The 23% band is empty as it stands (the HUD plate ends at 68px, about 9%), so nothing is crowded and the marker reads first, right above the title.

//

// ⚠ 两个函数都用 `gSw` 推尺寸(不写死像素)⇒ 换个分辨率的屏幕不会变成"小图标"或者"顶出屏幕"。

// ⚠ Both functions derive their size from `gSw` (no hard-coded pixels) ⇒ on another resolution it will not shrink into a tiny icon or run off screen.


// 通关:大奖杯(宽 ≈ 屏宽 1/3)。**高光每 60 拍扫过一次 + 整体微微上浮**,

// Cleared: a big trophy (about a third of the screen wide). **The highlight sweeps across every 60 ticks and the whole thing bobs gently**,

// 两样都**只由 `gTick` 推相位**,不存任何新状态。

// and both are driven **purely by `gTick`'s phase** with no new state stored.

void DrawTrophy()
{
    int cx;
    int cy;
    int w;
    int h;
    int bowlCY;
    int bowlRX;
    int bowlRY;
    int hx;
    int hw;
    int ph;
    cx = gSw / 2;
    cy = gSh * 23 / 100;
    w = gSw / 3;
    h = w;
    // 微微上浮:±3px 的正弦(`isin` 返回 -1000..1000)

    // A gentle float: a ±3px sine (`isin` returns -1000..1000)

    cy = cy + isin((gTick * 4) % 360) * 3 / 1000;
    bowlRX = w * 30 / 100;
    bowlRY = h * 26 / 100;
    bowlCY = cy - h * 18 / 100;
    // 杯耳先画(在下层,被杯身压住一截才像"长在杯子上")

    // The handles go first (they are underneath, and being overlapped by the cup is what makes them look attached to it)

    ui_2d_circle(cx - w * 38 / 100, cy - h * 24 / 100, w * 11 / 100, C_TROPHY_DK, 0, 9);
    ui_2d_circle(cx + w * 38 / 100, cy - h * 24 / 100, w * 11 / 100, C_TROPHY_DK, 0, 9);
    // 杯口横档 + 杯身(一个椭圆当碗,下缘被杯柄接住)

    // The rim bar and the cup (an ellipse as the bowl, its lower edge caught by the stem)

    ui_2d_rect(cx - w * 32 / 100, bowlCY - bowlRY - 5, w * 64 / 100, 9, C_TROPHY, 1, 0, 3);
    ui_2d_ellipse(cx, bowlCY, bowlRX, bowlRY, C_TROPHY, 1, 0);
    // 杯柄 + 两级底座

    // The stem and the two-tier base

    ui_2d_rect(cx - w * 6 / 100, cy + h * 2 / 100, w * 12 / 100, h * 22 / 100, C_TROPHY_DK, 1, 0, 2);
    ui_2d_rect(cx - w * 26 / 100, cy + h * 24 / 100, w * 52 / 100, h * 7 / 100, C_TROPHY, 1, 0, 2);
    ui_2d_rect(cx - w * 34 / 100, cy + h * 31 / 100, w * 68 / 100, h * 9 / 100, C_TROPHY_DK, 1, 0, 3);
    // 杯身上的**竖高光**(静态那道)与**扫过的那道**(每 60 拍从左到右一趟)

    // The **static vertical highlight** on the cup plus the **sweeping one** (one left-to-right pass every 60 ticks)

    ui_2d_rect(cx - bowlRX / 2, bowlCY - bowlRY + 6, 5, bowlRY * 2 - 12, 0x50FFFFFF, 1, 0, 2);
    ph = gTick % 60;
    hw = bowlRX - 8;
    hx = cx - hw + (2 * hw) * ph / 60;
    ui_2d_rect(hx, bowlCY - bowlRY + 6, 5, bowlRY * 2 - 12, 0xA0FFFFFF, 1, 0, 2);
}

// 命尽:墓碑(石身 + 圆顶 + 碑面十字 + 脚下土堆)。**不加动画** —— 墓碑动起来很怪。

// Out of lives: a tombstone (stone body, round top, a cross on the face, a mound at its foot). **No animation** — a moving tombstone is just wrong.

void DrawTombstone()
{
    int cx;
    int cy;
    int w;
    int top;
    cx = gSw / 2;
    cy = gSh * 23 / 100;
    w = gSw / 4;
    // 圆顶与碑身**同色** ⇒ 看起来是一块整石,而不是"石碑上顶了个球"

    // The round top and the body share one colour ⇒ it reads as a single block of stone rather than "a ball balanced on a slab"

    top = cy - w * 3 / 4;
    ui_2d_ellipse(cx, top, w / 2, w / 4, C_STONE, 1, 0);
    ui_2d_rect(cx - w / 2, top, w, w * 5 / 4, C_STONE, 1, 0, 5);
    // 内嵌面板(比石身暗一档 ⇒ 一眼看出"碑面"是凹进去的)

    // The inset panel (one shade darker than the body ⇒ the face reads as recessed)

    ui_2d_rect(cx - w / 2 + 8, top + 16, w - 16, w * 5 / 4 - 26, C_STONE_DK, 1, 0, 4);
    // 碑面刻的十字

    // The cross engraved on the face

    ui_2d_rect(cx - 6, top + 30, 12, w / 2, C_STONE_ENG, 1, 0, 2);
    ui_2d_rect(cx - w / 4, top + 44, w / 2, 12, C_STONE_ENG, 1, 0, 2);
    // 脚下的土堆(把碑"钉"在地上,不画的话像飘在空中)

    // The mound at its foot (it plants the stone on the ground; without it, it looks like it is floating)

    ui_2d_rect(cx - w * 70 / 100, top + w * 5 / 4, w * 140 / 100, 14, C_GROUND, 1, 0, 6);
}

void DrawEndScreen()
{
    // 半透明压暗整屏(不压暗的话背景星星会和文字抢注意力)

    // Darken the whole screen with a translucent plate (without it the background stars compete with the text for attention)

    ui_2d_rect(0, 0, gSw, gSh, 0xC0000814, 1, 0, 0);
    if (gState == ST_CLEAR)
    {
        // 标志物在**标题上方**:先看到奖杯/墓碑,再读到"通关/结束"

        // The marker sits **above the title**: you see the trophy or the tombstone first, then read "cleared / over"

        DrawTrophy();
        ui_2d_text(gSw / 2, gSh * 38 / 100, gLang == 0 ? "通 关 !" : "CLEARED !", C_GOLD, 30, VML_ANCHOR_CENTER);
        ui_2d_text(gSw / 2, gSh * 46 / 100, gLang == 0 ? "三种 BOSS 全部击落" : "All three BOSSes down", C_TEXT, 14, VML_ANCHOR_CENTER);
    }
    else
    {
        DrawTombstone();
        ui_2d_text(gSw / 2, gSh * 38 / 100, gLang == 0 ? "游 戏 结 束" : "GAME OVER", C_WARN, 30, VML_ANCHOR_CENTER);
        ui_2d_text(gSw / 2, gSh * 46 / 100, gLang == 0 ? "飞机全部损失" : "All craft lost", C_TEXT_DIM, 14, VML_ANCHOR_CENTER);
    }
    ui_2d_text(gSw / 2, gSh * 52 / 100, gLang == 0 ? "打到第" : "WAVE", C_TEXT_DIM, 12, VML_ANCHOR_CENTER);
    ui_2d_text(gSw / 2, gSh * 52 / 100 + 20, numstr(gWave), C_TEXT, 16, VML_ANCHOR_CENTER);
    ui_2d_text(gSw / 2, gSh * 61 / 100, gLang == 0 ? "本局得分" : "SCORE", C_TEXT_DIM, 13, VML_ANCHOR_CENTER);
    ui_2d_text(gSw / 2, gSh * 61 / 100 + 30, numstr(gScore), C_TEXT, 26, VML_ANCHOR_CENTER);
    if (gScore > gHi)
    {
        ui_2d_text(gSw / 2, gSh * 61 / 100 + 52, gLang == 0 ? "新纪录!" : "NEW RECORD!", C_GOLD, 14, VML_ANCHOR_CENTER);
    }
    else
    {
        ui_2d_text(gSw / 2, gSh * 61 / 100 + 52, gLang == 0 ? "最高分" : "BEST", C_TEXT_DIM, 12, VML_ANCHOR_CENTER);
        ui_2d_text(gSw / 2, gSh * 61 / 100 + 70, numstr(gHi), C_GOLD, 15, VML_ANCHOR_CENTER);
    }
    ui_2d_text(gSw / 2, gSh * 82 / 100, gLang == 0 ? "点 击 屏 幕 重 新 开 始" : "TAP TO PLAY AGAIN", C_TEXT, 16, VML_ANCHOR_CENTER);
}

// 激光柱(预警 + 发射)。三种 BOSS 里只有激光型走这里,**画在最上层**(在自机之上:

// The laser column (warning and firing). Only the laser BOSS draws here, and it goes **on top of everything** (above the craft:

// 它是一击必躲的东西,被自机盖住就成了"看不见的伤害")。

// it is a thing you must see coming, and covering it with the craft would make it "invisible damage").

void DrawLaser()
{
    int i;
    int y0;
    int len;
    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] != 0 && enType[i] == BOSS_KIND0 + 1 && enLPh[i] != 0)
        {
            y0 = enY[i] + 44;
            len = gSh - y0;
            if (enLPh[i] == 1)
            {
                // 充能:**闪烁的细预警线**(闪得越快到"该躲了"越明显)

                // Charging: a **blinking thin warning line** (the faster it blinks, the more it says "move now")

                if ((gTick / 4) % 2 == 0)
                {
                    ui_2d_rect(enX[i] - 1, y0, 3, len, 0xFFFF8080, 1, 0, 0);
                }
                ui_2d_circle(enX[i], enY[i] + 34, 10, 0xFFFF6060, 1, 0);
            }
            else
            {
                ui_2d_rect(enX[i] - LASER_W / 2 - 6, y0, LASER_W + 12, len, 0x40FF6060, 1, 0, 0);
                ui_2d_rect(enX[i] - LASER_W / 2, y0, LASER_W, len, 0xFFFF9060, 1, 0, 0);
                ui_2d_rect(enX[i] - 5, y0, 10, len, 0xFFFFFFFF, 1, 0, 0);
            }
        }
        i = i + 1;
    }
}

void GameDraw()
{
    int i;
    int r;
    ui_2d_clear(C_SPACE);
    if (gFlash > 0) { ui_2d_rect(0, 0, gSw, gSh, 0x40FF8080, 1, 0, 0); }
    BackDraw();

    // 自机子弹(先画:在敌机下面,看着像"打进去了")

    // Player bullets first (under the enemies, which reads as "going into" them)

    i = 0;
    while (i < PBUL_MAX)
    {
        if (pbLive[i] != 0)
        {
            if (gPBulBid > 0) { ui_2d_draw_block(gPBulBid, pbX[i], pbY[i], 1000, 1000, 0); }
            else { ui_2d_rect(pbX[i] - 2, pbY[i] - 9, 4, 18, 0xFFFFE070, 1, 0, 2); }
        }
        i = i + 1;
    }

    i = 0;
    while (i < ENEMY_MAX)
    {
        if (enLive[i] != 0) { EnemyDraw(i); }
        i = i + 1;
    }

    // 掉落物画在自机**之前**:被自机盖住一点,才有"接住了"的感觉

    // Items go **before** the craft: being partly covered by it is what makes catching one feel like a catch

    i = 0;
    while (i < ITEM_MAX)
    {
        if (itLive[i] != 0) { ItemDraw(i); }
        i = i + 1;
    }

    if (gState == ST_PLAY) { gPlayer.Draw(); }

    // 敌弹画在自机之后:**必须看得见**,被自机盖住就成了"看不见的子弹"

    // Enemy bullets come after the player craft: they **must stay visible**, and being covered by the craft would make them "invisible bullets"

    i = 0;
    while (i < EBUL_MAX)
    {
        if (ebLive[i] != 0)
        {
            if (ebKind[i] == EK_BIG)
            {
                if (gEBulBigBid > 0) { ui_2d_draw_block(gEBulBigBid, ebX[i], ebY[i], 1000, 1000, 0); }
                else { ui_2d_circle(ebX[i], ebY[i], ebR[i], 0xFFC060FF, 1, 0); }
            }
            if (ebKind[i] == EK_FAST)
            {
                if (gEBulFastBid > 0) { ui_2d_draw_block(gEBulFastBid, ebX[i], ebY[i], 1000, 1000, 0); }
                else { ui_2d_rect(ebX[i] - 2, ebY[i] - 8, 4, 16, 0xFF60E0FF, 1, 0, 2); }
            }
            if (ebKind[i] == EK_AIM)
            {
                if (gEBulAimBid > 0) { ui_2d_draw_block(gEBulAimBid, ebX[i], ebY[i], 1000, 1000, 0); }
                else { ui_2d_circle(ebX[i], ebY[i], ebR[i], 0xFFFFD040, 1, 0); }
            }
            if (ebKind[i] == EK_RING)
            {
                if (gEBulRingBid > 0) { ui_2d_draw_block(gEBulRingBid, ebX[i], ebY[i], 1000, 1000, 0); }
                else { ui_2d_circle(ebX[i], ebY[i], ebR[i], 0xFFB070FF, 1, 0); }
            }
            if (ebKind[i] == EK_HOME)
            {
                if (gEBulHomeBid > 0) { ui_2d_draw_block(gEBulHomeBid, ebX[i], ebY[i], 1000, 1000, 0); }
                else { ui_2d_circle(ebX[i], ebY[i], ebR[i], 0xFFFF80C0, 1, 0); }
            }
            if (ebKind[i] == EK_SMALL)
            {
                if (gEBulBid > 0) { ui_2d_draw_block(gEBulBid, ebX[i], ebY[i], 1000, 1000, 0); }
                else { ui_2d_circle(ebX[i], ebY[i], ebR[i], 0xFFFF7040, 1, 0); }
            }
        }
        i = i + 1;
    }

    // 爆炸:三层同心圆,半径随 `bmT` 长大 —— 由内到外是"白热 → 主色 → 余烬"

    // Explosions: three concentric rings whose radius grows with `bmT` — white hot → main colour → embers from the inside out

    i = 0;
    while (i < BOOM_MAX)
    {
        if (bmLive[i] != 0)
        {
            r = bmSize[i] + bmT[i] * 2;
            ui_2d_circle(bmX[i], bmY[i], r + 4, 0x80FF8030, 1, 0);
            if (r > 3) { ui_2d_circle(bmX[i], bmY[i], r, bmCol[i], 1, 0); }
            if (r > 8) { ui_2d_circle(bmX[i], bmY[i], r - 6, 0xFFFFF0B0, 1, 0); }
        }
        i = i + 1;
    }

    if (gState == ST_PLAY) { DrawLaser(); }
    DrawHud();
    if (gState == ST_PLAY) { DrawBossTag(); }
    if (gState != ST_PLAY) { DrawEndScreen(); }
    ui_2d_present();
}

// ════════════════════════════════════════════════════════════════════

// 输入

// Input

// ════════════════════════════════════════════════════════════════════


void KeyDown(int k)
{
    if (k == VML_KEY_ESCAPE) { gState = -1; return; }
    if (gState != ST_PLAY)
    {
        // 结束画面:任意键重开(键盘用户的回车/空格)

        // End screen: any key restarts (Enter/Space for keyboard users)

        GameReset();
        return;
    }
    // 手动开火:**补一轮**(自动开火照旧),不是"改成手动"

    // Manual fire: **one extra volley** (auto fire stays as it is), not "switch to manual"

    if (k == VML_KEY_ENTER || k == VML_KEY_SPACE || k == VML_KEY_PAD_A)
    {
        PlayerFire();
        gPlayer.fireT = FireGap();
    }
}

// 按下:记住手指起点,**不动飞机** —— 一按就把飞机挪到指尖底下是最常见的手机射击手感事故

// Press: record where the finger started and **do not move the craft** — teleporting it under the fingertip on press is the classic mobile-shooter feel bug

void TouchDown()
{
    if (gState != ST_PLAY)
    {
        GameReset();
        return;
    }
    gDragOn = 1;
    gDragX = ui_msg_get_a();
    gDragY = ui_msg_get_b();
}

// 拖动:按**位移**移动飞机(相对拖动),并夹在可行区域内

// Drag: move the craft by the **displacement** (relative dragging) and clamp it to the playable area

void TouchMove()
{
    int px;
    int py;
    if (gState != ST_PLAY) { return; }
    if (gDragOn == 0)
    {
        // 直接从"按住"开始的拖动(有些宿主只发 MOVE 不发 DOWN):把这一条当起点,别当成位移

        // A drag that begins straight at "held" (some hosts send MOVE without DOWN): treat this one as the origin, not as displacement

        gDragOn = 1;
        gDragX = ui_msg_get_a();
        gDragY = ui_msg_get_b();
        return;
    }
    px = ui_msg_get_a();
    py = ui_msg_get_b();
    gPlayer.MoveBy(px - gDragX, py - gDragY);
    gDragX = px;
    gDragY = py;
}

// ════════════════════════════════════════════════════════════════════

// 图块

// Blocks

//

// **开窗之后建一次**(块是宿主那边的资源,没有场景建不起来),之后一直贴。

// **Built once after the window opens** (blocks are a host-side resource and cannot be created without a scene), then stamped forever.

// 每帧 `ui_2d_create_block` 会在 128 帧后把块表撑满(上限 128 个槽),

// Calling `ui_2d_create_block` every frame would fill the block table after 128 frames (the cap is 128 slots),

// 那时 `ui_2d_create_block` 一律返回 0 —— 画面会**悄悄**退回逐帧画。

// at which point `ui_2d_create_block` always returns 0 — and the picture would **quietly** fall back to per-frame drawing.

//

// ⚠ **顺序不能改**:句柄号就是按这里的调用顺序发的,而 `--trace-draw` 只记 `blkdraw handle=N`

// ⚠ **The order must not change**: handle numbers are handed out in the order of the calls here, and `--trace-draw` records only `blkdraw handle=N`

//   ⇒ 顺序一乱,"数一数哪种敌机出现过"这类测试就全部错位(映射表见文件头)。

//   ⇒ any reshuffle silently invalidates tests like "count which enemy types appeared" (the mapping table is in the file header).

// ════════════════════════════════════════════════════════════════════


// 1 自机:36×34,中心在局部 (18,17)

// 1 the craft: 36×34 with its centre at local (18,17)

void MakePlayerBlock()
{
    gPlayerBid = ui_2d_create_block(36, 34, 0);
    ui_2d_rect(2, 16, 32, 7, 0xFF2E7BD6, 1, 0, 3);      // 主翼

    // Main wing

    ui_2d_rect(13, 3, 10, 28, 0xFF9ED2FF, 1, 0, 4);     // 机身

    // Fuselage

    ui_2d_ellipse(18, 7, 4, 7, 0xFFFFFFFF, 1, 0);       // 机头

    // Nose

    ui_2d_ellipse(18, 15, 4, 6, 0xFF4FC3F7, 1, 0);      // 座舱

    // Cockpit

    ui_2d_rect(9, 26, 18, 6, 0xFF1E5AA0, 1, 0, 2);      // 尾翼

    // Tail plane

    gPlayerBid = ui_2d_end_block();
}

void MakeEnemyBlockScout()
{
    // 2 侦察机(红):30×26,中心 (15,13)

    // 2 scout (red): 30×26, centre (15,13)

    gEnemyBid[ET_SCOUT] = ui_2d_create_block(30, 26, 0);
    ui_2d_rect(0, 8, 30, 7, 0xFFB03627, 1, 0, 2);
    ui_2d_rect(10, 1, 10, 22, 0xFFE0533C, 1, 0, 4);
    ui_2d_ellipse(15, 10, 4, 5, 0xFFFFD060, 1, 0);
    ui_2d_ellipse(15, 23, 3, 3, 0xFFFFA060, 1, 0);
    gEnemyBid[ET_SCOUT] = ui_2d_end_block();
}

void MakeEnemyBlockSweep()
{
    // 3 横扫机(紫):34×22,中心 (17,11)

    // 3 sweeper (purple): 34×22, centre (17,11)

    gEnemyBid[ET_SWEEP] = ui_2d_create_block(34, 22, 0);
    ui_2d_rect(0, 8, 34, 6, 0xFF6A3FB0, 1, 0, 2);
    ui_2d_ellipse(17, 11, 10, 8, 0xFF8A5CD6, 1, 0);
    ui_2d_ellipse(17, 9, 4, 4, 0xFFFFD060, 1, 0);
    ui_2d_rect(2, 3, 6, 5, 0xFF4A2A80, 1, 0, 1);
    ui_2d_rect(26, 3, 6, 5, 0xFF4A2A80, 1, 0, 1);
    gEnemyBid[ET_SWEEP] = ui_2d_end_block();
}

void MakeEnemyBlockDive()
{
    // 4 斜插机(青绿):28×28,中心 (14,14)

    // 4 diver (teal): 28×28, centre (14,14)

    gEnemyBid[ET_DIVE] = ui_2d_create_block(28, 28, 0);
    ui_2d_line(1, 20, 27, 8, 0xFF2A8A72, 4);
    ui_2d_line(1, 8, 27, 20, 0xFF2A8A72, 4);
    ui_2d_ellipse(14, 14, 8, 12, 0xFF3FB59A, 1, 0);
    ui_2d_ellipse(14, 7, 3, 5, 0xFFBFFFE8, 1, 0);
    ui_2d_ellipse(14, 17, 4, 4, 0xFFFFD060, 1, 0);
    gEnemyBid[ET_DIVE] = ui_2d_end_block();
}

void MakeEnemyBlockSnake()
{
    // 5 蛇行机(黄绿):34×24,中心 (17,12) —— 三个菱形串成的"蛇节"

    // 5 snake (yellow-green): 34×24, centre (17,12) — three diamonds strung into a "snake"

    gEnemyBid[ET_SNAKE] = ui_2d_create_block(34, 24, 0);
    ui_2d_ellipse(6, 12, 6, 9, 0xFF3E8A28, 1, 0);
    ui_2d_ellipse(17, 12, 7, 11, 0xFF7FE05A, 1, 0);
    ui_2d_ellipse(28, 12, 6, 9, 0xFF3E8A28, 1, 0);
    ui_2d_ellipse(17, 9, 3, 5, 0xFFE8FFD0, 1, 0);
    ui_2d_ellipse(17, 18, 3, 3, 0xFFFFD060, 1, 0);
    gEnemyBid[ET_SNAKE] = ui_2d_end_block();
}

void MakeEnemyBlockTurret()
{
    // 6 炮台机(钢灰):42×36,中心 (21,18) —— 厚装甲 + 两根朝下的炮管

    // 6 turret (steel grey): 42×36, centre (21,18) — thick armour plus two downward barrels

    gEnemyBid[ET_TURRET] = ui_2d_create_block(42, 36, 0);
    ui_2d_rect(0, 12, 42, 10, 0xFF4A5260, 1, 0, 3);
    ui_2d_rect(6, 4, 30, 24, 0xFF8A94A8, 1, 0, 6);
    ui_2d_ellipse(21, 16, 11, 9, 0xFFC8D0DC, 1, 0);
    ui_2d_ellipse(21, 16, 5, 4, 0xFFFF6060, 1, 0);
    ui_2d_rect(11, 26, 7, 10, 0xFF3A4048, 1, 0, 1);
    ui_2d_rect(24, 26, 7, 10, 0xFF3A4048, 1, 0, 1);
    gEnemyBid[ET_TURRET] = ui_2d_end_block();
}

void MakeEnemyBlockKami()
{
    // 7 自杀机(洋红):26×30,中心 (13,15) —— 尖锐的箭头,一看就知道要撞过来

    // 7 kamikaze (magenta): 26×30, centre (13,15) — a sharp arrowhead, obviously a rammer

    gEnemyBid[ET_KAMI] = ui_2d_create_block(26, 30, 0);
    ui_2d_ellipse(13, 17, 9, 10, 0xFFC02A80, 1, 0);
    ui_2d_rect(4, 2, 5, 20, 0xFFFF60A0, 1, 0, 2);
    ui_2d_rect(17, 2, 5, 20, 0xFFFF60A0, 1, 0, 2);
    ui_2d_ellipse(13, 22, 6, 6, 0xFFFF90C0, 1, 0);
    ui_2d_ellipse(13, 24, 3, 3, 0xFFFFF0B0, 1, 0);
    gEnemyBid[ET_KAMI] = ui_2d_end_block();
}

void MakeBossBlockFan()
{
    // 8 BOSS 扇弹型(红):120×96,中心 (60,48)

    // 8 fan BOSS (red): 120×96, centre (60,48)

    gBossBid[0] = ui_2d_create_block(120, 96, 0);
    ui_2d_rect(2, 42, 116, 14, 0xFF5E1A1A, 1, 0, 5);        // 主翼

    // Main wing

    ui_2d_ellipse(60, 46, 42, 32, 0xFF8E2A2A, 1, 0);        // 外壳

    // Hull

    ui_2d_rect(24, 22, 72, 46, 0xFFC04040, 1, 0, 12);       // 装甲板

    // Armour plate

    ui_2d_ellipse(60, 42, 16, 13, 0xFFFFD060, 1, 0);        // 指挥舱

    // Command pod

    ui_2d_ellipse(60, 42, 7, 6, 0xFFFF6030, 1, 0);
    ui_2d_rect(16, 74, 11, 18, 0xFF444A58, 1, 0, 2);        // 炮管

    // Gun barrels

    ui_2d_rect(93, 74, 11, 18, 0xFF444A58, 1, 0, 2);
    ui_2d_circle(28, 30, 5, 0xFFFF6060, 1, 0);              // 警示灯

    // Warning lights

    ui_2d_circle(92, 30, 5, 0xFFFF6060, 1, 0);
    gBossBid[0] = ui_2d_end_block();
}

void MakeBossBlockLaser()
{
    // 9 BOSS 激光型(紫蓝):110×96,中心 (55,48) —— 下腹一整块**聚焦镜**

    // 9 laser BOSS (violet blue): 110×96, centre (55,48) — one big **focusing lens** on the underside

    gBossBid[1] = ui_2d_create_block(110, 96, 0);
    ui_2d_rect(4, 40, 102, 16, 0xFF2A2460, 1, 0, 5);
    ui_2d_ellipse(55, 44, 40, 30, 0xFF4A3AA0, 1, 0);
    ui_2d_rect(22, 20, 66, 44, 0xFF6E58D0, 1, 0, 12);
    ui_2d_ellipse(55, 38, 18, 14, 0xFFB8A0FF, 1, 0);
    ui_2d_ellipse(55, 38, 8, 7, 0xFFFFFFFF, 1, 0);
    ui_2d_ellipse(55, 76, 16, 12, 0xFF302880, 1, 0);        // 聚焦镜

    // The focusing lens

    ui_2d_ellipse(55, 78, 9, 7, 0xFFE0D0FF, 1, 0);
    ui_2d_circle(20, 26, 5, 0xFFB8A0FF, 1, 0);
    ui_2d_circle(90, 26, 5, 0xFFB8A0FF, 1, 0);
    gBossBid[1] = ui_2d_end_block();
}

void MakeBossBlockHome()
{
    // 10 BOSS 追踪弹幕型(墨绿):116×96,中心 (58,48) —— 腹部一圈发射管

    // 10 homing-barrage BOSS (dark green): 116×96, centre (58,48) — a ring of launch tubes on the belly

    gBossBid[2] = ui_2d_create_block(116, 96, 0);
    ui_2d_rect(2, 42, 112, 14, 0xFF1A4A2A, 1, 0, 5);
    ui_2d_ellipse(58, 46, 42, 32, 0xFF2A7A44, 1, 0);
    ui_2d_rect(22, 22, 72, 46, 0xFF3EA860, 1, 0, 12);
    ui_2d_ellipse(58, 42, 16, 13, 0xFFD0FFD0, 1, 0);
    ui_2d_ellipse(58, 42, 7, 6, 0xFFFFF060, 1, 0);
    ui_2d_circle(34, 78, 5, 0xFFE8FFE8, 1, 0);
    ui_2d_circle(58, 84, 5, 0xFFE8FFE8, 1, 0);
    ui_2d_circle(82, 78, 5, 0xFFE8FFE8, 1, 0);
    ui_2d_circle(46, 68, 4, 0xFFE8FFE8, 1, 0);
    ui_2d_circle(70, 68, 4, 0xFFE8FFE8, 1, 0);
    gBossBid[2] = ui_2d_end_block();
}

void MakeEnemyBlocks()
{
    MakeEnemyBlockScout();
    MakeEnemyBlockSweep();
    MakeEnemyBlockDive();
    MakeEnemyBlockSnake();
    MakeEnemyBlockTurret();
    MakeEnemyBlockKami();
    MakeBossBlockFan();
    MakeBossBlockLaser();
    MakeBossBlockHome();
}

void MakeBulletBlocks()
{
    // 11 自机子弹:6×18,中心 (3,9)

    // 11 player bullet: 6×18, centre (3,9)

    gPBulBid = ui_2d_create_block(6, 18, 0);
    ui_2d_rect(0, 0, 6, 18, 0xFFFFE070, 1, 0, 3);
    ui_2d_rect(2, 2, 2, 14, 0xFFFFFFFF, 1, 0, 1);
    gPBulBid = ui_2d_end_block();

    // 12 小敌弹:12×12,中心 (6,6)

    // 12 small enemy bullet: 12×12, centre (6,6)

    gEBulBid = ui_2d_create_block(12, 12, 0);
    ui_2d_circle(6, 6, 5, 0xFFFF7040, 1, 0);
    ui_2d_circle(6, 6, 2, 0xFFFFF0A0, 1, 0);
    gEBulBid = ui_2d_end_block();

    // 13 快弹:10×20,中心 (5,10) —— 细长 + 冷色,与慢弹一眼分得开

    // 13 fast bullet: 10×20, centre (5,10) — slender and cold-toned, unmistakable against the slow one

    gEBulFastBid = ui_2d_create_block(10, 20, 0);
    ui_2d_rect(2, 0, 6, 20, 0xFF60E0FF, 1, 0, 3);
    ui_2d_rect(3, 4, 4, 12, 0xFFFFFFFF, 1, 0, 1);
    gEBulFastBid = ui_2d_end_block();

    // 14 瞄准弹:16×16,中心 (8,8) —— 黄色方钻

    // 14 aimed bullet: 16×16, centre (8,8) — a yellow lozenge

    gEBulAimBid = ui_2d_create_block(16, 16, 0);
    ui_2d_ellipse(8, 8, 7, 7, 0xFFFFD040, 1, 0);
    ui_2d_ellipse(8, 8, 3, 3, 0xFFFFFFFF, 1, 0);
    gEBulAimBid = ui_2d_end_block();

    // 15 环弹:16×16,中心 (8,8) —— 紫环

    // 15 ring bullet: 16×16, centre (8,8) — a violet ring

    gEBulRingBid = ui_2d_create_block(16, 16, 0);
    ui_2d_circle(8, 8, 7, 0xFFB070FF, 1, 0);
    ui_2d_circle(8, 8, 4, 0xFFFFFFFF, 0, 2);
    gEBulRingBid = ui_2d_end_block();

    // 16 追踪弹:14×14,中心 (7,7) —— 粉球

    // 16 homing bullet: 14×14, centre (7,7) — a pink ball

    gEBulHomeBid = ui_2d_create_block(14, 14, 0);
    ui_2d_circle(7, 7, 6, 0xFFFF80C0, 1, 0);
    ui_2d_circle(7, 7, 2, 0xFFFFFFFF, 1, 0);
    gEBulHomeBid = ui_2d_end_block();

    // 17 BOSS 大弹:18×18,中心 (9,9)

    // 17 the BOSS's big bullet: 18×18, centre (9,9)

    gEBulBigBid = ui_2d_create_block(18, 18, 0);
    ui_2d_circle(9, 9, 8, 0xFFC060FF, 1, 0);
    ui_2d_circle(9, 9, 4, 0xFFFFE0FF, 1, 0);
    gEBulBigBid = ui_2d_end_block();
}

void MakeBlocks()
{
    gPlayerBid = 0;
    gEnemyBid[0] = 0;
    gEnemyBid[1] = 0;
    gEnemyBid[2] = 0;
    gEnemyBid[3] = 0;
    gEnemyBid[4] = 0;
    gEnemyBid[5] = 0;
    gBossBid[0] = 0;
    gBossBid[1] = 0;
    gBossBid[2] = 0;
    gPBulBid = 0;
    gEBulBid = 0;
    gEBulFastBid = 0;
    gEBulAimBid = 0;
    gEBulRingBid = 0;
    gEBulHomeBid = 0;
    gEBulBigBid = 0;
    MakePlayerBlock();
    MakeEnemyBlocks();
    MakeBulletBlocks();
}

// ════════════════════════════════════════════════════════════════════

// 主循环

// Main loop

//

// 与 `gorilla.cpp` 同构:`ui_msg_fetch_wait` 收消息、`ui_timer_set` 做节拍、

// The same shape as `gorilla.cpp`: `ui_msg_fetch_wait` receives messages, `ui_timer_set` provides the cadence, ticks are

// **按 `ui_time_get_ms()` 的真实流逝时间补拍**(这样物理与帧率解耦),每帧末尾 `ui_2d_present()`。

// **caught up from `ui_time_get_ms()`'s elapsed real time** (which decouples the physics from the frame rate), and every frame ends with `ui_2d_present()`.

// ════════════════════════════════════════════════════════════════════


int main()
{
    int tid;
    int t;
    int done;
    int needDraw;
    int lastMs;
    int now;
    int want;
    char hb[24];
    int hn;

    gLang = ui_scr_get_language();
    gScore = 0;
    gHi = 0;
    gState = ST_PLAY;
    gFlash = 0;
    gBannerT = 0;
    gDragOn = 0;
    gBeepCd = 0;
    gTick = 0;
    gWave = 1;
    gBossShown = 0;
    gEbCap = WaveEBulCap(1);
    gOnScreen = WaveOnScreen(1);

    hn = ui_store_get("1945.hi", hb, 20);
    if (hn > 0) { gHi = myatoi(hb); }

    // 竖屏锁死 + 不要手柄区(这个游戏只用手指,手柄区连折叠条一起吃画布高度)

    // Portrait locked, no gamepad area (this game is fingers only, and the gamepad area plus its collapse bar would eat canvas height)

    // 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)

    // Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)

    ui_win_open_ex(gLang == 0 ? "1945 空战" : "1945 Sky Raider", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD);
    ui_scr_set_keep_on(1);

    // 尺寸:开窗**之后**再问一次 —— 这时 `ui_scr_get_w/h` 给的是实测画布(真值)

    // Size: re-read **after** the open — `ui_scr_get_w/h` now report the measured canvas (the real value)

    gSw = ui_scr_get_w();
    gSh = ui_scr_get_h();
    if (gSw <= 0) { gSw = 411; }
    if (gSh <= 0) { gSh = 726; }

    // ⚠ **必须在开窗之后**(块是宿主资源),而且**只建这一次**(块表不随 `ui_2d_clear` 清)

    // ⚠ **Must come after the window opens** (blocks are a host resource), and **only this once** (the block table is not cleared by `ui_2d_clear`)

    MakeBlocks();

    GameReset();
    lastMs = ui_time_get_ms();
    tid = ui_timer_set(TICK_MS, 0);
    done = 0;
    needDraw = 1;

    while (done == 0 && ui_win_is_closed() == 0)
    {
        // 刷新由**变化**驱动:没消息就不重画(见 `gorilla.cpp` 同一条规矩)

        // Redraws are driven by **change**: no message means no repaint (the same rule as `gorilla.cpp`)

        if (needDraw != 0)
        {
            GameDraw();
            needDraw = 0;
        }

        t = ui_msg_fetch_wait(0);

    handle_msg:
        needDraw = 1;

        if (t == VML_MSG_WINDOWCLOSE) { done = 1; }

        if (t == VML_MSG_TIMER)
        {
            // **按真实经过时间补拍**:定时器消息的语义是"该推进了",而主循环一帧才消费一条

            // **Catch up by elapsed real time**: a timer message means "it is time to advance", and the main loop consumes only one per frame

            // ⇒ 只数消息的话,帧率一掉物理就跟着变慢(手感发黏)。

            // ⇒ counting messages alone would make the physics slow down whenever the frame rate does (a sticky feel).

            // ⚠ 上限 8 拍:真机上偶尔卡几百毫秒,不设上限的话恢复那一帧会一口气跑几十拍 —— 飞机瞬移。

            // ⚠ The cap is 8 ticks: a real device occasionally stalls for a few hundred milliseconds, and without a cap the recovering frame would run dozens of ticks at once — the craft teleporting.

            now = ui_time_get_ms();
            want = (now - lastMs) / TICK_MS;
            if (want < 1) { want = 1; }
            if (want > 8) { want = 8; }
            while (want > 0)
            {
                Tick();
                want = want - 1;
            }
            lastMs = ui_time_get_ms();
            // 补完拍再清积压:**顺序反过来**就是把"物理变慢"换成"物理跳帧"

            // Catch up first, clear the backlog second: **the other order** trades "slower physics" for "skipped frames"

            ui_msg_drop(VML_MSG_KIND_TIMER);
        }

        if (t == VML_MSG_KEYDOWN) { KeyDown(ui_msg_get_a()); }
        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) { TouchDown(); }
        if (t == VML_MSG_TOUCHMOVE || t == VML_MSG_MOUSEMOVE)
        {
            // 丢掉已经排队的过期移动事件:移动是"追最新位置"的语义,中间那些一个都没用,

            // Drop the stale move events already queued: movement means "follow the latest position", the ones in between are all useless,

            // 留着它们只会让触摸要等半天才反应(实测队列能涨到两千多条)。

            // and keeping them only makes touch take ages to respond (measured backlogs of over two thousand).

            ui_msg_drop(VML_MSG_KIND_TOUCH);
            TouchMove();
        }

        if (gState < 0) { done = 1; }

        // 把已经排队的消息一次抽干(`ui_msg_fetch_wait` 一次只取一条,产生得比消费快)

        // Drain the queued messages in one go (`ui_msg_fetch_wait` takes one at a time, and they are produced faster than consumed)

        // ⚠ 判据用 `ui_msg_get_count()` 而**不是**再调一次 `ui_msg_fetch_wait(0)`:后者没消息时会阻塞。

        // ⚠ The test uses `ui_msg_get_count()` and **not** another `ui_msg_fetch_wait(0)`: the latter blocks when there is nothing to read.

        if (done == 0 && ui_msg_get_count() > 0)
        {
            t = ui_msg_fetch();
            goto handle_msg;
        }
    }

    if (tid != 0) { ui_timer_kill(tid); }
    // 退出前静音:胜负音还响着就走,那一声会一直响到进程结束

    // Silence before exiting: leaving while a win/lose tone is playing would keep that tone going until the process ends

    ui_sfx_panic();
    // 最高分落盘(`ui_store_set` 的键由宿主统一加 `vml.` 前缀,不会和 App 自己的设置打架)

    // Persist the high score (the host prefixes `ui_store_set` keys with `vml.`, so they never clash with the app's own settings)

    gHi = imax(gHi, gScore);
    ui_store_set("1945.hi", numstr(gHi));
    ui_scr_set_keep_on(0);
    return 0;
}