码桶

发现社区成员的开源项目

main
404.html54.5 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404 · 赛道模式 | Race Day</title>
<script src="https://unpkg.com/[email protected]/build/three.min.js"></script>
<style>
    * { box-sizing: border-box; }
    html, body {
        margin: 0; padding: 0; height: 100%; overflow: hidden;
        background: #9fd9ff; color: #16202b;
        font-family: "PingFang SC", "Microsoft YaHei", "Segoe UI", system-ui, sans-serif;
        -webkit-font-smoothing: antialiased; user-select: none;
    }
    #canvas-container { position: fixed; inset: 0; z-index: 0; }
    #loading {
        position: fixed; inset: 0; z-index: 100; display: flex;
        align-items: center; justify-content: center;
        background: linear-gradient(180deg, #2b8ee6 0%, #bfe8ff 100%);
        color: #fff; font-size: 20px; font-weight: 800; letter-spacing: 3px;
    }

    /* ---------- 左上信息卡 ---------- */
    .hud-card {
        position: fixed; top: 18px; left: 18px; z-index: 10;
        padding: 14px 18px 12px; border-radius: 20px;
        background: rgba(255, 255, 255, 0.94);
        border: 3px solid #fff;
        box-shadow: 0 10px 0 rgba(15, 40, 70, 0.12), 0 20px 34px rgba(15, 40, 70, 0.2);
        pointer-events: none; min-width: 232px;
    }
    .brand { display: flex; align-items: baseline; gap: 8px; }
    .brand-num {
        font-size: 34px; font-weight: 900; font-style: italic; letter-spacing: -1px;
        color: #ff5a2b; text-shadow: 0 2px 0 #ffd23f;
    }
    .brand-sub { font-size: 11px; font-weight: 800; letter-spacing: 3px; color: #7b8794; }
    .stat-row { display: flex; gap: 10px; margin-top: 8px; }
    .stat { flex: 1; text-align: center; padding: 6px 4px 5px; border-radius: 12px; background: #eef6ff; }
    .stat .k { font-size: 10px; font-weight: 700; color: #7f93a8; letter-spacing: 1px; }
    .stat .v {
        font-size: 19px; font-weight: 900; font-style: italic; color: #1f6fd0;
        font-variant-numeric: tabular-nums; line-height: 1.15;
    }
    .track-tag {
        margin-top: 8px; display: inline-block; padding: 4px 12px; border-radius: 999px;
        background: linear-gradient(120deg, #ffd23f, #ff9a1a);
        font-size: 11px; font-weight: 800; color: #6a3a00; letter-spacing: 1px;
    }

    /* ---------- 雷达小地图 ---------- */
    .minimap-wrap {
        position: fixed; top: 18px; right: 18px; width: 176px; height: 176px;
        border-radius: 50%; overflow: hidden; z-index: 10;
        border: 5px solid #fff; background: #eaf7ff;
        box-shadow: 0 10px 0 rgba(15, 40, 70, 0.12), 0 18px 32px rgba(15, 40, 70, 0.22);
    }
    .minimap-wrap canvas { width: 100%; height: 100%; display: block; }

    /* ---------- 速度表 ---------- */
    .speedo {
        position: fixed; left: 22px; bottom: 22px; z-index: 10;
        padding: 10px 16px 12px; border-radius: 18px;
        background: rgba(255, 255, 255, 0.94);
        border: 3px solid #fff; pointer-events: none;
        box-shadow: 0 10px 0 rgba(15, 40, 70, 0.12), 0 18px 30px rgba(15, 40, 70, 0.18);
    }
    .speedo-num {
        font-size: 34px; font-weight: 900; font-style: italic; color: #10243a;
        font-variant-numeric: tabular-nums; line-height: 1;
    }
    .speedo-num small { font-size: 11px; font-style: normal; color: #7f93a8; margin-left: 5px; letter-spacing: 1px; }
    .speedo-bar { margin-top: 7px; width: 150px; height: 10px; border-radius: 999px; background: #dce8f5; overflow: hidden; }
    .speedo-bar i {
        display: block; height: 100%; width: 0%;
        border-radius: 999px; background: linear-gradient(90deg, #35d07f, #ffd23f 55%, #ff5a2b);
        transition: width 0.08s linear;
    }

    /* ---------- 道具提示 ---------- */
    .item-prompt {
        position: fixed; left: 50%; bottom: 26px; z-index: 20;
        display: flex; align-items: center; gap: 14px;
        padding: 12px 22px 12px 14px; border-radius: 22px;
        background: rgba(255, 255, 255, 0.96); border: 3px solid #fff;
        box-shadow: 0 12px 0 rgba(15, 40, 70, 0.14), 0 22px 40px rgba(15, 40, 70, 0.24);
        transform: translate(-50%, 34px); opacity: 0; pointer-events: none;
        transition: transform 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.3), opacity 0.28s ease;
    }
    .item-prompt.show { transform: translate(-50%, 0); opacity: 1; pointer-events: auto; cursor: pointer; }
    .ip-icon {
        width: 50px; height: 50px; border-radius: 16px; flex: 0 0 auto;
        display: flex; align-items: center; justify-content: center; font-size: 24px;
        background: var(--accent, #22c55e); color: #fff;
        box-shadow: inset 0 -4px 0 rgba(0, 0, 0, 0.16);
    }
    .ip-title { font-size: 19px; font-weight: 900; font-style: italic; color: #16202b; }
    .ip-title b { color: var(--accent, #22c55e); }
    .ip-hint { font-size: 12px; color: #6c7d8f; margin-top: 2px; font-weight: 600; }
    .ip-hint kbd {
        background: #16202b; color: #fff; border-radius: 6px; padding: 1px 7px;
        font-family: inherit; font-size: 11px; font-weight: 800;
    }

    .hint {
        position: fixed; right: 22px; bottom: 24px; z-index: 10;
        font-size: 12px; font-weight: 700; color: #fff; letter-spacing: 0.5px;
        text-shadow: 0 2px 8px rgba(10, 40, 70, 0.55); pointer-events: none;
    }

    /* ---------- 倒计时 ---------- */
    #countdown {
        position: fixed; left: 50%; top: 40%; z-index: 30; display: none;
        transform: translate(-50%, -50%);
        font-size: 150px; font-weight: 900; font-style: italic; color: #fff;
        text-shadow: 0 10px 0 rgba(15, 40, 70, 0.2), 0 0 46px rgba(255, 255, 255, 0.7);
        pointer-events: none;
    }

    /* ---------- 过关 ---------- */
    .levelup {
        position: fixed; inset: 0; z-index: 40; display: flex;
        align-items: center; justify-content: center;
        background: rgba(8, 30, 58, 0.32); backdrop-filter: blur(4px);
        opacity: 0; pointer-events: none; transition: opacity 0.35s ease;
    }
    .levelup.show { opacity: 1; }
    .lu-card {
        background: #fff; border-radius: 30px; padding: 34px 56px 30px;
        text-align: center; border: 4px solid #fff;
        box-shadow: 0 18px 0 rgba(15, 40, 70, 0.16), 0 30px 60px rgba(15, 40, 70, 0.3);
        transform: translateY(14px) scale(0.96); transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.3);
    }
    .levelup.show .lu-card { transform: translateY(0) scale(1); }
    .lu-kicker {
        display: inline-block; padding: 5px 16px; border-radius: 999px;
        background: #ff5a2b; color: #fff; font-size: 12px; font-weight: 900;
        letter-spacing: 4px; font-style: italic;
    }
    .lu-title { font-size: 34px; font-weight: 900; font-style: italic; margin-top: 14px; color: #16202b; }
    .lu-time { font-size: 15px; color: #64778a; margin-top: 8px; font-weight: 700; }
    .lu-next { font-size: 13px; color: #1f6fd0; margin-top: 16px; font-weight: 800; }

    .toast {
        position: fixed; left: 50%; top: 22%; z-index: 25;
        transform: translate(-50%, -12px); opacity: 0; pointer-events: none;
        padding: 10px 24px; border-radius: 999px;
        background: rgba(22, 32, 43, 0.82); color: #fff;
        font-size: 15px; font-weight: 800; letter-spacing: 1px;
        transition: all 0.3s ease; white-space: nowrap;
    }
    .toast.show { opacity: 1; transform: translate(-50%, 0); }

    /* ---------- 氮气按钮 ---------- */
    .nitro-btn {
        position: fixed; right: 22px; bottom: 62px; width: 98px; height: 98px;
        border-radius: 50%; border: none; padding: 0; z-index: 15; cursor: pointer;
        background: conic-gradient(#22d3ee var(--p, 0%), rgba(255, 255, 255, 0.45) 0);
        box-shadow: 0 10px 0 rgba(15, 40, 70, 0.14), 0 18px 32px rgba(15, 40, 70, 0.26);
        display: flex; align-items: center; justify-content: center;
        -webkit-tap-highlight-color: transparent;
    }
    .nitro-btn::after {
        content: ''; position: absolute; inset: 8px; border-radius: 50%;
        background: #ffffff; box-shadow: inset 0 4px 12px rgba(20, 60, 90, 0.14);
    }
    .nitro-core {
        position: relative; z-index: 1; text-align: center; line-height: 1.05;
        pointer-events: none;
    }
    .nitro-core b { display: block; font-size: 19px; font-style: italic; font-weight: 900; color: #0f7f96; }
    .nitro-core span { display: block; font-size: 10px; font-weight: 800; color: #7f93a8; letter-spacing: 1px; margin-top: 2px; }
    .nitro-btn.ready { animation: nitroPulse 1.6s ease-in-out infinite; }
    .nitro-btn.spent { opacity: 0.55; cursor: default; }
    .nitro-btn:active { transform: translateY(2px); }
    @keyframes nitroPulse {
        0%, 100% { box-shadow: 0 10px 0 rgba(15, 40, 70, 0.14), 0 0 0 0 rgba(34, 211, 238, 0.5); }
        50% { box-shadow: 0 10px 0 rgba(15, 40, 70, 0.14), 0 0 0 12px rgba(34, 211, 238, 0); }
    }

    /* ---------- 氮气视觉 ---------- */
    #boostFx {
        position: fixed; inset: 0; z-index: 5; pointer-events: none; opacity: 0;
        transition: opacity 0.22s ease;
        background: radial-gradient(ellipse at center, rgba(180, 245, 255, 0) 34%, rgba(95, 215, 255, 0.34) 100%);
    }
    #boostFx::before {
        content: ''; position: absolute; inset: -25%;
        background: repeating-linear-gradient(102deg, rgba(255, 255, 255, 0.5) 0 2px, transparent 2px 68px);
        opacity: 0.22; transform: translateX(0)
    }
    body.boosting #boostFx { opacity: 1; }
    body.boosting #boostFx::before { animation: speedStreak 0.38s linear infinite; }
    @keyframes speedStreak { from { transform: translateX(-68px); } to { transform: translateX(0); } }
</style>
</head>
<body>

<div id="loading">正在准备赛道…</div>
<div id="canvas-container"></div>
<div id="boostFx"></div>

<div class="hud-card">
    <div class="brand">
        <span class="brand-num">404</span>
        <span class="brand-sub">RACE DAY</span>
    </div>
    <div class="stat-row">
        <div class="stat"><div class="k">关卡</div><div class="v" id="levelVal">1</div></div>
        <div class="stat"><div class="k">圈数</div><div class="v" id="lapVal">1/2</div></div>
        <div class="stat"><div class="k">用时</div><div class="v" id="timeVal">0.0</div></div>
    </div>
    <div class="track-tag" id="trackName">晴空赛道</div>
</div>

<div class="minimap-wrap">
    <canvas id="minimap" width="352" height="352"></canvas>
</div>

<div class="speedo">
    <div class="speedo-num"><span id="speedNum">0</span><small>km/h</small></div>
    <div class="speedo-bar"><i id="speedBar"></i></div>
</div>

<div class="item-prompt" id="itemPrompt">
    <div class="ip-icon" id="promptIcon">🎁</div>
    <div class="ip-body">
        <div class="ip-title">前往 <b id="targetName">主页</b></div>
        <div class="ip-hint">按 <kbd>ENTER</kbd> 打开 · 也可直接点击</div>
    </div>
</div>

<div class="hint">W 加速 · S 刹车 · A / D 转向 · SPACE 氮气 · ENTER 打开道具</div>

<button type="button" id="nitroBtn" class="nitro-btn" style="--p:100%">
    <span class="nitro-core"><b>氮气</b><span id="nitroPct">100%</span></span>
</button>

<div id="countdown">3</div>

<div class="levelup" id="levelup">
    <div class="lu-card">
        <div class="lu-kicker">FINISH!</div>
        <div class="lu-title">第 <span id="luLevel">1</span> 关完成</div>
        <div class="lu-time">用时 <span id="luTime">0.0</span> 秒</div>
        <div class="lu-next">正在进入第 <span id="luNext">2</span> 关…</div>
    </div>
</div>

<div class="toast" id="toast"></div>

<script>
/* =========================================================
   404 · 赛道模式
   白天卡丁车:专业环道 / 卡丁车 / 道具箱(可跳转页面)/ 冲线进入下一关
   ========================================================= */

const CONFIG = {
    destinations: [
        { text: '主页',   url: 'https://bok.wang/',        color: '#22c55e' },
        { text: '博客',   url: 'https://lxlss.com',        color: '#3b82f6' },
        { text: 'GitHub', url: 'https://github.com/9llll', color: '#a855f7' },
        { text: '联系我', url: 'https://1c1.top/',         color: '#f59e0b' }
    ],
    lapsPerLevel: 2,       // 每关圈数
    roadHalfWidth: 7,      // 赛道半宽
    samples: 720           // 赛道采样点
};

const PALETTES = [
    { name: '晴空赛道', skyTop: 0x2b8ee6, skyBottom: 0xd8f1ff, fog: 0xd8efff, hemiSky: 0xcdeaff, hemiGround: 0x69a84a, ground: 0x7ac94f, road: '#4a4f5c' },
    { name: '阳光环道', skyTop: 0x1f80d8, skyBottom: 0xeaf8ff, fog: 0xe2f5ff, hemiSky: 0xd7f0ff, hemiGround: 0x74b04a, ground: 0x8ad35a, road: '#4d5260' },
    { name: '午后飞驰', skyTop: 0x2f8fe2, skyBottom: 0xfff0d2, fog: 0xf3f4e6, hemiSky: 0xd8efff, hemiGround: 0x6fae45, ground: 0x74c24a, road: '#50515f' },
    { name: '青草直线', skyTop: 0x33a0e6, skyBottom: 0xe9fbff, fog: 0xe4f8ff, hemiSky: 0xd5f1ff, hemiGround: 0x70ab47, ground: 0x82cf54, road: '#484e5b' }
];

/* ---------------- 全局状态 ---------------- */
let scene, camera, renderer;
let car, carBody, wheels = [];
let world = null;              // 每一关的场景容器
let track = null;              // { samples, length, maxR }
let skyDome, hemi, sun, groundMat;
let items = [];
let treeColliders = [];
let activeItem = null;
let palette = PALETTES[0];

let level = 1;
let lapsDone = 0;
let progress = 0;              // 沿赛道的累计进度(1 = 一圈)
let lastU = 0;
let lastIdx = 0;
let offRoad = false;
let speed = 0;

let state = 'loading';         // countdown | race | levelup
let countdownStart = 0;
let raceStart = 0;
let raceTime = 0;
let navigating = false;
let toastTimer = 0;

/* 氮气 */
const NITRO_MAX = 100;
const NITRO_COST = 40;
let nitro = NITRO_MAX;
let boosting = false;
let boostEnd = 0;
let flames = [];

const keys = { w: false, a: false, s: false, d: false };

/* ---------------- DOM ---------------- */
const levelEl = document.getElementById('levelVal');
const lapEl = document.getElementById('lapVal');
const timeEl = document.getElementById('timeVal');
const trackNameEl = document.getElementById('trackName');
const speedNumEl = document.getElementById('speedNum');
const speedBarEl = document.getElementById('speedBar');
const promptEl = document.getElementById('itemPrompt');
const targetNameEl = document.getElementById('targetName');
const promptIconEl = document.getElementById('promptIcon');
const countdownEl = document.getElementById('countdown');
const levelupEl = document.getElementById('levelup');
const luLevelEl = document.getElementById('luLevel');
const luTimeEl = document.getElementById('luTime');
const luNextEl = document.getElementById('luNext');
const toastEl = document.getElementById('toast');
const nitroBtnEl = document.getElementById('nitroBtn');
const nitroPctEl = document.getElementById('nitroPct');

let mmCtx = null;

/* =========================================================
   启动
   ========================================================= */
window.addEventListener('load', boot);

function boot() {
    if (typeof THREE === 'undefined') {
        const el = document.getElementById('loading');
        el.textContent = '引擎加载失败,请检查网络后刷新';
        el.style.color = '#ffdddd';
        return;
    }
    try {
        init();
        document.getElementById('loading').style.display = 'none';
        animate();
    } catch (err) {
        console.error(err);
        const el = document.getElementById('loading');
        el.textContent = '初始化失败:' + err.message;
        el.style.fontSize = '15px';
    }
}

function init() {
    const container = document.getElementById('canvas-container');

    scene = new THREE.Scene();
    scene.fog = new THREE.Fog(0xd8efff, 340, 1000);

    camera = new THREE.PerspectiveCamera(62, window.innerWidth / window.innerHeight, 0.1, 3000);
    camera.position.set(0, 24, 0);

    renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.NoToneMapping;
    container.appendChild(renderer.domElement);

    /* 光照:白天顶光 + 暖阳光 */
    hemi = new THREE.HemisphereLight(0xcdeaff, 0x69a84a, 1.15);
    scene.add(hemi);

    sun = new THREE.DirectionalLight(0xfff6dc, 2.35);
    sun.position.set(170, 250, 130);
    sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    sun.shadow.camera.left = -230;
    sun.shadow.camera.right = 230;
    sun.shadow.camera.top = 230;
    sun.shadow.camera.bottom = -230;
    sun.shadow.camera.near = 1;
    sun.shadow.camera.far = 900;
    sun.shadow.bias = -0.0006;
    sun.shadow.normalBias = 0.7;
    sun.target.position.set(0, 0, 0);
    scene.add(sun);
    scene.add(sun.target);

    skyDome = createSky();
    scene.add(skyDome);

    createClouds();
    createGround();
    createCar();

    mmCtx = document.getElementById('minimap').getContext('2d');

    buildLevel(1);

    window.addEventListener('resize', onWindowResize, false);
    window.addEventListener('keydown', onKeyDown, false);
    window.addEventListener('keyup', onKeyUp, false);
    promptEl.addEventListener('click', openItem);
    nitroBtnEl.addEventListener('click', tryBoost);
}

/* =========================================================
   天空 / 云 / 草地
   ========================================================= */
function createSky() {
    const geo = new THREE.SphereGeometry(1500, 32, 16);
    const mat = new THREE.ShaderMaterial({
        uniforms: {
            topColor: { value: new THREE.Color(0x2b8ee6) },
            bottomColor: { value: new THREE.Color(0xd8f1ff) }
        },
        vertexShader: [
            'varying vec3 vPos;',
            'void main() {',
            '    vPos = position;',
            '    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
            '}'
        ].join('\n'),
        fragmentShader: [
            'uniform vec3 topColor;',
            'uniform vec3 bottomColor;',
            'varying vec3 vPos;',
            'void main() {',
            '    float h = clamp(normalize(vPos).y * 1.7 + 0.12, 0.0, 1.0);',
            '    gl_FragColor = vec4(mix(bottomColor, topColor, h), 1.0);',
            '}'
        ].join('\n'),
        side: THREE.BackSide,
        depthWrite: false,
        fog: false
    });
    return new THREE.Mesh(geo, mat);
}

function createClouds() {
    const mat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.92, fog: false });
    const geo = new THREE.SphereGeometry(1, 12, 10);
    for (let i = 0; i < 16; i++) {
        const g = new THREE.Group();
        const parts = 3 + Math.floor(Math.random() * 3);
        for (let j = 0; j < parts; j++) {
            const m = new THREE.Mesh(geo, mat);
            m.position.set((Math.random() - 0.5) * 34, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 16);
            m.scale.set(14 + Math.random() * 16, 8 + Math.random() * 6, 12 + Math.random() * 10);
            g.add(m);
        }
        const a = Math.random() * Math.PI * 2;
        const r = 280 + Math.random() * 520;
        g.position.set(Math.cos(a) * r, 150 + Math.random() * 90, Math.sin(a) * r);
        scene.add(g);
    }
}

function createGround() {
    const c = document.createElement('canvas');
    c.width = c.height = 256;
    const x = c.getContext('2d');
    x.fillStyle = '#7ac94f';
    x.fillRect(0, 0, 256, 256);
    for (let i = 0; i < 2600; i++) {
        const g = Math.random();
        x.fillStyle = g > 0.5 ? 'rgba(255,255,255,0.10)' : 'rgba(20,90,30,0.14)';
        x.fillRect(Math.random() * 256, Math.random() * 256, 2 + Math.random() * 4, 2 + Math.random() * 4);
    }
    const tex = new THREE.CanvasTexture(c);
    tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
    tex.repeat.set(110, 110);
    tex.anisotropy = 4;

    groundMat = new THREE.MeshStandardMaterial({ map: tex, color: 0xffffff, roughness: 1, metalness: 0 });
    const ground = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000), groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    scene.add(ground);
}

/* =========================================================
   卡丁车
   ========================================================= */
function createCar() {
    car = new THREE.Group();
    carBody = new THREE.Group();
    car.add(carBody);

    const paint = new THREE.MeshStandardMaterial({ color: 0xff4d3f, roughness: 0.35, metalness: 0.3 });
    const yellow = new THREE.MeshStandardMaterial({ color: 0xffd23f, roughness: 0.4, metalness: 0.2 });
    const dark = new THREE.MeshStandardMaterial({ color: 0x232838, roughness: 0.75 });
    const chrome = new THREE.MeshStandardMaterial({ color: 0xdfe6ef, roughness: 0.25, metalness: 0.85 });

    const pan = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.4, 3.8), paint);
    pan.position.y = 0.62;
    carBody.add(pan);

    [-1, 1].forEach(function (s) {
        const pod = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.5, 2.0), yellow);
        pod.position.set(s * 1.12, 0.72, 0.15);
        carBody.add(pod);
    });

    const nose = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.36, 1.3), paint);
    nose.position.set(0, 0.6, -2.05);
    carBody.add(nose);

    const bumper = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.28, 0.5), dark);
    bumper.position.set(0, 0.5, -2.7);
    carBody.add(bumper);

    const seat = new THREE.Mesh(new THREE.BoxGeometry(0.95, 1.0, 0.75), dark);
    seat.position.set(0, 1.15, 0.62);
    carBody.add(seat);

    const col = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 0.75, 8), chrome);
    col.position.set(0, 1.18, -0.35);
    col.rotation.x = -0.5;
    carBody.add(col);

    const sw = new THREE.Mesh(new THREE.TorusGeometry(0.33, 0.07, 8, 20), dark);
    sw.position.set(0, 1.4, -0.55);
    sw.rotation.x = -0.9;
    carBody.add(sw);

    const helmet = new THREE.Mesh(
        new THREE.SphereGeometry(0.42, 18, 14),
        new THREE.MeshStandardMaterial({ color: 0x2ea8ff, roughness: 0.3, metalness: 0.15 })
    );
    helmet.position.set(0, 1.98, 0.55);
    carBody.add(helmet);

    const visor = new THREE.Mesh(new THREE.BoxGeometry(0.64, 0.26, 0.22), dark);
    visor.position.set(0, 1.96, 0.86);
    carBody.add(visor);

    const wing = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.16, 0.55), yellow);
    wing.position.set(0, 1.45, 1.72);
    carBody.add(wing);

    [-0.75, 0.75].forEach(function (x) {
        const sup = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.6, 0.12), dark);
        sup.position.set(x, 1.12, 1.72);
        carBody.add(sup);
    });

    [-0.42, 0.42].forEach(function (x) {
        const ex = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.13, 0.5, 10), chrome);
        ex.position.set(x, 0.78, 1.95);
        ex.rotation.x = Math.PI / 2;
        carBody.add(ex);
    });

    /* 氮气火焰 */
    const flameGeo = new THREE.ConeGeometry(0.27, 1.9, 10);
    const flameMat = new THREE.MeshBasicMaterial({
        color: 0x8beeff, transparent: true, opacity: 0.85,
        blending: THREE.AdditiveBlending, depthWrite: false
    });
    [-0.42, 0.42].forEach(function (x) {
        const f = new THREE.Mesh(flameGeo, flameMat);
        f.position.set(x, 0.78, 3.25);
        f.rotation.x = Math.PI / 2;
        f.visible = false;
        carBody.add(f);
        flames.push(f);
    });

    const wheelGeo = new THREE.CylinderGeometry(0.56, 0.56, 0.5, 18);
    const rimGeo = new THREE.CylinderGeometry(0.3, 0.3, 0.54, 12);
    const tyreMat = new THREE.MeshStandardMaterial({ color: 0x1c1f26, roughness: 0.95 });
    const rimMat = new THREE.MeshStandardMaterial({ color: 0xf2f5fa, roughness: 0.3, metalness: 0.8 });
    const spots = [
        [-1.16, -1.25, true], [1.16, -1.25, true],
        [-1.16, 1.3, false], [1.16, 1.3, false]
    ];
    spots.forEach(function (spot) {
        const pivot = new THREE.Group();
        pivot.position.set(spot[0], 0.56, spot[1]);
        const spinner = new THREE.Group();
        pivot.add(spinner);

        const tyre = new THREE.Mesh(wheelGeo, tyreMat);
        tyre.rotation.z = Math.PI / 2;
        spinner.add(tyre);

        const rim = new THREE.Mesh(rimGeo, rimMat);
        rim.rotation.z = Math.PI / 2;
        spinner.add(rim);

        carBody.add(pivot);
        wheels.push({ pivot: pivot, spinner: spinner, isFront: spot[2] });
    });

    car.traverse(function (o) {
        if (o.isMesh) { o.castShadow = true; o.receiveShadow = false; }
    });

    scene.add(car);
}

