/* gyro.c —— 「拧螺丝」：**转手机来拧**（陀螺仪的活样例）
 * gyro.c -- "Screwdriver": twist the phone to drive a screw (a live gyroscope sample).
 *
 * 玩法：把手机**像拧方向盘那样转**，屏幕中央的螺栓就跟着转。
 * How to play: rotate the phone like turning a steering wheel; the bolt in the center turns with it.
 * 在 20 秒内拧满 5 圈就赢；松手不转，螺纹会慢慢**回退**。
 * Turn 5 full laps within 20 seconds to win; if you stop turning, the thread slowly backs off.
 * 点屏幕任意处 = 重开。
 * Tap anywhere on the screen = restart.
 *
 * ## 为什么这个玩法适合陀螺仪（写"用传感器玩"的游戏先看这段）
 * ## Why this gameplay suits the gyroscope (read this first when writing sensor games)
 *
 * 三种传感器能拿到的量完全不同，**选错了玩法就会难受**：
 * The three sensors report completely different quantities; picking the wrong one makes the game feel bad:
 *
 *   · **陀螺仪 = 角速度**（转得多快）。它天生适合**"转圈"这个动作本身** ——
 *   · Gyroscope = angular velocity (how fast you turn). It naturally fits the act of spinning itself --
 *     玩家要的是"我拧了多少"，而这个量**累加就好**，不需要知道"我现在朝向哪"。
 *     the player wants "how much did I turn", and that quantity just accumulates; no heading is needed.
 *     这正是本游戏的做法。
 *     That is exactly what this game does.
 *
 *   · ⚠ **别用陀螺仪去算"绝对朝向"**。角速度积分出来的角度会**漂**：
 *   · Warning: do not use the gyroscope to compute an absolute heading. The integrated angle drifts:
 *     陀螺仪静止时读数并不精确为零（有偏置），积分几分钟就偏出可用范围。
 *     at rest the reading is not exactly zero (there is a bias), so a few minutes of integration leaves the usable range.
 *     本游戏只累计**短时间（20 秒）内的转动量**，漂移还没来得及咬人；
 *     This game only accumulates the rotation over a short time (20 seconds), before drift can bite;
 *     而且**回退机制**顺手把慢漂也压住了（偏置不会一直往一边跑）。
 *     and the back-off mechanism also keeps slow drift in check (the bias does not run one way forever).
 *     真要长时间稳定的朝向，用 `VML_SENS_ROTATION`（融合姿态），它不漂。
 *     For a long-term stable heading use `VML_SENS_ROTATION` (fused attitude); it does not drift.
 *
 *   · **加速度计 = 倾斜**（往哪边歪）。做"滚球 / 迷宫"那类用 `VML_SENS_ACCEL`，
 *   · Accelerometer = tilt (which way you lean). For "rolling ball / maze" games use `VML_SENS_ACCEL`,
 *     见 `tilt.c`。**那是另一个游戏**，别混着用 —— 拿加速度计做转圈会得到
 *     see `tilt.c`. That is a different game; do not mix them -- spinning with the accelerometer gives
 *     "转到某个角度就读不出来"的怪现象（越过 90° 时倾斜量的符号会翻回来）。
 *     the odd symptom of "it stops reading past a certain angle" (the tilt sign flips back beyond 90 degrees).
 *
 * ## 三个让手感成立的细节
 * ## Three details that make the feel work
 *
 *   ① **死区**：`|角速度| < DEAD` 当 0。陀螺仪静止时有偏置（几百 mdps 量级），
 *   1. Dead zone: treat `|angular velocity| < DEAD` as 0. A resting gyroscope has a bias (hundreds of mdps),
 *      不去掉的话螺栓会**自己慢慢转**，玩家以为游戏坏了。
 *      and without removing it the bolt slowly turns on its own and the player thinks the game is broken.
 *      死区同时也是"手抖不算" —— 一举两得。
 *      The dead zone also ignores hand shake -- two birds with one stone.
 *
 *   ② **回退**：不转的时候进度往回走。没有它，玩家可以慢慢蹭；
 *   2. Back-off: progress slides back while you are not turning. Without it the player could creep forward;
 *      而且"拧螺丝"这件事本身就该有回弹感。
 *      and screwing a screw should have a spring-back feel anyway.
 *
 *   ③ **用真实 dt 而不是"每拍固定 30ms"**：卡顿时才不会少转。
 *   3. Use the real dt instead of a fixed 30 ms per tick, so a stutter does not lose rotation.
 *
 * ## ⚠ 本游戏**刻意不区分转动方向**（这是设计，不是没做完）
 * ## Warning: this game deliberately ignores the rotation direction (by design, not unfinished)
 *
 * 进度按 `|角速度|` 累加 —— **转就涨，往哪边转都算**。两个理由：
 * Progress accumulates `|angular velocity|` -- turning always adds, either direction counts. Two reasons:
 *
 *   · **绕开一个桌面验不了的平台约定**。陀螺仪 z 轴的**正方向**是平台/硬件约定
 *   · It sidesteps a platform convention that cannot be verified on the desktop. The positive direction of
 *     （右手定则 + 设备坐标系），桌面上注入的只是数字，**"我往这边拧、读数是多少"
 *     the gyroscope z axis is a platform/hardware convention (right-hand rule + device frame); on the desktop
 *     只能在真机上确认**。而这里的错法是"拧了、但进度在**掉**" ——
 *     only numbers are injected, so "which way do I turn and what reading comes back" needs a real device.
 *     玩家不会觉得"我拧反了"，只会觉得**游戏坏了**。
 *     And the failure mode here is "you turned, but the progress is dropping" -- the player will not think
 *     （这一仓在 `tilt.c` 的符号上就是这么补的：先物理推导、桌面验"给定读数走哪边"、
 *     "I turned the wrong way", only that the game is broken. (That is how the sign was settled in `tilt.c`:
 *     真机补"轴的正方向"。那次推导是对的，但**那个环节绕不过去** ——
 *     physics first, a desktop check of "given this reading, which way does it move", then the device for the
 *     能不依赖它就不依赖。）
 *     positive axis direction. That derivation was correct, but the step is unavoidable -- so avoid depending on it.)
 *
 *   · **玩法本身更顺**。"拧螺丝"就是**一直往一个方向拧**，玩家不该先猜哪边对。
 *   · The gameplay is simply smoother. Screwing means turning one way the whole time; the player should not
 *     实测手把一直朝一个方向转、进度条一直涨，比"拧反了往回退"更好上手。
 *     have to guess which way first. Holding one direction and watching the bar fill is easier to pick up.
 *
 * 代价是失去"拧反了会松"的真实感 —— 但那点真实感换不来一个"可能让玩家以为
 * The cost is losing the realism of "turning the wrong way loosens it" -- but that realism is not worth
 * 游戏坏了"的风险。**要做成双向也行**，前提是先在真机上把符号确认下来。
 * the risk of the player thinking the game is broken. Two-way is fine too, once the sign is confirmed on a device.
 *
 * ## 不用浮点 sin/cos —— 用定点整数旋转矩阵逐档递推（**缘由已过期，写法保留**）
 * ## No floating-point sin/cos -- a fixed-point integer rotation matrix stepped step by step (**the reason expired; the style stays**)
 *
 * 螺栓那条手把要随角度转。~~而 C 前端的 `sin`/`cos` 不可靠（见 `basiclib.c` 那条有记录的缺陷）~~，
 * The bolt handle must rotate with the angle. ~~The C frontend's `sin`/`cos` were unreliable (see the recorded defect in `basiclib.c`)~~;
 * ✅ **已修好（2026-10-04 复验）**：`cos(3.14159265f)` 实测得 **-1000**（＝ -1.000），`sin`/`cos` 现在都正常。
 * ✅ **fixed (re-checked 2026-10-04)**: `cos(3.14159265f)` measured **-1000** (= -1.000); `sin`/`cos` are both correct now.
 * 这里仍用**定点整数旋转矩阵逐档递推**（能跑就别动）；`cos(10°)=985/1024`、`sin(10°)=178/1024` 是预先算好的常数，
 * This file still uses the **fixed-point integer rotation matrix** (if it runs, do not touch it); `cos(10 deg)=985/1024` and `sin(10 deg)=178/1024` are precomputed constants,
 * 整个循环只有整数乘法与移位 —— 不碰浮点、不查表。**新写的代码可以直接用 `sin`/`cos`**（记得 `#include <math.h>`，见 g3_all.c）。
 * so the whole loop is integer multiplies and shifts only. **New code may use `sin`/`cos` directly** (remember `#include <math.h>`, see g3_all.c).
 *
 * ## 桌面上怎么验（`vmlcli` 没有陀螺仪）
 * ## How to test on the desktop (`vmlcli` has no gyroscope)
 *
 * 靠输入脚本注入**瞬时**角速度（单位：度/秒）：
 * Inject instantaneous angular velocity (in degrees per second) from an input script:
 *
 *     gyro 0 0 400        # 绕 z 轴 400 度/秒 ⇒ 每 30ms 转 12 度
 *     gyro 0 0 400        # 400 deg/s about z => 12 degrees every 30 ms
 *     gyro 0 0 0          # 停（不注就一直保持上一次的值）
 *     gyro 0 0 0          # stop (the last value sticks if you stop injecting)
 *
 * 连续注几拍就模拟出"一直在转"，注 0 就是"松手"。
 * Injecting a few ticks in a row simulates continuous turning; injecting 0 is "letting go".
 */

