🎮ArcadeLab

疾风跑酷 · 自动横屏

by LaserPirate33
1213 lines34.2 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#05071a">
<title>疾风跑酷 · 自动横屏</title>
<style>
  :root {
    --sat: env(safe-area-inset-top, 0px);
    --sar: env(safe-area-inset-right, 0px);
    --sab: env(safe-area-inset-bottom, 0px);
    --sal: env(safe-area-inset-left, 0px);
  }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    height: 100%;
    background: #05071a;
    overflow: hidden;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif;
  }
  /* 舞台:无论手机横竖,游戏画面始终保持横屏,靠旋转适配 */
  #stage {
    position: fixed;
    left: 50%;
    top: 50%;
    transform-origin: 50% 50%;
    transform: translate(-50%, -50%) rotate(0deg);
    will-change: transform;
  }
  canvas {
    display: block;
    border-radius: 14px;
    background: #080b1c;
    box-shadow: 0 24px 80px rgba(0, 0, 0, .8), 0 0 0 1px rgba(255, 255, 255, .06);
  }
</style>
</head>
<body>

<div id="stage"><canvas id="game"></canvas></div>

<script>
(function () {
  'use strict';

  // ================= 画布与舞台 =================
  const W = 960, H = 540;           // 游戏内部逻辑分辨率(横屏)
  const canvas = document.getElementById('game');
  const stage = document.getElementById('stage');
  const ctx = canvas.getContext('2d');
  const DPR = Math.min(window.devicePixelRatio || 1, 2);

  canvas.width = Math.round(W * DPR);
  canvas.height = Math.round(H * DPR);
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);

  let rotatedDeg = 0;      // 0 = 正常横屏;90 = 竖屏时画面侧转
  let scale = 1;           // 逻辑像素 -> CSS 像素
  let stageW = W, stageH = H;
  let stageCenterX = window.innerWidth / 2;
  let stageCenterY = window.innerHeight / 2;

  function readSafeInsets() {
    const cs = getComputedStyle(document.documentElement);
    const num = function (name) {
      const v = parseFloat(cs.getPropertyValue(name));
      return isNaN(v) ? 0 : v;
    };
    return { t: num('--sat'), r: num('--sar'), b: num('--sab'), l: num('--sal') };
  }

  // 核心:窗口是竖的 -> 画面旋转 90°;窗口是横的 -> 正常横屏
  function fit() {
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    const ins = readSafeInsets();

    const availW = Math.max(120, vw - ins.l - ins.r);
    const availH = Math.max(120, vh - ins.t - ins.b);

    // 安全区中心,避免刘海遮挡内容
    stageCenterX = (ins.l + (vw - ins.r)) / 2;
    stageCenterY = (ins.t + (vh - ins.b)) / 2;

    const wantRotate = vh > vw ? 90 : 0;   // 竖屏就侧转

    let s;
    if (wantRotate === 90) {
      // 旋转后:视觉宽度 = stageH,视觉高度 = stageW
      s = Math.min(availW / H, availH / W);
    } else {
      s = Math.min(availW / W, availH / H);
    }
    s = Math.max(0.05, s);

    rotatedDeg = wantRotate;
    scale = s;
    stageW = Math.round(W * s);
    stageH = Math.round(H * s);

    stage.style.left = stageCenterX + 'px';
    stage.style.top = stageCenterY + 'px';
    stage.style.width = stageW + 'px';
    stage.style.height = stageH + 'px';
    stage.style.transform = 'translate(-50%, -50%) rotate(' + rotatedDeg + 'deg)';

    canvas.style.width = stageW + 'px';
    canvas.style.height = stageH + 'px';
  }

  // 屏幕坐标 -> 游戏逻辑坐标(考虑旋转与缩放)
  function getPos(e) {
    const dx = e.clientX - stageCenterX;
    const dy = e.clientY - stageCenterY;
    const rad = -rotatedDeg * Math.PI / 180;
    const cos = Math.cos(rad), sin = Math.sin(rad);
    const rx = dx * cos - dy * sin;
    const ry = dx * sin + dy * cos;
    return {
      x: rx / scale + W / 2,
      y: ry / scale + H / 2
    };
  }

  let fitTimer = null;
  function scheduleFit() {
    fit();
    clearTimeout(fitTimer);
    fitTimer = setTimeout(fit, 240);   // 应对微信里 innerHeight 延迟更新
    setTimeout(fit, 620);
  }

  fit();
  window.addEventListener('resize', scheduleFit);
  window.addEventListener('orientationchange', scheduleFit);
  if (window.visualViewport) {
    window.visualViewport.addEventListener('resize', scheduleFit);
  }

  // ================= 常量 =================
  const GROUND_Y = 440;
  const GRAVITY = 2600;
  const JUMP_V = -920;
  const JUMP2_V = -780;
  const PW = 32, PH = 48;   // 站立碰撞盒
  const SW = 50, SH = 28;   // 滑行碰撞盒
  const PLAYER_X = 160;
  const BULLET_SPEED = 950;
  const SKILL_CD = 0.20;

  const BTN = {
    skill: { x: 102, y: 412, r: 52, color: '#b06bff', glow: '#8a2be2' },
    jump:  { x: 858, y: 372, r: 56, color: '#4de2ff', glow: '#00a8ff' },
    slide: { x: 738, y: 466, r: 42, color: '#ffb347', glow: '#ff8c00' }
  };

  // ================= 状态 =================
  const state = {
    mode: 'menu',       // menu | playing | over
    time: 0,
    meters: 0,
    speed: 300,
    scroll: 0,
    spawnTimer: 1.6,
    bulletTimer: 2.2,
    skillCd: 0,
    kills: 0,
    score: 0,
    best: 0,
    shake: 0,
    overTimer: 0
  };
  try { state.best = Number(localStorage.getItem('runnerBestLandscape') || 0) || 0; } catch (e) {}

  const player = {
    x: PLAYER_X,
    y: GROUND_Y,
    vy: 0,
    jumps: 0,
    sliding: false,
    runPhase: 0
  };

  let obstacles = [];
  let enemyBullets = [];
  let playerBullets = [];
  let particles = [];

  const clouds = [];
  const stars = [];

  for (let i = 0; i < 7; i++) {
    clouds.push({
      x: Math.random() * W,
      y: 40 + Math.random() * 200,
      s: 0.6 + Math.random() * 0.9,
      sp: 0.08 + Math.random() * 0.12
    });
  }
  for (let i = 0; i < 90; i++) {
    stars.push({
      x: Math.random() * W,
      y: Math.random() * 380,
      r: Math.random() * 1.3 + 0.3,
      p: Math.random() * Math.PI * 2
    });
  }

  // ================= 输入状态 =================
  const pressed = { skill: false, jump: false, slide: false };
  const slidePointers = new Set();

  // ================= 工具 =================
  function hash(n) {
    const s = Math.sin(n * 127.1) * 43758.5453;
    return s - Math.floor(s);
  }
  function roundRect(c, x, y, w, h, r) {
    r = Math.max(0, Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2));
    c.beginPath();
    c.moveTo(x + r, y);
    c.arcTo(x + w, y, x + w, y + h, r);
    c.arcTo(x + w, y + h, x, y + h, r);
    c.arcTo(x, y + h, x, y, r);
    c.arcTo(x, y, x + w, y, r);
    c.closePath();
  }
  function difficulty() {
    return Math.min(state.meters / 1800, 1);
  }
  function onGround() {
    return player.y >= GROUND_Y - 0.5;
  }
  function playerRect() {
    if (player.sliding && onGround()) {
      return { x: player.x - SW / 2, y: player.y - SH, w: SW, h: SH };
    }
    return { x: player.x - PW / 2, y: player.y - PH, w: PW, h: PH };
  }
  function overlap(a, b) {
    return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
  }
  function circleRect(cx, cy, cr, r) {
    const nx = Math.max(r.x, Math.min(cx, r.x + r.w));
    const ny = Math.max(r.y, Math.min(cy, r.y + r.h));
    const dx = cx - nx, dy = cy - ny;
    return dx * dx + dy * dy < cr * cr;
  }
  function inCircle(p, c) {
    const dx = p.x - c.x, dy = p.y - c.y;
    return dx * dx + dy * dy < c.r * c.r;
  }

  // ================= 粒子 =================
  function spawnParticles(x, y, n, color, spread) {
    spread = spread || 240;
    for (let i = 0; i < n; i++) {
      if (particles.length > 340) particles.shift();
      particles.push({
        x: x, y: y,
        vx: (Math.random() - 0.5) * spread,
        vy: (Math.random() - 0.5) * spread,
        life: 0.30 + Math.random() * 0.40,
        max: 0.70,
        r: 1.5 + Math.random() * 3,
        color: color
      });
    }
  }
  function updateParticles(dt) {
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life -= dt;
      if (p.life <= 0) { particles.splice(i, 1); continue; }
      p.vy += 760 * dt;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      if (p.y > GROUND_Y + 4) {
        p.y = GROUND_Y + 4;
        p.vy *= -0.35;
        p.vx *= 0.7;
      }
    }
  }

  // ================= 游戏流程 =================
  function restart() {
    state.mode = 'playing';
    state.meters = 0;
    state.speed = 300;
    state.scroll = 0;
    state.spawnTimer = 1.5;
    state.bulletTimer = 1.8;
    state.skillCd = 0;
    state.kills = 0;
    state.score = 0;
    state.shake = 0;
    state.overTimer = 0;

    player.y = GROUND_Y;
    player.vy = 0;
    player.jumps = 0;
    player.sliding = false;
    player.runPhase = 0;

    obstacles = [];
    enemyBullets = [];
    playerBullets = [];
    particles = [];
    slidePointers.clear();
    pressed.skill = pressed.jump = pressed.slide = false;
  }

  function gameOver() {
    if (state.mode !== 'playing') return;
    state.mode = 'over';
    state.overTimer = 0;
    state.shake = 16;
    spawnParticles(player.x, player.y - 24, 30, '#ff5ea8', 420);

    const total = Math.floor(state.meters) + state.score;
    if (total > state.best) {
      state.best = total;
      try { localStorage.setItem('runnerBestLandscape', String(total)); } catch (e) {}
    }
  }

  function doJump() {
    if (state.mode !== 'playing') return;
    if (player.jumps >= 2) return;
    player.vy = player.jumps === 0 ? JUMP_V : JUMP2_V;
    player.jumps++;
    spawnParticles(player.x, player.y, 8, player.jumps === 1 ? '#6ef0ff' : '#ff5ea8', 200);
  }

  function fireSkill() {
    if (state.mode !== 'playing') return;
    if (state.skillCd > 0) return;
    state.skillCd = SKILL_CD;

    const sx = player.x + 22;
    const sy = player.y - 28;

    let target = null;
    let bestDx = Infinity;
    for (let i = 0; i < enemyBullets.length; i++) {
      const b = enemyBullets[i];
      const dx = b.x - sx;
      if (dx < 0) continue;
      if (dx < bestDx && dx < 760) {
        bestDx = dx;
        target = b;
      }
    }

    let vx = BULLET_SPEED, vy = 0;
    if (target) {
      const dx = target.x - sx;
      const dy = target.y - sy;
      const len = Math.hypot(dx, dy) || 1;
      vx = dx / len * BULLET_SPEED;
      vy = dy / len * BULLET_SPEED;
    }

    playerBullets.push({
      x: sx, y: sy,
      vx: vx, vy: vy,
      r: 7,
      life: 1.35
    });

    spawnParticles(sx, sy, 6, '#6ef0ff', 180);
  }

  // ================= 生成障碍 =================
  function spawnObstacle() {
    const d = difficulty();
    const x = W + 60;

    const pool = [
      { k: 'low', w: 3 },
      { k: 'overhead', w: d > 0.10 ? 2.4 : 0 },
      { k: 'double', w: d > 0.42 ? 1.4 + d * 1.6 : 0 }
    ];

    let total = 0;
    for (let i = 0; i < pool.length; i++) total += pool[i].w;
    let pick = Math.random() * total;
    let kind = 'low';
    for (let i = 0; i < pool.length; i++) {
      if (pick < pool[i].w) { kind = pool[i].k; break; }
      pick -= pool[i].w;
    }

    if (kind === 'low') {
      const h = 34 + Math.random() * 10;
      obstacles.push({
        x: x, y: GROUND_Y - h,
        w: 30 + Math.random() * 10, h: h,
        kind: 'low'
      });
    } else if (kind === 'overhead') {
      obstacles.push({
        x: x, y: GROUND_Y - 32 - 160,
        w: 62, h: 160,
        kind: 'overhead'
      });
    } else {
      obstacles.push({ x: x, y: GROUND_Y - 38, w: 28, h: 38, kind: 'low' });
      obstacles.push({ x: x + 84 + Math.random() * 26, y: GROUND_Y - 38, w: 28, h: 38, kind: 'low' });
    }
  }

  // ================= 生成弹幕 =================
  function spawnEnemyBullet() {
    const d = difficulty();
    const y = 190 + Math.random() * 220;
    const speed = 300 + d * 280;
    const ang = (Math.random() - 0.5) * 0.22;

    enemyBullets.push({
      x: W + 40,
      y: y,
      vx: -Math.cos(ang) * speed,
      vy: Math.sin(ang) * speed,
      r: 11,
      phase: Math.random() * Math.PI * 2
    });
  }

  // ================= 单步逻辑 =================
  function step(dt) {
    const d = difficulty();
    state.speed = 300 + d * 520;
    state.meters += state.speed * dt / 10;
    state.scroll += state.speed * dt;

    if (state.skillCd > 0) {
      state.skillCd = Math.max(0, state.skillCd - dt);
    }

    // --- 玩家物理 ---
    player.vy += GRAVITY * dt;

    if (player.sliding && !onGround()) {
      player.vy += GRAVITY * 1.25 * dt;   // 空中按住滑行快速下坠
    }

    player.y += player.vy * dt;

    if (player.y >= GROUND_Y) {
      if (player.vy > 520) {
        spawnParticles(player.x, GROUND_Y, 6, '#6ef0ff', 160);
      }
      player.y = GROUND_Y;
      player.vy = 0;
      player.jumps = 0;
    }

    player.runPhase += dt * (state.speed / 55);

    if (onGround() && Math.random() < dt * 26) {
      if (particles.length > 340) particles.shift();
      particles.push({
        x: player.x - 12,
        y: GROUND_Y - 2,
        vx: -state.speed * 0.16 - Math.random() * 40,
        vy: -Math.random() * 70,
        life: 0.38, max: 0.38,
        r: 1.4 + Math.random() * 2.2,
        color: '#4de2ff'
      });
    }

    // --- 生成障碍 ---
    state.spawnTimer -= dt;
    if (state.spawnTimer <= 0) {
      spawnObstacle();
      const gap = (1.55 - d * 0.90) * (0.82 + Math.random() * 0.42);
      state.spawnTimer = Math.max(0.48, gap);
    }

    // --- 生成弹幕 ---
    state.bulletTimer -= dt;
    if (state.bulletTimer <= 0) {
      spawnEnemyBullet();
      const gap = (2.45 - d * 1.70) * (0.75 + Math.random() * 0.55);
      state.bulletTimer = Math.max(0.55, gap);
    }

    // --- 移动障碍 ---
    for (let i = obstacles.length - 1; i >= 0; i--) {
      const o = obstacles[i];
      o.x -= state.speed * dt;
      if (o.x + o.w < -80) obstacles.splice(i, 1);
    }

    // --- 移动敌方弹幕 ---
    for (let i = enemyBullets.length - 1; i >= 0; i--) {
      const b = enemyBullets[i];
      b.x += b.vx * dt;
      b.y += b.vy * dt;
      if (b.x + b.r < -50 || b.y < -60 || b.y > H + 60) {
        enemyBullets.splice(i, 1);
      }
    }

    // --- 移动玩家子弹 ---
    for (let i = playerBullets.length - 1; i >= 0; i--) {
      const b = playerBullets[i];
      b.x += b.vx * dt;
      b.y += b.vy * dt;
      b.life -= dt;
      if (b.life <= 0 || b.x > W + 50 || b.y < -50 || b.y > H + 50) {
        playerBullets.splice(i, 1);
      }
    }

    // --- 子弹 vs 弹幕 ---
    for (let i = playerBullets.length - 1; i >= 0; i--) {
      const pb = playerBullets[i];
      let hit = false;

      for (let j = enemyBullets.length - 1; j >= 0; j--) {
        const eb = enemyBullets[j];
        const dx = pb.x - eb.x;
        const dy = pb.y - eb.y;
        if (dx * dx + dy * dy < 30 * 30) {
          spawnParticles(eb.x, eb.y, 16, '#ff5ea8', 320);
          spawnParticles(eb.x, eb.y, 6, '#ffffff', 160);
          enemyBullets.splice(j, 1);
          state.kills++;
          state.score += 50;
          hit = true;
          break;
        }
      }

      if (hit) {
        playerBullets.splice(i, 1);
      }
    }

    // --- 玩家 vs 障碍 ---
    const pr = playerRect();
    for (let i = 0; i < obstacles.length; i++) {
      if (overlap(pr, obstacles[i])) {
        gameOver();
        return;
      }
    }

    // --- 玩家 vs 弹幕 ---
    for (let i = 0; i < enemyBullets.length; i++) {
      const b = enemyBullets[i];
      if (circleRect(b.x, b.y, b.r * 0.85, pr)) {
        gameOver();
        return;
      }
    }
  }

  // ================= 主更新 =================
  function update(dt) {
    state.time += dt;

    for (let i = 0; i < clouds.length; i++) {
      const c = clouds[i];
      const spd = state.mode === 'playing' ? state.speed : 90;
      c.x -= (c.sp * spd * 0.32 + 10) * dt;
      if (c.x < -160) {
        c.x = W + 160;
        c.y = 40 + Math.random() * 200;
        c.s = 0.6 + Math.random() * 0.9;
      }
    }

    if (state.shake > 0) {
      state.shake *= Math.pow(0.0008, dt);
      if (state.shake < 0.4) state.shake = 0;
    }

    updateParticles(dt);

    if (state.mode === 'over') state.overTimer += dt;
    if (state.mode !== 'playing') return;

    const steps = Math.max(1, Math.ceil(dt / 0.016));
    const sub = dt / steps;
    for (let i = 0; i < steps; i++) {
      if (state.mode !== 'playing') break;
      step(sub);
    }
  }

  // ================= 绘制:背景 =================
  function drawSky() {
    const d = difficulty();
    const g = ctx.createLinearGradient(0, 0, 0, H);
    g.addColorStop(0, 'hsl(' + (228 + d * 42) + ', 60%, 8%)');
    g.addColorStop(0.55, 'hsl(' + (250 + d * 55) + ', 55%, 17%)');
    g.addColorStop(1, 'hsl(' + (276 + d * 62) + ', 50%, 27%)');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);
  }

  function drawStars() {
    for (let i = 0; i < stars.length; i++) {
      const s = stars[i];
      const a = 0.22 + 0.78 * Math.abs(Math.sin(state.time * 1.4 + s.p));
      ctx.globalAlpha = a;
      ctx.fillStyle = '#cfe8ff';
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function drawClouds() {
    ctx.fillStyle = 'rgba(150, 190, 255, .065)';
    for (let i = 0; i < clouds.length; i++) {
      const c = clouds[i];
      ctx.beginPath();
      ctx.ellipse(c.x, c.y, 58 * c.s, 18 * c.s, 0, 0, Math.PI * 2);
      ctx.ellipse(c.x + 38 * c.s, c.y + 6 * c.s, 36 * c.s, 13 * c.s, 0, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawSkyline(parallax, color, baseY, maxH, period) {
    const scroll = state.scroll * parallax;
    const startIdx = Math.floor(scroll / period);
    const offset = scroll - startIdx * period;

    for (let i = -1; i <= Math.ceil(W / period) + 1; i++) {
      const idx = startIdx + i;
      const x = i * period - offset;
      const h = 42 + hash(idx) * maxH;

      ctx.fillStyle = color;
      ctx.fillRect(x, baseY - h, period - 5, h);

      const rows = Math.floor(h / 20);
      for (let r = 1; r < rows; r++) {
        for (let c = 0; c < 2; c++) {
          if (hash(idx * 31 + r * 7 + c * 13) > 0.58) {
            ctx.fillStyle = 'rgba(120, 200, 255, .13)';
            ctx.fillRect(x + 7 + c * 15, baseY - h + r * 20, 4, 7);
          }
        }
      }
    }
  }

  function drawGround() {
    ctx.fillStyle = '#070a1a';
    ctx.fillRect(0, GROUND_Y, W, H - GROUND_Y);

    ctx.save();
    ctx.shadowBlur = 18;
    ctx.shadowColor = '#4de2ff';
    ctx.strokeStyle = '#4de2ff';
    ctx.lineWidth = 2.5;
    ctx.beginPath();
    ctx.moveTo(0, GROUND_Y);
    ctx.lineTo(W, GROUND_Y);
    ctx.stroke();
    ctx.restore();

    const off = state.scroll % 56;
    ctx.strokeStyle = 'rgba(77, 226, 255, .11)';
    ctx.lineWidth = 3;
    for (let x = -off; x < W + 56; x += 56) {
      ctx.beginPath();
      ctx.moveTo(x, GROUND_Y + 26);
      ctx.lineTo(x + 26, GROUND_Y + 26);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(x + 26, GROUND_Y + 52);
      ctx.lineTo(x + 46, GROUND_Y + 52);
      ctx.stroke();
    }
  }

  // ================= 绘制:障碍 =================
  function drawObstacles() {
    for (let i = 0; i < obstacles.length; i++) {
      const o = obstacles[i];
      let color = '#ff4d6d', glow = '#ff2d55';

      if (o.kind === 'overhead') {
        color = '#ff9f43';
        glow = '#ff7f00';
      }

      ctx.save();
      ctx.shadowBlur = 18;
      ctx.shadowColor = glow;
      ctx.fillStyle = color;
      roundRect(ctx, o.x, o.y, o.w, o.h, 6);
      ctx.fill();
      ctx.restore();

      ctx.fillStyle = 'rgba(255, 255, 255, .24)';
      roundRect(ctx, o.x + 4, o.y + 4, Math.min(o.w - 8, 7), o.h - 8, 3);
      ctx.fill();

      if (o.kind === 'overhead') {
        ctx.save();
        ctx.shadowBlur = 16;
        ctx.shadowColor = glow;
        ctx.fillStyle = 'rgba(255, 200, 120, .75)';
        roundRect(ctx, o.x - 6, o.y - 6, o.w + 12, 10, 4);
        ctx.fill();
        ctx.restore();
      }
    }
  }

  // ================= 绘制:弹幕与子弹 =================
  function drawEnemyBullets() {
    for (let i = 0; i < enemyBullets.length; i++) {
      const b = enemyBullets[i];
      const pulse = 1 + Math.sin(state.time * 10 + b.phase) * 0.10;

      ctx.save();
      ctx.shadowBlur = 22;
      ctx.shadowColor = '#ff2d9e';

      const g = ctx.createRadialGradient(b.x, b.y, 1, b.x, b.y, b.r * 2.3 * pulse);
      g.addColorStop(0, '#ffe0f2');
      g.addColorStop(0.35, '#ff5ea8');
      g.addColorStop(1, 'rgba(170, 20, 95, 0)');

      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r * 2.3 * pulse, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();

      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r * 0.42, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawPlayerBullets() {
    for (let i = 0; i < playerBullets.length; i++) {
      const b = playerBullets[i];

      ctx.save();
      ctx.globalAlpha = 0.55;
      ctx.strokeStyle = '#6ef0ff';
      ctx.lineWidth = 5;
      ctx.lineCap = 'round';
      ctx.shadowBlur = 14;
      ctx.shadowColor = '#00c8ff';
      ctx.beginPath();
      ctx.moveTo(b.x, b.y);
      ctx.lineTo(b.x - b.vx * 0.028, b.y - b.vy * 0.028);
      ctx.stroke();
      ctx.restore();

      ctx.save();
      ctx.shadowBlur = 18;
      ctx.shadowColor = '#00e5ff';
      const g = ctx.createRadialGradient(b.x, b.y, 1, b.x, b.y, b.r * 2);
      g.addColorStop(0, '#ffffff');
      g.addColorStop(0.45, '#6ef0ff');
      g.addColorStop(1, 'rgba(0, 200, 255, 0)');
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r * 2, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
  }

  // ================= 绘制:玩家 =================
  function drawPlayer() {
    const r = playerRect();
    const cx = player.x;

    const airH = GROUND_Y - player.y;
    const shScale = Math.max(0, 1 - airH / 220);
    if (shScale > 0.02) {
      ctx.globalAlpha = 0.30 * shScale;
      ctx.fillStyle = '#000';
      ctx.beginPath();
      ctx.ellipse(player.x, GROUND_Y + 6, 26 * shScale + 4, 6 * shScale + 2, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 1;
    }

    ctx.save();
    ctx.strokeStyle = '#ff5ea8';
    ctx.lineWidth = 5;
    ctx.lineCap = 'round';
    ctx.shadowBlur = 14;
    ctx.shadowColor = '#ff5ea8';
    ctx.beginPath();
    const sy = r.y + r.h * 0.26;
    ctx.moveTo(r.x + 4, sy);
    ctx.quadraticCurveTo(
      r.x - 18, sy + Math.sin(state.time * 18) * 8,
      r.x - 36, sy + 8 + Math.sin(state.time * 18 + 1) * 10
    );
    ctx.stroke();
    ctx.restore();

    ctx.save();
    ctx.shadowBlur = 22;
    ctx.shadowColor = '#4de2ff';
    const g = ctx.createLinearGradient(r.x, r.y, r.x, r.y + r.h);
    g.addColorStop(0, '#9df6ff');
    g.addColorStop(1, '#3ac9ff');
    ctx.fillStyle = g;
    roundRect(ctx, r.x, r.y, r.w, r.h, 8);
    ctx.fill();
    ctx.restore();

    ctx.fillStyle = '#061426';
    const eyeY = r.y + r.h * 0.36;
    const eyeR = (player.sliding && onGround()) ? 2.6 : 3.4;
    ctx.beginPath();
    ctx.arc(cx - r.w * 0.20, eyeY, eyeR, 0, Math.PI * 2);
    ctx.arc(cx + r.w * 0.20, eyeY, eyeR, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = 'rgba(255, 255, 255, .9)';
    ctx.beginPath();
    ctx.arc(cx - r.w * 0.20 - 1, eyeY - 1, 1.2, 0, Math.PI * 2);
    ctx.arc(cx + r.w * 0.20 - 1, eyeY - 1, 1.2, 0, Math.PI * 2);
    ctx.fill();
  }

  // ================= 绘制:粒子 =================
  function drawParticles() {
    for (let i = 0; i < particles.length; i++) {
      const p = particles[i];
      ctx.globalAlpha = Math.max(0, p.life / p.max);
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  // ================= 绘制:按钮 =================
  function drawIconSkill() {
    ctx.beginPath();
    ctx.moveTo(-3, -20);
    ctx.lineTo(-15, 3);
    ctx.lineTo(-2, 3);
    ctx.lineTo(4, 21);
    ctx.lineTo(15, -3);
    ctx.lineTo(2, -3);
    ctx.closePath();
    ctx.fill();
  }
  function drawIconJump() {
    ctx.beginPath();
    ctx.moveTo(0, -21);
    ctx.lineTo(-17, 2);
    ctx.lineTo(-7, 2);
    ctx.lineTo(-7, 21);
    ctx.lineTo(7, 21);
    ctx.lineTo(7, 2);
    ctx.lineTo(17, 2);
    ctx.closePath();
    ctx.fill();
  }
  function drawIconSlide() {
    ctx.beginPath();
    ctx.moveTo(0, 19);
    ctx.lineTo(-15, -2);
    ctx.lineTo(-5, -2);
    ctx.lineTo(-5, -19);
    ctx.lineTo(5, -19);
    ctx.lineTo(5, -2);
    ctx.lineTo(15, -2);
    ctx.closePath();
    ctx.fill();
  }

  function drawButton(btn, active, iconFn, cooldownRatio) {
    const x = btn.x, y = btn.y, r = btn.r;

    ctx.save();
    ctx.globalAlpha = active ? 0.95 : 0.55;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fillStyle = 'rgba(8, 12, 28, .58)';
    ctx.fill();

    ctx.lineWidth = active ? 4 : 3;
    ctx.strokeStyle = btn.color;
    ctx.shadowBlur = active ? 26 : 12;
    ctx.shadowColor = btn.glow;
    ctx.stroke();
    ctx.restore();

    ctx.save();
    ctx.translate(x, y);
    ctx.fillStyle = btn.color;
    ctx.strokeStyle = btn.color;
    ctx.lineWidth = 3.5;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.shadowBlur = active ? 18 : 10;
    ctx.shadowColor = btn.glow;
    iconFn();
    ctx.restore();

    if (cooldownRatio > 0) {
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.arc(x, y, r - 1, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * cooldownRatio, false);
      ctx.closePath();
      ctx.fillStyle = 'rgba(5, 8, 20, .60)';
      ctx.fill();
    }
  }

  function drawButtons() {
    if (state.mode !== 'playing') return;

    const cd = state.skillCd > 0 ? state.skillCd / SKILL_CD : 0;
    drawButton(BTN.skill, pressed.skill, drawIconSkill, cd);
    drawButton(BTN.jump, pressed.jump, drawIconJump, 0);
    drawButton(BTN.slide, pressed.slide, drawIconSlide, 0);
  }

  // ================= 绘制:HUD =================
  function drawHUD() {
    if (state.mode === 'menu') return;

    const total = Math.floor(state.meters) + state.score;
    const d = difficulty();

    ctx.save();
    ctx.textAlign = 'left';

    ctx.fillStyle = '#e8f6ff';
    ctx.font = 'bold 30px system-ui, sans-serif';
    ctx.fillText(Math.floor(state.meters) + ' m', 28, 52);

    ctx.font = 'bold 17px system-ui, sans-serif';
    ctx.fillStyle = '#6ef0ff';
    ctx.fillText('得分 ' + total, 28, 80);

    ctx.font = '13px system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255, 170, 210, .75)';
    ctx.fillText('击落 ' + state.kills, 28, 104);

    ctx.textAlign = 'right';
    ctx.font = '13px system-ui, sans-serif';
    ctx.fillStyle = 'rgba(170, 215, 255, .62)';
    ctx.fillText('最高 ' + state.best, W - 28, 30);

    const bw = 150, bh = 8;
    const bx = W - bw - 28, by = 44;

    ctx.fillStyle = 'rgba(255, 255, 255, .12)';
    roundRect(ctx, bx, by, bw, bh, 4);
    ctx.fill();

    if (d > 0.01) {
      const g = ctx.createLinearGradient(bx, 0, bx + bw, 0);
      g.addColorStop(0, '#4de2ff');
      g.addColorStop(0.55, '#b06bff');
      g.addColorStop(1, '#ff4d6d');
      ctx.fillStyle = g;
      roundRect(ctx, bx, by, bw * d, bh, 4);
      ctx.fill();
    }

    ctx.textAlign = 'right';
    ctx.font = '12px system-ui, sans-serif';
    ctx.fillStyle = 'rgba(170, 215, 255, .55)';
    ctx.fillText('难度', bx + bw, by - 8);
    ctx.restore();
  }

  // ================= 绘制:覆盖层 =================
  function drawOverlay() {
    if (state.mode === 'playing') return;

    ctx.fillStyle = 'rgba(5, 7, 20, .76)';
    ctx.fillRect(0, 0, W, H);
    ctx.textAlign = 'center';

    if (state.mode === 'menu') {
      ctx.save();
      ctx.shadowBlur = 32;
      ctx.shadowColor = '#4de2ff';
      ctx.fillStyle = '#6ef0ff';
      ctx.font = 'bold 54px system-ui, sans-serif';
      ctx.fillText('疾风跑酷', W / 2, 168);
      ctx.restore();

      ctx.fillStyle = 'rgba(220, 240, 255, .86)';
      ctx.font = '17px system-ui, sans-serif';
      ctx.fillText('左手释放能量弹,拦截飞来的敌方弹幕', W / 2, 228);
      ctx.fillText('右手跳跃躲地面障碍,按住滑行穿过悬挂障碍', W / 2, 260);

      ctx.fillStyle = 'rgba(150, 220, 255, .7)';
      ctx.font = '14px system-ui, sans-serif';
      ctx.fillText('竖屏拿手机时画面会自动旋转为横屏', W / 2, 296);

      ctx.fillStyle = 'rgba(255, 160, 200, .82)';
      ctx.font = '15px system-ui, sans-serif';
      ctx.fillText('速度越来越快,弹幕越来越密 —— 你能跑多远?', W / 2, 332);

      ctx.fillStyle = '#ff5ea8';
      ctx.font = 'bold 22px system-ui, sans-serif';
      ctx.fillText('点击屏幕开始', W / 2, 406);
    } else {
      ctx.save();
      ctx.shadowBlur = 28;
      ctx.shadowColor = '#ff2d55';
      ctx.fillStyle = '#ff5ea8';
      ctx.font = 'bold 48px system-ui, sans-serif';
      ctx.fillText('撞车了!', W / 2, 150);
      ctx.restore();

      const total = Math.floor(state.meters) + state.score;
      ctx.fillStyle = '#e8f6ff';
      ctx.font = 'bold 46px system-ui, sans-serif';
      ctx.fillText(String(total), W / 2, 220);

      ctx.fillStyle = 'rgba(170, 215, 255, .7)';
      ctx.font = '16px system-ui, sans-serif';
      ctx.fillText('总分(距离 ' + Math.floor(state.meters) + ' m + 击落 ' + state.kills + ' × 50)', W / 2, 254);

      const isNew = total >= state.best && total > 0;
      ctx.fillStyle = isNew ? '#6ef0ff' : 'rgba(170, 215, 255, .7)';
      ctx.font = 'bold 17px system-ui, sans-serif';
      ctx.fillText(isNew ? '新纪录!' : '最高纪录 ' + state.best, W / 2, 292);

      if (state.overTimer > 0.6) {
        ctx.fillStyle = '#ff5ea8';
        ctx.font = 'bold 19px system-ui, sans-serif';
        ctx.fillText('点击屏幕重新开始', W / 2, 370);
      }
    }
  }

  // ================= 渲染 =================
  function render() {
    ctx.save();

    if (state.shake > 0) {
      ctx.translate(
        (Math.random() - 0.5) * state.shake,
        (Math.random() - 0.5) * state.shake
      );
    }

    drawSky();
    drawStars();
    drawClouds();
    drawSkyline(0.10, 'rgba(18, 22, 52, .95)', GROUND_Y, 130, 70);
    drawSkyline(0.28, 'rgba(10, 13, 34, .98)', GROUND_Y, 88, 54);
    drawGround();

    drawObstacles();
    drawEnemyBullets();
    drawPlayerBullets();
    drawParticles();

    if (state.mode !== 'over') drawPlayer();

    drawHUD();
    drawButtons();
    drawOverlay();

    ctx.restore();
  }

  // ================= 输入处理 =================
  function handleTap(p, pointerId) {
    if (state.mode === 'menu') {
      restart();
      return;
    }
    if (state.mode === 'over') {
      if (state.overTimer > 0.6) restart();
      return;
    }

    if (inCircle(p, BTN.skill)) {
      pressed.skill = true;
      fireSkill();
      return;
    }

    if (inCircle(p, BTN.jump)) {
      pressed.jump = true;
      doJump();
      return;
    }

    if (inCircle(p, BTN.slide)) {
      pressed.slide = true;
      slidePointers.add(pointerId);
      player.sliding = true;
      return;
    }

    // 区域兜底:左半屏技能,右半屏跳跃
    if (p.x < W / 2) {
      pressed.skill = true;
      fireSkill();
    } else {
      pressed.jump = true;
      doJump();
    }
  }

  document.addEventListener('pointerdown', function (e) {
    e.preventDefault();

    const p = getPos(e);
    const inside = p.x >= 0 && p.x <= W && p.y >= 0 && p.y <= H;

    // 游戏中只响应画面内的点击;菜单/结算允许点黑边
    if (state.mode === 'playing' && !inside) return;

    handleTap(p, e.pointerId);
  });

  function releasePointer(e) {
    pressed.skill = false;
    pressed.jump = false;

    if (slidePointers.has(e.pointerId)) {
      slidePointers.delete(e.pointerId);
      if (slidePointers.size === 0) {
        player.sliding = false;
        pressed.slide = false;
      }
    }
  }

  window.addEventListener('pointerup', releasePointer);
  window.addEventListener('pointercancel', releasePointer);

  // 键盘(电脑调试用)
  window.addEventListener('keydown', function (e) {
    if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
      e.preventDefault();
      if (!e.repeat) doJump();
      pressed.jump = true;
    }
    if (e.code === 'ArrowDown' || e.code === 'KeyS') {
      e.preventDefault();
      if (state.mode === 'playing') player.sliding = true;
      pressed.slide = true;
    }
    if (e.code === 'KeyJ' || e.code === 'KeyK' || e.code === 'KeyF' || e.code === 'ShiftLeft') {
      e.preventDefault();
      if (!e.repeat) fireSkill();
      pressed.skill = true;
    }
    if (e.code === 'KeyR' && state.mode !== 'menu') {
      restart();
    }
  });

  window.addEventListener('keyup', function (e) {
    if (e.code === 'ArrowDown' || e.code === 'KeyS') {
      player.sliding = false;
      pressed.slide = false;
    }
    if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
      pressed.jump = false;
    }
    if (e.code === 'KeyJ' || e.code === 'KeyK' || e.code === 'KeyF' || e.code === 'ShiftLeft') {
      pressed.skill = false;
    }
  });

  window.addEventListener('blur', function () {
    player.sliding = false;
    slidePointers.clear();
    pressed.skill = pressed.jump = pressed.slide = false;
  });

  // ================= 主循环 =================
  let last = performance.now();
  function loop(now) {
    let dt = (now - last) / 1000;
    last = now;
    if (dt > 0.05) dt = 0.05;
    update(dt);
    render();
    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Game Source: 疾风跑酷 · 自动横屏

Creator: LaserPirate33

Libraries: none

Complexity: complex (1213 lines, 34.2 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-laserpirate33" to link back to the original. Then publish at arcadelab.ai/publish.