/* =========================================================
   赛道生成
   ========================================================= */
function mulberry32(a) {
    return function () {
        a |= 0; a = a + 0x6D2B79F5 | 0;
        let t = Math.imul(a ^ a >>> 15, 1 | a);
        t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
        return ((t ^ t >>> 14) >>> 0) / 4294967296;
    };
}

function buildTrack(n) {
    /* 赛道形状固定,每关循环复用,只更换场景素材 */
    const rnd = mulberry32(20240608);
    const nodeCount = 11;
    const pts = [];
    for (let i = 0; i < nodeCount; i++) {
        const a = (i / nodeCount) * Math.PI * 2;
        const r = 102 + (rnd() - 0.5) * 52;
        pts.push(new THREE.Vector3(Math.cos(a) * r, 0, Math.sin(a) * r));
    }
    const curve = new THREE.CatmullRomCurve3(pts, true, 'catmullrom', 0.5);
    curve.arcLengthDivisions = 1400;

    const N = CONFIG.samples;
    const samples = [];
    for (let i = 0; i < N; i++) {
        const u = i / N;
        const p = curve.getPointAt(u);
        const t = curve.getTangentAt(u).normalize();
        const nrm = new THREE.Vector3(-t.z, 0, t.x).normalize();
        samples.push({ p: p, t: t, n: nrm, u: u });
    }

    let maxR = 1;
    samples.forEach(function (s) {
        maxR = Math.max(maxR, Math.hypot(s.p.x, s.p.z));
    });

    return { curve: curve, samples: samples, length: curve.getLength(), maxR: maxR };
}

