🎮ArcadeLab

西游横版动作 - 火焰山篇

by MegaComet18
668 lines18.6 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>西游横版动作 - 火焰山篇</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:system-ui}
body{background:#000;color:#fff;text-align:center;overflow:hidden}
#gameCanvas{background:#222;display:block;margin:0 auto}
#selectBox{position:fixed;inset:0;background:rgba(0,0,0,0.92);display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:12px;z-index:200}
.charSel{padding:14px 22px;font-size:19px;border-radius:10px;border:none;background:#f80;color:#fff;font-weight:bold}
.hud{position:fixed;top:8px;left:8px;text-align:left;font-size:14px;z-index:120;line-height:1.6}
.hpBar{width:160px;height:12px;background:#400;border:1px solid #880;border-radius:6px;overflow:hidden;margin-top:4px}
.hpFill{height:100%;background:linear-gradient(90deg,#f44,#fa4);transition:width 0.15s}
.spBox{margin-top:6px}
.spDot{display:inline-block;width:14px;height:14px;border-radius:50%;background:#333;border:1px solid #fa0;margin-right:4px;vertical-align:middle}
.spDot.on{background:#fa0;box-shadow:0 0 8px #fa0}
.bossInfo{position:fixed;top:8px;left:50%;transform:translateX(-50%);text-align:center;font-size:14px;z-index:120;display:none}
.bossHpBar{width:260px;height:14px;background:#400;border:1px solid #f44;border-radius:8px;overflow:hidden;margin-top:4px}
.bossHpFill{height:100%;background:linear-gradient(90deg,#f22,#f88);transition:width 0.2s}
.leftPanel{position:fixed;bottom:18px;left:18px;display:grid;grid-template-columns:64px 64px;gap:10px;z-index:110}
.moveBtn{width:64px;height:64px;border-radius:50%;border:none;background:#25f;color:#fff;font-size:22px;font-weight:bold}
.rightPanel{position:fixed;bottom:18px;right:18px;display:grid;grid-template-columns:64px 64px 64px;grid-template-rows:repeat(2,64px);gap:10px;z-index:110}
.actBtn{border-radius:14px;border:none;color:#fff;font-size:16px;font-weight:bold}
.atk{background:#f70;grid-column:1}
.s1{background:#2c4}
.s2{background:#28f}
.s3{background:#c2f}
.ult{background:#f22;grid-column:span 3}
</style>
</head>
<body>

<div id="selectBox">
  <div style="width:100%;text-align:center;font-size:22px;margin-bottom:10px">选择角色</div>
  <button class="charSel" onclick="pick('wukong')">孙悟空</button>
  <button class="charSel" onclick="pick('bajie')">猪八戒</button>
  <button class="charSel" onclick="pick('shaseng')">沙僧</button>
  <button class="charSel" onclick="pick('tangseng')">唐僧</button>
</div>

<div class="hud" id="hud" style="display:none">
  <div>关卡:火焰山</div>
  <div id="charName">孙悟空</div>
  <div class="hpBar"><div class="hpFill" id="hpFill" style="width:100%"></div></div>
  <div class="spBox">技能点:
    <span class="spDot" id="sp0"></span>
    <span class="spDot" id="sp1"></span>
    <span class="spDot" id="sp2"></span>
    <span class="spDot" id="sp3"></span>
  </div>
</div>

<div class="bossInfo" id="bossInfo">
  <div>牛魔王</div>
  <div class="bossHpBar"><div class="bossHpFill" id="bossHpFill" style="width:100%"></div></div>
</div>

<canvas id="gameCanvas"></canvas>

<div class="leftPanel" id="leftPanel" style="display:none">
  <button class="moveBtn" id="btnLeft">←</button>
  <button class="moveBtn" id="btnJump">跳</button>
  <button class="moveBtn" id="btnRight">→</button>
</div>

<div class="rightPanel" id="rightPanel" style="display:none">
  <button class="actBtn atk" id="btnAtk">普攻</button>
  <button class="actBtn s1" id="s1">技1</button>
  <button class="actBtn s2" id="s2">技2</button>
  <button class="actBtn s3" id="s3">技3</button>
  <button class="actBtn ult" id="ult">大招</button>
</div>

<script>
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
canvas.width = 720;
canvas.height = 360;

const WORLD_WIDTH = 2400;
const GROUND_Y = 300;
const GRAVITY = 0.7;

const chars = {
  wukong:{
    name:"孙悟空",hp:100,atk:20,speed:4.2,color:#ffcc33,
    skills:["金箍棒横扫","筋斗云冲刺","火眼金睛"],
    ult:"大圣齐天"
  },
  bajie:{
    name:"猪八戒",hp:140,atk:17,speed:3.4,color:#ff9933,
    skills:["钉耙重击","九齿旋风","贪吃回血"],
    ult:"天蓬降世"
  },
  shaseng:{
    name:"沙僧",hp:120,atk:19,speed:3.7,color:#66ccff,
    skills:["降妖杖劈砍","流沙护盾","流沙陷阱"],
    ult:"黄沙万里"
  },
  tangseng:{
    name:"唐僧",hp:80,atk:15,speed:3.5,color:#ff66cc,
    skills:["紧箍咒","佛光普照","诵经禁锢"],
    ult:"如来法旨"
  }
};

let game = {
  state:"select",char:null,
  cameraX:0,
  player:null,
  monsters:[],
  effects:[],
  sp:0,
  boss:null,
  fightStarted:false,
  bossDefeated:false,
  keys:{left:false,right:false,jump:false}
};

class Player{
  constructor(cfg){
    this.x=120;this.y=200;this.w=42;this.h=58;
    this.vx=0;this.vy=0;
    this.hp=cfg.hp;this.maxHp=cfg.hp;
    this.atk=cfg.atk;this.speed=cfg.speed;
    this.color=cfg.color;
    this.facing=1;
    this.onGround=false;
    this.attackTimer=0;
    this.invincible=0;
    this.animT=0;
  }
  update(){
    this.animT++;
    if(game.keys.left){this.vx=-this.speed;this.facing=-1}
    else if(game.keys.right){this.vx=this.speed;this.facing=1}
    else this.vx=0;

    if(game.keys.jump&&this.onGround){this.vy=-13;game.keys.jump=false}

    this.vy+=GRAVITY;
    this.x+=this.vx;
    this.y+=this.vy;

    if(this.x<0)this.x=0;
    if(this.x>WORLD_WIDTH-this.w)this.x=WORLD_WIDTH-this.w;
    if(this.y>=GROUND_Y-this.h){this.y=GROUND_Y-this.h;this.vy=0;this.onGround=true}
    else this.onGround=false;

    if(this.attackTimer>0)this.attackTimer--;
    if(this.invincible>0)this.invincible--;

    // 相机跟随
    const targetCam = Math.max(0, Math.min(WORLD_WIDTH-canvas.width, this.x-canvas.width*0.4));
    game.cameraX += (targetCam-game.cameraX)*0.12;

    // 走到地图中段开始战斗
    if(this.x>420&&!game.fightStarted){
      startFight();
    }
  }
  draw(){
    const sx=this.x-game.cameraX;
    const sy=this.y;
    ctx.save();
    ctx.translate(sx+this.w/2, sy);
    ctx.scale(this.facing,1);
    ctx.translate(-this.w/2,0);

    // 身体
    ctx.fillStyle=this.color;
    ctx.fillRect(6,18,30,34);

    // 头
    ctx.beginPath();
    ctx.arc(21,12,12,0,Math.PI*2);
    ctx.fillStyle=this.color;
    ctx.fill();
    ctx.strokeStyle="#000";
    ctx.lineWidth=1.5;
    ctx.stroke();

    // 眼睛
    ctx.fillStyle="#000";
    ctx.fillRect(24,9,4,4);

    // 脚
    ctx.fillStyle="#444";
    const leg=this.onGround?0:Math.sin(this.animT*0.4)*4;
    ctx.fillRect(8,52+leg,10,6);
    ctx.fillRect(24,52-leg,10,6);

    // 武器
    ctx.strokeStyle="#888";
    ctx.lineWidth=4;
    ctx.beginPath();
    ctx.moveTo(36,28);
    ctx.lineTo(56,14);
    ctx.stroke();

    ctx.restore();

    // 技能范围预览
    if(this.attackTimer>0){
      ctx.save();
      ctx.globalAlpha=0.25;
      ctx.fillStyle="#ff0";
      ctx.fillRect(sx+this.w/2-40, sy+10, 80, 40);
      ctx.restore();
    }
  }
}

class Monster{
  constructor(x){
    this.x=x;this.y=GROUND_Y-44;this.w=38;this.h=44;
    this.hp=35;this.vx=1.2;this.dir=1;this.hitT=0;
  }
  update(){
    this.x+=this.vx*this.dir;
    if(this.x<120||this.x>WORLD_WIDTH-80)this.dir*=-1;
    if(this.hitT>0)this.hitT--;

    // 碰撞玩家
    if(rectHit(this,game.player)&&game.player.invincible<=0){
      game.player.hp-=0.4;
    }
  }
  draw(){
    const sx=this.x-game.cameraX;
    ctx.save();
    ctx.translate(sx+this.w/2,0);
    ctx.scale(this.dir,1);
    ctx.translate(-this.w/2,0);

    ctx.fillStyle=this.hitT>0?"#fff":"#f44";
    ctx.fillRect(4,10,30,26);
    ctx.beginPath();
    ctx.arc(19,8,9,0,Math.PI*2);
    ctx.fill();
    ctx.fillStyle="#000";
    ctx.fillRect(22,6,3,3);
    ctx.fillStyle="#444";
    ctx.fillRect(6,36,8,8);
    ctx.fillRect(24,36,8,8);
    ctx.restore();

    // 血条
    ctx.fillStyle="#500";
    ctx.fillRect(sx,this.y-8,this.w,4);
    ctx.fillStyle="#f44";
    ctx.fillRect(sx,this.y-8,this.w*(this.hp/35),4);
  }
}

class Boss{
  constructor(){
    this.x=1800;this.y=GROUND_Y-80;this.w=74;this.h=80;
    this.hp=420;this.maxHp=420;this.timer=0;this.hitT=0;
  }
  update(){
    this.timer++;
    if(this.hitT>0)this.hitT--;

    // 向玩家移动
    if(this.x>game.player.x+120)this.x-=1.2;
    if(this.x<game.player.x-120)this.x+=1.2;

    // 攻击
    if(this.timer>100){
      this.timer=0;
      if(Math.abs(this.x-game.player.x)<110&&game.player.invincible<=0){
        game.player.hp-=14;
        game.player.invincible=40;
      }
    }
  }
  draw(){
    const sx=this.x-game.cameraX;
    ctx.save();
    ctx.translate(sx+this.w/2,0);
    ctx.scale(this.x<game.player.x?1:-1);
    ctx.translate(-this.w/2,0);

    ctx.fillStyle=this.hitT>0?"#fff":"#c42";
    ctx.fillRect(10,24,54,40);
    ctx.beginPath();
    ctx.arc(37,18,16,0,Math.PI*2);
    ctx.fill();
    ctx.fillStyle="#000";
    ctx.fillRect(42,14,5,5);
    ctx.fillStyle="#fa0";
    ctx.beginPath();
    ctx.moveTo(20,6);
    ctx.lineTo(14,-6);
    ctx.lineTo(28,4);
    ctx.fill();
    ctx.beginPath();
    ctx.moveTo(54,6);
    ctx.lineTo(60,-6);
    ctx.lineTo(48,4);
    ctx.fill();
    ctx.restore();
  }
}

class Effect{
  constructor(x,y,type){
    this.x=x;this.y=y;this.type=type;this.life=20;this.maxLife=20;
  }
  update(){this.life--}
  draw(){
    const sx=this.x-game.cameraX;
    const t=1-this.life/this.maxLife;
    ctx.save();
    ctx.globalAlpha=this.life/this.maxLife;

    if(this.type=="slash"){
      ctx.strokeStyle="#ff0";
      ctx.lineWidth=4;
      ctx.beginPath();
      ctx.arc(sx,this.y,20+t*30,-0.6,0.6);
      ctx.stroke();
    }
    if(this.type=="dash"){
      ctx.fillStyle="#44f";
      ctx.fillRect(sx-20,this.y,40,8);
    }
    if(this.type=="fireeye"){
      ctx.fillStyle="#f80";
      ctx.beginPath();
      ctx.arc(sx,this.y,10+t*14,0,Math.PI*2);
      ctx.fill();
    }
    if(this.type=="spin"){
      ctx.strokeStyle="#f4f";
      ctx.lineWidth=5;
      ctx.beginPath();
      ctx.arc(sx,this.y,20+t*24,0,Math.PI*2);
      ctx.stroke();
    }
    if(this.type=="shield"){
      ctx.strokeStyle="#4ff";
      ctx.lineWidth=4;
      ctx.beginPath();
      ctx.arc(sx,this.y,28+t*6,0,Math.PI*2);
      ctx.stroke();
    }
    if(this.type=="trap"){
      ctx.fillStyle="#fc3";
      ctx.fillRect(sx-24,this.y+20,48,10);
    }
    if(this.type=="chant"){
      ctx.fillStyle="#f0f";
      ctx.beginPath();
      ctx.arc(sx,this.y,12+t*10,0,Math.PI*2);
      ctx.fill();
    }
    if(this.type=="buddha"){
      ctx.fillStyle="#ff0";
      ctx.globalAlpha=0.5;
      ctx.fillRect(sx-60,0,120,180);
      ctx.globalAlpha=1;
      ctx.fillStyle="#fff";
      ctx.font="bold 28px sans-serif";
      ctx.fillText("佛",sx-10,100);
    }
    if(this.type=="ult"){
      ctx.fillStyle="#f80";
      ctx.globalAlpha=0.4;
      ctx.fillRect(sx-80,0,160,200);
      ctx.globalAlpha=1;
      ctx.strokeStyle="#ff0";
      ctx.lineWidth=6;
      ctx.beginPath();
      ctx.arc(sx,this.y,40+t*40,0,Math.PI*2);
      ctx.stroke();
    }

    ctx.restore();
  }
}

function rectHit(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 startFight(){
  game.fightStarted=true;
  game.monsters=[new Monster(620),new Monster(860),new Monster(1100),new Monster(1340)];
}

function spawnBoss(){
  if(!game.boss){
    game.boss=new Boss();
    document.getElementById("bossInfo").style.display="block";
  }
}

function playerAtk(){
  if(!game.player||game.player.attackTimer>0)return;
  game.player.attackTimer=18;
  const p=game.player;
  const box={x:p.facing>0?p.x+p.w-8:p.x-40,y:p.y+10,w:48,h:40};

  game.effects.push(new Effect(box.x+box.w/2, box.y+box.h/2, "slash"));

  for(const m of game.monsters){
    if(rectHit(box,m)){
      m.hp-=p.atk;
      m.hitT=10;
    }
  }
  if(game.boss&&rectHit(box,game.boss)){
    game.boss.hp-=p.atk;
    game.boss.hitT=10;
  }
}

function useSkill(n){
  if(!game.player)return;
  const p=game.player;
  const box={x:p.facing>0?p.x+p.w-8:p.x-40,y:p.y+10,w:48,h:40};

  if(game.char.name=="孙悟空"){
    if(n==1){
      game.effects.push(new Effect(box.x+box.w/2, box.y+box.h/2, "slash"));
      for(const m of game.monsters)if(rectHit(box,m)){m.hp-=p.atk*1.6;m.hitT=10}
      if(game.boss&&rectHit(box,game.boss))game.boss.hp-=p.atk*1.6;
    }
    if(n==2){
      p.x+=p.facing*90;
      p.vy=-4;
      game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "dash"));
      for(const m of game.monsters)if(rectHit(box,m)){m.hp-=p.atk*1.3;m.hitT=10}
      if(game.boss&&rectHit(box,game.boss))game.boss.hp-=p.atk*1.3;
    }
    if(n==3){
      game.effects.push(new Effect(p.x+p.w/2, p.y+10, "fireeye"));
      for(const m of game.monsters){
        const dx=Math.abs(m.x+p.w/2-p.x);
        if(dx<260)m.hp-=10;
      }
      if(game.boss){
        const dx=Math.abs(game.boss.x+p.w/2-p.x);
        if(dx<320)game.boss.hp-=12;
      }
    }
  }

  if(game.char.name=="猪八戒"){
    if(n==1){
      game.effects.push(new Effect(box.x+box.w/2, box.y+box.h/2, "slash"));
      for(const m of game.monsters)if(rectHit(box,m)){m.hp-=p.atk*1.8;m.hitT=10}
      if(game.boss&&rectHit(box,game.boss))game.boss.hp-=p.atk*1.8;
    }
    if(n==2){
      game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "spin"));
      for(const m of game.monsters){
        const dx=Math.abs(m.x+p.w/2-p.x);
        if(dx<110)m.hp-=p.atk*1.2;
      }
      if(game.boss&&Math.abs(game.boss.x+p.w/2-p.x)<130)game.boss.hp-=p.atk*1.2;
    }
    if(n==3){
      p.hp+=20;
      if(p.hp>p.maxHp)p.hp=p.maxHp;
      game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "shield"));
    }
  }

  if(game.char.name=="沙僧"){
    if(n==1){
      game.effects.push(new Effect(box.x+box.w/2, box.y+box.h/2, "slash"));
      for(const m of game.monsters)if(rectHit(box,m)){m.hp-=p.atk*1.5;m.hitT=10}
      if(game.boss&&rectHit(box,game.boss))game.boss.hp-=p.atk*1.5;
    }
    if(n==2){
      game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "shield"));
      p.hp+=12;
      if(p.hp>p.maxHp)p.hp=p.maxHp;
    }
    if(n==3){
      game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "trap"));
      for(const m of game.monsters){
        const dx=Math.abs(m.x+p.w/2-p.x);
        if(dx<80)m.hp-=8;
      }
    }
  }

  if(game.char.name=="唐僧"){
    if(n==1){
      game.effects.push(new Effect(box.x+box.w/2, box.y+box.h/2, "chant"));
      for(const m of game.monsters)if(rectHit(box,m)){m.hp-=p.atk*1.4;m.hitT=10}
      if(game.boss&&rectHit(box,game.boss))game.boss.hp-=p.atk*1.4;
    }
    if(n==2){
      p.hp+=26;
      if(p.hp>p.maxHp)p.hp=p.maxHp;
      game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "shield"));
    }
    if(n==3){
      game.effects.push(new Effect(box.x+box.w/2, box.y+p.h/2, "chant"));
      for(const m of game.monsters)if(rectHit(box,m)){m.hp-=p.atk*1.1;m.hitT=10}
      if(game.boss&&rectHit(box,game.boss))game.boss.hp-=p.atk*1.1;
    }
  }

  game.sp++;
  if(game.sp>4)game.sp=4;
}

function useUlt(){
  if(game.sp<4||!game.player)return;
  game.sp=0;
  const p=game.player;
  game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "ult"));

  for(const m of game.monsters){
    const dx=Math.abs(m.x+p.w/2-p.x);
    if(dx<260)m.hp-=60;
  }
  if(game.boss){
    const dx=Math.abs(game.boss.x+p.w/2-p.x);
    if(dx<320)game.boss.hp-=90;
  }

  if(game.char.name=="唐僧"){
    game.effects.push(new Effect(p.x+p.w/2, p.y+p.h/2, "buddha"));
    if(game.boss){
      const dx=Math.abs(game.boss.x+p.w/2-p.x);
      if(dx<200)game.boss.hp-=60;
    }
  }
}

function pick(id){
  game.char=chars[id];
  game.player=new Player(game.char);
  document.getElementById("selectBox").style.display="none";
  document.getElementById("hud").style.display="block";
  document.getElementById("leftPanel").style.display="grid";
  document.getElementById("rightPanel").style.display="grid";
  document.getElementById("charName").innerText=game.char.name;
  game.state="playing";
  loop();
}

function drawWorld(){
  // 天空
  const grad=ctx.createLinearGradient(0,0,0,canvas.height);
  grad.addColorStop(0,"#f73");
  grad.addColorStop(0.5,"#fa3");
  grad.addColorStop(1,"#fda");
  ctx.fillStyle=grad;
  ctx.fillRect(0,0,canvas.width,canvas.height);

  // 远山
  ctx.fillStyle="#b53";
  for(let i=0;i<8;i++){
    const mx=i*340-game.cameraX*0.3;
    ctx.beginPath();
    ctx.moveTo(mx,260);
    ctx.lineTo(mx+120,140);
    ctx.lineTo(mx+240,260);
    ctx.closePath();
    ctx.fill();
  }

  // 地面
  ctx.fillStyle="#742";
  ctx.fillRect(0,GROUND_Y,canvas.width,canvas.height-GROUND_Y);
  ctx.fillStyle="#531";
  for(let i=0;i<40;i++){
    const gx=i*70-game.cameraX;
    ctx.fillRect(gx,GROUND_Y,40,6);
  }
}

function updateHUD(){
  if(!game.player)return;
  document.getElementById("hpFill").style.width=(game.player.hp/game.player.maxHp*100)+"%";
  for(let i=0;i<4;i++){
    document.getElementById("sp"+i).classList.toggle("on",i<game.sp);
  }
  if(game.boss){
    document.getElementById("bossHpFill").style.width=Math.max(0,game.boss.hp/game.boss.maxHp*100)+"%";
  }
  document.getElementById("ult").disabled=game.sp<4;
}

function loop(){
  if(game.state!="playing")return;

  ctx.clearRect(0,0,canvas.width,canvas.height);
  drawWorld();

  if(game.player)game.player.update();

  // 进入战斗后小怪才会活动
  if(game.fightStarted){
    for(const m of game.monsters)m.update();
    game.monsters=game.monsters.filter(m=>m.hp>0);

    // 清完小怪出BOSS
    if(game.monsters.length<=0&&!game.boss)spawnBoss();
  }

  if(game.boss){
    game.boss.update();
    if(game.boss.hp<=0&&!game.bossDefeated){
      game.bossDefeated=true;
      document.getElementById("bossInfo").style.display="none";
      alert("牛魔王被击败!火焰山通过!");
      game.state="select";
    }
  }

  for(const e of game.effects)e.update();
  game.effects=game.effects.filter(e=>e.life>0);

  // 绘制
  if(game.player)game.player.draw();
  for(const m of game.monsters)m.draw();
  if(game.boss)game.boss.draw();
  for(const e of game.effects)e.draw();

  updateHUD();

  if(game.player&&game.player.hp<=0){
    alert("战败!重新选择角色再来一次。");
    game.state="select";
    location.reload();
    return;
  }

  requestAnimationFrame(loop);
}

// 触屏控制
function bind(id,key,isAtk,isSkill,isUlt){
  const b=document.getElementById(id);
  b.addEventListener("touchstart",e=>{
    e.preventDefault();
    if(isAtk)playerAtk();
    else if(isSkill)useSkill(Number(id.slice(1)));
    else if(isUlt)useUlt();
    else game.keys[key]=true;
  });
  b.addEventListener("touchend",e=>{
    e.preventDefault();
    if(!isAtk&&!isSkill&&!isUlt)game.keys[key]=false;
  });
}
bind("btnLeft","left");
bind("btnRight","right");
bind("btnJump","jump");
bind("btnAtk",null,true);
bind("s1",null,false,true);
bind("s2",null,false,true);
bind("s3",null,false,true);
bind("ult",null,false,false,true);
</script>
</body>
</html>

Game Source: 西游横版动作 - 火焰山篇

Creator: MegaComet18

Libraries: none

Complexity: complex (668 lines, 18.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-megacomet18-mw7hzk5t" to link back to the original. Then publish at arcadelab.ai/publish.