🎮ArcadeLab

花语守护 - Bloom Guardian

by StealthStar79
677 lines19.6 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>花语守护 - Bloom Guardian</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: linear-gradient(180deg, #e8f5e9 0%, #c8e6c9 50%, #a5d6a7 100%);
  font-family: 'Arial', 'Microsoft YaHei', sans-serif;
  overflow: hidden;
  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
#gameContainer {
  width: 100vw;
  height: 100vh;
  position: relative;
}
canvas {
  display: block;
  margin: 0 auto;
}
#topBar {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 15px;
  z-index: 10;
}
#topBar .info {
  background: rgba(255,255,255,0.85);
  border-radius: 20px;
  padding: 6px 14px;
  font-size: 14px;
  color: #2e7d32;
  font-weight: bold;
}
#lives { color: #e53935; }
#bottomBar {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  height: 80px;
  display: flex;
  align-items: center;
  justify-content: space-around;
  padding: 0 10px;
  z-index: 10;
}
.propBtn {
  background: rgba(255,255,255,0.9);
  border: 2px solid #66bb6a;
  border-radius: 15px;
  padding: 8px 12px;
  font-size: 13px;
  cursor: pointer;
  text-align: center;
  min-width: 70px;
}
.propBtn:active { transform: scale(0.95); }
.propBtn.disabled { opacity: 0.4; pointer-events: none; }
#gardenBtn {
  position: absolute;
  bottom: 90px;
  right: 15px;
  background: rgba(255,255,255,0.9);
  border: 2px solid #ff7043;
  border-radius: 20px;
  padding: 10px 18px;
  font-size: 14px;
  cursor: pointer;
  z-index: 10;
}
#overlay {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.6);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
#overlayContent {
  background: white;
  border-radius: 20px;
  padding: 30px;
  text-align: center;
  max-width: 85vw;
}
#overlayContent h2 { color: #2e7d32; margin-bottom: 10px; }
#overlayContent p { color: #555; margin-bottom: 20px; }
#overlayContent button {
  background: #66bb6a;
  color: white;
  border: none;
  border-radius: 15px;
  padding: 10px 30px;
  font-size: 16px;
  cursor: pointer;
}
#gardenView {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: linear-gradient(180deg, #fff3e0 0%, #ffe0b2 100%);
  display: none;
  z-index: 50;
  overflow-y: auto;
  padding: 20px;
}
#gardenView h2 { color: #e65100; text-align: center; margin-bottom: 15px; }
.gardenGrid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  max-width: 400px;
  margin: 0 auto 20px;
}
.gardenPlot {
  aspect-ratio: 1;
  border-radius: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 36px;
  background: #bdbdbd;
  transition: all 0.5s;
}
.gardenPlot.restored { background: #a5d6a7; }
#shopView {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  max-width: 400px;
  margin: 0 auto;
}
.shopItem {
  background: white;
  border: 2px solid #ddd;
  border-radius: 12px;
  padding: 10px;
  font-size: 12px;
  text-align: center;
  cursor: pointer;
  min-width: 80px;
}
.shopItem.owned { border-color: #66bb6a; background: #e8f5e9; }
.shopItem.equipped { border-color: #ff7043; background: #fbe9e7; }
#backBtn {
  display: block;
  margin: 15px auto 0;
  background: #ff7043;
  color: white;
  border: none;
  border-radius: 15px;
  padding: 10px 30px;
  font-size: 16px;
  cursor: pointer;
}
</style>
</head>
<body>
<div id="gameContainer">
  <canvas id="gameCanvas"></canvas>
  <div id="topBar">
    <div class="info" id="levelInfo">第 1 关</div>
    <div class="info" id="scoreInfo">🌱 0</div>
    <div class="info" id="lives">❤️❤️❤️❤️❤️</div>
    <div class="info" id="dewInfo">💧 0</div>
  </div>
  <div id="bottomBar">
    <div class="propBtn" id="propScan" onclick="useProp('scan')">🔍<br>花镜</div>
    <div class="propBtn" id="propShield" onclick="useProp('shield')">🛡️<br>护盾</div>
    <div class="propBtn" id="propReveal" onclick="useProp('reveal')">💧<br>清露</div>
  </div>
  <button id="gardenBtn" onclick="showGarden()">🌳 拯救花园</button>
  <div id="overlay"><div id="overlayContent">
    <h2 id="overlayTitle"></h2>
    <p id="overlayText"></p>
    <button id="overlayBtn" onclick="overlayAction()">确定</button>
  </div></div>
  <div id="gardenView">
    <h2>🌳 拯救花园</h2>
    <p style="text-align:center;color:#666;margin-bottom:10px;">通关收集种子,修复你的花园</p>
    <div class="gardenGrid" id="gardenGrid"></div>
    <h3 style="color:#e65100;text-align:center;">🛒 皮肤商店</h3>
    <div id="shopView"></div>
    <button id="backBtn" onclick="hideGarden()">返回游戏</button>
  </div>
</div>

<script>
// ============ 游戏核心数据 ============
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H, offsetY;

function resize() {
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width = W;
  canvas.height = H;
  offsetY = 60;
}
resize();
window.addEventListener('resize', resize);

// 存档
let save = JSON.parse(localStorage.getItem('bloomGuardian') || 'null') || {
  level: 1, score: 0, dew: 0, lives: 5,
  garden: [false,false,false,false,false,false],
  skins: [], equippedSkin: null,
  props: { scan: 2, shield: 1, reveal: 1 }
};

function saveGame() { localStorage.setItem('bloomGuardian', JSON.stringify(save)); }

// 关卡配置
function getLevelConfig(level) {
  const gridSizes = [3,3,4,4,5,5,6,6,6,6];
  const bombRatios = [0.1, 0.12, 0.15, 0.18, 0.22, 0.25, 0.28, 0.32, 0.36, 0.4];
  const idx = Math.min(level - 1, gridSizes.length - 1);
  return { grid: gridSizes[idx], bombRatio: bombRatios[idx] };
}

// 皮肤配置
const SKINS = [
  { id: 'sakura', name: '🌸樱花', price: 50, type: 'flower' },
  { id: 'sunflower', name: '🌻向日葵', price: 80, type: 'flower' },
  { id: 'rose', name: '🌹玫瑰', price: 100, type: 'flower' },
  { id: 'star', name: '🌟星空花', price: 120, type: 'flower' },
  { id: 'firefly', name: '✨萤火特效', price: 60, type: 'effect' },
  { id: 'rainbow', name: '🌈彩虹光', price: 60, type: 'effect' },
  { id: 'windmill', name: '🎡风车装饰', price: 40, type: 'deco' },
  { id: 'fountain', name: '⛲喷泉装饰', price: 40, type: 'deco' },
];

// 游戏状态
let grid = [], cols, rows, cellSize, offsetX;
let revealed = [], bombs = [];
let gameState = 'playing'; // playing, won, lost
let shieldActive = false;
let revealActive = false;
let animFrame;

// ============ 初始化关卡 ============
function initLevel() {
  const conf = getLevelConfig(save.level);
  cols = rows = conf.grid;
  const maxW = Math.min(W * 0.9, 400);
  cellSize = maxW / cols;
  offsetX = (W - cellSize * cols) / 2;

  grid = []; bombs = []; revealed = [];
  const total = cols * rows;
  const bombCount = Math.floor(total * conf.bombRatio);

  // 随机放炸弹
  let positions = Array.from({length: total}, (_, i) => i);
  shuffle(positions);
  for (let i = 0; i < bombCount; i++) {
    bombs.push(positions[i]);
  }

  for (let r = 0; r < rows; r++) {
    grid[r] = [];
    revealed[r] = [];
    for (let c = 0; c < cols; c++) {
      grid[r][c] = {
        isBomb: bombs.includes(r * cols + c),
        revealed: false,
        animProgress: 0,
        bombAnim: 0
      };
      revealed[r][c] = false;
    }
  }

  gameState = 'playing';
  shieldActive = false;
  revealActive = false;
  updateUI();
  draw();
}

function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
}

