/* race3d.c —— **3D 赛车**：第三个"用真游戏验接口"的探针。
 * race3d.c — **3D racing**: the third "write a real game to test the API" probe.
 *
 * ## 它压的是前两个游戏都压不到的那一环：**跟随相机**
 * ## It stresses the link neither of the first two games could: the **follow camera**
 *
 * `solar3d` / `snake3d` / `pool3d` 的相机都是**静止**的（看一个固定场景，靠 `ui_3d_viewport_set_orbit` 转），
 * The camera in `solar3d` / `snake3d` / `pool3d` is **static** (it views one fixed scene, turned with `ui_3d_viewport_set_orbit`),
 * 而赛车要的是**相机每帧跟着车跑**。这一条逼出两个接口事实（本轮实测）：
 * whereas a racing game needs the **camera to follow the car every frame**. That forced out two facts about the API (measured this round):
 *
 *   · **跟随相机不能配 `ui_3d_viewport_fit`**：`fit` 每帧按"半径 → 距离"重算眼睛位置，
 *   · **a follow camera must not use `ui_3d_viewport_fit`**: `fit` recomputes the eye position from "radius -> distance" every frame,
 *     会把 `ui_3d_viewport_set_camera` 给的坐标**整个覆盖掉** ⇒ 车一动相机就被拽回去。
 *     which **overwrites entirely** the coordinates given by `ui_3d_viewport_set_camera` => the camera snaps back the moment the car moves.
 *     正解是**只用 `ui_3d_viewport_set_camera`**（`FitRadius` 保持 0 时，眼睛坐标就是原样使用）。
 *     The fix is to **use `ui_3d_viewport_set_camera` only** (while `FitRadius` stays 0, the eye coordinates are used as given).
 *   · **`ui_3d_viewport_set_camera` 是每帧调用也不贵的**（一次 syscall，不重建任何东西）——
 *   · **calling `ui_3d_viewport_set_camera` every frame is not expensive** (one syscall, nothing is rebuilt) --
 *     这条值得钉住：它是"能不能做追随/第一人称"的前提。
 *     worth pinning down: it is the precondition for "can we do follow / first-person cameras".
 *
 * 另外它还压了：`ui_3d_obj_set_rot` 按**朝向**每帧转（不是 `spin` 那种声明式自转）、
 * It also stresses: `ui_3d_obj_set_rot` turning by **heading** every frame (not `spin`'s declarative self-rotation),
 * 一圈路标（几十个静态物体）、以及"在环上跑"的连续坐标。
 * a ring of pylons (dozens of static objects), and the continuous coordinates of "driving around a ring".
 *
 * ## 玩法
 * ## Gameplay
 *
 * 车自己往前开，你只管**左右打方向**（触摸左右半屏拖 / ←→ 键）。
 * The car drives forward by itself; you only **steer left/right** (touch-drag on the left/right half, or the arrow keys).
 * 冲出赛道会减速。跑过的角度换算成"圈数"记在 HUD 上。
 * Going off the track slows you down. The angle covered is converted to "laps" and shown on the HUD.
 *
 * ## 判据
 * ## Criteria
 *
 * 机械化验证：把 `RUN_FRAMES` 改成大于 0，跑完打印
 * Mechanical check: set `RUN_FRAMES` to a value above 0; at the end it prints
 * `RACE-DONE frames=… laps=… off=…`（行首锚定即可断）。
 * `RACE-DONE frames=... laps=... off=...` (anchor on the line start to decide).
 */
#include <stdio.h>
#include <math.h>
#include <vml_3d.h>
#include <vml_ui.h>

#define RUN_FRAMES 0

/* ── 赛道 ─────────────────────────────────────────────────────────────── */
/* ── Track ──────────────────────────────────────────────────────────── */
#define R_TRACK   60.0f        /* 环中线半径 */
                               /* ring centre-line radius */
#define R_HALF    9.0f         /* 半路宽（内 51 / 外 69） */
                               /* half road width (inner 51 / outer 69) */