function disposeGroup(group) {
    group.traverse(function (o) {
        if (o.geometry) o.geometry.dispose();
        if (o.material) {
            const mats = Array.isArray(o.material) ? o.material : [o.material];
            mats.forEach(function (m) {
                if (m.map) m.map.dispose();
                m.dispose();
            });
        }
    });
}

function buildLevel(n) {
    level = n;
    palette = PALETTES[(n - 1) % PALETTES.length];

    if (world) {
        scene.remove(world);
        disposeGroup(world);
    }
    world = new THREE.Group();
    scene.add(world);

    items = [];
    treeColliders = [];
    activeItem = null;
    hidePrompt();
    hideToast();

    /* 天空 / 光照 / 草地换色 */
    skyDome.material.uniforms.topColor.value.setHex(palette.skyTop);
    skyDome.material.uniforms.bottomColor.value.setHex(palette.skyBottom);
    scene.fog.color.setHex(palette.fog);
    hemi.color.setHex(palette.hemiSky);
    hemi.groundColor.setHex(palette.hemiGround);
    groundMat.color.setHex(palette.ground);

    track = buildTrack(n);
    trackNameEl.textContent = palette.name;

    buildRoad();
    buildCurbs();
    buildStartLine();
    buildArch();
    buildTrees();
    buildFlags();
    buildItems();

    /* 发车位置:终点线前一点点 */
    const N = track.samples.length;
    const startIdx = (N - 18) % N;
    const s0 = track.samples[startIdx];
    car.position.set(s0.p.x, 0, s0.p.z);
    car.rotation.set(0, Math.atan2(-s0.t.x, -s0.t.z), 0);
    carBody.rotation.set(0, 0, 0);
    carBody.position.y = 0;

    speed = 0;
    nitro = NITRO_MAX;
    boosting = false;
    boostEnd = 0;
    document.body.classList.remove('boosting');
    flames.forEach(function (f) { f.visible = false; });
    lapsDone = 0;
    progress = -18 / N;
    lastU = startIdx / N;
    lastIdx = startIdx;
    offRoad = false;

    /* 重置 HUD */
    levelEl.textContent = level;
    lapEl.textContent = '1/' + CONFIG.lapsPerLevel;
    timeEl.textContent = '0.0';
    speedNumEl.textContent = '0';
    speedBarEl.style.width = '0%';
    levelupEl.classList.remove('show');

    state = 'countdown';
    countdownStart = performance.now();
    countdownEl.style.display = 'none';
}