// ============ 绘制 ============
function draw() {
  ctx.clearRect(0, offsetY, W, H - offsetY - 80);

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const cell = grid[r][c];
      const x = offsetX + c * cellSize;
      const y = offsetY + 10 + r * cellSize;
      const s = cellSize * 0.85;
      const cx = x + cellSize / 2;
      const cy = y + cellSize / 2;

      // 花瓣背景
      ctx.save();
      ctx.translate(cx, cy);

      if (!cell.revealed) {
        // 未翻开 - 花苞
        const isBomb = cell.isBomb;
        const skin = save.equippedSkin;
        let hue = 40 + (r * 30 + c * 20) % 40;
        let sat = 70 + (isBomb ? 15 : 0); // 炸弹饱和度略高(伪装破绽)
        let light = 65 + (isBomb ? 5 : 0);

        if (skin === 'sakura') { hue = 330; sat = 75; }
        else if (skin === 'sunflower') { hue = 45; sat = 90; }
        else if (skin === 'rose') { hue = 350; sat = 85; }
        else if (skin === 'star') { hue = 260; sat = 80; }

        // 呼吸动画
        const breathe = 1 + Math.sin(Date.now() * 0.003 + r + c) * 0.05;
        ctx.scale(breathe, breathe);

        // 花苞
        ctx.beginPath();
        ctx.arc(0, 0, s * 0.35, 0, Math.PI * 2);
        ctx.fillStyle = `hsl(${hue}, ${sat}%, ${light}%)`;
        ctx.fill();
        ctx.strokeStyle = `hsl(${hue}, ${sat}%, ${light - 15}%)`;
        ctx.lineWidth = 2;
        ctx.stroke();

        // 花瓣
        for (let p = 0; p < 5; p++) {
          ctx.save();
          ctx.rotate(p * Math.PI * 2 / 5);
          ctx.beginPath();
          ctx.ellipse(0, -s * 0.25, s * 0.12, s * 0.2, 0, 0, Math.PI * 2);
          ctx.fillStyle = `hsl(${hue}, ${sat}%, ${light + 10}%)`;
          ctx.fill();
          ctx.restore();
        }

        // 清露显形
        if (revealActive && isBomb) {
          ctx.beginPath();
          ctx.arc(0, 0, s * 0.5, 0, Math.PI * 2);
          ctx.fillStyle = 'rgba(255,0,0,0.3)';
          ctx.fill();
          ctx.fillStyle = 'red';
          ctx.font = `${s * 0.3}px Arial`;
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillText('💣', 0, 0);
        }

      } else {
        // 已翻开
        if (cell.isBomb) {
          // 爆炸
          const progress = cell.bombAnim || 0;
          ctx.scale(1 + progress * 0.5, 1 + progress * 0.5);
          ctx.beginPath();
          ctx.arc(0, 0, s * 0.4, 0, Math.PI * 2);
          ctx.fillStyle = `rgba(255, 100, 50, ${1 - progress})`;
          ctx.fill();
          ctx.fillStyle = 'rgba(255,255,255,0.9)';
          ctx.font = `${s * 0.5}px Arial`;
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillText('💥', 0, 0);
        } else {
          // 绽放
          const progress = cell.animProgress || 0;
          ctx.scale(progress, progress);

          // 花瓣
          for (let p = 0; p < 6; p++) {
            ctx.save();
            ctx.rotate(p * Math.PI / 3);
            ctx.beginPath();
            ctx.ellipse(0, -s * 0.3, s * 0.15, s * 0.3, 0, 0, Math.PI * 2);
            const hue = 40 + (r * 30 + c * 20) % 40;
            ctx.fillStyle = `hsla(${hue}, 80%, 75%, ${progress})`;
            ctx.fill();
            ctx.restore();
          }
          // 花心
          ctx.beginPath();
          ctx.arc(0, 0, s * 0.15, 0, Math.PI * 2);
          ctx.fillStyle = `rgba(255, 220, 50, ${progress})`;
          ctx.fill();

          // 特效
          if (save.equippedSkin === 'firefly') {
            ctx.fillStyle = `rgba(255,255,150,${progress * 0.6})`;
            for (let i = 0; i < 3; i++) {
              const angle = Date.now() * 0.005 + i * 2;
              ctx.beginPath();
              ctx.arc(Math.cos(angle) * s * 0.4, Math.sin(angle) * s * 0.4, 2, 0, Math.PI * 2);
              ctx.fill();
            }
          }
          if (save.equippedSkin === 'rainbow') {
            const grad = ctx.createLinearGradient(-s*0.3, 0, s*0.3, 0);
            grad.addColorStop(0, 'red'); grad.addColorStop(0.5, 'yellow'); grad.addColorStop(1, 'blue');
            ctx.strokeStyle = grad;
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.arc(0, 0, s * 0.45, 0, Math.PI * 2);
            ctx.stroke();
          }
        }
      }
      ctx.restore();
    }
  }

  // 护盾指示
  if (shieldActive) {
    ctx.fillStyle = 'rgba(100, 200, 255, 0.8)';
    ctx.font = '20px Arial';
    ctx.fillText('🛡️ 护盾已激活', 10, offsetY + 25);
  }

  animFrame = requestAnimationFrame(draw);
}

