像素冒险 - 森林秘境
body {
margin: 0;
background: #12131c;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: "Microsoft Yahei", sans-serif;
color: #fff;
overflow: hidden;
touch-action: none;
}
.game-container {
position: relative;
max-width: 100%;
display: flex;
justify-content: center;
box-shadow: 0 15px 40px rgba(0,0,0,0.8);
}
canvas {
border: 2px solid #3c4257;
image-rendering: pixelated;
image-rendering: crisp-edges;
background: #87ceeb;
max-width: 100%;
max-height: 85vh;
object-fit: contain;
}
.tip {
margin-top: 15px;
font-size: 14px;
color: #8892b0;
display: none;
}
#mobile-controls {
display: none;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.control-btn {
position: absolute;
background: rgba(255, 255, 255, 0.15);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
font-weight: bold;
pointer-events: auto;
user-select: none;
-webkit-user-select: none;
backdrop-filter: blur(4px);
}
.control-btn:active, .control-btn.active {
background: rgba(255, 255, 255, 0.4);
}
#btn-left { bottom: 20px; left: 20px; width: 60px; height: 60px; }
#btn-right { bottom: 20px; left: 100px; width: 60px; height: 60px; }
#btn-jump {
bottom: 30px; right: 30px; width: 80px; height: 80px;
background: rgba(255, 100, 100, 0.25);
border-color: rgba(255, 150, 150, 0.5);
}
@media (pointer: coarse) {
#mobile-controls { display: block; }
.tip { display: block; }
canvas { border-width: 2px; }
}
</style>
<div id="mobile-controls">
<div class="control-btn" id="btn-left" data-key="ArrowLeft">◀</div>
<div class="control-btn" id="btn-right" data-key="ArrowRight">▶</div>
<div class="control-btn" id="btn-jump" data-key="Space">跳</div>
</div>
</div>
<div class="tip">操作:← → 移动 | 空格 跳跃 | R 重新开始</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// ========== 游戏基础配置 ==========
const GRAVITY = 0.6;
const MOVE_SPEED = 4;
const JUMP_FORCE = -12;
const COYOTE_TIME = 6;
const JUMP_CUT = 0.45;
let LEVEL_WIDTH = 2000;
// ========== 输入控制 ==========
const keys = {};
document.addEventListener('keydown', e => {
if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'].includes(e.code)) e.preventDefault();
keys[e.code] = true;
if (e.code === 'KeyR') restartGame();
if (gameState === 'start' && (e.code === 'Space' || e.code === 'Enter')) {
currentLevel = 1;
initLevel(currentLevel);
gameState = 'playing';
}
if (gameState === 'win' && (e.code === 'Space' || e.code === 'Enter' || e.code === 'KeyR')) {
nextLevel();
}
});
document.addEventListener('keyup', e => keys[e.code] = false);
// 移动端触摸事件
const bindTouchControls = () => {
const handleTouch = (btnId, code) => {
const btn = document.getElementById(btnId);
const start = (e) => {
e.preventDefault();
keys[code] = true;
btn.classList.add('active');
if (gameState === 'start') {
currentLevel = 1;
initLevel(currentLevel);
gameState = 'playing';
} else if (gameState === 'win') {
nextLevel();
}
};
const end = (e) => {
e.preventDefault();
keys[code] = false;
btn.classList.remove('active');
};
btn.addEventListener('touchstart', start, {passive: false});
btn.addEventListener('touchend', end, {passive: false});
btn.addEventListener('touchcancel', end, {passive: false});
btn.addEventListener('mousedown', start);
btn.addEventListener('mouseup', end);
btn.addEventListener('mouseleave', end);
};
handleTouch('btn-left', 'ArrowLeft');
handleTouch('btn-right', 'ArrowRight');
handleTouch('btn-jump', 'Space');
};
bindTouchControls();
// 屏幕点击开始/下一关
canvas.addEventListener('touchstart', (e) => {
if (gameState === 'start') {
currentLevel = 1;
initLevel(currentLevel);
gameState = 'playing';
} else if (gameState === 'win') {
nextLevel();
}
}, {passive: true});
// ========== 玩家对象 ==========
let player;
function initPlayer() {
player = {
x: 50, y: 280,
w: 24, h: 30,
vx: 0, vy: 0,
onGround: false,
coyote: 0,
hp: 3,
invincible: 0,
facing: 1,
animFrame: 0,
animTimer: 0
};
}
// ========== 关卡数据 ==========
let platforms, spikes, coins, gems, enemies, goal, decorations;
let currentLevel = 1;
let maxLevel = 3;
function initLevel(level) {
coinCount = 0;
gemCount = 0;
player.x = 50;
player.y = 280;
player.vx = 0;
player.vy = 0;
player.hp = 3;
camera.x = 0;
if (level === 1) {
LEVEL_WIDTH = 1800;
platforms = [
[0, 380, 800, 70],
[900, 380, 500, 70],
[1500, 380, 300, 70],
[400, 280, 120, 20],
[1100, 260, 100, 20],
];
spikes = [];
coins = [
{x: 250, y: 330, collected: false},
{x: 450, y: 230, collected: false},
{x: 950, y: 330, collected: false},
{x: 1150, y: 210, collected: false},
{x: 1600, y: 330, collected: false},
];
gems = [
{x: 700, y: 330, collected: false},
{x: 1400, y: 330, collected: false},
];
enemies = [
{x: 600, y: 350, w: 32, h: 30, vx: 0.8, minX: 400, maxX: 750, alive: true},
];
decorations = [
{type: 'bgTree', x: 200, y: 380},
{type: 'bgTree', x: 1300, y: 380},
{type: 'mushroom', x: 350, y: 380},
{type: 'flower', x: 500, y: 380, color: '#ff6b6b'},
];
goal = {x: 1700, y: 316, w: 16, h: 64};
} else if (level === 2) {
LEVEL_WIDTH = 2400;
platforms = [
[0, 380, 600, 70],
[680, 380, 300, 70],
[1060, 320, 200, 40],
[1320, 260, 180, 40],
[1560, 380, 400, 70],
[2040, 320, 120, 40],
[2220, 380, 180, 70],
[200, 300, 100, 20],
[400, 230, 100, 20],
[800, 280, 100, 20],
[1700, 270, 120, 20],
[1900, 200, 100, 20],
];
spikes = [
[620, 360, 60, 20],
[1000, 360, 60, 20],
[1980, 360, 60, 20],
];
coins = [
{x: 280, y: 260, collected: false},
{x: 620, y: 260, collected: false},
{x: 880, y: 230, collected: false},
{x: 1400, y: 210, collected: false},
{x: 1950, y: 150, collected: false},
];
gems = [
{x: 380, y: 180, collected: false},
{x: 720, y: 200, collected: false},
{x: 1150, y: 250, collected: false},
{x: 1750, y: 200, collected: false},
];
enemies = [
{x: 420, y: 350, w: 32, h: 30, vx: 1.2, minX: 200, maxX: 580, alive: true},
{x: 850, y: 350, w: 32, h: 30, vx: 1.5, minX: 700, maxX: 950, alive: true},
{x: 1650, y: 350, w: 32, h: 30, vx: 1.8, minX: 1580, maxX: 1900, alive: true},
];
decorations = [
{type: 'bgTree', x: 150, y: 380},
{type: 'bgTree', x: 1000, y: 380},
{type: 'bgTree', x: 1750, y: 380},
{type: 'mushroom', x: 1100, y: 320},
];
goal = {x: 2300, y: 316, w: 16, h: 64};
} else if (level === 3) {
LEVEL_WIDTH = 2800;
platforms = [
[0, 380, 400, 70],
[480, 380, 200, 70],
[800, 300, 120, 20],
[1000, 220, 120, 20],
[1200, 380, 200, 70],
[1500, 320, 100, 20],
[1700, 240, 100, 20],
[1900, 380, 200, 70],
[2200, 300, 100, 20],
[2350, 200, 100, 20],
[2550, 380, 250, 70],
[400, 260, 80, 20],
[1400, 200, 80, 20],
];
spikes = [
[420, 360, 40, 20],
[720, 360, 60, 20],
[1420, 360, 60, 20],
[2100, 360, 80, 20],
];
coins = [
{x: 250, y: 260, collected: false},
{x: 520, y: 230, collected: false},
{x: 840, y: 240, collected: false},
{x: 1040, y: 160, collected: false},
{x: 1280, y: 260, collected: false},
{x: 1540, y: 260, collected: false},
{x: 1740, y: 180, collected: false},
{x: 2000, y: 260, collected: false},
{x: 2380, y: 140, collected: false},
{x: 2650, y: 260, collected: false},
];
gems = [
{x: 420, y: 200, collected: false},
{x: 850, y: 240, collected: false},
{x: 1450, y: 140, collected: false},
{x: 2250, y: 240, collected: false},
{x: 2600, y: 160, collected: false},
];
enemies = [
{x: 300, y: 350, w: 32, h: 30, vx: 2.0, minX: 200, maxX: 380, alive: true},
{x: 1250, y: 350, w: 32, h: 30, vx: 2.2, minX: 1200, maxX: 1380, alive: true},
{x: 1950, y: 350, w: 32, h: 30, vx: 2.5, minX: 1900, maxX: 2080, alive: true},
{x: 2600, y: 350, w: 32, h: 30, vx: 2.8, minX: 2550, maxX: 2780, alive: true},
];
decorations = [
{type: 'bgTree', x: 100, y: 380},
{type: 'bgTree', x: 2450, y: 380},
{type: 'mushroom', x: 600, y: 380},
{type: 'flower', x: 1300, y: 380, color: '#6bcb77'},
];
goal = {x: 2700, y: 316, w: 16, h: 64};
}
}
// ========== 状态变量 ==========
let camera = {x: 0};
let gameState = 'start';
let coinCount = 0;
let gemCount = 0;
// 预生成背景元素,保证不闪烁
let bgPines = [];
let bgTrees = [];
let particles = [];
function initBackground() {
bgPines = [];
bgTrees = [];
particles = [];
for(let i = 0; i < LEVEL_WIDTH; i += 250) {
// 发光松树
if (Math.random() > 0.3) {
bgPines.push({
x: i + Math.random()*100,
y: 380 + Math.random()*10,
scale: 0.8 + Math.random()*0.4,
offset: Math.random() * Math.PI * 2
});
}
// 远景树干
if (Math.random() > 0.5) {
bgTrees.push({
x: i + Math.random()*150,
w: 60 + Math.random()*40,
type: Math.floor(Math.random()*2)
});
}
}
// 光斑粒子
for(let i = 0; i < 60; i++) {
particles.push({
x: Math.random() * LEVEL_WIDTH,
y: Math.random() * canvas.height,
size: Math.random() * 2.5 + 1,
speedY: Math.random() * 0.4 + 0.1,
speedX: (Math.random() - 0.5) * 0.6,
alpha: Math.random() * 0.8 + 0.2,
pulse: Math.random() * Math.PI * 2
});
}
}
// ========== 碰撞检测 ==========
function rectCollide(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 update() {
// 粒子更新
for(let p of particles) {
p.y -= p.speedY;
p.x += p.speedX;
p.pulse += 0.05;
if(p.y < -10) {
p.y = canvas.height + 10;
p.x = Math.random() * LEVEL_WIDTH;
}
}
if (gameState !== 'playing') return;
let moveDir = 0;
if (keys['ArrowLeft'] || keys['KeyA']) moveDir -= 1;
if (keys['ArrowRight'] || keys['KeyD']) moveDir += 1;
player.vx = moveDir * MOVE_SPEED;
if (moveDir !== 0) player.facing = moveDir;
if (player.onGround) player.coyote = COYOTE_TIME;
else player.coyote--;
const jumpPressed = keys['Space'] || keys['KeyW'] || keys['ArrowUp'];
if (jumpPressed && player.coyote > 0) {
player.vy = JUMP_FORCE;
player.coyote = 0;
player.onGround = false;
}
if (!jumpPressed && player.vy < 0) player.vy *= JUMP_CUT;
player.vy += GRAVITY;
if (player.vy > 15) player.vy = 15;
player.x += player.vx;
if (player.x < 0) player.x = 0;
if (player.x + player.w > LEVEL_WIDTH) player.x = LEVEL_WIDTH - player.w;
for (let p of platforms) {
const plat = {x:p[0], y:p[1], w:p[2], h:p[3]};
if (rectCollide(player, plat)) {
if (player.vx > 0) player.x = plat.x - player.w;
else if (player.vx < 0) player.x = plat.x + plat.w;
}
}
player.y += player.vy;
player.onGround = false;
for (let p of platforms) {
const plat = {x:p[0], y:p[1], w:p[2], h:p[3]};
if (rectCollide(player, plat)) {
if (player.vy > 0) {
player.y = plat.y - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
player.y = plat.y + plat.h;
player.vy = 0;
}
}
}
if (player.y > canvas.height + 100) player.hp = 0;
for (let e of enemies) {
if (!e.alive) continue;
e.x += e.vx;
if (e.x <= e.minX || e.x + e.w >= e.maxX) e.vx *= -1;
const eBox = {x: e.x+4, y: e.y+4, w: e.w-8, h: e.h-4};
if (rectCollide(player, eBox)) {
if (player.vy > 0 && player.y + player.h - e.y < 20) {
e.alive = false;
player.vy = JUMP_FORCE * 0.6;
} else if (player.invincible <= 0) {
player.hp--;
player.invincible = 60;
player.vx = player.x < e.x ? -5 : 5;
player.vy = -6;
}
}
}
for (let s of spikes) {
const spike = {x:s[0], y:s[1]+10, w:s[2], h:s[3]-10};
if (rectCollide(player, spike) && player.invincible <= 0) {
player.hp--;
player.invincible = 60;
player.vy = -8;
}
}
for (let c of coins) {
if (c.collected) continue;
const coinBox = {x:c.x-12, y:c.y-12, w:24, h:24};
if (rectCollide(player, coinBox)) {
c.collected = true;
coinCount++;
}
}
for (let g of gems) {
if (g.collected) continue;
const gemBox = {x:g.x-12, y:g.y-16, w:24, h:32};
if (rectCollide(player, gemBox)) {
g.collected = true;
gemCount++;
}
}
if (rectCollide(player, goal)) gameState = 'win';
if (player.invincible > 0) player.invincible--;
if (player.hp <= 0) gameState = 'lose';
player.animTimer++;
if (player.animTimer > 8) {
player.animTimer = 0;
player.animFrame = (player.animFrame + 1) % 4;
}
camera.x = player.x - canvas.width / 2 + player.w / 2;
if (camera.x < 0) camera.x = 0;
if (camera.x > LEVEL_WIDTH - canvas.width) camera.x = LEVEL_WIDTH - canvas.width;
}
// ========== 渲染主函数 ==========
function render() {
if (!platforms) {
ctx.fillStyle = '#87ceeb';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawUI();
return;
}
drawBackground();
ctx.save();
ctx.translate(-camera.x, 0);
// 1. 背景发光松树
for (let pine of bgPines) {
drawBgPine(pine.x, pine.y, pine.scale, pine.offset);
}
// 2. 地面装饰(蘑菇、花)
for(let d of decorations) {
if(d.type === 'flower') drawFlower(d.x, d.y, d.color);
if(d.type === 'mushroom') drawMushroom(d.x, d.y);
}
// 3. 平台与地形
for (let p of platforms) drawPlatform(p[0], p[1], p[2], p[3]);
// 4. 前景大树装饰
for(let d of decorations) {
if(d.type === 'bgTree') drawForegroundTree(d.x, d.y);
}
// 5. 游戏实体
for (let s of spikes) drawSpikes(s[0], s[1], s[2], s[3]);
for (let c of coins) if (!c.collected) drawCoin(c.x, c.y);
for (let g of gems) if (!g.collected) drawGem(g.x, g.y);
for (let e of enemies) if (e.alive) drawSlime(e);
drawGoal(goal.x, goal.y, goal.w, goal.h);
drawFox();
ctx.restore();
// 6. 光斑粒子(在世界坐标系和屏幕坐标系之间)
drawParticles();
// 7. 顶层 UI
drawUI();
}
// ========== 极致背景渲染 ==========
function drawBackground() {
// 天空/远景渐变
const skyGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
skyGrad.addColorStop(0, '#4a7a5d');
skyGrad.addColorStop(0.4, '#8eb36b');
skyGrad.addColorStop(0.8, '#e5d28b');
skyGrad.addColorStop(1, '#fceea7');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 丁达尔光效 (阳光射线)
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < 5; i++) {
ctx.beginPath();
let offset = (i * 250) - (camera.x * 0.1) % 1000;
ctx.moveTo(offset - 200, -50);
ctx.lineTo(offset + 100, -50);
ctx.lineTo(offset + 400, canvas.height);
ctx.lineTo(offset + 100, canvas.height);
// 交替透明度
ctx.fillStyle = `rgba(255, 240, 180, ${0.04 + (i%2)*0.03})`;
ctx.fill();
}
ctx.restore();
// 远景山脉(视差0.1)
ctx.save();
ctx.translate(-camera.x * 0.1, 0);
ctx.fillStyle = '#8fae76';
ctx.beginPath();
ctx.moveTo(-200, canvas.height);
for(let i=0; i<4000; i+=200) {
ctx.lineTo(i + 100, 200 + Math.sin(i)*30);
ctx.lineTo(i + 200, 300);
}
ctx.lineTo(4000, canvas.height);
ctx.fill();
ctx.restore();
// 中景巨树 (视差0.3)
ctx.save();
ctx.translate(-camera.x * 0.3, 0);
for(let i=0; i<bgTrees.length; i++) {
let t = bgTrees[i];
// 树底色
ctx.fillStyle = '#593d2b';
ctx.fillRect(t.x, 0, t.w, canvas.height);
// 树皮纹理
ctx.fillStyle = '#3d2719';
for(let j=0; j<t.w; j+=15) {
ctx.fillRect(t.x + j, 0, 3, canvas.height);
}
// 暗部阴影
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(t.x, 0, t.w * 0.3, canvas.height);
// 受光面
ctx.fillStyle = 'rgba(255, 200, 100, 0.1)';
ctx.fillRect(t.x + t.w * 0.7, 0, t.w * 0.3, canvas.height);
}
ctx.restore();
// 顶部树冠与垂藤 (视差0.8)
ctx.save();
ctx.translate(-camera.x * 0.8, 0);
ctx.fillStyle = '#1e331e';
for(let i=0; i<4000; i+=120) {
// 树冠团
ctx.beginPath();
ctx.arc(i, -40, 100, 0, Math.PI*2);
ctx.fill();
// 粗大的垂藤
if (i % 240 === 0) {
ctx.fillStyle = '#152415';
ctx.fillRect(i, 0, 10, 150 + (i%3)*50);
ctx.fillStyle = '#1e331e';
}
}
ctx.restore();
}
function drawBgPine(x, y, scale, offset) {
const time = Date.now() / 500;
const glow = Math.sin(time + offset) * 0.3 + 0.7;
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
// 树干
ctx.fillStyle = '#3d2515';
ctx.fillRect(-4, -40, 8, 40);
// 多层树叶 (从下到上)
const layers = [
{w: 45, h: 40, y: -40, c: '#203d1c'},
{w: 35, h: 35, y: -70, c: '#2a4f24'},
{w: 25, h: 30, y: -100, c: '#33612b'}
];
for (let i=0; i<layers.length; i++) {
let l = layers[i];
ctx.fillStyle = l.c;
ctx.beginPath();
ctx.moveTo(0, l.y - l.h);
ctx.lineTo(l.w, l.y);
ctx.lineTo(-l.w, l.y);
ctx.fill();
}
// 发光点
for(let i=0; i<6; i++) {
let lx = Math.sin(i*12) * 20;
let ly = -50 + Math.cos(i*8) * 30;
// 发光光晕
ctx.fillStyle = `rgba(255, 230, 100, ${0.2 * glow})`;
ctx.beginPath();
ctx.arc(lx, ly, 4, 0, Math.PI*2);
ctx.fill();
// 核心亮点
ctx.fillStyle = `rgba(255, 255, 200, ${glow})`;
ctx.fillRect(lx-1, ly-1, 2, 2);
}
ctx.restore();
}
function drawParticles() {
for(let p of particles) {
let alpha = p.alpha + Math.sin(p.pulse) * 0.2;
if(alpha < 0) alpha = 0;
if(alpha > 1) alpha = 1;
// 光晕
ctx.fillStyle = `rgba(255, 250, 200, ${alpha * 0.4})`;
ctx.beginPath();
ctx.arc(p.x - camera.x * 0.5, p.y, p.size * 2.5, 0, Math.PI*2);
ctx.fill();
// 核心
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(p.x - camera.x * 0.5, p.y, p.size, 0, Math.PI*2);
ctx.fill();
}
}
// ========== 地形与实体绘制 ==========
function drawPlatform(x, y, w, h) {
// 底座
ctx.fillStyle = '#2d3326';
ctx.fillRect(x, y, w, h);
// 石砖纹理 (错落有致)
for (let i = 0; i < w; i += 32) {
for (let j = 12; j < h; j += 20) {
const offset = ((j - 12) / 20) % 2 === 0 ? 0 : 16;
const bx = x + i + offset;
if (bx > x + w - 16) continue;
// 砖块主体
ctx.fillStyle = '#49533d';
ctx.fillRect(bx + 2, y + j + 2, 28, 16);
// 砖块暗部
ctx.fillStyle = '#3b4230';
ctx.fillRect(bx + 2, y + j + 10, 28, 8);
// 高光边缘
ctx.fillStyle = '#626e50';
ctx.fillRect(bx + 2, y + j + 2, 28, 2);
// 砖缝阴影
ctx.fillStyle = '#1c2118';
ctx.fillRect(bx, y + j, 32, 2);
ctx.fillRect(bx, y + j, 2, 20);
}
}
// 顶部草地
ctx.fillStyle = '#3b6e31';
ctx.fillRect(x, y, w, 12);
// 草地锯齿与受光
for(let i=0; i<w; i+=6) {
let grassH = 4 + Math.sin(i * 0.5) * 6;
// 暗绿底色
ctx.fillStyle = '#3b6e31';
ctx.fillRect(x+i, y, 6, grassH);
// 亮绿高光
ctx.fillStyle = '#5ea34e';
ctx.fillRect(x+i, y, 4, grassH * 0.7);
}
// 垂下的藤蔓
for (let i = 10; i < w; i += 45) {
const vlen = 30 + (i % 4) * 25;
for(let j=0; j<vlen; j+=6) {
ctx.fillStyle = '#2a4a24';
ctx.fillRect(x + i + Math.sin(j*0.3)*3, y + 12 + j, 3, 6);
// 叶片
if(j % 12 === 0 && j > 0) {
ctx.fillStyle = '#4a8540';
ctx.fillRect(x + i - 4, y + 12 + j, 6, 3);
ctx.fillRect(x + i + 5, y + 12 + j + 4, 6, 3);
}
}
}
}
function drawSpikes(x, y, w, h) {
for(let i=0; i<w; i+=16) {
// 尖刺暗部
ctx.fillStyle = '#4e5d6e';
ctx.beginPath();
ctx.moveTo(x + i, y + h);
ctx.lineTo(x + i + 8, y);
ctx.lineTo(x + i + 16, y + h);
ctx.fill();
// 亮部
ctx.fillStyle = '#7b93a8';
ctx.beginPath();
ctx.moveTo(x + i + 2, y + h);
ctx.lineTo(x + i + 8, y + 4);
ctx.lineTo(x + i + 10, y + h);
ctx.fill();
}
}
function drawCoin(x, y) {
const bob = Math.sin(Date.now() / 200) * 4;
// 阴影
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.beginPath();
ctx.ellipse(x, y + 16, 12, 4, 0, 0, Math.PI*2);
ctx.fill();
// 主体厚度
ctx.fillStyle = '#b87d00';
ctx.fillRect(x - 10, y - 14 + bob, 20, 28);
// 亮面
ctx.fillStyle = '#ffb400';
ctx.fillRect(x - 8, y - 12 + bob, 16, 24);
// 内圈凹槽
ctx.fillStyle = '#ffda55';
ctx.fillRect(x - 4, y - 8 + bob, 8, 16);
// 高光
ctx.fillStyle = '#ffffff';
ctx.fillRect(x - 6, y - 10 + bob, 4, 6);
}
function drawGem(x, y) {
const bob = Math.sin(Date.now() / 250) * 4;
// 阴影
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.beginPath();
ctx.ellipse(x, y + 16, 10, 4, 0, 0, Math.PI*2);
ctx.fill();
// 外轮廓暗部
ctx.fillStyle = '#0055cc';
ctx.beginPath();
ctx.moveTo(x, y - 16 + bob);
ctx.lineTo(x + 12, y + bob);
ctx.lineTo(x, y + 16 + bob);
ctx.lineTo(x - 12, y + bob);
ctx.fill();
// 内部亮部
ctx.fillStyle = '#33aaff';
ctx.beginPath();
ctx.moveTo(x, y - 12 + bob);
ctx.lineTo(x + 8, y + bob);
ctx.lineTo(x, y + 12 + bob);
ctx.lineTo(x - 8, y + bob);
ctx.fill();
// 中心切面高光
ctx.fillStyle = '#99eeff';
ctx.beginPath();
ctx.moveTo(x, y - 6 + bob);
ctx.lineTo(x + 4, y + bob);
ctx.lineTo(x, y + 4 + bob);
ctx.lineTo(x - 4, y + bob);
ctx.fill();
// 边缘白边
ctx.fillStyle = '#ffffff';
ctx.fillRect(x - 2, y - 8 + bob, 3, 3);
}
function drawSlime(e) {
const time = Date.now() / 150;
const stretch = Math.sin(time) * 3;
// 投影
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.beginPath();
ctx.ellipse(e.x + e.w/2, e.y + e.h - 2, e.w/2, 5, 0, 0, Math.PI*2);
ctx.fill();
// 身体底座
ctx.fillStyle = '#39b539';
ctx.beginPath();
ctx.arc(e.x + e.w/2, e.y + e.h - 8 + stretch, e.w/2, Math.PI, 0);
ctx.fill();
ctx.fillRect(e.x, e.y + e.h - 8 + stretch, e.w, 8 - stretch);
// 身体亮面渐变
ctx.fillStyle = '#5eeb5e';
ctx.beginPath();
ctx.arc(e.x + e.w/2, e.y + e.h - 6 + stretch, e.w/2 - 2, Math.PI, 0);
ctx.fill();
ctx.fillRect(e.x + 2, e.y + e.h - 6 + stretch, e.w - 4, 6 - stretch);
// 顶部高光
ctx.fillStyle = '#a3ffa3';
ctx.beginPath();
ctx.arc(e.x + e.w/2 - 4, e.y + e.h - 14 + stretch, 4, 0, Math.PI*2);
ctx.fill();
// 眼睛
ctx.fillStyle = '#1a331a';
ctx.fillRect(e.x + 7, e.y + 10 + stretch, 3, 5);
ctx.fillRect(e.x + e.w - 10, e.y + 10 + stretch, 3, 5);
// 眼睛高光
ctx.fillStyle = '#fff';
ctx.fillRect(e.x + 7, e.y + 10 + stretch, 1.5, 1.5);
ctx.fillRect(e.x + e.w - 10, e.y + 10 + stretch, 1.5, 1.5);
// 腮红
ctx.fillStyle = 'rgba(255, 80, 80, 0.7)';
ctx.fillRect(e.x + 3, e.y + 15 + stretch, 5, 3);
ctx.fillRect(e.x + e.w - 8, e.y + 15 + stretch, 5, 3);
}
function drawFox() {
ctx.save();
if (player.invincible > 0 && Math.floor(player.invincible / 4) % 2 === 0) {
ctx.globalAlpha = 0.6;
}
ctx.translate(player.x + player.w / 2, player.y);
if (player.facing === -1) {
ctx.scale(-1, 1);
}
ctx.translate(-player.w / 2, 0);
const w = player.w;
const h = player.h;
const legOffset = player.onGround && Math.abs(player.vx) > 0.1
? [0, 2, 0, -2][player.animFrame] : 0;
// 尾巴
ctx.fillStyle = '#f29244';
ctx.fillRect(-10, 14, 12, 12);
ctx.fillStyle = '#ffffff';
ctx.fillRect(-10, 20, 8, 6); // 白尾尖
// 背包
ctx.fillStyle = '#6b4226';
ctx.fillRect(w - 8, 12, 10, 14);
ctx.fillStyle = '#8c5836';
ctx.fillRect(w - 6, 14, 6, 4);
// 背带
ctx.fillStyle = '#4a2e1a';
ctx.fillRect(8, 12, 2, 10);
// 身体
ctx.fillStyle = '#f29244';
ctx.fillRect(2, 14, 16, 12);
ctx.fillStyle = '#ffffff';
ctx.fillRect(6, 18, 8, 8); // 白肚皮
// 绿披风
ctx.fillStyle = '#4d8c42';
ctx.fillRect(0, 12, 6, 14);
ctx.fillRect(2, 10, 12, 4);
// 披风阴影
ctx.fillStyle = '#386832';
ctx.fillRect(0, 20, 6, 6);
// 头部
ctx.fillStyle = '#f29244';
ctx.fillRect(4, 0, 16, 14);
// 耳朵
ctx.fillStyle = '#f29244';
ctx.fillRect(2, -6, 6, 8);
ctx.fillRect(16, -6, 6, 8);
ctx.fillStyle = '#3d2010'; // 黑耳尖
ctx.fillRect(4, -4, 2, 4);
ctx.fillRect(18, -4, 2, 4);
// 脸部细节
ctx.fillStyle = '#ffffff';
ctx.fillRect(8, 6, 8, 6); // 口鼻
ctx.fillStyle = '#111'; // 眼睛
ctx.fillRect(10, 2, 2, 4);
ctx.fillRect(14, 2, 2, 4);
ctx.fillStyle = '#e85d22'; // 鼻子
ctx.fillRect(11, 8, 2, 2);
// 围巾
ctx.fillStyle = '#5db54f';
ctx.fillRect(6, 12, 8, 4);
// 腿
ctx.fillStyle = '#d96f32';
ctx.fillRect(4, 26, 4, 4 + legOffset);
ctx.fillRect(12, 26, 4, 4 - legOffset);
ctx.restore();
}
function drawGoal(x, y, w, h) {
// 阴影
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x - 4, y + h - 4, w + 8, 4);
// 旗杆
ctx.fillStyle = '#c0c0c0';
ctx.fillRect(x, y, w, h);
ctx.fillStyle = '#808080';
ctx.fillRect(x + w - 4, y, 4, h);
// 顶部圆球
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(x + w/2, y - 6, 8, 0, Math.PI*2);
ctx.fill();
// 高光
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x + w/2 - 2, y - 8, 3, 0, Math.PI*2);
ctx.fill();
// 飘动旗帜
const wave = Math.sin(Date.now()/200) * 8;
ctx.fillStyle = '#ff4500';
ctx.beginPath();
ctx.moveTo(x + w, y + 10);
ctx.quadraticCurveTo(x + w + 20, y + 10 + wave, x + w + 40, y + 20);
ctx.lineTo(x + w, y + 35);
ctx.fill();
}
// ========== 前景装饰 ==========
function drawForegroundTree(x, y) {
// 树干
ctx.fillStyle = '#4a3426';
ctx.fillRect(x - 8, y - 80, 16, 80);
// 树皮纹理
ctx.fillStyle = '#332216';
ctx.fillRect(x - 4, y - 80, 4, 80);
ctx.fillRect(x + 2, y - 80, 2, 80);
// 树冠
ctx.fillStyle = '#2d5a27';
ctx.beginPath();
ctx.arc(x, y - 90, 35, 0, Math.PI*2);
ctx.arc(x - 25, y - 70, 25, 0, Math.PI*2);
ctx.arc(x + 25, y - 70, 25, 0, Math.PI*2);
ctx.fill();
// 树冠高光
ctx.fillStyle = '#44883c';
ctx.beginPath();
ctx.arc(x - 5, y - 95, 18, 0, Math.PI*2);
ctx.fill();
}
function drawMushroom(x, y) {
// 杆
ctx.fillStyle = '#e0d8c8';
ctx.fillRect(x - 5, y - 14, 10, 14);
// 杆暗部
ctx.fillStyle = '#b8b0a0';
ctx.fillRect(x + 1, y - 14, 4, 14);
// 伞盖
ctx.fillStyle = '#ff4d4d';
ctx.beginPath();
ctx.arc(x, y - 14, 12, Math.PI, 0);
ctx.fill();
// 伞盖阴影
ctx.fillStyle = '#cc3333';
ctx.fillRect(x, y - 14, 12, 4);
// 白点
ctx.fillStyle = '#fff';
ctx.fillRect(x - 8, y - 20, 5, 5);
ctx.fillRect(x + 3, y - 18, 4, 4);
}
function drawFlower(x, y, color) {
// 茎
ctx.fillStyle = '#3cb371';
ctx.fillRect(x, y - 12, 2, 12);
// 叶片
ctx.fillRect(x - 4, y - 8, 4, 2);
ctx.fillRect(x + 2, y - 5, 4, 2);
// 花瓣
ctx.fillStyle = color;
ctx.fillRect(x - 3, y - 16, 8, 6);
// 花蕊
ctx.fillStyle = '#ffd700';
ctx.fillRect(x, y - 15, 2, 2);
}
// ========== UI绘制 ==========
function drawUI() {
if (gameState === 'playing') {
// 左上角关卡
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillRect(10, 10, 105, 36);
ctx.fillStyle = '#fff';
ctx.font = 'bold 22px "Microsoft Yahei", sans-serif';
ctx.textAlign = 'left';
const levelNames = ['一', '二', '三'];
ctx.fillText('关卡' + levelNames[currentLevel - 1], 20, 36);
// 右上角金币 UI
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillRect(canvas.width - 120, 10, 110, 36);
// 绘制UI金币图标
const coinX = canvas.width - 95;
const coinY = 28;
ctx.fillStyle = '#ffc800';
ctx.beginPath();
ctx.arc(coinX, coinY, 12, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.fillRect(coinX - 4, coinY - 6, 8, 12);
ctx.fillStyle = '#ffc800';
ctx.fillRect(coinX - 2, coinY - 4, 4, 8);
ctx.fillStyle = '#fff';
ctx.font = 'bold 24px monospace';
ctx.fillText('x' + coinCount, coinX + 20, 36);
// 左下角生命值
for (let i = 0; i < player.hp; i++) {
ctx.fillStyle = '#ff3366';
// 像素心形
ctx.fillRect(25 + i * 25, 420, 4, 4);
ctx.fillRect(35 + i * 25, 420, 4, 4);
ctx.fillRect(20 + i * 25, 425, 24, 6);
ctx.fillRect(25 + i * 25, 431, 14, 4);
ctx.fillRect(30 + i * 25, 435, 4, 4);
}
}
// 界面层
if (gameState === 'start') {
ctx.fillStyle = 'rgba(18,19,28,0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = 'bold 40px "Microsoft Yahei", sans-serif';
ctx.textAlign = 'center';
ctx.fillText('森林秘境', canvas.width/2, canvas.height/2 - 20);
ctx.font = '20px "Microsoft Yahei", sans-serif';
ctx.fillStyle = '#ccc';
ctx.fillText('点击屏幕或按空格开始', canvas.width/2, canvas.height/2 + 30);
} else if (gameState === 'win') {
ctx.fillStyle = 'rgba(18,19,28,0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 40px "Microsoft Yahei", sans-serif';
ctx.textAlign = 'center';
if (currentLevel < maxLevel) {
ctx.fillText('关卡通过!', canvas.width/2, canvas.height/2 - 40);
ctx.fillStyle = '#fff';
ctx.font = '20px "Microsoft Yahei", sans-serif';
ctx.fillText('点击屏幕或按空格进入下一关', canvas.width/2, canvas.height/2 + 30);
} else {
ctx.fillText('全部通关!', canvas.width/2, canvas.height/2 - 40);
ctx.fillStyle = '#fff';
ctx.font = '20px "Microsoft Yahei", sans-serif';
ctx.fillText('点击屏幕或按 R 重新开始', canvas.width/2, canvas.height/2 + 30);
}
ctx.fillText('金币: ' + coinCount + ' 宝石: ' + gemCount, canvas.width/2, canvas.height/2 + 70);
} else if (gameState === 'lose') {
ctx.fillStyle = 'rgba(18,19,28,0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ff4500';
ctx.font = 'bold 40px "Microsoft Yahei", sans-serif';
ctx.textAlign = 'center';
ctx.fillText('游戏结束', canvas.width/2, canvas.height/2 - 20);
ctx.fillStyle = '#fff';
ctx.font = '20px "Microsoft Yahei", sans-serif';
ctx.fillText('点击屏幕或按 R 重新开始', canvas.width/2, canvas.height/2 + 30);
}
ctx.textAlign = 'left';
}
// ========== 状态控制 ==========
function restartGame() {
currentLevel = 1;
initPlayer();
initLevel(currentLevel);
initBackground();
gameState = 'playing';
}
function nextLevel() {
if (currentLevel < maxLevel) {
currentLevel++;
initPlayer();
initLevel(currentLevel);
initBackground();
gameState = 'playing';
} else {
restartGame();
}
}
// ========== 主循环 ==========
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
initPlayer();
initLevel(1);
initBackground();
gameState = 'start';
gameLoop();
</script>