萌宠闯关
by SonicKnight511420 lines35.2 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>萌宠闯关</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
html, body {
height: 100%;
overflow: hidden;
font-family: "Microsoft YaHei", sans-serif;
background: #ffeaf4;
}
#gameWrap {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(180deg, #fff5fb 0%, #e4f3ff 100%);
}
.overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.96);
z-index: 10;
overflow-y: auto;
overflow-x: hidden;
padding: 24px 16px 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.overlay h1 {
font-size: clamp(28px, 7vw, 42px);
color: #ff4d8d;
text-shadow: 2px 2px 0 #ffd1e3;
text-align: center;
}
.overlay p {
font-size: clamp(14px, 3.5vw, 18px);
color: #666;
text-align: center;
line-height: 1.6;
}
.btn {
background: linear-gradient(180deg, #ff9ec7, #ff66a5);
color: #fff;
border: none;
border-radius: 999px;
padding: 14px 32px;
font-size: 18px;
font-weight: bold;
box-shadow: 0 8px 20px rgba(255, 102, 165, 0.35);
margin: 4px;
}
.btn-sm {
padding: 8px 16px;
font-size: 14px;
}
.role-grid {
display: grid;
grid-template-columns: repeat(3, minmax(90px, 110px));
gap: 14px;
margin: 10px 0;
}
.role-card {
background: #fff;
border: 3px solid #ffd1e3;
border-radius: 18px;
padding: 10px;
text-align: center;
}
.role-card.active {
border-color: #ff66a5;
box-shadow: 0 10px 20px rgba(255, 102, 165, 0.3);
}
.role-card canvas {
width: 80px;
height: 80px;
background: #fff;
border-radius: 14px;
}
.role-card .name {
margin-top: 6px;
color: #ff4d8d;
font-weight: bold;
font-size: 14px;
}
.level-grid {
display: grid;
grid-template-columns: repeat(3, 70px);
gap: 10px;
margin: 10px 0;
}
.level-btn {
width: 70px;
height: 70px;
border-radius: 14px;
border: none;
background: #ffd1e3;
color: #ff4d8d;
font-weight: bold;
font-size: 20px;
}
.touchPad {
position: absolute;
bottom: 24px;
left: 0;
right: 0;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 5;
}
.dirGroup {
display: flex;
gap: 12px;
}
.touchBtn {
width: 64px;
height: 64px;
border-radius: 50%;
border: none;
background: rgba(255, 105, 180, 0.85);
color: #fff;
font-size: 24px;
font-weight: bold;
}
.jumpBtn {
width: 72px;
height: 72px;
}
.top-bar {
position: absolute;
top: 12px;
right: 12px;
display: none;
gap: 8px;
z-index: 6;
}
.hint {
font-size: 13px;
color: #999;
}
/* 歪歪叽奖励舞台 */
.waiwai-stage {
width: 260px;
height: 340px;
position: relative;
margin: 10px 0;
}
.waiwai-stage canvas {
width: 100%;
height: 100%;
background: radial-gradient(circle, #fff0f8 0%, #ffe1f2 100%);
border-radius: 24px;
border: 3px solid #ffb3dc;
}
.reward-box {
background: #fff7bc;
border-radius: 16px;
padding: 16px 24px;
border: 2px solid #ffd24d;
text-align: center;
}
.reward-text {
color: #cc8800;
font-weight: bold;
font-size: 18px;
}
/* 歪歪叽入场 */
@keyframes waiwaiPop {
0% { transform: scale(0.2) translateY(80px); opacity: 0; }
60% { transform: scale(1.15) translateY(-10px); opacity: 1; }
100% { transform: scale(1) translateY(0); opacity: 1; }
}
.waiwai-enter {
animation: waiwaiPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
</style>
</head>
<body>
<div id="gameWrap">
<canvas id="game"></canvas>
<div class="top-bar" id="topBar">
<button class="btn btn-sm" id="restartLevelBtn">重开本关</button>
<button class="btn btn-sm" id="backHomeBtn">返回主页</button>
</div>
<div class="touchPad" id="touchUI">
<div class="dirGroup">
<button class="touchBtn" id="btnLeft">←</button>
<button class="touchBtn" id="btnRight">→</button>
</div>
<button class="touchBtn jumpBtn" id="btnJump">跳</button>
</div>
<!-- 主页 -->
<div class="overlay" id="homeScreen">
<h1>萌宠闯关</h1>
<p>选择可爱萌宠,闯关收集星星</p>
<button class="btn" id="homeStartBtn">开始游戏</button>
<button class="btn" id="homeRoleBtn">选择形象</button>
<button class="btn" id="homeLevelBtn">关卡选择</button>
<div class="hint">手机:下方按键 | 电脑:AD移动,空格跳跃</div>
</div>
<!-- 选形象 -->
<div class="overlay" id="roleScreen" style="display:none;">
<h1>挑选萌宠</h1>
<div class="role-grid" id="roleGrid"></div>
<button class="btn" id="roleBackBtn">返回</button>
</div>
<!-- 关卡选择 -->
<div class="overlay" id="levelScreen" style="display:none;">
<h1>选择关卡</h1>
<div class="level-grid" id="levelGrid"></div>
<button class="btn" id="levelBackBtn">返回</button>
</div>
<!-- 歪歪叽奖励弹窗 -->
<div class="overlay" id="waiwaiRewardScreen" style="display:none;">
<h1 id="rewardTitle">🎉 关卡通关!</h1>
<div class="waiwai-stage">
<canvas id="waiwaiCanvas"></canvas>
</div>
<div class="reward-box">
<p class="reward-text">本局收集星星:<span id="starReward">0</span> 颗</p>
<p class="reward-text" id="rewardName">🎁 奖励:获得歪歪叽的祝福</p>
</div>
<p id="rewardTip">太棒啦!</p>
<button class="btn" id="rewardNextBtn">继续</button>
</div>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
let gameState = 'home';
let currentLevel = 0;
let selectedRole = 0;
let stars = 0;
let lives = 3;
let invincible = 0;
let hitCooldown = 0;
let walkAnimTimer = 0;
const roles = [
{ name: '小猫', color: '#ffb3d1', ear: 'cat' },
{ name: '小兔子', color: '#d4b3ff', ear: 'rabbit' },
{ name: '小狗', color: '#ffd7a8', ear: 'dog' },
{ name: '小鸟', color: '#b3ecff', ear: 'bird' },
{ name: '熊猫', color: '#333333', ear: 'panda' }
];
const player = {
x: 60, y: 400, w: 44, h: 54,
vx: 0, vy: 0, speed: 4.2, jump: -13.5,
onGround: false, facing: 1, walkFrame: 0
};
const levels = [
{
name: '弯弯小路',
width: 2400,
start: { x: 60, y: 400 },
platforms: [
{ x: 0, y: 540, w: 2400, h: 60 },
{ x: 260, y: 440, w: 140, h: 20 },
{ x: 460, y: 380, w: 140, h: 20 },
{ x: 660, y: 440, w: 140, h: 20 },
{ x: 860, y: 360, w: 180, h: 20 },
{ x: 1100, y: 440, w: 140, h: 20 },
{ x: 1300, y: 380, w: 140, h: 20 },
{ x: 1500, y: 320, w: 160, h: 20 },
{ x: 1720, y: 400, w: 140, h: 20 },
{ x: 1920, y: 340, w: 140, h: 20 },
{ x: 2120, y: 440, w: 140, h: 20 }
],
moving: [
{ x: 560, y: 300, w: 120, h: 18, minX: 500, maxX: 720, speed: 1.2, dir: 1 },
{ x: 1200, y: 260, w: 120, h: 18, minX: 1120, maxX: 1360, speed: 1.6, dir: 1 }
],
spikes: [
{ x: 420, y: 520, w: 40, h: 20 },
{ x: 780, y: 520, w: 40, h: 20 },
{ x: 1420, y: 520, w: 40, h: 20 },
{ x: 1820, y: 520, w: 40, h: 20 }
],
stars: [
{ x: 320, y: 400 }, { x: 520, y: 340 }, { x: 940, y: 320 },
{ x: 1360, y: 340 }, { x: 1580, y: 280 }, { x: 1980, y: 300 },
{ x: 2200, y: 400 }
],
enemies: [
{ x: 600, y: 500, w: 36, h: 40, minX: 540, maxX: 720, speed: 1.1, dir: 1 },
{ x: 1600, y: 500, w: 36, h: 40, minX: 1520, maxX: 1720, speed: 1.3, dir: 1 }
],
goal: { x: 2320, y: 460 }
},
{
name: '跳跃阶梯',
width: 2600,
start: { x: 60, y: 400 },
platforms: [
{ x: 0, y: 540, w: 520, h: 60 },
{ x: 180, y: 460, w: 120, h: 20 },
{ x: 340, y: 380, w: 120, h: 20 },
{ x: 500, y: 300, w: 120, h: 20 },
{ x: 680, y: 360, w: 140, h: 20 },
{ x: 880, y: 440, w: 140, h: 20 },
{ x: 1080, y: 360, w: 140, h: 20 },
{ x: 1280, y: 280, w: 140, h: 20 },
{ x: 1480, y: 360, w: 140, h: 20 },
{ x: 1680, y: 440, w: 140, h: 20 },
{ x: 1880, y: 340, w: 140, h: 20 },
{ x: 2080, y: 260, w: 140, h: 20 },
{ x: 2280, y: 340, w: 140, h: 20 },
{ x: 2420, y: 540, w: 180, h: 60 }
],
moving: [
{ x: 760, y: 220, w: 120, h: 18, minX: 700, maxX: 900, speed: 1.4, dir: 1 },
{ x: 1560, y: 240, w: 120, h: 18, minX: 1480, maxX: 1680, speed: 1.7, dir: 1 },
{ x: 1960, y: 180, w: 120, h: 18, minX: 1880, maxX: 2080, speed: 2, dir: 1 }
],
spikes: [
{ x: 540, y: 520, w: 40, h: 20 },
{ x: 1000, y: 520, w: 40, h: 20 },
{ x: 1400, y: 520, w: 40, h: 20 },
{ x: 1800, y: 520, w: 40, h: 20 },
{ x: 2200, y: 520, w: 40, h: 20 }
],
stars: [
{ x: 240, y: 420 }, { x: 400, y: 340 }, { x: 560, y: 260 },
{ x: 740, y: 320 }, { x: 1140, y: 320 }, { x: 1340, y: 240 },
{ x: 1740, y: 400 }, { x: 1940, y: 300 }, { x: 2140, y: 220 },
{ x: 2340, y: 300 }
],
enemies: [
{ x: 820, y: 400, w: 36, h: 40, minX: 760, maxX: 920, speed: 1.3, dir: 1 },
{ x: 1340, y: 240, w: 36, h: 40, minX: 1280, maxX: 1420, speed: 1.5, dir: 1 },
{ x: 1940, y: 300, w: 36, h: 40, minX: 1880, maxX: 2020, speed: 1.8, dir: 1 }
],
goal: { x: 2520, y: 460 }
},
{
name: '云海霸主',
width: 2800,
start: { x: 60, y: 400 },
platforms: [
{ x: 0, y: 540, w: 420, h: 60 },
{ x: 140, y: 440, w: 120, h: 20 },
{ x: 300, y: 360, w: 120, h: 20 },
{ x: 480, y: 420, w: 140, h: 20 },
{ x: 680, y: 340, w: 140, h: 20 },
{ x: 880, y: 260, w: 140, h: 20 },
{ x: 1080, y: 340, w: 140, h: 20 },
{ x: 1280, y: 420, w: 140, h: 20 },
{ x: 1480, y: 320, w: 140, h: 20 },
{ x: 1680, y: 240, w: 140, h: 20 },
{ x: 1880, y: 320, w: 140, h: 20 },
{ x: 2080, y: 400, w: 140, h: 20 },
{ x: 2280, y: 300, w: 140, h: 20 },
{ x: 2480, y: 220, w: 140, h: 20 },
{ x: 2640, y: 540, w: 160, h: 60 }
],
moving: [
{ x: 420, y: 300, w: 120, h: 18, minX: 360, maxX: 540, speed: 1.6, dir: 1 },
{ x: 760, y: 200, w: 120, h: 18, minX: 680, maxX: 860, speed: 1.9, dir: 1 },
{ x: 1160, y: 220, w: 120, h: 18, minX: 1080, maxX: 1260, speed: 2.1, dir: 1 },
{ x: 1560, y: 180, w: 120, h: 18, minX: 1480, maxX: 1660, speed: 2.3, dir: 1 },
{ x: 1960, y: 220, w: 120, h: 18, minX: 1880, maxX: 2060, speed: 2.5, dir: 1 },
{ x: 2360, y: 180, w: 120, h: 18, minX: 2280, maxX: 2460, speed: 2.7, dir: 1 }
],
spikes: [
{ x: 440, y: 520, w: 40, h: 20 },
{ x: 740, y: 520, w: 40, h: 20 },
{ x: 1040, y: 520, w: 40, h: 20 },
{ x: 1340, y: 520, w: 40, h: 20 },
{ x: 1740, y: 520, w: 40, h: 20 },
{ x: 2140, y: 520, w: 40, h: 20 },
{ x: 2540, y: 520, w: 40, h: 20 }
],
stars: [
{ x: 200, y: 400 }, { x: 360, y: 320 }, { x: 540, y: 380 },
{ x: 740, y: 300 }, { x: 940, y: 220 }, { x: 1140, y: 300 },
{ x: 1340, y: 380 }, { x: 1540, y: 280 }, { x: 1740, y: 200 },
{ x: 1940, y: 280 }, { x: 2140, y: 360 }, { x: 2340, y: 260 },
{ x: 2540, y: 180 }, { x: 2700, y: 500 }
],
enemies: [
{ x: 520, y: 380, w: 36, h: 40, minX: 480, maxX: 620, speed: 1.4, dir: 1 },
{ x: 940, y: 220, w: 36, h: 40, minX: 880, maxX: 1020, speed: 1.7, dir: 1 },
{ x: 1340, y: 380, w: 36, h: 40, minX: 1280, maxX: 1420, speed: 1.9, dir: 1 },
{ x: 1740, y: 200, w: 36, h: 40, minX: 1680, maxX: 1820, speed: 2.1, dir: 1 },
{ x: 2140, y: 360, w: 36, h: 40, minX: 2080, maxX: 2220, speed: 2.3, dir: 1 },
{ x: 2540, y: 180, w: 36, h: 40, minX: 2480, maxX: 2620, speed: 2.5, dir: 1 }
],
goal: { x: 2720, y: 460 }
}
];
let level = null;
let camera = { x: 0 };
const keys = {};
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (['Space', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
document.addEventListener('keyup', e => {
keys[e.code] = false;
});
const btnLeft = document.getElementById('btnLeft');
const btnRight = document.getElementById('btnRight');
const btnJump = document.getElementById('btnJump');
function bindTouch(btn, key) {
btn.addEventListener('touchstart', ev => {
ev.preventDefault();
keys[key] = true;
});
btn.addEventListener('touchend', ev => {
ev.preventDefault();
keys[key] = false;
});
btn.addEventListener('touchcancel', ev => {
ev.preventDefault();
keys[key] = false;
});
}
bindTouch(btnLeft, 'ArrowLeft');
bindTouch(btnRight, 'ArrowRight');
bindTouch(btnJump, 'Space');
function loadLevel(i) {
currentLevel = i;
level = JSON.parse(JSON.stringify(levels[i]));
player.x = level.start.x;
player.y = level.start.y;
player.vx = 0;
player.vy = 0;
player.onGround = false;
camera.x = 0;
}
function drawRole(role, x, y, w, h, facing = 1, walkFrame = 0) {
ctx.save();
ctx.translate(x + w / 2, y + h / 2);
ctx.scale(facing, 1);
ctx.fillStyle = role.color;
ctx.beginPath();
ctx.ellipse(0, 4, w / 2 - 4, h / 2 - 12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(0, -14, 16, 0, Math.PI * 2);
ctx.fill();
if (role.ear === 'cat') {
ctx.beginPath();
ctx.moveTo(-10, -24);
ctx.lineTo(-14, -36);
ctx.lineTo(-4, -26);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(10, -24);
ctx.lineTo(14, -36);
ctx.lineTo(4, -26);
ctx.closePath();
ctx.fill();
} else if (role.ear === 'rabbit') {
ctx.beginPath();
ctx.ellipse(-8, -30, 4, 12, -0.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(8, -30, 4, 12, 0.2, 0, Math.PI * 2);
ctx.fill();
} else if (role.ear === 'dog') {
ctx.beginPath();
ctx.ellipse(-14, -18, 6, 10, -0.4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(14, -18, 6, 10, 0.4, 0, Math.PI * 2);
ctx.fill();
} else if (role.ear === 'bird') {
ctx.beginPath();
ctx.moveTo(-14, -16);
ctx.lineTo(-22, -8);
ctx.lineTo(-10, -10);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(14, -16);
ctx.lineTo(22, -8);
ctx.lineTo(10, -10);
ctx.closePath();
ctx.fill();
} else if (role.ear === 'panda') {
ctx.fillStyle = '#222';
ctx.beginPath();
ctx.arc(-12, -22, 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(12, -22, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = role.color;
}
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.ellipse(0, -10, 10, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#222';
ctx.beginPath();
ctx.arc(-5, -16, 2.4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(5, -16, 2.4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255, 100, 130, 0.5)';
ctx.beginPath();
ctx.ellipse(-10, -10, 3.4, 2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(10, -10, 3.4, 2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = role.color;
const legOffset = Math.sin(walkFrame * 0.4) * 7;
ctx.beginPath();
ctx.ellipse(-7, 18 + legOffset, 5, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(7, 18 - legOffset, 5, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawStar(x, y, t) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(t * 0.04);
ctx.fillStyle = '#ffd24d';
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const a = (i * Math.PI * 2) / 5 - Math.PI / 2;
const r = i % 2 === 0 ? 12 : 5;
ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r);
}
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#ffb347';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
function drawSpike(x, y, w, h) {
ctx.fillStyle = '#ff4d6d';
const n = Math.floor(w / 12);
for (let i = 0; i < n; i++) {
ctx.beginPath();
ctx.moveTo(x + i * 12, y + h);
ctx.lineTo(x + i * 12 + 6, y);
ctx.lineTo(x + i * 12 + 12, y + h);
ctx.closePath();
ctx.fill();
}
}
function drawEnemy(e, t) {
ctx.save();
ctx.translate(e.x + e.w / 2, e.y + e.h / 2);
ctx.scale(e.dir, 1);
ctx.fillStyle = '#a06bff';
ctx.beginPath();
ctx.ellipse(0, 4, e.w / 2, e.h / 2 - 4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(0, -8, 12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(-4, -10, 3, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(4, -10, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#222';
ctx.beginPath();
ctx.arc(-4, -10, 1.4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(4, -10, 1.4, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#a06bff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-6, -18);
ctx.lineTo(-10, -26 + Math.sin(t * 0.1));
ctx.moveTo(6, -18);
ctx.lineTo(10, -26 + Math.sin(t * 0.1 + 1));
ctx.stroke();
ctx.restore();
}
function drawGoal(g, t) {
ctx.save();
ctx.translate(g.x, g.y);
ctx.fillStyle = '#8b5a2b';
ctx.fillRect(-3, -100, 6, 100);
ctx.fillStyle = '#ff66a5';
ctx.beginPath();
ctx.moveTo(3, -100);
ctx.lineTo(60 + Math.sin(t * 0.1) * 4, -80);
ctx.lineTo(3, -60);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px sans-serif';
ctx.fillText('终点', 12, -74);
ctx.restore();
}
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, canvas.height);
g.addColorStop(0, '#fff5fb');
g.addColorStop(1, '#dceeff');
ctx.fillStyle = g;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
for (let i = 0; i < 10; i++) {
const cx = ((i * 320 - camera.x * 0.2) % (canvas.width + 400) + canvas.width + 400) % (canvas.width + 400) - 200;
const cy = 60 + (i % 3) * 50;
ctx.beginPath();
ctx.arc(cx, cy, 30, 0, Math.PI * 2);
ctx.arc(cx + 24, cy - 8, 24, 0, Math.PI * 2);
ctx.arc(cx + 48, cy, 28, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = 'rgba(180, 210, 255, 0.5)';
for (let i = 0; i < 8; i++) {
const mx = ((i * 360 - camera.x * 0.4) % (canvas.width + 400) + canvas.width + 400) % (canvas.width + 400) - 200;
ctx.beginPath();
ctx.moveTo(mx, 420);
ctx.lineTo(mx + 120, 260);
ctx.lineTo(mx + 240, 420);
ctx.closePath();
ctx.fill();
}
}
function rectsCollide(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() {
if (!level) return;
walkAnimTimer += 1;
if (keys['ArrowLeft'] || keys['KeyA']) {
player.vx = -player.speed;
player.facing = -1;
player.walkFrame = walkAnimTimer;
} else if (keys['ArrowRight'] || keys['KeyD']) {
player.vx = player.speed;
player.facing = 1;
player.walkFrame = walkAnimTimer;
} else {
player.vx = 0;
}
if ((keys['Space'] || keys['ArrowUp'] || keys['KeyW']) && player.onGround) {
player.vy = player.jump;
player.onGround = false;
}
player.vy += 0.55;
if (player.vy > 14) player.vy = 14;
player.x += player.vx;
player.y += player.vy;
player.onGround = false;
for (let p of level.platforms) {
if (rectsCollide(player, p)) {
if (player.vy > 0 && player.y + player.h - player.vy <= p.y + 10) {
player.y = p.y - player.h;
player.vy = 0;
player.onGround = true;
}
}
}
for (let m of level.moving) {
m.x += m.speed * m.dir;
if (m.x < m.minX) {
m.x = m.minX;
m.dir = 1;
}
if (m.x + m.w > m.maxX + m.w) {
m.x = m.maxX;
m.dir = -1;
}
if (rectsCollide(player, m)) {
if (player.vy > 0 && player.y + player.h - player.vy <= m.y + 10) {
player.y = m.y - player.h;
player.vy = 0;
player.onGround = true;
player.x += m.speed * m.dir;
}
}
}
for (let e of level.enemies) {
e.x += e.speed * e.dir;
if (e.x < e.minX) {
e.x = e.minX;
e.dir = 1;
}
if (e.x + e.w > e.maxX + e.w) {
e.x = e.maxX;
e.dir = -1;
}
}
if (invincible > 0) invincible--;
if (hitCooldown > 0) hitCooldown--;
for (let s of level.spikes) {
if (rectsCollide(player, s) && invincible === 0) {
lives--;
invincible = 90;
hitCooldown = 60;
if (lives <= 0) {
loadLevel(currentLevel);
lives = 3;
}
player.x = level.start.x;
player.y = level.start.y;
player.vy = 0;
}
}
for (let e of level.enemies) {
if (rectsCollide(player, e) && invincible === 0) {
lives--;
invincible = 90;
hitCooldown = 60;
if (lives <= 0) {
loadLevel(currentLevel);
lives = 3;
}
player.x = level.start.x;
player.y = level.start.y;
player.vy = 0;
}
}
for (let i = level.stars.length - 1; i >= 0; i--) {
const s = level.stars[i];
if (rectsCollide(player, { x: s.x - 12, y: s.y - 12, w: 24, h: 24 })) {
stars++;
level.stars.splice(i, 1);
}
}
if (rectsCollide(player, { x: level.goal.x - 20, y: level.goal.y - 80, w: 40, h: 80 })) {
showWaiwaiReward(currentLevel);
}
if (player.x < 0) player.x = 0;
if (player.x > level.width - player.w) player.x = level.width - player.w;
const targetCam = player.x - canvas.width / 2 + player.w / 2;
camera.x += (targetCam - camera.x) * 0.08;
if (camera.x < 0) camera.x = 0;
if (camera.x > level.width - canvas.width) camera.x = level.width - canvas.width;
}
function render(t) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
ctx.save();
ctx.translate(-camera.x, 0);
for (let p of level.platforms) {
ctx.fillStyle = '#b3e299';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#86c564';
ctx.fillRect(p.x, p.y, p.w, 4);
}
for (let m of level.moving) {
ctx.fillStyle = '#89c9ff';
ctx.fillRect(m.x, m.y, m.w, m.h);
}
for (let s of level.spikes) drawSpike(s.x, s.y, s.w, s.h);
for (let s of level.stars) drawStar(s.x, s.y, t);
for (let e of level.enemies) drawEnemy(e, t);
drawGoal(level.goal.x, level.goal.y, t);
if (invincible === 0 || Math.floor(t / 8) % 2 === 0) {
drawRole(roles[selectedRole], player.x, player.y, player.w, player.h, player.facing, player.walkFrame);
}
ctx.restore();
ctx.fillStyle = '#333';
ctx.font = 'bold 18px sans-serif';
ctx.fillText('星星:' + stars, 20, 30);
ctx.fillText('生命:' + lives, 20, 55);
}
function loop(t) {
if (gameState === 'play') {
update();
render(t);
}
requestAnimationFrame(loop);
}
function hideAllOverlay() {
document.querySelectorAll('.overlay').forEach(el => el.style.display = 'none');
}
function showOverlay(id) {
hideAllOverlay();
document.getElementById(id).style.display = 'flex';
}
// ========== 歪歪叽奖励系统 ==========
const waiwaiCanvas = document.getElementById('waiwaiCanvas');
const waiwaiCtx = waiwaiCanvas.getContext('2d');
let waiwaiAction = 'wave';
let waiwaiAnim = 0;
function resizeWaiwaiCanvas() {
const rect = waiwaiCanvas.getBoundingClientRect();
waiwaiCanvas.width = rect.width * window.devicePixelRatio;
waiwaiCanvas.height = rect.height * window.devicePixelRatio;
waiwaiCtx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
}
function drawWaiwaiStand(scale = 1) {
const w = waiwaiCanvas.width / window.devicePixelRatio;
const h = waiwaiCanvas.height / window.devicePixelRatio;
waiwaiCtx.save();
waiwaiCtx.translate(w / 2, h / 2);
waiwaiCtx.scale(scale, scale);
// 身体
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(0, 40, 50, 60, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 毛绒外套质感
waiwaiCtx.fillStyle = '#ffeef5';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(0, 40, 44, 54, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 粉白条纹内搭
waiwaiCtx.fillStyle = '#ffd6e8';
waiwaiCtx.fillRect(-30, 0, 60, 20);
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.fillRect(-30, 20, 60, 20);
waiwaiCtx.fillStyle = '#ffd6e8';
waiwaiCtx.fillRect(-30, 40, 60, 20);
// 白色短裙
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.beginPath();
waiwaiCtx.moveTo(-40, 80);
waiwaiCtx.lineTo(40, 80);
waiwaiCtx.lineTo(30, 110);
waiwaiCtx.lineTo(-30, 110);
waiwaiCtx.closePath();
waiwaiCtx.fill();
// 白色长袜
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.fillRect(-24, 110, 18, 50);
waiwaiCtx.fillRect(6, 110, 18, 50);
// 蓬松袜套
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-15, 140, 16, 12, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(15, 140, 16, 12, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 浅棕鞋子
waiwaiCtx.fillStyle = '#d9b38c';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-15, 162, 14, 8, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(15, 162, 14, 8, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 头
waiwaiCtx.fillStyle = '#ffe0b3';
waiwaiCtx.beginPath();
waiwaiCtx.arc(0, -40, 40, 0, Math.PI * 2);
waiwaiCtx.fill();
// 双马尾卷发
waiwaiCtx.fillStyle = '#f2c96b';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-44, -30, 14, 40, -0.3, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(44, -30, 14, 40, 0.3, 0, Math.PI * 2);
waiwaiCtx.fill();
// 刘海
waiwaiCtx.fillStyle = '#f2c96b';
waiwaiCtx.beginPath();
waiwaiCtx.moveTo(-30, -70);
waiwaiCtx.quadraticCurveTo(-15, -95, 0, -70);
waiwaiCtx.quadraticCurveTo(15, -95, 30, -70);
waiwaiCtx.closePath();
waiwaiCtx.fill();
// 黑色项链
waiwaiCtx.strokeStyle = '#222222';
waiwaiCtx.lineWidth = 2;
waiwaiCtx.beginPath();
waiwaiCtx.moveTo(-14, 20);
waiwaiCtx.lineTo(0, 30);
waiwaiCtx.lineTo(14, 20);
waiwaiCtx.stroke();
// 眼睛
waiwaiCtx.fillStyle = '#222222';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-14, -44, 4, 6, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(14, -44, 4, 6, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 腮红
waiwaiCtx.fillStyle = 'rgba(255, 105, 135, 0.45)';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-26, -34, 6, 4, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(26, -34, 6, 4, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.restore();
}
function drawWaiwaiWave() {
const w = waiwaiCanvas.width / window.devicePixelRatio;
const h = waiwaiCanvas.height / window.devicePixelRatio;
const swing = Math.sin(waiwaiAnim * 0.15) * 18;
waiwaiCtx.clearRect(0, 0, w, h);
drawWaiwaiStand(1);
// 挥手手臂
waiwaiCtx.save();
waiwaiCtx.translate(w / 2 + 40, h / 2 + 10);
waiwaiCtx.rotate((swing * Math.PI) / 180);
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(0, -30, 10, 28, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 比心
waiwaiCtx.fillStyle = '#ff66a5';
waiwaiCtx.beginPath();
waiwaiCtx.moveTo(0, -52);
waiwaiCtx.bezierCurveTo(-14, -68, -24, -52, 0, -30);
waiwaiCtx.bezierCurveTo(24, -52, 14, -68, 0, -30);
waiwaiCtx.closePath();
waiwaiCtx.fill();
waiwaiCtx.restore();
}
function drawWaiwaiMiss() {
const w = waiwaiCanvas.width / window.devicePixelRatio;
const h = waiwaiCanvas.height / window.devicePixelRatio;
const bob = Math.sin(waiwaiAnim * 0.1) * 4;
waiwaiCtx.clearRect(0, 0, w, h);
drawWaiwaiStand(1);
// 双手托腮
waiwaiCtx.save();
waiwaiCtx.translate(w / 2, h / 2 - 40 + bob);
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-30, 0, 10, 22, 0.4, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(30, 0, 10, 22, -0.4, 0, Math.PI * 2);
waiwaiCtx.fill();
// 害羞表情
waiwaiCtx.fillStyle = '#222222';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-14, -44, 4, 6, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(14, -44, 4, 6, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 想念感小泪滴
waiwaiCtx.fillStyle = '#89c9ff';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(-22, -30, 3, 6, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
waiwaiCtx.restore();
}
function drawWaiwaiKiss() {
const w = waiwaiCanvas.width / window.devicePixelRatio;
const h = waiwaiCanvas.height / window.devicePixelRatio;
const float = Math.sin(waiwaiAnim * 0.12) * 5;
waiwaiCtx.clearRect(0, 0, w, h);
drawWaiwaiStand(1);
// 飞吻姿势
waiwaiCtx.save();
waiwaiCtx.translate(w / 2, h / 2 - 40 + float);
waiwaiCtx.fillStyle = '#ffffff';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(0, -10, 10, 26, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 飞吻嘴
waiwaiCtx.fillStyle = '#ff66a5';
waiwaiCtx.beginPath();
waiwaiCtx.ellipse(0, -40, 10, 7, 0, 0, Math.PI * 2);
waiwaiCtx.fill();
// 飞出爱心
for (let i = 0; i < 3; i++) {
const t = waiwaiAnim * 0.1 + i * 2;
const ax = Math.cos(t) * 40 + i * 22;
const ay = Math.sin(t * 1.3) * 18 - 20 - i * 12;
waiwaiCtx.fillStyle = '#ff4d8d';
waiwaiCtx.beginPath();
waiwaiCtx.moveTo(ax, ay);
waiwaiCtx.bezierCurveTo(ax - 10, ay - 10, ax - 18, ay + 4, ax, ay + 14);
waiwaiCtx.bezierCurveTo(ax + 18, ay + 4, ax + 10, ay - 10, ax, ay);
waiwaiCtx.closePath();
waiwaiCtx.fill();
}
waiwaiCtx.restore();
}
function waiwaiLoop() {
waiwaiAnim++;
if (waiwaiAction === 'wave') drawWaiwaiWave();
else if (waiwaiAction === 'miss') drawWaiwaiMiss();
else if (waiwaiAction === 'kiss') drawWaiwaiKiss();
requestAnimationFrame(waiwaiLoop);
}
function showWaiwaiReward(levelIndex) {
gameState = 'reward';
document.getElementById('topBar').style.display = 'none';
const rewardList = [
{
title: '🎉 第1关通关!',
action: 'wave',
name: '🎁 奖励:获得歪歪叽的祝福',
tip: '祝福送达,继续加油!'
},
{
title: '🎉 第2关通关!',
action: 'miss',
name: '🎁 奖励:获得歪歪叽的想念',
tip: '歪歪叽在想你~'
},
{
title: '🎉 第3关通关!',
action: 'kiss',
name: '🎁 奖励:获得歪歪叽香吻一枚',
tip: '啵啵~通关成功!'
}
];
const reward = rewardList[levelIndex];
document.getElementById('rewardTitle').innerText = reward.title;
document.getElementById('rewardName').innerText = reward.name;
document.getElementById('rewardTip').innerText = reward.tip;
document.getElementById('starReward').innerText = stars;
waiwaiAction = reward.action;
resizeWaiwaiCanvas();
showOverlay('waiwaiRewardScreen');
}
document.getElementById('rewardNextBtn').onclick = () => {
if (currentLevel < levels.length - 1) {
currentLevel++;
loadLevel(currentLevel);
gameState = 'play';
document.getElementById('topBar').style.display = 'flex';
hideAllOverlay();
} else {
gameState = 'home';
showOverlay('homeScreen');
stars = 0;
lives = 3;
currentLevel = 0;
}
};
const roleGrid = document.getElementById('roleGrid');
function renderRoleSelect() {
roleGrid.innerHTML = '';
roles.forEach((r, i) => {
const card = document.createElement('div');
card.className = 'role-card' + (i === selectedRole ? ' active' : '');
card.innerHTML = `<canvas width="80" height="80"></canvas><div class="name">${r.name}</div>`;
const c = card.querySelector('canvas');
const cx = c.getContext('2d');
cx.save();
cx.translate(40, 40);
cx.fillStyle = r.color;
cx.beginPath();
cx.ellipse(0, 4, 30, 36, 0, 0, Math.PI * 2);
cx.fill();
cx.beginPath();
cx.arc(0, -12, 14, 0, Math.PI * 2);
cx.fill();
if (r.ear === 'cat') {
cx.beginPath();
cx.moveTo(-10, -24);
cx.lineTo(-14, -36);
cx.lineTo(-4, -26);
cx.closePath();
cx.fill();
cx.beginPath();
cx.moveTo(10, -24);
cx.lineTo(14, -36);
cx.lineTo(4, -26);
cx.closePath();
cx.fill();
} else if (r.ear === 'rabbit') {
cx.beginPath();
cx.ellipse(-8, -30, 4, 12, -0.2, 0, Math.PI * 2);
cx.fill();
cx.beginPath();
cx.ellipse(8, -30, 4, 12, 0.2, 0, Math.PI * 2);
cx.fill();
} else if (r.ear === 'dog') {
cx.beginPath();
cx.ellipse(-14, -18, 6, 10, -0.4, 0, Math.PI * 2);
cx.fill();
cx.beginPath();
cx.ellipse(14, -18, 6, 10, 0.4, 0, Math.PI * 2);
cx.fill();
} else if (r.ear === 'bird') {
cx.beginPath();
cx.moveTo(-14, -16);
cx.lineTo(-22, -8);
cx.lineTo(-10, -10);
cx.closePath();
cx.fill();
cx.beginPath();
cx.moveTo(14, -16);
cx.lineTo(22, -8);
cx.lineTo(10, -10);
cx.closePath();
cx.fill();
} else if (r.ear === 'panda') {
cx.fillStyle = '#222';
cx.beginPath();
cx.arc(-12, -22, 6, 0, Math.PI * 2);
cx.fill();
cx.beginPath();
cx.arc(12, -22, 6, 0, Math.PI * 2);
cx.fill();
cx.fillStyle = r.color;
}
cx.fillStyle = '#fff';
cx.beginPath();
cx.ellipse(0, -10, 10, 8, 0, 0, Math.PI * 2);
cx.fill();
cx.fillStyle = '#222';
cx.beginPath();
cx.arc(-5, -16, 2.4, 0, Math.PI * 2);
cx.fill();
cx.beginPath();
cx.arc(5, -16, 2.4, 0, Math.PI * 2);
cx.fill();
cx.fillStyle = 'rgba(255, 100, 130, 0.5)';
cx.beginPath();
cx.ellipse(-10, -10, 3.4, 2, 0, 0, Math.PI * 2);
cx.fill();
cx.beginPath();
cx.ellipse(10, -10, 3.4, 2, 0, 0, Math.PI * 2);
cx.fill();
cx.restore();
card.onclick = () => {
selectedRole = i;
renderRoleSelect();
};
roleGrid.appendChild(card);
});
}
const levelGrid = document.getElementById('levelGrid');
function renderLevelSelect() {
levelGrid.innerHTML = '';
for (let i = 0; i < levels.length; i++) {
const btn = document.createElement('button');
btn.className = 'level-btn';
btn.innerText = i + 1;
btn.onclick = () => {
hideAllOverlay();
document.getElementById('topBar').style.display = 'flex';
gameState = 'play';
stars = 0;
lives = 3;
loadLevel(i);
};
levelGrid.appendChild(btn);
}
}
document.getElementById('homeStartBtn').onclick = () => {
hideAllOverlay();
document.getElementById('topBar').style.display = 'flex';
gameState = 'play';
stars = 0;
lives = 3;
loadLevel(0);
};
document.getElementById('homeRoleBtn').onclick = () => {
showOverlay('roleScreen');
renderRoleSelect();
};
document.getElementById('homeLevelBtn').onclick = () => {
showOverlay('levelScreen');
renderLevelSelect();
};
document.getElementById('roleBackBtn').onclick = () => {
showOverlay('homeScreen');
};
document.getElementById('levelBackBtn').onclick = () => {
showOverlay('homeScreen');
};
document.getElementById('restartLevelBtn').onclick = () => {
stars = 0;
lives = 3;
loadLevel(currentLevel);
};
document.getElementById('backHomeBtn').onclick = () => {
gameState = 'home';
showOverlay('homeScreen');
document.getElementById('topBar').style.display = 'none';
};
document.getElementById('restartBtn').onclick = () => {
showOverlay('homeScreen');
stars = 0;
lives = 3;
currentLevel = 0;
gameState = 'home';
};
requestAnimationFrame(loop);
waiwaiLoop();
</script>
</body>
</html>Game Source: 萌宠闯关
Creator: SonicKnight51
Libraries: none
Complexity: complex (1420 lines, 35.2 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-sonicknight51-mwd0wfnl" to link back to the original. Then publish at arcadelab.ai/publish.