#include <vml_ui.h>

#define TARGET_MDEG  1800000   /* 目标：5 圈（5 × 360 × 1000 千分之一度） */
                               /* Goal: 5 full turns (5 x 360 x 1000 millidegrees). */
#define GAME_MS      20000     /* 限时 20 秒 */
                               /* Time limit: 20 seconds. */
#define DEAD_MDPS    1200      /* 死区：|角速度| 小于它当 0（挡静止偏置与手抖）*/
                               /* Dead zone: |angular velocity| below this counts as 0 (blocks rest bias and hand shake). */
#define BACK_MDPS    9000      /* 松手后的回退速率（千分之一度/秒 ⇒ 9 度/秒）*/
                               /* Back-off rate after letting go (millidegrees/sec => 9 deg/sec). */

/* 状态放文件级全局（C 前端的惯例，见 §3 约束 2） */
/* State lives in file-level globals (C frontend convention, see section 3 constraint 2). */
static int sw;
static int sh;
static int cx;
static int cy;
static int cr;            /* 圆盘半径 */
                          /* Disc radius. */

static int angle;         /* **累计角度**（千分之一度）—— 本游戏唯一的进度 */
                          /* Accumulated angle (millidegrees) -- the only progress value in this game. */
static int omega;         /* 最新角速度（千分之一度/秒） */
                          /* Latest angular velocity (millidegrees/sec). */