// ============ 点击处理 ============
canvas.addEventListener('click', handleClick);
canvas.addEventListener('touchstart', function(e) {
  e.preventDefault();
  const touch = e.touches[0];
  const rect = canvas.getBoundingClientRect();
  handleClick({ clientX: touch.clientX - rect.left, clientY: touch.clientY - rect.top });
}, {passive: false});

function handleClick(e) {
  if (gameState !== 'playing') return;

  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;

  const c = Math.floor((mx - offsetX) / cellSize);
  const r = Math.floor((my - offsetY - 10) / cellSize);

  if (r < 0 || r >= rows || c < 0 || c >= cols) return;
  if (grid[r][c].revealed) return;

  revealCell(r, c);
}

function revealCell(r, c) {
  const cell = grid[r][c];
  cell.revealed = true;

  if (cell.isBomb) {
    // 炸弹
    cell.bombAnim = 0;
    animateBomb(cell);

    if (shieldActive) {
      shieldActive = false;
      return;
    }

    save.lives--;
    updateUI();

    if (save.lives <= 0) {
      gameState = 'lost';
      setTimeout(() => showOverlay('游戏结束', `你已用完所有生命\n得分: ${save.score}`, '重新开始'), 800);
    } else {
      setTimeout(() => {
        showOverlay('💥 炸弹!', `剩余生命: ${'❤️'.repeat(save.lives)}\n继续挑战?`, '继续');
      }, 600);
    }
  } else {
    // 安全花
    cell.animProgress = 0;
    animateBloom(cell);
    save.score += 10 * save.level;
    updateUI();

    // 检查是否赢了
    let unrevealedSafe = 0;
    for (let rr = 0; rr < rows; rr++)
      for (let cc = 0; cc < cols; cc++)
        if (!grid[rr][cc].revealed && !grid[rr][cc].isBomb)
          unrevealedSafe++;

    if (unrevealedSafe === 0) {
      gameState = 'won';
      const seeds = 1 + Math.floor(save.level / 3);
      save.dew += 5 * save.level;
      // 修复花园
      const plotIdx = Math.min(save.level - 1, 5);
      if (!save.garden[plotIdx]) save.garden[plotIdx] = true;

      setTimeout(() => {
        showOverlay('🎉 通关!', `获得 ${seeds} 颗种子 🌱\n获得 ${5 * save.level} 露水 💧\n花园修复进度 +1`, '下一关');
      }, 500);
    }
  }
  saveGame();
}

