🎮ArcadeLab

黄金矿工

by ThunderBolt64
872 lines22.0 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>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    width: 100%; height: 100%;
    overflow: hidden;
    background: #1a0a00;
    font-family: 'Arial', sans-serif;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
  }
  #gameContainer {
    position: relative;
    width: 100vw; height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  canvas {
    display: block;
    background: #2c1a0e;
  }
  /* HUD overlay */
  #hud {
    position: absolute;
    top: 0; left: 0; right: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 16px;
    background: linear-gradient(180deg, rgba(0,0,0,0.7) 0%, transparent 100%);
    color: #fff;
    font-size: 16px;
    font-weight: bold;
    z-index: 10;
    pointer-events: none;
  }
  #hud span { text-shadow: 1px 1px 3px rgba(0,0,0,0.8); }
  .hud-item { display: flex; align-items: center; gap: 6px; }
  .hud-icon { font-size: 20px; }

  /* Overlay screens */
  .overlay {
    position: absolute;
    top: 0; left: 0; width: 100%; height: 100%;
    background: rgba(0,0,0,0.75);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    z-index: 20;
    color: #fff;
    text-align: center;
  }
  .overlay.hidden { display: none; }
  .overlay h1 {
    font-size: 42px;
    margin-bottom: 12px;
    text-shadow: 0 0 20px #ffd700, 0 0 40px #ff8c00;
    color: #ffd700;
  }
  .overlay h2 {
    font-size: 28px;
    margin-bottom: 8px;
    color: #ffd700;
    text-shadow: 0 0 10px #ff8c00;
  }
  .overlay p {
    font-size: 18px;
    margin-bottom: 20px;
    color: #ddd;
    max-width: 320px;
    line-height: 1.5;
  }
  .overlay .score-display {
    font-size: 36px;
    color: #ffd700;
    font-weight: bold;
    margin: 10px 0 20px;
    text-shadow: 0 0 15px #ff8c00;
  }
  .btn {
    padding: 14px 48px;
    font-size: 20px;
    font-weight: bold;
    border: none;
    border-radius: 50px;
    cursor: pointer;
    background: linear-gradient(135deg, #ffd700, #ff8c00);
    color: #1a0a00;
    box-shadow: 0 4px 15px rgba(255,215,0,0.4);
    transition: transform 0.15s, box-shadow 0.15s;
    -webkit-tap-highlight-color: transparent;
  }
  .btn:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(255,215,0,0.6); }
  .btn:active { transform: scale(0.97); }

  /* Level transition */
  .overlay .level-tag {
    display: inline-block;
    background: #ff8c00;
    color: #1a0a00;
    padding: 4px 16px;
    border-radius: 20px;
    font-size: 14px;
    font-weight: bold;
    margin-bottom: 10px;
  }

  /* How to play hint */
  .hint {
    font-size: 14px;
    color: #aaa;
    margin-top: 16px;
  }
</style>
</head>
<body>
<div id="gameContainer">
  <canvas id="gameCanvas"></canvas>

  <!-- HUD -->
  <div id="hud">
    <div class="hud-item"><span class="hud-icon">🪙</span><span id="scoreText">0</span></div>
    <div class="hud-item"><span id="levelText">第 1 关</span></div>
    <div class="hud-item"><span class="hud-icon">⏱</span><span id="timerText">30</span></div>
  </div>

  <!-- Start Screen -->
  <div id="startScreen" class="overlay">
    <h1>⛏ 黄金矿工</h1>
    <p>控制钩子抓取地下的金块和钻石,在限时内尽量多得分!</p>
    <button class="btn" id="startBtn">开始游戏</button>
    <p class="hint">点击屏幕 / 按空格键 释放钩子</p>
  </div>

  <!-- Level Complete Screen -->
  <div id="levelScreen" class="overlay hidden">
    <div class="level-tag" id="levelTag">第 1 关</div>
    <h2>时间到!</h2>
    <div class="score-display" id="levelScore">0</div>
    <button class="btn" id="nextBtn">下一关</button>
  </div>

  <!-- Game Over Screen -->
  <div id="endScreen" class="overlay hidden">
    <h1>游戏结束</h1>
    <div class="score-display" id="finalScore">0</div>
    <button class="btn" id="restartBtn">再来一次</button>
  </div>
</div>