static int hasGyro;
static int playing;
static int leftMs;
static int lastTick;
static int win;
static int finalAngle;    /* 结束那一刻的角度 —— 分享要用，而 angle 会被 reset 清掉 */
                          /* Angle at the moment the round ended -- needed for sharing, while angle is cleared by reset(). */
static int lang;          /* 界面语言：开局查一次（ui_scr_get_language 是 syscall，别每帧调） */
                          /* UI language: queried once at start (ui_scr_get_language is a syscall, do not call it every frame) */

/* 「分享成绩」按钮的几何：**在 main 里算一次，画与命中共用**（本仓的规矩 ——
 * The geometry of the "share score" button: computed once in main and shared by drawing and hit testing (repo rule --
 * 各算一遍就会出现"看着在按钮上、点下去没反应"）。
 * computing it twice gives "it looks like it is on the button but tapping does nothing").
 */
static int shX;
static int shY;
static int shW;
static int shH;
static char shareBuf[96];  /* 拼好的分享文本（见 buildShare）*/
                           /* The assembled share text (see buildShare). */

/* ── 整数工具（本仓惯例：不依赖 sprintf 家族的格式化路径）──────────── */
/* ── Integer helpers (repo convention: do not rely on the sprintf family of formatting paths) ── */
static char g_buf[24];