function buildRoad() {
    const samples = track.samples;
    const N = samples.length;
    const halfW = CONFIG.roadHalfWidth;

    const c = document.createElement('canvas');
    c.width = c.height = 256;
    const x = c.getContext('2d');
    x.fillStyle = '#5a5f6b';
    x.fillRect(0, 0, 256, 256);
    for (let i = 0; i < 6000; i++) {
        const g = Math.random() * 44 - 22;
        x.fillStyle = 'rgba(' + Math.round(120 + g) + ',' + Math.round(122 + g) + ',' + Math.round(130 + g) + ',0.35)';
        x.fillRect(Math.random() * 256, Math.random() * 256, 2, 2);
    }
    const tex = new THREE.CanvasTexture(c);
    tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
    tex.anisotropy = 8;

    const pos = [], uv = [], idx = [];
    for (let i = 0; i <= N; i++) {
        const s = samples[i % N];
        pos.push(s.p.x + s.n.x * halfW, 0.02, s.p.z + s.n.z * halfW);
        pos.push(s.p.x - s.n.x * halfW, 0.02, s.p.z - s.n.z * halfW);
        const v = (i / N) * (track.length / 9);
        uv.push(0, v, 1, v);
    }
    for (let i = 0; i < N; i++) {
        const a = i * 2, b = a + 1, cc = a + 2, d = a + 3;
        idx.push(a, cc, b, b, cc, d);
    }

    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
    geo.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
    geo.setIndex(idx);
    geo.computeVertexNormals();

    const mat = new THREE.MeshStandardMaterial({
        map: tex, color: 0xffffff, roughness: 0.92, metalness: 0.05, side: THREE.DoubleSide
    });
    const road = new THREE.Mesh(geo, mat);
    road.receiveShadow = true;
    world.add(road);
}

