🎮ArcadeLab

僵尸生存-最终完整版

by SuperPhoenix65
752 lines29.2 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<title>僵尸生存-最终完整版</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#111;display:flex;justify-content:center;align-items:center;min-height:100vh;font-family:Arial}
#gameBox{position:relative;border:3px solid #fff;width:1200px;height:700px;overflow:hidden}
#gameCanvas{display:block;background:#222}

/*左上角主页方框按钮*/
#homeBox{
    position:absolute;top:8px;left:90px;
    width:90px;height:42px;border:2px solid #ffd700;
    background:#252525;color:#fff;cursor:pointer;
    display:flex;align-items:center;justify-content:center;z-index:99;font-size:14px;
}

#langBtn{position:absolute;top:8px;left:16px;width:72px;height:42px;background:#336;border:2px solid #fff;
color:#fff;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:99}
#langBtn>span:first-child{font-size:16px}
#langBtn>span:last-child{font-size:12px;opacity:0.8}

#ui{position:absolute;top:55px;left:16px;color:#fff;font:bold 20px Arial;text-shadow:2px 2px #000;line-height:1.4}

#joyWrap{position:absolute;width:140px;height:140px;bottom:30px;left:30px;background:rgba(255,255,255,0.15);border-radius:50%;touch-action:none}
#joyHandle{position:absolute;width:70px;height:70px;background:rgba(255,255,255,0.6);border-radius:50%;left:50%;top:50%;transform:translate(-50%,-50%)}
#mobileBtnGroup{position:absolute;left:190px;bottom:30px;display:flex;gap:8px;flex-wrap:wrap;width:180px}
.mobileBtn{width:52px;height:52px;background:#246;border:2px solid #fff;color:#fff;font-size:14px;cursor:pointer;border-radius:6px;display:flex;align-items:center;justify-content:center}
#pauseMobile{width:52px;height:52px;background:#622;border:2px solid #ff5;display:flex;align-items:center;justify-content:center;color:#fff;border-radius:6px;cursor:pointer}

#autoBtn{position:absolute;bottom:30px;right:30px;width:70px;height:70px;background:rgba(255,60,60,0.7);color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:bold;cursor:pointer}

#hotbar{position:absolute;bottom:180px;left:50%;transform:translateX(-50%);display:flex;gap:8px}
.slot{width:60px;height:60px;background:rgba(0,0,0,0.6);border:2px solid #888;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;position:relative;cursor:pointer}
.slot.active{border-color:#ff0}
.slot.weapon{background:rgba(20,30,60,0.5)}
.slot.food{background:rgba(60,30,20,0.5)}

#bookPanel{position:absolute;top:10px;right:10px;width:290px;background:rgba(0,0,0,0.9);border:2px solid #ffd700;color:#fff;padding:12px;display:none}
.book-title{display:flex;justify-content:space-between;align-items:center;font-size:22px;color:#ffd700;margin-bottom:10px;border-bottom:1px solid #ffd700;padding-bottom:6px}
.closeBook{width:26px;height:26px;background:#f44;color:#fff;text-align:center;line-height:26px;cursor:pointer;font-weight:bold}
.mon-item{margin:8px 0;padding:6px;border-bottom:1px solid #444}
.mon-locked{color:#666}
.mon-unlock{color:#fff}
.mon-name{font-weight:bold;font-size:17px}
.mon-desc{font-size:13px;color:#ccc;line-height:1.4}

#backpack{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:540px;height:540px;background:rgba(0,0,0,0.95);border:2px solid #fff;display:grid;grid-template-columns:repeat(9,1fr);gap:2px;padding:10px;display:none}
.bp-slot{width:56px;height:56px;background:rgba(40,40,40);border:1px solid #666;display:flex;align-items:center;justify-content:center;color:#fff;cursor:pointer}
.bp-slot.selected{border:2px solid #ff0}

#statBars{position:absolute;bottom:260px;left:50%;transform:translateX(-50%);width:300px;height:20px;display:flex;flex-direction:column;gap:4px}
.bar{width:100%;height:16px;background:#444;border:1px solid #fff}
.fill{height:100%}
.hp{background:#e33}
.stam{background:#3c3}
.hunger{background:#fa3}

/*ESC暂停正方体菜单样式*/
#pauseMenu{
    position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
    width:320px;height:360px;background:#222;border:3px solid #ffd700;
    display:none;z-index:101;padding:20px;display:flex;flex-direction:column;gap:22px;align-items:center;justify-content:center;
}
.pauseBtn{
    width:220px;height:52px;background:#347;color:#fff;border:2px solid #fff;
    font-size:17px;cursor:pointer;border-radius:4px;
}
#bugTipPop{
    position:absolute;left:50%;top:35%;transform:translate(-50%,-50%);
    background:#111;border:2px solid #ff6;color:#ff6;padding:12px;display:none;z-index:102;max-width:380px;line-height:1.6;
}
</style>
</head>
<body>
<div id="gameBox">
    <div id="langBtn"><span>语言</span><span>EN</span></div>
    <!--左上角方框返回主页按钮-->
    <div id="homeBox">返回主页</div>

    <div id="ui">分数:0<br>血量:100<br>体力:100<br>饥饿:100<br>武器:手枪</div>
    <div id="statBars"><div class="bar"><div class="fill hp" id="hpFill"></div></div><div class="bar"><div class="fill stam" id="stamFill"></div></div><div class="bar"><div class="fill hunger" id="hungerFill"></div></div></div>
    <div id="hotbar"></div>
    <div id="autoBtn">AUTO</div>
    <div id="bookPanel">
        <div class="book-title">📖怪物图鉴 <span class="closeBook" id="closeBookBtn">×</span></div>
        <div id="bookContent"></div>
    </div>
    <div id="backpack"></div>
    <canvas id="gameCanvas" width="1200" height="700"></canvas>
    <div id="joyWrap"><div id="joyHandle"></div></div>
    <div id="mobileBtnGroup">
        <div class="mobileBtn" id="btnBag">背包</div>
        <div class="mobileBtn" id="btnBook">图鉴</div>
        <div class="mobileBtn" id="btnSprint">冲刺</div>
        <div class="mobileBtn" id="btnPick">拾取</div>
        <div id="pauseMobile">暂停</div>
    </div>
    <!--ESC暂停正方体菜单(!公告移入此处)-->
    <div id="pauseMenu">
        <button class="pauseBtn" id="resumeBtn">继续游戏</button>
        <button class="pauseBtn" id="homePauseBtn">返回主页面</button>
        <button class="pauseBtn" id="bugInfoBtn">!</button>
    </div>
    <!--版本公告弹窗-->
    <div id="bugTipPop">BUG已经全部修复,后续会新增更多武器,游戏画风逐步优化为写实风格,敬请期待!</div>
</div>

<script>
const cvs = document.getElementById("gameCanvas");
const ctx = cvs.getContext("2d");
const ui = document.getElementById("ui");
const hotbar = document.getElementById("hotbar");
const backpack = document.getElementById("backpack");
const autoBtn = document.getElementById("autoBtn");
const joyWrap = document.getElementById("joyWrap");
const joyH = document.getElementById("joyHandle");
const bookPanel = document.getElementById("bookPanel");
const bookContent = document.getElementById("bookContent");
const closeBookBtn = document.getElementById("closeBookBtn");
const hpFill = document.getElementById("hpFill");
const stamFill = document.getElementById("stamFill");
const hungerFill = document.getElementById("hungerFill");
const langBtn = document.getElementById("langBtn");
const homeBox = document.getElementById("homeBox");
const btnBag=document.getElementById("btnBag");
const btnBook=document.getElementById("btnBook");
const btnSprint=document.getElementById("btnSprint");
const btnPick=document.getElementById("btnPick");
const pauseMobile=document.getElementById("pauseMobile");
//暂停菜单DOM
const pauseMenu=document.getElementById("pauseMenu");
const resumeBtn=document.getElementById("resumeBtn");
const homePauseBtn=document.getElementById("homePauseBtn");
const bugInfoBtn=document.getElementById("bugInfoBtn");
const bugTipPop=document.getElementById("bugTipPop");

//暂停菜单逻辑
let isPause=false;
function openPause(){
    if(!gameStart || gameOver)return;
    isPause=true;
    pauseMenu.style.display="flex";
}
function closePause(){
    isPause=false;
    pauseMenu.style.display="none";
    bugTipPop.style.display="none";
}
//左上角主页按钮+暂停页主页按钮共用重置
function backHomeAll(){
    closePause();
    gameStart=false;
    gameOver=false;
    resetGame();
}
resumeBtn.onclick=closePause;
homePauseBtn.onclick=backHomeAll;
homeBox.onclick=backHomeAll;
bugInfoBtn.onclick=()=>{
    bugTipPop.style.display=bugTipPop.style.display==="block"?"none":"block";
}
pauseMobile.onclick=openPause;

//武器双语名称配置
const weaponLang = [
    {cn:"手枪",en:"Pistol"},
    {cn:"机枪",en:"Rifle"},
    {cn:"散弹枪",en:"Shotgun"},
    {cn:"狙击枪",en:"Sniper"}
];

//全局多语言
const langData = {
    cn:{
        score:"分数",hp:"血量",sta:"体力",hung:"饥饿",weapon:"武器",
        tipStart:"点击屏幕任意位置开始游戏",tipDead:"你已阵亡",tipDeadBack:"按任意按键返回主界面",
        auto:"自动开火",book:"怪物图鉴",close:"×",bossName:"BOSS巨型暴君",bossDesc:"巨型BOSS,击杀高额分数",
        bag:"背包",codex:"图鉴",sprint:"冲刺",pick:"拾取",pause:"暂停",resume:"继续游戏",home:"返回主页面"
    },
    en:{
        score:"Score",hp:"HP",sta:"Stamina",hung:"Hunger",weapon:"Weapon",
        tipStart:"Click screen to start game",tipDead:"You Died",tipDeadBack:"Press any key to return",
        auto:"AUTO",book:"Monster Book",close:"×",bossName:"Tyrant Boss",bossDesc:"Big Boss,high score reward",
        bag:"Bag",codex:"Codex",sprint:"Run",pick:"Pick",pause:"Pause",resume:"Resume",home:"Home"
    }
};
let isCN = true;
function refreshLang(){
    let d = isCN?langData.cn:langData.en;
    autoBtn.innerText = d.auto;
    btnBag.innerText=d.bag;
    btnBook.innerText=d.codex;
    btnSprint.innerText=d.sprint;
    btnPick.innerText=d.pick;
    pauseMobile.innerText=d.pause;
    resumeBtn.innerText=d.resume;
    homePauseBtn.innerText=d.home;
    homeBox.innerText=d.home;
    refreshUI();renderHotbar();
}
langBtn.onclick = ()=>{isCN = !isCN;refreshLang();}

const MAP_W = 4000;
const MAP_H = 3000;
let camX = 0, camY = 0;
let score = 0;
let gameOver = false;
let gameStart = false;
let autoAttack = false;
let showBook = false;
let showBackpack = false;
let selectBagIndex = -1;
let unlockMonster = new Set();
let bossSpawned = false;

let player = {
    x: MAP_W/2, y: MAP_H/2, size:28, speed:5, runSpeed:8,
    hp:100, maxHp:100,
    stamina:100, maxStam:100,
    hunger:100, maxHunger:100,
    color:"#3f3"
};

const keys = {w:false,a:false,s:false,d:false,ctrl:false};
let joyActive = false, jx=0,jy=0,maxJoy=38;

let zombies = [];
let bullets = [];
let walls = [];
let zProj = [];
let groundItems = [];
let chests = [];

let hotbarItems = Array(8).fill(null);
let backpackItems = Array(81).fill(null);
let currentWeapon = 0;

const weapons = [
    {dmg:16, rate:280, speed:16, count:1, spread:0, type:"gun"},
    {dmg:9, rate:90, speed:19, count:1, spread:0.08, type:"gun"},
    {dmg:10, rate:550, speed:14, count:8, spread:0.35, type:"gun"},
    {dmg:85, rate:750, speed:32, count:1, spread:0, type:"gun"}
];
const foodList = {
    "面包":30,
    "罐头":50,
    "能量棒":20
};

const monsterData = [
    {type:"normal", name:"普通僵尸", hp:40, speed:1.7, color:"#f33", desc:"基础僵尸,近身攻击"},
    {type:"fast", name:"迅捷僵尸", hp:22, speed:2.9, color:"#fa6", desc:"移速超快,血量低"},
    {type:"tank", name:"坦克僵尸", hp:130, speed:0.9, color:"#711", desc:"血量超高,伤害高"},
    {type:"range", name:"毒液僵尸", hp:45, speed:1.3, color:"#c4f", desc:"远程发射毒液"},
    {type:"boss", name:"BOSS巨型暴君", hp:550, speed:0.5, color:"#900", desc:"巨型BOSS,击杀高额分数"}
];

let lastShot = 0;
closeBookBtn.onclick=()=>{showBook=false;bookPanel.style.display="none"};
btnBag.onclick=()=>{showBackpack=!showBackpack;backpack.style.display=showBackpack?"grid":"none"};
btnBook.onclick=()=>{showBook=!showBook;bookPanel.style.display=showBook?"block":"none";refreshBookUI()};
btnSprint.onmousedown=()=>keys.ctrl=true;
btnSprint.onmouseup=()=>keys.ctrl=false;
btnSprint.ontouchstart=()=>keys.ctrl=true;
btnSprint.ontouchend=()=>keys.ctrl=false;
btnPick.onclick=()=>{
    let mx=cvs.width/2,my=cvs.height/2;
    pickItem(mx,my);
}

function refreshBookUI(){
    bookContent.innerHTML="";
    monsterData.forEach(mon=>{
        let div=document.createElement("div");
        div.className="mon-item";
        let isOpen=unlockMonster.has(mon.type);
        div.classList.add(isOpen?"mon-unlock":"mon-locked");
        div.innerHTML=`<div class="mon-name">${isOpen?mon.name:"???"}</div><div class="mon-desc">${isOpen?mon.desc:"尚未击杀,信息未知"}`;
        bookContent.appendChild(div);
    })
}

function rand(min,max){return Math.random()*(max-min)+min}
function dist(x1,y1,x2,y2){return Math.hypot(x1-x2,y1-y2)}
function rectCollide(r,c){
    let cx=Math.max(r.x,Math.min(c.x,r.x+r.w));
    let cy=Math.max(r.y,Math.min(c.y,r.y+r.h));
    return dist(cx,cy,c.x,c.y)<=c.size;
}

function createWalls(){
    walls=[];
    for(let i=0;i<25;i++){
        walls.push({x:rand(0,MAP_W),y:rand(0,MAP_H),w:rand(100,500),h:40});
        walls.push({x:rand(0,MAP_W),y:rand(0,MAP_H),w:40,h:rand(100,500)});
    }
}

function spawnChest(){
    chests.push({
        x:rand(100,MAP_W-100),
        y:rand(100,MAP_H-100),
        w:40,h:40,
        opened:false
    })
}

function initStartChest(){
    for(let i=0;i<12;i++) spawnChest();
}

function openChest(chest){
    if(chest.opened)return;
    chest.opened=true;
    let foodArr = Object.keys(foodList);
    let loot = foodArr[Math.floor(rand(0,foodArr.length))];
    groundItems.push({x:chest.x,y:chest.y,name:loot,size:16,type:"food"});
}

function autoPickItem(){
    for(let i=groundItems.length-1;i>=0;i--){
        let it = groundItems[i];
        if(dist(player.x,player.y,it.x,it.y)<60){
            let ok = false;
            for(let h=4;h<8;h++){
                if(!hotbarItems[h]){
                    hotbarItems[h]=it.name;
                    groundItems.splice(i,1);
                    renderHotbar();
                    ok=true;break;
                }
            }
            if(ok)continue;
            for(let b=0;b<backpackItems.length;b++){
                if(!backpackItems[b]){
                    backpackItems[b]=it.name;
                    groundItems.splice(i,1);
                    renderBackpack();
                    ok=true;break;
                }
            }
        }
    }
}

function pickItem(mouseX,mouseY){
    let worldX = mouseX + camX;
    let worldY = mouseY + camY;
    for(let i=groundItems.length-1;i>=0;i--){
        let it = groundItems[i];
        if(dist(worldX,worldY,it.x,it.y)<35 && dist(player.x,player.y,it.x,it.y)<90){
            for(let b=0;b<backpackItems.length;b++){
                if(!backpackItems[b]){
                    backpackItems[b] = it.name;
                    groundItems.splice(i,1);
                    renderBackpack();
                    return;
                }
            }
        }
    }
}

function refreshUI(){
    let d = isCN?langData.cn:langData.en;
    const curGunName = isCN ? weaponLang[currentWeapon].cn : weaponLang[currentWeapon].en;
    ui.innerHTML=`${d.score}:${score|0}<br>${d.hp}:${player.hp|0}<br>${d.sta}:${player.stamina|0}<br>${d.hung}:${player.hunger|0}<br>${d.weapon}:${curGunName}`;
    hpFill.style.width=player.hp+"%";
    stamFill.style.width=player.stamina+"%";
    hungerFill.style.width=player.hunger+"%";
}

function resetGame(){
    player={x:MAP_W/2,y:MAP_H/2,size:28,speed:5,runSpeed:8,hp:100,maxHp:100,stamina:100,maxStam:100,hunger:100,maxHunger:100,color:"#3f3"};
    score=0;gameOver=false;autoAttack=false;bossSpawned=false;isPause=false;
    zombies=[];bullets=[];zProj=[];groundItems=[];chests=[];
    hotbarItems = Array(8).fill(null);
    backpackItems=Array(81).fill(null);
    currentWeapon=0;
    selectBagIndex=-1;
    for(let i=0;i<4;i++)hotbarItems[i]=weapons[i];
    createWalls();
    initStartChest();
    refreshUI();
    renderHotbar();
    renderBackpack();
    refreshBookUI();
    pauseMenu.style.display="none";
}

function renderHotbar(){
    hotbar.innerHTML="";
    hotbarItems.forEach((itm,i)=>{
        let d=document.createElement("div");
        d.className="slot "+(i<4?"weapon":"food")+(i==currentWeapon&&i<4?" active":"");
        if(i<4){
            d.innerText = isCN ? weaponLang[i].cn : weaponLang[i].en;
        }else{
            d.innerText=itm||"空";
        }
        let clickTimer=null;
        d.onclick=()=>{
            clickTimer=setTimeout(()=>{
                if(i<4){
                    currentWeapon=i;renderHotbar();refreshUI();return;
                }
                if(foodList[itm]){
                    player.hunger = Math.min(player.maxHunger, player.hunger + foodList[itm]);
                    hotbarItems[i]=null;
                    refreshUI();renderHotbar();
                }
            },220)
        }
        d.ondblclick=()=>{
            clearTimeout(clickTimer);
            if(!itm)return;
            groundItems.push({x:player.x+rand(-60,60),y:player.y+rand(-60,60),name:itm,size:16,type:"food"});
            hotbarItems[i]=null;renderHotbar();
        }
        hotbar.appendChild(d);
    })
}

function renderBackpack(){
    backpack.innerHTML="";
    backpackItems.forEach((itm,i)=>{
        let d=document.createElement("div");
        d.className="bp-slot"+(i===selectBagIndex?" selected":"");
        d.innerText=itm||"";
        let clkT=null;
        d.onclick=()=>{
            selectBagIndex=i;
            clkT=setTimeout(()=>{
                if(foodList[itm]){
                    player.hunger=Math.min(player.maxHunger,player.hunger+foodList[itm]);
                    backpackItems[i]=null;
                    refreshUI();renderBackpack();
                }
            },220);
            renderBackpack();
        }
        d.ondblclick=()=>{
            clearTimeout(clkT);
            if(!itm)return;
            groundItems.push({x:player.x+rand(-50,50),y:player.y+rand(-50,50),name:itm,size:16,type:"food"});
            backpackItems[i]=null;renderBackpack();
        }
        backpack.appendChild(d);
    })
}

function spawnZombie(){
    if(!gameStart||gameOver||isPause)return;
    if(!bossSpawned && score>=8){
        bossSpawned=true;
        let boss={type:"boss",size:55,speed:0.5,hp:550,color:"#900",atk:1.8,spit:0,x:rand(0,MAP_W),y:-80};
        zombies.push(boss);
        unlockMonster.add("boss");refreshBookUI();
        return;
    }
    let t=rand(0,100)|0;
    let z;
    if(t<50)z={type:"normal",size:24,speed:1.7,hp:40,color:"#f33",atk:0.4,spit:0};
    else if(t<78)z={type:"fast",size:20,speed:2.9,hp:22,color:"#fa6",atk:0.3,spit:0};
    else if(t<92)z={type:"tank",size:34,speed:0.9,hp:130,color:"#711",atk:1.0,spit:0};
    else z={type:"range",size:25,speed:1.3,hp:45,color:"#c4f",atk:0,spit:0};
    let side=rand(0,4)|0;
    if(side==0){z.x=rand(0,MAP_W);z.y=-60}
    else if(side==1){z.x=MAP_W+60;z.y=rand(0,MAP_H)}
    else if(side==2){z.x=rand(0,MAP_W);z.y=MAP_H+60}
    else{z.x=-60;z.y=rand(0,MAP_H)}
    zombies.push(z);
}

function shoot(tx,ty){
    if(isPause)return;
    let now=Date.now();
    let w=weapons[currentWeapon];
    if(!w||w.type!=="gun")return;
    if(now-lastShot<w.rate)return;
    lastShot=now;
    let dx=tx-player.x, dy=ty-player.y, len=dist(dx,dy,0,0);
    let nx=dx/len, ny=dy/len;
    for(let i=0;i<w.count;i++){
        let sx=nx+rand(-w.spread,w.spread);
        let sy=ny+rand(-w.spread,w.spread);
        let l=dist(sx,sy,0,0);
        bullets.push({
            x:player.x,y:player.y,
            dx:sx/l,dy:sy/l,
            speed:w.speed,
            dmg:w.dmg,
            size:4,col:"#ff3"
        })
    }
}

function autoShoot(){
    if(!autoAttack||zombies.length<1||!gameStart||gameOver||isPause)return;
    let tar=zombies[0];
    let min=dist(player.x,player.y,tar.x,tar.y);
    zombies.forEach(z=>{
        let d=dist(player.x,player.y,z.x,z.y);
        if(d<min){min=d;tar=z}
    })
    shoot(tar.x,tar.y);
}

function update(){
    if(!gameStart||gameOver||isPause)return;
    autoPickItem();

    //饥饿缓慢下降
    player.hunger-=0.008;
    if(player.hunger<0)player.hunger=0;

    //【饱腹回血:饥饿满100缓慢回血,回血速率偏低】
    if(player.hunger >= player.maxHunger){
        if(player.hp < player.maxHp){
            player.hp += 0.012; //回血很慢
            if(player.hp>player.maxHp)player.hp=player.maxHp;
        }
    }else if(player.hunger<=0){
        //饿肚子掉血
        player.hp-=0.08;
        if(player.hp<0)player.hp=0;
    }

    let moving=false;
    let mx=0,my=0;
    if(keys.w)my--;if(keys.s)my++;if(keys.a)mx--;if(keys.d)mx++;
    if(joyActive){mx=jx;my=jy}
    let l=dist(mx,my,0,0);
    if(l>0){mx/=l;my/=l;moving=true}

    let spd=player.speed;
    if(keys.ctrl&&player.stamina>0&&moving){
        spd=player.runSpeed;
        player.stamina-=0.5;
        if(player.stamina<0)player.stamina=0;
    }else if(!moving&&player.stamina<100){
        player.stamina+=0.4;
    }

    let nx=player.x+mx*spd;
    let ny=player.y+my*spd;
    let ok=true;
    walls.forEach(w=>{if(rectCollide(w,{x:nx,y:ny,size:player.size}))ok=false});
    if(ok){player.x=nx;player.y=ny}
    player.x=Math.max(player.size,Math.min(MAP_W-player.size,player.x));
    player.y=Math.max(player.size,Math.min(MAP_H-player.size,player.y));

    camX=player.x-cvs.width/2;
    camY=player.y-cvs.height/2;
    camX=Math.max(0,Math.min(MAP_W-cvs.width,camX));
    camY=Math.max(0,Math.min(MAP_H-cvs.height,camY));

    zombies.forEach(z=>{
        let dx=player.x-z.x, dy=player.y-z.y, d=dist(dx,dy,0,0);
        if(d>0){dx/=d;dy/=d}
        let tx=z.x+dx*z.speed;
        let ty=z.y+dy*z.speed;
        let can=true;
        walls.forEach(w=>{if(rectCollide(w,{x:tx,y:ty,size:z.size}))can=false});
        if(can){z.x=tx;z.y=ty}
        if(d<player.size+z.size&&z.type!="range"){
            player.hp-=z.atk/20;
            if(player.hp<0)player.hp=0;
        }
        if(z.type=="range"){
            z.spit++;
            if(z.spit>200&&dist(z.x,z.y,player.x,player.y)<450){
                z.spit=0;
                let dx=player.x-z.x,dy=player.y-z.y,dd=dist(dx,dy,0,0);
                zProj.push({x:z.x,y:z.y,dx:dx/dd,dy:dy/dd,speed:5,size:6,col:"#f0f"});
            }
        }
    })

    zProj=zProj.filter(p=>{
        p.x+=p.dx*p.speed;p.y+=p.dy*p.speed;
        if(p.x<0||p.x>MAP_W||p.y<0||p.y>MAP_H)return false;
        walls.forEach(w=>{if(rectCollide(w,p))return false});
        if(dist(p.x,p.y,player.x,player.y)<player.size+p.size){
            player.hp-=0.5;
            return false;
        }
        return true;
    })

    bullets=bullets.filter(b=>{
        b.x+=b.dx*b.speed;b.y+=b.dy*b.speed;
        if(b.x<0||b.x>MAP_W||b.y<0||b.y>MAP_H)return false;
        walls.forEach(w=>{if(rectCollide(w,b))return false});
        let hit=false;
        zombies=zombies.filter(z=>{
            if(dist(b.x,b.y,z.x,z.y)<z.size+b.size){
                hit=true;z.hp-=b.dmg;
                if(z.hp<=0){
                    unlockMonster.add(z.type);
                    refreshBookUI();
                    score += z.type==="boss"?35:1;
                    return false;
                }
            }
            return true;
        })
        return !hit;
    })

    if(player.hp<=0){gameOver=true}

    autoShoot();
    refreshUI();
}

function draw(){
    let d = isCN?langData.cn:langData.en;
    if(!gameStart){
        ctx.fillStyle="#111";ctx.fillRect(0,0,cvs.width,cvs.height);
        ctx.fillStyle="#0f0";ctx.font="bold 48px Arial";ctx.textAlign="center";
        ctx.fillText(isCN?"僵尸生存射击":"Zombie Survival Shooting",cvs.width/2,120);
        ctx.font="24px Arial";ctx.fillStyle="#fff";
        if(isCN){
            ctx.fillText("WASD/摇杆 = 移动",cvs.width/2,200);
            ctx.fillText("左键射击|右键拾取,靠近物品自动拾取",cvs.width/2,240);
            ctx.fillText("背包/快捷栏:单击吃食物,双击丢弃",cvs.width/2,280);
            ctx.fillText("Ctrl疾跑|E开箱子|B背包|M图鉴|ESC暂停",cvs.width/2,320);
        }else{
            ctx.fillText("WASD/Joystick = Move",cvs.width/2,200);
            ctx.fillText("Left shoot | Right pickup, auto pickup nearby items",cvs.width/2,240);
            ctx.fillText("Bag/Hotbar:Click eat,Double click drop",cvs.width/2,280);
            ctx.fillText("Ctrl Sprint | E Open chest | B Bag | M Codex | ESC Pause",cvs.width/2,320);
        }
        ctx.fillStyle="#ff0";ctx.font="bold 30px Arial";
        ctx.fillText(d.tipStart,cvs.width/2,480);
        return;
    }
    if(gameOver){
        ctx.fillStyle="#111";ctx.fillRect(0,0,cvs.width,cvs.height);
        ctx.fillStyle="#f33";ctx.font="bold 50px Arial";ctx.textAlign="center";
        ctx.fillText(d.tipDead,cvs.width/2,220);
        ctx.fillStyle="#ff0";ctx.font="bold 30px Arial";
        ctx.fillText(d.tipDeadBack,cvs.width/2,320);
        return;
    }

    ctx.fillStyle="#1c1c1c";ctx.fillRect(0,0,cvs.width,cvs.height);
    ctx.save();ctx.translate(-camX,-camY);

    for(let x=0;x<MAP_W;x+=80){ctx.strokeStyle="#333";ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,MAP_H);ctx.stroke()}
    for(let y=0;y<MAP_H;y+=80){ctx.strokeStyle="#333";ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(MAP_W,y);ctx.stroke()}

    ctx.fillStyle="#666";walls.forEach(w=>{ctx.fillRect(w.x,w.y,w.w,w.h)});

    chests.forEach(c=>{
        ctx.fillStyle=c.opened?"#555":"#963";
        ctx.fillRect(c.x,c.y,c.w,c.h);
    })

    groundItems.forEach(it=>{
        ctx.fillStyle="#ffd700";
        ctx.beginPath();ctx.arc(it.x,it.y,it.size,0,Math.PI*2);ctx.fill();
    })

    ctx.fillStyle=player.color;ctx.beginPath();ctx.arc(player.x,player.y,player.size,0,Math.PI*2);ctx.fill();

    zombies.forEach(z=>{ctx.fillStyle=z.color;ctx.beginPath();ctx.arc(z.x,z.y,z.size,0,Math.PI*2);ctx.fill()});
    bullets.forEach(b=>{ctx.fillStyle=b.col;ctx.beginPath();ctx.arc(b.x,b.y,b.size,0,Math.PI*2);ctx.fill()});
    zProj.forEach(p=>{ctx.fillStyle=p.col;ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,Math.PI*2);ctx.fill()});

    ctx.restore();
}

function loop(){update();draw();requestAnimationFrame(loop)}

window.onkeydown=e=>{
    //ESC打开暂停菜单
    if(e.key==="Escape"){openPause();return;}
    if(gameOver){
        gameOver=false;gameStart=false;return;
    }
    let k=e.key.toLowerCase();
    if(k==" ")if(!gameStart){gameStart=true;resetGame()}
    if(k=="r")if(gameOver)resetGame();
    if(k=="m"){showBook=!showBook;bookPanel.style.display=showBook?"block":"none";refreshBookUI();}
    if(k=="b"){showBackpack=!showBackpack;backpack.style.display=showBackpack?"grid":"none"}
    if(k=="e")chests.forEach(c=>{if(!c.opened&&dist(c.x,c.y,player.x,player.y)<80)openChest(c)});
    if(k=="control")keys.ctrl=true;
    if(!gameStart||gameOver||isPause)return;
    if(k=="w")keys.w=true;if(k=="s")keys.s=true;if(k=="a")keys.a=true;if(k=="d")keys.d=true;
}
window.onkeyup=e=>{
    let k=e.key.toLowerCase();
    if(k=="control")keys.ctrl=false;
    if(k=="w")keys.w=false;if(k=="s")keys.s=false;if(k=="a")keys.a=false;if(k=="d")keys.d=false;
}

autoBtn.onclick=()=>{autoAttack=!autoAttack;autoBtn.style.background=autoAttack?"rgba(0,200,0,0.7)":"rgba(255,60,60,0.7)";refreshLang();};

cvs.onmousedown=e=>{
    const rect=cvs.getBoundingClientRect();
    const mx = e.clientX - rect.left;
    const my = e.clientY - rect.top;
    if(!gameStart){gameStart=true;resetGame();return;}
    if(gameOver||showBackpack||isPause)return;
    if(e.button===0){
        let wx=mx+camX;let wy=my+camY;
        shoot(wx,wy);
    }else if(e.button===2){
        pickItem(mx,my);
    }
}
let touchTimer=null;
cvs.ontouchstart=e=>{
    if(!gameStart){gameStart=true;resetGame();return;}
    let t = e.touches[0];
    const rect=cvs.getBoundingClientRect();
    let mx = t.clientX - rect.left;
    let my = t.clientY - rect.top;
    touchTimer=setTimeout(()=>{pickItem(mx,my)},350);
}
cvs.ontouchend=()=>{clearTimeout(touchTimer)}
cvs.oncontextmenu=e=>e.preventDefault();

function joyUpdate(x,y){
    let r=joyWrap.getBoundingClientRect();
    let cx=r.left+r.width/2, cy=r.top+r.height/2;
    let dx=x-cx, dy=y-cy, d=dist(dx,dy,0,0);
    if(d>maxJoy){dx=dx/d*maxJoy;dy=dy/d*maxJoy}
    joyH.style.transform=`translate(calc(-50%+${dx}px),calc(-50%+${dy}px))`;
    jx=dx/maxJoy;jy=dy/maxJoy;
}
joyWrap.onmousedown=e=>{joyActive=true;joyUpdate(e.clientX,e.clientY)}
window.onmousemove=e=>{if(joyActive)joyUpdate(e.clientX,e.clientY)}
window.onmouseup=()=>{joyActive=false;jx=0;jy=0;joyH.style.transform="translate(-50%,-50%)"};
joyWrap.ontouchstart=e=>{joyActive=true;joyUpdate(e.touches[0].clientX,e.touches[0].clientY)}
window.ontouchmove=e=>{if(joyActive)joyUpdate(e.touches[0].clientX,e.touches[0].clientY)}
window.ontouchend=()=>{joyActive=false;jx=0;jy=0;joyH.style.transform="translate(-50%,-50%)"};

let spawnTimer=setInterval(spawnZombie,800);
let chestTimer=setInterval(spawnChest,2200);
refreshLang();
renderHotbar();
loop();
</script>
</body>
</html>

Game Source: 僵尸生存-最终完整版

Creator: SuperPhoenix65

Libraries: none

Complexity: complex (752 lines, 29.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: -superphoenix65" to link back to the original. Then publish at arcadelab.ai/publish.