static char* numStr(int v)
{
    int i;
    int neg;
    int n;
    neg = 0;
    if (v < 0) { neg = 1; v = 0 - v; }
    i = 0;
    if (v == 0) { g_buf[i] = '0'; i = i + 1; }
    while (v > 0)
    {
        g_buf[i] = (char)(48 + v % 10);
        v = v / 10;
        i = i + 1;
    }
    if (neg != 0) { g_buf[i] = '-'; i = i + 1; }
    g_buf[i] = 0;
    n = 0;
    while (n < i / 2)
    {
        char t;
        t = g_buf[n];
        g_buf[n] = g_buf[i - 1 - n];
        g_buf[i - 1 - n] = t;
        n = n + 1;
    }
    return g_buf;
}

/* 把 s 接到 d 后面（**逐字符手写**，不依赖本平台的字符串库 ——
 * Append s after d (written character by character by hand; do not rely on this platform's string library --
 * 那个库在这条链上出过问题，而这里只需要十行）。返回新的末尾指针。
 * that library has misbehaved on this chain, and only ten lines are needed here). Returns the new end pointer. */
static char* appStr(char* d, char* s)
{
    while (*s != 0)
    {
        *d = *s;
        d = d + 1;
        s = s + 1;
    }
    *d = 0;
    return d;
}

static int iabs(int v)
{
    if (v < 0) return 0 - v;
    return v;
}

/* 角度（千分之一度）→ "x.y"（圈，一位小数）。
 * Angle (millidegrees) -> "x.y" (laps, one decimal place).
 * ⚠ 字符串拼接在本平台是坏的（会得空串；**2026-10-04 复验仍成立**：`"AB" + "CD"` 实测得空串），
 * Warning: string concatenation is broken on this platform (it yields an empty string; **still holds, re-checked 2026-10-04**: `"AB" + "CD"` measured empty),
 * 所以**原地往 out 里写**（照旧保留；`strcat` 那条路是好的，但这里不折腾）。
 * so write into out in place (this stays; the `strcat` route works, but this file will not churn). */
static void lapsStr(int mdeg, char* out)
{
    int lap10;
    int whole;
    int v;
    int i;
    char tmp[16];
    int n;
    lap10 = mdeg * 10 / 360000;
    if (lap10 < 0) lap10 = 0;
    whole = lap10 / 10;
    v = whole;
    i = 0;
    if (v == 0) { tmp[i] = '0'; i = i + 1; }
    while (v > 0) { tmp[i] = (char)(48 + v % 10); v = v / 10; i = i + 1; }
    n = 0;
    while (i > 0) { i = i - 1; out[n] = tmp[i]; n = n + 1; }
    out[n] = '.'; n = n + 1;
    out[n] = (char)(48 + (lap10 % 10)); n = n + 1;
    out[n] = 0;
}

/* ── 旋转：把 (0,-arm) 绕中心转 `mdeg` 千分之一度，得到端点 ─────────────
 * ── Rotation: rotate (0,-arm) about the center by `mdeg` millidegrees to get the endpoints ──
 *
 * 用**定点整数旋转矩阵逐档递推**（本平台没有可靠的 sin/cos，见文件头）：
 * Uses a fixed-point integer rotation matrix, one 10-degree step at a time (no reliable sin/cos here, see the header):
 * 从 0° 出发每档转 10°，递推到当前档位。
 * start at 0 degrees, rotate 10 degrees per step, up to the current step.
 * 返回时把端点写进 out[0..3]（x1,y1,x2,y2）。
 * On return the endpoints are written into out[0..3] (x1,y1,x2,y2).
 *
 * ⚠ **先归一到 [0,36) 再迭代** —— 不然转了几圈要迭代几千次，每帧都做就是白白烧 CPU。
 * Warning: normalize into [0,36) before iterating -- otherwise a few laps means thousands of iterations per frame, burning CPU for nothing.
 *   （这一条踩过：第一版直接拿总档数去循环，转到第 5 圈时每帧迭代 180 次。）
 *   (Learned the hard way: the first version looped over the total step count and iterated 180 times per frame by lap 5.)
 */