function buildCurbs() {
    const samples = track.samples;
    const N = samples.length;
    const halfW = CONFIG.roadHalfWidth;
    const red = new THREE.Color(0xe23b3b);
    const white = new THREE.Color(0xf7f7f7);

    [1, -1].forEach(function (side) {
        const pos = [], col = [], idx = [];
        for (let i = 0; i <= N; i++) {
            const s = samples[i % N];
            const inn = halfW * side;
            const out = (halfW + 1.5) * side;
            pos.push(s.p.x + s.n.x * inn, 0.055, s.p.z + s.n.z * inn);
            pos.push(s.p.x + s.n.x * out, 0.055, s.p.z + s.n.z * out);
            const c = (Math.floor(i / 8) % 2 === 0) ? red : white;
            col.push(c.r, c.g, c.b, c.r, c.g, c.b);
        }
        for (let i = 0; i < N; i++) {
            const a = i * 2, b = a + 1, cc = a + 2, d = a + 3;
            idx.push(a, cc, b, b, cc, d);
        }
        const geo = new THREE.BufferGeometry();
        geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
        geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
        geo.setIndex(idx);
        geo.computeVertexNormals();
        const mat = new THREE.MeshStandardMaterial({
            vertexColors: true, roughness: 0.65, metalness: 0.02, side: THREE.DoubleSide
        });
        const curb = new THREE.Mesh(geo, mat);
        curb.receiveShadow = true;
        world.add(curb);
    });

    /* 中线虚线 */
    const dashMat = new THREE.MeshStandardMaterial({ color: 0xf4f7ff, roughness: 0.7 });
    const dashGeo = new THREE.PlaneGeometry(0.5, 3.6);
    for (let i = 0; i < N; i += 14) {
        const s = samples[i];
        const g = new THREE.Group();
        g.position.set(s.p.x, 0.04, s.p.z);
        g.rotation.y = Math.atan2(-s.t.x, -s.t.z);
        const m = new THREE.Mesh(dashGeo, dashMat);
        m.rotation.x = -Math.PI / 2;
        g.add(m);
        world.add(g);
    }
}

function buildStartLine() {
    const s0 = track.samples[0];
    const g = new THREE.Group();
    g.position.set(s0.p.x, 0, s0.p.z);
    g.rotation.y = Math.atan2(-s0.t.x, -s0.t.z);
    world.add(g);

    const darkMat = new THREE.MeshStandardMaterial({ color: 0x1b2130, roughness: 0.6 });
    const lightMat = new THREE.MeshStandardMaterial({ color: 0xf7f9ff, roughness: 0.6 });

    for (let col = 0; col < 10; col++) {
        for (let row = 0; row < 2; row++) {
            const tile = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 1.4), (col + row) % 2 === 0 ? darkMat : lightMat);
            tile.rotation.x = -Math.PI / 2;
            tile.position.set(-6.3 + col * 1.4, 0.045, row * 1.4 - 0.7);
            tile.receiveShadow = true;
            g.add(tile);
        }
    }
}

function buildArch() {
    const s0 = track.samples[0];
    const g = new THREE.Group();
    g.position.set(s0.p.x, 0, s0.p.z);
    g.rotation.y = Math.atan2(-s0.t.x, -s0.t.z);
    world.add(g);

    const whiteMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.45 });
    const redMat = new THREE.MeshStandardMaterial({ color: 0xff4d3f, roughness: 0.45 });
    const halfW = CONFIG.roadHalfWidth;

    [-1, 1].forEach(function (side) {
        const pillar = new THREE.Mesh(new THREE.BoxGeometry(1.5, 8.6, 1.5), whiteMat);
        pillar.position.set(side * (halfW + 2.6), 4.3, 0);
        pillar.castShadow = true;
        g.add(pillar);

        const cap = new THREE.Mesh(new THREE.BoxGeometry(2.1, 1.1, 2.1), redMat);
        cap.position.set(side * (halfW + 2.6), 8.95, 0);
        cap.castShadow = true;
        g.add(cap);
    });

    const barW = (halfW + 3.3) * 2;
    const bar = new THREE.Mesh(new THREE.BoxGeometry(barW, 2.6, 0.8), whiteMat);
    bar.position.set(0, 9.7, 0);
    bar.castShadow = true;
    g.add(bar);

    /* 横幅贴图 */
    const cv = document.createElement('canvas');
    cv.width = 1024; cv.height = 256;
    const ctx = cv.getContext('2d');
    ctx.fillStyle = '#ff5a2b';
    ctx.fillRect(0, 0, 1024, 256);
    for (let i = 0; i < 16; i++) {
        ctx.fillStyle = i % 2 === 0 ? '#101826' : '#ffffff';
        ctx.fillRect(i * 64, 0, 64, 22);
        ctx.fillRect(i * 64, 234, 64, 22);
    }
    ctx.fillStyle = '#ffffff';
    ctx.font = 'italic 900 112px "Microsoft YaHei", Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('404 GRAND PRIX', 512, 132);
    const tex = new THREE.CanvasTexture(cv);
    tex.colorSpace = THREE.SRGBColorSpace;

    const bannerMat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.5 });
    [1, -1].forEach(function (dir) {
        const plane = new THREE.Mesh(new THREE.PlaneGeometry(barW - 0.5, 2.2), bannerMat);
        plane.position.set(0, 9.7, dir * 0.42);
        if (dir < 0) plane.rotation.y = Math.PI;
        g.add(plane);
    });
}

