僵尸生存射击 正式版
by SuperPhoenix65387 lines36.1 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}
#modePanel{
position:absolute;top:0;left:0;width:100%;height:100%;
background:rgba(0,0,0,0.95);z-index:200;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:30px;
}
.modeBtn{width:280px;height:70px;font-size:24px;background:#246;color:#fff;border:2px solid #ffd700;cursor:pointer;border-radius:8px;transition:0.2s}
.modeBtn:hover{background:#369}
.modeBtn.lock{background:#444;border-color:#666;cursor:not-allowed}
.gameTitle{font-size:52px;color:#0f0;margin-bottom:20px}
.tipText{color:#ccc;font-size:18px}
#adminBtn{
position:absolute;left:20px;top:50%;transform:translateY(-50%);
width:60px;height:60px;background:#845EC2;border:3px solid #fff;
color:#fff;font-size:14px;font-weight:bold;cursor:pointer;
display:flex;align-items:center;justify-content:center;border-radius:4px;
z-index:210; box-shadow:0 0 10px #845EC2;
}
#adminBtn:hover{background:#946ED2}
#pwdPanel,#levelSelectPanel{
position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
width:350px;height:220px;background:#222;border:3px solid #ffd700;
z-index:220;display:none;flex-direction:column;align-items:center;justify-content:center;gap:15px;padding:20px;
}
#levelSelectPanel{height:300px;}
#pwdInput{width:220px;height:40px;font-size:18px;padding:0 10px;background:#333;color:#fff;border:2px solid #888;outline:none;}
.adminBtn{width:100px;height:40px;font-size:16px;cursor:pointer;background:#246;color:#fff;border:none;border-radius:4px;}
.levelSelBtn{width:200px;height:38px;margin:4px 0;font-size:16px;background:#357;color:#fff;border:none;border-radius:4px;cursor:pointer;}
.adminTip{color:#fff;font-size:18px;}
#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{opacity:0.8}
#levelInfo{position:absolute;top:60px;right:20px;color:#fff;font-size:20px;text-shadow:2px 2px #000;z-index:98;line-height:1.6}
#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)}
.slot.lock{opacity:0.3;cursor:not-allowed}
#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}
#pauseMenu,#winPanel{
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;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;}
.winText{font-size:28px;color:#0f0;text-align:center;}
</style>
</head>
<body>
<div id="gameBox">
<div id="adminBtn">管理员</div>
<div id="pwdPanel"><div class="adminTip">请输入管理员密码</div><input type="password" id="pwdInput" placeholder="输入密码"><div><button class="adminBtn" id="confirmPwd">确认</button><button class="adminBtn" id="closePwd">取消</button></div></div>
<div id="levelSelectPanel"><div class="adminTip">选择关卡/模式</div><button class="levelSelBtn" data-lv="0">第一关</button><button class="levelSelBtn" data-lv="1">第二关</button><button class="levelSelBtn" data-lv="2">第三关</button><button class="levelSelBtn" data-lv="3">第四关(最终关)</button><button class="levelSelBtn" data-lv="endless">直接解锁无尽模式</button><button class="adminBtn" id="closeLevelSel">返回</button></div>
<div id="modePanel"><div class="gameTitle">僵尸生存射击 正式版</div><button class="modeBtn" id="storyMode">闯关模式</button><button class="modeBtn lock" id="endlessMode">无尽模式(通关解锁)</button><div class="tipText">通关全部4个关卡,解锁无尽模式</div></div>
<div id="langBtn"><span>语言</span><span>CN</span></div><div id="homeBox">返回主页</div><div id="levelInfo"></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 class="mobileBtn" id="btnOpen">开箱</div><div id="pauseMobile">暂停</div></div>
<div id="pauseMenu"><button class="pauseBtn" id="resumeBtn">继续游戏</button><button class="pauseBtn" id="homePauseBtn">返回主页面</button><button class="pauseBtn" id="bugInfoBtn">!</button></div>
<div id="winPanel"><div class="winText" id="winText">关卡通关!</div><button class="pauseBtn" id="nextLevelBtn">下一关</button><button class="pauseBtn" id="backModeBtn">返回主页</button></div>
<div id="bugTipPop">BUG已经全部修复,后续持续优化,敬请体验!</div>
</div>
<script>
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type){const osc=audioCtx.createOscillator();const gain=audioCtx.createGain();osc.connect(gain);gain.connect(audioCtx.destination);gain.gain.value=0.15;const now=audioCtx.currentTime;switch(type){case "gun":osc.type="square";osc.frequency.setValueAtTime(180,now);osc.frequency.exponentialRampToValueAtTime(60,now+0.1);gain.gain.exponentialRampToValueAtTime(0.01,now+0.1);osc.start(now);osc.stop(now+0.1);break;case "die":osc.type="sawtooth";osc.frequency.setValueAtTime(120,now);osc.frequency.exponentialRampToValueAtTime(30,now+0.25);gain.gain.exponentialRampToValueAtTime(0.01,now+0.25);osc.start(now);osc.stop(now+0.25);break;case "walk":osc.type="sine";osc.frequency.value=90;gain.gain.value=0.08;gain.gain.exponentialRampToValueAtTime(0.01,now+0.04);osc.start(now);osc.stop(now+0.04);break;case "run":osc.type="sine";osc.frequency.value=110;gain.gain.value=0.1;gain.gain.exponentialRampToValueAtTime(0.01,now+0.03);osc.start(now);osc.stop(now+0.03);break;}}
let walkCd=0,runCd=0;
const cvs=document.getElementById("gameCanvas"),ctx=cvs.getContext("2d");
const ui=document.getElementById("ui"),levelInfo=document.getElementById("levelInfo");
const hotbar=document.getElementById("hotbar"),backpack=document.getElementById("backpack");
const autoBtn=document.getElementById("autoBtn"),joyWrap=document.getElementById("joyWrap"),joyH=document.getElementById("joyHandle");
const bookPanel=document.getElementById("bookPanel"),bookContent=document.getElementById("bookContent"),closeBookBtn=document.getElementById("closeBookBtn");
const hpFill=document.getElementById("hpFill"),stamFill=document.getElementById("stamFill"),hungerFill=document.getElementById("hungerFill");
const langBtn=document.getElementById("langBtn"),homeBox=document.getElementById("homeBox");
const btnBag=document.getElementById("btnBag"),btnBook=document.getElementById("btnBook"),btnSprint=document.getElementById("btnSprint"),btnPick=document.getElementById("btnPick"),btnOpen=document.getElementById("btnOpen"),pauseMobile=document.getElementById("pauseMobile");
const pauseMenu=document.getElementById("pauseMenu"),resumeBtn=document.getElementById("resumeBtn"),homePauseBtn=document.getElementById("homePauseBtn"),bugInfoBtn=document.getElementById("bugInfoBtn"),bugTipPop=document.getElementById("bugTipPop");
const modePanel=document.getElementById("modePanel"),storyMode=document.getElementById("storyMode"),endlessMode=document.getElementById("endlessMode");
const winPanel=document.getElementById("winPanel"),winText=document.getElementById("winText"),nextLevelBtn=document.getElementById("nextLevelBtn"),backModeBtn=document.getElementById("backModeBtn");
const adminBtn=document.getElementById("adminBtn");
const pwdPanel=document.getElementById("pwdPanel");
const levelSelectPanel=document.getElementById("levelSelectPanel");
const pwdInput=document.getElementById("pwdInput");
const confirmPwd=document.getElementById("confirmPwd");
const closePwd=document.getElementById("closePwd");
const closeLevelSel=document.getElementById("closeLevelSel");
const levelSelBtns=document.querySelectorAll(".levelSelBtn");
const ADMIN_PWD="916520";
function openPwdPanel(){pwdPanel.style.display="flex";pwdInput.value="";pwdInput.focus();}
adminBtn.onclick = openPwdPanel;
adminBtn.ontouchstart = e => {e.preventDefault(); openPwdPanel();}
closePwd.onclick = () => {pwdPanel.style.display="none";}
closeLevelSel.onclick = () => {levelSelectPanel.style.display="none";}
confirmPwd.onclick = () => {if(pwdInput.value===ADMIN_PWD){pwdPanel.style.display="none";levelSelectPanel.style.display="flex";}else{alert("密码错误!");}}
levelSelBtns.forEach(btn=>{
btn.onclick = () => {
const val=btn.dataset.lv;backHomeAll();
if(val==="endless"){endlessUnlocked=true;endlessMode.classList.remove("lock");alert("已解锁无尽模式!");}
else{curLevel=parseInt(val);gameMode="story";modePanel.style.display="none";gameStart=true;resetGame();}
levelSelectPanel.style.display="none";
}
});
const levelConfig=[{target:20,unlock:[0],name:"第一关",weaponName:"手枪"},{target:50,unlock:[0,2],name:"第二关",weaponName:"散弹枪"},{target:75,unlock:[0,2,3],name:"第三关",weaponName:"狙击枪"},{target:200,unlock:[0,1,2,3],name:"第四关",weaponName:"机枪"}];
let curLevel=0,gameMode="",levelPassed=[false,false,false,false],endlessUnlocked=false;
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;gameMode="";modePanel.style.display="flex";pauseMenu.style.display="none";winPanel.style.display="none";resetGame();}
function showWin(){isPause=true;winPanel.style.display="flex";if(curLevel>=levelConfig.length-1){winText.innerText="全部关卡通关!\n已解锁无尽模式";endlessUnlocked=true;endlessMode.classList.remove("lock");}else{winText.innerText=`${levelConfig[curLevel].name} 通关!\n解锁${levelConfig[curLevel+1].weaponName}`;}}
resumeBtn.onclick = closePause;
resumeBtn.ontouchstart = e => {e.preventDefault(); closePause();}
homePauseBtn.onclick = backHomeAll;
homePauseBtn.ontouchstart = e => {e.preventDefault(); backHomeAll();}
homeBox.onclick = backHomeAll;
homeBox.ontouchstart = e => {e.preventDefault(); backHomeAll();}
bugInfoBtn.onclick = () => {bugTipPop.style.display=bugTipPop.style.display==="block"?"none":"block";};
bugInfoBtn.ontouchstart = bugInfoBtn.onclick;
pauseMobile.onclick = openPause;
pauseMobile.ontouchstart = e => {e.preventDefault(); openPause();}
nextLevelBtn.onclick = () => {curLevel++;resetGame();winPanel.style.display="none";isPause=false;};
nextLevelBtn.ontouchstart = nextLevelBtn.onclick;
backModeBtn.onclick = backHomeAll;
backModeBtn.ontouchstart = backModeBtn.onclick;
storyMode.onclick = () => {gameMode="story";curLevel=0;modePanel.style.display="none";gameStart=true;resetGame();};
storyMode.ontouchstart = storyMode.onclick;
endlessMode.onclick = () => {if(!endlessUnlocked)return;gameMode="endless";modePanel.style.display="none";gameStart=true;resetGame();};
endlessMode.ontouchstart = endlessMode.onclick;
const weaponLang=[{cn:"手枪",en:"Pistol"},{cn:"机枪",en:"Rifle"},{cn:"散弹枪",en:"Shotgun"},{cn:"狙击枪",en:"Sniper"}];
const langData={
cn:{score:"分数",hp:"血量",sta:"体力",hung:"饥饿",weapon:"武器",tipStart:"点击屏幕任意位置开始游戏 | 按键 1/2/3/4 切换武器",tipDead:"你已阵亡",tipDeadBack:"按空格返回主界面",auto:"自动开火",book:"怪物图鉴",close:"×",giantName:"巨人僵尸",giantDesc:"红色大块头,血量中等,近战攻击",bossName:"暴君BOSS",bossDesc:"终极暴君,可召唤小弟、投掷火球,威胁极高",bag:"背包",codex:"图鉴",sprint:"冲刺",pick:"拾取",open:"开箱",pause:"暂停",resume:"继续游戏",home:"返回主页面",keyInfo:"【电脑快捷键】WASD移动|ESC暂停|B背包|M图鉴|E开箱|Ctrl疾跑|1~4切换武器",level:"当前关卡",target:"目标分数",unlock:"已解锁武器",modeStory:"闯关模式",modeEndless:"无尽模式(通关解锁)",tipUnlockEndless:"通关全部4个关卡,解锁无尽模式",winLevelClear:"关卡通关!",winAllClear:"全部关卡通关!\n已解锁无尽模式",nextLevel:"下一关",backToMode:"返回主页",empty:"空"},
en:{score:"Score",hp:"HP",sta:"Stamina",hung:"Hunger",weapon:"Weapon",tipStart:"Click to Start | Press 1/2/3/4 to Switch Weapons",tipDead:"You Died",tipDeadBack:"Press SPACE to return",auto:"AUTO",book:"Monster Book",close:"×",giantName:"Giant Zombie",giantDesc:"Red brute, medium HP, melee attack",bossName:"Tyrant Boss",bossDesc:"Final boss, summons minions & throws fireball",bag:"Bag",codex:"Codex",sprint:"Sprint",pick:"Pick Up",open:"Open Box",pause:"Pause",resume:"Resume",home:"Home",keyInfo:"[PC Hotkey] WASD Move|ESC Pause|B Bag|M Book|E OpenBox|Ctrl Sprint|1~4 Switch Weapon",level:"Level",target:"Target",unlock:"Unlocked",modeStory:"Story Mode",modeEndless:"Endless Mode",tipUnlockEndless:"Clear all 4 levels to unlock Endless Mode",winLevelClear:"Level Cleared!",winAllClear:"All Levels Cleared!\nEndless Mode Unlocked",nextLevel:"Next Level",backToMode:"Back",empty:"Empty"}
};
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;btnOpen.innerText=d.open;pauseMobile.innerText=d.pause;resumeBtn.innerText=d.resume;homePauseBtn.innerText=d.home;homeBox.innerText=d.home;nextLevelBtn.innerText=d.nextLevel;backModeBtn.innerText=d.backToMode;storyMode.innerText=d.modeStory;endlessMode.innerText=d.modeEndless;document.querySelector(".tipText").innerText=d.tipUnlockEndless;langBtn.querySelector("span:last-child").innerText=isCN?"CN":"EN";refreshUI();renderHotbar();refreshBookUI();refreshLevelInfo();}
langBtn.onclick = () => {isCN=!isCN;refreshLang();};
langBtn.ontouchstart = langBtn.onclick;
const MAP_W=4000,MAP_H=3000;
let camX=0,camY=0,score=0,gameOver=false,gameStart=false,autoAttack=false,showBook=false,showBackpack=false,selectBagIndex=-1,unlockMonster=new Set(),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=[],bullets=[],walls=[],zProj=[],fireBalls=[],groundItems=[],chests=[];
let hotbarItems=Array(8).fill(null),backpackItems=Array(81).fill(null),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:"giant",name:"巨人僵尸",hp:130,speed:0.9,color:"#900",desc:"红色大块头,血量中等,近战猛击"},{type:"range",name:"毒液僵尸",hp:45,speed:1.3,color:"#c4f",desc:"远程发射毒液"},{type:"boss",name:"暴君BOSS",hp:550,speed:0.5,color:"#b00",desc:"终极暴君,可召唤小弟、投掷火球"}];
let lastShot=0;
function refreshLevelInfo(){if(!gameStart)return;const d=isCN?langData.cn:langData.en;const cfg=levelConfig[curLevel];let modeText=gameMode==="story"?d.modeStory:d.modeEndless;if(gameMode==="story"){levelInfo.innerHTML=`${modeText}<br>${d.level}:${cfg.name}<br>${d.target}:${score}/${cfg.target}`;}else{levelInfo.innerHTML=`${modeText}<br>${d.score}:${score}`;}}
function refreshBookUI(){bookContent.innerHTML="";const d=isCN?langData.cn:langData.en;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");let name=isOpen?mon.name:"???";let desc=mon.type==="giant"?(isOpen?d.giantDesc:"尚未击杀,信息未知"):mon.type==="boss"?(isOpen?d.bossDesc:"尚未击杀,信息未知"):(isOpen?mon.desc:"尚未击杀,信息未知");div.innerHTML=`<div class="mon-name">${name}</div><div class="mon-desc">${desc}</div>`;bookContent.appendChild(div);});}
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 renderHotbar(){hotbar.innerHTML="";const cfg=levelConfig[curLevel];const unlockList=cfg.unlock;const d=isCN?langData.cn:langData.en;hotbarItems.forEach((itm,i)=>{let div=document.createElement("div");let isUnlock=unlockList.includes(i);div.className="slot "+(i<4?"weapon":"food")+(i==currentWeapon&&i<4&&isUnlock?" active":"");if(!isUnlock&&i<4)div.classList.add("lock");if(i<4){div.innerText=isCN?weaponLang[i].cn:weaponLang[i].en;}else{div.innerText=itm||d.empty;}let clickTimer=null;div.onclick=()=>{clickTimer=setTimeout(()=>{if(i<4){if(!unlockList.includes(i))return;currentWeapon=i;renderHotbar();refreshUI();return;}if(foodList[itm]){player.hunger=Math.min(player.maxHunger,player.hunger+foodList[itm]);hotbarItems[i]=null;refreshUI();renderHotbar();}},220);};div.ontouchstart = div.onclick;div.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(div);});}
function renderBackpack(){backpack.innerHTML="";const d=isCN?langData.cn:langData.en;backpackItems.forEach((itm,i)=>{let div=document.createElement("div");div.className="bp-slot"+(i===selectBagIndex?" selected":"");div.innerText=itm||d.empty;let clkT=null;div.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();};div.ontouchstart = div.onclick;div.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(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=[];let safeRadius=450;let cx=MAP_W/2,cy=MAP_H/2;for(let i=0;i<25;i++){let wx=rand(0,MAP_W),wy=rand(0,MAP_H),ww=rand(100,500),wh=40;let farX=(wx+ww)<(cx-safeRadius)||wx>(cx+safeRadius);let farY=(wy+wh)<(cy-safeRadius)||wy>(cy+safeRadius);if(farX||farY) walls.push({x:wx,y:wy,w:ww,h:wh});let hx=rand(0,MAP_W),hy=rand(0,MAP_H),hw=40,hh=rand(100,500);let farX2=(hx+hw)<(cx-safeRadius)||hx>(cx+safeRadius);let farY2=(hy+hh)<(cy-safeRadius)||hy>(cy+safeRadius);if(farX2||farY2) walls.push({x:hx,y:hy,w:hw,h:hh});}}
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(mx,my){let wx=mx+camX,wy=my+camY;for(let i=groundItems.length-1;i>=0;i--){let it=groundItems[i];if(dist(wx,wy,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 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=[];fireBalls=[];groundItems=[];chests=[];
hotbarItems=Array(8).fill(null);backpackItems=Array(81).fill(null);
const cfg=levelConfig[curLevel];currentWeapon=cfg.unlock[0];selectBagIndex=-1;
joyActive=false;jx=0;jy=0;keys.w=keys.a=keys.s=keys.d=keys.ctrl=false;
createWalls();initStartChest();refreshUI();renderHotbar();renderBackpack();refreshBookUI();refreshLevelInfo();
pauseMenu.style.display="none";winPanel.style.display="none";
}
function spawnZombie(){
if(!gameStart||gameOver||isPause)return;
if(zombies.length>35)return;
if(!bossSpawned&&score>=8){bossSpawned=true;let boss={type:"boss",size:55,speed:0.5,hp:550,color:"#b00",atk:1.8,spit:0,callCd:0,x:rand(0,MAP_W),y:-80};zombies.push(boss);unlockMonster.add("boss");refreshBookUI();return;}
let t=Math.floor(rand(0,100));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:"giant",size:34,speed:0.9,hp:130,color:"#900",atk:1.0,spit:0};
else z={type:"range",size:25,speed:1.3,hp:45,color:"#c4f",atk:0,spit:0};
let side=Math.floor(rand(0,4));
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;playSound("gun");let dx=tx-player.x,dy=ty-player.y;let 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],minD=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<minD){minD=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;
if(player.hunger>=player.maxHunger){if(player.hp<player.maxHp) player.hp+=0.012;}
else if(player.hunger<=0){player.hp-=0.08;}
player.hp=Math.max(0,Math.min(player.maxHp,player.hp));
let moving=false,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;}
walkCd++;runCd++;
if(moving&&!keys.ctrl&&walkCd>15){playSound("walk");walkCd=0;}
if(moving&&keys.ctrl&&runCd>12&&player.stamina>0){playSound("run");runCd=0;}
let spd=player.speed;
if(keys.ctrl&&player.stamina>0&&moving){spd=player.runSpeed;player.stamina-=0.5;}
else if(!moving&&player.stamina<100){player.stamina+=0.4;}
player.stamina=Math.max(0,Math.min(player.maxStamina,player.stamina));
let nx=player.x+mx*spd;
let ny=player.y+my*spd;
let canMove=true;
for(let w of walls){if(rectCollide(w,{x:nx,y:ny,size:player.size})){canMove=false;break;}}
if(canMove){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,ty=z.y+dy*z.speed;
let zCanMove=true;
for(let w of walls){if(rectCollide(w,{x:tx,y:ty,size:z.size})){zCanMove=false;break;}}
if(zCanMove){z.x=tx;z.y=ty;}
if(d<player.size+z.size&&z.type!=="range"){player.hp-=z.atk/20;}
if(z.type==="range"){z.spit++;if(z.spit>200&&d<450){z.spit=0;let 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"});}}
if(z.type==="boss"){z.callCd++;if(z.callCd>=300){z.callCd=0;zombies.push({type:"normal",size:24,speed:1.7,hp:40,color:"#f33",atk:0.4,spit:0,x:z.x+rand(-80,80),y:z.y+rand(-80,80)});}z.spit++;if(z.spit>180&&d<600){z.spit=0;let dd=dist(dx,dy,0,0);fireBalls.push({x:z.x,y:z.y,dx:dx/dd,dy:dy/dd,speed:4,size:10,col:"#ff2222"});}}
});
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;for(let w of walls)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;});
fireBalls=fireBalls.filter(fb=>{fb.x+=fb.dx*fb.speed;fb.y+=fb.dy*fb.speed;if(fb.x<0||fb.x>MAP_W||fb.y<0||fb.y>MAP_H)return false;for(let w of walls)if(rectCollide(w,fb))return false;if(dist(fb.x,fb.y,player.x,player.y)<player.size+fb.size){player.hp-=1.2;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;for(let w of walls)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){playSound("die");unlockMonster.add(z.type);refreshBookUI();score+=z.type==="boss"?35:1;if(gameMode==="story"){const target=levelConfig[curLevel].target;if(score>=target&&!levelPassed[curLevel]){levelPassed[curLevel]=true;setTimeout(showWin,600);}}return false;}}return true;});return !hit;});
if(player.hp<=0) gameOver=true;
autoShoot();refreshUI();refreshLevelInfo();
}
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 Shooter",cvs.width/2,120);
ctx.font="22px Arial";ctx.fillStyle="#fff";
ctx.fillText(d.keyInfo,cvs.width/2,180);
ctx.fillText(isCN?"左键射击|右键拾取|靠近自动拾取":"Left Shoot | Right Pick | Auto Collect",cvs.width/2,220);
ctx.fillText(isCN?"快捷栏:单击吃食物 双击丢弃":"Hotbar: Click Eat | Double Click Drop",cvs.width/2,260);
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="#fff";ctx.font="22px Arial";
ctx.fillText(d.keyInfo,cvs.width/2,280);
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);
ctx.strokeStyle="#333";ctx.lineWidth=1;
for(let x=0;x<MAP_W;x+=80){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,MAP_H);ctx.stroke();}
for(let y=0;y<MAP_H;y+=80){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(MAP_W,y);ctx.stroke();}
walls.forEach(w=>{ctx.fillStyle="#666";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();});
fireBalls.forEach(fb=>{ctx.fillStyle=fb.col;ctx.beginPath();ctx.arc(fb.x,fb.y,fb.size,0,Math.PI*2);ctx.fill();});
ctx.restore();
ctx.fillStyle="rgba(255,255,255,0.7)";ctx.font="15px Arial";ctx.textAlign="right";
ctx.fillText(d.keyInfo,cvs.width-12,cvs.height-12);
}
function loop(){update();draw();requestAnimationFrame(loop);}
let spawnTimer=setInterval(spawnZombie,800);
let chestTimer=setInterval(spawnChest,2200);
refreshLang();
renderHotbar();
loop();
document.addEventListener("keydown",e=>{
if(gameOver&&e.code==="Space"){backHomeAll();return;}
if(e.code==="Escape"){if(gameStart&&!gameOver)openPause();}
if(e.code==="KeyW")keys.w=true;if(e.code==="KeyA")keys.a=true;if(e.code==="KeyS")keys.s=true;if(e.code==="KeyD")keys.d=true;if(e.code==="ControlLeft")keys.ctrl=true;
if(e.code==="Digit1")currentWeapon=0;if(e.code==="Digit2")currentWeapon=1;if(e.code==="Digit3")currentWeapon=2;if(e.code==="Digit4")currentWeapon=3;
if(e.code==="KeyB"){showBackpack=!showBackpack;backpack.style.display=showBackpack?"grid":"none";renderBackpack();}
if(e.code==="KeyM"){showBook=!showBook;bookPanel.style.display=showBook?"block":"none";refreshBookUI();}
if(e.code==="KeyE"){for(let c of chests){if(dist(player.x,player.y,c.x,c.y)<70){openChest(c);}}}
renderHotbar();refreshUI();
});
document.addEventListener("keyup",e=>{if(e.code==="KeyW")keys.w=false;if(e.code==="KeyA")keys.a=false;if(e.code==="KeyS")keys.s=false;if(e.code==="KeyD")keys.d=false;if(e.code==="ControlLeft")keys.ctrl=false;});
cvs.addEventListener("mousedown",e=>{if(!gameStart){gameStart=true;resetGame();return;}if(gameOver)return;if(isPause)return;if(showBackpack||showBook)return;if(e.button===0)shoot(e.clientX+camX,e.clientY+camY);if(e.button===2)pickItem(e.clientX,e.clientY);});
cvs.addEventListener("touchstart",e=>{if(!gameStart){gameStart=true;resetGame();return;}if(gameOver||isPause||showBackpack||showBook)return;const touch=e.touches[0];const rect=cvs.getBoundingClientRect();const x=touch.clientX-rect.left+camX;const y=touch.clientY-rect.top+camY;shoot(x,y);e.preventDefault();});
cvs.addEventListener("contextmenu",e=>e.preventDefault());
autoBtn.onclick = () => {autoAttack=!autoAttack;autoBtn.style.background=autoAttack?"rgba(255,60,60,1)":"rgba(255,60,60,0.7)";};
autoBtn.ontouchstart = autoBtn.onclick;
joyWrap.addEventListener("touchstart",e=>{joyActive=true;let rect=joyWrap.getBoundingClientRect();let tx=e.touches[0].clientX-rect.left;let ty=e.touches[0].clientY-rect.top;jx=tx-70;jy=ty-70;let l=dist(jx,jy,0,0);if(l>maxJoy){jx=jx/l*maxJoy;jy=jy/l*maxJoy;}joyH.style.left=70+jx+"px";joyH.style.top=70+jy+"px";});
joyWrap.addEventListener("touchmove",e=>{let rect=joyWrap.getBoundingClientRect();let tx=e.touches[0].clientX-rect.left;let ty=e.touches[0].clientY-rect.top;jx=tx-70;jy=ty-70;let l=dist(jx,jy,0,0);if(l>maxJoy){jx=jx/l*maxJoy;jy=jy/l*maxJoy;}joyH.style.left=70+jx+"px";joyH.style.top=70+jy+"px";});
joyWrap.addEventListener("touchend",()=>{joyActive=false;jx=0;jy=0;joyH.style.left="50%";joyH.style.top="50%";});
btnBag.onclick = () => {showBackpack=!showBackpack;backpack.style.display=showBackpack?"grid":"none";renderBackpack();};
btnBag.ontouchstart = btnBag.onclick;
btnBook.onclick = () => {showBook=!showBook;bookPanel.style.display=showBook?"block":"none";refreshBookUI();};
btnBook.ontouchstart = btnBook.onclick;
closeBookBtn.onclick = () => {showBook=false;bookPanel.style.display="none";};
closeBookBtn.ontouchstart = closeBookBtn.onclick;
btnSprint.ontouchstart = () => {keys.ctrl=true;};
btnSprint.ontouchend = () => {keys.ctrl=false;};
btnPick.onclick = () => {autoPickItem();};
btnPick.ontouchstart = btnPick.onclick;
btnOpen.onclick = () => {for(let c of chests){if(dist(player.x,player.y,c.x,c.y)<70){openChest(c);}}};
btnOpen.ontouchstart = btnOpen.onclick;
</script>
</body>
</html>Game Source: 僵尸生存射击 正式版
Creator: SuperPhoenix65
Libraries: none
Complexity: complex (387 lines, 36.1 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.