static void boltEnds(int mdeg, int arm, int* out)
{
    /* cos(10°) = 0.98481 → 1008/1024；sin(10°) = 0.17365 → 178/1024 */
    int vx = 1024;      /* 从 (1, 0) 出发 */
                        /* Start from (1, 0). */
    int vy = 0;
    int steps;
    int k;
    int nx;

    steps = (mdeg / 10000) % 36;      /* 千分之一度 → 10 度档，再归一 */
                                      /* Millidegrees -> 10-degree steps, then normalized. */
    if (steps < 0) steps = steps + 36;

    k = 0;
    while (k < steps)
    {
        nx = (vx * 1008 - vy * 178) / 1024;
        vy = (vx * 178 + vy * 1008) / 1024;
        vx = nx;
        k = k + 1;
    }

    /* 手把在圆环上：(vx,vy) 是单位向量的 1024 倍 */
    /* The handle sits on the ring: (vx,vy) is the unit vector scaled by 1024. */
    out[0] = cx - arm * vx / 1024;
    out[1] = cy - arm * vy / 1024;
    out[2] = cx + arm * vx / 1024;
    out[3] = cy + arm * vy / 1024;
}

/* 拼分享文本。⚠ **手动逐字符拼**（见 appStr）——
 * Builds the share text. Warning: assembled character by character by hand (see appStr) --
 * 本平台的字符串库在这条链上出过问题，而这里只需要三次 append。
 * this platform's string library has misbehaved on this chain, and only three appends are needed here. */
static void buildShare(void)
{
    char* p;
    char laps[16];
    lapsStr(finalAngle, laps);
    p = shareBuf;
    p = appStr(p, lang == 0 ? "我在「拧螺丝」里拧了 " : "I turned ");
    p = appStr(p, laps);
    p = appStr(p, lang == 0 ? " 圈！" : " laps in Screwdriver!");
}

/* ── 绘制 ───────────────────────────────────────────────────────────── */
/* ── Drawing ────────────────────────────────────────────────────────── */

static void drawBolt(void)
{
    int ends[4];      /* **数组单独一行**（C 前端的老约束；**2026-10-04 复验已修好**，写法照旧保留）*/
                      /* Arrays go on a line of their own (old C frontend constraint; **fixed as of the 2026-10-04 re-check**, the style stays). */
    int arm;
    int a;
    int b;

    ui_2d_circle(cx, cy, cr, 0xFF1E2430, 1, 0);
    ui_2d_circle(cx, cy, cr, 0xFF3C4658, 0, 3);
    ui_2d_circle(cx, cy, cr * 55 / 100, 0xFF2A3346, 0, 2);

    /* 随角度旋转的横杆 + 环上的手把 */
    /* The cross bar that rotates with the angle, plus the handle on the ring. */
    arm = cr * 66 / 100;
    boltEnds(angle, arm, ends);
    ui_2d_line(ends[0], ends[1], ends[2], ends[3], 0xFFFFD166, 5);
    ui_2d_circle(ends[2], ends[3], 13, 0xFFFF8A65, 1, 0);
    ui_2d_circle(ends[2], ends[3], 13, 0xFFFFFFFF, 0, 2);

    /* 中心轴 */
    /* Center axle. */
    ui_2d_circle(cx, cy, 16, 0xFF3C4658, 1, 0);
    ui_2d_circle(cx, cy, 16, 0xFF6B7789, 0, 2);

    /* 进度条（横的）—— 不用弧：本平台没有画弧的接口，
     * Progress bar (horizontal) -- not an arc: this platform has no arc API,
     * 而横条同样直观，还省掉一堆三角函数。
     * and a bar is just as intuitive while saving a pile of trigonometry. */
    a = 30;
    b = sw - 30;
    ui_2d_rect(a, cy + cr + 96, b - a, 14, 0xFF232A38, 1, 0, 7);
    {
        int filled;
        filled = (b - a) * angle / TARGET_MDEG;
        if (filled > b - a) filled = b - a;
        if (filled > 0) ui_2d_rect(a, cy + cr + 96, filled, 14, 0xFF4ADE80, 1, 0, 7);
    }
}