function distToTrackXZ(x, z) {
    const samples = track.samples;
    let best = Infinity;
    for (let i = 0; i < samples.length; i += 5) {
        const dx = samples[i].p.x - x;
        const dz = samples[i].p.z - z;
        const d = dx * dx + dz * dz;
        if (d < best) best = d;
    }
    return Math.sqrt(best);
}

function buildTrees() {
    const rnd = mulberry32(level * 337 + 7);
    const trunkGeo = new THREE.CylinderGeometry(0.42, 0.6, 3.4, 6);
    const trunkMat = new THREE.MeshStandardMaterial({ color: 0x8a5a33, roughness: 1 });
    const leafGeo = new THREE.SphereGeometry(1, 8, 6);
    const leafSets = [
        [0x3fae4a, 0x59c65f],
        [0x2f9e5a, 0x6fd07a],
        [0x57a83f, 0x8ed15c],
        [0x3f9e6a, 0x66c98a]
    ];
    const leafSet = leafSets[(level - 1) % leafSets.length];
    const leafA = new THREE.MeshStandardMaterial({ color: leafSet[0], roughness: 0.95, flatShading: true });
    const leafB = new THREE.MeshStandardMaterial({ color: leafSet[1], roughness: 0.95, flatShading: true });

    let placed = 0, tries = 0;
    while (placed < 150 && tries < 2200) {
        tries++;
        const a = rnd() * Math.PI * 2;
        const r = 42 + rnd() * 250;
        const x = Math.cos(a) * r;
        const z = Math.sin(a) * r;
        if (distToTrackXZ(x, z) < CONFIG.roadHalfWidth + 10) continue;

        const g = new THREE.Group();
        const trunk = new THREE.Mesh(trunkGeo, trunkMat);
        trunk.position.y = 1.7;
        trunk.castShadow = true;
        g.add(trunk);

        const l1 = new THREE.Mesh(leafGeo, leafA);
        l1.position.y = 4.1;
        l1.scale.set(2.5, 2.0, 2.5);
        l1.castShadow = true;
        g.add(l1);

        const l2 = new THREE.Mesh(leafGeo, leafB);
        l2.position.set(0.3, 5.6, -0.2);
        l2.scale.set(1.8, 1.5, 1.8);
        l2.castShadow = true;
        g.add(l2);

        g.position.set(x, 0, z);
        const sc = 0.75 + rnd() * 0.7;
        g.scale.setScalar(sc);
        g.rotation.y = rnd() * Math.PI * 2;
        world.add(g);
        treeColliders.push({ x: x, z: z, r: 1.0 * sc + 0.5 });
        placed++;
    }
}

function buildFlags() {
    const samples = track.samples;
    const N = samples.length;
    const accentColors = [0xff5a2b, 0x22c55e, 0x3b82f6, 0xffd23f];
    const accent = accentColors[(level - 1) % accentColors.length];

    const poleGeo = new THREE.CylinderGeometry(0.08, 0.08, 4.4, 6);
    const poleMat = new THREE.MeshStandardMaterial({ color: 0xf2f5fa, roughness: 0.55 });
    const flagGeo = new THREE.PlaneGeometry(1.7, 1.0);
    const flagMat = new THREE.MeshStandardMaterial({
        color: accent, roughness: 0.7, metalness: 0.05, side: THREE.DoubleSide
    });

    for (let i = 0; i < N; i += 55) {
        const s = samples[i];
        const side = ((i / 55) % 2 === 0) ? 1 : -1;
        const g = new THREE.Group();
        g.position.set(
            s.p.x + s.n.x * side * (CONFIG.roadHalfWidth + 3.4),
            0,
            s.p.z + s.n.z * side * (CONFIG.roadHalfWidth + 3.4)
        );
        g.rotation.y = Math.atan2(-s.t.x, -s.t.z);

        const pole = new THREE.Mesh(poleGeo, poleMat);
        pole.position.y = 2.2;
        pole.castShadow = true;
        g.add(pole);

        const flag = new THREE.Mesh(flagGeo, flagMat);
        flag.position.set(0.85, 3.55, 0);
        flag.castShadow = true;
        g.add(flag);

        world.add(g);
    }
}

/* =========================================================
   道具箱(可跳转页面)
   ========================================================= */
function roundRectPath(ctx, x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r);
    ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r);
    ctx.arcTo(x, y, x + w, y, r);
    ctx.closePath();
}

function itemTexture(color) {
    const c = document.createElement('canvas');
    c.width = c.height = 256;
    const x = c.getContext('2d');

    const grad = x.createLinearGradient(0, 0, 256, 256);
    grad.addColorStop(0, color);
    grad.addColorStop(1, '#ffffff');
    roundRectPath(x, 10, 10, 236, 236, 48);
    x.fillStyle = grad;
    x.fill();
    x.lineWidth = 14;
    x.strokeStyle = '#ffffff';
    x.stroke();

    x.fillStyle = 'rgba(255,255,255,0.92)';
    x.font = '900 150px Arial';
    x.textAlign = 'center';
    x.textBaseline = 'middle';
    x.fillText('?', 128, 136);

    const tex = new THREE.CanvasTexture(c);
    tex.colorSpace = THREE.SRGBColorSpace;
    return tex;
}

function labelSprite(text, color) {
    const c = document.createElement('canvas');
    c.width = 320; c.height = 128;
    const x = c.getContext('2d');
    roundRectPath(x, 8, 22, 304, 84, 26);
    x.fillStyle = color;
    x.fill();
    x.lineWidth = 8;
    x.strokeStyle = 'rgba(255,255,255,0.95)';
    x.stroke();

    x.fillStyle = '#ffffff';
    x.font = '900 42px "Microsoft YaHei", Arial';
    x.textAlign = 'center';
    x.textBaseline = 'middle';
    x.fillText(text, 160, 66);

    const tex = new THREE.CanvasTexture(c);
    tex.colorSpace = THREE.SRGBColorSpace;
    const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false });
    const sp = new THREE.Sprite(mat);
    sp.scale.set(7.4, 2.96, 1);
    return sp;
}

function buildItems() {
    const N = track.samples.length;
    const total = CONFIG.destinations.length;
    const rnd = mulberry32(level * 97 + 31);
    const halfW = CONFIG.roadHalfWidth;

    CONFIG.destinations.forEach(function (dest, k) {
        const u = (0.14 + k * (0.82 / total) + (rnd() - 0.5) * 0.04 + 1) % 1;
        const s = track.samples[Math.floor(u * N) % N];
        const side = (k % 2 === 0) ? 1 : -1;

        const g = new THREE.Group();
        g.position.set(
            s.p.x + s.n.x * side * halfW * 0.5,
            0,
            s.p.z + s.n.z * side * halfW * 0.5
        );

        const tex = itemTexture(dest.color);
        const box = new THREE.Mesh(
            new THREE.BoxGeometry(2.4, 2.4, 2.4),
            new THREE.MeshStandardMaterial({
                map: tex, emissive: new THREE.Color(dest.color), emissiveIntensity: 0.35,
                roughness: 0.35, metalness: 0.15
            })
        );
        box.position.y = 2.9;
        box.castShadow = true;
        g.add(box);

        const ring = new THREE.Mesh(
            new THREE.TorusGeometry(2.25, 0.1, 8, 32),
            new THREE.MeshStandardMaterial({
                color: 0xffffff, emissive: new THREE.Color(dest.color), emissiveIntensity: 0.9, roughness: 0.3
            })
        );
        ring.rotation.x = Math.PI / 2;
        ring.position.y = 2.9;
        g.add(ring);

        const label = labelSprite(dest.text, dest.color);
        label.position.y = 6.3;
        g.add(label);

        g.userData = { box: box, ring: ring, phase: rnd() * Math.PI * 2, dest: dest };
        world.add(g);
        items.push(g);
    });
}