function animateBloom(cell) {
  const start = Date.now();
  function step() {
    const t = (Date.now() - start) / 300;
    if (t < 1) {
      cell.animProgress = easeOutBack(t);
      requestAnimationFrame(step);
    } else {
      cell.animProgress = 1;
    }
  }
  step();
}

function animateBomb(cell) {
  const start = Date.now();
  function step() {
    const t = (Date.now() - start) / 500;
    if (t < 1) {
      cell.bombAnim = t;
      requestAnimationFrame(step);
    } else {
      cell.bombAnim = 1;
    }
  }
  step();
}

function easeOutBack(t) {
  const c1 = 1.70158;
  const c3 = c1 + 1;
  return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}

// ============ 道具 ============
function useProp(type) {
  if (gameState !== 'playing') return;
  if (save.props[type] <= 0) return;

  save.props[type]--;
  if (type === 'scan') {
    // 找一朵安全花标记
    for (let r = 0; r < rows; r++)
      for (let c = 0; c < cols; c++)
        if (!grid[r][c].revealed && !grid[r][c].isBomb) {
          grid[r][c].scanMark = true;
          setTimeout(() => { grid[r][c].scanMark = false; }, 2000);
          updateUI();
          return;
        }
  } else if (type === 'shield') {
    shieldActive = true;
  } else if (type === 'reveal') {
    revealActive = true;
    setTimeout(() => { revealActive = false; }, 2000);
  }
  updateUI();
  saveGame();
}