static void draw(void)
{
    char buf[24];

    ui_2d_clear(0xFF0E1218);

    /* 顶部：剩余秒数 */
    /* Top: seconds remaining. */
    if (playing == 1)
    {
        ui_2d_text(sw / 2, 26, numStr(leftMs / 1000), 0xFFFFFFFF, 26, VML_ANCHOR_CENTER);
        ui_2d_text(sw / 2 + 44, 34, lang == 0 ? "秒" : "s", 0xFF8A94A8, 15, VML_ANCHOR_CENTER);
    }

    drawBolt();

    /* 进度：拧了几圈 */
    /* Progress: how many laps have been turned. */
    lapsStr(angle, buf);
    ui_2d_text(cx, cy + cr + 30, buf, 0xFF4ADE80, 36, VML_ANCHOR_CENTER);
    ui_2d_text(cx, cy + cr + 62, lang == 0 ? "圈 / 目标 5.0" : "laps / target 5.0", 0xFF8A94A8, 14, VML_ANCHOR_CENTER);

    /* 底部提示 */
    /* Bottom hints. */
    if (hasGyro == 0)
    {
        ui_2d_text(sw / 2, sh - 74, lang == 0 ? "这台设备没有陀螺仪" : "No gyroscope on this device", 0xFFFF8A80, 16, VML_ANCHOR_CENTER);
        ui_2d_text(sw / 2, sh - 48, lang == 0 ? "手机端才有（桌面可以用 gyro 注入模拟）" : "Mobile only (on the desktop inject with `gyro`)", 0xFF8A94A8, 13, VML_ANCHOR_CENTER);
    }
    else if (playing == 1)
    {
        ui_2d_text(sw / 2, sh - 74, lang == 0 ? "像拧方向盘那样转手机" : "Turn the phone like a steering wheel", 0xFFFFD166, 16, VML_ANCHOR_CENTER);
        /* 当前转速 —— 给玩家一个"我转得够不够快"的反馈。
         * Current spin rate -- gives the player feedback on whether they are turning fast enough.
         * ⚠ 报**绝对值**：进度不区分方向，转速却显示成负数会让玩家以为拧反了。
         * Warning: report the absolute value; progress ignores direction, so a negative rate would look like a wrong-way turn. */
        ui_2d_text(sw / 2, sh - 48, numStr(iabs(omega) / 1000), 0xFF8A94A8, 14, VML_ANCHOR_CENTER);
        ui_2d_text(sw / 2 + 56, sh - 48, lang == 0 ? "度/秒" : "deg/s", 0xFF8A94A8, 14, VML_ANCHOR_CENTER);
    }
    else
    {
        if (win == 1) ui_2d_text(sw / 2, sh - 150, lang == 0 ? "拧到底了！" : "Screwed all the way in!", 0xFF4ADE80, 18, VML_ANCHOR_CENTER);
        else ui_2d_text(sw / 2, sh - 150, lang == 0 ? "时间到 —— 还差一点" : "Time is up -- almost there", 0xFFFF8A80, 18, VML_ANCHOR_CENTER);
        /* 「分享成绩」按钮（**几何在 main 里算好，画与命中共用**）。
         * The "share score" button (geometry worked out in main, shared by drawing and hit testing).
         * ⚠ 桌面没有分享面板 —— `ui_share_text` 会返回 0，游戏**照常继续**
         * Warning: the desktop has no share panel -- `ui_share_text` returns 0 and the game just carries on
         *   （0 是"这一端没有这个能力"，不是错误）。
         *   (0 means "this end has no such capability", not an error). */
        ui_2d_rect(shX, shY, shW, shH, 0xFF2A6E3A, 1, 0, 10);
        ui_2d_rect(shX, shY, shW, shH, 0xFF4ADE80, 0, 2, 10);
        ui_2d_text(shX + shW / 2, shY + 14, lang == 0 ? "分享成绩" : "Share score", 0xFFFFFFFF, 16, VML_ANCHOR_CENTER);
        ui_2d_text(sw / 2, sh - 46, lang == 0 ? "点别处再来一次" : "Tap elsewhere to play again", 0xFF8A94A8, 14, VML_ANCHOR_CENTER);
    }

    ui_2d_present();
}

