🎮ArcadeLab

星尘收集者

by EpicCoder88
704 lines19.8 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>星尘收集者</title>
<style>
  html, body {
    margin: 0;
    height: 100%;
    background: #05060f;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
    -webkit-tap-highlight-color: transparent;
  }
  canvas {
    width: min(96vw, 170vh);
    height: auto;
    display: block;
    border-radius: 14px;
    box-shadow: 0 0 80px rgba(110, 130, 255, 0.28), 0 0 0 1px rgba(120, 150, 255, 0.12);
    touch-action: none;
  }
  #touch {
    position: fixed;
    inset: 0;
    pointer-events: none;
    display: none;
  }
  #touch button {
    pointer-events: auto;
    position: absolute;
    bottom: 26px;
    width: 78px;
    height: 78px;
    border-radius: 50%;
    border: 2px solid rgba(140, 180, 255, 0.45);
    background: rgba(40, 60, 120, 0.45);
    color: #cfe0ff;
    font-size: 26px;
    backdrop-filter: blur(6px);
    user-select: none;
  }
  #touch button:active { background: rgba(90, 130, 230, 0.6); }
  #btnL { left: 22px; }
  #btnR { left: 118px; }
  #btnJ { right: 22px; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="touch">
  <button id="btnL">◀</button>
  <button id="btnR">▶</button>
  <button id="btnJ">▲</button>