// ============ UI ============
function updateUI() {
  document.getElementById('levelInfo').textContent = `第 ${save.level} 关`;
  document.getElementById('scoreInfo').textContent = `🌱 ${save.score}`;
  document.getElementById('lives').textContent = '❤️'.repeat(Math.max(0, save.lives));
  document.getElementById('dewInfo').textContent = `💧 ${save.dew}`;

  document.getElementById('propScan').textContent = `🔍\n花镜(${save.props.scan})`;
  document.getElementById('propShield').textContent = `🛡️\n护盾(${save.props.shield})`;
  document.getElementById('propReveal').textContent = `💧\n清露(${save.props.reveal})`;

  document.getElementById('propScan').className = 'propBtn' + (save.props.scan <= 0 ? ' disabled' : '');
  document.getElementById('propShield').className = 'propBtn' + (save.props.shield <= 0 ? ' disabled' : '');
  document.getElementById('propReveal').className = 'propBtn' + (save.props.reveal <= 0 ? ' disabled' : '');
}

// ============ 弹窗 ============
let overlayCallback = null;
function showOverlay(title, text, btnText) {
  document.getElementById('overlayTitle').textContent = title;
  document.getElementById('overlayText').textContent = text;
  document.getElementById('overlayBtn').textContent = btnText;
  document.getElementById('overlay').style.display = 'flex';

  overlayCallback = btnText;
}

function overlayAction() {
  document.getElementById('overlay').style.display = 'none';
  if (overlayCallback === '重新开始') {
    save.lives = 5;
    save.score = 0;
    save.level = 1;
    saveGame();
    initLevel();
  } else if (overlayCallback === '下一关') {
    save.level++;
    save.lives = 5;
    saveGame();
    initLevel();
  } else if (overlayCallback === '继续') {
    // 不重置,继续当前关卡
  }
}

// ============ 花园 ============
function showGarden() {
  document.getElementById('gardenView').style.display = 'block';
  renderGarden();
  renderShop();
}

function hideGarden() {
  document.getElementById('gardenView').style.display = 'none';
}

function renderGarden() {
  const grid = document.getElementById('gardenGrid');
  grid.innerHTML = '';
  const emojis = ['🌱','🌿','🌻','🌸','🌹','🌳'];
  for (let i = 0; i < 6; i++) {
    const plot = document.createElement('div');
    plot.className = 'gardenPlot' + (save.garden[i] ? ' restored' : '');
    plot.textContent = save.garden[i] ? emojis[i] : '❓';
    grid.appendChild(plot);
  }
}

function renderShop() {
  const shop = document.getElementById('shopView');
  shop.innerHTML = '';
  SKINS.forEach(skin => {
    const item = document.createElement('div');
    item.className = 'shopItem';
    const owned = save.skins.includes(skin.id);
    const equipped = save.equippedSkin === skin.id;
    if (owned) item.classList.add('owned');
    if (equipped) item.classList.add('equipped');

    item.innerHTML = `${skin.name}<br>${owned ? (equipped ? '已装备' : '点击装备') : `${skin.price}💧`}`;
    item.onclick = () => {
      if (owned) {
        save.equippedSkin = skin.id;
      } else if (save.dew >= skin.price) {
        save.dew -= skin.price;
        save.skins.push(skin.id);
        save.equippedSkin = skin.id;
      }
      saveGame();
      renderShop();
      updateUI();
    };
    shop.appendChild(item);
  });
}

// ============ 启动 ============
initLevel();
</script>
</body>
</html>

Game Source: 花语守护 - Bloom Guardian

Creator: StealthStar79

Libraries: none

Complexity: complex (677 lines, 19.6 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: bloom-guardian-stealthstar79" to link back to the original. Then publish at arcadelab.ai/publish.