/* ── 一拍 ───────────────────────────────────────────────────────────── */
/* ── One tick ───────────────────────────────────────────────────────── */

static void step(int dtMs)
{
    int g[3];
    int w;

    if (playing != 1) return;

    /* 读陀螺仪。**只要 z 轴** —— "像拧方向盘那样转"就是绕屏幕法线转。 */
    /* Read the gyroscope. Only the z axis -- "turning like a steering wheel" means spinning about the screen normal. */
    if (ui_sensor_read(VML_SENS_GYRO, g) == 1) w = g[2];
    else w = 0;
    omega = w;

    /* **死区**：挡住静止偏置与手抖。⚠ 没有它螺栓会自己慢慢转。 */
    /* Dead zone: blocks the rest bias and hand shake. Warning: without it the bolt slowly turns on its own. */
    if (iabs(w) < DEAD_MDPS) w = 0;

    if (w != 0)
    {
        /* 角速度(千分之一度/秒) × 时长(ms) / 1000 = 转过的千分之一度。
         * Angular velocity (millidegrees/sec) x duration (ms) / 1000 = millidegrees turned.
         * ⚠ 用 `iabs(w)` —— **刻意不区分方向**，理由见文件头那段。
         * Warning: uses `iabs(w)` -- the direction is deliberately ignored, see the file header for why. */
        angle = angle + iabs(w) * dtMs / 1000;
    }
    else if (angle > 0)
    {
        /* 回退：松手后螺纹往回走 */
        /* Back-off: the thread slides back after you let go. */
        angle = angle - BACK_MDPS * dtMs / 1000;
        if (angle < 0) angle = 0;
    }

    leftMs = leftMs - dtMs;

    /* ⚠ 结束那一刻**先把角度存下来** —— 后面 `reset()` 会把它清掉，
     * Warning: save the angle at the moment the round ends -- `reset()` clears it later,
     *   而分享文案要用的是"这一局拧到哪"。
     *   and the share text needs "how far this round got". */
    if (angle >= TARGET_MDEG) { playing = 0; win = 1; finalAngle = angle; buildShare(); return; }
    if (leftMs <= 0) { leftMs = 0; playing = 0; win = 0; finalAngle = angle; buildShare(); }
}

static void reset(void)
{
    angle = 0;
    omega = 0;
    playing = 1;
    leftMs = GAME_MS;
    win = 0;
    lastTick = ui_time_get_ms();
}

