西游横版动作 - 火焰山篇
by MegaComet18668 lines18.6 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;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.