#define N_PYLON   48           /* 外侧路标数（每个 15°） */
                               /* number of outer pylons (one per 15°) */
#define N_TREE    24           /* 内侧树/柱，撑画面纵深 */
                               /* inner trees/posts, adding depth to the view */

#define CAR_LEN   2.4f
#define CAR_W     1.4f
#define CAR_H     0.7f

#define C_BG      0xFF0A1622
#define C_ROAD    0xFF3A4048
#define C_PYLON   0xFFE8E8E8
#define C_TREE    0xFF2E6B34
#define C_CAR     0xFFE03A3A
#define C_STRIPE  0xFFF5C542

/* ══ 网格装配（与另外三个 3D 例子同一套写法）══ */
/* ══ Mesh assembly (same style as the other three 3D examples) ══ */
float MB[4400];
int g_vn;
int g_in;

void mb_vertex(int i, float px, float py, float pz, float nx, float ny, float nz) {
    int b;
    b = i * 8;
    MB[b + 0] = px; MB[b + 1] = py; MB[b + 2] = pz;
    MB[b + 3] = nx; MB[b + 4] = ny; MB[b + 5] = nz;
    MB[b + 6] = 0.0f; MB[b + 7] = 0.0f;
}

void mb_index(int k, int v) {
    int* ip;
    ip = (int*)&MB[0];
    ip[g_vn * 8 + k] = v;
}

int mb_ptr(void) { return (int)&MB[0]; }

/* 环带（路面）：XZ 平面上，法线 +Y。内/外半径按**绝对单位**给。
/* Ring band (the road): on the XZ plane, normal +Y. Inner/outer radii in **absolute units**.
 * ⚠ 刻意**不做背面剔除**（后端也没开）：平面环带一剔除，从一半角度看过去会整片消失。
 * ⚠ **Back-face culling is deliberately off** (and is off in the backend too): cull a flat ring band and it vanishes completely from half the viewing angles. */
void gen_ring(int segs, float inner, float outer) {
    int j; int k; int v;
    v = 0;
    for (j = 0; j <= segs; j = j + 1) {
        float th = 6.28318531f * j / segs;
        float c = cos(th);
        float s = sin(th);
        mb_vertex(v, c * inner, 0.0f, s * inner, 0.0f, 1.0f, 0.0f); v = v + 1;
        mb_vertex(v, c * outer, 0.0f, s * outer, 0.0f, 1.0f, 0.0f); v = v + 1;
    }
    g_vn = v;
    k = 0;
    for (j = 0; j < segs; j = j + 1) {
        int a = j * 2;
        mb_index(k, a);     k = k + 1;
        mb_index(k, a + 1); k = k + 1;
        mb_index(k, a + 2); k = k + 1;
        mb_index(k, a + 1); k = k + 1;
        mb_index(k, a + 3); k = k + 1;
        mb_index(k, a + 2); k = k + 1;
    }
    g_in = k;
}

