花语守护 - Bloom Guardian
by StealthStar79677 lines19.6 KB
<!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.