int main(void)
{
    int msg[4];
    int t;
    int now;

    lang = ui_scr_get_language();

    /* 竖屏 + 不要手柄区：全程靠转手机，一个键都不用。 */
    /* Portrait plus no gamepad area: everything is done by turning the phone, no keys at all. */
    ui_win_open_ex(lang == 0 ? "拧螺丝" : "Screwdriver", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD);
    sw = ui_scr_get_w();
    sh = ui_scr_get_h();
    if (sw <= 0) sw = 380;
    if (sh <= 0) sh = 660;
    ui_scr_set_keep_on(1);

    cx = sw / 2;
    cy = sh / 2 - 50;
    cr = sw / 2 - 40;
    if (cr > 140) cr = 140;

    /* 「分享成绩」按钮的几何 —— **只在这里算一次**，画与命中都用它。 */
    /* Geometry of the "share score" button -- computed only once here, used by both drawing and hit testing. */
    shW = 200;
    if (shW > sw - 60) shW = sw - 60;
    shH = 44;
    shX = (sw - shW) / 2;
    shY = sh - 116;

    hasGyro = ui_sensor_is_available(VML_SENS_GYRO);
    /* 转圈是快动作，采密一点（20ms）。间隔越大越省电，但手感越钝。 */
    /* Spinning is a fast motion, so sample densely (20 ms). A longer interval saves power but feels duller. */
    ui_sensor_set_rate(VML_SENS_GYRO, 20);

    reset();
    buildShare();
    ui_timer_set(30, 0);
    draw();

    while (ui_win_is_closed() == 0)
    {
        t = ui_msg_wait(msg, 0);
        if (t == 0) continue;
        if (t == VML_MSG_WINDOWCLOSE) break;

        if (t == VML_MSG_TIMER)
        {
            now = ui_time_get_ms();
            {
                int dt;
                int wasPlaying;
                dt = now - lastTick;
                if (dt < 1) dt = 30;
                if (dt > 200) dt = 200;    /* 卡顿一下别一口气补太多 */
                                           /* One stutter should not be compensated all at once. */
                lastTick = now;
                wasPlaying = playing;
                step(dt);
                /* ⚠ **结束后不弹框**（v0.96.498 改的）。
                 * Warning: no dialog box once the round ends (changed in v0.96.498).
                 *
                 * 原来这里弹一个 `ui_dlg_msg` 再 `reset()` —— 而弹框是**阻塞**的，
                 * Previously this popped up a `ui_dlg_msg` and then called `reset()` -- but the dialog blocks,
                 * 玩家看完点掉就直接重开了，**挂在同一屏上的「分享成绩」按钮
                 * so the player dismissed it and the game restarted at once, leaving the "share score" button
                 * 根本没有机会被点到**（画了，但永远点不着）。
                 * on the same screen with no chance of ever being tapped (drawn, but unreachable).
                 *
                 * 现在把结果留在画面上（见 draw() 的结束面板），玩家想分享就点按钮、
                 * Now the result stays on the screen (see the end panel in draw()): tap the button to share,
                 * 想重来就点别处 —— **两个动作都是他自己选的**，也没有模态打断。
                 * tap elsewhere to play again -- both actions are the player's own choice, with no modal interruption.
                 * （这也是"按钮画出来了"与"按钮能点到"是两件事的一个实例：
                 * (This is also an instance of "the button is drawn" and "the button is tappable" being two different things:
                 *   本仓记过好几回"看着在键上、点下去没反应"。）
                 *   this repo has recorded "it looks like it is on the key but tapping does nothing" several times.) */
                (void)wasPlaying;
            }
            draw();
            continue;
        }

        /* 触摸：**先判「分享」按钮，再判"点别处重开"** ——
         * Touch: test the "share" button first, then "tap elsewhere to restart" --
         * 用 `shX/shY/shW/shH` 那组数（与画按钮时**同一组**，本仓的规矩）。
         * using the `shX/shY/shW/shH` values (the very same set used when drawing the button, a repo rule). */
        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN)
        {
            int px;
            int py;
            px = msg[1];
            py = msg[2];
            if (playing == 0 && px >= shX && px < shX + shW && py >= shY && py < shY + shH)
            {
                /* ⚠ 返回值**不看**：0 只表示"这一端没有分享面板"（桌面就是），
                 * Warning: the return value is ignored -- 0 only means "this end has no share panel" (the desktop does not),
                 *   不是错误 —— 游戏照常继续。
                 *   it is not an error, and the game carries on as usual. */
                ui_share_text(shareBuf, lang == 0 ? "拧螺丝" : "Screwdriver");
                continue;
            }
            reset();
            draw();
            continue;
        }

        if (t == VML_MSG_KEYDOWN)
        {
            if (msg[1] == VML_KEY_ESCAPE) break;
        }
    }

    ui_scr_set_keep_on(0);
    ui_win_close();
    return 0;
}
