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;
}