<script>
// ===================== GAME CONFIG =====================
const CONFIG = {
  LEVELS: 2,
  TIME_PER_LEVEL: 30,
  HOOK_SWING_SPEED: 0.025,
  HOOK_EXTEND_SPEED: 4,
  HOOK_RETRACT_SPEED: 3,
  HOOK_RETRACT_SPEED_LOADED: 1.2,
  HOOK_MAX_LENGTH_RATIO: 0.85,
  MIN_ANGLE: -Math.PI * 0.42,
  MAX_ANGLE: Math.PI * 0.42,
  GROUND_RATIO: 0.18,      // ground starts at 18% from top
  MINER_RATIO: 0.14,       // miner Y position
};

// Item types: name, score, radius, weight (affects retract speed), color
const ITEM_TYPES = {
  SMALL_GOLD:  { name: '小金块', score: 50,  radius: 16, weight: 1,   color: '#FFD700', type: 'gold' },
  MEDIUM_GOLD: { name: '中金块', score: 150, radius: 24, weight: 2,   color: '#FFC107', type: 'gold' },
  LARGE_GOLD:  { name: '大金块', score: 300, radius: 34, weight: 3.5, color: '#FF9800', type: 'gold' },
  DIAMOND:     { name: '钻石',   score: 500, radius: 14, weight: 0.5, color: '#00E5FF', type: 'diamond' },
  ROCK:        { name: '石头',   score: 10,  radius: 28, weight: 4,   color: '#8D6E63', type: 'rock' },
};

// Level configs: what items to spawn
const LEVEL_CONFIG = [
  {
    items: [
      { type: 'SMALL_GOLD', count: 5 },
      { type: 'MEDIUM_GOLD', count: 3 },
      { type: 'LARGE_GOLD', count: 1 },
      { type: 'DIAMOND', count: 1 },
      { type: 'ROCK', count: 2 },
    ]
  },
  {
    items: [
      { type: 'SMALL_GOLD', count: 3 },
      { type: 'MEDIUM_GOLD', count: 3 },
      { type: 'LARGE_GOLD', count: 2 },
      { type: 'DIAMOND', count: 2 },
      { type: 'ROCK', count: 4 },
    ]
  },
];

// ===================== CANVAS SETUP =====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