/* =========================================================
   主循环
   ========================================================= */
let lastFrame = 0;

function animate() {
    requestAnimationFrame(animate);
    const now = performance.now();
    let dt = (now - lastFrame) / 1000;
    lastFrame = now;
    if (!isFinite(dt) || dt <= 0) dt = 1 / 60;
    dt = Math.min(dt, 0.05);

    try {
        update(dt, now);
    } catch (err) {
        console.error(err);
    }
    renderer.render(scene, camera);
}

function update(dt, now) {
    if (!track) return;

    if (state === 'countdown') {
        updateCountdown(now);
        updateCamera(dt);
        updateItemVisuals(dt, now);
        updateNitroUI();
        updateMinimap();
        return;
    }

    if (state === 'race') {
        updatePhysics(dt, now);
        updateProgress();
        updateItemInteraction(now);
        updateHUD(now);
    }

    updateItemVisuals(dt, now);
    updateNitroUI();
    updateCamera(dt);
    updateMinimap();
}

function updateCountdown(now) {
    const e = (now - countdownStart) / 1000;

    if (e < 3) {
        const num = 3 - Math.floor(e);
        const frac = e - Math.floor(e);
        countdownEl.style.display = 'block';
        countdownEl.textContent = String(num);
        countdownEl.style.color = num === 1 ? '#ffe45e' : '#ffffff';
        countdownEl.style.transform = 'translate(-50%,-50%) scale(' + (1.35 - frac * 0.35) + ')';
        countdownEl.style.opacity = String(1 - frac * 0.25);
    } else if (e < 3.85) {
        countdownEl.style.display = 'block';
        countdownEl.textContent = 'GO!';
        countdownEl.style.color = '#ffe45e';
        countdownEl.style.transform = 'translate(-50%,-50%) scale(1.15)';
        countdownEl.style.opacity = '1';
    } else {
        countdownEl.style.display = 'none';
        state = 'race';
        raceStart = now;
    }
}

function updatePhysics(dt, now) {
    const step = Math.min(dt * 60, 2.2);
    const boostingNow = boosting && now < boostEnd;
    if (boosting && !boostingNow) endBoost();

    const maxSpeed = boostingNow ? 1.72 : 1.05;

    if (keys.w) speed += 0.021 * step;
    if (keys.s) speed -= 0.05 * step;
    if (boostingNow) speed += 0.075 * step;

    const drag = offRoad ? 0.945 : 0.9915;
    speed *= Math.pow(drag, step);

    if (offRoad && speed > 0.62 && !boostingNow) speed -= (speed - 0.62) * 0.12 * step;
    speed = Math.max(-0.45, Math.min(1.78, speed));
    if (speed > maxSpeed) speed += (maxSpeed - speed) * Math.min(1, 0.035 * step);
    if (Math.abs(speed) < 0.004) speed = 0;

    /* 氮气回充:在赛道上跑动蓄能 */
    if (!boostingNow) {
        nitro = Math.min(NITRO_MAX, nitro + (offRoad ? 3.2 : 9) * dt);
    }

    /* 尾焰 */
    flames.forEach(function (f) {
        f.visible = boostingNow;
        if (boostingNow) {
            const k = 0.75 + Math.random() * 0.75;
            f.scale.set(0.9 + Math.random() * 0.35, k, 0.9 + Math.random() * 0.35);
        }
    });

    const steer = keys.a ? 1 : (keys.d ? -1 : 0);
    const grip = Math.min(1, Math.abs(speed) / 0.28);
    const dir = speed >= 0 ? 1 : -1;
    car.rotation.y += steer * 0.048 * step * grip * dir;

    car.translateZ(-speed * step);

    /* 简易树干碰撞,避免穿模 */
    for (let i = 0; i < treeColliders.length; i++) {
        const t = treeColliders[i];
        const dx = car.position.x - t.x;
        const dz = car.position.z - t.z;
        const d = Math.sqrt(dx * dx + dz * dz);
        const minD = t.r + 1.15;
        if (d < minD && d > 0.0001) {
            const push = minD - d;
            car.position.x += (dx / d) * push;
            car.position.z += (dz / d) * push;
            speed *= 0.7;
        }
    }

    const targetRoll = -steer * 0.10 * Math.min(1, Math.abs(speed));
    carBody.rotation.z += (targetRoll - carBody.rotation.z) * Math.min(1, dt * 8);
    carBody.rotation.x = -speed * 0.02;
    carBody.position.y = Math.sin(performance.now() * 0.018) * 0.012;

    wheels.forEach(function (w) {
        w.spinner.rotation.x += speed * step * 3.0;
        if (w.isFront) w.pivot.rotation.y = steer * 0.34;
    });
}

function nearestSampleIndex() {
    const samples = track.samples;
    const N = samples.length;
    let best = lastIdx;
    let bestD = Infinity;

    for (let k = -50; k <= 50; k++) {
        const i = (lastIdx + k + N) % N;
        const dx = samples[i].p.x - car.position.x;
        const dz = samples[i].p.z - car.position.z;
        const d = dx * dx + dz * dz;
        if (d < bestD) { bestD = d; best = i; }
    }

    /* 冲出赛道太远时做一次全局兜底搜索 */
    if (bestD > 2500) {
        for (let i = 0; i < N; i += 3) {
            const dx = samples[i].p.x - car.position.x;
            const dz = samples[i].p.z - car.position.z;
            const d = dx * dx + dz * dz;
            if (d < bestD) { bestD = d; best = i; }
        }
    }
    return { index: best, dist: Math.sqrt(bestD) };
}

function updateProgress() {
    const N = track.samples.length;
    const res = nearestSampleIndex();
    const idx = res.index;

    if (idx !== lastIdx) {
        const u = idx / N;
        let delta = u - lastU;
        if (delta > 0.5) delta -= 1;
        if (delta < -0.5) delta += 1;
        progress += delta;
        lastU = u;
        lastIdx = idx;
    }

    offRoad = res.dist > CONFIG.roadHalfWidth + 0.9;

    while (progress >= lapsDone + 1) {
        lapsDone++;
        if (lapsDone >= CONFIG.lapsPerLevel) {
            finishLevel();
            return;
        }
        lapEl.textContent = (lapsDone + 1) + '/' + CONFIG.lapsPerLevel;
        showToast('第 ' + lapsDone + ' 圈完成!最后一圈冲刺');
    }
}

function updateCamera(dt) {
    const offset = new THREE.Vector3(0, 6.6, 13.2);
    offset.applyQuaternion(car.quaternion);
    const desired = offset.add(car.position);

    const k = 1 - Math.pow(0.0008, dt);
    camera.position.lerp(desired, k);

    const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(car.quaternion);
    const look = car.position.clone().add(new THREE.Vector3(0, 1.5, 0)).addScaledVector(forward, 6.5);
    camera.lookAt(look);

    const targetFov = 62 + Math.min(10, Math.abs(speed) * 9);
    if (Math.abs(camera.fov - targetFov) > 0.05) {
        camera.fov += (targetFov - camera.fov) * Math.min(1, dt * 4);
        camera.updateProjectionMatrix();
    }
}

/* =========================================================
   道具交互
   ========================================================= */
