🎮ArcadeLab

万关俯首·歪歪叽

by SonicKnight51
1044 lines34.3 KB
▶ Play
<!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;
  }

  .waiwai-enter {
    animation: waiwaiPop 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
  @keyframes waiwaiPop {
    0% { transform: scale(0.25) translateY(90px); opacity:0; }
    60% { transform: scale(1.12) translateY(-14px); opacity:1; }
    100% { transform: scale(1) translateY(0); opacity:1; }
  }

  #waiwaiRewardScreen {
    animation: screenPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
  @keyframes screenPop {
    0% { opacity:0; transform:scale(0.85); }
    100% { opacity:1; transform:scale(1); }
  }

  .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;
  }
</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" id="waiwaiStage">
      <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 class="overlay" id="winScreen" style="display:none;">
    <h1>🏆全部关卡通关!</h1>
    <p>万关俯首,歪歪叽为你喝彩!</p>
    <button class="btn" id="restartBtn">再来一局</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 })) {
    if(currentLevel < levels.length -1){
      showWaiwaiReward(currentLevel);
    }else{
      gameState = 'win';
      showOverlay('winScreen');
      document.getElementById('topBar').style.display = 'none';
    }
  }

  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,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;
let waiwaiLoopStarted = false;

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 drawWaiwaiBase(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.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.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();
}

//动作1 挥手比心 第1关
function drawWaiwaiWave(){
  const w = waiwaiCanvas.width / window.devicePixelRatio;
  const h = waiwaiCanvas.height / window.devicePixelRatio;

  const swing = Math.sin(waiwaiAnim * 0.18) * 22;
  const breathe = Math.sin(waiwaiAnim * 0.08) * 4;
  const bounce = Math.sin(waiwaiAnim * 0.1) * 3;

  waiwaiCtx.clearRect(0, 0, w, h);
  drawWaiwaiBase(1 + breathe * 0.006);

  waiwaiCtx.save();
  waiwaiCtx.translate(w / 2 + 42, h / 2 + 12 + bounce);
  waiwaiCtx.rotate((swing * Math.PI) / 180);

  waiwaiCtx.fillStyle = '#ffffff';
  waiwaiCtx.beginPath();
  waiwaiCtx.ellipse(0, -32, 11, 30, 0, 0, Math.PI * 2);
  waiwaiCtx.fill();

  waiwaiCtx.fillStyle = '#ff66a5';
  waiwaiCtx.beginPath();
  waiwaiCtx.moveTo(0, -54);
  waiwaiCtx.bezierCurveTo(-16, -70, -26, -54, 0, -32);
  waiwaiCtx.bezierCurveTo(26, -54, 16, -70, 0, -32);
  waiwaiCtx.closePath();
  waiwaiCtx.fill();

  waiwaiCtx.restore();

  for(let i=0;i<6;i++){
    const t = waiwaiAnim*0.12 + i *1.3;
    const sx = Math.cos(t)*36 +24;
    const sy = Math.sin(t*1.4)*22 -44 -i*11;
    const alpha = 0.75 - i*0.1;
    waiwaiCtx.fillStyle='rgba(255,210,77,'+alpha+')';
    waiwaiCtx.beginPath(); waiwaiCtx.arc(w/2+sx, h/2+sy,7,0,Math.PI*2); waiwaiCtx.fill();
  }
}

//动作2 托腮害羞 第2关
function drawWaiwaiMiss(){
  const w = waiwaiCanvas.width / window.devicePixelRatio;
  const h = waiwaiCanvas.height / window.devicePixelRatio;
  const bob = Math.sin(waiwaiAnim*0.1)*4;
  const breathe = Math.sin(waiwaiAnim*0.08)*3;
  waiwaiCtx.clearRect(0,0,w,h);
  drawWaiwaiBase(1 + breathe*0.005);

  waiwaiCtx.save();
  waiwaiCtx.translate(w/2, h/2 -40 + bob);
  waiwaiCtx.fillStyle='#ffe0b3';
  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();

  for(let i=0;i<4;i++){
    const t = waiwaiAnim*0.09 +i;
    const bx = Math.cos(t)*25 -30;
    const by = Math.sin(t)*15 -60 -i*12;
    waiwaiCtx.fillStyle='rgba(180,200,255,'+(0.6-i*0.13)+')';
    waiwaiCtx.beginPath(); waiwaiCtx.arc(bx,by,7,0,Math.PI*2); waiwaiCtx.fill();
  }
  waiwaiCtx.restore();
}