function resizeCanvas() {
  const ratio = window.devicePixelRatio || 1;
  const w = window.innerWidth;
  const h = window.innerHeight;
  canvas.width = w * ratio;
  canvas.height = h * ratio;
  canvas.style.width = w + 'px';
  canvas.style.height = h + 'px';
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();

// ===================== GAME STATE =====================
let game = {
  state: 'idle',  // idle | playing | levelEnd | gameOver
  level: 1,
  score: 0,
  levelScore: 0,
  timeLeft: CONFIG.TIME_PER_LEVEL,
  timerInterval: null,
  items: [],
  hook: {
    angle: 0,
    swingDir: 1,
    length: 0,
    maxLength: 0,
    state: 'swinging', // swinging | extending | retracting
    tipX: 0,
    tipY: 0,
    grabbed: null,
  },
  particles: [],
};

// ===================== DOM REFS =====================
const scoreText = document.getElementById('scoreText');
const levelText = document.getElementById('levelText');
const timerText = document.getElementById('timerText');
const startScreen = document.getElementById('startScreen');
const levelScreen = document.getElementById('levelScreen');
const endScreen = document.getElementById('endScreen');
const levelTag = document.getElementById('levelTag');
const levelScoreEl = document.getElementById('levelScore');
const finalScoreEl = document.getElementById('finalScore');

// ===================== HELPERS =====================
function getW() { return window.innerWidth; }
function getH() { return window.innerHeight; }
function getMinerX() { return getW() / 2; }
function getMinerY() { return getH() * CONFIG.MINER_RATIO; }
function getGroundY() { return getH() * CONFIG.GROUND_RATIO; }

function dist(x1, y1, x2, y2) {
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

function rand(min, max) {
  return Math.random() * (max - min) + min;
}

// ===================== ITEMS =====================
function spawnItems(levelIdx) {
  const items = [];
  const cfg = LEVEL_CONFIG[levelIdx];
  const groundY = getGroundY() + 40;
  const bottomY = getH() - 40;
  const leftX = 50;
  const rightX = getW() - 50;

  cfg.items.forEach(entry => {
    const template = ITEM_TYPES[entry.type];
    for (let i = 0; i < entry.count; i++) {
      let x, y, overlapping;
      let attempts = 0;
      // avoid overlap
      do {
        x = rand(leftX + template.radius, rightX - template.radius);
        y = rand(groundY + template.radius, bottomY - template.radius);
        overlapping = items.some(it => dist(x, y, it.x, it.y) < it.radius + template.radius + 10);
        attempts++;
      } while (overlapping && attempts < 50);

      items.push({
        ...template,
        x, y,
        grabbed: false,
        sparkle: Math.random() * Math.PI * 2,
      });
    }
  });
  return items;
}

// ===================== PARTICLES =====================
function addParticles(x, y, color, count) {
  for (let i = 0; i < count; i++) {
    game.particles.push({
      x, y,
      vx: rand(-3, 3),
      vy: rand(-4, -1),
      life: rand(20, 40),
      maxLife: 40,
      color,
      size: rand(2, 5),
    });
  }
}

function updateParticles() {
  game.particles = game.particles.filter(p => {
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.12;
    p.life--;
    return p.life > 0;
  });
}

function drawParticles() {
  game.particles.forEach(p => {
    const alpha = p.life / p.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
    ctx.fill();
  });
  ctx.globalAlpha = 1;
}

// ===================== DRAWING =====================
function drawBackground() {
  const w = getW(), h = getH();
  const groundY = getGroundY();

  // Sky
  const skyGrad = ctx.createLinearGradient(0, 0, 0, groundY);
  skyGrad.addColorStop(0, '#87CEEB');
  skyGrad.addColorStop(1, '#E8D5B7');
  ctx.fillStyle = skyGrad;
  ctx.fillRect(0, 0, w, groundY);

  // Ground
  const groundGrad = ctx.createLinearGradient(0, groundY, 0, h);
  groundGrad.addColorStop(0, '#8B6914');
  groundGrad.addColorStop(0.3, '#6B4E0A');
  groundGrad.addColorStop(1, '#3E2A04');
  ctx.fillStyle = groundGrad;
  ctx.fillRect(0, groundY, w, h - groundY);

  // Ground line
  ctx.strokeStyle = '#5C8A2F';
  ctx.lineWidth = 4;
  ctx.beginPath();
  ctx.moveTo(0, groundY);
  ctx.lineTo(w, groundY);
  ctx.stroke();

  // Grass tufts
  ctx.fillStyle = '#5C8A2F';
  for (let x = 0; x < w; x += 30) {
    ctx.beginPath();
    ctx.moveTo(x, groundY);
    ctx.lineTo(x + 5, groundY - 8);
    ctx.lineTo(x + 10, groundY);
    ctx.fill();
  }

  // Underground dirt texture (subtle dots)
  ctx.fillStyle = 'rgba(139,105,20,0.3)';
  for (let i = 0; i < 60; i++) {
    const dx = (i * 137.5) % w;
    const dy = groundY + 30 + (i * 97.3) % (h - groundY - 50);
    ctx.beginPath();
    ctx.arc(dx, dy, 2, 0, Math.PI * 2);
    ctx.fill();
  }
}

function drawMiner() {
  const mx = getMinerX();
  const my = getMinerY();

  // Body
  ctx.fillStyle = '#D32F2F';
  ctx.fillRect(mx - 12, my - 10, 24, 30);

  // Head
  ctx.fillStyle = '#FFCCBC';
  ctx.beginPath();
  ctx.arc(mx, my - 20, 14, 0, Math.PI * 2);
  ctx.fill();

  // Hard hat
  ctx.fillStyle = '#FFC107';
  ctx.beginPath();
  ctx.ellipse(mx, my - 28, 16, 8, 0, Math.PI, 0);
  ctx.fill();
  ctx.fillRect(mx - 16, my - 28, 32, 4);

  // Hat light
  ctx.fillStyle = '#FF5722';
  ctx.beginPath();
  ctx.arc(mx, my - 32, 4, 0, Math.PI * 2);
  ctx.fill();

  // Eyes
  ctx.fillStyle = '#333';
  ctx.beginPath();
  ctx.arc(mx - 5, my - 22, 2, 0, Math.PI * 2);
  ctx.arc(mx + 5, my - 22, 2, 0, Math.PI * 2);
  ctx.fill();

  // Arms
  ctx.strokeStyle = '#FFCCBC';
  ctx.lineWidth = 4;
  ctx.beginPath();
  ctx.moveTo(mx - 12, my);
  ctx.lineTo(mx - 22, my + 12);
  ctx.moveTo(mx + 12, my);
  ctx.lineTo(mx + 22, my + 12);
  ctx.stroke();
}

function drawRopeAndHook() {
  const mx = getMinerX();
  const my = getMinerY() + 15;
  const hook = game.hook;

  // Calculate hook tip
  hook.tipX = mx + Math.sin(hook.angle) * hook.length;
  hook.tipY = my + Math.cos(hook.angle) * hook.length;

  // Rope
  ctx.strokeStyle = '#8D6E63';
  ctx.lineWidth = 3;
  ctx.beginPath();
  ctx.moveTo(mx, my);
  ctx.lineTo(hook.tipX, hook.tipY);
  ctx.stroke();

  // Hook
  const hs = 10; // hook size
  ctx.strokeStyle = '#B0BEC5';
  ctx.lineWidth = 3;
  ctx.lineCap = 'round';

  if (hook.grabbed) {
    // Closed claw
    ctx.beginPath();
    ctx.moveTo(hook.tipX - hs, hook.tipY - 4);
    ctx.lineTo(hook.tipX, hook.tipY + 4);
    ctx.lineTo(hook.tipX + hs, hook.tipY - 4);
    ctx.stroke();
  } else {
    // Open claw
    ctx.beginPath();
    ctx.moveTo(hook.tipX - hs * 0.7, hook.tipY);
    ctx.lineTo(hook.tipX, hook.tipY + 8);
    ctx.lineTo(hook.tipX + hs * 0.7, hook.tipY);
    ctx.stroke();
  }
  ctx.lineCap = 'butt';
}

function drawItems() {
  const now = Date.now() / 1000;
  game.items.forEach(item => {
    if (item.grabbed) return;

    ctx.save();
    ctx.translate(item.x, item.y);

    if (item.type === 'diamond') {
      // Diamond shape
      const r = item.radius;
      const sparkle = Math.sin(now * 3 + item.sparkle) * 0.15 + 1;
      ctx.scale(sparkle, sparkle);

      // Glow
      ctx.shadowColor = '#00E5FF';
      ctx.shadowBlur = 12;

      ctx.fillStyle = item.color;
      ctx.beginPath();
      ctx.moveTo(0, -r);
      ctx.lineTo(r * 0.7, 0);
      ctx.lineTo(0, r);
      ctx.lineTo(-r * 0.7, 0);
      ctx.closePath();
      ctx.fill();

      // Highlight
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.beginPath();
      ctx.moveTo(0, -r * 0.6);
      ctx.lineTo(r * 0.3, -r * 0.1);
      ctx.lineTo(0, r * 0.1);
      ctx.lineTo(-r * 0.3, -r * 0.1);
      ctx.closePath();
      ctx.fill();

      ctx.shadowBlur = 0;
    } else if (item.type === 'gold') {
      // Gold nugget (rounded rectangle-ish)
      const r = item.radius;
      const sparkle = Math.sin(now * 2 + item.sparkle) * 0.08 + 1;
      ctx.scale(sparkle, sparkle);

      // Glow
      ctx.shadowColor = '#FFD700';
      ctx.shadowBlur = 8;

      ctx.fillStyle = item.color;
      ctx.beginPath();
      ctx.moveTo(-r * 0.8, -r * 0.5);
      ctx.lineTo(r * 0.6, -r * 0.6);
      ctx.lineTo(r * 0.9, r * 0.2);
      ctx.lineTo(r * 0.3, r * 0.7);
      ctx.lineTo(-r * 0.7, r * 0.6);
      ctx.lineTo(-r * 0.95, r * 0);
      ctx.closePath();
      ctx.fill();

      // Shine
      ctx.fillStyle = 'rgba(255,255,255,0.35)';
      ctx.beginPath();
      ctx.ellipse(-r * 0.2, -r * 0.2, r * 0.35, r * 0.2, -0.3, 0, Math.PI * 2);
      ctx.fill();

      ctx.shadowBlur = 0;
    } else if (item.type === 'rock') {
      // Rock
      const r = item.radius;
      ctx.fillStyle = item.color;
      ctx.beginPath();
      ctx.moveTo(-r * 0.7, -r * 0.5);
      ctx.lineTo(r * 0.3, -r * 0.7);
      ctx.lineTo(r * 0.8, -r * 0.2);
      ctx.lineTo(r * 0.6, r * 0.6);
      ctx.lineTo(-r * 0.2, r * 0.7);
      ctx.lineTo(-r * 0.9, r * 0.3);
      ctx.closePath();
      ctx.fill();

      // Texture
      ctx.strokeStyle = 'rgba(0,0,0,0.2)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(-r * 0.3, -r * 0.3);
      ctx.lineTo(r * 0.2, r * 0.1);
      ctx.stroke();
    }

    ctx.restore();
  });
}

// Draw grabbed item following hook
function drawGrabbedItem() {
  const hook = game.hook;
  if (!hook.grabbed) return;

  const item = hook.grabbed;
  ctx.save();
  ctx.translate(hook.tipX, hook.tipY + item.radius * 0.5);

  // Simple draw for grabbed
  if (item.type === 'diamond') {
    const r = item.radius * 0.9;
    ctx.fillStyle = item.color;
    ctx.shadowColor = '#00E5FF';
    ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.moveTo(0, -r);
    ctx.lineTo(r * 0.7, 0);
    ctx.lineTo(0, r);
    ctx.lineTo(-r * 0.7, 0);
    ctx.closePath();
    ctx.fill();
    ctx.shadowBlur = 0;
  } else if (item.type === 'gold') {
    const r = item.radius * 0.9;
    ctx.fillStyle = item.color;
    ctx.shadowColor = '#FFD700';
    ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(0, 0, r * 0.7, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
  } else {
    const r = item.radius * 0.9;
    ctx.fillStyle = item.color;
    ctx.beginPath();
    ctx.arc(0, 0, r * 0.65, 0, Math.PI * 2);
    ctx.fill();
  }

  ctx.restore();
}

// ===================== HOOK LOGIC =====================
function updateHook() {
  const hook = game.hook;
  const mx = getMinerX();
  const my = getMinerY() + 15;

  hook.maxLength = getH() * CONFIG.HOOK_MAX_LENGTH_RATIO;

  switch (hook.state) {
    case 'swinging':
      hook.angle += CONFIG.HOOK_SWING_SPEED * hook.swingDir;
      if (hook.angle >= CONFIG.MAX_ANGLE) {
        hook.angle = CONFIG.MAX_ANGLE;
        hook.swingDir = -1;
      } else if (hook.angle <= CONFIG.MIN_ANGLE) {
        hook.angle = CONFIG.MIN_ANGLE;
        hook.swingDir = 1;
      }
      hook.length = 30;
      break;

    case 'extending':
      hook.length += CONFIG.HOOK_EXTEND_SPEED;
      // Check collision with items
      checkCollision();
      // Check boundary
      if (hook.length >= hook.maxLength || hook.tipX < 5 || hook.tipX > getW() - 5 || hook.tipY > getH() - 5) {
        hook.state = 'retracting';
      }
      break;

    case 'retracting':
      const speed = hook.grabbed
        ? CONFIG.HOOK_RETRACT_SPEED_LOADED / hook.grabbed.weight
        : CONFIG.HOOK_RETRACT_SPEED;
      hook.length -= speed * 1.8;

      // Move grabbed item with hook
      if (hook.grabbed) {
        hook.grabbed.x = hook.tipX;
        hook.grabbed.y = hook.tipY + hook.grabbed.radius * 0.5;
      }

      if (hook.length <= 30) {
        hook.length = 30;
        if (hook.grabbed) {
          // Score!
          const item = hook.grabbed;
          game.score += item.score;
          game.levelScore += item.score;
          addParticles(mx, my, item.color, 15);
          hook.grabbed = null;
        }
        hook.state = 'swinging';
      }
      break;
  }
}

function checkCollision() {
  const hook = game.hook;
  for (let item of game.items) {
    if (item.grabbed) continue;
    const d = dist(hook.tipX, hook.tipY, item.x, item.y);
    if (d < item.radius + 8) {
      item.grabbed = true;
      hook.grabbed = item;
      hook.state = 'retracting';
      addParticles(item.x, item.y, item.color, 8);
      break;
    }
  }
}

function releaseHook() {
  if (game.state !== 'playing') return;
  if (game.hook.state === 'swinging') {
    game.hook.state = 'extending';
  }
}

// ===================== GAME FLOW =====================
function startGame() {
  game.score = 0;
  game.level = 1;
  startScreen.classList.add('hidden');
  endScreen.classList.add('hidden');
  startLevel(1);
}

function startLevel(lvl) {
  game.level = lvl;
  game.levelScore = 0;
  game.timeLeft = CONFIG.TIME_PER_LEVEL;
  game.state = 'playing';
  game.items = spawnItems(lvl - 1);
  game.hook = {
    angle: 0,
    swingDir: 1,
    length: 30,
    maxLength: getH() * CONFIG.HOOK_MAX_LENGTH_RATIO,
    state: 'swinging',
    tipX: getMinerX(),
    tipY: getMinerY() + 45,
    grabbed: null,
  };
  game.particles = [];

  updateHUD();

  // Timer
  clearInterval(game.timerInterval);
  game.timerInterval = setInterval(() => {
    if (game.state !== 'playing') return;
    game.timeLeft--;
    updateHUD();
    if (game.timeLeft <= 0) {
      endLevel();
    }
  }, 1000);
}

function endLevel() {
  clearInterval(game.timerInterval);
  game.state = 'levelEnd';

  if (game.level >= CONFIG.LEVELS) {
    // Game over
    finalScoreEl.textContent = game.score;
    endScreen.classList.remove('hidden');
  } else {
    // Show level score
    levelTag.textContent = `第 ${game.level} 关`;
    levelScoreEl.textContent = `${game.levelScore} 分`;
    levelScreen.classList.remove('hidden');
  }
}

function nextLevel() {
  levelScreen.classList.add('hidden');
  startLevel(game.level + 1);
}

function restartGame() {
  endScreen.classList.add('hidden');
  startGame();
}

function updateHUD() {
  scoreText.textContent = game.score;
  levelText.textContent = `第 ${game.level} 关`;
  timerText.textContent = Math.max(0, game.timeLeft);

  // Timer color
  if (game.timeLeft <= 5) {
    timerText.style.color = '#FF5252';
  } else if (game.timeLeft <= 10) {
    timerText.style.color = '#FFB74D';
  } else {
    timerText.style.color = '#fff';
  }
}

// ===================== SCORE POPUP =====================
let scorePopups = [];

function addScorePopup(x, y, score) {
  scorePopups.push({
    x, y, score,
    life: 40,
    vy: -2,
  });
}

// Override score adding to show popup
const origHook = game.hook;
// We handle popups in the main loop after scoring

// ===================== MAIN LOOP =====================
function gameLoop() {
  const w = getW(), h = getH();
  ctx.clearRect(0, 0, w, h);

  drawBackground();
  drawItems();

  if (game.state === 'playing') {
    updateHook();
  }

  drawMiner();
  drawRopeAndHook();
  drawGrabbedItem();
  updateParticles();
  drawParticles();

  // Score popups
  scorePopups = scorePopups.filter(p => {
    p.y += p.vy;
    p.life--;
    const alpha = p.life / 40;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = '#FFD700';
    ctx.font = 'bold 22px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(`+${p.score}`, p.x, p.y);
    ctx.globalAlpha = 1;
    return p.life > 0;
  });

  // Detect score change for popup
  if (game.hook.state === 'swinging' && game.hook.grabbed === null) {
    // just after retract with item — popup handled via particles
  }

  requestAnimationFrame(gameLoop);
}

// ===================== INPUT =====================
// Click / Tap
canvas.addEventListener('click', () => releaseHook());
canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  releaseHook();
}, { passive: false });

// Keyboard
document.addEventListener('keydown', (e) => {
  if (e.code === 'Space' || e.key === ' ') {
    e.preventDefault();
    releaseHook();
  }
});

// Buttons
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('nextBtn').addEventListener('click', nextLevel);
document.getElementById('restartBtn').addEventListener('click', restartGame);

// ===================== SCORE POPUP HOOK =====================
// Patch scoring to show popups
let _prevScore = 0;
const _origUpdateHook = updateHook;
// We use a simpler approach: check score delta each frame
setInterval(() => {
  if (game.score > _prevScore) {
    const diff = game.score - _prevScore;
    addScorePopup(getMinerX(), getMinerY() - 20, diff);
    _prevScore = game.score;
  }
  if (game.score < _prevScore) {
    _prevScore = game.score; // reset
  }
}, 50);

// ===================== START =====================
gameLoop();
</script>
</body>
</html>

Game Source: 黄金矿工

Creator: ThunderBolt64

Libraries: none

Complexity: complex (872 lines, 22.0 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-thunderbolt64" to link back to the original. Then publish at arcadelab.ai/publish.