/* 长方体（以原点为中心）—— 车、路标、树都用它。 */
/* Box (centred on the origin) -- the car, pylons and trees all use it. */
void gen_box(float hx, float hy, float hz) {
    int f; int v; int k;
    float nx[6]; float ny[6]; float nz[6];
    nx[0] = 1.0f;  ny[0] = 0.0f;  nz[0] = 0.0f;
    nx[1] = -1.0f; ny[1] = 0.0f;  nz[1] = 0.0f;
    nx[2] = 0.0f;  ny[2] = 1.0f;  nz[2] = 0.0f;
    nx[3] = 0.0f;  ny[3] = -1.0f; nz[3] = 0.0f;
    nx[4] = 0.0f;  ny[4] = 0.0f;  nz[4] = 1.0f;
    nx[5] = 0.0f;  ny[5] = 0.0f;  nz[5] = -1.0f;
    v = 0;
    for (f = 0; f < 6; f = f + 1) {
        int axisU; int axisW; int q;
        if (f == 0 || f == 1) { axisU = 1; axisW = 2; }
        else if (f == 2 || f == 3) { axisU = 0; axisW = 2; }
        else { axisU = 0; axisW = 1; }
        for (q = 0; q < 4; q = q + 1) {
            float hu; float hw; float cx; float cy; float cz;
            hu = ((q == 0 || q == 3) ? 1.0f : -1.0f) *
                 ((axisU == 0) ? hx : ((axisU == 1) ? hy : hz));
            hw = ((q == 0 || q == 1) ? 1.0f : -1.0f) *
                 ((axisW == 0) ? hx : ((axisW == 1) ? hy : hz));
            if (f == 0)      { cx = hx;  cy = hu; cz = hw; }
            else if (f == 1) { cx = -hx; cy = hu; cz = hw; }
            else if (f == 2) { cx = hu; cy = hy;  cz = hw; }
            else if (f == 3) { cx = hu; cy = -hy; cz = hw; }
            else if (f == 4) { cx = hu; cy = hw;  cz = hz; }
            else             { cx = hu; cy = hw;  cz = -hz; }
            mb_vertex(v, cx, cy, cz, nx[f], ny[f], nz[f]);
            v = v + 1;
        }
    }
    g_vn = v;
    k = 0;
    for (f = 0; f < 6; f = f + 1) {
        int base;
        base = f * 4;
        mb_index(k, base);     k = k + 1;
        mb_index(k, base + 1); k = k + 1;
        mb_index(k, base + 2); k = k + 1;
        mb_index(k, base);     k = k + 1;
        mb_index(k, base + 2); k = k + 1;
        mb_index(k, base + 3); k = k + 1;
    }
    g_in = k;
}

/* ══ 车与场景 ═══════════════════════════════════════════════════════════ */
/* ══ Car and scene ══════════════════════════════════════════════════ */
int vp;
int o_car;
int o_pylon[N_PYLON];
int o_tree[N_TREE];

float car_x; float car_z; float car_h;      /* 位置 + 朝向（弧度，绕 Y） */
                                            /* position + heading (radians, around Y) */
float car_speed;
float laps;                                  /* 跑过的累计角度 / 2π */
                                             /* accumulated angle covered / 2pi */
float last_angle;
int off_count;                               /* 冲出赛道的帧数（诊断用） */
                                             /* frames spent off the track (diagnostics) */
int frame;
int steer;                                   /* -1 / 0 / +1 */

float ang_norm(float a) {
    while (a > 3.14159265f) a = a - 6.28318531f;
    while (a < -3.14159265f) a = a + 6.28318531f;
    return a;
}

void reset_car(void) {
    car_x = R_TRACK;
    car_z = 0.0f;
    /* 车头朝 +Z（切向）：环上 (R,0) 处，逆时针跑的切线方向是 +Z */
    /* Nose points +Z (tangential): at (R,0) on the ring, the counter-clockwise tangent is +Z */
    car_h = 0.0f;
    car_speed = 26.0f;
    laps = 0.0f;
    last_angle = 0.0f;
    off_count = 0;
}

/* 把车摆到当前位姿：位置 + 绕 Y 转朝向。
/* Place the car at its current pose: position + heading rotation around Y.
 * ⚠ 用 `ui_3d_obj_set_rot(o, 0, 角度, 0)` 是**每帧改朝向**（不是 `ui_3d_obj_set_spin` 那种声明式自转）——
 * ⚠ `ui_3d_obj_set_rot(o, 0, angle, 0)` **changes the heading every frame** (not `ui_3d_obj_set_spin`'s declarative self-rotation) --
 *   两者都能转，但"跟着输入转"只能走这条。
 *   both can rotate, but "turn with the input" can only go this way. */
void sync_car(void) {
    ui_3d_obj_set_pos(o_car, car_x, CAR_H * 0.5f + 0.15f, car_z);
    ui_3d_obj_set_rot(o_car, 0.0f, car_h * 57.29577951f, 0.0f);
}