//动作3 飞吻 第3关
function drawWaiwaiKiss(){
  const w = waiwaiCanvas.width / window.devicePixelRatio;
  const h = waiwaiCanvas.height / window.devicePixelRatio;
  const float = Math.sin(waiwaiAnim*0.12)*5;
  const breathe = Math.sin(waiwaiAnim*0.08)*3;
  waiwaiCtx.clearRect(0,0,w,h);
  drawWaiwaiBase(1 + breathe*0.005);

  waiwaiCtx.save();
  waiwaiCtx.translate(w/2, h/2 -40 + float);
  waiwaiCtx.fillStyle='#ffe0b3';
  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<4;i++){
    const t = waiwaiAnim*0.1 + i*1.8;
    const ax = Math.cos(t)*35 + i*18;
    const ay = Math.sin(t*1.3)*16 -22 -i*14;
    waiwaiCtx.fillStyle='rgba(255,77,141,'+(0.8-i*0.15)+')';
    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);
}

const rewardCfg = [
  {title:"🎉第1关通关!",action:"wave",name:"🎁 奖励:获得歪歪叽的祝福",tip:"祝福送达,继续加油!"},
  {title:"🎉第2关通关!",action:"miss",name:"🎁 奖励:获得歪歪叽的想念",tip:"歪歪叽在想你~"},
  {title:"🎉第3关通关!",action:"kiss",name:"🎁 奖励:获得歪歪叽香吻一枚",tip:"啵啵~全部挑战完成!"}
];

function showWaiwaiReward(levelIndex){
  gameState='reward';
  document.getElementById('topBar').style.display='none';
  const cfg = rewardCfg[levelIndex];
  document.getElementById('rewardTitle').innerText = cfg.title;
  document.getElementById('rewardName').innerText = cfg.name;
  document.getElementById('rewardTip').innerText = cfg.tip;
  document.getElementById('starReward').innerText = String(stars);

  waiwaiAction = cfg.action;
  waiwaiAnim = 0;
  resizeWaiwaiCanvas();
  showOverlay('waiwaiRewardScreen');

  if (!waiwaiLoopStarted) {
    waiwaiLoopStarted = true;
    waiwaiLoop();
  }
  const stage = document.getElementById('waiwaiStage');
  stage.classList.remove('waiwai-enter');
  void stage.offsetWidth;
  stage.classList.add('waiwai-enter');
}

document.getElementById('rewardNextBtn').onclick = ()=>{
  currentLevel++;
  if(currentLevel < levels.length){
    stars = 0; lives =3;
    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,-22);cx.lineTo(-14,-34);cx.lineTo(-4,-24);cx.closePath();cx.fill();
      cx.beginPath(); cx.moveTo(10,-22);cx.lineTo(14,-34);cx.lineTo(4,-24);cx.closePath();cx.fill();
    }else if(r.ear==='rabbit'){
      cx.beginPath(); cx.ellipse(-8,-28,4,10,-0.2,0,Math.PI*2);cx.fill();
      cx.beginPath(); cx.ellipse(8,-28,4,10,0.2,0,Math.PI*2);cx.fill();
    }else if(r.ear==='dog'){
      cx.beginPath(); cx.ellipse(-14,-16,6,10,-0.4,0,Math.PI*2);cx.fill();
      cx.beginPath(); cx.ellipse(14,-16,6,10,0.4,0,Math.PI*2);cx.fill();
    }else if(r.ear==='bird'){
      cx.beginPath(); cx.moveTo(-14,-14);cx.lineTo(-22,-6);cx.lineTo(-10,-8);cx.closePath();cx.fill();
      cx.beginPath(); cx.moveTo(14,-14);cx.lineTo(22,-6);cx.lineTo(10,-8);cx.closePath();cx.fill();
    }else if(r.ear==='panda'){
      cx.fillStyle='#222';
      cx.beginPath(); cx.arc(-12,-20,5,0,Math.PI*2);cx.fill();
      cx.beginPath(); cx.arc(12,-20,5,0,Math.PI*2);cx.fill();
      cx.fillStyle=r.color;
    }
    cx.fillStyle='#fff';
    cx.beginPath(); cx.ellipse(0,-8,8,6,0,0,Math.PI*2);cx.fill();
    cx.fillStyle='#222';
    cx.beginPath(); cx.arc(-4,-14,2,0,Math.PI*2);cx.fill();
    cx.beginPath(); cx.arc(4,-14,2,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 = String(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 (1044 lines, 34.3 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-mwd18g1q" to link back to the original. Then publish at arcadelab.ai/publish.