🎮ArcadeLab

横屏2D射击练习&关卡游戏

by SparkShark50
278 lines10.0 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-HK">
<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>
*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui, sans-serif}
html,body{width:100%;height:100%;overflow:hidden;background:#000}
#gameCanvas{width:100vw;height:100vh;background:#222;display:block;touch-action:none}
/* UI层 */
#ui{position:fixed;inset:0;color:#fff;font-size:20px}
/* 主菜单 */
.mainMenu{position:absolute;inset:0;background:rgba(20,20,20,0.9);display:flex;flex-direction:column;justify-content:flex-end;align-items:flex-end;padding:40px;gap:16px}
.menuBtn{width:180px;height:70px;font-size:22px;border:none;border-radius:10px;background:#348ede;color:white}
/* 模式选择介面 */
.modePanel{position:absolute;inset:0;background:rgba(0,0,0,0.92);display:none;flex-direction:row;gap:30px;align-items:center;justify-content:center}
.modeBox{width:220px;height:280px;background:#333;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:26px;color:#fff;border:2px #666 solid}
/* 角色皮肤介面 */
.skinPanel{position:absolute;inset:0;background:rgba(0,0,0,0.92);display:none;flex-direction:column;align-items:center;padding:20px}
.skinRow{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin:15px 0}
.skinItem{width:90px;height:90px;border-radius:8px;border:2px solid #777}
.skinAdvBtn{padding:12px 24px;font-size:18px}
/* 摇杆区域 */
.joyArea{position:absolute;bottom:20px;left:20px;width:160px;height:160px;background:rgba(255,255,255,0.15);border-radius:50%}
.joyStick{width:70px;height:70px;background:rgba(255,255,255,0.4);border-radius:50%;position:absolute;left:45px;top:45px}
.shootBtn{position:absolute;bottom:40px;right:30px;width:110px;height:110px;border-radius:50%;background:rgba(255,60,60,0.6);border:none;color:#fff;font-size:22px}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
    <!-- 主菜单 -->
    <div class="mainMenu" id="mainMenu">
        <button class="menuBtn" id="modeSelectBtn">选择模式</button>
        <button class="menuBtn" id="startBtn">开始游戏</button>
    </div>
    <!-- 模式选择:关卡 / 练习 -->
    <div class="modePanel" id="modePanel">
        <div class="modeBox" data-mode="stage">关卡模式</div>
        <div class="modeBox" data-mode="practice">练习模式</div>
    </div>
    <!-- 皮肤选择介面 -->
    <div class="skinPanel" id="skinPanel">
        <h2>角色皮肤</h2>
        <div class="skinRow">
            <div class="skinItem" style="background:white" data-color="#ffffff"></div>
            <div class="skinItem" style="background:black" data-color="#000000"></div>
            <div class="skinItem" style="background:#348ede" data-color="#348ede"></div>
            <div class="skinItem" style="background:#ff7700" data-color="#ff7700"></div>
        </div>
        <button class="skinAdvBtn">进阶皮肤 (50币)</button>
        <button onclick="closeSkinPanel()">返回</button>
    </div>
</div>

<script>
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
function resizeCanvas(){
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener("resize",resizeCanvas);

// ===== 游戏设定参数 =====
const PLAYER_MAX_HP = 100;
let player = {
    x:120,y:300,w:44,h:44,hp:PLAYER_MAX_HP,color:"#ffffff",
    bulletReserve:3,bulletReloadTimer:0,
    weapon:"pistol",shootCooldown:0
};
const WEAPON_DATA = {
    pistol:{dmg:5,delay:0.25,bulletCount:1},
    shotgun:{dmg:2,delay:0.5,bulletCount:10},
    sniper:{dmg:30,delay:1,bulletCount:1}
};
let gameState = "menu"; // menu / play
let gameMode = "practice";
let enemies = [];
let bullets = [];
let damageTexts = [];
let poisonCircle = {radius:800,shrinkSpeed:0.1};

// 摇杆变量
let joy = {active:false,originX:0,originY:0,dx:0,dy:0};
const joyArea = document.createElement("div");
joyArea.className="joyArea";
const joyStick = document.createElement("div");
joyStick.className="joyStick";
joyArea.appendChild(joyStick);
document.getElementById("ui").appendChild(joyArea);

const shootBtn = document.createElement("button");
shootBtn.className="shootBtn";
shootBtn.innerText="射击";
document.getElementById("ui").appendChild(shootBtn);

// 摇杆触摸
joyArea.addEventListener("touchstart",(e)=>{
    joy.active=true;
    const t = e.touches[0];
    joy.originX = t.clientX;
    joy.originY = t.clientY;
})
document.addEventListener("touchmove",(e)=>{
    if(!joy.active) return;
    const t = e.touches[0];
    const dx = t.clientX - joy.originX;
    const dy = t.clientY - joy.originY;
    const dist = Math.sqrt(dx*dx+dy*dy);
    const maxR = 80;
    if(dist>maxR){
        joy.dx = dx/dist*maxR;
        joy.dy = dy/dist*maxR;
    }else{
        joy.dx = dx;joy.dy = dy;
    }
    joyStick.style.left = (45 + joy.dx)+"px";
    joyStick.style.top = (45 + joy.dy)+"px";
})
document.addEventListener("touchend",()=>{
    joy.active=false;joy.dx=0;joy.dy=0;
    joyStick.style.left="45px";joyStick.style.top="45px";
})
shootBtn.addEventListener("touchstart",shoot);

// 菜单按钮
document.getElementById("modeSelectBtn").onclick = ()=>{
    document.getElementById("modePanel").style.display="flex";
}
document.querySelectorAll(".modeBox").forEach(box=>{
    box.onclick = ()=>{
        gameMode = box.dataset.mode;
        document.getElementById("modePanel").style.display="none";
        startGame();
    }
})
document.getElementById("startBtn").onclick = startGame;

function startGame(){
    gameState="play";
    player.hp = PLAYER_MAX_HP;
    player.x=120;player.y=300;
    player.bulletReserve=3;player.bulletReloadTimer=0;
    enemies=[];bullets=[];damageTexts=[];
    poisonCircle.radius = 800;
    if(gameMode==="practice"){
        //练习模式,敌人企定
        enemies = [
            {x:400,y:320,w:44,h:44,hp:80,movable:false,color:"#f44"}
        ]
    }else{
        enemies = [
            {x:400,y:320,w:44,h:44,hp:80,movable:true,color:"#f44"}
        ]
    }
}
function shoot(){
    if(gameState!="play") return;
    if(player.shootCooldown>0) return;
    const wp = WEAPON_DATA[player.weapon];
    player.shootCooldown = wp.delay;
    if(player.bulletReserve<=0) return;
    player.bulletReserve--;
    const count = wp.bulletCount;
    for(let i=0;i<count;i++){
        let spread = (Math.random()-0.5)*0.25;
        bullets.push({
            x:player.x+player.w/2,
            y:player.y+player.h/2,
            vx:Math.cos(spread)*10,
            vy:Math.sin(spread)*10,
            dmg:wp.dmg,isPlayer:true
        })
    }
}

function loop(timestamp){
    ctx.fillStyle="#222";
    ctx.fillRect(0,0,canvas.width,canvas.height);
    if(gameState==="play"){
        //玩家移动
        const spd = 4;
        player.x += joy.dx*0.06*spd;
        player.y += joy.dy*0.06*spd;

        //子弹冷却计时
        if(player.shootCooldown>0) player.shootCooldown -= 1/60;
        //子弹补给
        player.bulletReloadTimer +=1/60;
        if(player.bulletReloadTimer>1.2 && player.bulletReserve<3){
            player.bulletReserve++;
            player.bulletReloadTimer=0;
        }
        //毒圈收缩
        poisonCircle.radius -= poisonCircle.shrinkSpeed;
        const centerX = canvas.width/2;
        const centerY = canvas.height/2;
        const distToCenter = Math.sqrt((player.x-centerX)**2 + (player.y-centerY)**2);
        if(distToCenter>poisonCircle.radius){
            player.hp -= 10/60;
        }

        //更新子弹
        for(let i=bullets.length-1;i>=0;i--){
            const b = bullets[i];
            b.x += b.vx; b.y += b.vy;
            if(b.isPlayer){
                for(let ei=enemies.length-1;ei>=0;ei--){
                    const e = enemies[ei];
                    if(checkHit(b.x,b.y,8,8,e.x,e.y,e.w,e.h)){
                        e.hp -= b.dmg;
                        damageTexts.push({x:e.x,y:e.y,val:b.dmg,life:1.2});
                        bullets.splice(i,1);
                        if(e.hp<=0) enemies.splice(ei,1);
                        break;
                    }
                }
            }
        }
        //画敌人
        enemies.forEach(e=>{
            ctx.fillStyle = e.color;
            ctx.fillRect(e.x,e.y,e.w,e.h);
            //敌人双眼
            ctx.fillStyle="#000";
            ctx.beginPath();
            ctx.arc(e.x+12,e.y+14,5,0,Math.PI*2);
            ctx.arc(e.x+32,e.y+14,5,0,Math.PI*2);
            ctx.fill();
        })
        //画玩家
        ctx.fillStyle=player.color;
        ctx.fillRect(player.x,player.y,player.w,player.h);
        //玩家双眼
        ctx.fillStyle="#000";
        ctx.beginPath();
        ctx.arc(player.x+12,player.y+14,5,0,Math.PI*2);
        ctx.arc(player.x+32,player.y+14,5,0,Math.PI*2);
        ctx.fill();
        //玩家绿色血条
        const barW = player.w + 6;
        const barH = 6;
        ctx.fillStyle="#38c948";
        ctx.fillRect(player.x-3,player.y-12,barW*(player.hp/PLAYER_MAX_HP),barH);
        ctx.strokeStyle="#fff";ctx.strokeRect(player.x-3,player.y-12,barW,barH);
        //子弹3格UI(橙色/黑色)
        for(let bi=0;bi<3;bi++){
            ctx.fillStyle = bi < player.bulletReserve ? "#ff9922":"#222";
            ctx.fillRect(player.x+bi*14,player.y-22,12,6);
        }
        //飘伤害数字
        for(let di=damageTexts.length-1;di>=0;di--){
            const d = damageTexts[di];
            d.y -= 1.2; d.life -= 1/60;
            ctx.fillStyle="#fff";
            ctx.font="20px sans-serif";
            ctx.fillText(d.val, d.x,d.y);
            if(d.life <=0) damageTexts.splice(di,1);
        }
        //画毒圈
        ctx.strokeStyle="#992222";
        ctx.lineWidth=3;
        ctx.beginPath();
        ctx.arc(centerX,centerY,poisonCircle.radius,0,Math.PI*2);
        ctx.stroke();
    }
    requestAnimationFrame(loop);
}
function checkHit(x1,y1,w1,h1,x2,y2,w2,h2){
    return x1<x2+w2 && x1+w1>x2 && y1<y2+h2 && y1+h1>y2;
}
requestAnimationFrame(loop);
</script>
</body>
</html>

Game Source: 横屏2D射击练习&关卡游戏

Creator: SparkShark50

Libraries: none

Complexity: complex (278 lines, 10.0 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: 2d-sparkshark50" to link back to the original. Then publish at arcadelab.ai/publish.