僵尸生存-最终完整版
by SuperPhoenix65752 lines29.2 KB
<!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.