</div>
<script>
(() => {
  'use strict';

  // ===== 画布 =====
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const W = 960, H = 540;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = W * dpr;
  canvas.height = H * dpr;
  ctx.scale(dpr, dpr);

  // ===== 音效 =====
  let audioCtx = null;
  function initAudio() {
    if (!audioCtx) {
      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
    }
    if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  }
  function tone(freq, dur, type = 'square', vol = 0.05, slideTo = null) {
    if (!audioCtx) return;
    const t = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq, t);
    if (slideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t + dur);
    gain.gain.setValueAtTime(0.0001, t);
    gain.gain.exponentialRampToValueAtTime(vol, t + 0.008);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start(t);
    osc.stop(t + dur + 0.03);
  }

  // ===== 关卡数据 =====
  const LEVEL = {
    spawn: { x: 60, y: 440 },
    platforms: [
      { x: 0,   y: 500, w: 960, h: 40 },
      { x: 140, y: 405, w: 120, h: 18 },
      { x: 330, y: 330, w: 110, h: 18 },
      { x: 520, y: 405, w: 120, h: 18 },
      { x: 720, y: 330, w: 130, h: 18 },
      { x: 640, y: 225, w: 120, h: 18 },
      { x: 480, y: 150, w: 120, h: 18 },
      { x: 280, y: 215, w: 120, h: 18 },
      { x: 770, y: 130, w: 170, h: 18 }
    ],
    stars: [
      { x: 200, y: 370 }, { x: 385, y: 295 }, { x: 580, y: 375 }, { x: 785, y: 300 },
      { x: 700, y: 195 }, { x: 540, y: 120 }, { x: 340, y: 185 }, { x: 900, y: 465 }
    ],
    enemies: [
      { x: 525, y: 381, w: 22, h: 24, minX: 525, maxX: 640, dir: 1, speed: 1.4 },
      { x: 320, y: 476, w: 22, h: 24, minX: 320, maxX: 780, dir: 1, speed: 1.9 }
    ],
    goal: { x: 822, y: 40, w: 66, h: 90 }
  };

  // ===== 状态 =====
  let player, stars = [], enemies = [], particles = [], bgStars = [];
  let collectedCount = 0, deaths = 0, time = 0, frame = 0, shake = 0;
  let state = 'playing', winTimer = 0;

  const keys = {};

  for (let i = 0; i < 130; i++) {
    bgStars.push({
      x: Math.random() * W,
      y: Math.random() * H,
      r: Math.random() * 1.5 + 0.35,
      p: Math.random() * Math.PI * 2,
      s: 0.015 + Math.random() * 0.035
    });
  }

  // ===== 工具 =====
  function rectsOverlap(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 roundRect(x, y, w, h, r) {
    r = Math.min(r, w / 2, h / 2);
    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 starPath(cx, cy, outer, inner, points, rot) {
    ctx.beginPath();
    for (let i = 0; i < points * 2; i++) {
      const r = i % 2 === 0 ? outer : inner;
      const a = rot + i * Math.PI / points - Math.PI / 2;
      const x = cx + Math.cos(a) * r;
      const y = cy + Math.sin(a) * r;
      i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    }
    ctx.closePath();
  }
  function burst(x, y, color, n, power = 4) {
    for (let i = 0; i < n; i++) {
      const a = Math.random() * Math.PI * 2;
      const sp = 0.8 + Math.random() * power;
      particles.push({
        x, y,
        vx: Math.cos(a) * sp,
        vy: Math.sin(a) * sp - 1,
        life: 1,
        decay: 0.014 + Math.random() * 0.022,
        color,
        size: 1.5 + Math.random() * 2.6
      });
    }
  }
  function formatTime(t) {
    const m = Math.floor(t / 60);
    const s = Math.floor(t % 60);
    const cs = Math.floor((t % 1) * 100);
    return `${m}:${String(s).padStart(2, '0')}.${String(cs).padStart(2, '0')}`;
  }

  // ===== 初始化 =====
  function resetPlayer() {
    player.x = LEVEL.spawn.x;
    player.y = LEVEL.spawn.y;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
    player.coyote = 0;
    player.jumpBuffer = 0;
    player.facing = 1;
    player.dead = false;
    player.respawnTimer = 0;
  }

  function init() {
    player = { x: 0, y: 0, w: 20, h: 28, vx: 0, vy: 0, onGround: false, coyote: 0, jumpBuffer: 0, facing: 1, dead: false, respawnTimer: 0 };
    restart();
  }

  function restart() {
    stars = LEVEL.stars.map(s => ({ ...s, collected: false, phase: Math.random() * Math.PI * 2 }));
    enemies = LEVEL.enemies.map(e => ({ ...e }));
    particles = [];
    collectedCount = 0;
    deaths = 0;
    time = 0;
    frame = 0;
    shake = 0;
    winTimer = 0;
    state = 'playing';
    resetPlayer();
  }

  function killPlayer() {
    if (player.dead || state !== 'playing') return;
    player.dead = true;
    player.respawnTimer = 38;
    deaths++;
    shake = 13;
    burst(player.x + player.w / 2, player.y + player.h / 2, '#ff5c7a', 22, 5);
    tone(240, 0.32, 'sawtooth', 0.07, 60);
  }

  // ===== 物理 =====
  function moveAndCollide(ent) {
    // 水平
    ent.x += ent.vx;
    for (const p of LEVEL.platforms) {
      if (rectsOverlap(ent, p)) {
        if (ent.vx > 0) ent.x = p.x - ent.w;
        else if (ent.vx < 0) ent.x = p.x + p.w;
        ent.vx = 0;
      }
    }
    // 垂直
    ent.onGround = false;
    ent.y += ent.vy;
    for (const p of LEVEL.platforms) {
      if (rectsOverlap(ent, p)) {
        if (ent.vy > 0) { ent.y = p.y - ent.h; ent.onGround = true; }
        else if (ent.vy < 0) { ent.y = p.y + p.h; }
        ent.vy = 0;
      }
    }
  }

  // ===== 更新 =====
  function update() {
    frame++;
    if (shake > 0.4) shake *= 0.87; else shake = 0;

    // 粒子
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.vy += 0.16;
      p.vx *= 0.975;
      p.x += p.vx;
      p.y += p.vy;
      p.life -= p.decay;
      if (p.life <= 0) particles.splice(i, 1);
    }

    if (state === 'won') { winTimer++; return; }

    if (player.dead) {
      player.respawnTimer--;
      if (player.respawnTimer <= 0) resetPlayer();
      return;
    }

    time += 1 / 60;

    const left = keys['ArrowLeft'] || keys['KeyA'];
    const right = keys['ArrowRight'] || keys['KeyD'];
    const jumpHeld = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];

    // 水平加速度
    const ACC = 0.85, MAXV = 5.0;
    if (left) { player.vx -= ACC; player.facing = -1; }
    if (right) { player.vx += ACC; player.facing = 1; }
    if (!left && !right) {
      player.vx *= player.onGround ? 0.75 : 0.93;
      if (Math.abs(player.vx) < 0.12) player.vx = 0;
    }
    player.vx = Math.max(-MAXV, Math.min(MAXV, player.vx));

    // 土狼时间
    if (player.onGround) player.coyote = 8;
    else if (player.coyote > 0) player.coyote--;

    // 跳跃缓冲
    if (player.jumpBuffer > 0) player.jumpBuffer--;

    if (player.jumpBuffer > 0 && player.coyote > 0) {
      player.vy = -14;
      player.jumpBuffer = 0;
      player.coyote = 0;
      player.onGround = false;
      tone(520, 0.11, 'square', 0.045, 900);
      for (let i = 0; i < 6; i++) {
        burst(player.x + player.w / 2, player.y + player.h, 'rgba(140,200,255,0.9)', 1, 2);
      }
    }

    // 重力(可变跳跃高度)
    let g = 0.65;
    if (player.vy > 0) g = 0.85;
    if (!jumpHeld && player.vy < 0) g = 1.7;
    player.vy = Math.min(player.vy + g, 19);

    const wasOnGround = player.onGround;
    const impact = player.vy;
    moveAndCollide(player);

    if (!wasOnGround && player.onGround && impact > 6) {
      for (let i = 0; i < 7; i++) {
        burst(player.x + player.w / 2, player.y + player.h, 'rgba(150,190,255,0.8)', 1, 2.4);
      }
      tone(150, 0.06, 'sine', 0.028);
    }

    // 边界
    player.x = Math.max(0, Math.min(W - player.w, player.x));

    // 掉出屏幕
    if (player.y > H + 80) killPlayer();

    // 敌人
    for (const e of enemies) {
      e.x += e.dir * e.speed;
      if (e.x <= e.minX) { e.x = e.minX; e.dir = 1; }
      if (e.x + e.w >= e.maxX) { e.x = e.maxX - e.w; e.dir = -1; }
      if (rectsOverlap(player, e)) killPlayer();
    }

    // 星星
    for (const s of stars) {
      if (s.collected) continue;
      const dx = (player.x + player.w / 2) - s.x;
      const dy = (player.y + player.h / 2) - s.y;
      if (dx * dx + dy * dy < 900) {
        s.collected = true;
        collectedCount++;
        burst(s.x, s.y, '#ffd93d', 16, 4);
        shake = Math.max(shake, 4);
        tone(760 + collectedCount * 45, 0.13, 'sine', 0.06, 1400);
      }
    }

    // 传送门
    if (collectedCount === stars.length && state === 'playing') {
      if (rectsOverlap(player, LEVEL.goal)) {
        state = 'won';
        shake = 16;
        burst(LEVEL.goal.x + LEVEL.goal.w / 2, LEVEL.goal.y + LEVEL.goal.h / 2, '#7bf1a8', 40, 7);
        tone(660, 0.5, 'sine', 0.07, 1320);
        setTimeout(() => tone(990, 0.5, 'sine', 0.06, 1760), 120);
      }
    }
  }

  // ===== 绘制 =====
  function drawBackground() {
    const g = ctx.createLinearGradient(0, 0, 0, H);
    g.addColorStop(0, '#080c22');
    g.addColorStop(0.55, '#111a3c');
    g.addColorStop(1, '#191036');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);

    for (const s of bgStars) {
      const a = 0.22 + Math.sin(frame * s.s + s.p) * 0.24;
      ctx.globalAlpha = Math.max(0, a);
      ctx.fillStyle = s.r > 1.15 ? '#cfe0ff' : '#7f97d8';
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    const g2 = ctx.createRadialGradient(W / 2, H + 120, 40, W / 2, H + 120, 620);
    g2.addColorStop(0, 'rgba(95,72,210,0.35)');
    g2.addColorStop(1, 'rgba(95,72,210,0)');
    ctx.fillStyle = g2;
    ctx.fillRect(0, 0, W, H);
  }

  function drawPlatforms() {
    for (const p of LEVEL.platforms) {
      const g = ctx.createLinearGradient(0, p.y, 0, p.y + p.h);
      g.addColorStop(0, '#4d63b0');
      g.addColorStop(1, '#232c60');
      ctx.fillStyle = g;
      roundRect(p.x, p.y, p.w, p.h, 7);
      ctx.fill();

      ctx.fillStyle = 'rgba(168, 205, 255, 0.65)';
      roundRect(p.x + 3, p.y + 2, p.w - 6, 2.4, 1.2);
      ctx.fill();
    }
  }

  function drawStarsCollectibles() {
    for (const s of stars) {
      if (s.collected) continue;
      const t = frame * 0.05 + s.phase;
      const fy = s.y + Math.sin(t) * 4.5;
      ctx.save();
      ctx.shadowColor = '#ffd93d';
      ctx.shadowBlur = 18;
      ctx.fillStyle = '#ffe66d';
      starPath(s.x, fy, 11, 5, 5, t * 0.7);
      ctx.fill();
      ctx.restore();

      ctx.save();
      ctx.globalAlpha = 0.35 + Math.sin(t * 2) * 0.2;
      ctx.fillStyle = '#fff6b0';
      starPath(s.x, fy, 4.5, 2, 5, -t);
      ctx.fill();
      ctx.restore();
    }
  }

  function drawGoal() {
    const gx = LEVEL.goal.x + LEVEL.goal.w / 2;
    const gy = LEVEL.goal.y + LEVEL.goal.h / 2;
    const active = collectedCount === stars.length;
    const t = frame * 0.035;

    ctx.save();
    if (active) {
      ctx.shadowColor = '#7bf1a8';
      ctx.shadowBlur = 26;
      ctx.strokeStyle = 'rgba(123, 241, 168, 0.95)';
    } else {
      ctx.strokeStyle = 'rgba(110, 135, 190, 0.35)';
    }
    ctx.lineWidth = 3.5;

    for (let i = 0; i < 3; i++) {
      const pulse = 1 + Math.sin(t + i * 1.1) * 0.06;
      ctx.globalAlpha = active ? (0.9 - i * 0.22) : (0.5 - i * 0.13);
      ctx.beginPath();
      ctx.ellipse(gx, gy, 26 * pulse + i * 5, 42 * pulse + i * 5, 0, 0, Math.PI * 2);
      ctx.stroke();
    }
    ctx.globalAlpha = 1;

    if (active) {
      const core = ctx.createRadialGradient(gx, gy, 2, gx, gy, 34);
      core.addColorStop(0, 'rgba(200, 255, 225, 0.85)');
      core.addColorStop(0.5, 'rgba(123, 241, 168, 0.35)');
      core.addColorStop(1, 'rgba(123, 241, 168, 0)');
      ctx.fillStyle = core;
      ctx.beginPath();
      ctx.ellipse(gx, gy, 30, 46, 0, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }

  function drawEnemies() {
    for (const e of enemies) {
      const cx = e.x + e.w / 2;
      const cy = e.y + e.h / 2;
      const rot = frame * 0.045;

      ctx.save();
      ctx.shadowColor = '#ff4d6d';
      ctx.shadowBlur = 16;
      ctx.fillStyle = '#ff4d6d';
      ctx.beginPath();
      for (let i = 0; i < 20; i++) {
        const r = i % 2 === 0 ? 13.5 : 9;
        const a = rot + i * Math.PI / 10;
        const x = cx + Math.cos(a) * r;
        const y = cy + Math.sin(a) * r * 1.05;
        i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
      }
      ctx.closePath();
      ctx.fill();
      ctx.restore();

      ctx.fillStyle = '#2a0410';
      ctx.beginPath();
      ctx.arc(cx - 4, cy - 2, 2.3, 0, Math.PI * 2);
      ctx.arc(cx + 4, cy - 2, 2.3, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawPlayer() {
    const { x, y, w, h } = player;
    ctx.save();
    ctx.shadowColor = '#66d9ff';
    ctx.shadowBlur = 16;
    const g = ctx.createLinearGradient(0, y, 0, y + h);
    g.addColorStop(0, '#a8f0ff');
    g.addColorStop(1, '#3fb8e8');
    ctx.fillStyle = g;
    roundRect(x, y, w, h, 7);
    ctx.fill();
    ctx.restore();

    ctx.fillStyle = '#0b1020';
    const ex = player.facing > 0 ? 1.6 : -1.6;
    ctx.beginPath();
    ctx.arc(x + w * 0.34 + ex, y + 10, 2.6, 0, Math.PI * 2);
    ctx.arc(x + w * 0.68 + ex, y + 10, 2.6, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawParticles() {
    for (const p of particles) {
      ctx.globalAlpha = Math.max(0, p.life);
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function drawHUD() {
    ctx.save();
    ctx.textBaseline = 'middle';

    // 星星计数
    ctx.save();
    ctx.shadowColor = '#ffd93d';
    ctx.shadowBlur = 10;
    ctx.fillStyle = '#ffe66d';
    starPath(34, 34, 11, 5, 5, frame * 0.02);
    ctx.fill();
    ctx.restore();

    ctx.fillStyle = '#e8ecff';
    ctx.font = 'bold 22px system-ui, -apple-system, sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText(`${collectedCount} / ${stars.length}`, 54, 36);

    // 计时 / 失误
    ctx.textAlign = 'right';
    ctx.fillStyle = 'rgba(205, 220, 255, 0.85)';
    ctx.font = 'bold 21px system-ui, -apple-system, sans-serif';
    ctx.fillText(formatTime(time), W - 24, 32);
    ctx.fillStyle = 'rgba(205, 220, 255, 0.45)';
    ctx.font = '15px system-ui, -apple-system, sans-serif';
    ctx.fillText(`失误 ${deaths}`, W - 24, 58);

    // 顶部提示
    ctx.textAlign = 'center';
    if (state === 'playing') {
      if (collectedCount < stars.length) {
        ctx.fillStyle = 'rgba(175, 195, 255, 0.3)';
        ctx.font = '16px system-ui, -apple-system, sans-serif';
        ctx.fillText('收集所有星尘,传送门才会开启', W / 2, 28);
      } else {
        const a = 0.55 + Math.sin(frame * 0.13) * 0.4;
        ctx.fillStyle = `rgba(123, 241, 168, ${a})`;
        ctx.font = 'bold 17px system-ui, -apple-system, sans-serif';
        ctx.fillText('✦ 传送门已开启 ✦', W / 2, 28);
      }
    }
    ctx.restore();

    // 操作提示(前几秒)
    if (frame < 300 && state === 'playing') {
      ctx.save();
      ctx.globalAlpha = Math.max(0, 1 - frame / 300) * 0.5;
      ctx.textAlign = 'center';
      ctx.fillStyle = '#cfe0ff';
      ctx.font = '16px system-ui, -apple-system, sans-serif';
      ctx.fillText('← → / A D 移动 空格 / ↑ 跳跃 R 重开', W / 2, H - 34);
      ctx.restore();
    }
  }

  function drawWin() {
    ctx.save();
    ctx.fillStyle = 'rgba(5, 8, 20, 0.74)';
    ctx.fillRect(0, 0, W, H);

    const scale = 1 + Math.min(1, winTimer / 20) * 0 + 1;
    ctx.textAlign = 'center';

    ctx.shadowColor = '#7bf1a8';
    ctx.shadowBlur = 26;
    ctx.fillStyle = '#7bf1a8';
    ctx.font = 'bold 58px system-ui, -apple-system, sans-serif';
    ctx.fillText('✦ 通关 ✦', W / 2, H / 2 - 70);
    ctx.shadowBlur = 0;

    ctx.fillStyle = '#e8ecff';
    ctx.font = 'bold 30px system-ui, -apple-system, sans-serif';
    ctx.fillText(`用时 ${formatTime(time)}`, W / 2, H / 2 + 5);

    ctx.fillStyle = 'rgba(200, 215, 255, 0.7)';
    ctx.font = '20px system-ui, -apple-system, sans-serif';
    ctx.fillText(`失误 ${deaths} 次`, W / 2, H / 2 + 50);

    const a = 0.5 + Math.sin(winTimer * 0.1) * 0.4;
    ctx.fillStyle = `rgba(160, 200, 255, ${a})`;
    ctx.font = 'bold 18px system-ui, -apple-system, sans-serif';
    ctx.fillText('按 R 重新开始', W / 2, H / 2 + 110);
    ctx.restore();
  }

  function render() {
    ctx.save();
    ctx.clearRect(0, 0, W, H);

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

    drawBackground();
    drawPlatforms();
    drawStarsCollectibles();
    drawGoal();
    drawEnemies();
    if (!player.dead) drawPlayer();
    drawParticles();
    drawHUD();

    if (state === 'won') drawWin();

    ctx.restore();
  }

  // ===== 主循环 =====
  const STEP = 1000 / 60;
  let lastTime = performance.now();
  let acc = 0;

  function loop(now) {
    requestAnimationFrame(loop);
    let delta = now - lastTime;
    lastTime = now;
    if (delta > 200) delta = 200;
    acc += delta;
    let guard = 0;
    while (acc >= STEP && guard++ < 5) {
      update();
      acc -= STEP;
    }
    render();
  }

  // ===== 输入 =====
  window.addEventListener('keydown', e => {
    initAudio();
    if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) e.preventDefault();

    if (!e.repeat) {
      if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
        player.jumpBuffer = 8;
      }
      if (e.code === 'KeyR') restart();
    }
    keys[e.code] = true;
  });

  window.addEventListener('keyup', e => {
    keys[e.code] = false;
  });

  // 触屏
  const touchBar = document.getElementById('touch');
  if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
    touchBar.style.display = 'block';
    const bind = (id, code, isJump) => {
      const el = document.getElementById(id);
      const on = ev => {
        ev.preventDefault();
        initAudio();
        keys[code] = true;
        if (isJump) player.jumpBuffer = 8;
      };
      const off = ev => { ev.preventDefault(); keys[code] = false; };
      el.addEventListener('pointerdown', on);
      el.addEventListener('pointerup', off);
      el.addEventListener('pointercancel', off);
      el.addEventListener('pointerleave', off);
    };
    bind('btnL', 'ArrowLeft', false);
    bind('btnR', 'ArrowRight', false);
    bind('btnJ', 'Space', true);
  }

  // ===== 启动 =====
  init();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Game Source: 星尘收集者

Creator: EpicCoder88

Libraries: none

Complexity: complex (704 lines, 19.8 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-epiccoder88-murui34x" to link back to the original. Then publish at arcadelab.ai/publish.