function updateItemVisuals(dt, now) {
    const t = now * 0.001;
    items.forEach(function (it) {
        const d = it.userData;
        const bob = Math.sin(t * 2 + d.phase) * 0.26;
        it.rotation.y += dt * 1.15;
        d.box.position.y = 2.9 + bob;
        d.ring.position.y = 2.72 + bob;
        const target = (it === activeItem) ? 1.16 : 1.0;
        const cur = d.box.scale.x;
        const next = cur + (target - cur) * Math.min(1, dt * 10);
        d.box.scale.setScalar(next);
    });
}

function updateItemInteraction() {
    if (items.length === 0) return;

    let best = null;
    let bestD = Infinity;
    items.forEach(function (it) {
        const dx = it.position.x - car.position.x;
        const dz = it.position.z - car.position.z;
        const d = Math.sqrt(dx * dx + dz * dz);
        if (d < bestD) { bestD = d; best = it; }
    });

    if (bestD < 7) {
        if (activeItem !== best) {
            activeItem = best;
            showPrompt(best.userData.dest);
        }
    } else if (activeItem) {
        activeItem = null;
        hidePrompt();
    }
}

function showPrompt(dest) {
    targetNameEl.textContent = dest.text;
    promptEl.style.setProperty('--accent', dest.color);
    promptIconEl.textContent = '🎁';
    promptEl.classList.add('show');
}

function hidePrompt() {
    promptEl.classList.remove('show');
}

function openItem() {
    if (!activeItem || state !== 'race' || navigating) return;
    navigating = true;
    const url = activeItem.userData.dest.url;
    hidePrompt();
    document.body.style.transition = 'opacity 0.7s ease';
    document.body.style.opacity = '0';
    setTimeout(function () { window.location.href = url; }, 700);
}

/* =========================================================
   氮气加速
   ========================================================= */
function tryBoost() {
    if (state !== 'race' || boosting || navigating) return;
    if (nitro < NITRO_COST) {
        showToast('氮气不足,继续跑动蓄能');
        return;
    }
    nitro -= NITRO_COST;
    boosting = true;
    boostEnd = performance.now() + 1500;
    document.body.classList.add('boosting');
    showToast('氮气加速!');
    updateNitroUI();
}

function endBoost() {
    boosting = false;
    document.body.classList.remove('boosting');
    flames.forEach(function (f) { f.visible = false; });
}

function updateNitroUI() {
    const pct = Math.max(0, Math.min(NITRO_MAX, nitro));
    nitroBtnEl.style.setProperty('--p', pct.toFixed(1) + '%');
    nitroPctEl.textContent = Math.round(pct) + '%';
    const ready = pct >= NITRO_COST && state === 'race' && !boosting;
    nitroBtnEl.classList.toggle('ready', ready);
    nitroBtnEl.classList.toggle('spent', pct < NITRO_COST || state !== 'race');
}

/* =========================================================
   HUD
   ========================================================= */
function updateHUD(now) {
    raceTime = (now - raceStart) / 1000;
    timeEl.textContent = raceTime.toFixed(1);

    const kmh = Math.round(Math.abs(speed) / 1.05 * 180);
    speedNumEl.textContent = String(kmh);
    speedBarEl.style.width = Math.min(100, Math.abs(speed) / 1.05 * 100).toFixed(0) + '%';
}

function showToast(text) {
    toastEl.textContent = text;
    toastEl.classList.add('show');
    clearTimeout(toastTimer);
    toastTimer = setTimeout(function () { toastEl.classList.remove('show'); }, 1700);
}

function hideToast() {
    toastEl.classList.remove('show');
}

/* =========================================================
   小地图
   ========================================================= */
function updateMinimap() {
    if (!mmCtx || !track) return;

    const ctx = mmCtx;
    const W = 352, C = 176, R = 168;
    const s = 158 / (track.maxR + 22);

    ctx.clearRect(0, 0, W, W);
    ctx.save();
    ctx.beginPath();
    ctx.arc(C, C, R, 0, Math.PI * 2);
    ctx.clip();

    const grd = ctx.createRadialGradient(C, C, 12, C, C, R);
    grd.addColorStop(0, '#f6fcff');
    grd.addColorStop(1, '#dceeff');
    ctx.fillStyle = grd;
    ctx.fillRect(0, 0, W, W);

    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';
    ctx.beginPath();
    track.samples.forEach(function (sm, i) {
        const x = C + sm.p.x * s;
        const y = C + sm.p.z * s;
        if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    });
    ctx.closePath();
    ctx.strokeStyle = 'rgba(255,255,255,0.95)';
    ctx.lineWidth = 24;
    ctx.stroke();
    ctx.strokeStyle = palette.road;
    ctx.lineWidth = 15;
    ctx.stroke();

    /* 起点 */
    const sp = track.samples[0];
    ctx.beginPath();
    ctx.arc(C + sp.p.x * s, C + sp.p.z * s, 6, 0, Math.PI * 2);
    ctx.fillStyle = '#ffffff';
    ctx.fill();
    ctx.lineWidth = 3;
    ctx.strokeStyle = '#16202b';
    ctx.stroke();

    /* 道具 */
    items.forEach(function (it) {
        const x = C + it.position.x * s;
        const y = C + it.position.z * s;
        const isActive = (it === activeItem);
        ctx.beginPath();
        ctx.arc(x, y, isActive ? 9 : 6.5, 0, Math.PI * 2);
        ctx.fillStyle = it.userData.dest.color;
        ctx.fill();
        ctx.lineWidth = 3.5;
        ctx.strokeStyle = '#ffffff';
        ctx.stroke();
    });

    /* 车 */
    const px = C + car.position.x * s;
    const py = C + car.position.z * s;
    const fx = -Math.sin(car.rotation.y);
    const fz = -Math.cos(car.rotation.y);
    const px2 = -fz, py2 = fx;
    ctx.beginPath();
    ctx.moveTo(px + fx * 15, py + fz * 15);
    ctx.lineTo(px - fx * 9 + px2 * 9, py - fz * 9 + py2 * 9);
    ctx.lineTo(px - fx * 4, py - fz * 4);
    ctx.lineTo(px - fx * 9 - px2 * 9, py - fz * 9 - py2 * 9);
    ctx.closePath();
    ctx.fillStyle = '#ff4d3f';
    ctx.fill();
    ctx.lineWidth = 3.5;
    ctx.strokeStyle = '#ffffff';
    ctx.stroke();

    ctx.restore();
}

/* =========================================================
   过关 / 输入 / 自适应
   ========================================================= */
function finishLevel() {
    if (state !== 'race') return;

    state = 'levelup';
    raceTime = (performance.now() - raceStart) / 1000;
    speed = 0;
    activeItem = null;
    hidePrompt();

    luLevelEl.textContent = String(level);
    luTimeEl.textContent = raceTime.toFixed(1);
    luNextEl.textContent = String(level + 1);
    levelupEl.classList.add('show');

    setTimeout(function () {
        levelupEl.classList.remove('show');
        buildLevel(level + 1);
    }, 3000);
}

function onKeyDown(e) {
    const k = e.key.toLowerCase();
    if (k === 'w' || k === 'a' || k === 's' || k === 'd') {
        keys[k] = true;
        e.preventDefault();
    }
    if ((e.key === 'Enter') && activeItem && state === 'race') {
        e.preventDefault();
        openItem();
        return;
    }
    if (e.key === ' ' || e.code === 'Space') {
        e.preventDefault();
        tryBoost();
    }
}

function onKeyUp(e) {
    const k = e.key.toLowerCase();
    if (k === 'w' || k === 'a' || k === 's' || k === 'd') {
        keys[k] = false;
        e.preventDefault();
    }
}

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}
</script>
</body>
</html>