del:移除了美西螈彩蛋
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled

This commit is contained in:
AxTps committed 2026-09-12 23:35:57 +08:00
1 parent ceda000578
commit ea80a98143
16 files changed
+75 -1374

No files matched your search

@@ -1,229 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>坦克动荡 - 战术版</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0f1e;display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:'Segoe UI','Microsoft YaHei',system-ui,sans-serif;overflow:hidden;background:radial-gradient(ellipse at 50% -20%,#1c2d52 0%,#0a0f1e 55%,#04060c 100%)}
body::before{content:'';position:fixed;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,0.02) 0 1px,transparent 1px 4px),repeating-linear-gradient(90deg,rgba(255,255,255,0.02) 0 1px,transparent 1px 4px)}
.game-container{position:relative;width:800px;height:600px;background:#0e1626;border-radius:18px;overflow:hidden;box-shadow:0 0 0 1px rgba(148,163,184,0.18),0 12px 60px rgba(0,0,0,0.85),0 0 50px rgba(56,189,248,0.14);user-select:none}
canvas{display:block;cursor:crosshair}
.ui-overlay{position:absolute;top:0;left:0;right:0;padding:6px 10px;display:flex;justify-content:space-between;align-items:flex-start;pointer-events:none;z-index:10}
.ui-badge{background:linear-gradient(135deg,rgba(15,23,42,0.85),rgba(15,23,42,0.45));color:#e2e8f0;padding:6px 14px;border-radius:20px;font-size:14px;font-weight:600;backdrop-filter:blur(10px);border:1px solid rgba(148,163,184,0.22);box-shadow:0 2px 14px rgba(0,0,0,0.4)}
.health-bar{width:130px;height:9px;background:rgba(255,255,255,0.1);border-radius:5px;overflow:hidden;margin-top:7px;box-shadow:inset 0 1px 3px rgba(0,0,0,0.6)}
.health-fill{height:100%;background:linear-gradient(90deg,#22c55e,#4ade80);border-radius:5px;transition:width 0.2s,background 0.2s;box-shadow:0 0 10px rgba(74,222,128,0.7)}
.screen{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at center,rgba(15,23,42,0.5) 0%,rgba(4,6,12,0.93) 100%);backdrop-filter:blur(12px);z-index:30;transition:opacity 0.35s;pointer-events:all}
.screen.hidden{opacity:0;pointer-events:none}
.screen h1{color:#fff;font-size:54px;margin:0 0 12px;letter-spacing:10px;font-weight:800;animation:titleGlow 2.2s ease-in-out infinite}
@keyframes titleGlow{0%,100%{text-shadow:0 0 14px rgba(74,222,128,0.7),0 0 45px rgba(34,197,94,0.4),0 2px 6px rgba(0,0,0,0.8)}50%{text-shadow:0 0 26px rgba(74,222,128,1),0 0 90px rgba(34,197,94,0.75),0 2px 6px rgba(0,0,0,0.8)}}
.screen p{color:#cbd5e1;font-size:16px;margin:0 0 30px;letter-spacing:1px}
.btn{background:linear-gradient(135deg,#22c55e,#059669);color:#fff;border:none;padding:15px 54px;font-size:18px;font-weight:800;border-radius:12px;cursor:pointer;transition:all 0.2s;letter-spacing:3px;box-shadow:0 4px 24px rgba(34,197,94,0.45),inset 0 1px 0 rgba(255,255,255,0.25)}
.btn:hover{transform:translateY(-3px) scale(1.04);box-shadow:0 10px 36px rgba(34,197,94,0.7),inset 0 1px 0 rgba(255,255,255,0.25);filter:brightness(1.1)}
.btn:active{transform:translateY(0) scale(0.98)}
.level-text{color:#fbbf24;font-size:22px;margin-bottom:12px;font-weight:800;text-shadow:0 0 14px rgba(251,191,36,0.7);letter-spacing:3px}
.stats{color:#9ca3af;font-size:14px;margin-bottom:22px}
.ammo-current{position:absolute;left:50%;top:6px;transform:translateX(-50%);z-index:16;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:rgba(10,15,30,0.75);border:2px solid rgba(74,222,128,0.5);border-radius:12px;box-shadow:0 0 16px rgba(74,222,128,0.3);pointer-events:none}
.ammo-current img{width:22px;height:22px;object-fit:contain}
.ammo-panel{position:absolute;bottom:14px;right:14px;z-index:15;display:flex;flex-direction:column-reverse;gap:3px;align-items:flex-end;pointer-events:none}
.ammo-row{display:flex;align-items:center;gap:5px;background:rgba(10,15,30,0.72);border:1px solid rgba(148,163,184,0.2);border-radius:7px;padding:2px 7px;font-size:11px;color:#e2e8f0;backdrop-filter:blur(6px);transition:border-color .12s,box-shadow .12s}
.ammo-row.active{border-color:rgba(74,222,128,0.65);box-shadow:0 0 10px rgba(74,222,128,0.3)}
.ammo-icon{width:16px;height:16px;object-fit:contain}
.ammo-name{color:#cbd5e1}
.ammo-count{font-weight:700;color:#fbbf24;min-width:26px;text-align:right}
.controls-hint{position:absolute;bottom:6px;left:6px;z-index:15;color:#cbd5e1;font-size:11px;pointer-events:none;background:rgba(10,15,30,0.72);padding:7px 14px;border-radius:10px;border:1px solid rgba(148,163,184,0.2);backdrop-filter:blur(6px);box-shadow:0 2px 10px rgba(0,0,0,0.35)}
.controls-hint kbd{background:rgba(15,23,42,0.9);padding:2px 7px;border-radius:5px;font-family:monospace;color:#d1d5db;border:1px solid rgba(148,163,184,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.5)}
.game-icon{width:76px;height:76px;margin-bottom:20px;filter:drop-shadow(0 0 18px rgba(74,222,128,0.7))}
.signature{position:absolute;bottom:16px;left:0;right:0;text-align:center;z-index:32;color:rgba(148,163,184,0.65);font-size:13px;letter-spacing:4px;pointer-events:none;font-style:italic;text-shadow:0 0 10px rgba(56,189,248,0.5)}
.credit{position:absolute;bottom:42px;left:0;right:0;text-align:center;z-index:32;color:rgba(148,163,184,0.5);font-size:12px;letter-spacing:1px;pointer-events:none}
#startScreen{background:rgba(0,0,0,0.88);backdrop-filter:blur(10px)}
#startScreen h1{font-size:44px;letter-spacing:3px;font-weight:700;animation:none;text-shadow:0 2px 10px rgba(0,0,0,0.5)}
#startScreen p{color:#aaa;font-size:15px;margin:0 0 28px;letter-spacing:0}
#startScreen .btn{background:#fff;color:#111;padding:14px 44px;font-size:17px;font-weight:700;border-radius:10px;box-shadow:none;letter-spacing:1px}
#startScreen .btn:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(255,255,255,0.18);filter:none}
#startScreen .btn:active{transform:translateY(0) scale(1)}
.reward-hint{color:#94a3b8;font-size:13px;margin-bottom:14px;letter-spacing:1px}
.reward-cards{display:flex;gap:14px;justify-content:center;flex-wrap:wrap;padding:0 20px;max-width:760px}
.reward-card{width:190px;background:linear-gradient(160deg,rgba(30,41,59,0.96),rgba(15,23,42,0.9));border:2px solid rgba(148,163,184,0.28);border-radius:14px;padding:16px 12px;cursor:pointer;text-align:center;transition:transform .15s,border-color .15s,box-shadow .15s}
.reward-card:hover{transform:translateY(-4px) scale(1.04);border-color:rgba(74,222,128,0.75);box-shadow:0 8px 30px rgba(74,222,128,0.3)}
.reward-card:active{transform:translateY(-1px) scale(0.98)}
.reward-card.selected{border-color:rgba(74,222,128,0.95);box-shadow:0 0 26px rgba(74,222,128,0.5),inset 0 0 12px rgba(74,222,128,0.15);transform:translateY(-4px) scale(1.04)}
.reward-confirm{margin-top:22px;font-size:16px;padding:12px 46px;letter-spacing:4px;opacity:0.45;cursor:not-allowed;filter:grayscale(0.4)}
.reward-confirm.enabled{opacity:1;cursor:pointer;filter:none}
.reward-confirm.enabled:hover{transform:translateY(-3px) scale(1.04);box-shadow:0 10px 36px rgba(34,197,94,0.7)}
.reward-icon{width:36px;height:36px;object-fit:contain;margin-bottom:8px;margin-left:auto;margin-right:auto}
.reward-name{color:#fff;font-weight:800;font-size:15px;margin-bottom:4px}
.reward-val{color:#fbbf24;font-weight:700;font-size:14px}
.reward-desc{color:#9ca3af;font-size:11px;margin-top:6px;line-height:1.45}</style>
</head>
<body>
<div class="game-container" id="gameBox">
<div class="ammo-current" id="ammoCurrent"><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNNiAxMkwyMiAxMkMyNSAxMiAyOCAxNCAyOCAxNkMyOCAxOCAyNSAyMCAyMiAyMEw2IDIwWiIgc3Ryb2tlPSIjNGFkZTgwIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTEwIDEyTDEwIDIwIiBzdHJva2U9IiM0YWRlODAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIwLjUiLz48L3N2Zz4=" alt="" /></div>
<div class="ammo-panel" id="ammoPanel"></div>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="ui-overlay">
<div><div class="ui-badge">关卡 <span id="levelNum">1</span></div><div class="health-bar"><div class="health-fill" id="hpBar" style="width:100%"></div></div></div>
<div class="ui-badge">剩余敌人: <span id="enemyCount">0</span></div>
</div>
<div class="screen" id="startScreen"><h1>坦克动荡</h1><p>WASD移动 · 鼠标瞄准 · 左键射击 · 子弹反弹</p><button class="btn" id="btnStart">开始游戏</button><div class="signature">— cyf112233</div><div class="credit">音乐由[乌鸦Producer]提供</div></div>
<div class="screen hidden" id="rewardScreen"><div class="level-text">关卡完成!选择一项强化</div><div class="reward-hint">先选中一张强化卡,再点击确定</div><div class="reward-cards" id="rewardCards"></div><button class="btn reward-confirm" id="rewardConfirm" disabled>确定</button></div>
<div class="screen hidden" id="levelScreen"><div class="level-text">关卡完成!</div><h1>第 <span id="nextLevel">2</span> 关</h1><p class="stats">敌人已进化 — 更聪明、更危险</p><button class="btn" id="btnNext">下一关</button></div>
<div class="screen hidden" id="overScreen"><h1>游戏结束</h1><p class="stats">你坚持到了第 <span id="finalLevel">1</span> 关</p><button class="btn" id="btnRestart">重新开始</button></div>
<div class="controls-hint"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> 移动 · 鼠标瞄准 · 左键射击 · 滚轮切换子弹 · <kbd>M</kbd> 静音</div>
</div>
<script>
// Audio Engine
class AudioEngine{
constructor(){this.ctx=null;this.enabled=true;this.external={};this.masterGain=null;this.bgmAudio=null;this.bgmRequested=false}
init(){if(this.ctx)return;const AudioContext=window.AudioContext||window.webkitAudioContext;this.ctx=new AudioContext();this.masterGain=this.ctx.createGain();this.masterGain.gain.value=0.35;this.masterGain.connect(this.ctx.destination);if(this.ctx.state==='suspended')this.ctx.resume().catch(()=>{});setTimeout(()=>this.loadExternal(),150)}
async loadExternal(){const files={shoot:'data:audio/mpeg;base64,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 truncated
toggle(){this.enabled=!this.enabled;if(!this.enabled&&this.masterGain)this.masterGain.gain.setTargetAtTime(0,this.ctx.currentTime,0.1);else if(this.enabled&&this.masterGain)this.masterGain.gain.setTargetAtTime(0.35,this.ctx.currentTime,0.1);if(this.enabled)this.startBGM();else this.stopBGM();return this.enabled}
_now(){return this.ctx?this.ctx.currentTime:0}
play(name){if(!this.enabled)return;if(name==='bgm')return;if(this.external[name]){const src=this.ctx.createBufferSource();src.buffer=this.external[name];const gain=this.ctx.createGain();gain.gain.value=name==='explosion'?0.35:0.45;src.connect(gain);gain.connect(this.masterGain);src.start();return}this.synth(name)}
synth(name){if(name==='shoot')this.synthShoot();else if(name==='explosion')this.synthExplosion();else if(name==='hitwall')this.synthHitWall();else if(name==='levelup')this.synthLevelUp()}
startBGM(){if(!this.enabled)return;this.bgmRequested=true;if(this.bgmAudio)this.playBGM()}
playBGM(){if(!this.enabled||!this.bgmAudio)return;const src=this.ctx.createBufferSource();src.buffer=this.bgmAudio;src.loop=true;const gain=this.ctx.createGain();gain.gain.value=0.25;src.connect(gain);gain.connect(this.masterGain);this.bgmSource=src;src.start()}
stopBGM(){this.bgmRequested=false;if(this.bgmSource){try{this.bgmSource.stop()}catch(e){}this.bgmSource=null}if(this.bgmInterval){clearInterval(this.bgmInterval);this.bgmInterval=null}}
synthShoot(){if(!this.ctx)return;const t=this._now();const osc=this.ctx.createOscillator();const gain=this.ctx.createGain();osc.type='square';osc.frequency.setValueAtTime(800,t);osc.frequency.exponentialRampToValueAtTime(150,t+0.12);gain.gain.setValueAtTime(0.12,t);gain.gain.exponentialRampToValueAtTime(0.001,t+0.12);osc.connect(gain);gain.connect(this.masterGain);osc.start(t);osc.stop(t+0.12)}
synthExplosion(){if(!this.ctx)return;const t=this._now();const bufferSize=this.ctx.sampleRate*0.3;const buffer=this.ctx.createBuffer(1,bufferSize,this.ctx.sampleRate);const data=buffer.getChannelData(0);for(let i=0;i<bufferSize;i++)data[i]=Math.random()*2-1;const noise=this.ctx.createBufferSource();noise.buffer=buffer;const gain=this.ctx.createGain();const filter=this.ctx.createBiquadFilter();filter.type='lowpass';filter.frequency.setValueAtTime(800,t);filter.frequency.exponentialRampToValueAtTime(100,t+0.3);gain.gain.setValueAtTime(0.2,t);gain.gain.exponentialRampToValueAtTime(0.001,t+0.3);noise.connect(filter);filter.connect(gain);gain.connect(this.masterGain);noise.start(t);noise.stop(t+0.3);const osc=this.ctx.createOscillator();const g2=this.ctx.createGain();osc.type='sawtooth';osc.frequency.setValueAtTime(120,t);osc.frequency.exponentialRampToValueAtTime(40,t+0.25);g2.gain.setValueAtTime(0.15,t);g2.gain.exponentialRampToValueAtTime(0.001,t+0.25);osc.connect(g2);g2.connect(this.masterGain);osc.start(t);osc.stop(t+0.25)}
synthHitWall(){if(!this.ctx)return;const t=this._now();const osc=this.ctx.createOscillator();const gain=this.ctx.createGain();osc.type='triangle';osc.frequency.setValueAtTime(600,t);osc.frequency.exponentialRampToValueAtTime(200,t+0.08);gain.gain.setValueAtTime(0.06,t);gain.gain.exponentialRampToValueAtTime(0.001,t+0.08);osc.connect(gain);gain.connect(this.masterGain);osc.start(t);osc.stop(t+0.08)}
synthLevelUp(){if(!this.ctx)return;const t=this._now();const notes=[523.25,659.25,783.99,1046.50];notes.forEach((freq,i)=>{const osc=this.ctx.createOscillator();const gain=this.ctx.createGain();osc.type='sine';osc.frequency.setValueAtTime(freq,t+i*0.12);gain.gain.setValueAtTime(0,t+i*0.12);gain.gain.linearRampToValueAtTime(0.1,t+i*0.12+0.03);gain.gain.exponentialRampToValueAtTime(0.001,t+i*0.12+0.25);osc.connect(gain);gain.connect(this.masterGain);osc.start(t+i*0.12);osc.stop(t+i*0.12+0.25)})}
}
const audio=new AudioEngine();
// Utils
function normalizeAngle(a){while(a>Math.PI)a-=Math.PI*2;while(a<-Math.PI)a+=Math.PI*2;return a}
function dist(x1,y1,x2,y2){return Math.sqrt((x1-x2)**2+(y1-y2)**2)}
function lineIntersectsRect(x1,y1,x2,y2,rx,ry,rw,rh){const minX=Math.min(x1,x2),maxX=Math.max(x1,x2);const minY=Math.min(y1,y2),maxY=Math.max(y1,y2);if(maxX<rx||minX>rx+rw||maxY<ry||minY>ry+rh)return false;const dx=x2-x1,dy=y2-y1;if(Math.abs(dx)<0.001&&Math.abs(dy)<0.001)return false;const left=(rx-x1)/dx,right=(rx+rw-x1)/dx;const top=(ry-y1)/dy,bottom=(ry+rh-y1)/dy;const tmin=Math.max(Math.min(left,right),Math.min(top,bottom));const tmax=Math.min(Math.max(left,right),Math.max(top,bottom));if(tmax<0||tmin>1||tmin>tmax)return false;return true}
function hasLineOfSight(x1,y1,x2,y2,walls){for(let w of walls){if(w.dead)continue;if(lineIntersectsRect(x1,y1,x2,y2,w.x,w.y,w.w,w.h))return false}return true}
function raycast(x,y,angle,maxDist,walls){for(let d=0;d<maxDist;d+=4){const cx=x+Math.cos(angle)*d;const cy=y+Math.sin(angle)*d;for(let w of walls){if(w.dead)continue;if(cx>w.x&&cx<w.x+w.w&&cy>w.y&&cy<w.y+w.h)return{hit:true,x:cx,y:cy,dist:d,wall:w}}}return{hit:false,dist:maxDist,wall:null}}
function findPath(sx,sy,tx,ty,walls){if(tx<1||tx>COLS-2||ty<1||ty>ROWS-2)return null;const cost=[];const brick=[];for(let r=0;r<ROWS;r++){cost[r]=[];brick[r]=[];for(let c=0;c<COLS;c++){cost[r][c]=1;brick[r][c]=false}}for(let w of walls){if(!w.dead&&w.c>=0&&w.c<COLS&&w.r>=0&&w.r<ROWS){if(w.type==='steel'){cost[w.r][w.c]=999}else{cost[w.r][w.c]=1;brick[w.r][w.c]=true}}}if(cost[sy][sx]>=999||cost[ty][tx]>=999)return null;const dist=[];for(let r=0;r<ROWS;r++){dist[r]=[];for(let c=0;c<COLS;c++)dist[r][c]=Infinity}dist[sy][sx]=0;const prev={};const key=(c,r)=>c+','+r;prev[key(sx,sy)]=null;const open=[[0,sx,sy]];const dirs=[[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];while(open.length){let mi=0;for(let i=1;i<open.length;i++)if(open[i][0]<open[mi][0])mi=i;const cur=open.splice(mi,1)[0];const d=cur[0],c=cur[1],r=cur[2];if(d>dist[r][c])continue;if(c===tx&&r===ty){const path=[];let p=key(c,r);while(p!==null){const parts=p.split(',');const pc=+parts[0],pr=+parts[1];path.unshift([pc*TILE+TILE/2,pr*TILE+TILE/2,brick[pr][pc]]);p=prev[p]}path.shift();return path.length>0?path:null}for(let dd of dirs){const nc=c+dd[0],nr=r+dd[1];if(nc<1||nc>COLS-2||nr<1||nr>ROWS-2)continue;if(cost[nr][nc]>=999)continue;if(Math.abs(dd[0])===1&&Math.abs(dd[1])===1&&(cost[nr][c]>=999||cost[r][nc]>=999))continue;const nd=d+cost[nr][nc];if(nd<dist[nr][nc]){dist[nr][nc]=nd;prev[key(nc,nr)]=key(c,r);open.push([nd,nc,nr])}}}return null}
// Input
const keys={};
window.addEventListener('keydown',e=>{keys[e.code]=true;if(e.code==='KeyM'){const on=audio.toggle();document.querySelector('.controls-hint').innerHTML=on?'<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> 移动 · 鼠标瞄准 · 左键射击 · <kbd>M</kbd> 静音':'<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> 移动 · 鼠标瞄准 · 左键射击 · <kbd>M</kbd> 开启音效'}if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code))e.preventDefault()});
window.addEventListener('keyup',e=>{keys[e.code]=false});
// Game Constants
const canvas=document.getElementById('gameCanvas');
const ctx=canvas.getContext('2d');
const TILE=40,COLS=20,ROWS=15;
const AIState={PATROL:0,HUNT:1,SEARCH:2,TAKECOVER:3,PEEK:4,RETREAT:5,STRAFE:6};
class Particle{
constructor(x,y,color,speed,life){this.x=x;this.y=y;this.vx=(Math.random()-0.5)*speed;this.vy=(Math.random()-0.5)*speed;this.color=color;this.life=life;this.maxLife=life;this.size=Math.random()*3+1}
update(){this.x+=this.vx;this.y+=this.vy;this.life--;this.size*=0.96}
draw(ctx){ctx.globalAlpha=Math.max(0,this.life/this.maxLife);ctx.fillStyle=this.color;ctx.beginPath();ctx.arc(this.x,this.y,Math.max(0.1,this.size),0,Math.PI*2);ctx.fill();ctx.globalAlpha=1}
}
class Wall{
constructor(c,r,type){this.c=c;this.r=r;this.x=c*TILE;this.y=r*TILE;this.w=TILE;this.h=TILE;this.type=type;this.health=type==='brick'?2:9999;this.maxHealth=this.health;this.dead=false}
hit(){if(this.type==='brick'){this.health--;if(this.health<=0){this.dead=true;return true}}return false}
draw(ctx){if(this.type==='steel'){ctx.fillStyle='#4a5568';ctx.fillRect(this.x+1,this.y+1,this.w-2,this.h-2);ctx.strokeStyle='#718096';ctx.lineWidth=1;ctx.strokeRect(this.x+3,this.y+3,this.w-6,this.h-6);ctx.fillStyle='#2d3748';ctx.fillRect(this.x+10,this.y+10,this.w-20,this.h-20);ctx.fillStyle='#718096';[[6,6],[26,6],[6,26],[26,26]].forEach(([rx,ry])=>{ctx.beginPath();ctx.arc(this.x+rx,this.y+ry,2,0,Math.PI*2);ctx.fill()})}else{const dmg=1-this.health/this.maxHealth;ctx.fillStyle=dmg>0.5?'#8b4513':'#cd853f';ctx.fillRect(this.x+1,this.y+1,this.w-2,this.h-2);ctx.strokeStyle=dmg>0.5?'#654321':'#8b4513';ctx.lineWidth=1;for(let i=1;i<4;i++){ctx.beginPath();ctx.moveTo(this.x+i*10,this.y+1);ctx.lineTo(this.x+i*10,this.y+this.h-1);ctx.stroke()}for(let i=1;i<3;i++){ctx.beginPath();ctx.moveTo(this.x+1,this.y+i*13);ctx.lineTo(this.x+this.w-1,this.y+i*13);ctx.stroke()}}}
}
class Bullet{
constructor(x,y,angle,owner,type){this.x=x;this.y=y;this.speed=5.5;this.vx=Math.cos(angle)*this.speed;this.vy=Math.sin(angle)*this.speed;this.owner=owner;this.isPlayer=owner.isPlayer;this.radius=3;this.bounces=0;this.maxBounces=type==='bounce'?99:3+Math.floor(Math.random()*3);this.dead=false;this.trail=[];this.age=0;this.type=type||'normal';this.damage=type==='kill'?99:this.isPlayer?(owner.damage||1):1}
update(walls,tanks){if(this.dead)return;this.age++;this.trail.push({x:this.x,y:this.y});if(this.trail.length>10)this.trail.shift();let nextX=this.x+this.vx;let nextY=this.y+this.vy;for(let w of walls){if(w.dead)continue;if(nextX+this.radius>w.x&&nextX-this.radius<w.x+w.w&&this.y+this.radius>w.y&&this.y-this.radius<w.y+w.h){this.vx=-this.vx;nextX=this.x+this.vx;this.bounces++;const destroyed=w.hit();if(destroyed){game.addParticles(w.x+w.w/2,w.y+w.h/2,'#cd853f',10,20);game.walls=game.walls.filter(wall=>!wall.dead);audio.play('explosion');game.addNoise(w.x+w.w/2,w.y+w.h/2,350)}else{game.addParticles(this.x,this.y,'#deb887',3,6);audio.play('hitwall')}}if(this.x+this.radius>w.x&&this.x-this.radius<w.x+w.w&&nextY+this.radius>w.y&&nextY-this.radius<w.y+w.h){this.vy=-this.vy;nextY=this.y+this.vy;this.bounces++;const destroyed=w.hit();if(destroyed){game.addParticles(w.x+w.w/2,w.y+w.h/2,'#cd853f',10,20);game.walls=game.walls.filter(wall=>!wall.dead);audio.play('explosion');game.addNoise(w.x+w.w/2,w.y+w.h/2,350)}else{game.addParticles(this.x,this.y,'#deb887',3,6);audio.play('hitwall')}}}this.x=nextX;this.y=nextY;if(this.x<0||this.x>canvas.width||this.y<0||this.y>canvas.height)this.dead=true;if(this.bounces>this.maxBounces)this.dead=true;if(this.age>400)this.dead=true;for(let t of tanks){if(t.dead)continue;if(t.isPlayer===this.isPlayer)continue;if(t===this.owner&&this.age<10)continue;const d=Math.sqrt((this.x-t.x)**2+(this.y-t.y)**2);if(d<t.radius+this.radius+2){t.takeDamage(this.damage,this.owner);this.dead=true;game.addParticles(this.x,this.y,'#ff4444',10,18);if(t.isPlayer)game.shake=10;break}}}
draw(ctx){ctx.save();let col=this.isPlayer?'#4ade80':'#f87171';if(this.type==='kill')col='#c084fc';else if(this.type==='bounce')col='#22d3ee';ctx.shadowColor=col;ctx.shadowBlur=8;ctx.fillStyle=col;ctx.beginPath();ctx.arc(this.x,this.y,this.radius+1,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.strokeStyle=this.type==='kill'?'rgba(192,132,252,0.5)':this.type==='bounce'?'rgba(34,211,238,0.5)':(this.isPlayer?'rgba(74,222,128,0.4)':'rgba(248,113,113,0.4)');ctx.lineWidth=2;ctx.beginPath();for(let i=0;i<this.trail.length;i++){if(i===0)ctx.moveTo(this.trail[i].x,this.trail[i].y);else ctx.lineTo(this.trail[i].x,this.trail[i].y)}ctx.stroke();ctx.restore()}
}
class Tank{
constructor(x,y,isPlayer,evolution,stats){this.x=x;this.y=y;this.vx=0;this.vy=0;this.radius=14;this.isPlayer=isPlayer;this.angle=0;this.turretAngle=0;this.evo=evolution||{level:1,speedMul:1,reactionMul:1,dodgeSkill:0,peekSkill:0,accuracy:0};this.speed=isPlayer?2.2:Math.min(2.4,1.6+this.evo.speedMul*0.16);if(isPlayer&&stats){this.health=stats.maxHp;this.maxHealth=stats.maxHp;this.maxCooldown=Math.max(6,20-stats.fireRate);this.damage=stats.damage;this.extraLives=stats.extraLives;this.ammo=stats.ammo;this.selectedType=stats.selected}else{this.health=isPlayer?10:(4+Math.floor(this.evo.level*0.5));this.maxHealth=this.health;this.maxCooldown=isPlayer?20:Math.max(14,32-this.evo.reactionMul*3);this.damage=1;this.extraLives=0;this.ammo=null;this.selectedType='normal'}this.invincible=0;this.dead=false;this.shootCooldown=0;this.color=isPlayer?'#4ade80':'#f87171';this.activeBullets=0;this.maxActiveBullets=isPlayer?(stats&&stats.maxBullets?stats.maxBullets:8):12;this.state=AIState.PATROL;this.stateTimer=0;this.patrolTarget=null;this.lastSeenPlayerPos=null;this.lastSeenTime=0;this.alerted=false;this.alertTimer=0;this.lastKnownPlayerPos=null;this.commsTimer=0;this.coverWall=null;this.coverSide=0;this.peekTimer=0;this.dodgeDir=null;this.dodgeTimer=0;this.searchOffset=null;this.dodgeTarget=null;this.noiseTarget=null;this.tension=0;this.suspectBullet=null;this.suspectPos=null;this.suspectTimer=0;this.hesitateTimer=0;this.path=null;this.pathIdx=0;this.pathTimer=15;this.idealMinDist=140+this.evo.level*8;this.idealMaxDist=260+this.evo.level*10}
setState(s,duration){this.state=s;this.stateTimer=duration||90;if(s===AIState.PEEK)this.peekTimer=20+Math.random()*25}
canShoot(){return this.activeBullets<this.maxActiveBullets&&this.shootCooldown<=0}
takeDamage(dmg,attacker){if(this.invincible>0)return;this.health-=dmg;this.tension=Math.min(1,this.tension+0.2);if(!this.isPlayer&&attacker){this.alerted=true;this.alertTimer=400+this.evo.reactionMul*40;this.lastSeenPlayerPos={x:attacker.x,y:attacker.y};this.lastSeenTime=game.frame;if(this.state!==AIState.TAKECOVER&&this.state!==AIState.PEEK){this.findCover(game.walls,attacker)}}if(this.health<=0){this.dead=true;game.addParticles(this.x,this.y,this.color,30,50);game.addParticles(this.x,this.y,'#ffaa00',15,30);audio.play('explosion');game.addNoise(this.x,this.y,350)}}
canMoveTo(nx,ny,walls){for(let w of walls){if(w.dead)continue;if(nx+this.radius>w.x&&nx-this.radius<w.x+w.w&&ny+this.radius>w.y&&ny-this.radius<w.y+w.h)return false}return nx>this.radius&&nx<canvas.width-this.radius&&ny>this.radius&&ny<canvas.height-this.radius}
findCover(walls,threat){let bestWall=null,bestScore=-Infinity;for(let w of walls){if(w.dead)continue;const cx=w.x+w.w/2,cy=w.y+w.h/2;const toThreat=Math.atan2(threat.y-cy,threat.x-cx);const coverX=cx+Math.cos(toThreat+Math.PI)*50;const coverY=cy+Math.sin(toThreat+Math.PI)*50;if(coverX<30||coverX>canvas.width-30||coverY<30||coverY>canvas.height-30)continue;if(!this.canMoveTo(coverX,coverY,walls))continue;const blocksLOS=!hasLineOfSight(coverX,coverY,threat.x,threat.y,walls);if(!blocksLOS)continue;let score=200-dist(this.x,this.y,coverX,coverY)*0.3;if(w.type==='steel')score+=50;if(score>bestScore){bestScore=score;bestWall=w}}if(bestWall){this.coverWall=bestWall;this.setState(AIState.TAKECOVER,120)}}
getCoverPosition(threat){if(!this.coverWall)return null;const cx=this.coverWall.x+this.coverWall.w/2;const cy=this.coverWall.y+this.coverWall.h/2;const toThreat=Math.atan2(threat.y-cy,threat.x-cx);return{x:cx+Math.cos(toThreat+Math.PI)*45,y:cy+Math.sin(toThreat+Math.PI)*45}}
getPeekPosition(threat){if(!this.coverWall)return null;const cx=this.coverWall.x+this.coverWall.w/2;const cy=this.coverWall.y+this.coverWall.h/2;const toThreat=Math.atan2(threat.y-cy,threat.x-cx);const side=this.coverSide||(Math.random()<0.5?-1:1);this.coverSide=side;return{x:cx+Math.cos(toThreat+Math.PI)*25+Math.cos(toThreat+side*Math.PI/2)*35,y:cy+Math.sin(toThreat+Math.PI)*25+Math.sin(toThreat+side*Math.PI/2)*35}}
getThreateningBullets(bullets){const threats=[];for(let b of bullets){if(b.dead||b.isPlayer===this.isPlayer)continue;const dx=b.x-this.x,dy=b.y-this.y;const distToBullet=Math.sqrt(dx*dx+dy*dy);if(distToBullet>350)continue;const bulletSpeed=Math.sqrt(b.vx*b.vx+b.vy*b.vy);if(bulletSpeed<0.1)continue;const toTankX=this.x-b.x,toTankY=this.y-b.y;const dot=toTankX*b.vx+toTankY*b.vy;if(dot<-10)continue;const timeToImpact=distToBullet/bulletSpeed;if(timeToImpact>95||timeToImpact<0)continue;const nvx=b.vx/bulletSpeed,nvy=b.vy/bulletSpeed;const proj=dx*nvx+dy*nvy;const perpX=dx-nvx*proj;const perpY=dy-nvy*proj;const perpDist=Math.sqrt(perpX*perpX+perpY*perpY);if(perpDist<this.radius+55){threats.push({bullet:b,perpDist,timeToImpact,dodgeAngle:Math.atan2(perpY,perpX),urgency:(70-timeToImpact)/70*0.6+(40-perpDist)/40*0.4})}}threats.sort((a,b)=>b.urgency-a.urgency);return threats}
getBestMoveDir(targetX,targetY,walls,bullets,allTanks,player){const samples=32;let bestScore=-Infinity,bestDir=this.angle;const toTarget=Math.atan2(targetY-this.y,targetX-this.x);const toTargetDist=dist(this.x,this.y,targetX,targetY);const threats=this.getThreateningBullets(bullets);const playerDist=player?dist(this.x,this.y,player.x,player.y):999;const tooClose=playerDist<this.idealMinDist;const tooFar=playerDist>this.idealMaxDist;for(let i=0;i<samples;i++){const dir=(i/samples)*Math.PI*2;const step=this.speed*3;const nx=this.x+Math.cos(dir)*step;const ny=this.y+Math.sin(dir)*step;if(!this.canMoveTo(nx,ny,walls))continue;let score=0;const angleDiff=Math.abs(normalizeAngle(dir-toTarget));score+=(1-angleDiff/Math.PI)*40;if(player&&!player.dead){const newDist=dist(nx,ny,player.x,player.y);if(tooClose)score+=(newDist-playerDist)*3;else if(tooFar)score+=(playerDist-newDist)*3;else{const toPlayer=Math.atan2(player.y-this.y,player.x-this.x);const lateralDiff=Math.abs(normalizeAngle(dir-(toPlayer+Math.PI/2)));score+=(1-lateralDiff/Math.PI)*30}}for(let t of threats){const b=t.bullet;const futureBulletX=b.x+b.vx*t.timeToImpact*0.5;const futureBulletY=b.y+b.vy*t.timeToImpact*0.5;const toFutureBullet=Math.atan2(futureBulletY-ny,futureBulletX-nx);const dodgeDiff=Math.abs(normalizeAngle(dir-t.dodgeAngle));const awayFromBullet=Math.abs(normalizeAngle(dir-toFutureBullet));score+=(1-dodgeDiff/Math.PI)*120*t.urgency;score+=(awayFromBullet/Math.PI)*60*t.urgency}const fvx=nx+Math.cos(dir)*35;const fvy=ny+Math.sin(dir)*35;for(let w of walls){if(w.dead)continue;if(fvx+this.radius>w.x&&fvx-this.radius<w.x+w.w&&fvy+this.radius>w.y&&fvy-this.radius<w.y+w.h)score-=100}for(let t of allTanks){if(t!==this&&!t.isPlayer&&!t.dead){const d=dist(nx,ny,t.x,t.y);if(d<50)score-=40;else if(d<100)score-=10;else if(d<180)score+=5}}let wallProximity=0;for(let w of walls){if(w.dead)continue;const d=dist(nx,ny,w.x+w.w/2,w.y+w.h/2);if(d<45)wallProximity+=(45-d)}score-=wallProximity*0.4;const inertiaDiff=Math.abs(normalizeAngle(dir-this.angle));score+=(1-inertiaDiff/Math.PI)*6;if(score>bestScore){bestScore=score;bestDir=dir}}return bestDir}
update(walls,player,allTanks,bullets){if(this.dead)return;if(this.shootCooldown>0)this.shootCooldown--;if(this.alertTimer>0){this.alertTimer--;if(this.alertTimer<=0)this.alerted=false}if(this.commsTimer>0)this.commsTimer--;if(this.dodgeTimer>0)this.dodgeTimer--;if(this.peekTimer>0)this.peekTimer--;if(this.pathTimer>0)this.pathTimer--;if(this.isPlayer){this.updatePlayer(walls);return}this.updateAI(walls,player,allTanks,bullets)}
updatePlayer(walls){let vx=0,vy=0;if(keys['KeyW']||keys['ArrowUp'])vy-=1;if(keys['KeyS']||keys['ArrowDown'])vy+=1;if(keys['KeyA']||keys['ArrowLeft'])vx-=1;if(keys['KeyD']||keys['ArrowRight'])vx+=1;const px=this.x,py=this.y;if(vx!==0||vy!==0){const len=Math.sqrt(vx*vx+vy*vy);vx/=len;vy/=len;const nx=this.x+vx*this.speed;const ny=this.y+vy*this.speed;if(this.canMoveTo(nx,this.y,walls))this.x=nx;if(this.canMoveTo(this.x,ny,walls))this.y=ny;this.angle=Math.atan2(vy,vx)}this.vx=this.x-px;this.vy=this.y-py;this.turretAngle=Math.atan2(game.mouseY-this.y,game.mouseX-this.x)}
updateAI(walls,player,allTanks,bullets){const seesPlayer=player&&!player.dead&&hasLineOfSight(this.x,this.y,player.x,player.y,walls);const playerDist=player?dist(this.x,this.y,player.x,player.y):9999;const latestNoise=game.lastNoise;if(latestNoise&&!this.alerted&&!seesPlayer&&game.frame-latestNoise.frame<90&&(this.state===AIState.PATROL||this.state===AIState.SEARCH)){const nd=dist(this.x,this.y,latestNoise.x,latestNoise.y);if(nd<latestNoise.radius&&this.noiseTarget!==latestNoise){const loud=Math.max(0,1-nd/latestNoise.radius);if(loud>=0.2)this.tension=Math.min(1,this.tension+0.15+loud*0.3);if(this.tension>=0.35){this.noiseTarget=latestNoise;this.lastSeenPlayerPos={x:latestNoise.x,y:latestNoise.y};this.lastSeenTime=game.frame;this.setState(AIState.SEARCH,150)}}}if(this.noiseTarget&&!seesPlayer&&dist(this.x,this.y,this.noiseTarget.x,this.noiseTarget.y)<60){this.tension=Math.max(0,this.tension-0.1);this.noiseTarget=null;this.lastSeenPlayerPos=null}const friends=allTanks.filter(t=>t!==this&&!t.isPlayer&&!t.dead);if(this.commsTimer<=0){for(let other of friends){if(hasLineOfSight(this.x,this.y,other.x,other.y,walls)){if(this.lastSeenPlayerPos&&(!other.lastSeenPlayerPos||this.lastSeenTime>other.lastSeenTime)){other.lastSeenPlayerPos={x:this.lastSeenPlayerPos.x,y:this.lastSeenPlayerPos.y};other.lastSeenTime=this.lastSeenTime;other.alerted=true;other.alertTimer=Math.max(other.alertTimer,120)}if(other.lastSeenPlayerPos&&(!this.lastSeenPlayerPos||other.lastSeenTime>this.lastSeenTime)){this.lastSeenPlayerPos={x:other.lastSeenPlayerPos.x,y:other.lastSeenPlayerPos.y};this.lastSeenTime=other.lastSeenTime;this.alerted=true}}}this.commsTimer=10}if(seesPlayer){this.lastSeenPlayerPos={x:player.x,y:player.y};this.lastSeenTime=game.frame;this.lastKnownPlayerPos={x:player.x,y:player.y}}const threats=this.getThreateningBullets(bullets);const inDanger=threats.length>0;if(inDanger){const t=threats[0];if(this.dodgeTarget!==t.bullet){this.dodgeDir=t.dodgeAngle+(Math.random()-0.5)*0.4;this.dodgeTarget=t.bullet}this.dodgeTimer=12}else{this.dodgeTarget=null;if(this.dodgeTimer>0)this.dodgeTimer--}if(this.hesitateTimer>0)this.hesitateTimer--;if(this.suspectTimer>0){this.suspectTimer--;if(this.suspectTimer<=0){this.suspectBullet=null;this.suspectPos=null}}let nearBulletCount=0,suspectBullet=null,suspectD=9999;for(let b of bullets){if(b.dead||!b.isPlayer)continue;const bd=dist(this.x,this.y,b.x,b.y);if(bd<400)nearBulletCount++;if(bd<350&&b.bounces<2&&bd<suspectD){suspectD=bd;suspectBullet=b}}if(seesPlayer){this.tension=Math.min(1,this.tension+0.5)}else if(nearBulletCount>0){this.tension=Math.min(1,this.tension+0.35)}else{this.tension=Math.max(0,this.tension-0.001)}if(suspectBullet){if(!suspectBullet.seenBySet)suspectBullet.seenBySet=new Set();suspectBullet.seenBySet.add(this);if(suspectBullet!==this.suspectBullet){this.suspectBullet=suspectBullet;const spd=Math.sqrt(suspectBullet.vx**2+suspectBullet.vy**2)||1;const gx=Math.max(30,Math.min(canvas.width-30,suspectBullet.x-(suspectBullet.vx/spd)*120));const gy=Math.max(30,Math.min(canvas.height-30,suspectBullet.y-(suspectBullet.vy/spd)*120));this.suspectPos={x:gx,y:gy};this.suspectTimer=120;const witnesses=suspectBullet.seenBySet.size;const dense=nearBulletCount>=2;const willing=(0.2+0.1*witnesses)*(1+this.tension);if(!dense&&!seesPlayer&&!inDanger&&willing>0.28&&(this.state===AIState.PATROL||this.state===AIState.SEARCH)){this.lastSeenPlayerPos={x:gx,y:gy};this.lastSeenTime=game.frame;this.setState(AIState.SEARCH,150);this.suspectTimer=90+30*witnesses;this.hesitateTimer=(50+Math.random()*50)/(0.4+0.6*witnesses)}}if(this.suspectPos&&!seesPlayer&&dist(this.x,this.y,this.suspectPos.x,this.suspectPos.y)<60){this.tension=Math.max(0,this.tension-0.1);this.suspectBullet=null;this.suspectPos=null;this.suspectTimer=0;this.lastSeenPlayerPos=null}}if(friends.length>0){let gfN=0,gfT=0,bt=null;for(let t of friends){if(t.dead)continue;const fd=dist(this.x,this.y,t.x,t.y);if(fd<260){gfN++;gfT+=t.tension;if(t.suspectPos&&t.suspectTimer>0&&(!bt||t.suspectTimer>bt.age))bt={x:t.suspectPos.x,y:t.suspectPos.y,age:t.suspectTimer};if(t.noiseTarget){const na=90-(game.frame-t.noiseTarget.frame);if(na>0&&(!bt||na>bt.age))bt={x:t.noiseTarget.x,y:t.noiseTarget.y,age:na}}}}if(gfN>0)this.tension=Math.min(1,this.tension*0.6+gfT/gfN*0.4);if(bt&&!seesPlayer&&(this.state===AIState.PATROL||this.state===AIState.SEARCH)&&bt.age>this.suspectTimer){this.suspectPos={x:bt.x,y:bt.y};this.suspectTimer=bt.age;this.lastSeenPlayerPos={x:bt.x,y:bt.y};this.lastSeenTime=game.frame;this.setState(AIState.SEARCH,150)}}this.stateTimer--;let targetX=this.x,targetY=this.y;let aimX=null,aimY=null;let shouldShoot=false;if(seesPlayer&&(this.state===AIState.PATROL||this.state===AIState.SEARCH)){this.setState(AIState.HUNT,10)}switch(this.state){case AIState.PATROL:if(seesPlayer){this.setState(AIState.HUNT,10);break}else if(this.alerted&&this.lastSeenPlayerPos){if((game.frame-this.lastSeenTime)<45){this.setState(AIState.HUNT,20)}else this.setState(AIStLine truncated
shoot(){if(!this.canShoot())return;this.shootCooldown=this.maxCooldown;this.activeBullets++;const bx=this.x+Math.cos(this.turretAngle)*22;const by=this.y+Math.sin(this.turretAngle)*22;let proj=null;let useType=this.isPlayer&&this.ammo?this.selectedType:'normal';if(useType!=='normal'&&(!this.ammo||this.ammo[useType]<=0)){useType='normal';if(this.isPlayer)this.selectedType='normal'}if(useType!=='normal'&&this.ammo){this.ammo[useType]--;if(this.isPlayer)game.updateAmmoHud()}if(useType==='kill')proj=new Bullet(bx,by,this.turretAngle,this,'kill');else if(useType==='bounce')proj=new Bullet(bx,by,this.turretAngle,this,'bounce');else if(useType==='mine')game.mines.push(new Mine(this.x,this.y));else if(useType==='missile')proj=new Missile(bx,by,this,game.tanks);else proj=new Bullet(bx,by,this.turretAngle,this,'normal');if(proj){proj.onDeath=()=>{if(this.activeBullets>0)this.activeBullets--};game.bullets.push(proj)}else{if(this.activeBullets>0)this.activeBullets--}game.addParticles(bx,by,'#fbbf24',3,5);audio.play('shoot');game.addNoise(bx,by,200);if(this.isPlayer)game.updateAmmoHud()}
draw(ctx){if(this.dead)return;ctx.save();ctx.translate(this.x,this.y);ctx.rotate(this.angle);ctx.shadowColor=this.color;ctx.shadowBlur=10;ctx.fillStyle=this.color;ctx.fillRect(-12,-10,24,20);ctx.strokeStyle='rgba(0,0,0,0.45)';ctx.lineWidth=2;ctx.strokeRect(-12,-10,24,20);ctx.shadowBlur=0;ctx.fillStyle='#222';ctx.fillRect(-14,-12,4,24);ctx.fillRect(10,-12,4,24);ctx.fillStyle='rgba(255,255,255,0.18)';ctx.fillRect(-8,-6,16,3);ctx.restore();ctx.save();ctx.translate(this.x,this.y);ctx.rotate(this.turretAngle);ctx.shadowColor=this.isPlayer?'#4ade80':'#f87171';ctx.shadowBlur=8;ctx.fillStyle=this.isPlayer?'#22c55e':'#dc2626';ctx.beginPath();ctx.arc(0,0,8,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='#111';ctx.fillRect(0,-3,22,6);ctx.restore();if(!this.isPlayer){const hw=26;ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(this.x-hw/2,this.y-26,hw,5);ctx.fillStyle=this.health>1?'#f87171':'#ef4444';ctx.fillRect(this.x-hw/2,this.y-26,hw*(this.health/this.maxHealth),5);const labels=['巡逻','追击','搜索','掩体','探头','撤退','侧移'];const colors=['#888','#ff4444','#fbbf24','#60a5fa','#34d399','#a78bfa','#fb923c'];ctx.fillStyle=colors[this.state]||'#888';ctx.font='10px sans-serif';ctx.textAlign='center';ctx.fillText(labels[this.state]||'',this.x,this.y-30)}}
}
class Mine{
constructor(x,y){this.x=x;this.y=y;this.radius=9;this.armTimer=20;this.armed=false;this.dead=false;this.blink=0}
update(walls,tanks){if(this.dead)return;if(this.armTimer>0){this.armTimer--;return}this.armed=true;this.blink++;for(let t of tanks){if(t.isPlayer||t.dead)continue;const d=Math.sqrt((this.x-t.x)**2+(this.y-t.y)**2);if(d<t.radius+12){this.dead=true;this.explode(t);break}}}
explode(t){t.takeDamage(5,game.player);game.addParticles(this.x,this.y,'#ff9900',18,30);game.addParticles(this.x,this.y,'#ff4444',12,18);game.addParticles(this.x,this.y,'#ffffff',8,8);game.shake=Math.max(game.shake,8);audio.play('explosion');game.addNoise(this.x,this.y,320)}
draw(ctx){ctx.save();if(this.armed&&this.blink%12<6)ctx.globalAlpha=0.5;ctx.fillStyle='#1f2937';ctx.beginPath();ctx.arc(this.x,this.y,this.radius,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#f87171';ctx.lineWidth=2;ctx.beginPath();ctx.arc(this.x,this.y,this.radius,0,Math.PI*2);ctx.stroke();ctx.fillStyle='#f87171';for(let i=0;i<6;i++){const a=i/6*Math.PI*2;ctx.beginPath();ctx.arc(this.x+Math.cos(a)*this.radius,this.y+Math.sin(a)*this.radius,2,0,Math.PI*2);ctx.fill()}ctx.restore()}
}
class Missile{
constructor(x,y,owner,tanks){this.x=x;this.y=y;this.owner=owner;this.tanks=tanks;this.speed=4.8;this.radius=4;this.dead=false;this.age=0;this.trail=[];this.target=null;this.turn=0.1;this.acquire();if(this.target){const a=Math.atan2(this.target.y-this.y,this.target.x-this.x);this.vx=Math.cos(a)*this.speed;this.vy=Math.sin(a)*this.speed}else{this.vx=Math.cos(owner.turretAngle)*this.speed;this.vy=Math.sin(owner.turretAngle)*this.speed}}
acquire(){let best=null,bd=Infinity;for(let t of this.tanks){if(t.isPlayer||t.dead)continue;const d=Math.sqrt((t.x-this.x)**2+(t.y-this.y)**2);if(d<bd){bd=d;best=t}}this.target=best;return best}
update(walls,tanks){if(this.dead)return;this.age++;this.trail.push({x:this.x,y:this.y});if(this.trail.length>14)this.trail.shift();if(!this.target||this.target.dead)this.acquire();const curAngle=Math.atan2(this.vy,this.vx);let desired=curAngle;if(this.target)desired=Math.atan2(this.target.y-this.y,this.target.x-this.x);let diff=desired-curAngle;while(diff>Math.PI)diff-=Math.PI*2;while(diff<-Math.PI)diff+=Math.PI*2;const maxTurn=this.turn;const turnDir=Math.max(-maxTurn,Math.min(maxTurn,diff));const angle=curAngle+turnDir;this.vx=Math.cos(angle)*this.speed;this.vy=Math.sin(angle)*this.speed;this.x+=this.vx;this.y+=this.vy;if(this.x<-80||this.x>canvas.width+80||this.y<-80||this.y>canvas.height+80){this.dead=true;return}if(this.age>600)this.dead=true;for(let t of tanks){if(t.isPlayer||t.dead)continue;const d=Math.sqrt((this.x-t.x)**2+(this.y-t.y)**2);if(d<t.radius+this.radius){this.dead=true;t.takeDamage(Math.floor(t.maxHealth*0.8),this.owner);this.explodeAt(this.x,this.y);return}}}
explodeAt(x,y){game.addParticles(x,y,'#f97316',14,22);game.addParticles(x,y,'#ffcc00',10,14);audio.play('explosion');game.addNoise(x,y,260)}
draw(ctx){ctx.save();ctx.shadowColor='#f97316';ctx.shadowBlur=10;ctx.fillStyle='#f97316';ctx.beginPath();ctx.arc(this.x,this.y,this.radius+1,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.strokeStyle='rgba(249,115,22,0.5)';ctx.lineWidth=2;ctx.beginPath();for(let i=0;i<this.trail.length;i++){if(i===0)ctx.moveTo(this.trail[i].x,this.trail[i].y);else ctx.lineTo(this.trail[i].x,this.trail[i].y)}ctx.stroke();ctx.restore()}
}
const ICONS={bullet:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNNiAxMkwyMiAxMkMyNSAxMiAyOCAxNCAyOCAxNkMyOCAxOCAyNSAyMCAyMiAyMEw2IDIwWiIgc3Ryb2tlPSIjNGFkZTgwIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTEwIDEyTDEwIDIwIiBzdHJva2U9IiM0YWRlODAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIwLjUiLz48L3N2Zz4=',kill:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMTYgNEM5IDQgNCA5IDQgMTZDNCAyMCA2IDIzIDkgMjVMOSAyOEwyMyAyOEwyMyAyNUMyNiAyMyAyOCAyMCAyOCAxNkMyOCA5IDIzIDQgMTYgNFoiIHN0cm9rZT0iI2MwODRmYyIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xMCAxNEEyIDIgMCAxIDEgMTQgMTRBMiAyIDAgMSAxIDEwIDE0TTIwIDE0QTIgMiAwIDEgMSAyNCAxNEEyIDIgMCAxIDEgMjAgMTQiIHN0cm9rZT0iI2MwODRmYyIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xMiAyNEwyMCAyNCIgc3Ryb2tlPSIjYzA4NGZjIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE2IDE4TDE1IDIwTDE3IDIwWiIgc3Ryb2tlPSIjYzA4NGZjIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==',bounce:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNNiAyNEMxMCAxOCAxNCAxMCAxNiA2QzE4IDEwIDIyIDE4IDI2IDI0IiBzdHJva2U9IiMyMmQzZWUiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=',mine:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNOCAxNkE4IDggMCAxIDEgMjQgMTZBOCA4IDAgMSAxIDggMTYiIHN0cm9rZT0iI2Y4NzE3MSIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xNiA4TDE2IDVNMTQgNUwxOCA1TTIxLjUgMTAuNUwyNCA4TTI0IDE2TDI4IDE2TTIxLjUgMjEuNUwyNCAyNE0xNiAyNEwxNiAyOE0xMC41IDIxLjVMOCAyNE04IDE2TDQgMTZNMTAuNSAxMC41TDggOCIgc3Ryb2tlPSIjZjg3MTcxIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+',missile:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMTAgMTJMMjIgMTJDMjYgMTIgMjggMTQgMjggMTZDMjggMTggMjYgMjAgMjIgMjBMMTAgMjBaIiBzdHJva2U9IiNmOTczMTYiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTAgMTJMNyA4TDEwIDE0Wk0xMCAyMEw3IDI0TDEwIDE4WiIgc3Ryb2tlPSIjZjk3MzE2IiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTcgMTRMMyAxNkw3IDE4IiBzdHJva2U9IiNmOTczMTYiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=',heart:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMTYgMjhDNiAyMCAyIDE0IDQgOUM1IDUgOCA0IDEyIDZDMTQgNyAxNSA4IDE2IDEwQzE3IDggMTggNyAyMCA2QzI0IDQgMjcgNSAyOCA5QzMwIDE0IDI2IDIwIDE2IDI4WiIgc3Ryb2tlPSIjZWY0NDQ0IiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+',damage:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMTYgMkwxOCAxMkwyOCAxNkwxOCAyMEwxNiAyOEwxNCAyMEw0IDE2TDE0IDEyWiIgc3Ryb2tlPSIjZmI5MjNjIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+',speed:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMTggNEwxMCAxNkwxNiAxNkwxNCAyOEwyNCAxNEwxOCAxNFoiIHN0cm9rZT0iI2ZiYmYyNCIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==',life:'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMTYgMjhDNiAyMCAyIDE0IDQgOUM1IDUgOCA0IDEyIDZDMTQgNyAxNSA4IDE2IDEwQzE3IDggMTggNyAyMCA2QzI0IDQgMjcgNSAyOCA5QzMwIDE0IDI2IDIwIDE2IDI4WiIgc3Ryb2tlPSIjNGFkZTgwIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE4LjUgMjJBMy41IDMuNSAwIDEgMSAyNS41IDIyQTMuNSAzLjUgMCAxIDEgMTguNSAyMiIgc3Ryb2tlPSIjNGFkZTgwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yMiAxOS41TDIyIDI0LjVNMTkuNSAyMkwyNC41IDIyIiBzdHJva2U9IiM0YWRlODAiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGlLine truncated
const REWARD_DEFS=[
{name:'血量上限',icon:ICONS.heart,desc:'提高生命值上限',gen(){const v=2+Math.floor(Math.random()*3);return{label:'+'+v,apply(){game.playerStats.maxHp+=v;if(game.player&&!game.player.dead){game.player.maxHealth+=v;game.player.health+=v}}}}},
{name:'伤害提升',icon:ICONS.damage,desc:'子弹伤害提升',gen(){const v=1+Math.floor(Math.random()*2);return{label:'+'+v,apply(){game.playerStats.damage+=v}}}},
{name:'攻速提升',icon:ICONS.speed,desc:'射击间隔缩短,最大子弹+2',gen(){const v=2+Math.floor(Math.random()*3);return{label:'-'+v+' · 弹容+2',apply(){game.playerStats.fireRate+=v;game.playerStats.maxBullets+=2}}}},
{name:'额外命',icon:ICONS.life,desc:'死亡时自动复活并回满血',gen(){return{label:'+1 命',apply(){game.playerStats.extraLives++}}}},
{name:'必杀弹',icon:ICONS.kill,desc:'一发射杀敌人,滚轮选择使用',gen(){const v=1+Math.floor(Math.random()*3);return{label:v+' 发',apply(){game.playerStats.ammo.kill+=v}}}},
{name:'永久反弹弹',icon:ICONS.bounce,desc:'子弹无限反弹,滚轮选择使用',gen(){const v=10+Math.floor(Math.random()*11);return{label:v+' 发',apply(){game.playerStats.ammo.bounce+=v}}}},
{name:'地雷',icon:ICONS.mine,desc:'放置地雷只炸敌人,滚轮选择使用',gen(){const v=3+Math.floor(Math.random()*3);return{label:v+' 个',apply(){game.playerStats.ammo.mine+=v}}}},
{name:'自动导弹',icon:ICONS.missile,desc:'自动追踪敌人的导弹,滚轮选择使用',gen(){const v=1+Math.floor(Math.random()*2);return{label:v+' 枚',apply(){game.playerStats.ammo.missile+=v}}}}
];
class Game{
constructor(){
this.state='menu';this.level=1;this.walls=[];this.bullets=[];this.tanks=[];this.particles=[];this.player=null;this.mouseX=400;this.mouseY=300;this.shake=0;this.frame=0;this.mouseDown=false;this.evolution={level:1,speedMul:1,reactionMul:1,dodgeSkill:0,peekSkill:0,accuracy:0};this.lastNoise=null;this.playerStats={maxHp:10,damage:1,fireRate:0,extraLives:0,maxBullets:8,ammo:{kill:0,bounce:0,mine:0,missile:0},selected:'normal'};this.mines=[];this.missiles=[];
canvas.addEventListener('mousemove',e=>{const rect=canvas.getBoundingClientRect();this.mouseX=(e.clientX-rect.left)*(canvas.width/rect.width);this.mouseY=(e.clientY-rect.top)*(canvas.height/rect.height)});
canvas.addEventListener('mousedown',e=>{if(e.button===0){this.mouseDown=true;if(this.state==='playing'&&this.player&&!this.player.dead)this.player.shoot()}});
canvas.addEventListener('wheel',e=>{e.preventDefault();game.cycleAmmo(e.deltaY>0?1:-1)});
canvas.addEventListener('mouseup',e=>{if(e.button===0)this.mouseDown=false});
canvas.addEventListener('contextmenu',e=>e.preventDefault());
document.getElementById('btnStart').addEventListener('click',()=>this.start());
document.getElementById('btnNext').addEventListener('click',()=>this.nextLevel());
document.getElementById('btnRestart').addEventListener('click',()=>this.restart());
this.loop()
}
addNoise(x,y,r){this.lastNoise={x,y,radius:r,frame:this.frame}}
generateMap(){
this.walls=[];
for(let c=0;c<COLS;c++){this.walls.push(new Wall(c,0,'steel'));this.walls.push(new Wall(c,ROWS-1,'steel'))}
for(let r=1;r<ROWS-1;r++){this.walls.push(new Wall(0,r,'steel'));this.walls.push(new Wall(COLS-1,r,'steel'))}
for(let r=2;r<ROWS-2;r+=2){for(let c=2;c<COLS-2;c+=2){if(Math.random()<0.85)this.walls.push(new Wall(c,r,'steel'))}}
for(let i=0;i<8;i++){const c=2+Math.floor(Math.random()*(COLS-4));const r=2+Math.floor(Math.random()*(ROWS-4));if(!this.walls.find(w=>w.c===c&&w.r===r))this.walls.push(new Wall(c,r,'steel'))}
for(let r=1;r<ROWS-1;r++){for(let c=1;c<COLS-1;c++){if(this.walls.find(w=>w.c===c&&w.r===r))continue;const edgeDist=Math.min(c,r,COLS-1-c,ROWS-1-r);const chance=edgeDist<=1?0.15:0.45;if(Math.random()<chance)this.walls.push(new Wall(c,r,'brick'))}}
for(let i=0;i<20;i++){const c=1+Math.floor(Math.random()*(COLS-2));const r=1+Math.floor(Math.random()*(ROWS-2));if(!this.walls.find(w=>w.c===c&&w.r===r))this.walls.push(new Wall(c,r,'brick'))}
this.clearArea(1,1,3,3);this.clearArea(COLS-4,ROWS-4,3,3);this.clearArea(1,ROWS-4,3,3);this.clearArea(COLS-4,1,3,3);this.ensureReachable()
}
clearArea(c0,r0,w,h){for(let r=r0;r<r0+h;r++){for(let c=c0;c<c0+w;c++){const idx=this.walls.findIndex(wall=>wall.c===c&&wall.r===r);if(idx>=0)this.walls.splice(idx,1)}}}
ensureReachable(){const solid=[];for(let r=0;r<ROWS;r++)solid.push(new Array(COLS).fill(false));const wallAt=[];for(let r=0;r<ROWS;r++)wallAt.push(new Array(COLS).fill(null));for(let w of this.walls){if(!w.dead&&w.c>=0&&w.c<COLS&&w.r>=0&&w.r<ROWS){solid[w.r][w.c]=true;wallAt[w.r][w.c]=w}}let changed=true;while(changed){changed=false;const seen=[];for(let r=0;r<ROWS;r++)seen.push(new Array(COLS).fill(false));for(let sr=1;sr<ROWS-1;sr++){for(let sc=1;sc<COLS-1;sc++){if(solid[sr][sc]||seen[sr][sc])continue;const q=[[sc,sr]];seen[sr][sc]=true;const boundary=[];while(q.length){const p=q.pop(),c=p[0],r=p[1];for(let k=0;k<4;k++){const nc=c+[1,-1,0,0][k],nr=r+[0,0,1,-1][k];if(nc<1||nc>COLS-2||nr<1||nr>ROWS-2)continue;if(!solid[nr][nc]){if(!seen[nr][nc]){seen[nr][nc]=true;q.push([nc,nr])}}else boundary.push([nc,nr])}}if(boundary.length>0){let allSteel=true,anySteel=false;for(let i=0;i<boundary.length;i++){const w=wallAt[boundary[i][1]][boundary[i][0]];if(w&&w.type==='steel')anySteel=true;else{allSteel=false;break}}if(allSteel&&anySteel){const b=boundary[0];const w=wallAt[b[1]][b[0]];if(w&&w.type==='steel'){w.type='brick';w.health=2;w.maxHealth=2;changed=true}}}}}}}
findClearSpot(px,py,minDist,tanks){const reqD=[minDist,Math.max(140,minDist*0.7),Math.max(100,minDist*0.45)];const reqT=[90,70,50];for(let pass=0;pass<3;pass++){for(let attempt=0;attempt<120;attempt++){const x=40+Math.random()*(canvas.width-80);const y=40+Math.random()*(canvas.height-80);if(dist(x,y,px,py)<reqD[pass])continue;let clear=true;for(let w of this.walls){if(w.dead)continue;if(x+18>w.x&&x-18<w.x+w.w&&y+18>w.y&&y-18<w.y+w.h){clear=false;break}}if(!clear)continue;let tc=true;for(let t of tanks){if(!t||t.dead)continue;if(dist(x,y,t.x,t.y)<reqT[pass]){tc=false;break}}if(!tc)continue;if(pass<2&&hasLineOfSight(x,y,px,py,this.walls))continue;return{x,y}}}for(let r=1;r<ROWS-1;r++){for(let c=1;c<COLS-1;c++){const x=c*TILE+TILE/2,y=r*TILE+TILE/2;if(dist(x,y,px,py)<Math.max(90,minDist*0.4))continue;let clear=true;for(let w of this.walls){if(w.dead)continue;if(x+18>w.x&&x-18<w.x+w.w&&y+18>w.y&&y-18<w.y+w.h){clear=false;break}}if(!clear)continue;let tc=true;for(let t of tanks){if(!t||t.dead)continue;if(dist(x,y,t.x,t.y)<45){tc=false;break}}if(!tc)continue;if(hasLineOfSight(x,y,px,py,this.walls))continue;return{x,y}}}const corners=[[80,80],[canvas.width-80,80],[80,canvas.height-80],[canvas.width-80,canvas.height-80]].sort((a,b)=>dist(b[0],b[1],px,py)-dist(a[0],a[1],px,py));for(const cc of corners){let clear=true;for(let w of this.walls){if(w.dead)continue;if(cc[0]+18>w.x&&cc[0]-18<w.x+w.w&&cc[1]+18>w.y&&cc[1]-18<w.y+w.h){clear=false;break}}if(!clear)continue;let tc=true;for(let t of tanks){if(!t||t.dead)continue;if(dist(cc[0],cc[1],t.x,t.y)<60){tc=false;break}}if(tc)return{x:cc[0],y:cc[1]}}return{x:px<canvas.width/2?canvas.width-80:80,y:py<canvas.height/2?canvas.height-80:80}}
spawnTanks(){this.tanks=[];const corners=[[80,80],[canvas.width-80,80],[80,canvas.height-80],[canvas.width-80,canvas.height-80]];const c=corners[Math.floor(Math.random()*4)];this.player=new Tank(c[0],c[1],true,null,this.playerStats);this.tanks.push(this.player);const aiCount=Math.min(2+this.level,8);for(let i=0;i<aiCount;i++){const spot=this.findClearSpot(this.player.x,this.player.y,240,this.tanks);this.tanks.push(new Tank(spot.x,spot.y,false,this.evolution))}}
evolve(){this.evolution.level=this.level;this.evolution.speedMul=Math.min(5,1+this.level*0.5);this.evolution.reactionMul=Math.min(6,1+this.level*0.8);this.evolution.dodgeSkill=Math.min(10,this.level*1.5);this.evolution.peekSkill=Math.min(5,Math.floor(this.level/2));this.evolution.accuracy=Math.min(8,this.level*1.2);}
addParticles(x,y,color,speed,count){for(let i=0;i<count;i++)this.particles.push(new Particle(x,y,color,speed,18+Math.random()*18))}
start(){this.mines=[];audio.init();audio.startBGM();document.getElementById('startScreen').classList.add('hidden');this.level=1;this.evolution={level:1,speedMul:1,reactionMul:1,dodgeSkill:0,peekSkill:0,accuracy:0};this.playerStats={maxHp:10,damage:1,fireRate:0,extraLives:0,maxBullets:8,ammo:{kill:0,bounce:0,mine:0,missile:0},selected:'normal'};this.startLevel()}
startLevel(){this.state='playing';this.bullets=[];this.particles=[];this.missiles=[];this.generateMap();this.spawnTanks();this.evolve();this.updateUI();this.updateAmmoHud()}
nextLevel(){audio.play('levelup');document.getElementById('levelScreen').classList.add('hidden');this.level++;this.startLevel()}
restart(){this.mines=[];document.getElementById('overScreen').classList.add('hidden');this.level=1;this.evolution={level:1,speedMul:1,reactionMul:1,dodgeSkill:0,peekSkill:0,accuracy:0};this.playerStats={maxHp:10,damage:1,fireRate:0,extraLives:0,maxBullets:8,ammo:{kill:0,bounce:0,mine:0,missile:0},selected:'normal'};this.startLevel()}
updateUI(){document.getElementById('levelNum').textContent=this.level;const enemies=this.tanks.filter(t=>!t.isPlayer&&!t.dead).length;document.getElementById('enemyCount').textContent=enemies;if(this.player){const hpPct=Math.max(0,(this.player.health/this.player.maxHealth)*100);const hpBar=document.getElementById('hpBar');hpBar.style.width=hpPct+'%';hpBar.style.background=hpPct>50?'linear-gradient(90deg,#22c55e,#4ade80)':hpPct>25?'linear-gradient(90deg,#f59e0b,#fbbf24)':'linear-gradient(90deg,#ef4444,#f87171)'}}
update(){if(this.state!=='playing')return;this.frame++;if(this.mouseDown&&this.frame%7===0&&this.player&&!this.player.dead)this.player.shoot();for(let t of this.tanks)t.update(this.walls,this.player,this.tanks,this.bullets);if(this.player&&this.player.invincible>0)this.player.invincible--;for(let b of this.bullets)b.update(this.walls,this.tanks);for(let i=this.bullets.length-1;i>=0;i--){if(this.bullets[i].dead){if(this.bullets[i].onDeath)this.bullets[i].onDeath();this.bullets.splice(i,1)}}for(let m of this.mines)m.update(this.walls,this.tanks);this.mines=this.mines.filter(m=>!m.dead);for(let m of this.missiles)m.update(this.walls,this.tanks);this.missiles=this.missiles.filter(m=>!m.dead);for(let p of this.particles)p.update();this.particles=this.particles.filter(p=>p.life>0);const aliveEnemies=this.tanks.filter(t=>!t.isPlayer&&!t.dead).length;if(aliveEnemies===0&&this.player&&!this.player.dead){this.state='reward';this.showRewardChoices();this.mouseDown=false}if(this.player&&this.player.dead){if(this.player.extraLives>0){this.player.extraLives--;game.playerStats.extraLives--;this.updateAmmoHud();this.player.dead=false;this.player.health=this.player.maxHealth;this.player.invincible=90;this.addParticles(this.player.x,this.player.y,'#4ade80',20,40);this.addParticles(this.player.x,this.player.y,'#ffffff',14,24);audio.play('levelup');this.shake=Math.max(this.shake,10)}else{this.state='over';document.getElementById('finalLevel').textContent=this.level;document.getElementById('overScreen').classList.remove('hidden');this.mouseDown=false}}if(this.shake>0)this.shake--;this.updateUI()}
draw(){const bg=ctx.createLinearGradient(0,0,0,canvas.height);bg.addColorStop(0,'#243349');bg.addColorStop(1,'#111a2c');ctx.fillStyle=bg;ctx.fillRect(0,0,canvas.width,canvas.height);ctx.strokeStyle='rgba(255,255,255,0.035)';ctx.lineWidth=1;for(let x=0;x<=canvas.width;x+=TILE){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,canvas.height);ctx.stroke()}for(let y=0;y<=canvas.height;y+=TILE){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(canvas.width,y);ctx.stroke()}ctx.save();if(this.shake>0){ctx.translate((Math.random()-0.5)*this.shake,(Math.random()-0.5)*this.shake)}for(let w of this.walls)w.draw(ctx);for(let b of this.bullets)b.draw(ctx);for(let t of this.tanks)t.draw(ctx);for(let p of this.particles)p.draw(ctx);if(this.player&&!this.player.dead){for(let t of this.tanks){if(t.isPlayer||t.dead)continue;if(t.lastSeenPlayerPos&&hasLineOfSight(t.x,t.y,t.lastSeenPlayerPos.x,t.lastSeenPlayerPos.y,this.walls)){ctx.strokeStyle='rgba(251,191,36,0.12)';ctx.lineWidth=1;ctx.setLineDash([3,5]);ctx.beginPath();ctx.moveTo(t.x,t.y);ctx.lineTo(t.lastSeenPlayerPos.x,t.lastSeenPlayerPos.y);ctx.stroke();ctx.setLineDash([])}}ctx.strokeStyle='rgba(74,222,128,0.12)';ctx.lineWidth=1;ctx.setLineDash([4,6]);ctx.beginPath();ctx.moveTo(this.player.x,this.player.y);ctx.lineTo(this.mouseX,this.mouseY);ctx.stroke();ctx.setLineDash([]);ctx.strokeStyle='rgba(74,222,128,0.35)';ctx.lineWidth=2;ctx.beginPath();ctx.arc(this.mouseX,this.mouseY,8,0,Math.PI*2);ctx.stroke()}for(let m of this.mines)m.draw(ctx);for(let m of this.missiles)m.draw(ctx);ctx.restore()}
loop(){this.update();this.draw();requestAnimationFrame(()=>this.loop())}
cycleAmmo(dir){const order=['normal','kill','bounce','mine','missile'];const ammo=this.playerStats.ammo;let idx=order.indexOf(this.playerStats.selected);for(let i=1;i<=order.length;i++){const next=(idx+i*dir+order.length*2)%order.length;const t=order[next];if(t==='normal'||ammo[t]>0){this.playerStats.selected=t;break}}if(this.player)this.player.selectedType=this.playerStats.selected;this.updateAmmoHud()}
updateAmmoHud(){const panel=document.getElementById('ammoPanel');if(!panel)return;const ammo=this.playerStats.ammo;if(this.playerStats.selected!=='normal'&&ammo[this.playerStats.selected]<=0){const order=['kill','bounce','mine','missile'];this.playerStats.selected='normal';for(const t of order){if(ammo[t]>0){this.playerStats.selected=t;break}}if(this.player)this.player.selectedType=this.playerStats.selected}const defs=[{t:'kill',icon:ICONS.kill,name:'必杀'},{t:'bounce',icon:ICONS.bounce,name:'反弹'},{t:'mine',icon:ICONS.mine,name:'地雷'},{t:'missile',icon:ICONS.missile,name:'导弹'}];let h='';if(this.playerStats.extraLives>0)h+='<div class="ammo-row"><img class="ammo-icon" src="'+ICONS.life+'" alt="" /><span class="ammo-name">命</span><span class="ammo-count">×'+this.playerStats.extraLives+'</span></div>';for(const d of defs){const n=ammo[d.t];if(n<=0)continue;h+='<div class="ammo-row'+(this.playerStats.selected===d.t?' active':'')+'"><img class="ammo-icon" src="'+d.icon+'" alt="" /><span class="ammo-name">'+d.name+'</span><span class="ammo-count">×'+n+'</span></div>'}panel.innerHTML=h;const icons={normal:ICONS.bullet,kill:ICONS.kill,bounce:ICONS.bounce,mine:ICONS.mine,missile:ICONS.missile};const cur=document.getElementById('ammoCurrent');if(cur)cur.innerHTML='<img src="'+(icons[this.playerStats.selected]||ICONS.bullet)+'" alt="" />'}
showRewardChoices(){this.pendingReward=null;const pool=REWARD_DEFS.slice();const picks=[];for(let i=0;i<3;i++){const idx=Math.floor(Math.random()*pool.length);picks.push(pool.splice(idx,1)[0])}const container=document.getElementById('rewardCards');container.innerHTML='';const confirmBtn=document.getElementById('rewardConfirm');confirmBtn.classList.remove('enabled');confirmBtn.disabled=true;picks.forEach(def=>{const card=document.createElement('div');card.className='reward-card';const r=def.gen();card.innerHTML='<img class="reward-icon" src="'+def.icon+'" alt="" /><div class="reward-name">'+def.name+'</div><div class="reward-val">'+r.label+'</div><div class="reward-desc">'+def.desc+'</div>';card.addEventListener('click',()=>{for(const c of container.children)c.classList.remove('selected');card.classList.add('selected');this.pendingReward=r.apply;confirmBtn.classList.add('enabled');confirmBtn.disabled=false});container.appendChild(card)});document.getElementById('rewardScreen').classList.remove('hidden')}
afterReward(){document.getElementById('rewardScreen').classList.add('hidden');document.getElementById('rewardConfirm').classList.remove('enabled');document.getElementById('rewardConfirm').disabled=true;this.pendingReward=null;this.updateAmmoHud();this.state='level';document.getElementById('nextLevel').textContent=this.level+1;document.getElementById('levelScreen').classList.remove('hidden');this.mouseDown=false;this.updateUI()}
}
const game=new Game();
function confirmReward(){if(!game.pendingReward)return;const apply=game.pendingReward;game.pendingReward=null;apply();game.afterReward()}
document.getElementById('rewardConfirm').addEventListener('click',confirmReward);
</script>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

@@ -0,0 +1,53 @@
<!--
彩蛋占位组件(Easter Egg Placeholder)
======================================
原来的「美西螈合成小游戏」已被移除,这里预留给自定义彩蛋。
触发方式(沿用原框架,见 AboutSettings.vue):
1. 输入暗号:'cyf112233' 或 'cxkcxkckx'
2. Konami 秘技:↑↑↓↓←→←→BA
3. 长按「关于」页某位开发者名字(见 about-member-experiences.ts)
如何改造成你自己的彩蛋:
- 直接在下方 <template> 里写你的内容(游戏 / 动画 / 展示页都行)。
- 如需全屏,沿用根容器的 absolute inset-0 布局。
- 组件通过 defineEmits 暴露 exit 事件,调用它会关闭弹窗。
- 若彩蛋需要独立的静态资源(图片/HTML/JS),放到 public/easteregg/ 下,
用 `${window.location.origin}/easteregg/...` 引用,或直接在 <template> 内联。
当前状态:空占位,仅显示一行提示。
-->
<script setup lang="ts">
import { defineMessages, useVIntl } from '@modrinth/ui'
const emit = defineEmits<{ exit: [] }>()
const { formatMessage } = useVIntl()
const messages = defineMessages({
placeholder: {
id: 'app.settings.about.easteregg.placeholder',
defaultMessage: 'Easter egg goes here.',
},
exit: {
id: 'app.settings.about.easteregg.exit',
defaultMessage: 'Close',
},
})
</script>
<template>
<div class="absolute inset-0 z-10 flex flex-col items-center justify-center gap-4 bg-surface-1">
<p class="m-0 text-sm text-secondary">
{{ formatMessage(messages.placeholder) }}
</p>
<button
type="button"
class="rounded-lg bg-surface-3 px-4 py-1.5 text-sm font-medium text-contrast transition-colors hover:bg-surface-4"
@click.stop="emit('exit')"
>
{{ formatMessage(messages.exit) }}
</button>
</div>
</template>
File diff suppressed because it is too large. Load diff
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { defineMessages, NewModal, useVIntl } from '@modrinth/ui'
import { ref } from 'vue'
import AboutEasterEgg from '../AboutEasterEgg.vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
@@ -14,8 +17,6 @@ const messages = defineMessages({
const modal = ref<InstanceType<typeof NewModal> | null>(null)
const gameVisible = ref(false)
const gameUrl = `${window.location.origin}/easteregg/games/game.html`
function show() {
gameVisible.value = true
modal.value?.show()
@@ -25,6 +26,10 @@ function onHide() {
gameVisible.value = false
}
function closeGame() {
modal.value?.hide()
}
defineExpose({ show })
</script>
@@ -37,13 +42,8 @@ defineExpose({ show })
noblur
:on-hide="onHide"
>
<div class="flex flex-col items-center py-2">
<iframe
v-if="gameVisible"
:src="gameUrl"
title="Mini Game"
class="h-[600px] w-[800px] max-w-full rounded-xl border-none bg-black"
/>
<div class="relative h-[600px] w-[800px] max-w-full overflow-hidden rounded-xl bg-surface-1">
<AboutEasterEgg v-if="gameVisible" @exit="closeGame" />
</div>
</NewModal>
</template>
@@ -1,19 +1,23 @@
import type { Component } from 'vue'
import AboutMergeGame from '../AboutMergeGame.vue'
import AboutEasterEgg from '../AboutEasterEgg.vue'
export type AboutMemberExperience = {
component: Component
longPressDuration: number
}
// 长按「关于」页成员名字触发的彩蛋体验。
// 原 'axolotl-merge' 彩蛋已移除,改为通用占位组件 AboutEasterEgg.vue。
// 若要为特定成员挂载自定义彩蛋,在这里新增条目即可。
const memberExperiences: Record<string, AboutMemberExperience> = {
'axolotl-merge': {
component: AboutMergeGame,
'easter-egg': {
component: AboutEasterEgg,
longPressDuration: 800,
},
}
export function getAboutMemberExperience(experience: unknown): AboutMemberExperience | undefined {
return typeof experience === 'string' ? memberExperiences[experience] : undefined
}
}
+2 -1
View File
@@ -3,7 +3,8 @@
"name": "Mystic Stars",
"avatar": "mystic-stars.png",
"url": "https://github.com/Mystic-Stars",
"experience": "axolotl-merge"
"experience": "easter-egg"
},
{
"name": "ItzJerry",
@@ -6689,63 +6689,6 @@
"app.settings.about.easteregg.game-title": {
"message": "Mini Game"
},
"app.settings.about.game.best": {
"message": "Best: {score}"
},
"app.settings.about.game.best-label": {
"message": "Best"
},
"app.settings.about.game.completed": {
"message": "Rainbow axolotl!"
},
"app.settings.about.game.exit": {
"message": "Exit game"
},
"app.settings.about.game.game-over": {
"message": "The axolotls got stranded!"
},
"app.settings.about.game.highest-level": {
"message": "Highest: level {level}"
},
"app.settings.about.game.level-label": {
"message": "Highest level"
},
"app.settings.about.game.new-record": {
"message": "New record!"
},
"app.settings.about.game.overtime": {
"message": "Tide surge!"
},
"app.settings.about.game.overtime-detail": {
"message": "The water rushes back — keep merging!"
},
"app.settings.about.game.overtimes-label": {
"message": "Overtimes"
},
"app.settings.about.game.restart": {
"message": "Restart"
},
"app.settings.about.game.score": {
"message": "Score: {score}"
},
"app.settings.about.game.score-label": {
"message": "Score"
},
"app.settings.about.game.settle": {
"message": "Settle"
},
"app.settings.about.game.settle-title": {
"message": "Run settled!"
},
"app.settings.about.game.tap-to-drop": {
"message": "Click to drop a pink axolotl"
},
"app.settings.about.game.tide-hint": {
"message": "The tide is falling — keep your axolotls underwater!"
},
"app.settings.about.game.title": {
"message": "Starlight merge"
},
"app.settings.about.license-attribution": {
"message": "License & attribution"
},
+1 -57
View File
@@ -10309,62 +10309,6 @@
},
"app.settings.about.easteregg.game-title": {
"message": "彩蛋小游戏"
},
"app.settings.about.game.best": {
"message": "最高分:{score}"
},
"app.settings.about.game.best-label": {
"message": "最高分"
},
"app.settings.about.game.completed": {
"message": "彩虹美西螈!"
},
"app.settings.about.game.exit": {
"message": "退出游戏"
},
"app.settings.about.game.game-over": {
"message": "美西螈搁浅了!"
},
"app.settings.about.game.highest-level": {
"message": "最高等级:{level}"
},
"app.settings.about.game.level-label": {
"message": "最高等级"
},
"app.settings.about.game.new-record": {
"message": "新纪录!"
},
"app.settings.about.game.overtime": {
"message": "潮水大涨!"
},
"app.settings.about.game.overtime-detail": {
"message": "水位回升,继续合成!"
},
"app.settings.about.game.overtimes-label": {
"message": "加时次数"
},
"app.settings.about.game.restart": {
"message": "重新开始"
},
"app.settings.about.game.score": {
"message": "分数:{score}"
},
"app.settings.about.game.score-label": {
"message": "分数"
},
"app.settings.about.game.settle": {
"message": "结算"
},
"app.settings.about.game.settle-title": {
"message": "结算完成!"
},
"app.settings.about.game.tap-to-drop": {
"message": "点击场地,投放粉色美西螈"
},
"app.settings.about.game.tide-hint": {
"message": "潮水正在退去,别让美西螈搁浅!"
},
"app.settings.about.game.title": {
"message": "合成美西螈"
}
}
Vendored Submodule
+1
Submodule apps/app/vendor/cubiomes added at e61f90580c.