int main(void) {
    int sw; int sh; int w; int h;
    int mesh_road; int mesh_car; int mesh_pylon; int mesh_tree;
    int i;
    int touch[3];
    int dragging; int lastTx;
    int lastMs; int nowMs;   /* 墙钟步长（见主循环顶部） */
                             /* wall-clock step (see the top of the main loop) */
    float dt;
    float camD; float camH;
    float trackLen;

    ui_win_open_ex("3D 赛车 Race 3D", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    sw = ui_scr_get_w_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
    sh = ui_scr_get_h_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
    if (sw <= 0) sw = 640;
    if (sh <= 0) sh = 640;
    w = sw; h = sh;

    vp = ui_3d_viewport_create(0, 0, w, h, 0);
    if (vp <= 0) { printf("NO_3D_DEVICE=1\n"); return 0; }
    ui_3d_viewport_set_bg(vp, C_BG);
    ui_3d_viewport_set_fov(vp, 62.0f, 0.4f, 3000.0f);
    /* ⚠ **不调 `ui_3d_viewport_fit`** —— 它每帧按半径重算距离，会把下面逐帧给的相机坐标覆盖掉。 */
    /* ⚠ **no `ui_3d_viewport_fit` call** -- it recomputes distance from the radius every frame and would overwrite the per-frame camera coordinates below. */
    ui_3d_viewport_set_light(vp, 0.35f, 1.0f, 0.25f, 0xFFFFFFFF, 0.50f);

    /* ── 网格 ─────────────────────────────────────────────────────────── */
    /* ── Mesh ───────────────────────────────────────────────────────── */
    gen_ring(96, R_TRACK - R_HALF, R_TRACK + R_HALF);
    mesh_road = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    /* ⚠ **长轴取 Z 而不是 X**：车的朝向是绕 Y 转的 `ui_3d_obj_set_rot(o,0,yaw,0)`，
    /* ⚠ **the long axis is Z, not X**: the car's heading is `ui_3d_obj_set_rot(o,0,yaw,0)` around Y,
     *   而 heading=0 时车往 **+Z** 走（见下面的位移公式）⇒ 车头必须长在 Z 上。
     *   and at heading=0 the car moves toward **+Z** (see the displacement formula below) => the nose must be on Z.
     *   写成 X 的话车会**横着开**（车身与行进方向垂直），一眼就看得出但很容易忽略。
     *   Written as X the car would **drive sideways** (body perpendicular to travel) -- obvious at a glance, yet easy to overlook. */
    gen_box(CAR_W * 0.5f, CAR_H * 0.5f, CAR_LEN * 0.5f);
    mesh_car = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    gen_box(0.6f, 1.6f, 0.6f);
    mesh_pylon = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    gen_box(1.4f, 3.2f, 1.4f);
    mesh_tree = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    printf("RACE-MESH road=%d car=%d pylon=%d tree=%d\n", mesh_road, mesh_car, mesh_pylon, mesh_tree);
    if (mesh_road <= 0 || mesh_car <= 0 || mesh_pylon <= 0 || mesh_tree <= 0) { printf("NO_MESH=1\n"); return 0; }

    /* 路面 */
    /* Road surface */
    {
        int o;
        o = ui_3d_obj_create(vp, mesh_road);
        ui_3d_obj_set_pos(o, 0.0f, 0.0f, 0.0f);
        ui_3d_obj_set_color(o, C_ROAD);
    }

    /* 外侧路标：每 15° 一根（两色交替，转起来才有速度感） */
    /* Outer pylons: one every 15° (alternating two colours, so the sense of speed appears once you are moving) */
    for (i = 0; i < N_PYLON; i = i + 1) {
        float th = 6.28318531f * i / N_PYLON;
        int o;
        o = ui_3d_obj_create(vp, mesh_pylon);
        o_pylon[i] = o;
        ui_3d_obj_set_pos(o, cos(th) * (R_TRACK + R_HALF + 1.2f), 1.6f, sin(th) * (R_TRACK + R_HALF + 1.2f));
        ui_3d_obj_set_color(o, (i % 2) ? C_PYLON : C_STRIPE);
    }

    /* 内侧树：每 15° 一棵，半径更小 ⇒ 透视上形成纵深 */
    /* Inner trees: one every 15°, at a smaller radius => they create depth in perspective */
    for (i = 0; i < N_TREE; i = i + 1) {
        float th = 6.28318531f * i / N_TREE;
        int o;
        o = ui_3d_obj_create(vp, mesh_tree);
        o_tree[i] = o;
        ui_3d_obj_set_pos(o, cos(th) * (R_TRACK - R_HALF - 3.5f), 3.2f, sin(th) * (R_TRACK - R_HALF - 3.5f));
        ui_3d_obj_set_color(o, C_TREE);
    }

    o_car = ui_3d_obj_create(vp, mesh_car);
    ui_3d_obj_set_color(o_car, C_CAR);
    printf("RACE-OBJS=%d\n", 1 + N_PYLON + N_TREE);

    reset_car();
    sync_car();

    dragging = 0; lastTx = 0;
    camD = 13.0f; camH = 4.2f;
    frame = 0;
    trackLen = 0.0f;

    lastMs = ui_time_get_ms();
    dt = 0.04f;

    while (1) {
        /* 步长**按墙钟算**（用户 2026-10-04：帧率不该是玩法的一部分）。
        /* The step is **computed from the wall clock** (user, 2026-10-04: frame rate should not be part of the gameplay).
         * 老写法把 0.04（=1/25 秒）写死，宿主改成跟着显示走之后会快 2.4 倍。
         * The old form hard-coded 0.04 (= 1/25 s), which runs 2.4x too fast once the host follows the display. */
        nowMs = ui_time_get_ms();
        dt = (nowMs - lastMs) * 0.001f;
        if (dt > 0.1f) dt = 0.1f;
        if (dt <= 0.0f) dt = 0.0001f;
        lastMs = nowMs;

        /* ── 输入：左右打方向 ──────────────────────────────────────────── */
        /* ── Input: steering ───────────────────────────────────── */
        steer = 0;
        if (ui_key_is_down(VML_KEY_LEFT)) steer = -1;
        if (ui_key_is_down(VML_KEY_RIGHT)) steer = 1;
        if (ui_key_is_down(65)) steer = -1;   /* 'A' */
        if (ui_key_is_down(68)) steer = 1;    /* 'D' */
        if (ui_key_is_down(VML_KEY_ESCAPE)) break;

        /* 触摸：按住左/右半屏就持续打方向（比"滑动一次转一次"更适合赛车） */
        /* Touch: hold the left/right half to keep steering (better for racing than "one swipe, one turn") */
        if (ui_touch_read(0, touch)) {
            if (touch[2]) {
                if (touch[0] < w / 2) steer = -1;
                else steer = 1;
            }
        }

        /* ── 车：匀速前进，转向改朝向；冲出赛道减速 ───────────────────── */
        /* ── Car: constant speed, steering turns, off-track slows ── */
        {
            float d; float off;
            car_h = car_h + (float)steer * 1.45f * dt;
            if (car_h > 3.14159265f) car_h = car_h - 6.28318531f;
            if (car_h < -3.14159265f) car_h = car_h + 6.28318531f;
            car_x = car_x + sin(car_h) * car_speed * dt;
            car_z = car_z + cos(car_h) * car_speed * dt;
            d = sqrt(car_x * car_x + car_z * car_z);
            off = d - R_TRACK;
            if (off > R_HALF || off < -R_HALF) {
                /* 出界：往回拉 + 减速（不是硬墙，手感更像草地） */
                /* Off-track: pull back + slow down (not a hard wall; feels more like grass) */
                car_speed = car_speed * 0.94f;
                if (car_speed < 7.0f) car_speed = 7.0f;
                off_count = off_count + 1;
                if (off > R_HALF) {
                    car_x = car_x * (R_TRACK + R_HALF) / d;
                    car_z = car_z * (R_TRACK + R_HALF) / d;
                } else {
                    car_x = car_x * (R_TRACK - R_HALF) / d;
                    car_z = car_z * (R_TRACK - R_HALF) / d;
                }
            } else {
                car_speed = car_speed + (40.0f - car_speed) * 0.02f;
            }
            /* 圈数：按极角累加（跨 ±π 时取最短弧） */
            /* Laps: accumulate by polar angle (take the shortest arc across ±pi) */
            {
                float a = atan2(car_z, car_x);
                laps = laps + ang_norm(a - last_angle) / 6.28318531f;
                last_angle = a;
            }
            trackLen = trackLen + car_speed * dt;
        }
        sync_car();

        /* ── 跟随相机：**每帧给一次眼睛坐标**（不用 `vp_fit`，见文件头）─────
        /* ── Follow camera: **give the eye coordinates once per frame** (no `vp_fit`; see the header) ─────
         * 眼睛在车的**后方**（把朝向反过来），抬高一点；注视车头前方一点，
         * The eye sits **behind** the car (heading reversed), a little higher, looking slightly ahead of the nose,
         * 这样画面里能同时看到车与前方赛道。
         * so the frame shows both the car and the track ahead. */
        {
            float bx2 = car_x - sin(car_h) * camD;
            float bz2 = car_z - cos(car_h) * camD;
            float tx2 = car_x + sin(car_h) * 8.0f;
            float tz2 = car_z + cos(car_h) * 8.0f;
            ui_3d_viewport_set_camera(vp, bx2, camH, bz2, tx2, 0.6f, tz2);
        }

        /* ── HUD ──────────────────────────────────────────────────────── */
        ui_2d_clear(0);
        ui_2d_text(12, 10, "3D 赛车 Race 3D", -1, 15, VML_ANCHOR_LEFT);
        {
            char buf[80];
            sprintf(buf, "速度 %.0f   圈数 %.2f   里程 %.0f", car_speed, laps, trackLen);
            ui_2d_text(12, 30, buf, -8355712, 12, VML_ANCHOR_LEFT);
        }
        ui_2d_text(12, 48, "按住屏幕左/右半屏打方向（或用 ← →）", -8355712, 11, VML_ANCHOR_LEFT);
        if (off_count > 0) {
            ui_2d_text(12, 66, "冲出赛道！", -65536, 12, VML_ANCHOR_LEFT);
        }

        ui_2d_present();
        ui_msg_fetch_wait(25);        /* 只是「等输入最多等一会儿」（2026-10-04 起**上屏节奏由宿主管**：`ui_2d_present()` 之后宿主按显示预算走，并把这个超时夹到「距下一帧还有多久」） */
                                      /* just "wait for input for a while" (since 2026-10-04 **the host owns the pacing**: after `ui_2d_present()` the host goes by the display budget and clamps this timeout to "how long until the next frame") */

        frame = frame + 1;
        if (RUN_FRAMES > 0) { if (frame >= RUN_FRAMES) break; }
        /* 只在**机械验证**时提前收：无人驾驶时车迟早出界，别空转到超时。
        /* Stop early only under **mechanical checking**: with no driver the car eventually goes off, so do not idle until the timeout.
         * ⚠ 正常玩**不能**加这一句 —— 出界是玩法的一部分，不是退出条件。
         * ⚠ **Do not** add this line in normal play -- going off is part of the game, not an exit condition. */
        if (RUN_FRAMES > 0 && off_count > 6) break;
    }

    printf("RACE-DONE frames=%d laps=%d off=%d\n", frame, (int)laps, off_count);
    ui_3d_mesh_free(mesh_road);
    ui_3d_mesh_free(mesh_car);
    ui_3d_mesh_free(mesh_pylon);
    ui_3d_mesh_free(mesh_tree);
    ui_3d_viewport_free(vp);
    return 0;
}
