几何特工 - 最终整合版
by BraveRaven691169 lines194.1 KB
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>几何特工 - 最终整合版</title>
<style>
body{margin:0;display:flex;justify-content:center;align-items:center;height:100vh;background:#1a1a2e;font-family:sans-serif;overflow:hidden}
canvas{display:block;touch-action:none;width:min(100vw,400px);height:min(100vw,400px);border:3px solid #ff99cc;border-radius:12px;box-shadow:0 0 30px rgba(255,105,180,0.5)}
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="400"></canvas>
<script>
// ==================== 模块1:核心配置 ====================
const canvas=document.getElementById('gameCanvas');
const ctx=canvas.getContext('2d');
const WORLD_SIZE=100,VIEW_SIZE=20,UNIT_PIXEL=20,CANVAS_SIZE=VIEW_SIZE*UNIT_PIXEL;
let gameStarted=false,homeBallX=-30,homeBallY=CANVAS_SIZE/2,homeBallVx=6,homeBallBounce=0;
let menuOpen=false;
const player={x:50,y:50,radius:0.4,baseSpeed:5,speed:5,hp:100,maxHp:100,shield:0,dead:false,aimAngle:0,invincibleTime:2,slowEffects:[],stunTime:0,dashCooldown:0,dashing:false,dashTimer:0,dashDir:{x:0,y:0},foodEffects:[],
_confuseTimer:0,_confuseDir:null,_confuseNextChange:0,_healFlash:0};
let monsters=[],allies=[],bullets=[],enemyBullets=[],allyBullets=[],grenades=[],explosions=[],traps=[],fragments=[],burnZones=[];
let structures=[],bossSkillWarnings=[],shockwaveEffects=[],enemyHexShockwaves=[];
let particles=[],damageNumbers=[],sunBullets=[];
let playerHurtFlash=0,hitstopTimer=0;
let energy=0,crystals=0,waveCount=0,waveDelay=0,attackCooldown=0,attackCount=0,attackEffectTimer=0;
let currentWeaponIndex=0,weaponPage=0,shopPage=0,phonePage=0,labTab='research';
let manualMode=false,shopOpen=false,phoneOpen=false,pediaOpen=false,labOpen=false;
let pediaSelectedType=null,pediaPage=0,lastSingleType='',massMutationWave=false,speedBattleWave=false;
let speedBattleTimer=0,speedBattleOverTime=0;
let boomerang=null,boss=null,bossIntro=null,airStrikeData=null;
let airStrikeCooldown=0,isVictory=false,specialEvent=null,infiniteSurvivalMode=false,infiniteSurvivalTime=0,eventNotice='';
let interactAllyIndex=-1;
let absenceCounters={pentagon:0,hexagon:0,kite:0,octagon:0,crescent:0,arrow:0},spawnedThisWave=[];
let discoveredEnemies={},unlockedEnemies={};
let upgrades={hp:0,support:0,move:0,damage:0};
const upgradeMax=5;
const upgradeCosts={hp:12,support:20,move:16,damage:18};
let _exterminateMode=false;
let _diamondMergeTimer=0;
let _sunNextWave=false;
let _sunBlazingUnlocked=false;
const _mirrorShield=new Set();
window.summonUnlocks = { hexStar:false };
window._intelPanelOpen = true;
window._nextWaveBudgetBonus = 0;
// ==================== 音效 ====================
const AudioSys=(()=>{
let ac=null,master=null,enabled=true;
function ensure(){if(!ac){try{ac=new(window.AudioContext||window.webkitAudioContext)();master=ac.createGain();master.gain.value=0.12;master.connect(ac.destination);}catch(e){enabled=false;}}if(ac&&ac.state==='suspended')ac.resume();return ac;}
function tone(freq,dur,type='square',vol=1,slideTo=null,delay=0){if(!enabled)return;const c=ensure();if(!c)return;const t0=c.currentTime+delay;const o=c.createOscillator(),g=c.createGain();o.type=type;o.frequency.setValueAtTime(freq,t0);if(slideTo!==null)o.frequency.exponentialRampToValueAtTime(Math.max(1,slideTo),t0+dur);g.gain.setValueAtTime(0.0001,t0);g.gain.exponentialRampToValueAtTime(vol,t0+0.005);g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);o.connect(g);g.connect(master);o.start(t0);o.stop(t0+dur+0.03);}
function noise(dur,vol=0.4){if(!enabled)return;const c=ensure();if(!c)return;const n=Math.floor(c.sampleRate*dur);const buf=c.createBuffer(1,n,c.sampleRate);const d=buf.getChannelData(0);for(let i=0;i<n;i++)d[i]=(Math.random()*2-1)*(1-i/n);const src=c.createBufferSource();src.buffer=buf;const g=c.createGain();g.gain.value=vol;src.connect(g);g.connect(master);src.start();}
const _last={};
function throttle(k,ms){const now=performance.now();if(_last[k]&&now-_last[k]<ms)return false;_last[k]=now;return true;}
return {
ensure, hit(){if(throttle('hit',25))tone(620,0.045,'square',0.25,380);},
hitMelee(){if(throttle('melee',40))tone(180,0.07,'square',0.35,90);},
kill(){if(throttle('kill',50))tone(440,0.1,'sawtooth',0.3,120);},
killBoss(){tone(280,0.3,'sawtooth',0.4,50);noise(0.25,0.2);},
hurt(){tone(95,0.14,'sine',0.5,45);},
shoot(){if(throttle('shoot',45))tone(720,0.035,'square',0.14,320);},
pick(){if(throttle('pick',35))tone(880,0.05,'sine',0.18,1400);},
upgrade(){tone(523,0.07,'square',0.3);tone(659,0.07,'square',0.3,null,0.07);tone(784,0.12,'square',0.3,null,0.14);},
wave(){tone(220,0.14,'triangle',0.28);tone(330,0.14,'triangle',0.28,null,0.14);tone(440,0.22,'triangle',0.28,null,0.28);},
boss(){tone(110,0.4,'sawtooth',0.35,55);tone(165,0.4,'sawtooth',0.28,82,0.1);}
};
})();
// ==================== 武器 ====================
function w(name,type,props){return {name,type,...props,isReloading:false,reloadTimer:0,runUnlocked:false,purchased:false};}
const weapons=[
w('军刀','melee',{damagePattern:[30,40,50],attackInterval:1,range:2.5,arcAngle:Math.PI/3,displayScale:1.5,knockback:0.5,runUnlocked:true}),
w('手枪','gun',{damage:25,attackInterval:0.5,magSize:4,reloadTime:1.2,bulletSpeed:10,bulletRadius:0.25,color:'#66ccff',currentMag:4,runUnlocked:true}),
w('回旋镖','boomerang',{damage:25,attackInterval:1,range:16,speed:12,returnSpeed:14,radius:0.35,color:'#ff99ff',ready:true,cooldown:0}),
w('筝形冲锋枪','kiteGun',{damage:9,attackInterval:0.25,magSize:20,reloadTime:1.5,bulletSpeed:14,bulletRadius:0.2,color:'#66ff66',currentMag:20}),
w('扇形霰弹枪','shotgun',{damage:15,attackInterval:1,magSize:4,reloadTime:1.8,bulletSpeed:12,bulletRadius:0.25,color:'#ff9966',currentMag:4}),
w('手雷','grenade',{damage:40,attackInterval:1,speed:8,explosionRadius:5,color:'#ffcc00',count:0}),
w('五角星锯','saw',{damage:5,attackInterval:1/12,magSize:150,reloadTime:1.5,range:2.5,color:'#ffdd00',currentMag:150}),
w('闪光陷阱','trap',{damage:0,attackInterval:0.5,color:'#ffffff',count:0}),
w('平行四边形步枪','paraGun',{damage:10,attackInterval:0.3,magSize:30,reloadTime:1.8,bulletSpeed:10,bulletRadius:0.2,color:'#cc99ff',currentMag:30}),
w('长方形狙击枪','sniper',{damage:120,attackInterval:1,magSize:1,reloadTime:2,bulletSpeed:30,bulletRadius:0.3,color:'#ffffff',currentMag:1}),
w('正六边形轻机枪','lmg',{damage:5,attackInterval:0.2,magSize:80,reloadTime:3,bulletSpeed:12,bulletRadius:0.2,color:'#ffcc00',currentMag:80,fireTime:0}),
w('矩形燃烧弹','burn',{damage:2,attackInterval:1,speed:8,burnDuration:10,burnRadius:5,color:'#ff6600',count:0}),
w('六角星追踪枪','hexGun',{damage:30,attackInterval:1,magSize:6,reloadTime:1.5,bulletSpeed:5,bulletRadius:0.3,color:'#66ddff',currentMag:6,homingTime:10}),
w('箭头长矛','melee',{damage:40,attackInterval:1/0.9,range:5,arcAngle:Math.PI/3,displayScale:1.5,knockback:0.5,color:'#ff9966'}),
w('破晓之时','blazing',{damage:42,attackInterval:1,magSize:3,reloadTime:2,bulletSpeed:6.5,bulletRadius:0.6,color:'#ffdd44',currentMag:3})
];
const SPEAR_INDEX=13;
const BLAZING_INDEX=14;
const labWeapons=[{name:'长方形狙击枪',cost:1,index:9},{name:'正六边形轻机枪',cost:2,index:10},{name:'矩形燃烧弹',cost:3,index:11},{name:'六角星追踪枪',cost:2,index:12}];
// ==================== 存档 ====================
function saveGame(){try{localStorage.setItem('geoagent_save',JSON.stringify({crystals,discoveredEnemies,purchased:weapons.map(w=>w.purchased),summonUnlocks:window.summonUnlocks,blazingUnlocked:_sunBlazingUnlocked}));}catch(e){}}
function loadGame(){try{let d=JSON.parse(localStorage.getItem('geoagent_save'));if(d){crystals=d.crystals||0;discoveredEnemies=d.discoveredEnemies||{};(d.purchased||[]).forEach((p,i)=>{if(i<weapons.length)weapons[i].purchased=p;});if(d.summonUnlocks)window.summonUnlocks=d.summonUnlocks;if(d.blazingUnlocked){_sunBlazingUnlocked=true;weapons[BLAZING_INDEX].purchased=true;weapons[BLAZING_INDEX].runUnlocked=true;}}}catch(e){}}
loadGame();
// ==================== 变异 ====================
const MIRROR_HP_MULT=2.0;
const MIRROR_VALUE_MULT=0.75;
function applyMutation(m){
if(!m.mutation)return m;
if(m.mutation==='giant'){m.radius*=Math.sqrt(1.5);m.hp*=2;m.maxHp*=2;m.damage*=1.6;m.speed*=0.7;m.value*=1.5;}
else if(m.mutation==='mini'){m.radius*=Math.sqrt(0.5);m.hp*=0.5;m.maxHp*=0.5;m.speed*=1.5;m.value*=1.5;}
else if(m.mutation==='burst'){m.burst=true;m.burstRadius=Math.min(m.maxHp/10,10);m.value*=1.3;}
else if(m.mutation==='converge'){
let traits=['triangle','trapezoid','isoscelesTrapezoid','pentagon','hexagon','octagon','kite','lShape','star'];
let available=traits.filter(t=>t!==m.type);
m.convergeTrait=available[Math.floor(Math.random()*available.length)];
m.value*=1.3;
}
else if(m.mutation==='mirror'){
m.color='#ffffff';m.stroke='#cccccc';
m.hp=m.maxHp=Math.round(m.hp*MIRROR_HP_MULT);
m.value=(m.value||1)*MIRROR_VALUE_MULT;
m._mirrorId='mirror_'+Math.random().toString(36).slice(2);
m._mirrorPending=true;
}
return m;
}
// ==================== 怪物生成 ====================
function _newMonsterBase(type,x,y,props){
const base={x,y,type,buffs:[],shield:0,stunTime:0,weakened:false,weakenTimer:0,invincibleTime:0,mutation:null,poisonStacks:0,poisonTimer:0,poisonTick:0,slowTimer:0,baseSpeed:null,burst:false,burstChild:false,convergeTrait:null,stuckTimer:0,extraSlow:0,damageMult:1,_hitFlash:0};
return applyMutation(Object.assign(base,props));
}
function createMonster(type,x,y){
let mutation=null;
if(massMutationWave||Math.random()<0.03){
const pool=['giant','mini','burst','converge','mirror'];
mutation=pool[Math.floor(Math.random()*pool.length)];
}
const defs={
square:{radius:0.5,hp:50,speed:3,damage:5,color:'#66ccff',stroke:'#3399cc',value:1},
triangle:{radius:0.55,hp:80,speed:5,damage:7,color:'#ff9966',stroke:'#cc6633',value:3},
trapezoid:{radius:0.55,hp:40,speed:2,damage:8,color:'#cc99ff',stroke:'#9933cc',value:2,state:'chase',chargeDir:{x:0,y:0},chargeTimer:0},
isoscelesTrapezoid:{radius:0.55,hp:40,speed:2,damage:6,color:'#99ff99',stroke:'#33cc33',value:2,speedTimer:3,speedBoost:false,speedBoostTimer:0},
pentagon:{radius:0.6,hp:100,speed:1,damage:0,color:'#ffd700',stroke:'#daa520',value:8,state:'idle',stateTimer:0.01,splitting:false,splittingTimer:0,gen:0,canSplit:true},
hexagon:{radius:0.55,hp:70,speed:3,damage:0,color:'#66ffff',stroke:'#00cccc',value:6,buffTimer:12,shockwaveState:'idle',shockwaveTimer:0,shockCooldown:0},
octagon:{radius:0.7,hp:180,speed:2,damage:10,color:'#ff6666',stroke:'#cc3333',value:7,shootTimer:3},
kite:{radius:0.65,hp:90,speed:4,damage:5,color:'#66ff66',stroke:'#33cc33',value:6,bubbleTimer:6},
crescent:{radius:0.6,hp:108,speed:3.25,damage:7,baseDamage:7,color:'#ccddff',stroke:'#6688cc',value:6,stealthTimer:14,stealthing:false,circleTimer:0,circleDir:1,chargeTimer:0,chargeDir:{x:0,y:0},state:'chase'},
lShape:{radius:0.6,hp:65,speed:3.5,damage:6,color:'#ccddff',stroke:'#6688cc',value:2,jumpTimer:2},
star:{radius:0.5,hp:50,speed:3,damage:0,color:'#ffff99',stroke:'#cc9900',value:2,shootTimer:3,dodgeThreshold:10,dodgeAccum:0,dodgeCooldown:0},
hexStar:{radius:0.55,hp:60,speed:3,damage:0,color:'#66ddff',stroke:'#3399cc',value:4,shootTimer:2,dodgeThreshold:10,dodgeAccum:0,dodgeCooldown:0},
spiral:{radius:0.6,hp:72,speed:1,damage:4,color:'#88bbff',stroke:'#4466aa',value:6,growTimer:15,orbitBullets:[]},
fourStar:{radius:0.5,hp:40,speed:3,damage:4,color:'#ffaa66',stroke:'#cc7733',value:1,maxHit:10},
octStar:{radius:0.85,hp:160,speed:2,damage:12,color:'#ff88cc',stroke:'#cc4488',value:9,shootTimer:6,slideThreshold:32,slideAccum:0,sliding:false,slideTimer:0,slideDir:0,slideShootTimer:0},
parallelogram:{radius:0.55,hp:35,speed:8,damage:6,color:'#cc99ff',stroke:'#9933cc',value:2},
heart:{radius:0.6,hp:100,speed:4,damage:-3,color:'#ff6699',stroke:'#cc3366',value:7},
diamond:{radius:1.0,hp:250,speed:3,damage:15,color:'#88ddff',stroke:'#3399cc',value:10},
smallDiamond:{radius:0.35,hp:25,speed:5.5,damage:3,color:'#aaddff',stroke:'#66aadd',value:1},
solidQuad:{radius:1.0,hp:444,speed:3,damage:24,color:'#ccd5ee',stroke:'#7788aa',value:24,_s3dLifeTimer:32,_s3dStealthTimer:3,_s3dStealthing:false,_s3dAngle:Math.random()*Math.PI*2},
dodecagon:{radius:0.9,hp:80,speed:4,damage:6,color:'#88eebb',stroke:'#339977',value:8,_dodecAbsorbed:0,_dodecMaxAbsorb:36},
sun:{radius:1.3,hp:731,speed:4,damage:30,color:'#ffdd44',stroke:'#cc8800',value:25,isSun:true,_sunShootTimer:2,_sunTeleportTimer:12,_sunHealTimer:1,_sunDotTimer:1,_sunTeleportWarning:null},
zShape:{radius:0.6,hp:70,speed:4,damage:0,color:'#ffdd88',stroke:'#ccaa33',value:6,lifeTimer:45,escaped:false},
shenShape:{radius:0.7,hp:350,speed:0,damage:0,color:'#88ddff',stroke:'#3399cc',value:12,shootTimer:1,isStationary:true}
};
if(type==='arrow'){
let tail=[];for(let i=0;i<9;i++)tail.push({x:x-i,y,hp:30,maxHp:30,radius:0.3,type:'tail'});
const m=_newMonsterBase(type,x,y,{radius:0.6,hp:100,maxHp:100,speed:3,damage:4,baseDamage:4,color:'#ff9999',stroke:'#cc3333',value:12,activated:false,tail});
if(mutation){m.mutation=mutation;applyMutation(m);}
return m;
}
if(type==='parallelogram'){
const m=_newMonsterBase(type,x,y,defs.parallelogram);
let a=Math.random()*Math.PI*2;
const snap=Math.round(a/(Math.PI/2))*(Math.PI/2);
if(Math.abs(a-snap)<0.2)a=snap+0.35;
m._paraDir={x:Math.cos(a),y:Math.sin(a)};
m._paraTrail=[];m._paraTrailTimer=0;m._paraBounceCount=0;m._paraFlash=0;
if(mutation){m.mutation=mutation;applyMutation(m);}
return m;
}
if(type==='diamond'){
const m=_newMonsterBase(type,x,y,{...defs.diamond,maxHp:defs.diamond.hp});
m._diamondTimer=8;
if(mutation){m.mutation=mutation;applyMutation(m);}
return m;
}
if(type==='dodecagon'){
const m=_newMonsterBase(type,x,y,{...defs.dodecagon,maxHp:defs.dodecagon.hp});
m._baseHp=m.maxHp;m._baseDmg=m.damage;m._baseSpeed=m.speed;m._baseRadius=m.radius;
if(mutation){m.mutation=mutation;applyMutation(m);}
return m;
}
const d=defs[type];
if(!d)return null;
const m=_newMonsterBase(type,x,y,{...d,maxHp:d.hp});
if(mutation){m.mutation=mutation;applyMutation(m);}
return m;
}
function createBoss(){return {type:'boss',x:50,y:50,radius:1.5,hp:777,maxHp:777,speed:2.5,damage:19,color:'#cc99ff',stroke:'#9933cc',phase:0,charging:false,chargeTimer:0,chargeDir:{x:0,y:0},chargeDistance:0,summonTimers:{square:6,mid:15,hex:20,heavy:30},skillCooldowns:{charge:0,circle:0,line:0,heal:0},circleData:null,lineData:null,healing:false,healTimer:0,invincible:false,shootTimer:1.5,stunTime:0,shield:0,stuckTimer:0};}
function createExterminateBoss(){return {type:'boss',x:50,y:50,radius:1.5,hp:343,maxHp:343,speed:2.5,damage:19,color:'#cc99ff',stroke:'#9933cc',phase:0,charging:false,chargeTimer:0,chargeDir:{x:0,y:0},chargeDistance:0,summonTimers:{square:9999,mid:9999,hex:9999,heavy:9999},skillCooldowns:{charge:0,circle:0,line:0,heal:9999},circleData:null,lineData:null,healing:false,healTimer:0,invincible:false,shootTimer:1.5,stunTime:0,shield:0,stuckTimer:0};}
// ==================== 粒子/飘字 ====================
function spawnKillParticles(x,y,color,radius){const count=Math.min(14,5+Math.floor(radius*4));for(let i=0;i<count;i++){const a=Math.random()*Math.PI*2,sp=3+Math.random()*6;particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,size:radius*(0.15+Math.random()*0.22),color,life:0.45+Math.random()*0.2,maxLife:0.65,shape:Math.random()<0.5?'square':'circle'});}}
function spawnDamageNumber(x,y,dmg,color){damageNumbers.push({x,y,text:(typeof dmg==='number'?Math.round(dmg):dmg),life:0.7,maxLife:0.7,vy:-28,vx:(Math.random()-0.5)*12,color:color||null});}
// ==================== 伤害 ====================
function damageMonster(m,dmg){
if(!m)return;
if(m.maxHit&&dmg>m.maxHit)dmg=m.maxHit;
if(m.damageReduction)dmg*=(1-m.damageReduction);
if(m.shield>0){let a=Math.min(m.shield,dmg);m.shield-=a;dmg-=a;}
const hpBefore=m.hp;if(dmg>0)m.hp-=dmg;m._hitFlash=0.08;
if(m.type==='heart'&&dmg>0&&!m.isAlly){m._heartDamaged=true;}
if(m.type==='octStar'){m.slideAccum=(m.slideAccum||0)+dmg;if(m.slideAccum>=m.slideThreshold&&!m.sliding){m.slideAccum-=m.slideThreshold;m.sliding=true;m.slideTimer=1;m.slideDir=Math.random()<0.5?-1:1;m.slideShootTimer=0;}}
if(m._mirrorId&&!_mirrorShield.has(m)){const partner=monsters.find(o=>o._mirrorId===m._mirrorId&&o!==m&&o.hp>0);if(partner){_mirrorShield.add(partner);damageMonster(partner,dmg);_mirrorShield.delete(partner);}}
if(dmg>=20&&damageNumbers.length<15)spawnDamageNumber(m.x,m.y-m.radius-0.2,dmg);
if(dmg>=60)hitstopTimer=Math.max(hitstopTimer,0.05);
if(hpBefore>0&&m.hp<=0){AudioSys.kill();spawnKillParticles(m.x,m.y,m.color||'#fff',m.radius);}else if(dmg>0){AudioSys.hit();}
}
function damageBoss(dmg){if(!boss||boss.invincible)return;if(boss.shield>0){let a=Math.min(boss.shield,dmg);boss.shield-=a;dmg-=a;}const hpBefore=boss.hp;const ratioBefore=hpBefore/boss.maxHp;if(dmg>0)boss.hp-=dmg;boss._hitFlash=0.08;if(dmg>=20&&damageNumbers.length<15)spawnDamageNumber(boss.x,boss.y-boss.radius-0.3,dmg);const ratioAfter=boss.hp/boss.maxHp;if(Math.floor(ratioAfter*5)<Math.floor(ratioBefore*5))hitstopTimer=Math.max(hitstopTimer,0.1);if(hpBefore>0&&boss.hp<=0){AudioSys.killBoss();spawnKillParticles(boss.x,boss.y,boss.color,boss.radius*1.5);}else if(dmg>0){AudioSys.hit();}}
function damageAlly(a,dmg){if(!a)return;if(a.isSun){damageMonster(a,dmg);return;}if(a.shield>0){let s=Math.min(a.shield,dmg);a.shield-=s;dmg-=s;}if(dmg>0)a.hp-=dmg;if(a&&a.isTurret)a._hitFlash=0.1;}
function damagePlayer(dmg){if(player.shield>0){let s=Math.min(player.shield,dmg);player.shield-=s;dmg-=s;}const hpBefore=player.hp;if(dmg>0)player.hp-=dmg*getPlayerDamageReductionMultiplier();if(player.hp<hpBefore){AudioSys.hurt();playerHurtFlash=0.35;}if(player.hp<=0)player.dead=true;}
// ==================== 玩家增益 ====================
function getPlayerSpeedMultiplier(){let m=1;player.foodEffects.forEach(e=>{if(e.type==='speed')m+=e.amount;});return m+upgrades.move*0.2;}
function getPlayerDamageReductionMultiplier(){let m=1;player.foodEffects.forEach(e=>{if(e.type==='reduce')m*=(1-e.amount);});return m;}
function getPlayerDamageBoostMultiplier(){let m=1;player.foodEffects.forEach(e=>{if(e.type==='damage')m*=(1+e.amount);});return m*(1+upgrades.damage*0.1);}
// ==================== 墙体与结构 ====================
function isBlocked(x,y,isPlayer,isTriangle,isBullet){
for(let s of structures)for(let w of s.walls){
if(w.dead)continue;
if(x>=w.x&&x<w.x+w.w&&y>=w.y&&y<w.y+w.h){
if(w.type==='solid'||w.type==='x')return true;
if(w.type==='hollow')return isBullet?false:true;
if(w.type==='shadow')return isBullet?true:false;
if(w.type==='circle')return (isPlayer&&!isBullet)?false:true;
if(w.type==='triangle')return (isTriangle&&!isBullet)?false:true;
}
}
return false;
}
function getWallAt(x,y){for(let s of structures)for(let w of s.walls){if(w.dead)continue;if(x>=w.x&&x<w.x+w.w&&y>=w.y&&y<w.y+w.h)return w;}return null;}
// A*寻路 (略减)
let pathCache=new Map();
function getGridPos(x,y,size){return{col:Math.floor(x/size),row:Math.floor(y/size)};}
function isGridBlocked(col,row,size,isP,isT){for(let i=0;i<size;i+=2)for(let j=0;j<size;j+=2){if(isBlocked(col*size+i,row*size+j,isP,isT,false))return true;}return false;}
function aStar(sx,sy,tx,ty,isP,isT,size=5){
let start=getGridPos(sx,sy,size),target=getGridPos(tx,ty,size);
if(start.col===target.col&&start.row===target.row)return[];
let cacheKey=`${start.col},${start.row},${target.col},${target.row},${isP?1:0},${isT?1:0}`;
if(pathCache.has(cacheKey))return pathCache.get(cacheKey).map(p=>({col:p.col,row:p.row}));
let open=[{col:start.col,row:start.row,g:0,h:0,f:0,parent:null}];
let closed=new Set();let openMap=new Map();openMap.set(start.col+','+start.row,open[0]);
while(open.length>0){
open.sort((a,b)=>a.f-b.f);let c=open.shift();openMap.delete(c.col+','+c.row);
let key=c.col+','+c.row;if(closed.has(key))continue;closed.add(key);
if(c.col===target.col&&c.row===target.row){let path=[];while(c.parent){path.unshift({col:c.col,row:c.row});c=c.parent;}if(pathCache.size>800)pathCache.clear();pathCache.set(cacheKey,path.map(p=>({col:p.col,row:p.row})));return path;}
let dirs=[{col:1,row:0},{col:-1,row:0},{col:0,row:1},{col:0,row:-1}];
for(let d of dirs){let nc=c.col+d.col,nr=c.row+d.row,nk=nc+','+nr;if(nc<0||nc>=WORLD_SIZE/size||nr<0||nr>=WORLD_SIZE/size||closed.has(nk))continue;if(isGridBlocked(nc,nr,size,isP,isT))continue;let g=c.g+1,h=Math.abs(nc-target.col)+Math.abs(nr-target.row),f=g+h;let existing=openMap.get(nk);if(existing&&existing.g<=g)continue;let node={col:nc,row:nr,g,h,f,parent:c};open.push(node);openMap.set(nk,node);}
}
return[];
}
function clearPathCache(){pathCache.clear();}
function moveToward(e,tx,ty,dt,isP,isT){
const dx=tx-e.x,dy=ty-e.y,dist=Math.hypot(dx,dy);if(!(dist>=0.01))return false;const sp=e.speed*dt;if(!isFinite(sp)||sp<=0)return false;
for(let step of [sp,sp/2,sp/4]){let nx=e.x+dx/dist*step,ny=e.y+dy/dist*step;if(isFinite(nx)&&isFinite(ny)&&!isBlocked(nx,ny,isP,isT,false)){e.x=nx;e.y=ny;return true;}nx=e.x+dx/dist*step;ny=e.y;if(isFinite(nx)&&isFinite(ny)&&!isBlocked(nx,ny,isP,isT,false)){e.x=nx;return true;}nx=e.x;ny=e.y+dy/dist*step;if(isFinite(nx)&&isFinite(ny)&&!isBlocked(nx,ny,isP,isT,false)){e.y=ny;return true;}}
return false;
}
function moveWithPathfinding(e,tx,ty,dt,isP,isT){
if(e.type==='square')return moveSquareDiscrete(e,tx,ty,dt);
if(e.type==='hexStar'){const cd=Math.hypot(player.x-e.x,player.y-e.y);const td=Math.hypot(player.x-tx,player.y-ty);if(cd<10&&td<cd-0.5)return false;}
if(moveToward(e,tx,ty,dt,isP,isT)){e.stuckTimer=0;return true;}
let path=aStar(e.x,e.y,tx,ty,isP,isT);if(path.length>0){let n=path[0];moveToward(e,n.col*5+2.5,n.row*5+2.5,dt,isP,isT);return true;}return false;
}
function teleportUnstuck(e){if(e.isSun||e.isStationary||e.type==='shenShape')return;for(let i=0;i<100;i++){let x=1+Math.random()*(WORLD_SIZE-2),y=1+Math.random()*(WORLD_SIZE-2);if(Math.hypot(x-player.x,y-player.y)<25)continue;if(!isBlocked(x,y,false,false,false)&&!isBlocked(x+e.radius,y,false,false,false)&&!isBlocked(x-e.radius,y,false,false,false)&&!isBlocked(x,y+e.radius,false,false,false)&&!isBlocked(x,y-e.radius,false,false,false)){e.x=x;e.y=y;e.stuckTimer=0;addShockwaveEffect(x,y,2,0.4);return;}}}
const SQUARE_STEP=1,SQUARE_IDLE_RATIO=0.25;
function moveSquareDiscrete(e,tx,ty,dt){
if(!isFinite(e.x)||!isFinite(e.y)){const a=Math.random()*Math.PI*2,d=6+Math.random()*4;e.x=Math.max(2,Math.min(WORLD_SIZE-2,player.x+Math.cos(a)*d));e.y=Math.max(2,Math.min(WORLD_SIZE-2,player.y+Math.sin(a)*d));e._sqState='idle';e._sqIdleTimer=0;e._sqSlideTarget=null;e._sqSlideSpeed=0;e.stuckTimer=0;return false;}
if(e._sqState===undefined){e._sqState='idle';e._sqIdleTimer=0;e._sqSlideTarget=null;e._sqSlideSpeed=0;}
if(e._sqState==='idle'){e._sqIdleTimer-=dt;if(e._sqIdleTimer>0)return false;const dx=tx-e.x,dy=ty-e.y,dist=Math.hypot(dx,dy);if(!(dist>=0.01))return false;const step=Math.min(SQUARE_STEP,dist);const nx=e.x+dx/dist*step,ny=e.y+dy/dist*step;if(!isFinite(nx)||!isFinite(ny))return false;if(isBlocked(nx,ny,false,false,false)){e._sqIdleTimer=(SQUARE_STEP/Math.max(0.1,e.speed))*SQUARE_IDLE_RATIO;e.stuckTimer=0;return false;}e._sqSlideTarget={x:nx,y:ny};const cycle=step/Math.max(0.1,e.speed);const slideTime=cycle*(1-SQUARE_IDLE_RATIO);e._sqSlideSpeed=step/Math.max(0.01,slideTime);if(!isFinite(e._sqSlideSpeed)||e._sqSlideSpeed<=0){e._sqState='idle';e._sqIdleTimer=0.05;return false;}e._sqState='sliding';return true;}
if(e._sqState==='sliding'){const target=e._sqSlideTarget;if(!target||!isFinite(target.x)||!isFinite(target.y)){e._sqState='idle';e._sqIdleTimer=0;return false;}const dx=target.x-e.x,dy=target.y-e.y,dist=Math.hypot(dx,dy);if(!(dist>=0.001)){e._sqState='idle';e._sqIdleTimer=(SQUARE_STEP/Math.max(0.1,e.speed))*SQUARE_IDLE_RATIO;return false;}const moveDist=e._sqSlideSpeed*dt;const actualDist=Math.min(dist,moveDist);const nx=e.x+dx/dist*actualDist,ny=e.y+dy/dist*actualDist;if(!isFinite(nx)||!isFinite(ny)){e._sqState='idle';e._sqIdleTimer=0;return false;}if(isBlocked(nx,ny,false,false,false)){e._sqState='idle';e._sqIdleTimer=(SQUARE_STEP/Math.max(0.1,e.speed))*SQUARE_IDLE_RATIO;return false;}e.x=nx;e.y=ny;if(actualDist>=dist){e._sqState='idle';const cycle=SQUARE_STEP/Math.max(0.1,e.speed);e._sqIdleTimer=cycle*SQUARE_IDLE_RATIO;}return true;}
return false;
}
const PARA_DEFLECT_CHANCE=0.20,PARA_DEFLECT_MIN=0.20,PARA_DEFLECT_MAX=0.40,PARA_STUCK_BOUNCES=6;
function rotateVec(v,angle){const c=Math.cos(angle),s=Math.sin(angle);const nx=v.x*c-v.y*s,ny=v.x*s+v.y*c;v.x=nx;v.y=ny;}
function updateParallelogram(m,dt){
if(!m._paraDir){const a=Math.random()*Math.PI*2;m._paraDir={x:Math.cos(a),y:Math.sin(a)};m._paraTrail=[];m._paraTrailTimer=0;m._paraBounceCount=0;m._paraFlash=0;}
const sp=m.speed;const tryX=m.x+m._paraDir.x*sp*dt,tryY=m.y+m._paraDir.y*sp*dt;let bounced=false;
if(!isBlocked(tryX,m.y,false,false,false)){m.x=tryX;}else{m._paraDir.x*=-1;bounced=true;}
if(!isBlocked(m.x,tryY,false,false,false)){m.y=tryY;}else{m._paraDir.y*=-1;bounced=true;}
if(m.x<m.radius){m.x=m.radius;m._paraDir.x=Math.abs(m._paraDir.x);bounced=true;}else if(m.x>WORLD_SIZE-m.radius){m.x=WORLD_SIZE-m.radius;m._paraDir.x=-Math.abs(m._paraDir.x);bounced=true;}
if(m.y<m.radius){m.y=m.radius;m._paraDir.y=Math.abs(m._paraDir.y);bounced=true;}else if(m.y>WORLD_SIZE-m.radius){m.y=WORLD_SIZE-m.radius;m._paraDir.y=-Math.abs(m._paraDir.y);bounced=true;}
if(bounced){m._paraBounceCount+=1;m._paraFlash=0.15;if(Math.random()<PARA_DEFLECT_CHANCE){const off=(Math.random()<0.5?1:-1)*(PARA_DEFLECT_MIN+Math.random()*(PARA_DEFLECT_MAX-PARA_DEFLECT_MIN));rotateVec(m._paraDir,off);}if(m._paraBounceCount>=PARA_STUCK_BOUNCES){m._paraBounceCount=0;rotateVec(m._paraDir,(Math.random()-0.5)*1.2);}}else{m._paraBounceCount=Math.max(0,m._paraBounceCount-dt*3);}
const dlen=Math.hypot(m._paraDir.x,m._paraDir.y)||1;m._paraDir.x/=dlen;m._paraDir.y/=dlen;if(m._paraFlash>0)m._paraFlash-=dt;
m._paraTrailTimer-=dt;if(m._paraTrailTimer<=0){m._paraTrailTimer=0.06;m._paraTrail.push({x:m.x,y:m.y,life:0.40});if(m._paraTrail.length>6)m._paraTrail.shift();}
for(let i=m._paraTrail.length-1;i>=0;i--){m._paraTrail[i].life-=dt;if(m._paraTrail[i].life<=0)m._paraTrail.splice(i,1);}
m.stuckTimer=0;
}
function findEnemyTarget(m){
if(!m.isSun){const sun=monsters.find(x=>x.isSun&&x.hp>0);if(sun){const sd=Math.hypot(sun.x-m.x,sun.y-m.y);if(sd<12){const pd=Math.hypot(player.x-m.x,player.y-m.y);let minAllyDist=Infinity;for(const a of allies)minAllyDist=Math.min(minAllyDist,Math.hypot(a.x-m.x,a.y-m.y));if(sd<pd&&sd<minAllyDist)return sun;}}}
let nearestAlly=null,allyDist=Infinity;for(let a of allies){let d=Math.hypot(a.x-m.x,a.y-m.y);if(d<allyDist){allyDist=d;nearestAlly=a;}}
let playerDist=Math.hypot(player.x-m.x,player.y-m.y);
const group1=['square','triangle','trapezoid','isoscelesTrapezoid','hexagon','arrow','star','fourStar'];
if(group1.includes(m.type))return (nearestAlly&&allyDist<playerDist)?nearestAlly:'player';
if(m.type==='pentagon'&&m.gen===2)return (nearestAlly&&allyDist<playerDist)?nearestAlly:'player';
const group2=['octagon','crescent','kite','lShape','hexStar','spiral','octStar'];
if(group2.includes(m.type)||(m.type==='pentagon'&&m.gen===1)){if(nearestAlly){if(allyDist<=3)return nearestAlly;if(playerDist-allyDist>=20)return nearestAlly;}return 'player';}
if(m.type==='boss')return 'player';
return (nearestAlly&&allyDist<playerDist)?nearestAlly:'player';
}
function getTargetPos(m){let t=findEnemyTarget(m);if(t==='player')return{x:player.x,y:player.y,target:'player'};return{x:t.x,y:t.y,target:t};}
// 结构模板 (含新结构位置)
const structureTemplates={
pillar:{walls:[{x:3,y:1,w:2,h:8,type:'solid'}]},
yinYang:{walls:[{x:1,y:3,w:8,h:2,type:'solid'}]},
tiger:{walls:[{x:1,y:1,w:7,h:1,type:'solid'},{x:7,y:2,w:1,h:7,type:'solid'}]},
mountain:{walls:[{x:2,y:2,w:4,h:4,type:'x',hp:200,maxHp:200,dead:false}]},
city:{walls:[{x:1,y:1,w:1,h:8,type:'solid'},{x:2,y:8,w:7,h:1,type:'solid'},{x:8,y:1,w:1,h:7,type:'solid'},{x:2,y:1,w:1,h:1,type:'solid'},{x:7,y:1,w:1,h:1,type:'solid'},{x:1,y:4,w:1,h:1,type:'hollow'},{x:8,y:6,w:1,h:1,type:'hollow'},{x:3,y:8,w:1,h:1,type:'hollow'}]},
altar:{walls:[{x:1,y:1,w:2,h:1,type:'solid'},{x:1,y:2,w:1,h:1,type:'solid'},{x:1,y:7,w:1,h:1,type:'solid'},{x:1,y:8,w:2,h:1,type:'solid'},{x:7,y:1,w:2,h:1,type:'solid'},{x:8,y:2,w:1,h:1,type:'solid'},{x:7,y:8,w:2,h:1,type:'solid'},{x:8,y:7,w:1,h:1,type:'solid'},{x:4,y:4,w:2,h:2,type:'solid'}]},
trench:{walls:[{x:3,y:2,w:1,h:1,type:'hollow'},{x:2,y:3,w:1,h:2,type:'hollow'},{x:4,y:6,w:4,h:1,type:'hollow'}]},
truce:{walls:[{x:1,y:1,w:4,h:1,type:'shadow'},{x:4,y:1,w:1,h:3,type:'shadow'},{x:8,y:1,w:1,h:4,type:'shadow'},{x:6,y:4,w:3,h:1,type:'shadow'},{x:5,y:8,w:4,h:1,type:'shadow'},{x:5,y:6,w:1,h:3,type:'shadow'},{x:1,y:5,w:3,h:1,type:'shadow'},{x:1,y:5,w:1,h:4,type:'shadow'}]},
dogHole:{walls:[{x:1,y:2,w:1,h:1,type:'solid'},{x:1,y:3,w:1,h:1,type:'solid'},{x:1,y:4,w:1,h:1,type:'solid'},{x:1,y:5,w:1,h:1,type:'solid'},{x:1,y:6,w:1,h:1,type:'solid'},{x:1,y:7,w:1,h:1,type:'circle'},{x:1,y:8,w:1,h:1,type:'solid'}]},
wallEye:{walls:[{x:1,y:2,w:1,h:1,type:'solid'},{x:2,y:2,w:1,h:1,type:'solid'},{x:3,y:2,w:1,h:1,type:'solid'},{x:4,y:2,w:1,h:1,type:'solid'},{x:5,y:2,w:1,h:1,type:'triangle'},{x:5,y:3,w:1,h:1,type:'triangle'},{x:5,y:4,w:1,h:1,type:'solid'},{x:5,y:5,w:1,h:1,type:'solid'},{x:5,y:6,w:1,h:1,type:'solid'},{x:5,y:7,w:1,h:1,type:'solid'},{x:5,y:8,w:1,h:1,type:'hollow'},{x:6,y:8,w:1,h:1,type:'hollow'},{x:7,y:8,w:1,h:1,type:'hollow'},{x:8,y:8,w:1,h:1,type:'hollow'}]}
};
const structureNames=['pillar','yinYang','tiger','mountain','city','altar','trench','truce','dogHole','wallEye'];
function getStructureWorldPos(bc,br,t){let walls=[],offX=bc*10,offY=br*10;structureTemplates[t]?.walls.forEach(w=>walls.push({x:offX+w.x,y:offY+w.y,w:w.w,h:w.h,type:w.type,hp:w.hp||null,maxHp:w.maxHp||null,dead:false}));return walls;}
function generateStructures(){
let pbc=Math.floor(player.x/10),pbr=Math.floor(player.y/10);
let kept=structures.filter(s=>s.bigCol===pbc&&s.bigRow===pbr);
let others=structures.filter(s=>!(s.bigCol===pbc&&s.bigRow===pbr)).sort(()=>Math.random()-0.5);
for(let i=0;i<2&&i<others.length;i++)kept.push(others[i]);
structures=structures.filter(s=>kept.includes(s));
let cells=[];for(let i=0;i<10;i++)for(let j=0;j<10;j++)if(i!==pbc&&j!==pbr)cells.push({col:i,row:j});
cells.sort(()=>Math.random()-0.5);
let count=Math.min(2+Math.floor(Math.random()*4),cells.length);
for(let i=0;i<count;i++){let c=cells[i],t=structureNames[Math.floor(Math.random()*structureNames.length)];structures.push({bigCol:c.col,bigRow:c.row,template:t,walls:getStructureWorldPos(c.col,c.row,t)});}
let unique=[];structures.forEach(s=>{if(!unique.find(u=>u.bigCol===s.bigCol&&u.bigRow===s.bigRow))unique.push(s);});structures=unique;clearPathCache();
}
// ==================== 波次系统 ====================
function getSpawnPos(){for(let i=0;i<100;i++){let x=1+Math.random()*(WORLD_SIZE-2),y=1+Math.random()*(WORLD_SIZE-2);if(Math.hypot(x-player.x,y-player.y)>=10)return{x,y};}return{x:1,y:1};}
function getValue(t){return{square:1,trapezoid:2,isoscelesTrapezoid:2,triangle:3,hexagon:6,kite:6,crescent:6,octagon:7,pentagon:8,arrow:12,lShape:2,star:2,hexStar:4,spiral:6,fourStar:1,octStar:9,parallelogram:2,heart:7,diamond:10,smallDiamond:1,solidQuad:24,dodecagon:8,sun:25,zShape:6,shenShape:12}[t]||1;}
function getCrystalDropRate(t){let v=getValue(t);return v<=2?0.01:v<=3?0.02:v<=6?0.04:v<=8?0.09:0.2;}
function tryDropCrystal(t,x,y){if(Math.random()<getCrystalDropRate(t)){crystals++;saveGame();}}
function getAvailableTypes(){let t=['square','triangle','trapezoid','isoscelesTrapezoid','lShape','star','fourStar'];if(waveCount>=2)t.push('hexStar');if(waveCount>=4)t.push('spiral');if(waveCount>=3)t.push('hexagon');if(waveCount>=4)t.push('kite');if(waveCount>=5)t.push('octagon','crescent');if(waveCount>=6)t.push('pentagon');if(waveCount>=8)t.push('octStar');if(waveCount>=10)t.push('arrow');return t;}
function checkAbsenceCompensation(){let c=[];if(waveCount>=3&&absenceCounters.hexagon>=3)c.push('hexagon');if(waveCount>=4&&absenceCounters.kite>=3)c.push('kite');if(waveCount>=5&&absenceCounters.octagon>=3)c.push('octagon');if(waveCount>=5&&absenceCounters.crescent>=3)c.push('crescent');if(waveCount>=6&&absenceCounters.pentagon>=3)c.push('pentagon');if(waveCount>=10&&absenceCounters.arrow>=3)c.push('arrow');return c;}
function spawnGuaranteed(t){let p=getSpawnPos(),m=createMonster(t,p.x,p.y);monsters.push(m);spawnedThisWave.push(t);absenceCounters[t]=0;if(t==='pentagon')pentagonSplitAtStart(m);}
function spawnWave(){
const wasSun=_sunNextWave;const bonus=window._nextWaveBudgetBonus;_sunNextWave=false;window._nextWaveBudgetBonus=0;
if(spawnedThisWave.length){['pentagon','hexagon','kite','octagon','crescent','arrow'].forEach(t=>{if(spawnedThisWave.includes(t))absenceCounters[t]=0;else absenceCounters[t]=(absenceCounters[t]||0)+1;});}
waveCount++;monsters=[];spawnedThisWave=[];
if(waveCount===13){bossIntro={phase:'circle',timer:0};return;}
massMutationWave=false;speedBattleWave=false;
if(waveCount>=5&&Math.random()<0.05){massMutationWave=true;eventNotice='⚠️ 群体变异波!所有敌人必定变异!';}
else if(waveCount>=5&&Math.random()<0.05){speedBattleWave=true;speedBattleTimer=45;speedBattleOverTime=0;eventNotice='⏱️ 速战速决!45秒内消灭所有敌人!';}
else if(waveCount>=5&&Math.random()<0.1){
specialEvent=Math.random()<0.5?'infinite':'singleType';
if(specialEvent==='infinite'){eventNotice='⚠️ 无尽生存!坚持90秒!';infiniteSurvivalMode=true;infiniteSurvivalTime=90;generateStructures();return;}
let types=getAvailableTypes();if(lastSingleType&&types.includes(lastSingleType)&&types.length>1)types=types.filter(t=>t!==lastSingleType);
let chosen=types[Math.floor(Math.random()*types.length)];lastSingleType=chosen;eventNotice=`⚠️ 单一敌人波:${chosen}!`;
for(let i=0;i<5+(waveCount-5)*2;i++){let p=getSpawnPos(),m=createMonster(chosen,p.x,p.y);if(m)monsters.push(m);}
if(monsters.length===0)spawnFallbackWave();generateStructures();return;
}else{eventNotice='';specialEvent=null;}
let guaranteed=[];if(waveCount===3)guaranteed.push('hexagon');if(waveCount===4)guaranteed.push('kite');if(waveCount===5)guaranteed.push('octagon','crescent');if(waveCount===6)guaranteed.push('pentagon');if(waveCount===7)guaranteed.push('heart');if(waveCount===10)guaranteed.push('arrow','diamond');if(waveCount===12)guaranteed.push('solidQuad');checkAbsenceCompensation().forEach(t=>{if(!guaranteed.includes(t))guaranteed.push(t);});guaranteed.forEach(t=>spawnGuaranteed(t));
let target=waveCount<=5?5+(waveCount-1)*2:waveCount<=10?13+(waveCount-5)*3:waveCount<=15?28+(waveCount-10)*4:48+(waveCount-15)*5;
const poolBudget=target;
let pool=[];for(let i=0;i<poolBudget;i++)pool.push('square');for(let i=0;i<poolBudget/2;i++)pool.push('triangle');for(let i=0;i<poolBudget/3;i++)pool.push('trapezoid');for(let i=0;i<poolBudget/4;i++)pool.push('isoscelesTrapezoid');for(let i=0;i<poolBudget/3;i++)pool.push('lShape');for(let i=0;i<poolBudget/4;i++)pool.push('star');for(let i=0;i<poolBudget/4;i++)pool.push('fourStar');
if(waveCount>=2)for(let i=0;i<poolBudget/4;i++)pool.push('hexStar');if(waveCount>=4)for(let i=0;i<poolBudget/5;i++)pool.push('spiral');if(waveCount>=3)for(let i=0;i<poolBudget/5;i++)pool.push('hexagon');if(waveCount>=4)for(let i=0;i<poolBudget/6;i++)pool.push('kite');if(waveCount>=5){for(let i=0;i<poolBudget/7;i++)pool.push('octagon');for(let i=0;i<poolBudget/8;i++)pool.push('crescent');}if(waveCount>=6)for(let i=0;i<poolBudget/8;i++)pool.push('pentagon');if(waveCount>=8)for(let i=0;i<poolBudget/10;i++)pool.push('octStar');if(waveCount>=10)pool.push('arrow');
if(waveCount>=3){const paraCount=Math.max(2,Math.floor(poolBudget/8));for(let i=0;i<paraCount;i++)pool.push('parallelogram');}
if(waveCount>=4){const heartCount=Math.max(1,Math.floor(poolBudget/12));for(let i=0;i<heartCount;i++)pool.push('heart');}
if(waveCount>=8){const diaCount=Math.max(1,Math.floor(poolBudget/15));for(let i=0;i<diaCount;i++)pool.push('diamond');}
if(waveCount>=6)pool.push('dodecagon');if(waveCount>=9)pool.push('solidQuad');
// V50: Z与申进池子
if(waveCount>=5){const zCount=Math.max(1,Math.floor(poolBudget/18));for(let i=0;i<zCount;i++)pool.push('zShape');}
if(waveCount>=7&&poolBudget>=15){pool.push('shenShape');if(waveCount>=10&&poolBudget>=25)pool.push('shenShape');}
pool.sort(()=>Math.random()-0.5);
let currentValue=0;for(let i=0;i<pool.length&¤tValue<poolBudget;i++){let t=pool[i],v=getValue(t);if(currentValue+v<=poolBudget){let p=getSpawnPos(),m=createMonster(t,p.x,p.y);if(m){monsters.push(m);spawnedThisWave.push(t);currentValue+=v;}}}
while(currentValue<poolBudget){let p=getSpawnPos(),m=createMonster('square',p.x,p.y);if(m){monsters.push(m);spawnedThisWave.push('square');currentValue++;}}
generateStructures();
if(waveCount!==13&&waveCount>=5&&!massMutationWave&&!speedBattleWave&&!specialEvent&&Math.random()<0.10){monsters=[];boss=createExterminateBoss();_exterminateMode=true;eventNotice='⚠️ 斩草除根!独自面对Boss!';return;}
if(wasSun&&waveCount!==13){const p=getSpawnPos();const sun=createMonster('sun',p.x,p.y);if(sun)monsters.push(sun);eventNotice='☀️ 太阳降临!';for(let i=monsters.length-1;i>=0;i--){if(monsters[i].type==='crescent')monsters.splice(i,1);}}
if(bonus>0){let remaining=bonus;const types=['square','triangle','trapezoid','isoscelesTrapezoid'];while(remaining>0){const t=types[Math.floor(Math.random()*types.length)];const v=getValue(t);if(v>remaining)break;const p=getSpawnPos();const m=createMonster(t,p.x,p.y);if(m){monsters.push(m);remaining-=v;}}eventNotice='⚠️ Z字形的代价:本波敌人更多!';}
}
function spawnFallbackWave(){eventNotice='⚠️ 已修复空波次';for(let i=0;i<8;i++){let p=getSpawnPos(),m=createMonster('square',p.x,p.y);if(m)monsters.push(m);}generateStructures();}
function pentagonSplitAtStart(m){if(m?.type==='pentagon')pentagonSplit(m);}
function pentagonSplit(m){let dmg=m.gen===0?8:m.gen===1?5:0,baseAngle=Math.random()*Math.PI*2,goldenAngle=137.5*Math.PI/180;for(let i=0;i<5;i++){let a=baseAngle+i*goldenAngle;enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a)*2,vy:Math.sin(a)*2,r:0.3,dmg,life:30,type:'goldenTriangle',turnRate:1,growthRate:0.5,speed:2});}if(m.gen<2){let c=createMonster('pentagon',m.x,m.y);c.mutation=m.mutation;c.gen=m.gen+1;c.radius=0.6*Math.pow(0.7,c.gen);c.hp=c.maxHp=c.gen===1?60:20;c.speed=c.gen===1?3:4.5;c.damage=c.gen>=2?4:5;c.value=c.gen===1?4:1;c.canSplit=c.gen<2;c.stateTimer=c.gen===0?30:10;monsters.push(c);}}
function pentagonDeathBurst(m){let baseAngle=Math.random()*Math.PI*2,goldenAngle=137.5*Math.PI/180;for(let i=0;i<5;i++){let a=baseAngle+i*goldenAngle;enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a)*2,vy:Math.sin(a)*2,r:0.3,dmg:2,life:30,type:'goldenTriangle',turnRate:1,growthRate:0.5,speed:2});}}
function spawnFragments(type,x,y,value){if(type==='boss'){for(let i=0;i<Math.floor(value/10);i++)fragments.push({x:x+(Math.random()-0.5)*3,y:y+(Math.random()-0.5)*3,value:10,radius:0.6,type:'big'});return;}let whole=Math.floor(value),small=Math.round((value-whole)*10);for(let i=0;i<whole;i++)fragments.push({x:x+(Math.random()-0.5)*1.5,y:y+(Math.random()-0.5)*1.5,value:1,radius:0.2,type});for(let i=0;i<small;i++)fragments.push({x:x+(Math.random()-0.5)*1.5,y:y+(Math.random()-0.5)*1.5,value:0.1,radius:0.12,type:'small'});if(type==='zShape'){for(let i=0;i<4;i++)fragments.push({x:x+(Math.random()-0.5)*2,y:y+(Math.random()-0.5)*2,value:1,radius:0.2,type:'zBonus'});}}
function updateFragments(dt){for(let i=fragments.length-1;i>=0;i--){let f=fragments[i];const dx=player.x-f.x,dy=player.y-f.y;const dist=Math.hypot(dx,dy);if(dist<=0.001){energy+=f.value;fragments.splice(i,1);continue;}if(dist<5){f.x+=dx/dist*15*dt;f.y+=dy/dist*15*dt;if(dist<0.5){energy+=f.value;fragments.splice(i,1);}}}}
function spawnInfiniteEnemy(){let types=getAvailableTypes(),weights=[];types.forEach(t=>{let v=getValue(t);weights.push(v<=2?25:v<=3?20:v<=6?15:v<=8?10:5);});let total=weights.reduce((a,b)=>a+b,0),rand=Math.random()*total,chosen=types[0];for(let i=0;i<types.length;i++){rand-=weights[i];if(rand<=0){chosen=types[i];break;}}if(lastSingleType===chosen&&Math.random()<0.7&&types.length>1){let av=types.filter(t=>t!==lastSingleType);chosen=av[Math.floor(Math.random()*av.length)];}lastSingleType=chosen;let p=getSpawnPos(),m=createMonster(chosen,p.x,p.y);if(m)monsters.push(m);}
// ← 第 1 段结束(继续粘下一段)
// ==================== 模块4:玩家攻击 ====================
function getSupportDiscount(){return 1-upgrades.support*0.1;}
function getMaxHp(){return player.maxHp+upgrades.hp*20;}
function performMelee(){
const w=weapons[currentWeaponIndex];
if(w.type==='melee'&&!w.damagePattern){
const dm=getPlayerDamageBoostMultiplier(),dmg=w.damage*dm;
attackEffectTimer=0.3;const rs=w.range*w.range,arc=w.arcAngle/2;
monsters.forEach(m=>{const dx=m.x-player.x,dy=m.y-player.y,ds=dx*dx+dy*dy,d=Math.sqrt(ds),ol=d<player.radius+m.radius;if(ds<=rs||ol){let hit=ol;if(!hit){const a=Math.atan2(dy,dx);let diff=a-player.aimAngle;while(diff>Math.PI)diff-=2*Math.PI;while(diff<-Math.PI)diff+=2*Math.PI;hit=Math.abs(diff)<=arc;}if(hit)damageMonster(m,dmg);}});
if(boss){const dx=boss.x-player.x,dy=boss.y-player.y,ds=dx*dx+dy*dy;if(ds<=rs||Math.sqrt(ds)<player.radius+boss.radius)damageBoss(dmg);}
AudioSys.hitMelee();return;
}
const dm=getPlayerDamageBoostMultiplier(),w2=weapons[0],dmg=w2.damagePattern[attackCount%3]*dm;
attackCount++;attackEffectTimer=0.3;let rs=w2.range*w2.range,arc=w2.arcAngle/2;
monsters.forEach(m=>{let dx=m.x-player.x,dy=m.y-player.y,ds=dx*dx+dy*dy,d=Math.sqrt(ds),ol=d<player.radius+m.radius;if(ds<=rs||ol){let hit=ol;if(!hit){let a=Math.atan2(dy,dx),diff=a-player.aimAngle;while(diff>Math.PI)diff-=2*Math.PI;while(diff<-Math.PI)diff+=2*Math.PI;hit=Math.abs(diff)<=arc;}if(hit)damageMonster(m,dmg);}});
if(boss){let dx=boss.x-player.x,dy=boss.y-player.y,ds=dx*dx+dy*dy;if(ds<=rs||Math.sqrt(ds)<player.radius+boss.radius)damageBoss(dmg);}
AudioSys.hitMelee();
}
function performShoot(){
let dm=getPlayerDamageBoostMultiplier(),w=weapons[currentWeaponIndex];
if(w.type==='blazing'){
if(w.currentMag<=0){startReload();return;}
w.currentMag--;const a=player.aimAngle;
bullets.push({x:player.x+Math.cos(a)*0.5,y:player.y+Math.sin(a)*0.5,vx:Math.cos(a)*w.bulletSpeed,vy:Math.sin(a)*w.bulletSpeed,r:w.bulletRadius,dmg:w.damage*dm,life:5,type:'blazing',color:w.color,penetrated:new Set()});
AudioSys.shoot();if(w.currentMag<=0)startReload();return;
}
if(w.currentMag<=0){startReload();return;}
w.currentMag--;let a=player.aimAngle;
if(['gun','kiteGun','shotgun','paraGun','sniper','lmg','hexGun'].includes(w.type)){
let count=w.type==='shotgun'?5:1,spread=w.type==='shotgun'?Math.PI/6:0;
for(let i=0;i<count;i++){
let angle=a+(i-(count-1)/2)*(spread/(count-1||1));
let b={x:player.x+Math.cos(angle)*0.5,y:player.y+Math.sin(angle)*0.5,vx:Math.cos(angle)*w.bulletSpeed,vy:Math.sin(angle)*w.bulletSpeed,r:w.bulletRadius,dmg:w.damage*dm,life:w.type==='hexGun'?w.homingTime:30,type:w.type,color:w.color,bounces:0,debuff:w.type==='kiteGun',homing:w.type==='hexGun',homingSpeed:5};
if(w.type==='lmg'){let sd=w.fireTime>5?30:w.fireTime>3?15:5,off=(Math.random()-0.5)*sd*Math.PI/180,sp=Math.hypot(b.vx,b.vy),na=Math.atan2(b.vy,b.vx)+off;b.vx=Math.cos(na)*sp;b.vy=Math.sin(na)*sp;let rec=w.fireTime>5?1:w.fireTime>3?0.6:0.3;player.x-=Math.cos(a)*rec*0.016;player.y-=Math.sin(a)*rec*0.016;}
bullets.push(b);
}
}
if(w.currentMag<=0)startReload();
AudioSys.shoot();
}
function performBurn(){let dm=getPlayerDamageBoostMultiplier(),w=weapons[11];if(w.count<=0)return;w.count--;let a=player.aimAngle;grenades.push({x:player.x,y:player.y,vx:Math.cos(a)*w.speed,vy:Math.sin(a)*w.speed,r:0.4,dmg:w.damage*dm,dist:0,maxDist:5,exR:w.burnRadius,type:'burn',burnDuration:w.burnDuration});}
function applyKiteGunDebuff(m){if(!m)return;m.slowTimer=5;m.poisonTimer=5;if(!m.poisonTick||m.poisonTick>0.2)m.poisonTick=0.2;m.poisonStacks=(m.poisonStacks||0)+1;}
function startReload(){let w=weapons[currentWeaponIndex];if(w.type==='blazing'&&!w.isReloading&&w.currentMag<=0){w.isReloading=true;w.reloadTimer=w.reloadTime;return;}if(!w.isReloading&&['gun','kiteGun','shotgun','paraGun','saw','sniper','lmg','hexGun'].includes(w.type)){w.isReloading=true;w.reloadTimer=w.reloadTime;}}
function performBoomerang(){let dm=getPlayerDamageBoostMultiplier(),w=weapons[2];if(!w.ready||boomerang)return;w.ready=false;let a=player.aimAngle;boomerang={x:player.x,y:player.y,vx:Math.cos(a)*w.speed,vy:Math.sin(a)*w.speed,ox:player.x,oy:player.y,maxR:w.range,r:w.radius,dmg:w.damage*dm,state:'outward',hitO:new Set(),hitR:new Set()};}
function performGrenade(){let dm=getPlayerDamageBoostMultiplier(),w=weapons[5];if(w.count<=0)return;w.count--;let a=player.aimAngle;grenades.push({x:player.x,y:player.y,vx:Math.cos(a)*w.speed,vy:Math.sin(a)*w.speed,r:0.4,dmg:w.damage*dm,dist:0,maxDist:5,exR:w.explosionRadius,type:'grenade'});}
function performTrap(){let w=weapons[7];if(w.count<=0)return;w.count--;traps.push({x:player.x,y:player.y,radius:3,triggerRadius:7,active:true});}
function performSaw(){let dm=getPlayerDamageBoostMultiplier(),w=weapons[6];if(w.currentMag<=0){startReload();return;}w.currentMag--;monsters.forEach(m=>{if(Math.hypot(m.x-player.x,m.y-player.y)<=w.range)damageMonster(m,w.damage*dm);});if(boss&&Math.hypot(boss.x-player.x,boss.y-player.y)<=w.range)damageBoss(w.damage*dm);}
// ==================== 模块4:支援系统 ====================
function useFood(type){
let costMulti=getSupportDiscount();
if(type==='cookie'&&energy>=10*costMulti){energy-=10*costMulti;player.hp=Math.min(getMaxHp(),player.hp+5);player.foodEffects.push({type:'speed',amount:0.2,time:5});}
if(type==='milk'&&energy>=20*costMulti){energy-=20*costMulti;player.hp=Math.min(getMaxHp(),player.hp+15);player.foodEffects.push({type:'reduce',amount:0.2,time:20});}
if(type==='chocolate'&&energy>=30*costMulti){energy-=30*costMulti;player.hp=Math.min(getMaxHp(),player.hp+30);player.foodEffects.push({type:'damage',amount:0.1,time:15});}
}
function summonAlly(type){
if(type==='hexStar'){
if(!window.summonUnlocks.hexStar)return;
const cost=30*getSupportDiscount();
if(energy<cost)return;energy-=cost;
const a=Math.random()*Math.PI*2;
const ally=createMonster('hexStar',player.x+Math.cos(a)*2,player.y+Math.sin(a)*2);
if(!ally)return;ally.isAlly=true;ally.mutation=null;ally.hp=ally.maxHp;ally.attackCooldown=0;ally.buffs=[];ally.shootTimer=2;
allies.push(ally);AudioSys.upgrade();return;
}
let costs={square:5,trapezoid:10,triangle:15,hexagon:30,kite:25,octagon:35,pentagon:30,crescent:18};
let cost=costs[type]*getSupportDiscount();
if(energy<cost)return;energy-=cost;
let a=Math.random()*Math.PI*2,ally=createMonster(type,player.x+Math.cos(a)*2,player.y+Math.sin(a)*2);
ally.isAlly=true;ally.mutation=null;ally.hp=ally.maxHp;ally.attackCooldown=0;
if(type==='trapezoid'){ally.state='chase';ally.chargeDir={x:0,y:0};ally.chargeTimer=0;}
if(type==='pentagon')ally.stateTimer=20;
if(type==='hexagon'){ally.buffTimer=12;ally.shockwaveState='idle';ally.shockwaveTimer=0;ally.shockCooldown=0;ally.shockwaveTimer2=0;}
allies.push(ally);
}
function airStrike(){let cost=50*getSupportDiscount();if(airStrikeCooldown>0||energy<cost)return;energy-=cost;airStrikeCooldown=180;let a=player.aimAngle;airStrikeData={startX:player.x,startY:player.y,dx:Math.cos(a),dy:Math.sin(a),timer:0.8,active:true,angle:a};}
function summonSun(){if(_sunNextWave)return;const cost=50*getSupportDiscount();if(energy<cost)return;energy-=cost;_sunNextWave=true;eventNotice='☀️ 下一波将出现太阳!';AudioSys.upgrade();}
const TURRET_HP=200,TURRET_RADIUS=0.4,TURRET_RANGE=20,TURRET_ALLOWED=new Set(['boomerang','kiteGun','shotgun','saw','paraGun','sniper','lmg','hexGun']);
function placeTurret(){const cost=45*getSupportDiscount();if(energy<cost)return;energy-=cost;allies.push({x:player.x,y:player.y,radius:TURRET_RADIUS,hp:TURRET_HP,maxHp:TURRET_HP,isTurret:true,isAlly:true,type:'turret',weaponType:null,weaponData:null,attackCooldown:0,aimAngle:0,buffs:[],shield:0,invincibleTime:0,_hitFlash:0,damageMult:1});AudioSys.upgrade();eventNotice='🔫 炮塔座架已部署';}
function putWeaponInTurret(t){const idx=currentWeaponIndex;const w=weapons[idx];if(!w||!w.runUnlocked||!TURRET_ALLOWED.has(w.type)||t.weaponType)return;t.weaponType=w.type;t.weaponData={name:w.name,damage:w.damage,attackInterval:w.attackInterval,bulletSpeed:w.bulletSpeed||0,bulletRadius:w.bulletRadius||0.2,color:w.color||'#fff',range:w.range||TURRET_RANGE};w.runUnlocked=false;if(w.type==='boomerang')w.ready=false;if(w.currentMag!==undefined)w.currentMag=w.magSize;if(w.count!==undefined)w.count=0;let newIdx=0;for(let i=0;i<weapons.length;i++){if(weapons[i].runUnlocked&&i!==idx){newIdx=i;break;}}currentWeaponIndex=newIdx;AudioSys.upgrade();eventNotice='✅ 已放入:'+w.name;}
function getTurretRange(weaponType){switch(weaponType){case'sniper':return 40;case'saw':return 3;default:return 20;}}
function updateTurret(t,dt){
if(t._hitFlash>0)t._hitFlash-=dt;if(t.invincibleTime>0)t.invincibleTime-=dt;if(t.attackCooldown>0)t.attackCooldown-=dt;
if(!t.weaponType)return;const range=getTurretRange(t.weaponType);let target=null,td=Infinity;
for(const m of monsters){if(m.hp<=0)continue;const d=Math.hypot(m.x-t.x,m.y-t.y);if(d<td&&d<=range){td=d;target=m;}}
if(!target&&boss&&boss.hp>0){const d=Math.hypot(boss.x-t.x,boss.y-t.y);if(d<=range){td=d;target=boss;}}
if(!target)return;t.aimAngle=Math.atan2(target.y-t.y,target.x-t.x);if(t.attackCooldown>0)return;fireTurret(t);t.attackCooldown=t.weaponData.attackInterval;
}
function fireTurret(t){
const wd=t.weaponData,dmg=wd.damage*getPlayerDamageBoostMultiplier(),a=t.aimAngle;
if(t.weaponType==='saw'){monsters.forEach(m=>{if(m.hp>0&&Math.hypot(m.x-t.x,m.y-t.y)<=2.5)damageMonster(m,dmg);});if(boss&&Math.hypot(boss.x-t.x,boss.y-t.y)<=2.5)damageBoss(dmg);AudioSys.hitMelee();return;}
if(t.weaponType==='boomerang'){allyBullets.push({x:t.x,y:t.y,vx:Math.cos(a)*12,vy:Math.sin(a)*12,r:0.35,dmg,life:1.6,type:'turretBullet',color:'#ff99ff'});AudioSys.shoot();return;}
if(t.weaponType==='shotgun'){for(let i=0;i<5;i++){const ang=a+(i-2)*(Math.PI/6/4);allyBullets.push({x:t.x+Math.cos(ang)*0.5,y:t.y+Math.sin(ang)*0.5,vx:Math.cos(ang)*(wd.bulletSpeed||12),vy:Math.sin(ang)*(wd.bulletSpeed||12),r:wd.bulletRadius||0.25,dmg,life:1.6,type:'turretBullet',color:wd.color});}AudioSys.shoot();return;}
const b={x:t.x+Math.cos(a)*0.5,y:t.y+Math.sin(a)*0.5,vx:Math.cos(a)*(wd.bulletSpeed||12),vy:Math.sin(a)*(wd.bulletSpeed||12),r:wd.bulletRadius||0.2,dmg,life:1.6,type:'turretBullet',color:wd.color};
if(t.weaponType==='hexGun'){b.homing=true;b.homingSpeed=5;b.homingRange=20;}
allyBullets.push(b);AudioSys.shoot();
}
// ==================== 模块4:爆裂/钻石 ====================
function burstExplode(m){let radius=m.burstRadius||Math.min(m.maxHp/10,10);enemyHexShockwaves.push({x:m.x,y:m.y,currentRadius:0,maxRadius:radius,speed:7.5,damage:10,hitSet:new Set()});addShockwaveEffect(m.x,m.y,radius,0.4);}
function spawnBurstChildren(m){for(let i=0;i<2;i++){let angle=Math.random()*Math.PI*2,dist=0.5+Math.random()*0.5;let c=createMonster(m.type,m.x+Math.cos(angle)*dist,m.y+Math.sin(angle)*dist);if(!c)continue;c.burst=false;c.burstChild=true;c.mutation=null;c.hp=c.maxHp=m.maxHp*0.5;c.damage*=0.5;c.radius*=0.7;c.value*=0.5;if(c.type==='arrow'&&c.tail)c.tail.forEach(t=>t.hp=t.maxHp=15);monsters.push(c);}}
const DIAMOND_SPLIT_INTERVAL=8,DIAMOND_MAX_SPLIT=10,DIAMOND_SPLIT_RADIUS=9,DIAMOND_SMALL_HP=25;
function splitDiamond(m){const hp=m.hp;if(hp<DIAMOND_SMALL_HP)return;const count=Math.min(DIAMOND_MAX_SPLIT,Math.floor(hp/DIAMOND_SMALL_HP));const eachHp=Math.floor(hp/count);const idx=monsters.indexOf(m);if(idx>=0)monsters.splice(idx,1);for(let i=0;i<count;i++){const a=Math.random()*Math.PI*2,d=Math.random()*DIAMOND_SPLIT_RADIUS;const sx=Math.max(1,Math.min(WORLD_SIZE-1,m.x+Math.cos(a)*d));const sy=Math.max(1,Math.min(WORLD_SIZE-1,m.y+Math.sin(a)*d));const small=createMonster('smallDiamond',sx,sy);if(small){small.hp=small.maxHp=eachHp;monsters.push(small);}}addShockwaveEffect(m.x,m.y,DIAMOND_SPLIT_RADIUS,0.8);_diamondMergeTimer=DIAMOND_SPLIT_INTERVAL;}
function mergeDiamondsAround(anchor){const x=anchor.x,y=anchor.y;let totalHp=0;const toRemove=[];for(const m of monsters){if(m.type==='smallDiamond'&&m.hp>0&&Math.hypot(m.x-x,m.y-y)<=DIAMOND_SPLIT_RADIUS){totalHp+=m.hp;toRemove.push(m);}}if(toRemove.length===0)return;for(const sm of toRemove){const idx=monsters.indexOf(sm);if(idx>=0)monsters.splice(idx,1);}const big=createMonster('diamond',x,y);if(big){big.hp=big.maxHp=Math.min(250,totalHp);monsters.push(big);}addShockwaveEffect(x,y,DIAMOND_SPLIT_RADIUS,0.6);}
// ==================== 模块4:爱心 ====================
const HEART_HOSTILE_DMG=7,HEART_HOSTILE_LIFESTEAL=1.0,HEART_ALLY_HEAL=3,HEART_ALLY_HEAL_INTERVAL=3,HEART_CONVERT_INTERVAL=25,HEART_CONVERT_RANGE=20,HEART_CONFUSE_TIME=5,HEART_CONFUSE_CHANGE_MIN=0.15,HEART_CONFUSE_CHANGE_MAX=0.40,HEART_COUNTER_DMG=7,HEART_COUNTER_COOLDOWN=0.5,HEART_COUNTER_DISARM=0.3;
const monsterNameMap={square:'正方形',triangle:'三角形',trapezoid:'直角梯形',isoscelesTrapezoid:'等腰梯形',pentagon:'正五边形',hexagon:'正六边形',octagon:'正八边形',kite:'筝形',crescent:'月牙形',lShape:'L形',star:'五角星',hexStar:'六角星',spiral:'螺旋',fourStar:'四角星',octStar:'八角星',arrow:'箭头',parallelogram:'平行四边形',heart:'爱心',diamond:'钻石',smallDiamond:'小钻石',solidQuad:'立体四边形',dodecagon:'正十二边形',sun:'太阳',zShape:'Z字形',shenShape:'申字形'};
function getHeartConvertChance(hv,tv){const diff=tv-hv;if(diff<=-2)return 1.0;if(diff<=1)return 0.7;if(diff<=3)return 0.45;return 0.15;}
function applyHeartBlacken(){for(const m of monsters){if(m.type==='heart'&&m.hp>0&&m._heartDamaged){m.damage=HEART_HOSTILE_DMG;m.color='#cc3366';m.stroke='#881133';m._heartHealCd=Infinity;}}}
function tryConvertHeartToAlly(){if(monsters.length!==1)return;const h=monsters[0];if(h.type!=='heart'||h.hp<=0)return;monsters.splice(0,1);h.isAlly=true;h.damage=0;h._heartDamaged=false;h.color='#ff6699';h.stroke='#cc3366';h.attackCooldown=0;h._allyHealTimer=0;h._convertTimer=HEART_CONVERT_INTERVAL;h.buffs=h.buffs||[];allies.push(h);addShockwaveEffect(h.x,h.y,5,0.8);AudioSys.upgrade();eventNotice='💗 爱心决定加入你!';}
function applyHeartLifesteal(hpBefore){const dmgDealt=hpBefore-player.hp;if(dmgDealt<=0)return;for(const m of monsters){if(m.type!=='heart'||!m._heartDamaged||m.hp<=0)continue;if(Math.hypot(m.x-player.x,m.y-player.y)<m.radius+player.radius+0.5){const heal=dmgDealt*HEART_HOSTILE_LIFESTEAL;const before=m.hp;m.hp=Math.min(m.maxHp,m.hp+heal);if(m.hp>before)spawnDamageNumber(m.x,m.y-m.radius-0.2,'+'+Math.round(m.hp-before),'#ff88aa');}}}
function applyHeartHeal(){for(const m of monsters){if(m.type!=='heart'||m.hp<=0||m._heartDamaged)continue;if(m._heartHealCd>0)continue;const dist=Math.hypot(player.x-m.x,player.y-m.y);if(dist<m.radius+player.radius){const before=player.hp;player.hp=Math.min(getMaxHp(),player.hp+3);if(player.hp>before){spawnDamageNumber(m.x,m.y-m.radius-0.2,'+3','#66ff88');player._healFlash=0.25;AudioSys.pick();}m._heartHealCd=0.8;}}}
function updateHeartAlly(a,dt){
if(a.attackCooldown>0)a.attackCooldown-=dt;
const dist=Math.hypot(player.x-a.x,player.y-a.y);const noEnemy=monsters.length===0&&!boss;const hurt=player.hp<getMaxHp();
if(noEnemy&&hurt){if(dist>0.8)moveWithPathfinding(a,player.x,player.y,dt,false,false);if(dist<1.3){a._allyHealTimer=(a._allyHealTimer||0)-dt;if(a._allyHealTimer<=0){a._allyHealTimer=HEART_ALLY_HEAL_INTERVAL;const before=player.hp;player.hp=Math.min(getMaxHp(),player.hp+HEART_ALLY_HEAL);if(player.hp>before){spawnDamageNumber(a.x,a.y-a.radius-0.2,'+'+HEART_ALLY_HEAL,'#66ff88');player._healFlash=0.25;AudioSys.pick();}}}}else{if(dist>2)moveWithPathfinding(a,player.x,player.y,dt,false,false);}
if(a.attackCooldown<=0){let counter=false;for(const m of monsters){if(m.hp<=0)continue;if(Math.hypot(m.x-a.x,m.y-a.y)<m.radius+a.radius+0.15){damageMonster(m,HEART_COUNTER_DMG);const before=a.hp;a.hp=Math.min(a.maxHp,a.hp+HEART_COUNTER_DMG);if(a.hp>before)spawnDamageNumber(a.x,a.y-a.radius-0.2,'+'+Math.round(a.hp-before),'#ff88aa');m._attackDisabledTimer=HEART_COUNTER_DISARM;a.attackCooldown=HEART_COUNTER_COOLDOWN;a._hitFlash=0.1;counter=true;break;}}if(!counter&&boss&&boss.hp>0&&!boss.invincible){if(Math.hypot(boss.x-a.x,boss.y-a.y)<boss.radius+a.radius+0.15){damageBoss(HEART_COUNTER_DMG);const before=a.hp;a.hp=Math.min(a.maxHp,a.hp+HEART_COUNTER_DMG);if(a.hp>before)spawnDamageNumber(a.x,a.y-a.radius-0.2,'+'+Math.round(a.hp-before),'#ff88aa');boss._attackDisabledTimer=HEART_COUNTER_DISARM;a.attackCooldown=HEART_COUNTER_COOLDOWN;a._hitFlash=0.1;}}}
a._convertTimer=(a._convertTimer||HEART_CONVERT_INTERVAL)-dt;
if(a._convertTimer<=0){a._convertTimer=HEART_CONVERT_INTERVAL;let best=null,bestVal=-1;for(const m of monsters){if(m.hp<=0)continue;if(m.type==='boss'||m.type==='sun'||m.type==='shenShape')continue;if(Math.hypot(m.x-a.x,m.y-a.y)>HEART_CONVERT_RANGE)continue;if((m.value||0)>bestVal){bestVal=m.value;best=m;}}if(best){const chance=getHeartConvertChance(a.value||7,best.value||1);if(Math.random()<chance){const idx=monsters.indexOf(best);if(idx>=0)monsters.splice(idx,1);best.isAlly=true;if(best.attackCooldown===undefined)best.attackCooldown=0;best.buffs=best.buffs||[];allies.push(best);addShockwaveEffect(best.x,best.y,3,0.6);AudioSys.upgrade();eventNotice='💗 策反了'+(monsterNameMap[best.type]||best.type)+'!';}else{addShockwaveEffect(a.x,a.y,2,0.3);spawnDamageNumber(a.x,a.y-a.radius-0.3,'失败','#ff8888');}}}
}
// ==================== 模块4:十二边形 ====================
const DODEC_MAX_ABSORB=36,DODEC_HP_PER_VALUE=5,DODEC_DMG_PER_VALUE=0.5,DODEC_SPD_PER_10=0.75,DODEC_AREA_PER_VALUE=0.03;
function updateDodecagon(m,dt){
m.stuckTimer=0;const pd=Math.hypot(player.x-m.x,player.y-m.y);
if(pd<20||m._dodecAbsorbed>=DODEC_MAX_ABSORB){moveWithPathfinding(m,player.x,player.y,dt,false,false);return;}
let nearest=null,nd=Infinity;for(const f of fragments){const d=Math.hypot(f.x-m.x,f.y-m.y);if(d<nd){nd=d;nearest=f;}}
if(!nearest){moveWithPathfinding(m,player.x,player.y,dt,false,false);return;}
if(nd>m.radius+0.5){moveToward(m,nearest.x,nearest.y,dt,false,false);return;}
const remaining=DODEC_MAX_ABSORB-m._dodecAbsorbed;const absorbValue=Math.min(nearest.value,remaining);if(absorbValue<=0)return;
m._dodecAbsorbed+=absorbValue;const a=m._dodecAbsorbed;const oldMax=m.maxHp;
m.maxHp=Math.round(m._baseHp+a*DODEC_HP_PER_VALUE);m.hp+=(m.maxHp-oldMax);m.damage=m._baseDmg+a*DODEC_DMG_PER_VALUE;
m.speed=m._baseSpeed+Math.floor(a/10)*DODEC_SPD_PER_10;m.radius=m._baseRadius*Math.sqrt(1+a*DODEC_AREA_PER_VALUE);
nearest.value-=absorbValue;if(nearest.value<=0.001){const idx=fragments.indexOf(nearest);if(idx>=0)fragments.splice(idx,1);}
if(m.isAlly)energy+=absorbValue;addShockwaveEffect(m.x,m.y,1.2,0.3);
}
// ==================== 模块4:太阳 ====================
const SUN_TELEPORT_CD=12,SUN_WARN_TIME=3,SUN_DOT=1,SUN_HEAL=2,SUN_HEAT_CD=2,SUN_HEAT_DMG=24,SUN_BLAST_DMG=48,SUN_BLAST_R=4,SUN_TOUCH_DMG=30;
function updateSunLogic(sun,dt){
if(sun.hp<=0)return;
const dx=player.x-sun.x,dy=player.y-sun.y;const dist=Math.hypot(dx,dy);
if(dist>1){const sp=sun.speed;const nx=sun.x+dx/dist*sp*dt,ny=sun.y+dy/dist*sp*dt;if(!isBlocked(nx,sun.y,false,false,false))sun.x=nx;if(!isBlocked(sun.x,ny,false,false,false))sun.y=ny;}sun.stuckTimer=0;
sun._sunHealTimer-=dt;if(sun._sunHealTimer<=0){sun._sunHealTimer=1;sun.hp=Math.min(sun.maxHp,sun.hp+SUN_HEAL);}
sun._sunDotTimer-=dt;if(sun._sunDotTimer<=0){sun._sunDotTimer=1;for(const m of monsters){if(m!==sun&&m.hp>0)m.hp-=SUN_DOT;}if(boss&&boss.hp>0&&!boss.invincible)boss.hp-=SUN_DOT;if(!player.dead&&player.invincibleTime<=0)player.hp-=SUN_DOT;for(const a of allies){if(a.hp>0)a.hp-=SUN_DOT;}}
sun._sunShootTimer-=dt;if(sun._sunShootTimer<=0){sun._sunShootTimer=SUN_HEAT_CD;let target=null,td=Infinity;for(const m of monsters){if(m===sun||m.hp<=0)continue;const d=Math.hypot(m.x-sun.x,m.y-sun.y);if(d<td){td=d;target={x:m.x,y:m.y};}}for(const a of allies){const d=Math.hypot(a.x-sun.x,a.y-sun.y);if(d<td){td=d;target={x:a.x,y:a.y};}}if(boss&&boss.hp>0){const d=Math.hypot(boss.x-sun.x,boss.y-sun.y);if(d<td){td=d;target={x:boss.x,y:boss.y};}}const pd=Math.hypot(player.x-sun.x,player.y-sun.y);if(pd<td){td=pd;target={x:player.x,y:player.y};}if(target){const tdx=target.x-sun.x,tdy=target.y-sun.y;const tl=Math.hypot(tdx,tdy)||1;sunBullets.push({x:sun.x,y:sun.y,vx:tdx/tl*6.5,vy:tdy/tl*6.5,r:0.75,dmg:SUN_HEAT_DMG,life:20,hitSet:new Set()});}}
sun._sunTeleportTimer-=dt;if(sun._sunTeleportTimer<=0&&!sun._sunTeleportWarning){sun._sunTeleportTimer=SUN_TELEPORT_CD;let target=null,td=Infinity;for(const m of monsters){if(m===sun||m.hp<=0)continue;const d=Math.hypot(m.x-sun.x,m.y-sun.y);if(d<td){td=d;target={x:m.x,y:m.y};}}for(const a of allies){const d=Math.hypot(a.x-sun.x,a.y-sun.y);if(d<td){td=d;target={x:a.x,y:a.y};}}if(boss&&boss.hp>0){const d=Math.hypot(boss.x-sun.x,boss.y-sun.y);if(d<td){td=d;target={x:boss.x,y:boss.y};}}if(target)sun._sunTeleportWarning={x:target.x,y:target.y,timer:SUN_WARN_TIME};}
if(sun._sunTeleportWarning){sun._sunTeleportWarning.timer-=dt;if(sun._sunTeleportWarning.timer<=0){const tx=sun._sunTeleportWarning.x,ty=sun._sunTeleportWarning.y;sun.x=tx;sun.y=ty;sun._sunTeleportWarning=null;addShockwaveEffect(tx,ty,SUN_BLAST_R,0.5);for(const m of monsters){if(m===sun||m.hp<=0)continue;if(Math.hypot(m.x-tx,m.y-ty)<SUN_BLAST_R)m.hp-=SUN_BLAST_DMG;}for(const a of allies){if(Math.hypot(a.x-tx,a.y-ty)<SUN_BLAST_R)a.hp-=SUN_BLAST_DMG;}if(boss&&boss.hp>0&&Math.hypot(boss.x-tx,boss.y-ty)<SUN_BLAST_R+boss.radius)boss.hp-=SUN_BLAST_DMG;if(Math.hypot(player.x-tx,player.y-ty)<SUN_BLAST_R&&player.invincibleTime<=0){damagePlayer(SUN_BLAST_DMG);player.invincibleTime=0.8;}}}
if(sun.hp<=0&&!sun._deathHandled){sun._deathHandled=true;if(!_sunBlazingUnlocked){_sunBlazingUnlocked=true;weapons[BLAZING_INDEX].runUnlocked=true;weapons[BLAZING_INDEX].purchased=true;saveGame();eventNotice='☀️ 破晓之时已解锁!';AudioSys.upgrade();}}
}
// ==================== 模块4:申字与Z字 ====================
function updateShenShape(m,dt){
m.shootTimer-=dt;if(m.shootTimer>0)return;m.shootTimer=1;
const ang=Math.atan2(player.y-m.y,player.x-m.x);const cos=Math.cos(ang),sin=Math.sin(ang);const perpX=-sin,perpY=cos;const SPD=5,OFFSET=0.5;
for(let i=-1;i<=1;i++){enemyBullets.push({x:m.x+perpX*i*OFFSET,y:m.y+perpY*i*OFFSET,vx:cos*SPD,vy:sin*SPD,r:0.25,dmg:7,life:30,type:'shenBullet'});}
for(const dir of [1,-1]){for(let i=-1;i<=1;i++){enemyBullets.push({x:m.x+cos*i*OFFSET+perpX*dir*0.6,y:m.y+sin*i*OFFSET+perpY*dir*0.6,vx:perpX*dir*SPD,vy:perpY*dir*SPD,r:0.25,dmg:7,life:30,type:'shenBullet'});}}
}
function updateZShape(m,dt){
m.lifeTimer-=dt;if(m.lifeTimer<=0){m.escaped=true;window._nextWaveBudgetBonus+=10;eventNotice='⚠️ Z字形逃逸!下一波预算 +10';addShockwaveEffect(m.x,m.y,3,0.5);const idx=monsters.indexOf(m);if(idx>=0)monsters.splice(idx,1);return;}
const dx=m.x-player.x,dy=m.y-player.y;const dist=Math.hypot(dx,dy)||1;let vx=dx/dist,vy=dy/dist;
const margin=8;if(m.x<margin)vx+=1.2;if(m.x>WORLD_SIZE-margin)vx-=1.2;if(m.y<margin)vy+=1.2;if(m.y>WORLD_SIZE-margin)vy-=1.2;
const vlen=Math.hypot(vx,vy)||1;vx/=vlen;vy/=vlen;
const nx=m.x+vx*m.speed*dt,ny=m.y+vy*m.speed*dt;
if(!isBlocked(nx,m.y,false,false,false))m.x=nx;else vx*=-1;
if(!isBlocked(m.x,ny,false,false,false))m.y=ny;else vy*=-1;
m.x=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.x));m.y=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.y));m.stuckTimer=0;
}
// ==================== 模块4:怪物主更新 ====================
const POISON_DMG_MULT=0.5,SOLID_QUAD_ALPHA_INV=0.15,BURN_DURATION=7;
function updateMonsters(dt){
applyHeartBlacken();tryConvertHeartToAlly();const hpBeforeAll=player.hp;
for(let i=monsters.length-1;i>=0;i--){const m=monsters[i];if(m._mirrorPending&&m.hp>0){m._mirrorPending=false;spawnMirrorClone(m);}}
for(let i=0;i<monsters.length;i++){const m=monsters[i];if(!isFinite(m.x)||!isFinite(m.y)){const a=Math.random()*Math.PI*2,d=6+Math.random()*4;m.x=Math.max(2,Math.min(WORLD_SIZE-2,player.x+Math.cos(a)*d));m.y=Math.max(2,Math.min(WORLD_SIZE-2,player.y+Math.sin(a)*d));if(m._sqState!==undefined){m._sqState='idle';m._sqIdleTimer=0;m._sqSlideTarget=null;}m.stuckTimer=0;}}
// 围殴独立计算
for(const m of monsters){if(m._contactCd>0){m._contactCd-=dt;continue;}if(m.hp<=0||m._attackDisabledTimer>0)continue;const eff=m.damage*(m.damageMult||1);if(eff<=0)continue;for(const a of allies){if(a.hp<=0||a.invincibleTime>0)continue;if(Math.hypot(a.x-m.x,a.y-m.y)<m.radius+a.radius){damageAlly(a,eff);m._contactCd=0.5;a.invincibleTime=0.01;break;}}}
const saved=[];for(const a of allies){saved.push({a,t:a.invincibleTime});a.invincibleTime=999;}
for(const m of monsters){if(m.hp<=0||m.stunTime>0)continue;if(m.type==='shenShape')updateShenShape(m,dt);else if(m.type==='zShape')updateZShape(m,dt);}
if(_diamondMergeTimer>0){_diamondMergeTimer-=dt;if(_diamondMergeTimer<=0){const smalls=monsters.filter(m=>m.type==='smallDiamond'&&m.hp>0);if(smalls.length>0)mergeDiamondsAround(smalls[Math.floor(Math.random()*smalls.length)]);}}
for(const m of monsters){if(m.isSun&&m.hp>0)updateSunLogic(m,dt);}
for(const m of monsters){if(m.burnTimer>0){m.burnTimer-=dt;m.burnTick-=dt;if(m.burnTick<=0){m.burnTick=0.1;m.hp-=0.5;}}}
for(let i=monsters.length-1;i>=0;i--){
let m=monsters[i];
if(m.stunTime>0){m.stunTime-=dt;continue;}
if(m.baseSpeed===undefined||m.baseSpeed===null)m.baseSpeed=m.speed;
if(m.slowTimer>0){m.slowTimer-=dt;m.speed=Math.max(0.1,m.baseSpeed-0.25*(1+(m.slowTimer>0?1:0))-m.extraSlow);}else m.speed=Math.max(0.1,m.baseSpeed-m.extraSlow);
if(m.poisonTimer>0){m.poisonTimer-=dt;m.poisonTick-=dt;if(m.poisonTick<=0){m.hp-=(m.poisonStacks||1)*POISON_DMG_MULT;m.poisonTick=0.2;}}
if(m.type==='arrow'){updateArrow(m,dt);if(m.hp<=0){if(m.burst){burstExplode(m);if(!m.burstChild)spawnBurstChildren(m);}tryDropCrystal(m.type,m.x,m.y);spawnFragments(m.type,m.x,m.y,m.value);monsters.splice(i,1);}continue;}
if(m.hp<=0){if(m.type==='pentagon'&&m.gen===2)pentagonDeathBurst(m);if(m.burst){burstExplode(m);if(!m.burstChild)spawnBurstChildren(m);}tryDropCrystal(m.type,m.x,m.y);spawnFragments(m.type,m.x,m.y,m.value);monsters.splice(i,1);continue;}
let targetInfo=getTargetPos(m);let tx=targetInfo.x,ty=targetInfo.y;let dx=tx-m.x,dy=ty-m.y,dist=Math.hypot(dx,dy);let pdx=player.x-m.x,pdy=player.y-m.y,pdist=Math.hypot(pdx,pdy);
if(m.weakened){m.weakenTimer-=dt;if(m.weakenTimer<=0)m.weakened=false;}
let moved=false;
if(m.isSun){moved=true;}
else if(m.type==='parallelogram'){updateParallelogram(m,dt);moved=true;}
else if(m.type==='heart'){moved=moveWithPathfinding(m,tx,ty,dt,false,false);m.stuckTimer=0;if(pdist<m.radius+player.radius&&player.invincibleTime<=0){player._confuseTimer=HEART_CONFUSE_TIME;player._confuseNextChange=0;}}
else if(m.type==='diamond'){moved=moveWithPathfinding(m,tx,ty,dt,false,false);m.stuckTimer=0;m._diamondTimer-=dt;if(m._diamondTimer<=0)splitDiamond(m);}
else if(m.type==='smallDiamond'){moved=moveWithPathfinding(m,tx,ty,dt,false,false);m.stuckTimer=0;}
else if(m.type==='solidQuad'){moved=moveWithPathfinding(m,tx,ty,dt,false,false);m.stuckTimer=0;m._s3dAngle+=dt*0.5;m._s3dLifeTimer-=dt;if(m._s3dLifeTimer<=0){addShockwaveEffect(m.x,m.y,3,0.5);monsters.splice(i,1);continue;}m._s3dStealthTimer-=dt;if(m._s3dStealthTimer<=0){m._s3dStealthing=!m._s3dStealthing;m._s3dStealthTimer=3;}}
else if(m.type==='dodecagon'){updateDodecagon(m,dt);moved=true;}
else{
switch(m.type){
case'square':case'triangle':case'fourStar':case'star':case'hexStar':case'octStar':moved=moveWithPathfinding(m,tx,ty,dt,false,m.type==='triangle');break;
case'trapezoid':if(m.state==='chase'){if(dist>10)moved=moveWithPathfinding(m,tx,ty,dt,false,false);else{m.state='charge';m.chargeTimer=2;m.chargeDir={x:dx/dist,y:dy/dist};moved=true;}}else if(m.state==='charge'){let nx=m.x+m.chargeDir.x*12*dt,ny=m.y+m.chargeDir.y*12*dt;if(!isBlocked(nx,ny,false,false,false)){m.x=nx;m.y=ny;moved=true;}m.chargeTimer-=dt;if(m.chargeTimer<=0){m.state='chase';m.weakened=true;m.weakenTimer=0.5;}}break;
case'isoscelesTrapezoid':if(m.speedBoost){m.speedBoostTimer-=dt;if(m.speedBoostTimer<=0){m.speedBoost=false;m.speedTimer=3;}}else{m.speedTimer-=dt;if(m.speedTimer<=0){m.speedBoost=true;m.speedBoostTimer=1;}}m.speed=m.speedBoost?7:2;moved=moveWithPathfinding(m,tx,ty,dt,false,false);break;
case'hexagon':if(m.shockCooldown>0)m.shockCooldown-=dt;if(m.shockwaveState==='idle'){if(pdist<=5&&m.shockCooldown<=0){m.shockwaveState='charge';m.shockwaveTimer=0.5;moved=true;}else{m.buffTimer-=dt;if(m.buffTimer<=0){m.buffTimer=12;giveRandomBuff(m);}moved=moveWithPathfinding(m,tx,ty,dt,false,false);}}else if(m.shockwaveState==='charge'){m.shockwaveTimer-=dt;if(m.shockwaveTimer<=0){enemyHexShockwaves.push({x:m.x,y:m.y,currentRadius:0,maxRadius:8,speed:7.5,damage:10,hitSet:new Set()});m.shockCooldown=3;m.shockwaveState='shockwave';m.shockwaveTimer=0.3;}moved=true;}else if(m.shockwaveState==='shockwave'){m.shockwaveTimer-=dt;if(m.shockwaveTimer<=0){m.shockwaveState='idle';m.buffTimer=12;}moved=true;}break;
case'octagon':m.shootTimer-=dt;if(m.shootTimer<=0){m.shootTimer=3;let a=Math.atan2(pdy,pdx);for(let j=-1;j<=1;j++)enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a+j*0.3)*4,vy:Math.sin(a+j*0.3)*4,r:0.25,dmg:6,life:30,type:'octagon'});}moved=moveWithPathfinding(m,tx,ty,dt,false,false);break;
case'kite':m.bubbleTimer-=dt;if(m.bubbleTimer<=0){m.bubbleTimer=6;let a=Math.random()*Math.PI*2;enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a)*1.5,vy:Math.sin(a)*1.5,r:0.3,dmg:1,life:8,type:'kite',debuff:true,homingPlayer:true,homingSpeed:1.5});}moved=moveWithPathfinding(m,tx,ty,dt,false,false);break;
case'pentagon':if(m.canSplit){if(m.splitting){m.splittingTimer-=dt;if(m.splittingTimer<=0)m.splitting=false;}else{m.stateTimer-=dt;if(m.stateTimer<=0){m.stateTimer=m.gen===0?30:10;m.splitting=true;m.splittingTimer=2;pentagonSplit(m);}}}if(!m.splitting){if(m.gen===0){m.x+=Math.cos(Date.now()*0.001)*m.speed*dt;m.y+=Math.sin(Date.now()*0.001)*m.speed*dt;moved=true;}else{moved=moveWithPathfinding(m,tx,ty,dt,false,false);}}break;
case'crescent':updateCrescent(m,dt,pdx,pdy);moved=true;break;
case'lShape':m.jumpTimer-=dt;if(m.jumpTimer<=0){m.jumpTimer=2;let jdist=Math.hypot(dx,dy)||1,ux=dx/jdist,uy=dy/jdist,side=Math.random()<0.5?1:-1,px=-uy*side,py=ux*side;let ntx=m.x+ux*6+px*3,nty=m.y+uy*6+py*3;ntx=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,ntx));nty=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,nty));m.x=ntx;m.y=nty;moved=true;}else moved=moveWithPathfinding(m,tx,ty,dt,false,false);break;
case'spiral':m.growTimer-=dt;if(m.growTimer<=0){m.growTimer=15;m.speed+=1;m.baseSpeed=m.speed;}moved=moveWithPathfinding(m,tx,ty,dt,false,false);for(let j=bullets.length-1;j>=0;j--){let b=bullets[j];if(Math.hypot(b.x-m.x,b.y-m.y)<=1){m.orbitBullets.push({x:b.x,y:b.y,vx:b.vx,vy:b.vy,dmg:b.dmg,r:b.r,life:25,angle:Math.atan2(b.y-m.y,b.x-m.x),originSpeed:Math.hypot(b.vx,b.vy)});bullets.splice(j,1);}}break;
}
}
if(m.type==='star'){m.shootTimer-=dt;if(m.shootTimer<=0){m.shootTimer=3;let d2=pdist||1;enemyBullets.push({x:m.x,y:m.y,vx:pdx/d2*5,vy:pdy/d2*5,r:0.3,dmg:3,life:5,type:'starBullet'});}if(m.dodgeAccum>=m.dodgeThreshold&&m.dodgeCooldown<=0){m.dodgeAccum-=m.dodgeThreshold;m.dodgeCooldown=0.3;let dir=Math.random()<0.5?-1:1,a=Math.atan2(pdy,pdx)+Math.PI+dir*Math.PI/4,nx=m.x+Math.cos(a)*4,ny=m.y+Math.sin(a)*4;if(!isBlocked(nx,ny,false,false,false)){m.x=nx;m.y=ny;}m.invincibleTime=0.25;}if(m.dodgeCooldown>0)m.dodgeCooldown-=dt;}
if(m.type==='hexStar'){m.shootTimer-=dt;if(m.shootTimer<=0){m.shootTimer=2;let d2=pdist||1;enemyBullets.push({x:m.x,y:m.y,vx:pdx/d2*4,vy:pdy/d2*4,r:0.3,dmg:6,life:5,type:'hexStarBullet',slow:0.25,slowTime:5,homingPlayer:true,homingSpeed:4,homingRange:25});}if(m.dodgeAccum>=m.dodgeThreshold&&m.dodgeCooldown<=0){m.dodgeAccum-=m.dodgeThreshold;m.dodgeCooldown=0.3;let dir=Math.random()<0.5?-1:1,a=Math.atan2(pdy,pdx)+Math.PI+dir*Math.PI/4,nx=m.x+Math.cos(a)*4,ny=m.y+Math.sin(a)*4;if(!isBlocked(nx,ny,false,false,false)){m.x=nx;m.y=ny;}m.invincibleTime=0.25;}if(m.dodgeCooldown>0)m.dodgeCooldown-=dt;let desired=null;if(pdist>18)desired=1;else if(pdist<10)desired=-1;if(desired===1)moved=moveWithPathfinding(m,player.x,player.y,dt,false,false);else if(desired===-1){let awayX=m.x-(player.x-m.x),awayY=m.y-(player.y-m.y);moved=moveWithPathfinding(m,awayX,awayY,dt,false,false);}}
if(m.type==='octStar'){if(m.sliding){m.slideTimer-=dt;let nx=m.x+m.slideDir*10*dt;if(!isBlocked(nx,m.y,false,false,false))m.x=nx;m.slideShootTimer-=dt;if(m.slideShootTimer<=0){m.slideShootTimer=0.25;let a=Math.random()*Math.PI*2;enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a)*4,vy:Math.sin(a)*4,r:0.3,dmg:8,life:8,type:'octStarBullet',homing:true,homingSpeed:5,homingRange:8});}if(m.slideTimer<=0)m.sliding=false;}else{m.shootTimer-=dt;if(m.shootTimer<=0){m.shootTimer=6;for(let j=0;j<8;j++){let a=j*Math.PI/4;enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a)*4,vy:Math.sin(a)*4,r:0.3,dmg:8,life:8,type:'octStarBullet',homing:true,homingSpeed:5,homingRange:8});}}}}
if(m.type==='spiral')updateSpiralOrbits(m,dt);
if(m.convergeTrait&&m.type!==m.convergeTrait)applyConvergeTrait(m,dt,dx,dy,dist,pdx,pdy);
if(m.invincibleTime>0)m.invincibleTime-=dt;
if(m.buffs){m.buffs=m.buffs.filter(b=>{b.time-=dt;return b.time>0;});const hasRed=m.buffs.some(b=>b.type==='red');const hasGreen=m.buffs.some(b=>b.type==='green');if(m.damageMult&&m.damageMult!==1&&!hasRed)m.damageMult=1;if(m.invisible&&!hasGreen)m.invisible=false;}
m.x=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.x));m.y=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.y));
if(moved)m.stuckTimer=0;else{m.stuckTimer+=dt;if(m.stuckTimer>3)teleportUnstuck(m);}
let tObj=targetInfo.target;let effectiveDmg=m.damage*(m.damageMult||1);
if(tObj==='player'){if(pdist<m.radius+player.radius&&!player.dead&&player.invincibleTime<=0&&effectiveDmg>0){damagePlayer(effectiveDmg);player.invincibleTime=0.8;}}else if(tObj&&tObj.hp>0){if(Math.hypot(tObj.x-m.x,tObj.y-m.y)<m.radius+tObj.radius&&tObj.invincibleTime<=0&&effectiveDmg>0){damageAlly(tObj,effectiveDmg);tObj.invincibleTime=0.5;}}
}
for(const it of saved){if(it.a.invincibleTime>=999)it.a.invincibleTime=it.t;}
applyHeartHeal();applyHeartLifesteal(hpBeforeAll);
}
function applyConvergeTrait(m,dt,dx,dy,dist,pdx,pdy){
switch(m.convergeTrait){
case'triangle':m.speed=Math.max(m.speed,5);break;
case'trapezoid':if(!m.chargeTimer2)m.chargeTimer2=5;m.chargeTimer2-=dt;if(m.chargeTimer2<=0){m.chargeTimer2=5;let cd=dist||1;m.chargeDir={x:dx/cd,y:dy/cd};m.charging=true;m.chargeTimer=1;}if(m.charging){let nx=m.x+m.chargeDir.x*12*dt,ny=m.y+m.chargeDir.y*12*dt;if(!isBlocked(nx,ny,false,false,false)){m.x=nx;m.y=ny;}m.chargeTimer-=dt;if(m.chargeTimer<=0)m.charging=false;}break;
case'isoscelesTrapezoid':if(!m.boostTimer2){m.boostTimer2=3;m.boosting2=false;}if(m.boosting2){m.boostTimer2-=dt;if(m.boostTimer2<=0){m.boosting2=false;m.boostTimer2=3;}else m.speed=Math.max(m.speed,7);}else{m.boostTimer2-=dt;if(m.boostTimer2<=0){m.boosting2=true;m.boostTimer2=1;}}break;
case'pentagon':if(!m.pentagonTimer2)m.pentagonTimer2=15;m.pentagonTimer2-=dt;if(m.pentagonTimer2<=0){m.pentagonTimer2=15;let c=createMonster(m.type,m.x,m.y);c.mutation='converge';c.convergeTrait=m.convergeTrait;c.hp=c.maxHp=m.maxHp*0.6;c.damage*=0.6;c.radius*=0.7;c.value*=0.6;monsters.push(c);}break;
case'hexagon':if(!m.buffTimer2)m.buffTimer2=12;m.buffTimer2-=dt;if(m.buffTimer2<=0){m.buffTimer2=12;giveRandomBuff(m);}break;
case'octagon':if(!m.shootTimer2)m.shootTimer2=3;m.shootTimer2-=dt;if(m.shootTimer2<=0){m.shootTimer2=3;let a=Math.atan2(pdy,pdx);for(let j=-1;j<=1;j++)enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a+j*0.3)*4,vy:Math.sin(a+j*0.3)*4,r:0.25,dmg:6,life:30,type:'octagon'});}break;
case'kite':if(!m.bubbleTimer2)m.bubbleTimer2=6;m.bubbleTimer2-=dt;if(m.bubbleTimer2<=0){m.bubbleTimer2=6;let a=Math.random()*Math.PI*2;enemyBullets.push({x:m.x,y:m.y,vx:Math.cos(a)*1.5,vy:Math.sin(a)*1.5,r:0.3,dmg:1,life:8,type:'kite',debuff:true,homingPlayer:true,homingSpeed:1.5});}break;
case'lShape':if(!m.jumpTimer2)m.jumpTimer2=2;m.jumpTimer2-=dt;if(m.jumpTimer2<=0){m.jumpTimer2=2;let jdist=dist||1,ux=dx/jdist,uy=dy/jdist,side=Math.random()<0.5?1:-1,px=-uy*side,py=ux*side;let ntx=m.x+ux*6+px*3,nty=m.y+uy*6+py*3;ntx=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,ntx));nty=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,nty));m.x=ntx;m.y=nty;}break;
case'star':if(!m.dodgeTimer2)m.dodgeTimer2=0;if(m.dodgeAccum2>=10&&m.dodgeTimer2<=0){m.dodgeAccum2-=10;m.dodgeTimer2=0.3;let a=Math.atan2(pdy,pdx)+Math.PI+(Math.random()<0.5?-1:1)*Math.PI/4,nx=m.x+Math.cos(a)*4,ny=m.y+Math.sin(a)*4;if(!isBlocked(nx,ny,false,false,false)){m.x=nx;m.y=ny;}m.invincibleTime=0.25;}if(m.dodgeTimer2>0)m.dodgeTimer2-=dt;break;
}
}
function updateSpiralOrbits(m,dt){for(let i=m.orbitBullets.length-1;i>=0;i--){let ob=m.orbitBullets[i];ob.angle+=dt*2;ob.life-=dt;ob.x=m.x+Math.cos(ob.angle)*1;ob.y=m.y+Math.sin(ob.angle)*1;if(player.invincibleTime<=0&&Math.hypot(ob.x-player.x,ob.y-player.y)<ob.r+player.radius){damagePlayer(ob.dmg);player.invincibleTime=0.8;}for(let a of allies){if(a.invincibleTime<=0&&Math.hypot(ob.x-a.x,ob.y-a.y)<ob.r+a.radius){damageAlly(a,ob.dmg);a.invincibleTime=0.5;}}if(ob.life<=0){let a=Math.random()*Math.PI*2;enemyBullets.push({x:ob.x,y:ob.y,vx:Math.cos(a)*ob.originSpeed,vy:Math.sin(a)*ob.originSpeed,r:ob.r,dmg:ob.dmg,life:5,type:'spiralOrbit'});m.orbitBullets.splice(i,1);}}}
function giveRandomBuff(m){let t=monsters.filter(o=>o!==m).sort((a,b)=>Math.hypot(a.x-m.x,a.y-m.y)-Math.hypot(b.x-m.x,b.y-m.y))[0]||m,r=Math.random();if(r<0.33){t.buffs.push({type:'red',time:20});t.damageMult=1.5;}else if(r<0.66){t.shield=(t.shield||0)+30;t.buffs.push({type:'blue',time:-1});}else if(!t.buffs.some(b=>b.type==='green')){t.buffs.push({type:'green',time:7});t.invisible=true;}}
function updateCrescent(m,dt,pdx,pdy){if(m.stealthing){m.stealthTimer-=dt;if(m.stealthTimer<=0){m.stealthing=false;m.stealthTimer=14;}}else{m.stealthTimer-=dt;if(m.stealthTimer<=0){m.stealthing=true;m.stealthTimer=7;}}let sp=m.stealthing?m.speed*1.5:m.speed,dmg=m.stealthing?m.baseDamage*2:m.baseDamage,dist=Math.hypot(pdx,pdy);if(m.state==='chase'){if(dist<=10){m.state='circle';m.circleTimer=10;m.circleDir=1;}else moveWithPathfinding(m,player.x,player.y,dt,false,false);}else if(m.state==='circle'){m.circleTimer-=dt;let a=Math.atan2(pdy,pdx);m.x+=Math.cos(a+m.circleDir*Math.PI/2)*sp*dt;m.y+=Math.sin(a+m.circleDir*Math.PI/2)*sp*dt;if(m.x<m.radius||m.x>WORLD_SIZE-m.radius||m.y<m.radius||m.y>WORLD_SIZE-m.radius)m.circleDir*=-1;if(m.circleTimer<=0){m.state='charge';m.chargeTimer=3;m.chargeDir={x:pdx/dist,y:pdy/dist};}}else if(m.state==='charge'){m.chargeTimer-=dt;let cd=Math.hypot(player.x-m.x,player.y-m.y);m.chargeDir={x:(player.x-m.x)/cd,y:(player.y-m.y)/cd};let nx=m.x+m.chargeDir.x*10*dt,ny=m.y+m.chargeDir.y*10*dt;if(!isBlocked(nx,ny,false,false,false)){m.x=nx;m.y=ny;}if(cd<m.radius+player.radius&&!player.dead&&player.invincibleTime<=0){damagePlayer(dmg);player.invincibleTime=0.8;m.state='chase';m.x=Math.random()*WORLD_SIZE;m.y=Math.random()*WORLD_SIZE;}if(m.chargeTimer<=0&&m.state==='charge'){m.state='chase';m.x=Math.random()*WORLD_SIZE;m.y=Math.random()*WORLD_SIZE;}}m.x=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.x));m.y=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.y));}
function updateArrow(m,dt){if(!m.activated){if(Math.hypot(player.x-m.x,player.y-m.y)<=15)m.activated=true;return;}let tc=m.tail.length,dmg=m.baseDamage+tc*2;m.damageReduction=Math.min(0.9,tc*0.1);moveWithPathfinding(m,player.x,player.y,dt,false,false);m.tail.forEach((t,i)=>{if(t.hp>0){t.x+=(m.x-(i+1)*0.5-t.x)*10*dt;t.y+=(m.y-t.y)*10*dt;}});m.x=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.x));m.y=Math.max(m.radius,Math.min(WORLD_SIZE-m.radius,m.y));let dist=Math.hypot(player.x-m.x,player.y-m.y);if(dist<m.radius+player.radius&&!player.dead&&player.invincibleTime<=0){damagePlayer(dmg);player.invincibleTime=0.8;}}
// 镜面复制
const MIRROR_SPAWN_DIST=3;
function spawnMirrorClone(parent){const angle=Math.random()*Math.PI*2;const dist=MIRROR_SPAWN_DIST+Math.random()*2;const cx=Math.max(parent.radius,Math.min(WORLD_SIZE-parent.radius,parent.x+Math.cos(angle)*dist));const cy=Math.max(parent.radius,Math.min(WORLD_SIZE-parent.radius,parent.y+Math.sin(angle)*dist));const clone=createMonster(parent.type,cx,cy);if(!clone)return;clone._mirrorId=parent._mirrorId;clone._mirrorPending=false;clone.mutation=parent.mutation;clone.color='#ffffff';clone.stroke='#cccccc';clone.maxHp=parent.maxHp;clone.hp=parent.hp;clone.value=parent.value;if(clone.type==='arrow'&&clone.tail){clone.tail.forEach(t=>t.hp=t.maxHp);clone.activated=false;}if(clone.type==='pentagon'){clone.gen=0;clone.canSplit=true;clone.stateTimer=20;}if(clone.type==='diamond')clone._diamondTimer=8;if(clone.type==='hexagon'){clone.shockwaveState='idle';clone.shockwaveTimer=0;clone.shockCooldown=0;clone.buffTimer=12;}monsters.push(clone);addShockwaveEffect(cx,cy,2.5,0.4);}
// ==================== 模块4:Boss ====================
const HEPTAGRAM_R=7,HEPTAGRAM_LASER_HALF=0.18;
function heptagramVerts(cx,cy,R){const out=[];for(let i=0;i<7;i++){const a=i*2*Math.PI/7-Math.PI/2;out.push({x:cx+Math.cos(a)*R,y:cy+Math.sin(a)*R});}return out;}
function heptagramEdges(cx,cy,R){const v=heptagramVerts(cx,cy,R);const out=[];for(let i=0;i<7;i++){const j=(i+2)%7;out.push({x1:v[i].x,y1:v[i].y,x2:v[j].x,y2:v[j].y});}return out;}
function distToSeg(px,py,x1,y1,x2,y2){const dx=x2-x1,dy=y2-y1;const len2=dx*dx+dy*dy;if(len2===0)return Math.hypot(px-x1,py-y1);let t=((px-x1)*dx+(py-y1)*dy)/len2;t=Math.max(0,Math.min(1,t));return Math.hypot(px-(x1+t*dx),py-(y1+t*dy));}
function applyHeptagramLaserDamage(bx,by){const edges=heptagramEdges(bx,by,HEPTAGRAM_R);const LASER_DMG=13;if(player.invincibleTime<=0){for(const e of edges){if(distToSeg(player.x,player.y,e.x1,e.y1,e.x2,e.y2)<HEPTAGRAM_LASER_HALF){damagePlayer(LASER_DMG);player.invincibleTime=0.8;break;}}}for(let i=allies.length-1;i>=0;i--){const a=allies[i];let hit=false;for(const e of edges){if(distToSeg(a.x,a.y,e.x1,e.y1,e.x2,e.y2)<HEPTAGRAM_LASER_HALF){hit=true;break;}}if(hit){damageAlly(a,LASER_DMG);if(a.hp<=0)allies.splice(i,1);}}}
function updateBoss(dt){
if(!boss)return;
bossSkillWarnings=bossSkillWarnings.filter(w=>w.timer>0);bossSkillWarnings.forEach(w=>w.timer-=dt);
if(boss.stunTime>0){boss.stunTime-=dt;return;}
Object.keys(boss.skillCooldowns).forEach(k=>{if(boss.skillCooldowns[k]>0)boss.skillCooldowns[k]-=dt;});
if(boss.healing){boss.healTimer-=dt;if(boss.healTimer<=0){boss.healing=false;boss.invincible=false;}}
boss.shootTimer-=dt;if(boss.shootTimer<=0){boss.shootTimer=1.5;let dx=player.x-boss.x,dy=player.y-boss.y,dist=Math.hypot(dx,dy)||1;enemyBullets.push({x:boss.x,y:boss.y,vx:dx/dist*5.5,vy:dy/dist*5.5,r:0.35,dmg:7,life:30,type:'boss'});}
let moved=false;
if(boss.charging){boss.chargeTimer-=dt;if(boss.chargeTimer<=0){let nx=boss.x+boss.chargeDir.x*15*dt,ny=boss.y+boss.chargeDir.y*15*dt;if(!isBlocked(nx,ny,false,false,false)){boss.x=nx;boss.y=ny;moved=true;}else{let wall=getWallAt(nx,ny);if(wall)wall.dead=true;}boss.chargeDistance+=15*dt;if(boss.chargeDistance>=30||boss.x<boss.radius||boss.x>WORLD_SIZE-boss.radius||boss.y<boss.radius||boss.y>WORLD_SIZE-boss.radius){boss.charging=false;boss.chargeDistance=0;}let dx=player.x-boss.x,dy=player.y-boss.y;if(Math.hypot(dx,dy)<boss.radius+player.radius&&player.invincibleTime<=0){damagePlayer(27);player.invincibleTime=0.8;boss.charging=false;}for(let i=allies.length-1;i>=0;i--){const a=allies[i];if(Math.hypot(a.x-boss.x,a.y-boss.y)<boss.radius+a.radius){damageAlly(a,27);if(a.hp<=0)allies.splice(i,1);}}}}else{let dx=player.x-boss.x,dy=player.y-boss.y,dist=Math.hypot(dx,dy);if(dist>1){let nx=boss.x+dx/dist*boss.speed*dt,ny=boss.y+dy/dist*boss.speed*dt;if(!isBlocked(nx,ny,false,false,false)){boss.x=nx;boss.y=ny;moved=true;}else{let wall=getWallAt(nx,ny);if(wall)wall.dead=true;}}boss.x=Math.max(boss.radius,Math.min(WORLD_SIZE-boss.radius,boss.x));boss.y=Math.max(boss.radius,Math.min(WORLD_SIZE-boss.radius,boss.y));if(dist<boss.radius+player.radius&&player.invincibleTime<=0){damagePlayer(boss.damage);player.invincibleTime=0.8;}for(let i=allies.length-1;i>=0;i--){const a=allies[i];if(Math.hypot(a.x-boss.x,a.y-boss.y)<boss.radius+a.radius){damageAlly(a,boss.damage);if(a.hp<=0)allies.splice(i,1);}}}
if(moved)boss.stuckTimer=0;else{boss.stuckTimer+=dt;if(boss.stuckTimer>3)teleportUnstuck(boss);}
let dist=Math.hypot(player.x-boss.x,player.y-boss.y);
if(boss.skillCooldowns.charge<=0&&dist>=27&&!boss.charging){boss.charging=true;boss.chargeTimer=0.5;boss.chargeDir={x:(player.x-boss.x)/dist,y:(player.y-boss.y)/dist};boss.chargeDistance=0;boss.skillCooldowns.charge=18*(boss.phase===0?1:2/3);bossSkillWarnings.push({type:'charge',x1:boss.x,y1:boss.y,x2:player.x,y2:player.y,timer:0.5});}
if(boss.skillCooldowns.circle<=0&&dist>=7&&!boss.circleData){boss.circleData={circles:[{x:player.x,y:player.y,r:5},{x:player.x+(Math.random()-0.5)*30,y:player.y+(Math.random()-0.5)*30,r:5},{x:player.x+(Math.random()-0.5)*30,y:player.y+(Math.random()-0.5)*30,r:5}],timer:1,active:true};boss.skillCooldowns.circle=12*(boss.phase===0?1:2/3);bossSkillWarnings.push({type:'circle',circles:boss.circleData.circles,timer:1});}
if(boss.skillCooldowns.line<=0&&dist<20&&!boss.lineData){boss.lineData={timer:1,active:true};boss.skillCooldowns.line=30*(boss.phase===0?1:2/3);bossSkillWarnings.push({type:'heptagram',x:boss.x,y:boss.y,timer:1,radius:HEPTAGRAM_R});}
if(boss.phase===0&&boss.hp<343&&!boss.healing&&!_exterminateMode){boss.phase=1;spawnBossSecondPhaseMonsters();Object.keys(boss.skillCooldowns).forEach(k=>boss.skillCooldowns[k]*=2/3);Object.keys(boss.summonTimers).forEach(k=>boss.summonTimers[k]*=2/3);eventNotice='⚠️ Boss进入二阶段!';}
if(!_exterminateMode&&boss.skillCooldowns.heal<=0&&boss.hp<343&&!boss.invincible){boss.invincible=true;boss.healing=true;boss.healTimer=3;boss.hp=Math.min(boss.maxHp,boss.hp+17);monsters.forEach(m=>{if(Math.hypot(m.x-boss.x,m.y-boss.y)<=12)m.hp+=21;});boss.skillCooldowns.heal=24*(boss.phase===0?1:2/3);}
if(boss.circleData&&boss.circleData.active){boss.circleData.timer-=dt;if(boss.circleData.timer<=0){boss.circleData.circles.forEach(c=>{if(Math.hypot(player.x-c.x,player.y-c.y)<=c.r&&player.invincibleTime<=0){damagePlayer(22);player.invincibleTime=0.8;}});for(let i=allies.length-1;i>=0;i--){const a=allies[i];boss.circleData.circles.forEach(c=>{if(Math.hypot(a.x-c.x,a.y-c.y)<=c.r){damageAlly(a,22);if(a.hp<=0)allies.splice(i,1);}});}boss.circleData.active=false;boss.circleData=null;}}
if(boss.lineData&&boss.lineData.active){boss.lineData.timer-=dt;if(boss.lineData.timer<=0){addShockwaveEffect(boss.x,boss.y,12,0.6);applyHeptagramLaserDamage(boss.x,boss.y);boss.lineData.active=false;boss.lineData=null;}}
Object.keys(boss.summonTimers).forEach(k=>{boss.summonTimers[k]-=dt;if(boss.summonTimers[k]<=0){if(_exterminateMode){boss.summonTimers[k]=9999;return;}if(k==='square')summonBoss('square');if(k==='mid')summonBoss(Math.random()<0.5?'trapezoid':'triangle');if(k==='hex')summonBoss(Math.random()<0.5?'hexagon':'kite');if(k==='heavy')summonBoss(['octagon','pentagon','fourStar','octStar','hexStar'][Math.floor(Math.random()*5)]);boss.summonTimers[k]=(k==='square'?6:k==='mid'?15:k==='hex'?20:30)*(boss.phase===0?1:2/3);}});
if(boss.hp<=0){if(_exterminateMode){const bx=boss.x,by=boss.y;crystals+=1;saveGame();for(let i=0;i<3;i++){fragments.push({x:bx+(Math.random()-0.5)*3,y:by+(Math.random()-0.5)*3,value:12,radius:0.6,type:'big'});}boss=null;_exterminateMode=false;eventNotice='✅ 斩草除根完成!';waveDelay=3;}else{crystals+=1+Math.floor(Math.random()*3);saveGame();spawnFragments('boss',boss.x,boss.y,100);boss=null;isVictory=true;monsters=[];}}
}
function summonBoss(type){let a=Math.random()*Math.PI*2,d=3+Math.random()*5;let x=Math.max(2,Math.min(WORLD_SIZE-2,boss.x+Math.cos(a)*d));let y=Math.max(2,Math.min(WORLD_SIZE-2,boss.y+Math.sin(a)*d));monsters.push(createMonster(type,x,y));}
function spawnBossSecondPhaseMonsters(){['square','square','square','square','square','square','square','square','square','trapezoid','trapezoid','trapezoid','trapezoid','trapezoid','trapezoid','triangle','triangle','triangle','triangle','hexagon','hexagon','hexagon','kite','kite','octagon','pentagon','isoscelesTrapezoid','isoscelesTrapezoid','fourStar','fourStar','star','star','lShape','lShape','hexStar','hexStar','spiral','spiral','octStar','crescent','arrow'].forEach(t=>{let x,y,att=0;do{x=2+Math.random()*(WORLD_SIZE-4);y=2+Math.random()*(WORLD_SIZE-4);att++;}while(Math.hypot(x-player.x,y-player.y)<10&&att<100);monsters.push(createMonster(t,Math.max(2,Math.min(WORLD_SIZE-2,x)),Math.max(2,Math.min(WORLD_SIZE-2,y))));});}
// ← 第 2 段结束(继续粘下一段)
// ==================== 模块4:子弹更新 ====================
function moveBulletWithCollision(b,dt){
let steps=Math.max(1,Math.ceil(Math.hypot(b.vx,b.vy)*dt/0.2));let sx=b.vx*dt/steps,sy=b.vy*dt/steps;
for(let s=0;s<steps;s++){
let nx=b.x+sx,ny=b.y+sy;
if(isBlocked(nx,ny,false,false,true)){
if(b.type==='paraGun'&&b.bounces<3){if(isBlocked(nx,b.y,false,false,true))b.vx*=-1;if(isBlocked(b.x,ny,false,false,true))b.vy*=-1;b.bounces++;sx=b.vx*dt/steps;sy=b.vy*dt/steps;continue;}
let wall=getWallAt(nx,ny);if(wall&&wall.type==='x'){wall.hp-=b.dmg;if(wall.hp<=0)wall.dead=true;}
return false;
}
b.x=nx;b.y=ny;
}
return true;
}
const MIRROR_REFLECT_SPD=8,MIRROR_REFLECT_DMG=1.0,HEXGUN_BLAST_RADIUS=2.5,HEXGUN_BLAST_RATIO=0.5,ALLY_BLAST_RADIUS=2.5;
function updateBullets(dt){
for(let i=bullets.length-1;i>=0;i--){const b=bullets[i];let reflected=false;for(const m of monsters){if(!m._mirrorId||m.hp<=0)continue;if(Math.hypot(b.x-m.x,b.y-m.y)<b.r+m.radius){const dx=player.x-b.x,dy=player.y-b.y,d=Math.hypot(dx,dy)||1;enemyBullets.push({x:b.x,y:b.y,vx:dx/d*MIRROR_REFLECT_SPD,vy:dy/d*MIRROR_REFLECT_SPD,r:b.r,dmg:b.dmg*MIRROR_REFLECT_DMG,life:20,type:'mirrorReflect'});addShockwaveEffect(m.x,m.y,2,0.25);bullets.splice(i,1);reflected=true;break;}}if(reflected)continue;}
const hexGunBefore=[];for(const b of bullets){if(b.type==='hexGun')hexGunBefore.push({b,x:b.x,y:b.y,dmg:b.dmg});}
for(let i=bullets.length-1;i>=0;i--){const b=bullets[i];if(b.type!=='blazing')continue;b.x+=b.vx*dt;b.y+=b.vy*dt;b.life-=dt;for(const m of monsters){if(m.hp<=0||b.penetrated.has(m))continue;if(Math.hypot(b.x-m.x,b.y-m.y)<b.r+m.radius){b.penetrated.add(m);damageMonster(m,b.dmg);m.burnTimer=BURN_DURATION;m.burnTick=0.1;}}if(boss&&!b.penetrated.has(boss)&&Math.hypot(b.x-boss.x,b.y-boss.y)<b.r+boss.radius){b.penetrated.add(boss);damageBoss(b.dmg);}if(b.life<=0||b.x<-5||b.x>WORLD_SIZE+5||b.y<-5||b.y>WORLD_SIZE+5){bullets.splice(i,1);}}
for(let i=bullets.length-1;i>=0;i--){let b=bullets[i];if(b.type==='blazing')continue;b.life-=dt;if(b.life<=0){bullets.splice(i,1);continue;}if(b.homing){let nearest=null,nd=Infinity;for(let m of monsters){let d=Math.hypot(m.x-b.x,m.y-b.y);if(d<nd){nd=d;nearest=m;}}if(boss){let d=Math.hypot(boss.x-b.x,boss.y-b.y);if(d<nd){nd=d;nearest=boss;}}if(nearest){let dx=nearest.x-b.x,dy=nearest.y-b.y,d=Math.hypot(dx,dy)||1;b.vx=b.homingSpeed*dx/d;b.vy=b.homingSpeed*dy/d;}}if(!moveBulletWithCollision(b,dt)){bullets.splice(i,1);continue;}if(b.x<0||b.x>WORLD_SIZE||b.y<0||b.y>WORLD_SIZE){if(b.type==='paraGun'&&b.bounces<3){if(b.x<0||b.x>WORLD_SIZE)b.vx*=-1;if(b.y<0||b.y>WORLD_SIZE)b.vy*=-1;b.bounces++;}else{bullets.splice(i,1);continue;}}let hit=false;for(let m of monsters){if(Math.hypot(b.x-m.x,b.y-m.y)<b.r+m.radius){damageMonster(m,b.dmg);if(b.debuff&&b.type==='kiteGun')applyKiteGunDebuff(m);if(m.type==='star'||m.type==='hexStar'){if(m.dodgeCooldown<=0)m.dodgeAccum=(m.dodgeAccum||0)+b.dmg;}if(m.convergeTrait==='star'&&m.dodgeTimer2<=0)m.dodgeAccum2=(m.dodgeAccum2||0)+b.dmg;if(m.type==='arrow'&&m.tail){let tailHit=false;m.tail.forEach(t=>{if(t.hp>0&&Math.hypot(b.x-t.x,b.y-t.y)<b.r+t.radius){t.hp-=b.dmg;tailHit=true;}});if(tailHit)m.tail=m.tail.filter(x=>x.hp>0);}hit=true;break;}}if(!hit&&boss&&Math.hypot(b.x-boss.x,b.y-boss.y)<b.r+boss.radius){damageBoss(b.dmg);hit=true;}if(hit)bullets.splice(i,1);}
for(const item of hexGunBefore){if(!bullets.includes(item.b)){explosions.push({x:item.x,y:item.y,r:HEXGUN_BLAST_RADIUS,timer:0.3,dmg:item.dmg*HEXGUN_BLAST_RATIO});}}
for(let i=enemyBullets.length-1;i>=0;i--){let b=enemyBullets[i];b.life-=dt;if(b.type==='hexStarBullet'&&b.life<=0){enemyHexShockwaves.push({x:b.x,y:b.y,currentRadius:0,maxRadius:5,speed:10,damage:6,hitSet:new Set()});addShockwaveEffect(b.x,b.y,5,0.3);enemyBullets.splice(i,1);continue;}if(b.life<=0){enemyBullets.splice(i,1);continue;}if(b.homing||b.homingPlayer){let target=null,tDist=Infinity;let dPlayer=Math.hypot(player.x-b.x,player.y-b.y);if(dPlayer<(b.homingRange||25)){target='player';tDist=dPlayer;}if(!target){for(let a of allies){let d=Math.hypot(a.x-b.x,a.y-b.y);if(d<(b.homingRange||25)&&d<tDist){target=a;tDist=d;}}}if(target){let tpx=target==='player'?player.x:target.x;let tpy=target==='player'?player.y:target.y;let dx=tpx-b.x,dy=tpy-b.y,d=Math.hypot(dx,dy)||1;b.vx=(b.homingSpeed||4)*dx/d;b.vy=(b.homingSpeed||4)*dy/d;}}if(b.type==='starBullet'){let dx=player.x-b.x,dy=player.y-b.y,dist=Math.hypot(dx,dy)||1;b.vx=dx/dist*5;b.vy=dy/dist*5;}if(b.turnRate){let a=Math.atan2(b.vy,b.vx)+b.turnRate*dt,sp=Math.hypot(b.vx,b.vy);if(b.growthRate){sp*=Math.pow(1.618,b.growthRate*dt);if(sp>20)sp=20;}b.vx=Math.cos(a)*sp;b.vy=Math.sin(a)*sp;}if(!moveBulletWithCollision(b,dt)){enemyBullets.splice(i,1);continue;}if(b.x<0||b.x>WORLD_SIZE||b.y<0||b.y>WORLD_SIZE){enemyBullets.splice(i,1);continue;}if(Math.hypot(b.x-player.x,b.y-player.y)<b.r+player.radius&&player.invincibleTime<=0){damagePlayer(b.dmg);if(b.slow)player.slowEffects.push({amount:b.slow,time:b.slowTime||5});if(b.type==='kite'&&b.debuff){player.slowEffects.push({amount:0.25,time:1});energy-=5;}player.invincibleTime=0.8;if(b.type==='hexStarBullet'){enemyHexShockwaves.push({x:b.x,y:b.y,currentRadius:0,maxRadius:5,speed:10,damage:6,hitSet:new Set()});addShockwaveEffect(b.x,b.y,5,0.3);}enemyBullets.splice(i,1);continue;}for(let j=allies.length-1;j>=0;j--){let a=allies[j];if(Math.hypot(b.x-a.x,b.y-a.y)<b.r+a.radius){damageAlly(a,b.dmg);if(b.slow){a.slowEffects=a.slowEffects||[];a.slowEffects.push({amount:b.slow,time:b.slowTime||5});}enemyBullets.splice(i,1);if(a.hp<=0)allies.splice(j,1);break;}}}
const allyHexBefore=[];for(const b of allyBullets){if(b.type==='hexStarBullet')allyHexBefore.push({b,x:b.x,y:b.y,dmg:b.dmg});}
for(let i=allyBullets.length-1;i>=0;i--){let b=allyBullets[i];b.life-=dt;if(b.life<=0){allyBullets.splice(i,1);continue;}if(b.homing&&b.homingRange&&b.life>0){let nearest=null,nd=Infinity;for(const m of monsters){if(m.hp<=0)continue;const d=Math.hypot(m.x-b.x,m.y-b.y);if(d<nd){nd=d;nearest=m;}}if(!nearest&&boss&&boss.hp>0){const d=Math.hypot(boss.x-b.x,boss.y-b.y);if(d<nd){nd=d;nearest=boss;}}if(nearest&&nd<b.homingRange){const ddx=nearest.x-b.x,ddy=nearest.y-b.y,dd=Math.hypot(ddx,ddy)||1;b.vx=b.homingSpeed*ddx/dd;b.vy=b.homingSpeed*ddy/dd;}}if(!moveBulletWithCollision(b,dt)){allyBullets.splice(i,1);continue;}if(b.x<0||b.x>WORLD_SIZE||b.y<0||b.y>WORLD_SIZE){allyBullets.splice(i,1);continue;}let hit=false;for(let m of monsters)if(Math.hypot(b.x-m.x,b.y-m.y)<b.r+m.radius){damageMonster(m,b.dmg);hit=true;break;}if(!hit&&boss&&Math.hypot(b.x-boss.x,b.y-boss.y)<b.r+boss.radius){damageBoss(b.dmg);hit=true;}if(hit)allyBullets.splice(i,1);}
for(const item of allyHexBefore){if(!allyBullets.includes(item.b)){explosions.push({x:item.x,y:item.y,r:ALLY_BLAST_RADIUS,timer:0.3,dmg:item.dmg*0.5});}}
for(let i=sunBullets.length-1;i>=0;i--){const b=sunBullets[i];b.x+=b.vx*dt;b.y+=b.vy*dt;b.life-=dt;for(const m of monsters){if(m.isSun||m.hp<=0)continue;if(b.hitSet.has(m))continue;if(Math.hypot(b.x-m.x,b.y-m.y)<b.r+m.radius){b.hitSet.add(m);m.hp-=b.dmg;}}for(const a of allies){if(b.hitSet.has(a))continue;if(Math.hypot(b.x-a.x,b.y-a.y)<b.r+a.radius){b.hitSet.add(a);a.hp-=b.dmg;}}if(!b.hitSet.has('player')&&Math.hypot(b.x-player.x,b.y-player.y)<b.r+player.radius){b.hitSet.add('player');if(player.invincibleTime<=0){damagePlayer(b.dmg);player.invincibleTime=0.8;}}if(b.life<=0||b.x<-10||b.x>WORLD_SIZE+10||b.y<-10||b.y>WORLD_SIZE+10)sunBullets.splice(i,1);}
if(boomerang){if(boomerang.state==='outward'){boomerang.x+=boomerang.vx*dt;boomerang.y+=boomerang.vy*dt;if(Math.hypot(boomerang.x-boomerang.ox,boomerang.y-boomerang.oy)>=boomerang.maxR||boomerang.x<0||boomerang.x>WORLD_SIZE||boomerang.y<0||boomerang.y>WORLD_SIZE)boomerang.state='returning';}else{let dx=player.x-boomerang.x,dy=player.y-boomerang.y,d=Math.hypot(dx,dy);if(d>0.5){let sp=weapons[2].returnSpeed;boomerang.x+=dx/d*sp*dt;boomerang.y+=dy/d*sp*dt;}else{boomerang=null;weapons[2].ready=true;}}if(boomerang){monsters.forEach(m=>{if(Math.hypot(boomerang.x-m.x,boomerang.y-m.y)<boomerang.r+m.radius){let key=boomerang.state==='outward'?'hitO':'hitR';if(!boomerang[key].has(m)){boomerang[key].add(m);damageMonster(m,boomerang.dmg);}}});if(boss&&Math.hypot(boomerang.x-boss.x,boomerang.y-boss.y)<boomerang.r+boss.radius){let key=boomerang.state==='outward'?'hitO':'hitR';if(!boomerang[key].has(boss)){boomerang[key].add(boss);damageBoss(boomerang.dmg);}}}}
for(let i=grenades.length-1;i>=0;i--){let g=grenades[i];g.x+=g.vx*dt;g.y+=g.vy*dt;g.dist+=Math.hypot(g.vx,g.vy)*dt;if(g.dist>=g.maxDist){if(g.type==='burn')burnZones.push({x:g.x,y:g.y,r:g.exR,life:g.burnDuration,damage:g.dmg,tick:0});else explosions.push({x:g.x,y:g.y,r:g.exR,timer:0.3,dmg:g.dmg});grenades.splice(i,1);}}
for(let i=explosions.length-1;i>=0;i--){let e=explosions[i];e.timer-=dt;if(e.timer<=0){monsters.forEach(m=>{if(Math.hypot(m.x-e.x,m.y-e.y)<=e.r)damageMonster(m,e.dmg);});if(boss&&Math.hypot(boss.x-e.x,boss.y-e.y)<=e.r)damageBoss(e.dmg);for(let j=allies.length-1;j>=0;j--){const a=allies[j];if(Math.hypot(a.x-e.x,a.y-e.y)<=e.r){damageAlly(a,e.dmg);if(a.hp<=0)allies.splice(j,1);}}explosions.splice(i,1);}}
for(let i=burnZones.length-1;i>=0;i--){let z=burnZones[i];z.life-=dt;z.tick+=dt;if(z.tick>=0.2){z.tick-=0.2;monsters.forEach(m=>{if(Math.hypot(m.x-z.x,m.y-z.y)<=z.r)damageMonster(m,z.damage);});if(boss&&Math.hypot(boss.x-z.x,boss.y-z.y)<=z.r)damageBoss(z.damage);}if(z.life<=0)burnZones.splice(i,1);}
}
// ==================== 模块4:冲击波/粒子/冲刺 ====================
function addShockwaveEffect(x,y,radius,timer){shockwaveEffects.push({x,y,radius,timer,maxTimer:timer});}
function updateShockwaveEffects(dt){shockwaveEffects=shockwaveEffects.filter(e=>(e.timer-=dt)>0);}
function updateEnemyHexShockwaves(dt){for(let i=enemyHexShockwaves.length-1;i>=0;i--){let sw=enemyHexShockwaves[i];sw.currentRadius+=sw.speed*dt;if(sw.ally){monsters.forEach(m=>{if(!sw.hitSet.has(m)&&Math.hypot(m.x-sw.x,m.y-sw.y)<=sw.currentRadius){damageMonster(m,sw.damage);sw.hitSet.add(m);}});}else{let dp=Math.hypot(player.x-sw.x,player.y-sw.y);if(dp<=sw.currentRadius&&!sw.hitSet.has('player')){sw.hitSet.add('player');if(player.invincibleTime<=0){damagePlayer(sw.damage);player.invincibleTime=0.8;}}for(let j=allies.length-1;j>=0;j--){const a=allies[j];if(!sw.hitSet.has(a)&&Math.hypot(a.x-sw.x,a.y-sw.y)<=sw.currentRadius){damageAlly(a,sw.damage);sw.hitSet.add(a);if(a.hp<=0)allies.splice(j,1);}}}if(sw.currentRadius>=sw.maxRadius)enemyHexShockwaves.splice(i,1);}}
function updateParticles(dt){for(let i=particles.length-1;i>=0;i--){const p=particles[i];p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=0.94;p.vy*=0.94;p.life-=dt;if(p.life<=0)particles.splice(i,1);}}
function updateDamageNumbers(dt){for(let i=damageNumbers.length-1;i>=0;i--){const d=damageNumbers[i];d.x+=d.vx*dt;d.y+=d.vy*dt;d.vy*=0.94;d.life-=dt;if(d.life<=0)damageNumbers.splice(i,1);}}
function updateHitFlashes(dt){for(const m of monsters)if(m._hitFlash>0)m._hitFlash-=dt;if(boss&&boss._hitFlash>0)boss._hitFlash-=dt;if(playerHurtFlash>0)playerHurtFlash-=dt;}
function performDash(){if(player.dashCooldown>0||player.dashing)return;player.dashing=true;player.dashTimer=0.15;player.dashCooldown=4;player.dashDir={x:Math.cos(player.aimAngle),y:Math.sin(player.aimAngle)};player.invincibleTime=0.15;}
function updatePlayerDash(dt){if(player.dashCooldown>0)player.dashCooldown-=dt;if(player.dashing){player.dashTimer-=dt;let nx=player.x+player.dashDir.x*26*dt,ny=player.y+player.dashDir.y*26*dt;if(!isBlocked(nx,player.y,true,false,false))player.x=nx;if(!isBlocked(player.x,ny,true,false,false))player.y=ny;if(player.dashTimer<=0){player.dashing=false;player.invincibleTime=0.15;}}}
// ==================== 模块4:盟友 AI ====================
function giveAllyBuff(ally){let targets=allies.filter(a=>a!==ally&&a.hp>0);let target=null;if(Math.hypot(player.x-ally.x,player.y-ally.y)<=5)target='player';else if(targets.length>0)target=targets[Math.floor(Math.random()*targets.length)];if(!target)return;let r=Math.random();if(r<0.33){if(target==='player')player.foodEffects.push({type:'damage',amount:0.1,time:20});else{target.buffs.push({type:'red',time:20});target.damageMult=1.5;}}else if(r<0.66){if(target==='player')player.shield=(player.shield||0)+30;else{target.shield=(target.shield||0)+30;target.buffs.push({type:'blue',time:-1});}}else{if(target==='player')player.foodEffects.push({type:'speed',amount:0.25,time:7});else if(!target.buffs.some(b=>b.type==='green')){target.buffs.push({type:'green',time:7});target.invisible=true;}}}
function updatePentagonAlly(a,dt){if(a.gen===undefined)a.gen=0;if(a.gen===0){a.x+=Math.cos(Date.now()*0.001)*a.speed*dt;a.y+=Math.sin(Date.now()*0.001)*a.speed*dt;if(a.stateTimer===undefined)a.stateTimer=20;a.stateTimer-=dt;if(a.stateTimer<=0){a.stateTimer=20;for(let j=0;j<5;j++){let da=Math.random()*Math.PI*2+j*2.4;allyBullets.push({x:a.x,y:a.y,vx:Math.cos(da)*4,vy:Math.sin(da)*4,r:0.3,dmg:10,life:30,type:'goldenTriangle'});}}}else{let target=monsters.filter(m=>m.hp>0).sort((m1,m2)=>Math.hypot(m1.x-a.x,m1.y-a.y)-Math.hypot(m2.x-a.x,m2.y-a.y))[0];if(!target&&boss&&boss.hp>0)target=boss;if(target){let dx=target.x-a.x,dy=target.y-a.y,dist=Math.hypot(dx,dy);if(dist>0.01)moveWithPathfinding(a,target.x,target.y,dt,false,false);if(dist<a.radius+target.radius&&a.attackCooldown<=0){if(target.type==='boss')damageBoss(a.damage||5);else damageMonster(target,a.damage||5);a.attackCooldown=0.5;}}if(a.gen===1){if(a.stateTimer===undefined)a.stateTimer=10;a.stateTimer-=dt;if(a.stateTimer<=0){a.stateTimer=10;let c=createMonster('pentagon',a.x,a.y);c.isAlly=true;c.mutation=null;c.gen=2;c.radius=0.6*Math.pow(0.7,2);c.hp=c.maxHp=20;c.speed=4.5;c.damage=4;c.value=1;c.canSplit=false;c.stateTimer=0;allies.push(c);}}}}
function splitDiamondAlly(a){const hp=a.hp;if(hp<25)return;const count=Math.min(10,Math.floor(hp/25));const eachHp=Math.floor(hp/count);const idx=allies.indexOf(a);if(idx>=0)allies.splice(idx,1);for(let i=0;i<count;i++){const ang=Math.random()*Math.PI*2;const d=Math.random()*9;const sx=Math.max(1,Math.min(WORLD_SIZE-1,a.x+Math.cos(ang)*d));const sy=Math.max(1,Math.min(WORLD_SIZE-1,a.y+Math.sin(ang)*d));const small=createMonster('smallDiamond',sx,sy);if(small){small.hp=small.maxHp=eachHp;small.isAlly=true;small.attackCooldown=0;small.buffs=[];allies.push(small);}}addShockwaveEffect(a.x,a.y,9,0.8);}
const ALLY_HANDLED=new Set(['square','triangle','trapezoid','octagon','hexagon','kite','crescent','pentagon','heart']);
function updateNewAlly(a,dt){
let target=null,tDist=Infinity;for(const m of monsters){if(m.hp<=0)continue;const d=Math.hypot(m.x-a.x,m.y-a.y);if(d<tDist){tDist=d;target=m;}}if(!target&&boss&&boss.hp>0)target=boss;
const dx=target?target.x-a.x:0,dy=target?target.y-a.y:0,dist=target?tDist:Infinity;
const attackIfClose=()=>{if(target&&dist<a.radius+target.radius&&a.attackCooldown<=0){if(target.type==='boss')damageBoss(a.damage);else damageMonster(target,a.damage);a.attackCooldown=0.5;}};
switch(a.type){
case'isoscelesTrapezoid':if(a.speedBoost){a.speedBoostTimer-=dt;if(a.speedBoostTimer<=0){a.speedBoost=false;a.speedTimer=3;}}else{a.speedTimer-=dt;if(a.speedTimer<=0){a.speedBoost=true;a.speedBoostTimer=1;}}const orig=a.speed;a.speed=a.speedBoost?7:2;if(target)moveWithPathfinding(a,target.x,target.y,dt,false,false);a.speed=orig;attackIfClose();break;
case'lShape':a.jumpTimer=(a.jumpTimer===undefined?2:a.jumpTimer)-dt;if(a.jumpTimer<=0&&target){a.jumpTimer=2;const jd=dist||1,ux=dx/jd,uy=dy/jd,side=Math.random()<0.5?1:-1,px=-uy*side,py=ux*side;let ntx=a.x+ux*6+px*3,nty=a.y+uy*6+py*3;ntx=Math.max(a.radius,Math.min(WORLD_SIZE-a.radius,ntx));nty=Math.max(a.radius,Math.min(WORLD_SIZE-a.radius,nty));a.x=ntx;a.y=nty;}else if(target){moveWithPathfinding(a,target.x,target.y,dt,false,false);}attackIfClose();break;
case'star':a.shootTimer=(a.shootTimer===undefined?3:a.shootTimer)-dt;if(a.shootTimer<=0&&target){a.shootTimer=3;const d2=dist||1;allyBullets.push({x:a.x,y:a.y,vx:dx/d2*5,vy:dy/d2*5,r:0.3,dmg:3,life:5,type:'starBullet',homing:true,homingSpeed:5,homingRange:20});}if(target&&dist>4)moveWithPathfinding(a,target.x,target.y,dt,false,false);else if(target&&dist<2){const ax=a.x-dx/(dist||1),ay=a.y-dy/(dist||1);moveWithPathfinding(a,ax,ay,dt,false,false);}attackIfClose();break;
case'fourStar':if(target)moveWithPathfinding(a,target.x,target.y,dt,false,false);attackIfClose();break;
case'arrow':if(target)moveWithPathfinding(a,target.x,target.y,dt,false,false);if(a.tail)a.tail.forEach((t,i)=>{if(t.hp>0){t.x+=(a.x-(i+1)*0.5-t.x)*10*dt;t.y+=(a.y-t.y)*10*dt;}});attackIfClose();break;
case'parallelogram':if(!a._paraDir){const ang=Math.random()*Math.PI*2;a._paraDir={x:Math.cos(ang),y:Math.sin(ang)};a._paraTrail=[];a._paraTrailTimer=0;a._paraBounceCount=0;a._paraFlash=0;}updateParallelogram(a,dt);if(a.attackCooldown<=0){for(const m of monsters){if(m.hp<=0)continue;if(Math.hypot(m.x-a.x,m.y-a.y)<m.radius+a.radius){damageMonster(m,a.damage);a.attackCooldown=0.5;break;}}if(a.attackCooldown<=0&&boss&&Math.hypot(boss.x-a.x,boss.y-a.y)<boss.radius+a.radius){damageBoss(a.damage);a.attackCooldown=0.5;}}break;
case'diamond':if(a._diamondTimer===undefined)a._diamondTimer=8;a._diamondTimer-=dt;if(target)moveWithPathfinding(a,target.x,target.y,dt,false,false);if(a._diamondTimer<=0)splitDiamondAlly(a);attackIfClose();break;
case'smallDiamond':if(target)moveWithPathfinding(a,target.x,target.y,dt,false,false);attackIfClose();break;
case'solidQuad':if(!a._s3dConverted){a._s3dLifeTimer=32;a._s3dConverted=true;}a._s3dAngle=(a._s3dAngle||0)+dt*0.5;a._s3dLifeTimer-=dt;a._s3dStealthTimer=(a._s3dStealthTimer===undefined?3:a._s3dStealthTimer)-dt;if(a._s3dStealthTimer<=0){a._s3dStealthing=!a._s3dStealthing;a._s3dStealthTimer=3;}if(a._s3dLifeTimer<=0){const idx=allies.indexOf(a);if(idx>=0)allies.splice(idx,1);addShockwaveEffect(a.x,a.y,3,0.5);return;}if(target)moveWithPathfinding(a,target.x,target.y,dt,false,false);attackIfClose();break;
case'hexStar':const pd=Math.hypot(player.x-a.x,player.y-a.y);if(pd>2.5)moveWithPathfinding(a,player.x,player.y,dt,false,false);a.shootTimer=(a.shootTimer===undefined?2:a.shootTimer)-dt;if(a.shootTimer<=0&&target){a.shootTimer=2;const d2=dist||1;allyBullets.push({x:a.x,y:a.y,vx:dx/d2*4,vy:dy/d2*4,r:0.3,dmg:6,life:5,type:'hexStarBullet',homing:true,homingSpeed:4,homingRange:20});}break;
case'spiral':const pd2=Math.hypot(player.x-a.x,player.y-a.y);if(pd2>3)moveWithPathfinding(a,player.x,player.y,dt,false,false);a.growTimer=(a.growTimer===undefined?15:a.growTimer)-dt;if(a.growTimer<=0){a.growTimer=15;a.speed+=1;a.baseSpeed=a.speed;}break;
case'octStar':const pd3=Math.hypot(player.x-a.x,player.y-a.y);if(pd3>3.5)moveWithPathfinding(a,player.x,player.y,dt,false,false);a.shootTimer=(a.shootTimer===undefined?6:a.shootTimer)-dt;if(a.shootTimer<=0&&target&&dist<10){a.shootTimer=6;for(let j=0;j<8;j++){const ang=j*Math.PI/4;allyBullets.push({x:a.x,y:a.y,vx:Math.cos(ang)*4,vy:Math.sin(ang)*4,r:0.3,dmg:8,life:8,type:'octStarBullet'});}}break;
case'dodecagon':if(target)moveWithPathfinding(a,target.x,target.y,dt,false,false);attackIfClose();break;
}
}
function updateAllies(dt){
for(const a of allies){if(a.invincibleTime>0)a.invincibleTime-=dt;}
const turrets=[];for(let i=allies.length-1;i>=0;i--){const a=allies[i];if(a.hp<=0){allies.splice(i,1);continue;}if(a.isTurret){updateTurret(a,dt);turrets.push(a);allies.splice(i,1);}}
for(let i=allies.length-1;i>=0;i--){
let a=allies[i];if(a.hp<=0){allies.splice(i,1);continue;}
if(a.slowEffects&&a.slowEffects.length){if(!a._baseSpeed)a._baseSpeed=a.speed;a.slowEffects=a.slowEffects.filter(e=>(e.time-=dt)>0);const total=a.slowEffects.reduce((s,e)=>s+e.amount,0);a.speed=total>0?Math.max(0.1,a._baseSpeed*(1-Math.min(0.8,total))):a._baseSpeed;}
if(a.buffs){a.buffs=a.buffs.filter(b=>{b.time-=dt;return b.time>0||b.time<0;});const hasRed=a.buffs.some(b=>b.type==='red');const hasGreen=a.buffs.some(b=>b.type==='green');if(a.damageMult&&a.damageMult!==1&&!hasRed)a.damageMult=1;if(a.invisible&&!hasGreen)a.invisible=false;}
if(a.attackCooldown>0)a.attackCooldown-=dt;if(a.invisible){a.invisibleTimer=(a.invisibleTimer||0)+dt;}
if(a.type==='heart'&&a.isAlly){updateHeartAlly(a,dt);continue;}
if(a.type==='pentagon'){updatePentagonAlly(a,dt);a.x=Math.max(a.radius,Math.min(WORLD_SIZE-a.radius,a.x));a.y=Math.max(a.radius,Math.min(WORLD_SIZE-a.radius,a.y));continue;}
if(!ALLY_HANDLED.has(a.type)){updateNewAlly(a,dt);continue;}
let target=monsters.filter(m=>m.hp>0).sort((m1,m2)=>Math.hypot(m1.x-a.x,m1.y-a.y)-Math.hypot(m2.x-a.x,m2.y-a.y))[0];if(!target&&boss&&boss.hp>0)target=boss;if(!target)continue;
let dx=target.x-a.x,dy=target.y-a.y,dist=Math.hypot(dx,dy);
switch(a.type){
case'square':case'triangle':moveWithPathfinding(a,target.x,target.y,dt,false,a.type==='triangle');if(dist<a.radius+target.radius&&a.attackCooldown<=0){if(target.type==='boss')damageBoss(a.damage);else damageMonster(target,a.damage);a.attackCooldown=a.type==='triangle'?0.35:0.5;}break;
case'trapezoid':if(a.state==='chase'){if(dist>10)moveWithPathfinding(a,target.x,target.y,dt,false,false);else{a.state='charge';a.chargeTimer=2;a.chargeDir={x:dx/dist,y:dy/dist};}}else if(a.state==='charge'){let nx=a.x+a.chargeDir.x*12*dt,ny=a.y+a.chargeDir.y*12*dt;if(!isBlocked(nx,ny,false,false,false)){a.x=nx;a.y=ny;}a.chargeTimer-=dt;if(a.chargeTimer<=0){a.state='chase';a.weakened=true;a.weakenTimer=0.5;}}if(dist<a.radius+target.radius&&a.attackCooldown<=0){if(target.type==='boss')damageBoss(a.damage);else damageMonster(target,a.damage);a.attackCooldown=0.5;}break;
case'octagon':if(a.shootTimer===undefined)a.shootTimer=3;a.shootTimer-=dt;if(a.shootTimer<=0){a.shootTimer=3;let ba=Math.atan2(dy,dx);for(let j=-1;j<=1;j++){let aa=ba+j*0.3;allyBullets.push({x:a.x,y:a.y,vx:Math.cos(aa)*4,vy:Math.sin(aa)*4,r:0.25,dmg:6,life:30,type:'octagon'});}}if(dist>2)moveWithPathfinding(a,target.x,target.y,dt,false,false);if(dist<a.radius+target.radius&&a.attackCooldown<=0){if(target.type==='boss')damageBoss(a.damage);else damageMonster(target,a.damage);a.attackCooldown=0.5;}break;
case'hexagon':if(a.shockwaveTimer2===undefined)a.shockwaveTimer2=0;if(a.shockCooldown>0)a.shockCooldown-=dt;let enemyNear=false;for(let m of monsters){if(Math.hypot(m.x-a.x,m.y-a.y)<=5){enemyNear=true;break;}}if(enemyNear&&a.shockCooldown<=0){a.shockwaveTimer2+=dt;if(a.shockwaveTimer2>=0.5){a.shockwaveTimer2=0;a.shockCooldown=4;enemyHexShockwaves.push({x:a.x,y:a.y,currentRadius:0,maxRadius:8,speed:7.5,damage:10,hitSet:new Set(),ally:true});}}else a.shockwaveTimer2=0;if(a.buffTimer===undefined)a.buffTimer=12;a.buffTimer-=dt;if(a.buffTimer<=0){a.buffTimer=12;giveAllyBuff(a);addShockwaveEffect(a.x,a.y,2,0.4);}if(Math.hypot(player.x-a.x,player.y-a.y)>2)moveWithPathfinding(a,player.x,player.y,dt,false,false);break;
case'kite':if(a.bubbleTimer===undefined)a.bubbleTimer=6;a.bubbleTimer-=dt;if(a.bubbleTimer<=0){a.bubbleTimer=6;let dd=dist||1;allyBullets.push({x:a.x,y:a.y,vx:dx/dd*3,vy:dy/dd*3,r:0.3,dmg:5,life:30,type:'kite'});}if(dist>2)moveWithPathfinding(a,target.x,target.y,dt,false,false);break;
case'crescent':if(dist>6)moveWithPathfinding(a,target.x,target.y,dt,false,false);else if(dist<4){let nx=a.x-dx/dist*a.speed*dt,ny=a.y-dy/dist*a.speed*dt;if(!isBlocked(nx,ny,false,false,false)){a.x=nx;a.y=ny;}}else{let ang=Math.atan2(dy,dx)+Math.PI/2,nx=a.x+Math.cos(ang)*a.speed*dt,ny=a.y+Math.sin(ang)*a.speed*dt;if(!isBlocked(nx,ny,false,false,false)){a.x=nx;a.y=ny;}}if(dist<a.radius+target.radius&&a.attackCooldown<=0){if(target.type==='boss')damageBoss(a.damage);else damageMonster(target,a.damage);a.attackCooldown=0.5;}break;
}
a.x=Math.max(a.radius,Math.min(WORLD_SIZE-a.radius,a.x));a.y=Math.max(a.radius,Math.min(WORLD_SIZE-a.radius,a.y));
}
for(const t of turrets){if(t.hp>0)allies.push(t);}
}
// ==================== 模块4:主更新 ====================
let _stuckTimer=0;
function update(dt){
if(!gameStarted){homeBallX+=homeBallVx*dt*60;if(homeBallX<CANVAS_SIZE/2){homeBallY=CANVAS_SIZE/2+Math.sin(homeBallBounce*0.5)*5;homeBallBounce++;}else homeBallY=CANVAS_SIZE/2;return;}
if(menuOpen||pediaOpen||shopOpen||labOpen||phoneOpen)return;
if(player.dead||isVictory)return;
if(hitstopTimer>0){hitstopTimer-=dt;updateParticles(dt);updateDamageNumbers(dt);updateHitFlashes(dt);return;}
if(attackEffectTimer>0)attackEffectTimer-=dt;if(player.invincibleTime>0)player.invincibleTime-=dt;
if(bossIntro){bossIntro.timer+=dt;if(bossIntro.phase==='circle'&&bossIntro.timer>1){bossIntro.phase='points';bossIntro.timer=0;}else if(bossIntro.phase==='points'&&bossIntro.timer>0.6){bossIntro.phase='heptagon';bossIntro.timer=0;}else if(bossIntro.phase==='heptagon'&&bossIntro.timer>0.8){bossIntro.phase='star';bossIntro.timer=0;}else if(bossIntro.phase==='star'&&bossIntro.timer>0.9){bossIntro.phase='flash';bossIntro.timer=0;}else if(bossIntro.phase==='flash'&&bossIntro.timer>0.5){bossIntro=null;startBossFight();}return;}
updatePlayerDash(dt);
player.slowEffects=player.slowEffects.filter(e=>{e.time-=dt;return e.time>0;});
player.speed=player.baseSpeed*getPlayerSpeedMultiplier();player.slowEffects.forEach(e=>player.speed-=e.amount);if(player.speed<0)player.speed=0;
player.foodEffects=player.foodEffects.filter(e=>{e.time-=dt;return e.time>0;});
weapons.forEach(w=>{if(w.isReloading){w.reloadTimer-=dt;if(w.reloadTimer<=0){w.isReloading=false;w.currentMag=w.magSize;}}});
if(weapons[2].cooldown>0){weapons[2].cooldown-=dt;if(weapons[2].cooldown<=0&&!weapons[2].ready&&!boomerang)weapons[2].ready=true;}
if(!manualMode){if(attackCooldown>0)attackCooldown-=dt;else{let w=weapons[currentWeaponIndex];if(w.type==='melee')performMelee();else if(['gun','kiteGun','shotgun','paraGun','sniper','lmg','hexGun'].includes(w.type)){if(w.currentMag>0){performShoot();if(w.type==='lmg'){w.fireTime+=w.attackInterval;w.attackInterval=w.fireTime>5?0.05:w.fireTime>3?0.1:0.2;}}else{startReload();if(w.type==='lmg'){w.fireTime=0;w.attackInterval=0.2;}}}else if(w.type==='blazing'){performShoot();}else if(w.type==='boomerang'){if(w.ready&&!boomerang)performBoomerang();}else if(w.type==='grenade'){if(w.count>0)performGrenade();}else if(w.type==='burn'){if(w.count>0)performBurn();}else if(w.type==='saw'){if(w.currentMag>0)performSaw();else startReload();}else if(w.type==='trap'){if(w.count>0)performTrap();}attackCooldown=w.attackInterval;}}else if(attackCooldown>0)attackCooldown-=dt;
if(airStrikeCooldown>0)airStrikeCooldown-=dt;
if(airStrikeData&&airStrikeData.active){airStrikeData.timer-=dt;if(airStrikeData.timer<=0){let dx=airStrikeData.dx,dy=airStrikeData.dy;monsters.forEach(m=>{let vx=m.x-airStrikeData.startX,vy=m.y-airStrikeData.startY,proj=vx*dx+vy*dy;if(proj>0&&Math.abs(vx*dy-vy*dx)<=5)damageMonster(m,120);});if(boss){let vx=boss.x-airStrikeData.startX,vy=boss.y-airStrikeData.startY,proj=vx*dx+vy*dy;if(proj>0&&Math.abs(vx*dy-vy*dx)<=5)damageBoss(120);}airStrikeData.active=false;}}
traps.forEach((t,i)=>{let should=false;monsters.forEach(m=>{if(Math.hypot(m.x-t.x,m.y-t.y)<=t.radius)should=true;});if(!should&&boss&&Math.hypot(boss.x-t.x,boss.y-t.y)<=t.radius)should=true;if(should){monsters.forEach(m=>{if(Math.hypot(m.x-t.x,m.y-t.y)<=t.triggerRadius)m.stunTime=3;});if(boss&&Math.hypot(boss.x-t.x,boss.y-t.y)<=t.triggerRadius)boss.stunTime=1.5;traps.splice(i,1);}});
updateBullets(dt);updateMonsters(dt);updateAllies(dt);updateFragments(dt);updateShockwaveEffects(dt);updateEnemyHexShockwaves(dt);updateParticles(dt);updateDamageNumbers(dt);updateHitFlashes(dt);
if(boss)updateBoss(dt);
if(infiniteSurvivalMode){infiniteSurvivalTime-=dt;if(infiniteSurvivalTime>60){if(Math.random()<dt/3)spawnInfiniteEnemy();}else if(infiniteSurvivalTime>30){if(Math.random()<dt/2.5)spawnInfiniteEnemy();}else{if(Math.random()<dt/2)spawnInfiniteEnemy();}if(infiniteSurvivalTime<=0){infiniteSurvivalMode=false;monsters=[];specialEvent=null;eventNotice='';}}
if(speedBattleWave){speedBattleTimer-=dt;if(speedBattleTimer<=0){speedBattleOverTime+=dt;if(speedBattleOverTime>=1){speedBattleOverTime-=1;if(player.invincibleTime<=0){damagePlayer(5);player.invincibleTime=0.8;}}}}
let mx=0,my=0;if(moveJoystick.active){let d=Math.hypot(moveJoystick.dx,moveJoystick.dy);if(d>0.1){mx=moveJoystick.dx/d*Math.min(1,d/moveJoystick.maxRadius);my=moveJoystick.dy/d*Math.min(1,d/moveJoystick.maxRadius);}}
if(mx||my){let sp=player.dashing?26:player.speed,nx=player.x+mx*sp*dt,ny=player.y+my*sp*dt;if(!isBlocked(nx,player.y,true,false,false))player.x=nx;if(!isBlocked(player.x,ny,true,false,false))player.y=ny;}
if(player._confuseTimer>0){player._confuseTimer-=dt;if(player._confuseTimer<=0){player._confuseDir=null;player._confuseNextChange=0;}else{if(!player._confuseNextChange||player._confuseNextChange<=0){const a=Math.random()*Math.PI*2;player._confuseDir={x:Math.cos(a),y:Math.sin(a)};player._confuseNextChange=HEART_CONFUSE_CHANGE_MIN+Math.random()*(HEART_CONFUSE_CHANGE_MAX-HEART_CONFUSE_CHANGE_MIN);}player._confuseNextChange-=dt;if(player._confuseDir&&!player.dashing){const sp=player.speed*0.9;const nx=player.x+player._confuseDir.x*sp*dt;const ny=player.y+player._confuseDir.y*sp*dt;if(!isBlocked(nx,player.y,true,false,false))player.x=nx;if(!isBlocked(player.x,ny,true,false,false))player.y=ny;}}}
player.x=Math.max(player.radius,Math.min(WORLD_SIZE-player.radius,player.x));player.y=Math.max(player.radius,Math.min(WORLD_SIZE-player.radius,player.y));
if(monsters.length===0&&!boss&&waveDelay<=0)waveDelay=5;else if(monsters.length===0&&!boss&&waveDelay>0){waveDelay-=dt;if(waveDelay<=0){if(speedBattleWave&&speedBattleTimer>0){let surplus=Math.floor(speedBattleTimer);energy+=surplus*3;if(Math.floor(surplus/15)>0){crystals+=Math.floor(surplus/15);saveGame();}}massMutationWave=false;speedBattleWave=false;spawnWave();}}
if(!gameStarted||menuOpen||player.dead||isVictory||bossIntro)return;if(infiniteSurvivalMode)return;
if(monsters.length===0&&!boss){_stuckTimer+=dt;if(_stuckTimer>15){_stuckTimer=0;waveDelay=0;spawnWave();}}else _stuckTimer=0;
}
// ==================== 模块5:实验室/输入 ====================
const moveJoystick={active:false,touchId:null,centerX:0,centerY:0,dx:0,dy:0,maxRadius:50};
const aimJoystick={active:false,touchId:null,centerX:0,centerY:0,dx:0,dy:0,maxRadius:50};
const shopItems=[
{name:'十字架',desc:'恢复10生命',cost:10,x:60,y:140,w:280,h:50,page:0},
{name:'双菱形回旋镖',desc:'解锁回旋镖',cost:20,x:60,y:200,w:280,h:50,page:0},
{name:'筝形冲锋枪',desc:'解锁冲锋枪',cost:25,x:60,y:260,w:280,h:50,page:0},
{name:'扇形霰弹枪',desc:'解锁霰弹枪',cost:35,x:60,y:140,w:280,h:50,page:1},
{name:'手雷',desc:'购买1个手雷',cost:12,x:60,y:200,w:280,h:50,page:1},
{name:'五角星锯',desc:'解锁近战锯',cost:30,x:60,y:260,w:280,h:50,page:1},
{name:'闪光陷阱',desc:'购买1个陷阱',cost:15,x:60,y:140,w:280,h:50,page:2},
{name:'平行四边形步枪',desc:'解锁弹射步枪',cost:32,x:60,y:200,w:280,h:50,page:2},
{name:'箭头长矛',desc:'解锁箭头长矛',cost:28,x:60,y:260,w:280,h:50,page:2},
{name:'长方形狙击枪',desc:'实验室永久解锁后可用,消耗20能量',cost:20,x:60,y:140,w:280,h:50,page:3},
{name:'正六边形轻机枪',desc:'实验室永久解锁后可用,消耗40能量',cost:40,x:60,y:200,w:280,h:50,page:3},
{name:'燃烧弹弹药',desc:'购买1个燃烧弹(需实验室解锁)',cost:15,x:60,y:260,w:280,h:50,page:3},
{name:'六角星追踪枪',desc:'实验室永久解锁后可用,消耗25能量',cost:25,x:60,y:320,w:280,h:50,page:3}
];
const labSummons=[{key:'hexStar',name:'六角星召唤',cost:3,price:30}];
const pediaCloseBtn={x:340,y:60,r:20},pediaPrevBtn={x:40,y:365,r:18},pediaNextBtn={x:360,y:365,r:18};
function getCanvasCoords(cx,cy){const r=canvas.getBoundingClientRect();return{x:(cx-r.left)*(canvas.width/r.width),y:(cy-r.top)*(canvas.height/r.height)};}
function isInsideRect(px,py,r){return px>=r.x&&px<=r.x+r.w&&py>=r.y&&py<=r.y+r.h;}
function isInsideCircle(px,py,c){return Math.hypot(px-c.x,py-c.y)<=c.r;}
function getPageItems(p){return shopItems.filter(i=>i.page===p);}
function purchaseItem(item){
if(item.name==='十字架'&&energy>=item.cost&&player.hp<getMaxHp()){energy-=item.cost;player.hp=Math.min(getMaxHp(),player.hp+10);AudioSys.upgrade();return;}
if(item.name==='双菱形回旋镖'&&energy>=item.cost&&!weapons[2].purchased){energy-=item.cost;weapons[2].purchased=weapons[2].runUnlocked=weapons[2].ready=true;AudioSys.upgrade();return;}
if(item.name==='筝形冲锋枪'&&energy>=item.cost&&!weapons[3].purchased){energy-=item.cost;weapons[3].purchased=weapons[3].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='扇形霰弹枪'&&energy>=item.cost&&!weapons[4].purchased){energy-=item.cost;weapons[4].purchased=weapons[4].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='手雷'&&energy>=item.cost){energy-=item.cost;weapons[5].count++;weapons[5].purchased=weapons[5].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='五角星锯'&&energy>=item.cost&&!weapons[6].purchased){energy-=item.cost;weapons[6].purchased=weapons[6].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='闪光陷阱'&&energy>=item.cost){energy-=item.cost;weapons[7].count++;weapons[7].purchased=weapons[7].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='平行四边形步枪'&&energy>=item.cost&&!weapons[8].purchased){energy-=item.cost;weapons[8].purchased=weapons[8].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='箭头长矛'&&energy>=item.cost&&!weapons[SPEAR_INDEX].purchased){energy-=item.cost;weapons[SPEAR_INDEX].purchased=true;weapons[SPEAR_INDEX].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='燃烧弹弹药'&&weapons[11].purchased&&energy>=item.cost){energy-=item.cost;weapons[11].count++;weapons[11].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='长方形狙击枪'&&weapons[9].purchased&&!weapons[9].runUnlocked&&energy>=item.cost){energy-=item.cost;weapons[9].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='正六边形轻机枪'&&weapons[10].purchased&&!weapons[10].runUnlocked&&energy>=item.cost){energy-=item.cost;weapons[10].runUnlocked=true;AudioSys.upgrade();return;}
if(item.name==='六角星追踪枪'&&weapons[12].purchased&&!weapons[12].runUnlocked&&energy>=item.cost){energy-=item.cost;weapons[12].runUnlocked=true;AudioSys.upgrade();return;}
}
function getPhoneButtonRects(){
if(phonePage===0)return[{x:60,y:140,w:280,h:40,action:'cookie'},{x:60,y:190,w:280,h:40,action:'milk'},{x:60,y:240,w:280,h:40,action:'chocolate'}];
if(phonePage===1)return[{x:30,y:140,w:100,h:40,action:'ally_square'},{x:150,y:140,w:100,h:40,action:'ally_triangle'},{x:270,y:140,w:100,h:40,action:'ally_trapezoid'},{x:30,y:190,w:100,h:40,action:'ally_pentagon'},{x:150,y:190,w:100,h:40,action:'ally_hexagon'},{x:270,y:190,w:100,h:40,action:'ally_kite'},{x:30,y:240,w:100,h:40,action:'ally_octagon'},{x:150,y:240,w:100,h:40,action:'ally_crescent'},{x:270,y:240,w:100,h:40,action:'ally_hexStar'}];
if(phonePage===2)return[{x:60,y:110,w:280,h:44,action:'airstrike'},{x:60,y:162,w:280,h:44,action:'sun'},{x:60,y:214,w:280,h:44,action:'turret'}];
return[];
}
function handlePhoneTap(c){
getPhoneButtonRects().forEach(r=>{
if(isInsideRect(c.x,c.y,r)){
if(r.action==='cookie')useFood('cookie');if(r.action==='milk')useFood('milk');if(r.action==='chocolate')useFood('chocolate');
if(r.action==='airstrike')airStrike();if(r.action==='sun')summonSun();if(r.action==='turret')placeTurret();
if(r.action.startsWith('ally_'))summonAlly(r.action.replace('ally_',''));
}
});
}
// ==================== 模块5:图鉴 ====================
const pediaData={
square:{name:'正方形',color:'#66ccff',desc:'生命50,速度3,伤害5,价值1。脉冲式滑动,一段一段地扑过来。',comment:'如果换个颜色是不是隐身了?毕竟和地板砖一个形状……'},
triangle:{name:'正三角形',color:'#ff9966',desc:'生命80,速度5,伤害7,价值3。高速追踪,撞击玩家。',comment:'三角形是最稳定哒!'},
trapezoid:{name:'直角梯形',color:'#cc99ff',desc:'生命40,速度2,伤害8,价值2。靠近后冲锋2秒,之后虚弱0.5秒。',comment:'猪突猛进!冲鸭!'},
isoscelesTrapezoid:{name:'等腰梯形',color:'#99ff99',desc:'生命40,速度2,伤害6,价值2。每3秒会突然高速冲刺1秒。',comment:'把马拉松视作一段段的短跑就行了!'},
pentagon:{name:'正五边形',color:'#ffd700',desc:'母体子弹8伤,子代子弹5伤/碰撞5伤,孙代碰撞4伤/亡语2伤。',comment:'其实最小的五边形也会分裂只是你看不见而已~'},
hexagon:{name:'正六边形',color:'#66ffff',desc:'生命70,速度3,价值6。给友方上Buff,靠近5格后释放半径8冲击波,伤害10。',comment:'她也许可能会开一个饮料铺?'},
octagon:{name:'正八边形',color:'#ff6666',desc:'生命180,速度2,伤害10。重装远程,发射扇形子弹。',comment:'原型是达芬奇坦克噢~'},
kite:{name:'筝形',color:'#66ff66',desc:'生命90,速度4,伤害5。吐泡泡造成减速和中毒。',comment:'代码改不好了,一直都是侧边对着玩家~不改了~'},
crescent:{name:'月牙形',color:'#ccddff',desc:'生命108,速度3.25,基础伤害7,隐身翻倍。',comment:'什么时候出太阳啊……'},
arrow:{name:'箭头',color:'#ff9999',desc:'生命100,速度3,基础伤害4。9条尾巴,满尾巴减伤90%。',comment:'天灵灵地灵灵,9个矩形上我身呀!'},
lShape:{name:'L形',color:'#ccddff',desc:'生命65,速度3.5,伤害6。每2秒沿矩形对角线快速滑动,可越墙。',comment:'楼梯成精了?!甚至会倒立?!"臭小子!我是L!"'},
star:{name:'五角星',color:'#ffff99',desc:'生命50,伤害0,价值2。发射追逐子弹,受击时向后冲刺闪避。',comment:'为什么同为凹边形我数值这么低呀……'},
hexStar:{name:'六角星',color:'#66ddff',desc:'生命60,速度3,价值4。每受10伤害闪避,发射追踪小六角星。靠近玩家时会后退保持距离。',comment:'艺术就是派大星!'},
spiral:{name:'螺旋',color:'#88bbff',desc:'生命72,速度会越来越快。缴获靠近的子弹绕自己旋转。',comment:'恶果自食吧你!'},
fourStar:{name:'四角星',color:'#ffaa66',desc:'生命40,速度3,近战4,价值1。单次受到伤害至多为10。',comment:'其实……没有四角星……'},
octStar:{name:'八角星',color:'#ff88cc',desc:'生命160,速度2,近战12,价值9。面积1.5倍,受击滑动,环形射击,发射追踪弹。',comment:'凝聚了希望之后绝望才显得更加锋利'},
parallelogram:{name:'平行四边形',color:'#cc99ff',desc:'生命35,速度8,伤害6,价值2。沿固定方向滑动,撞墙反弹,永不转向。偶尔会晕头转向。',comment:'啊啊啊……好晕啊……'},
heart:{name:'爱心',color:'#ff6699',desc:'生命100,速度4,价值7。初始接触玩家回3血但使其5秒内失去方向控制。受到任何伤害后黑化:伤害7且每次伤害吸取等量生命回自身。若场上只剩她一只,她会放弃战斗加入你。盟友状态下每25秒策反20格内价值最高的敌人(Boss与太阳除外),并按价值分级有不同成功率。',comment:'躁动的感情就是魔鬼!'},
diamond:{name:'钻石',color:'#88ddff',desc:'生命250,速度3,伤害15,价值10。每8秒分裂成若干小钻石,小钻石每8秒重新聚合成新钻石。',comment:'至臻永恒,纯粹流淌,千命葬汝心'},
smallDiamond:{name:'小钻石',color:'#aaddff',desc:'生命25,速度5.5,伤害3,价值1。钻石的分裂碎片。',comment:'直到分离,我才看见世界……'},
solidQuad:{name:'立体四边形',color:'#ccd5ee',desc:'生命444,速度3,伤害24,价值24。每3秒切换隐身状态。32秒后自动消失。若因其消失导致场上无怪,本波直接过关,但不给击杀奖励。',comment:'“即使多次尝试,但是我们不得不承认,我们无法对从未见过的存在想象建模”'},
dodecagon:{name:'正十二边形',color:'#88eebb',desc:'生命80,伤害6,速度4,价值8。当玩家不在其20格内时,会主动寻找并吸收场上碎片。每吸收价值会永久强化自己。盟友状态下吸收的碎片价值依然给予玩家。',comment:'这个世界本来就不是吃人就是被吃!'},
sun:{name:'太阳',color:'#ffdd44',desc:'生命731,速度4,伤害30,价值25。正二十四边形。任意敌人都攻击它。存在时全场每秒受1伤害,禁止月牙出现,每2秒发射穿透热浪(伤害24),每3秒预警传送爆炸(伤害48),每秒回2血。击杀后解锁特殊武器"破晓之时"。',comment:'冉冉大日啊!告诉我!这是谁的黄昏?!谁的黎明?!还是说连照耀万物的您也深陷于某种空虚之中吗?'},
zShape:{name:'Z字形',color:'#ffdd88',desc:'生命70,价值6,主动远离玩家。45秒后若未被击杀便消失,使下一波预算+10。击杀后额外给予4能量。',comment:'敌方非但不投降还胆敢还击!请求支援!'},
shenShape:{name:'申字形',color:'#88ddff',desc:'生命350,价值12,不可移动。每1秒瞄准玩家发射3发平行子弹,并向垂直方向两端各发射3发。每发伤害7。',comment:'“哥们,你这是什么形状?”“汉字形?”“预订象形文字”'},
boss:{name:'正七边形Boss',color:'#cc99ff',desc:'生命777,速度2.5。多种技能,343进入二阶段。',comment:'"我不是圣人,我什么也不是,我只不过是挺身反抗的芸芸众生之一罢了……无数苦苦寻求对抗那不可知之物的人需要一束光知晓自己灵魂深处的力量……所以……这就是了"'},
turret:{name:'炮塔座架',color:'#333',desc:'生命200,不可移动。玩家可靠近放入非初始/非近战武器。放入后自动瞄准攻击敌人,使用所放武器的攻击方式。',comment:'当时作者正在咖啡厅里写作业,然后她的那只笔落入了奶茶杯……'}
};
const pediaMutations={mirror:{name:'镜面变异',desc:'怪物通体纯白,生成一个完全相同的复制体。双方生命翻倍但共享伤害,且所有弹幕被反射回玩家。击杀奖励为原值的1.5倍。',comment:'你打它一下,你们俩一起疼。'}};
// ==================== 模块6:绘制基础 ====================
function drawHexStarShape(x,y,size){for(let i=0;i<12;i++){let r=i%2===0?size/2:size/4;let a=i*Math.PI/6-Math.PI/2;let px=x+Math.cos(a)*r,py=y+Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();}
function drawSpiralShape(x,y,size){ctx.beginPath();let turns=3;for(let i=0;i<=turns*30;i++){let t=i/(turns*30);let r=t*size*0.42;let a=t*Math.PI*2*turns-Math.PI/2;let px=x+Math.cos(a)*r,py=y+Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}}
function drawShape(type,x,y,size,fill,stroke,angle=0){
ctx.save();ctx.translate(x,y);if(angle)ctx.rotate(angle);ctx.fillStyle=fill;ctx.strokeStyle=stroke;ctx.lineWidth=2;ctx.beginPath();
const poly=n=>{for(let i=0;i<n;i++){let a=i*2*Math.PI/n-Math.PI/2,px=Math.cos(a)*size/2,py=Math.sin(a)*size/2;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();};
if(type==='square'){ctx.fillRect(-size/2,-size/2,size,size);ctx.strokeRect(-size/2,-size/2,size,size);}
else if(type==='triangle')poly(3);
else if(type==='trapezoid'||type==='isoscelesTrapezoid'){ctx.moveTo(-size/2,size/2);ctx.lineTo(size/2,size/2);ctx.lineTo(size/3,-size/2);ctx.lineTo(-size/3,-size/2);ctx.closePath();}
else if(type==='pentagon')poly(5);else if(type==='hexagon')poly(6);else if(type==='hexStar'){drawHexStarShape(0,0,size);}
else if(type==='octagon')poly(8);
else if(type==='kite'){ctx.moveTo(0,-size/2);ctx.lineTo(size/3,0);ctx.lineTo(0,size/4);ctx.lineTo(-size/3,0);ctx.closePath();}
else if(type==='crescent'){ctx.arc(0,0,size/2,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#2a2a40';ctx.beginPath();ctx.arc(size/6,0,size/4,0,Math.PI*2);ctx.fill();ctx.restore();return;}
else if(type==='arrow'){ctx.moveTo(size/2,0);ctx.lineTo(-size/3,size/3);ctx.lineTo(-size/6,0);ctx.lineTo(-size/3,-size/3);ctx.closePath();}
else if(type==='lShape'){ctx.fillRect(-size/2,-size/2,size,size/3);ctx.fillRect(-size/2,-size/2,size/3,size);}
else if(type==='star'){for(let i=0;i<10;i++){let r=i%2===0?size/2:size/4,a=i*Math.PI/5-Math.PI/2,px=Math.cos(a)*r,py=Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();}
else if(type==='fourStar'){for(let i=0;i<8;i++){let r=i%2===0?size/2:size/5,a=i*Math.PI/4-Math.PI/2,px=Math.cos(a)*r,py=Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();}
else if(type==='octStar'){for(let i=0;i<16;i++){let r=i%2===0?size/2:size/3.5,a=i*Math.PI/8-Math.PI/2,px=Math.cos(a)*r,py=Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();}
else if(type==='spiral'){ctx.closePath();ctx.strokeStyle=fill;ctx.lineWidth=size*0.12;ctx.lineCap='round';drawSpiralShape(0,0,size);ctx.stroke();ctx.fillStyle=fill;ctx.beginPath();ctx.arc(0,0,size*0.08,0,Math.PI*2);ctx.fill();ctx.restore();return;}
else if(type==='boss')poly(7);
else if(type==='parallelogram'){const s=size,off=s*0.22;ctx.moveTo(-s/2+off,-s/2);ctx.lineTo(s/2+off,-s/2);ctx.lineTo(s/2-off,s/2);ctx.lineTo(-s/2-off,s/2);ctx.closePath();}
else if(type==='heart'){const scale=size/34;for(let i=0;i<=24;i++){const t=i*Math.PI/12;const px=16*Math.pow(Math.sin(t),3)*scale;const py=-(13*Math.cos(t)-5*Math.cos(2*t)-2*Math.cos(3*t)-Math.cos(4*t))*scale;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();}
else if(type==='diamond'||type==='smallDiamond'){ctx.moveTo(0,-size/2);ctx.lineTo(size/2,0);ctx.lineTo(0,size/2);ctx.lineTo(-size/2,0);ctx.closePath();}
else if(type==='solidQuad'){const s=size/2;ctx.moveTo(-s+s*0.35,-s);ctx.lineTo(s+s*0.35,-s);ctx.lineTo(s-s*0.35,s);ctx.lineTo(-s-s*0.35,s);ctx.closePath();}
else if(type==='dodecagon')poly(12);
else if(type==='shenShape'){const half=size/2,boxHalf=half*0.5;ctx.strokeStyle=fill;ctx.lineWidth=Math.max(2,size*0.06);ctx.lineCap='round';ctx.lineJoin='round';ctx.moveTo(0,-half);ctx.lineTo(0,-boxHalf);ctx.moveTo(-boxHalf,-boxHalf);ctx.lineTo(boxHalf,-boxHalf);ctx.lineTo(boxHalf,boxHalf);ctx.lineTo(-boxHalf,boxHalf);ctx.lineTo(-boxHalf,-boxHalf);ctx.moveTo(-boxHalf,0);ctx.lineTo(boxHalf,0);ctx.moveTo(0,-boxHalf);ctx.lineTo(0,boxHalf);ctx.moveTo(0,boxHalf);ctx.lineTo(0,half);ctx.stroke();ctx.restore();return;}
else if(type==='zShape'){const h=size/2;ctx.strokeStyle=fill;ctx.lineWidth=Math.max(2,size*0.06);ctx.lineCap='round';ctx.lineJoin='round';ctx.moveTo(-h,-h);ctx.lineTo(h,-h);ctx.lineTo(-h,h);ctx.lineTo(h,h);ctx.stroke();ctx.restore();return;}
ctx.fill();ctx.stroke();ctx.restore();
}
function drawWall(w,cx,cy){
let sx=(w.x-cx)*UNIT_PIXEL,sy=(w.y-cy)*UNIT_PIXEL,ww=w.w*UNIT_PIXEL,wh=w.h*UNIT_PIXEL;if(w.dead)return;ctx.save();
if(w.type==='solid'){ctx.fillStyle='#111';ctx.fillRect(sx,sy,ww,wh);ctx.strokeStyle='#444';ctx.strokeRect(sx,sy,ww,wh);}
else if(w.type==='shadow'){ctx.fillStyle='rgba(100,100,200,0.4)';ctx.fillRect(sx,sy,ww,wh);ctx.strokeStyle='#88a';ctx.strokeRect(sx,sy,ww,wh);ctx.beginPath();for(let i=0;i<ww;i+=6){ctx.moveTo(sx+i,sy+wh);ctx.lineTo(sx+ww,sy+i);}ctx.stroke();}
else if(w.type==='hollow'){ctx.strokeStyle='#aaa';ctx.lineWidth=2;ctx.strokeRect(sx,sy,ww,wh);}
else if(w.type==='circle'){ctx.fillStyle='#111';ctx.fillRect(sx,sy,ww,wh);ctx.fillStyle='#3a3a5c';ctx.beginPath();ctx.arc(sx+ww/2,sy+wh/2,ww*0.32,0,Math.PI*2);ctx.fill();ctx.strokeStyle='rgba(255,153,204,0.75)';ctx.lineWidth=1.5;ctx.stroke();ctx.strokeStyle='#444';ctx.lineWidth=1;ctx.strokeRect(sx,sy,ww,wh);}
else if(w.type==='triangle'){ctx.fillStyle='#111';ctx.fillRect(sx,sy,ww,wh);ctx.fillStyle='#3a3a5c';ctx.beginPath();ctx.moveTo(sx+ww/2,sy+wh*0.22);ctx.lineTo(sx+ww*0.78,sy+wh*0.78);ctx.lineTo(sx+ww*0.22,sy+wh*0.78);ctx.closePath();ctx.fill();ctx.strokeStyle='rgba(255,153,102,0.75)';ctx.lineWidth=1.5;ctx.stroke();ctx.strokeStyle='#444';ctx.lineWidth=1;ctx.strokeRect(sx,sy,ww,wh);}
else if(w.type==='x'){let alpha=w.hp?Math.max(0.2,w.hp/w.maxHp):1;ctx.fillStyle='#331111';ctx.fillRect(sx,sy,ww,wh);ctx.strokeStyle=`rgba(255,${Math.floor(102*alpha)},${Math.floor(102*alpha)},1)`;ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(sx,sy);ctx.lineTo(sx+ww,sy+wh);ctx.moveTo(sx+ww,sy);ctx.lineTo(sx,sy+wh);ctx.stroke();ctx.fillStyle='rgba(255,255,255,0.7)';ctx.font='10px sans-serif';ctx.fillText(Math.ceil(w.hp),sx+ww/2-10,sy+wh/2+3);}
ctx.restore();
}
function drawMonsters(cx,cy){
for(const m of monsters){if(m.type!=='parallelogram')continue;if(!m._paraTrail)continue;for(const t of m._paraTrail){const sx=(t.x-cx)*UNIT_PIXEL,sy=(t.y-cy)*UNIT_PIXEL,r=m.radius*UNIT_PIXEL*0.7;const a=(t.life/0.40)*0.45;ctx.globalAlpha=a;ctx.fillStyle='#cc99ff';ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fill();}}ctx.globalAlpha=1;
monsters.forEach(m=>{
let sx=(m.x-cx)*UNIT_PIXEL,sy=(m.y-cy)*UNIT_PIXEL,r=m.radius*UNIT_PIXEL;ctx.save();
if(m.stealthing&&m.type==='crescent')ctx.globalAlpha=0.1;if(m.buffs&&m.buffs.some(b=>b.type==='green'))ctx.globalAlpha=0.1;if(m.type==='solidQuad'&&m._s3dStealthing)ctx.globalAlpha=SOLID_QUAD_ALPHA_INV;
let color=m.color,stroke=m.stroke||'#000';if(m.mutation==='burst'){color='#ff4444';stroke='#cc0000';}if(m.mutation==='converge'){color='#cc88ff';stroke='#9933cc';}
if(m.type==='arrow'){let a=Math.atan2(player.y-m.y,player.x-m.x);ctx.save();ctx.translate(sx,sy);ctx.rotate(a);ctx.fillStyle=color;ctx.strokeStyle=stroke;ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(r,0);ctx.lineTo(-r*0.8,r*0.6);ctx.lineTo(-r*0.4,0);ctx.lineTo(-r*0.8,-r*0.6);ctx.closePath();ctx.fill();ctx.stroke();ctx.restore();m.tail.forEach(t=>{let tx=(t.x-cx)*UNIT_PIXEL,ty=(t.y-cy)*UNIT_PIXEL;ctx.fillStyle='#cc3333';ctx.fillRect(tx-4,ty-4,8,8);ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(tx-5,ty-7,10,2);ctx.fillStyle='#ff3333';ctx.fillRect(tx-5,ty-7,10*(t.hp/t.maxHp),2);});}
else if(m.type==='parallelogram'){const ang=Math.atan2(m._paraDir?m._paraDir.y:0,m._paraDir?m._paraDir.x:1);drawShape('parallelogram',sx,sy,r*2,color,stroke,ang);}
else if(m.type==='solidQuad'){drawSolidQuadShape(sx,sy,r,color,stroke,m._s3dAngle,1);if(m._s3dStealthing){const t=performance.now()/1000;ctx.strokeStyle=`rgba(140,180,255,${0.3+0.2*Math.sin(t*4)})`;ctx.lineWidth=1.5;ctx.setLineDash([4,4]);ctx.beginPath();ctx.arc(sx,sy,r*1.5,0,Math.PI*2);ctx.stroke();ctx.setLineDash([]);}}
else if(m.isSun){const t=performance.now()/500;ctx.save();ctx.globalCompositeOperation='lighter';const grad=ctx.createRadialGradient(sx,sy,0,sx,sy,r*2);grad.addColorStop(0,'rgba(255,220,80,0.6)');grad.addColorStop(0.5,'rgba(255,150,50,0.25)');grad.addColorStop(1,'rgba(255,80,0,0)');ctx.fillStyle=grad;ctx.beginPath();ctx.arc(sx,sy,r*2,0,Math.PI*2);ctx.fill();ctx.restore();ctx.save();ctx.translate(sx,sy);ctx.rotate(t*0.3);ctx.beginPath();for(let i=0;i<24;i++){const a=i*2*Math.PI/24-Math.PI/2;const px=Math.cos(a)*r,py=Math.sin(a)*r;if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);}ctx.closePath();ctx.fillStyle='#ffdd44';ctx.fill();ctx.strokeStyle='#cc8800';ctx.lineWidth=2;ctx.stroke();ctx.restore();}
else if(m.type==='shenShape'){const half=r,boxHalf=half*0.5;ctx.save();ctx.translate(sx,sy);ctx.strokeStyle=m.stroke;ctx.lineWidth=3;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(0,-half);ctx.lineTo(0,-boxHalf);ctx.moveTo(-boxHalf,-boxHalf);ctx.lineTo(boxHalf,-boxHalf);ctx.lineTo(boxHalf,boxHalf);ctx.lineTo(-boxHalf,boxHalf);ctx.lineTo(-boxHalf,-boxHalf);ctx.moveTo(-boxHalf,0);ctx.lineTo(boxHalf,0);ctx.moveTo(0,-boxHalf);ctx.lineTo(0,boxHalf);ctx.moveTo(0,boxHalf);ctx.lineTo(0,half);ctx.stroke();ctx.restore();}
else if(m.type==='zShape'){const h=r*0.8;ctx.save();ctx.translate(sx,sy);ctx.strokeStyle=m.stroke;ctx.lineWidth=3;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(-h,-h);ctx.lineTo(h,-h);ctx.lineTo(-h,h);ctx.lineTo(h,h);ctx.stroke();ctx.restore();}
else{let angle=0;if(m.type==='triangle'||m.type==='kite'||m.type==='crescent')angle=Math.atan2(player.y-m.y,player.x-m.x);else if(m.type==='trapezoid')angle=m.state==='charge'?Math.atan2(m.chargeDir.y,m.chargeDir.x):Math.atan2(player.y-m.y,player.x-m.x);else if(m.type==='isoscelesTrapezoid')angle=Math.atan2(player.y-m.y,player.x-m.x);drawShape(m.type,sx,sy,r*2,color,stroke,angle);}
ctx.restore();
if(m.type!=='solidQuad'){let hw=r*2;ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(sx-r,sy-r-8,hw,4);ctx.fillStyle='#ff3333';ctx.fillRect(sx-r,sy-r-8,hw*(m.hp/m.maxHp),4);}else{ctx.save();ctx.globalAlpha=m._s3dStealthing?0.25:1;ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(sx-r,sy-r-8,r*2,4);ctx.fillStyle='#ff3333';ctx.fillRect(sx-r,sy-r-8,r*2*(m.hp/m.maxHp),4);ctx.restore();}
if(m.shield>0){ctx.fillStyle='#4488ff';ctx.font='10px sans-serif';ctx.fillText(`盾${m.shield}`,sx-5,sy-r-10);}
if(m.stunTime>0){ctx.fillStyle='#ffff00';ctx.font='10px sans-serif';ctx.fillText('眩晕',sx-8,sy-r-20);}
if(m.poisonTimer>0){ctx.fillStyle='#66ff66';ctx.font='10px sans-serif';ctx.fillText('毒',sx-5,sy+r+12);}
if(m.slowTimer>0){ctx.fillStyle='#88ccff';ctx.font='10px sans-serif';ctx.fillText('慢',sx+5,sy+r+12);}
if(m.burnTimer>0){ctx.fillStyle='#ff9944';ctx.font='10px sans-serif';ctx.fillText('燃',sx+15,sy+r+12);}
if(m._attackDisabledTimer>0){ctx.fillStyle='#ffcc66';ctx.font='bold 11px sans-serif';ctx.fillText('缴械',sx-12,sy-r-22);}
if(m.type==='dodecagon'&&m._dodecAbsorbed>0){ctx.fillStyle='#66ffaa';ctx.font='10px sans-serif';ctx.fillText('吸收'+Math.round(m._dodecAbsorbed)+'/36',sx-25,sy+r+20);}
if(m.type==='zShape'&&m.lifeTimer>0){ctx.fillStyle='#ffdd66';ctx.font='bold 10px sans-serif';ctx.textAlign='center';ctx.fillText(Math.ceil(m.lifeTimer)+'s',sx,sy+r+14);ctx.textAlign='left';}
if(m.type==='spiral'&&m.orbitBullets)m.orbitBullets.forEach(ob=>{let ox=(ob.x-cx)*UNIT_PIXEL,oy=(ob.y-cy)*UNIT_PIXEL;ctx.fillStyle='#66ccff';ctx.beginPath();ctx.arc(ox,oy,ob.r*UNIT_PIXEL,0,Math.PI*2);ctx.fill();});
if(m.isSun&&m._sunTeleportWarning){const wx=(m._sunTeleportWarning.x-cx)*UNIT_PIXEL,wy=(m._sunTeleportWarning.y-cy)*UNIT_PIXEL,tt=m._sunTeleportWarning.timer;ctx.save();ctx.strokeStyle='rgba(255,30,30,'+(0.6+0.4*Math.sin(tt*12))+')';ctx.lineWidth=4;ctx.beginPath();ctx.arc(wx,wy,4*UNIT_PIXEL,0,Math.PI*2);ctx.stroke();ctx.fillStyle='rgba(255,30,30,0.18)';ctx.fill();ctx.strokeStyle='rgba(255,80,80,0.9)';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(wx-15,wy);ctx.lineTo(wx+15,wy);ctx.moveTo(wx,wy-15);ctx.lineTo(wx,wy+15);ctx.stroke();ctx.restore();}
if(m._hitFlash>0){ctx.save();ctx.globalAlpha=(m._hitFlash/0.08)*0.6;ctx.globalCompositeOperation='lighter';ctx.fillStyle='#ffffff';ctx.beginPath();ctx.arc(sx,sy,r*1.25,0,Math.PI*2);ctx.fill();ctx.restore();}
if(m.type==='parallelogram'&&m._paraFlash>0){ctx.save();ctx.globalAlpha=(m._paraFlash/0.15)*0.7;ctx.globalCompositeOperation='lighter';ctx.fillStyle='#ffffff';ctx.beginPath();ctx.arc(sx,sy,r*1.4,0,Math.PI*2);ctx.fill();ctx.restore();}
});
for(const b of sunBullets){const sx=(b.x-cx)*UNIT_PIXEL,sy=(b.y-cy)*UNIT_PIXEL,r=b.r*UNIT_PIXEL;ctx.save();ctx.globalCompositeOperation='lighter';const grad=ctx.createRadialGradient(sx,sy,0,sx,sy,r*1.5);grad.addColorStop(0,'rgba(255,240,150,0.95)');grad.addColorStop(0.6,'rgba(255,160,50,0.6)');grad.addColorStop(1,'rgba(255,80,0,0)');ctx.fillStyle=grad;ctx.beginPath();ctx.arc(sx,sy,r*1.5,0,Math.PI*2);ctx.fill();ctx.restore();}
}
function drawSolidQuadShape(sx,sy,r,fill,stroke,angle,alpha){ctx.save();ctx.globalAlpha=alpha;ctx.translate(sx,sy);if(angle)ctx.rotate(angle);ctx.fillStyle=fill;ctx.strokeStyle=stroke;ctx.lineWidth=2;const a0=-Math.PI/2;ctx.beginPath();for(let i=0;i<3;i++){const a=a0+i*2*Math.PI/3;const px=Math.cos(a)*r,py=Math.sin(a)*r;if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);}ctx.closePath();ctx.fill();ctx.stroke();ctx.globalAlpha=alpha*0.45;ctx.lineWidth=1;ctx.beginPath();for(let i=0;i<3;i++){const a1=a0+i*2*Math.PI/3,a2=a0+(i+1)*2*Math.PI/3;ctx.moveTo(Math.cos(a1)*r,Math.sin(a1)*r);ctx.lineTo(0,0);ctx.lineTo(Math.cos(a2)*r,Math.sin(a2)*r);}ctx.stroke();ctx.restore();}
function drawAllies(cx,cy){
const turrets=[];for(let i=allies.length-1;i>=0;i--){if(allies[i].isTurret){turrets.push(allies[i]);allies.splice(i,1);}}
allies.forEach(a=>{
let sx=(a.x-cx)*UNIT_PIXEL,sy=(a.y-cy)*UNIT_PIXEL,r=a.radius*UNIT_PIXEL;ctx.save();if(a.buffs&&a.buffs.some(b=>b.type==='green'))ctx.globalAlpha=0.1;
if(a.type==='parallelogram'&&a._paraDir){const ang=Math.atan2(a._paraDir.y,a._paraDir.x);drawShape('parallelogram',sx,sy,r*2,a.color,'#0f0',ang);}else if(a.type==='solidQuad'){drawSolidQuadShape(sx,sy,r,a.color,'#0f0',a._s3dAngle||0,1);}else{drawShape(a.type,sx,sy,r*2,a.color,'#0f0');}
ctx.restore();ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(sx-r,sy-r-8,r*2,4);ctx.fillStyle='#33cc33';ctx.fillRect(sx-r,sy-r-8,r*2*(a.hp/a.maxHp),4);
if(a.shield>0){ctx.fillStyle='#4488ff';ctx.font='10px sans-serif';ctx.fillText(`盾${a.shield}`,sx-5,sy-r-10);}
});
for(const t of turrets){
const sx=(t.x-cx)*UNIT_PIXEL,sy=(t.y-cy)*UNIT_PIXEL,r=t.radius*UNIT_PIXEL;
ctx.fillStyle='#111';ctx.fillRect(sx-r,sy-r,r*2,r*2);ctx.strokeStyle='#666';ctx.lineWidth=2;ctx.strokeRect(sx-r,sy-r,r*2,r*2);ctx.fillStyle='#2a2a40';ctx.fillRect(sx-r*0.55,sy-r*0.55,r*1.1,r*1.1);
if(t.weaponType){ctx.save();ctx.translate(sx,sy);ctx.fillStyle=t.weaponData.color||'#fff';ctx.strokeStyle='#000';ctx.lineWidth=1;if(t.weaponType==='saw'){ctx.beginPath();for(let i=0;i<6;i++){const ang=i*Math.PI/3;ctx.lineTo(Math.cos(ang)*4,Math.sin(ang)*4);}ctx.closePath();ctx.fill();ctx.stroke();}else if(t.weaponType==='boomerang'){ctx.beginPath();ctx.moveTo(0,-4);ctx.lineTo(4,0);ctx.lineTo(0,4);ctx.lineTo(-4,0);ctx.closePath();ctx.fill();ctx.stroke();}else if(t.weaponType==='sniper'){ctx.fillRect(-5,-1.5,10,3);ctx.strokeRect(-5,-1.5,10,3);}else{ctx.fillRect(-3,-2,6,4);ctx.strokeRect(-3,-2,6,4);}ctx.restore();ctx.save();ctx.translate(sx,sy);ctx.rotate(t.aimAngle);ctx.strokeStyle='rgba(255,255,255,0.4)';ctx.lineWidth=1;ctx.setLineDash([4,4]);ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(14,0);ctx.stroke();ctx.setLineDash([]);ctx.restore();}
ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(sx-r,sy-r-8,r*2,4);ctx.fillStyle=t.weaponType?'#66ccff':'#ffaa44';ctx.fillRect(sx-r,sy-r-8,r*2*(t.hp/t.maxHp),4);
if(t._hitFlash>0){ctx.save();ctx.globalAlpha=(t._hitFlash/0.1)*0.6;ctx.globalCompositeOperation='lighter';ctx.fillStyle='#fff';ctx.fillRect(sx-r,sy-r,r*2,r*2);ctx.restore();}
}
for(const t of turrets)allies.push(t);
}
function drawBoss(cx,cy){if(!boss)return;let sx=(boss.x-cx)*UNIT_PIXEL,sy=(boss.y-cy)*UNIT_PIXEL,r=boss.radius*UNIT_PIXEL;ctx.save();if(boss.invincible)ctx.globalAlpha=0.7;drawShape('boss',sx,sy,r*2,boss.color,boss.stroke);ctx.restore();if(boss._hitFlash>0){ctx.save();ctx.globalAlpha=(boss._hitFlash/0.08)*0.6;ctx.globalCompositeOperation='lighter';ctx.fillStyle='#ffffff';ctx.beginPath();ctx.arc(sx,sy,r*1.25,0,Math.PI*2);ctx.fill();ctx.restore();}ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(sx-50,sy-r-15,100,10);ctx.fillStyle='#ff3333';ctx.fillRect(sx-50,sy-r-15,100*(boss.hp/boss.maxHp),10);}
// ==================== 模块6:特效/子弹/UI ====================
function drawBullets(cx,cy){
bullets.forEach(b=>{let sx=(b.x-cx)*UNIT_PIXEL,sy=(b.y-cy)*UNIT_PIXEL,r=b.r*UNIT_PIXEL;if(b.type==='blazing'){ctx.save();ctx.globalCompositeOperation='lighter';const grad=ctx.createRadialGradient(sx,sy,0,sx,sy,r*1.8);grad.addColorStop(0,'rgba(255,240,180,1)');grad.addColorStop(0.5,'rgba(255,180,60,0.7)');grad.addColorStop(1,'rgba(255,80,0,0)');ctx.fillStyle=grad;ctx.beginPath();ctx.arc(sx,sy,r*1.8,0,Math.PI*2);ctx.fill();ctx.restore();return;}if(b.type==='hexGun'){ctx.fillStyle=b.color;ctx.save();ctx.translate(sx,sy);drawHexStarShape(0,0,r*2.5);ctx.fill();ctx.restore();return;}ctx.fillStyle=b.type==='kiteGun'?'#66ff66':b.color;if(b.type==='sniper')ctx.fillRect(sx-r*2,sy-r,r*4,r*2);else if(b.type==='lmg'){ctx.beginPath();for(let i=0;i<6;i++){let a=i*Math.PI/3,px=sx+Math.cos(a)*r,py=sy+Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();ctx.fill();}else if(b.type==='kiteGun'){ctx.fillRect(sx-r,sy-r,r*2,r*2);ctx.fillStyle='#33cc33';ctx.beginPath();ctx.arc(sx,sy,r*0.4,0,Math.PI*2);ctx.fill();}else ctx.fillRect(sx-r,sy-r,r*2,r*2);});
enemyBullets.forEach(b=>{let sx=(b.x-cx)*UNIT_PIXEL,sy=(b.y-cy)*UNIT_PIXEL,r=b.r*UNIT_PIXEL;if(b.type==='mirrorReflect'){ctx.save();ctx.globalCompositeOperation='lighter';ctx.fillStyle='#ffffff';ctx.beginPath();ctx.arc(sx,sy,r*1.4,0,Math.PI*2);ctx.fill();ctx.restore();return;}if(b.type==='hexStarBullet'){ctx.fillStyle='#66ddff';ctx.save();ctx.translate(sx,sy);drawHexStarShape(0,0,r*2.5);ctx.fill();ctx.restore();return;}if(b.type==='octStarBullet'){ctx.fillStyle='#ff88cc';ctx.save();ctx.translate(sx,sy);for(let i=0;i<16;i++){let rr=i%2===0?r*1.5:r,a=i*Math.PI/8-Math.PI/2,px=Math.cos(a)*rr,py=Math.sin(a)*rr;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();ctx.fill();ctx.restore();return;}ctx.fillStyle='#ffd700';ctx.beginPath();if(b.type==='boss')for(let i=0;i<7;i++){let a=i*2*Math.PI/7-Math.PI/2,px=sx+Math.cos(a)*r,py=sy+Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}else if(b.type==='octagon')for(let i=0;i<8;i++){let a=i*2*Math.PI/8,px=sx+Math.cos(a)*r,py=sy+Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}else if(b.type==='starBullet')for(let i=0;i<10;i++){let rr=i%2===0?r:r*0.5,a=i*Math.PI/5,px=sx+Math.cos(a)*rr,py=sy+Math.sin(a)*rr;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}else ctx.arc(sx,sy,r,0,Math.PI*2);ctx.closePath();ctx.fill();});
allyBullets.forEach(b=>{let sx=(b.x-cx)*UNIT_PIXEL,sy=(b.y-cy)*UNIT_PIXEL;ctx.fillStyle='#aaffaa';ctx.beginPath();ctx.arc(sx,sy,b.r*UNIT_PIXEL,0,Math.PI*2);ctx.fill();});
}
function drawBoomerang(cx,cy){if(!boomerang)return;let sx=(boomerang.x-cx)*UNIT_PIXEL,sy=(boomerang.y-cy)*UNIT_PIXEL,r=boomerang.r*UNIT_PIXEL*1.2;ctx.fillStyle='#ff99ff';ctx.strokeStyle='#cc66cc';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(sx,sy-r/2);ctx.lineTo(sx+r/2,sy);ctx.lineTo(sx,sy+r/2);ctx.lineTo(sx-r/2,sy);ctx.closePath();ctx.fill();ctx.stroke();ctx.beginPath();ctx.moveTo(sx+r/2,sy);ctx.lineTo(sx+r,sy-r/2);ctx.lineTo(sx+r,sy+r/2);ctx.lineTo(sx+r/2,sy);ctx.closePath();ctx.fill();ctx.stroke();}
function drawBurnZones(cx,cy){burnZones.forEach(z=>{let sx=(z.x-cx)*UNIT_PIXEL,sy=(z.y-cy)*UNIT_PIXEL,r=z.r*UNIT_PIXEL;ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fillStyle='rgba(255,100,0,0.2)';ctx.fill();ctx.strokeStyle='rgba(255,100,0,0.6)';ctx.lineWidth=2;ctx.stroke();});}
function drawFragments(cx,cy){fragments.forEach(f=>{let sx=(f.x-cx)*UNIT_PIXEL,sy=(f.y-cy)*UNIT_PIXEL,r=f.radius*UNIT_PIXEL;if(f.type==='small'){ctx.fillStyle='#ffcc00';ctx.strokeStyle='#cc9900';ctx.lineWidth=1;ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fill();ctx.stroke();return;}ctx.fillStyle='#000';ctx.strokeStyle='#333';ctx.lineWidth=1;if(f.type==='big'){ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fill();ctx.stroke();}else if(f.type==='square'){ctx.fillRect(sx-r,sy-r,r*2,r*2);ctx.strokeRect(sx-r,sy-r,r*2,r*2);}else if(f.type==='triangle'){ctx.beginPath();for(let i=0;i<3;i++){let a=i*2*Math.PI/3,px=sx+Math.cos(a)*r,py=sy+Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();ctx.fill();ctx.stroke();}else if(f.type==='pentagon'){ctx.beginPath();for(let i=0;i<5;i++){let a=i*2*Math.PI/5-Math.PI/2,px=sx+Math.cos(a)*r,py=sy+Math.sin(a)*r;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();ctx.fill();ctx.stroke();}else{ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fill();ctx.stroke();}});}
function drawTraps(cx,cy){traps.forEach(t=>{let sx=(t.x-cx)*UNIT_PIXEL,sy=(t.y-cy)*UNIT_PIXEL;ctx.fillStyle='rgba(255,255,255,0.8)';ctx.beginPath();for(let i=0;i<6;i++){let a=i*2*Math.PI/6-Math.PI/2,px=sx+Math.cos(a)*8,py=sy+Math.sin(a)*8;i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);}ctx.closePath();ctx.fill();});}
function drawExplosions(cx,cy){explosions.forEach(e=>{let sx=(e.x-cx)*UNIT_PIXEL,sy=(e.y-cy)*UNIT_PIXEL,r=e.r*UNIT_PIXEL;ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fillStyle='rgba(255,0,0,0.3)';ctx.fill();ctx.strokeStyle='rgba(255,0,0,0.8)';ctx.lineWidth=2;ctx.stroke();});}
function drawBossSkillWarnings(cx,cy){bossSkillWarnings.forEach(w=>{const sx=(w.x-cx)*UNIT_PIXEL,sy=(w.y-cy)*UNIT_PIXEL;if(w.type==='charge'){const x1=(w.x1-cx)*UNIT_PIXEL,y1=(w.y1-cy)*UNIT_PIXEL,x2=(w.x2-cx)*UNIT_PIXEL,y2=(w.y2-cy)*UNIT_PIXEL;ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.strokeStyle='rgba(255,0,0,0.8)';ctx.lineWidth=4;ctx.setLineDash([10,5]);ctx.stroke();ctx.setLineDash([]);}else if(w.type==='circle'){w.circles.forEach(c=>{const ccx=(c.x-cx)*UNIT_PIXEL,ccy=(c.y-cy)*UNIT_PIXEL;ctx.beginPath();ctx.arc(ccx,ccy,c.r*UNIT_PIXEL,0,Math.PI*2);ctx.strokeStyle='rgba(255,0,0,0.8)';ctx.lineWidth=3;ctx.setLineDash([8,4]);ctx.stroke();ctx.setLineDash([]);});}else if(w.type==='heptagram'){const R=(w.radius||HEPTAGRAM_R)*UNIT_PIXEL;const prog=Math.max(0,Math.min(1,1-w.timer/1));const alpha=0.35+prog*0.65;const scale=0.25+0.75*prog;const verts=heptagramVerts(sx,sy,R*scale);ctx.strokeStyle=`rgba(255,80,80,${alpha})`;ctx.lineWidth=3+prog*2;for(let i=0;i<7;i++){const v1=verts[i],v2=verts[(i+2)%7];ctx.beginPath();ctx.moveTo(v1.x,v1.y);ctx.lineTo(v2.x,v2.y);ctx.stroke();}for(const v of verts){ctx.beginPath();ctx.arc(v.x,v.y,4+prog*3,0,Math.PI*2);ctx.fillStyle=`rgba(255,200,100,${alpha})`;ctx.fill();}}});}
function drawShockwaveEffects(cx,cy){shockwaveEffects.forEach(e=>{let sx=(e.x-cx)*UNIT_PIXEL,sy=(e.y-cy)*UNIT_PIXEL,cr=e.radius*(1-e.timer/e.maxTimer)*UNIT_PIXEL;ctx.beginPath();ctx.arc(sx,sy,cr,0,Math.PI*2);ctx.strokeStyle='rgba(100,150,255,0.8)';ctx.lineWidth=3;ctx.stroke();});}
function drawEnemyHexShockwaves(cx,cy){enemyHexShockwaves.forEach(sw=>{let sx=(sw.x-cx)*UNIT_PIXEL,sy=(sw.y-cy)*UNIT_PIXEL;ctx.beginPath();ctx.arc(sx,sy,sw.currentRadius*UNIT_PIXEL,0,Math.PI*2);ctx.strokeStyle=sw.ally?'rgba(100,255,100,0.8)':'rgba(255,100,100,0.8)';ctx.lineWidth=3;ctx.stroke();});}
function drawAirStrikeEffect(cx,cy){if(!airStrikeData||!airStrikeData.active)return;let sx=(airStrikeData.startX-cx)*UNIT_PIXEL,sy=(airStrikeData.startY-cy)*UNIT_PIXEL;let len=100;ctx.save();ctx.translate(sx,sy);ctx.rotate(airStrikeData.angle);ctx.fillStyle='rgba(255,0,0,0.3)';ctx.fillRect(0,-5*UNIT_PIXEL/2,len*UNIT_PIXEL,5*UNIT_PIXEL);ctx.strokeStyle='rgba(255,50,50,0.9)';ctx.lineWidth=2;ctx.strokeRect(0,-5*UNIT_PIXEL/2,len*UNIT_PIXEL,5*UNIT_PIXEL);ctx.restore();}
// ==================== 模块6:UI绘制 ====================
function wrapText(text,x,y,maxWidth,lineHeight){let chars=text.split('');let line='';for(let i=0;i<chars.length;i++){let test=line+chars[i];if(ctx.measureText(test).width>maxWidth&&line!==''){ctx.fillText(line,x,y);line=chars[i];y+=lineHeight;}else line=test;}ctx.fillText(line,x,y);return y+lineHeight;}
function drawPediaList(){ctx.fillStyle='rgba(0,0,0,0.9)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);ctx.fillStyle='#fff';ctx.font='24px sans-serif';ctx.fillText('📖 观测档案',160,50);ctx.fillStyle='#ff6666';ctx.beginPath();ctx.arc(340,60,20,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.fillText('×',334,67);let allKeys=Object.keys(pediaData).filter(k=>discoveredEnemies[k]);let perPage=7,totalPages=Math.max(1,Math.ceil(allKeys.length/perPage));if(pediaPage>=totalPages)pediaPage=totalPages-1;if(pediaPage<0)pediaPage=0;let start=pediaPage*perPage;let pageKeys=allKeys.slice(start,start+perPage);ctx.fillStyle='#aaa';ctx.font='12px sans-serif';ctx.fillText(`第${pediaPage+1}/${totalPages}页`,20,50);let y=80;pageKeys.forEach(k=>{ctx.fillStyle='rgba(255,255,255,0.15)';ctx.fillRect(40,y,320,32);ctx.strokeStyle='#fff';ctx.strokeRect(40,y,320,32);ctx.fillStyle='#fff';ctx.font='15px sans-serif';ctx.fillText(pediaData[k].name,55,y+21);y+=38;});if(allKeys.length===0){ctx.fillStyle='#666';ctx.font='14px sans-serif';ctx.fillText('未发现任何敌人',140,200);}ctx.fillStyle=pediaPage>0?'#66ccff':'#444';ctx.beginPath();ctx.arc(pediaPrevBtn.x,pediaPrevBtn.y,pediaPrevBtn.r,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.textAlign='center';ctx.fillText('‹',pediaPrevBtn.x,pediaPrevBtn.y+7);ctx.textAlign='left';ctx.fillStyle=pediaPage<totalPages-1?'#66ccff':'#444';ctx.beginPath();ctx.arc(pediaNextBtn.x,pediaNextBtn.y,pediaNextBtn.r,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.textAlign='center';ctx.fillText('›',pediaNextBtn.x,pediaNextBtn.y+7);ctx.textAlign='left';}
function drawPediaDetail(){ctx.fillStyle='rgba(0,0,0,0.9)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);let d=pediaData[pediaSelectedType];if(!d)return;ctx.fillStyle='#fff';ctx.font='22px sans-serif';ctx.fillText(d.name,20,45);ctx.fillStyle='#ffcc00';ctx.fillRect(20,60,70,28);ctx.fillStyle='#000';ctx.font='14px sans-serif';ctx.fillText('返回',35,79);drawShape(pediaSelectedType,80,170,80,d.color,'#000');ctx.fillStyle='#fff';ctx.font='12px sans-serif';let endY=wrapText(d.desc,180,110,200,18);ctx.fillStyle='#aaa';wrapText(d.comment,180,endY+10,200,18);}
function handlePediaTap(c){if(pediaSelectedType){if(c.x>=20&&c.x<=90&&c.y>=60&&c.y<=88)pediaSelectedType=null;return;}if(isInsideCircle(c.x,c.y,pediaCloseBtn)){pediaOpen=false;return;}if(isInsideCircle(c.x,c.y,pediaPrevBtn)){if(pediaPage>0)pediaPage--;return;}if(isInsideCircle(c.x,c.y,pediaNextBtn)){let allKeys=Object.keys(pediaData).filter(k=>discoveredEnemies[k]);let totalPages=Math.max(1,Math.ceil(allKeys.length/7));if(pediaPage<totalPages-1)pediaPage++;return;}let allKeys=Object.keys(pediaData).filter(k=>discoveredEnemies[k]);let start=pediaPage*7,pageKeys=allKeys.slice(start,start+7);let y=80;for(let k of pageKeys){if(isInsideRect(c.x,c.y,{x:40,y,w:320,h:32})){pediaSelectedType=k;return;}y+=38;}}
const INTEL_INFO={square:{n:'方',c:'#66ccff'},triangle:{n:'三',c:'#ff9966'},trapezoid:{n:'梯',c:'#cc99ff'},isoscelesTrapezoid:{n:'等',c:'#99ff99'},pentagon:{n:'五',c:'#ffd700'},hexagon:{n:'六',c:'#66ffff'},octagon:{n:'八',c:'#ff6666'},kite:{n:'筝',c:'#66ff66'},crescent:{n:'月',c:'#ccddff'},lShape:{n:'L',c:'#ccddff'},star:{n:'星',c:'#ffff99'},hexStar:{n:'六星',c:'#66ddff'},spiral:{n:'螺',c:'#88bbff'},fourStar:{n:'四',c:'#ffaa66'},octStar:{n:'八角',c:'#ff88cc'},arrow:{n:'箭',c:'#ff9999'},parallelogram:{n:'平',c:'#cc99ff'},heart:{n:'心',c:'#ff6699'},diamond:{n:'钻',c:'#88ddff'},smallDiamond:{n:'碎',c:'#aaddff'},solidQuad:{n:'立',c:'#ccd5ee'},dodecagon:{n:'十二',c:'#88eebb'},sun:{n:'日',c:'#ffdd44'},zShape:{n:'Z',c:'#ffdd88'},shenShape:{n:'申',c:'#88ddff'},boss:{n:'B',c:'#cc99ff'}};
const INTEL_PANEL={x:250,y:75,w:145,h:18},INTEL_TOGGLE={x:340,y:96,r:7};
function drawIntelPanel(){
if(shopOpen||labOpen||phoneOpen||pediaOpen)return;
const totalCount=monsters.length+(boss?1:0);let remainVal=0;for(const m of monsters)remainVal+=(m.value||1);if(boss)remainVal+=100;const isOpen=window._intelPanelOpen;const px=INTEL_PANEL.x,py=INTEL_PANEL.y,pw=INTEL_PANEL.w,ph=INTEL_PANEL.h;
ctx.save();ctx.fillStyle='rgba(20,15,40,0.78)';ctx.fillRect(px,py,pw,ph);ctx.strokeStyle='rgba(255,153,204,0.6)';ctx.lineWidth=1;ctx.strokeRect(px+0.5,py+0.5,pw-1,ph-1);ctx.font='bold 10px sans-serif';ctx.fillStyle='#ff99cc';ctx.textAlign='left';ctx.fillText('W'+waveCount,px+5,py+13);ctx.fillStyle='#ffffff';ctx.font='10px sans-serif';ctx.fillText('敌 '+totalCount,px+30,py+13);ctx.fillStyle='#ffdd66';ctx.fillText('⚡'+remainVal,px+65,py+13);
if(totalCount===0&&waveDelay>0){ctx.fillStyle='#88ff88';ctx.fillText('下一波 '+waveDelay.toFixed(1)+'s',px+98,py+13);}
ctx.fillStyle='rgba(255,153,204,0.9)';ctx.beginPath();if(isOpen){ctx.moveTo(INTEL_TOGGLE.x-5,INTEL_TOGGLE.y+2);ctx.lineTo(INTEL_TOGGLE.x+5,INTEL_TOGGLE.y+2);ctx.lineTo(INTEL_TOGGLE.x,INTEL_TOGGLE.y-4);}else{ctx.moveTo(INTEL_TOGGLE.x-5,INTEL_TOGGLE.y-4);ctx.lineTo(INTEL_TOGGLE.x+5,INTEL_TOGGLE.y-4);ctx.lineTo(INTEL_TOGGLE.x,INTEL_TOGGLE.y+2);}ctx.closePath();ctx.fill();ctx.restore();
if(!isOpen)return;const counts={};for(const m of monsters)counts[m.type]=(counts[m.type]||0)+1;if(boss)counts.boss=1;const types=Object.keys(counts).sort((a,b)=>counts[b]-counts[a]);if(types.length===0)return;const perRow=4,rowH=16;const startY=py+ph+4;
for(let i=0;i<types.length;i++){const t=types[i];const row=Math.floor(i/perRow);const col=i%perRow;const bx=px+col*36;const by=startY+row*rowH;const info=INTEL_INFO[t]||{n:t.slice(0,2),c:'#aaa'};ctx.save();ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(bx,by,34,14);ctx.strokeStyle=info.c;ctx.lineWidth=1;ctx.strokeRect(bx+0.5,by+0.5,33,13);ctx.fillStyle=info.c;ctx.font='bold 10px sans-serif';ctx.textAlign='left';ctx.fillText(info.n,bx+3,by+10);ctx.fillStyle='#fff';ctx.textAlign='right';ctx.fillText('×'+counts[t],bx+31,by+10);ctx.restore();}
ctx.textAlign='left';
}
function drawUI(){
ctx.fillStyle='#fff';ctx.font='12px sans-serif';ctx.fillText(`HP: ${Math.ceil(player.hp)}/${getMaxHp()}`,10,20);if(player.shield>0)ctx.fillText(`盾: ${Math.ceil(player.shield)}`,10,32);ctx.fillText(`能量: ${Math.round(energy)}`,10,44);ctx.fillText(`结晶: ${crystals}`,10,56);ctx.fillText(`波次: ${waveCount}`,10,68);
let w=weapons[currentWeaponIndex];ctx.fillStyle='#ffff99';ctx.fillText(`${w.name}`,10,82);if(['gun','kiteGun','shotgun','paraGun','saw','sniper','lmg','hexGun','blazing'].includes(w.type))ctx.fillText(`弹匣: ${w.currentMag}/${w.magSize}`,10,95);else if(w.type==='grenade'||w.type==='burn'||w.type==='trap')ctx.fillText(`数量: ${w.count}`,10,95);
if(speedBattleWave){ctx.fillStyle='#ff6666';ctx.font='bold 14px sans-serif';ctx.fillText(`⏱️ ${Math.ceil(speedBattleTimer)}s`,10,115);}
let mapX=CANVAS_SIZE-70,mapY=10,mapW=60,mapH=60;ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(mapX-2,mapY-2,mapW+4,mapH+4);ctx.fillStyle='rgba(255,255,255,0.1)';ctx.fillRect(mapX,mapY,mapW,mapH);ctx.strokeStyle='#fff';ctx.strokeRect(mapX,mapY,mapW,mapH);let scx=mapW/WORLD_SIZE,scy=mapH/WORLD_SIZE;
ctx.fillStyle='#ff99cc';ctx.beginPath();ctx.arc(mapX+player.x*scx,mapY+player.y*scy,3,0,Math.PI*2);ctx.fill();
for(const m of monsters){let color='#ff3333';if(m.type==='zShape')color='#888888';else if(m.type==='heart')color='#ff88aa';ctx.fillStyle=color;ctx.beginPath();ctx.arc(mapX+m.x*scx,mapY+m.y*scy,2,0,Math.PI*2);ctx.fill();}
if(boss){ctx.fillStyle='#cc99ff';ctx.beginPath();ctx.arc(mapX+boss.x*scx,mapY+boss.y*scy,4,0,Math.PI*2);ctx.fill();}
ctx.fillStyle=manualMode?'#ffcc00':'rgba(255,255,255,0.3)';ctx.fillRect(170,10,80,20);ctx.strokeStyle='#fff';ctx.strokeRect(170,10,80,20);ctx.fillStyle='#000';ctx.fillText(manualMode?'手动开':'手动关',180,25);
ctx.fillStyle='rgba(255,255,255,0.3)';ctx.fillRect(260,10,60,20);ctx.strokeRect(260,10,60,20);ctx.fillStyle='#000';ctx.fillText('武器',272,25);
ctx.fillStyle='rgba(255,255,255,0.3)';ctx.fillRect(330,10,60,20);ctx.strokeRect(330,10,60,20);ctx.fillStyle='#000';ctx.fillText('支援',348,25);
ctx.fillStyle='rgba(255,255,255,0.3)';ctx.fillRect(10,62,60,20);ctx.strokeRect(10,62,60,20);ctx.fillStyle='#000';ctx.fillText('档案',28,77);
ctx.fillStyle='rgba(255,255,255,0.3)';ctx.fillRect(80,62,60,20);ctx.strokeRect(80,62,60,20);ctx.fillStyle='#000';ctx.fillText('实验室',92,77);
ctx.beginPath();ctx.arc(340,240,25,0,Math.PI*2);ctx.fillStyle=player.dashCooldown>0?'rgba(100,100,100,0.5)':'rgba(255,200,0,0.8)';ctx.fill();ctx.strokeStyle='#fff';ctx.stroke();ctx.fillStyle='#fff';ctx.fillText('冲',335,245);if(player.dashCooldown>0)ctx.fillText(Math.ceil(player.dashCooldown),335,270);
if(manualMode&&!player.dead){ctx.beginPath();ctx.arc(340,270,25,0,Math.PI*2);ctx.fillStyle='rgba(255,100,100,0.7)';ctx.fill();ctx.strokeStyle='#fff';ctx.stroke();ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.fillText('⚔',330,277);ctx.beginPath();ctx.arc(280,270,25,0,Math.PI*2);ctx.fillStyle='rgba(100,100,255,0.7)';ctx.fill();ctx.strokeStyle='#fff';ctx.stroke();ctx.fillStyle='#fff';ctx.font='16px sans-serif';ctx.fillText('装填',264,276);}
let barY=CANVAS_SIZE-50;ctx.fillStyle='rgba(0,0,0,0.7)';ctx.fillRect(0,barY,CANVAS_SIZE,50);ctx.fillStyle='#555';ctx.fillRect(0,barY,30,50);ctx.fillRect(CANVAS_SIZE-30,barY,30,50);ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.textAlign='center';ctx.fillText('<',15,barY+35);ctx.fillText('>',CANVAS_SIZE-15,barY+35);ctx.textAlign='left';
let slotW=(CANVAS_SIZE-60)/3;for(let i=0;i<3;i++){let idx=weaponPage*3+i;if(idx>=weapons.length)continue;let ww=weapons[idx],sx2=30+i*slotW;ctx.fillStyle=idx===currentWeaponIndex?'#ffff99':'#fff';ctx.font='12px sans-serif';ctx.fillText(ww.runUnlocked?ww.name:'未解锁',sx2+5,barY+25);if(['gun','kiteGun','shotgun','paraGun','saw','sniper','lmg','hexGun','blazing'].includes(ww.type))ctx.fillText(`${ww.currentMag}/${ww.magSize}`,sx2+5,barY+42);else if(ww.type==='grenade'||ww.type==='burn'||ww.type==='trap')ctx.fillText(`${ww.count}`,sx2+5,barY+42);}
if(eventNotice){ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(100,120,200,30);ctx.fillStyle='#ffcc00';ctx.font='14px sans-serif';ctx.textAlign='center';ctx.fillText(eventNotice,200,140);ctx.textAlign='left';}
if(shopOpen){ctx.fillStyle='rgba(0,0,0,0.85)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);ctx.fillStyle='#fff';ctx.font='24px sans-serif';ctx.fillText('武器模块',160,50);ctx.fillStyle='#ff6666';ctx.beginPath();ctx.arc(340,60,20,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.fillText('×',334,67);ctx.font='12px sans-serif';ctx.fillText(`第${shopPage+1}/4页`,160,75);getPageItems(shopPage).forEach(item=>{let canBuy=energy>=item.cost;if(item.name==='十字架')canBuy=canBuy&&player.hp<getMaxHp();if(item.name==='双菱形回旋镖')canBuy=canBuy&&!weapons[2].purchased;if(item.name==='筝形冲锋枪')canBuy=canBuy&&!weapons[3].purchased;if(item.name==='扇形霰弹枪')canBuy=canBuy&&!weapons[4].purchased;if(item.name==='五角星锯')canBuy=canBuy&&!weapons[6].purchased;if(item.name==='平行四边形步枪')canBuy=canBuy&&!weapons[8].purchased;if(item.name==='箭头长矛')canBuy=canBuy&&!weapons[SPEAR_INDEX].purchased;if(item.name==='长方形狙击枪')canBuy=canBuy&&weapons[9].purchased&&!weapons[9].runUnlocked;if(item.name==='正六边形轻机枪')canBuy=canBuy&&weapons[10].purchased&&!weapons[10].runUnlocked;if(item.name==='燃烧弹弹药')canBuy=canBuy&&weapons[11].purchased;if(item.name==='六角星追踪枪')canBuy=canBuy&&weapons[12].purchased&&!weapons[12].runUnlocked;ctx.fillStyle='rgba(255,255,255,0.1)';ctx.fillRect(item.x,item.y,item.w,item.h);ctx.strokeStyle=canBuy?'#ffff00':'#555';ctx.strokeRect(item.x,item.y,item.w,item.h);ctx.fillStyle='#fff';ctx.fillText(item.name,item.x+10,item.y+20);ctx.fillText(item.desc+` (${item.cost}能量)`,item.x+10,item.y+40);});ctx.fillStyle='#fff';ctx.fillText('点击底部翻页',150,CANVAS_SIZE-20);}
if(labOpen){
ctx.fillStyle='rgba(0,0,0,0.9)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);ctx.fillStyle='#fff';ctx.font='22px sans-serif';ctx.fillText('🔬 实验室',160,50);ctx.fillStyle='#ff6666';ctx.beginPath();ctx.arc(340,60,20,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.fillText('×',334,67);ctx.fillStyle=labTab==='research'?'#ffcc00':'rgba(255,255,255,0.3)';ctx.fillRect(60,80,130,30);ctx.strokeStyle='#fff';ctx.strokeRect(60,80,130,30);ctx.fillStyle='#000';ctx.font='14px sans-serif';ctx.fillText('模块研发',88,100);ctx.fillStyle=labTab==='upgrade'?'#ffcc00':'rgba(255,255,255,0.3)';ctx.fillRect(210,80,130,30);ctx.strokeRect(210,80,130,30);ctx.fillStyle='#000';ctx.fillText('强化能力',240,100);ctx.font='14px sans-serif';ctx.fillStyle='#fff';ctx.fillText(`💎 ${crystals}`,20,130);ctx.fillText(`⚡ ${Math.round(energy)}`,90,130);
if(labTab==='research'){
labWeapons.forEach((lw,i)=>{let w=weapons[lw.index],rect={x:60,y:150+i*45,w:280,h:40};ctx.fillStyle='rgba(255,255,255,0.1)';ctx.fillRect(rect.x,rect.y,rect.w,rect.h);ctx.strokeStyle=w.purchased?'#66ff66':crystals>=lw.cost?'#ffff00':'#555';ctx.strokeRect(rect.x,rect.y,rect.w,rect.h);ctx.fillStyle='#fff';ctx.font='14px sans-serif';ctx.fillText(w.name,rect.x+10,rect.y+20);if(w.purchased)ctx.fillStyle='#66ff66';else ctx.fillStyle='#ffcc00';ctx.fillText(w.purchased?'已解锁':`${lw.cost}结晶解锁`,rect.x+200,rect.y+20);});
const startY=150+labWeapons.length*45;
labSummons.forEach((item,i)=>{const y=startY+i*45;const rect={x:60,y:y,w:280,h:40};const unlocked=window.summonUnlocks[item.key];const canBuy=crystals>=item.cost;ctx.fillStyle='rgba(255,255,255,0.1)';ctx.fillRect(rect.x,rect.y,rect.w,rect.h);ctx.strokeStyle=unlocked?'#66ff66':(canBuy?'#ffff00':'#555');ctx.lineWidth=1;ctx.strokeRect(rect.x+0.5,rect.y+0.5,rect.w-1,rect.h-1);ctx.fillStyle='#fff';ctx.font='14px sans-serif';ctx.fillText(item.name,rect.x+10,rect.y+20);ctx.font='11px sans-serif';ctx.fillStyle='#aaa';ctx.fillText(`支援模块可用 ${item.price} 能量召唤`,rect.x+10,rect.y+34);if(unlocked){ctx.fillStyle='#66ff66';ctx.font='14px sans-serif';ctx.fillText('已解锁',rect.x+210,rect.y+24);}else{ctx.fillStyle=canBuy?'#ffcc00':'#888';ctx.font='14px sans-serif';ctx.fillText(`${item.cost}结晶`,rect.x+210,rect.y+24);}});
}else{
let list=[{type:'hp',name:'生命强化',desc:'最大生命+20',cost:upgradeCosts.hp},{type:'support',name:'支援强化',desc:'消耗-10%',cost:upgradeCosts.support},{type:'move',name:'移动强化',desc:'速度+0.2',cost:upgradeCosts.move},{type:'damage',name:'输出强化',desc:'伤害+10%',cost:upgradeCosts.damage}];
list.forEach((u,i)=>{let rect={x:60,y:150+i*50,w:280,h:45};let canBuy=upgrades[u.type]<upgradeMax&&energy>=u.cost;ctx.fillStyle='rgba(255,255,255,0.1)';ctx.fillRect(rect.x,rect.y,rect.w,rect.h);ctx.strokeStyle=canBuy?'#ffff00':'#555';ctx.strokeRect(rect.x,rect.y,rect.w,rect.h);ctx.fillStyle='#fff';ctx.font='14px sans-serif';ctx.fillText(`${u.name} ${upgrades[u.type]}/${upgradeMax}`,rect.x+10,rect.y+20);ctx.font='11px sans-serif';ctx.fillText(u.desc,rect.x+10,rect.y+36);if(upgrades[u.type]<upgradeMax){ctx.fillStyle=canBuy?'#ffcc00':'#888';ctx.font='14px sans-serif';ctx.fillText(`${u.cost}`,rect.x+230,rect.y+25);}else{ctx.fillStyle='#66ff66';ctx.font='14px sans-serif';ctx.fillText('满级',rect.x+230,rect.y+25);}});
}
}
if(phoneOpen){
ctx.fillStyle='rgba(0,0,0,0.85)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);ctx.fillStyle='#fff';ctx.font='24px sans-serif';ctx.fillText('📱 支援模块',140,50);ctx.fillStyle='#ff6666';ctx.beginPath();ctx.arc(340,60,20,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.fillText('×',334,67);ctx.font='12px sans-serif';ctx.fillText(`第${phonePage+1}/3页`,160,75);if(upgrades.support>0){ctx.fillStyle='#66ff66';ctx.fillText(`支援消耗 -${upgrades.support*10}%`,120,95);}
const discount=getSupportDiscount();
if(phonePage===0){ctx.fillStyle='#ffcc99';ctx.fillRect(60,130,280,40);ctx.fillStyle='#000';ctx.font='13px sans-serif';ctx.fillText(`曲奇 ${Math.round(10*discount)}能量 回5血 加速`,70,155);ctx.fillStyle='#ffcc99';ctx.fillRect(60,180,280,40);ctx.fillStyle='#000';ctx.fillText(`草莓牛奶 ${Math.round(20*discount)}能量 回15血 减伤`,70,205);ctx.fillStyle='#ffcc99';ctx.fillRect(60,230,280,40);ctx.fillStyle='#000';ctx.fillText(`巧克力 ${Math.round(30*discount)}能量 回30血 增伤`,70,255);}
else if(phonePage===1){
const labels={ally_square:{n:'正方形',c:Math.round(5*discount)},ally_triangle:{n:'三角形',c:Math.round(15*discount)},ally_trapezoid:{n:'梯形',c:Math.round(10*discount)},ally_pentagon:{n:'五边形',c:Math.round(30*discount)},ally_hexagon:{n:'六边形',c:Math.round(30*discount)},ally_kite:{n:'筝形',c:Math.round(25*discount)},ally_octagon:{n:'八边形',c:Math.round(35*discount)},ally_crescent:{n:'月牙',c:Math.round(18*discount)},ally_hexStar:{n:'六角星',c:Math.round(30*discount)}};
const rects=getPhoneButtonRects();for(const r of rects){const info=labels[r.action];if(!info)continue;const isLocked=(r.action==='ally_hexStar'&&!window.summonUnlocks.hexStar);ctx.fillStyle=isLocked?'#333':(r.action==='ally_hexStar'?'#ffdd66':'#9999ff');ctx.fillRect(r.x,r.y,r.w,r.h);ctx.strokeStyle='rgba(255,255,255,0.4)';ctx.lineWidth=1;ctx.strokeRect(r.x+0.5,r.y+0.5,r.w-1,r.h-1);ctx.font='12px sans-serif';if(isLocked){ctx.fillStyle='#888';ctx.fillText('未解锁',r.x+8,r.y+25);}else{ctx.fillStyle='#fff';ctx.fillText(info.n,r.x+6,r.y+18);ctx.font='11px sans-serif';ctx.fillStyle='rgba(255,255,255,0.85)';ctx.fillText(info.c+'能量',r.x+6,r.y+32);}}
}else{
ctx.fillStyle='#ff9999';ctx.fillRect(60,110,280,44);ctx.fillStyle='#fff';ctx.font='13px sans-serif';ctx.fillText(airStrikeCooldown>0?`轨道打击冷却 ${Math.ceil(airStrikeCooldown)}s`:`轨道打击 ${Math.round(50*discount)}能量`,72,138);
ctx.fillStyle=_sunNextWave?'#ffaa44':'#ffdd66';ctx.fillRect(60,162,280,44);ctx.strokeStyle='#cc8800';ctx.lineWidth=2;ctx.strokeRect(60,162,280,44);ctx.fillStyle='#442200';ctx.font='bold 12px sans-serif';ctx.fillText(_sunNextWave?'☀️ 太阳已在下一波安排':`☀️ 破晓之时 ${Math.round(50*discount)}能量(下一波召唤太阳)`,68,190);
ctx.fillStyle='#8899cc';ctx.fillRect(60,214,280,44);ctx.fillStyle='#fff';ctx.font='bold 13px sans-serif';ctx.fillText(`🔫 炮塔座架 ${Math.round(45*discount)}能量`,75,242);ctx.font='10px sans-serif';ctx.fillStyle='rgba(255,255,255,0.85)';ctx.fillText('在当前位置部署,可放入非初始/非近战武器',75,254);
}
ctx.fillStyle='#fff';ctx.font='12px sans-serif';ctx.fillText('点击底部翻页',150,CANVAS_SIZE-20);
}
if(pediaOpen){if(pediaSelectedType)drawPediaDetail();else drawPediaList();}
drawIntelPanel();
if(interactAllyIndex>=0&&interactAllyIndex<allies.length){let a=allies[interactAllyIndex];if(a.type==='pentagon'&&energy>=10){ctx.fillStyle='rgba(255,255,0,0.8)';ctx.fillRect(CANVAS_SIZE-90,CANVAS_SIZE-160,80,30);ctx.fillStyle='#000';ctx.font='12px sans-serif';ctx.fillText('投入10能量',CANVAS_SIZE-85,CANVAS_SIZE-140);}}
if(window.interactTurretIndex>=0&&!shopOpen&&!labOpen&&!phoneOpen&&!pediaOpen){const t=allies[window.interactTurretIndex];if(t&&!t.weaponType){const w=weapons[currentWeaponIndex];if(w&&w.runUnlocked&&TURRET_ALLOWED.has(w.type)){ctx.fillStyle='rgba(100,150,255,0.9)';ctx.fillRect(CANVAS_SIZE-120,CANVAS_SIZE-200,110,40);ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.strokeRect(CANVAS_SIZE-120,CANVAS_SIZE-200,110,40);ctx.fillStyle='#fff';ctx.font='bold 12px sans-serif';ctx.fillText('放入武器',CANVAS_SIZE-105,CANVAS_SIZE-182);ctx.font='10px sans-serif';ctx.fillText(w.name,CANVAS_SIZE-105,CANVAS_SIZE-168);}}}
}
function drawMenu(){ctx.fillStyle='rgba(0,0,0,0.9)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);ctx.fillStyle='#fff';ctx.font='bold 28px sans-serif';ctx.textAlign='center';ctx.fillText('选择关卡',CANVAS_SIZE/2,80);ctx.textAlign='left';ctx.fillStyle='#ff99cc';ctx.fillRect(60,140,280,60);ctx.strokeStyle='#ff66aa';ctx.strokeRect(60,140,280,60);ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.textAlign='center';ctx.fillText('第一关',CANVAS_SIZE/2,178);ctx.textAlign='left';ctx.fillStyle='#888';ctx.fillRect(60,220,280,60);ctx.strokeStyle='#555';ctx.strokeRect(60,220,280,60);ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.textAlign='center';ctx.fillText('第二关(制作中)',CANVAS_SIZE/2,258);ctx.textAlign='left';ctx.fillStyle='#aaa';ctx.font='14px sans-serif';ctx.textAlign='center';ctx.fillText('点击返回首页',CANVAS_SIZE/2,320);ctx.textAlign='left';}
function drawHome(){ctx.fillStyle='#2a2a40';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);for(let i=0;i<=CANVAS_SIZE;i+=20){ctx.beginPath();ctx.moveTo(i,0);ctx.lineTo(i,CANVAS_SIZE);ctx.strokeStyle='#3a3a55';ctx.lineWidth=0.5;ctx.stroke();ctx.beginPath();ctx.moveTo(0,i);ctx.lineTo(CANVAS_SIZE,i);ctx.stroke();}ctx.fillStyle='#fff';ctx.font='bold 28px sans-serif';ctx.textAlign='center';ctx.fillText('几何特工',CANVAS_SIZE/2,80);ctx.textAlign='left';let ballY=homeBallY+Math.sin(homeBallBounce*0.5)*5;ctx.beginPath();ctx.arc(homeBallX,ballY,20,0,Math.PI*2);ctx.fillStyle='#ff99cc';ctx.fill();ctx.strokeStyle='#ff66aa';ctx.stroke();ctx.fillStyle='#ff99cc';ctx.fillRect(50,220,300,100);ctx.strokeStyle='#ff66aa';ctx.strokeRect(50,220,300,100);ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.textAlign='center';ctx.fillText('开始游戏',200,275);ctx.textAlign='left';ctx.fillStyle='#ffff99';ctx.font='14px sans-serif';ctx.fillText(`💎 结晶:${crystals}`,20,360);}
function drawBossIntro(){ctx.fillStyle='rgba(0,0,0,0.8)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);const cxp=CANVAS_SIZE/2,cyp=CANVAS_SIZE/2,R=50;const p=bossIntro.phase;const pts=[];for(let i=0;i<7;i++){const a=i*2*Math.PI/7-Math.PI/2;pts.push({x:cxp+Math.cos(a)*R,y:cyp+Math.sin(a)*R});}if(p==='circle'){ctx.beginPath();ctx.arc(cxp,cyp,bossIntro.timer*50,0,Math.PI*2);ctx.strokeStyle='#fff';ctx.lineWidth=3;ctx.stroke();}else if(p==='points'){const shown=Math.min(7,Math.floor(bossIntro.timer/0.6*7)+1);for(let i=0;i<shown;i++){ctx.beginPath();ctx.arc(pts[i].x,pts[i].y,5,0,Math.PI*2);ctx.fillStyle='#ffcc00';ctx.fill();}}else if(p==='heptagon'){for(const pt of pts){ctx.beginPath();ctx.arc(pt.x,pt.y,5,0,Math.PI*2);ctx.fillStyle='#ffcc00';ctx.fill();}const prog=Math.min(1,bossIntro.timer/0.8);const seg=7*prog;ctx.strokeStyle='rgba(255,200,100,0.9)';ctx.lineWidth=3;ctx.beginPath();for(let i=0;i<=7;i++){const a=i*2*Math.PI/7-Math.PI/2;const px=cxp+Math.cos(a)*R,py=cyp+Math.sin(a)*R;if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);if(i>=seg)break;}ctx.stroke();}else if(p==='star'){for(const pt of pts){ctx.beginPath();ctx.arc(pt.x,pt.y,5,0,Math.PI*2);ctx.fillStyle='#ffcc00';ctx.fill();}ctx.strokeStyle='rgba(255,200,100,0.25)';ctx.lineWidth=2;ctx.beginPath();for(let i=0;i<=7;i++){const a=i*2*Math.PI/7-Math.PI/2;const px=cxp+Math.cos(a)*R,py=cyp+Math.sin(a)*R;if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);}ctx.stroke();const prog=Math.min(1,bossIntro.timer/0.9);const edgesShown=prog*7;ctx.strokeStyle='rgba(255,90,90,0.95)';ctx.lineWidth=4;for(let i=0;i<7;i++){if(i>=edgesShown)break;const v1=pts[i],v2=pts[(i+2)%7];ctx.beginPath();ctx.moveTo(v1.x,v1.y);ctx.lineTo(v2.x,v2.y);ctx.stroke();}}else{ctx.fillStyle='#fff';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);}}
// ==================== 模块6:主绘制 ====================
function draw(){
ctx.clearRect(0,0,CANVAS_SIZE,CANVAS_SIZE);if(!gameStarted){drawHome();return;}if(menuOpen){drawMenu();return;}if(bossIntro){drawBossIntro();return;}
let cx=Math.max(0,Math.min(WORLD_SIZE-VIEW_SIZE,player.x-VIEW_SIZE/2));let cy=Math.max(0,Math.min(WORLD_SIZE-VIEW_SIZE,player.y-VIEW_SIZE/2));ctx.fillStyle='#2a2a40';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);
for(let i=Math.floor(cx);i<=Math.ceil(cx+VIEW_SIZE);i++){let x=(i-cx)*UNIT_PIXEL;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,CANVAS_SIZE);ctx.strokeStyle=i%5===0?'#555577':'#3a3a55';ctx.lineWidth=i%5===0?1:0.5;ctx.stroke();}
for(let j=Math.floor(cy);j<=Math.ceil(cy+VIEW_SIZE);j++){let y=(j-cy)*UNIT_PIXEL;ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(CANVAS_SIZE,y);ctx.strokeStyle=j%5===0?'#555577':'#3a3a55';ctx.lineWidth=j%5===0?1:0.5;ctx.stroke();}
ctx.strokeStyle='#ff99cc';ctx.lineWidth=3;ctx.strokeRect((0-cx)*UNIT_PIXEL,(0-cy)*UNIT_PIXEL,WORLD_SIZE*UNIT_PIXEL,WORLD_SIZE*UNIT_PIXEL);
structures.forEach(s=>s.walls.forEach(w=>drawWall(w,cx,cy)));drawBurnZones(cx,cy);drawFragments(cx,cy);drawTraps(cx,cy);drawExplosions(cx,cy);drawBossSkillWarnings(cx,cy);drawShockwaveEffects(cx,cy);drawEnemyHexShockwaves(cx,cy);drawAirStrikeEffect(cx,cy);drawAllies(cx,cy);drawMonsters(cx,cy);drawBoss(cx,cy);drawBullets(cx,cy);drawBoomerang(cx,cy);
let sx=(player.x-cx)*UNIT_PIXEL,sy=(player.y-cy)*UNIT_PIXEL,pr=player.radius*UNIT_PIXEL;ctx.beginPath();ctx.arc(sx,sy,pr,0,Math.PI*2);ctx.fillStyle='#ff99cc';ctx.fill();ctx.strokeStyle='#ff66aa';ctx.lineWidth=2;ctx.stroke();ctx.save();ctx.translate(sx,sy);ctx.rotate(player.aimAngle);ctx.fillStyle='rgba(255,255,255,0.8)';ctx.fillRect(pr,-1.5,16,3);ctx.restore();
let w2=weapons[currentWeaponIndex];ctx.save();ctx.translate(sx+Math.cos(player.aimAngle)*(pr+12),sy+Math.sin(player.aimAngle)*(pr+12));ctx.rotate(player.aimAngle);ctx.fillStyle=w2.color||'#fff';ctx.strokeStyle='#000';ctx.lineWidth=2;if(w2.type==='melee'&&w2.damagePattern){ctx.beginPath();ctx.moveTo(6,0);ctx.lineTo(-3,-5);ctx.lineTo(-3,5);ctx.closePath();ctx.fill();ctx.stroke();}else if(w2.type==='melee'&&!w2.damagePattern){ctx.fillRect(-8,-1.5,14,3);ctx.strokeRect(-8,-1.5,14,3);ctx.beginPath();ctx.moveTo(6,-4);ctx.lineTo(13,0);ctx.lineTo(6,4);ctx.closePath();ctx.fill();ctx.stroke();}else if(['gun','kiteGun','sniper','paraGun','lmg','hexGun'].includes(w2.type)){ctx.fillRect(-5,-2.5,10,5);ctx.strokeRect(-5,-2.5,10,5);}else if(w2.type==='shotgun'){ctx.fillRect(-6,-3,12,6);ctx.strokeRect(-6,-3,12,6);}else if(w2.type==='boomerang'){ctx.beginPath();ctx.moveTo(0,-5);ctx.lineTo(5,0);ctx.lineTo(0,5);ctx.lineTo(-5,0);ctx.closePath();ctx.fill();ctx.stroke();}else if(w2.type==='grenade'||w2.type==='burn'){ctx.beginPath();ctx.arc(0,0,5,0,Math.PI*2);ctx.fill();ctx.stroke();if(w2.type==='burn'){ctx.fillStyle='#ff6600';ctx.beginPath();ctx.arc(0,0,2.5,0,Math.PI*2);ctx.fill();}}else if(w2.type==='blazing'){ctx.beginPath();ctx.arc(0,0,6,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#fff8aa';ctx.beginPath();ctx.arc(0,0,3,0,Math.PI*2);ctx.fill();}else if(w2.type==='saw'){ctx.beginPath();for(let i=0;i<6;i++){let a=i*Math.PI/3;ctx.lineTo(Math.cos(a)*5,Math.sin(a)*5);}ctx.closePath();ctx.fill();ctx.stroke();}else if(w2.type==='trap'){ctx.beginPath();for(let i=0;i<10;i++){let r=i%2===0?5:2.5,a=i*Math.PI/5;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r);}ctx.closePath();ctx.fill();ctx.stroke();}ctx.restore();
if(attackEffectTimer>0){let alpha=Math.min(1,attackEffectTimer/0.3);ctx.beginPath();ctx.moveTo(sx,sy);ctx.lineTo(sx+Math.cos(player.aimAngle)*30,sy+Math.sin(player.aimAngle)*30);ctx.lineTo(sx+Math.cos(player.aimAngle+Math.PI/2)*20,sy+Math.sin(player.aimAngle+Math.PI/2)*20);ctx.closePath();ctx.fillStyle=`rgba(255,255,200,${alpha*0.5})`;ctx.fill();ctx.strokeStyle=`rgba(255,255,100,${alpha})`;ctx.lineWidth=2;ctx.stroke();}
for(const p of particles){const px=(p.x-cx)*UNIT_PIXEL,py=(p.y-cy)*UNIT_PIXEL,sz=p.size*UNIT_PIXEL;const a=Math.min(1,p.life/p.maxLife);ctx.globalAlpha=a;ctx.fillStyle=p.color;if(p.shape==='square')ctx.fillRect(px-sz,py-sz,sz*2,sz*2);else{ctx.beginPath();ctx.arc(px,py,sz,0,Math.PI*2);ctx.fill();}}ctx.globalAlpha=1;
ctx.font='bold 14px sans-serif';ctx.textAlign='center';for(const d of damageNumbers){const px=(d.x-cx)*UNIT_PIXEL,py=(d.y-cy)*UNIT_PIXEL;const a=Math.min(1,d.life/d.maxLife);ctx.globalAlpha=a;ctx.lineWidth=3;ctx.strokeStyle='#000';ctx.strokeText(d.text,px,py);ctx.fillStyle=d.color||'#ffee66';ctx.fillText(d.text,px,py);}ctx.globalAlpha=1;ctx.textAlign='left';
if(playerHurtFlash>0){const alpha=Math.min(0.7,playerHurtFlash*2);const grad=ctx.createRadialGradient(CANVAS_SIZE/2,CANVAS_SIZE/2,CANVAS_SIZE*0.28,CANVAS_SIZE/2,CANVAS_SIZE/2,CANVAS_SIZE*0.72);grad.addColorStop(0,'rgba(255,0,0,0)');grad.addColorStop(1,`rgba(255,30,30,${alpha})`);ctx.fillStyle=grad;ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);}
if(player._healFlash>0){player._healFlash-=1/60;const a=Math.min(1,player._healFlash*4);ctx.save();ctx.globalCompositeOperation='lighter';ctx.globalAlpha=a*0.7;ctx.strokeStyle='#66ff88';ctx.lineWidth=3;ctx.beginPath();ctx.arc(sx,sy,pr*1.6,0,Math.PI*2);ctx.stroke();ctx.restore();}
if(player._confuseTimer>0){const t=performance.now()/250;ctx.save();ctx.strokeStyle='rgba(255,100,150,0.75)';ctx.lineWidth=2;for(let i=0;i<3;i++){const a=t+i*Math.PI*2/3;ctx.beginPath();ctx.arc(sx+Math.cos(a)*22,sy+Math.sin(a)*22,4,0,Math.PI*2);ctx.stroke();}ctx.restore();}
drawUI();
if(moveJoystick.active){ctx.beginPath();ctx.arc(moveJoystick.centerX,moveJoystick.centerY,moveJoystick.maxRadius,0,Math.PI*2);ctx.fillStyle='rgba(255,255,255,0.15)';ctx.fill();ctx.strokeStyle='rgba(255,255,255,0.5)';ctx.stroke();ctx.beginPath();ctx.arc(moveJoystick.centerX+moveJoystick.dx,moveJoystick.centerY+moveJoystick.dy,25,0,Math.PI*2);ctx.fillStyle='rgba(255,153,204,0.8)';ctx.fill();}
if(aimJoystick.active){ctx.beginPath();ctx.arc(aimJoystick.centerX,aimJoystick.centerY,aimJoystick.maxRadius,0,Math.PI*2);ctx.fillStyle='rgba(255,255,255,0.15)';ctx.fill();ctx.strokeStyle='rgba(255,255,255,0.5)';ctx.stroke();ctx.beginPath();ctx.arc(aimJoystick.centerX+aimJoystick.dx,aimJoystick.centerY+aimJoystick.dy,25,0,Math.PI*2);ctx.fillStyle='rgba(255,255,100,0.8)';ctx.fill();}
if(player.dead){ctx.fillStyle='rgba(0,0,0,0.7)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);ctx.fillStyle='#ff66aa';ctx.font='30px sans-serif';ctx.textAlign='center';ctx.fillText('你死了',CANVAS_SIZE/2,CANVAS_SIZE/2);ctx.fillStyle='rgba(255,255,255,0.2)';ctx.fillRect(80,250,240,40);ctx.fillRect(80,300,240,40);ctx.strokeStyle='#fff';ctx.strokeRect(80,250,240,40);ctx.strokeRect(80,300,240,40);ctx.fillStyle='#fff';ctx.font='16px sans-serif';ctx.fillText('再来一次',200,275);ctx.fillText('返回首页',200,325);ctx.textAlign='left';}
if(isVictory){ctx.fillStyle='rgba(0,0,0,0.8)';ctx.fillRect(0,0,CANVAS_SIZE,CANVAS_SIZE);ctx.fillStyle='#ffcc00';ctx.font='28px sans-serif';ctx.textAlign='center';ctx.fillText('🎉 胜利! 🎉',CANVAS_SIZE/2,CANVAS_SIZE/2-60);ctx.fillStyle='#fff';ctx.font='20px sans-serif';ctx.fillText('获得 1288 个曲奇!',CANVAS_SIZE/2,CANVAS_SIZE/2-20);ctx.fillStyle='rgba(255,255,255,0.2)';ctx.fillRect(80,250,240,40);ctx.fillRect(80,300,240,40);ctx.strokeStyle='#fff';ctx.strokeRect(80,250,240,40);ctx.strokeRect(80,300,240,40);ctx.fillStyle='#fff';ctx.font='16px sans-serif';ctx.fillText('再来一次',200,275);ctx.fillText('返回首页',200,325);ctx.textAlign='left';}
}
// ==================== 模块6:重置 ====================
function resetGame(){
player.x=50;player.y=50;player.hp=player.maxHp=100;player.shield=0;player.dead=false;player.speed=player.baseSpeed=5;player.aimAngle=0;player.invincibleTime=2;player.slowEffects=[];player.stunTime=0;player.dashCooldown=0;player.dashing=false;player.dashTimer=0;player.foodEffects=[];player._confuseTimer=0;player._confuseDir=null;player._confuseNextChange=0;player._healFlash=0;
upgrades={hp:0,support:0,move:0,damage:0};currentWeaponIndex=0;
weapons.forEach((w,i)=>{w.runUnlocked=i<2;if(i>1&&i!==9&&i!==10&&i!==11&&i!==12&&i!==BLAZING_INDEX)w.purchased=false;if(w.type==='grenade'||w.type==='burn'||w.type==='trap')w.count=0;if(['gun','kiteGun','shotgun','paraGun','saw','sniper','lmg','hexGun','blazing'].includes(w.type)){w.currentMag=w.magSize;w.isReloading=false;}});weapons[2].ready=true;weapons[2].cooldown=0;
boomerang=null;bullets=[];grenades=[];explosions=[];enemyBullets=[];allyBullets=[];traps=[];allies=[];structures=[];fragments=[];burnZones=[];monsters=[];sunBullets=[];particles=[];damageNumbers=[];playerHurtFlash=0;hitstopTimer=0;
energy=0;waveCount=0;waveDelay=0;boss=null;bossIntro=null;isVictory=false;specialEvent=null;infiniteSurvivalMode=false;infiniteSurvivalTime=0;eventNotice='';absenceCounters={pentagon:0,hexagon:0,kite:0,octagon:0,crescent:0,arrow:0};spawnedThisWave=[];airStrikeCooldown=0;airStrikeData=null;interactAllyIndex=-1;pediaOpen=false;pediaSelectedType=null;pediaPage=0;lastSingleType='';bossSkillWarnings=[];shockwaveEffects=[];enemyHexShockwaves=[];massMutationWave=false;speedBattleWave=false;speedBattleTimer=0;speedBattleOverTime=0;labTab='research';attackEffectTimer=0;_exterminateMode=false;_diamondMergeTimer=0;_sunNextWave=false;window._nextWaveBudgetBonus=0;window.interactTurretIndex=-1;
if(_sunBlazingUnlocked){weapons[BLAZING_INDEX].purchased=true;weapons[BLAZING_INDEX].runUnlocked=true;weapons[BLAZING_INDEX].currentMag=weapons[BLAZING_INDEX].magSize;}
generateStructures();spawnWave();
}
function goHome(){gameStarted=false;menuOpen=false;homeBallX=-30;homeBallY=CANVAS_SIZE/2;homeBallBounce=0;player.dead=false;isVictory=false;_exterminateMode=false;window._nextWaveBudgetBonus=0;window.interactTurretIndex=-1;saveGame();}
function updateDiscovery(){let dirty=false;for(const m of monsters){const k=m.type==='boss'?'boss':m.type;if(pediaData[k]&&!discoveredEnemies[k]){discoveredEnemies[k]=true;dirty=true;}}if(boss&&!discoveredEnemies.boss){discoveredEnemies.boss=true;dirty=true;}if(dirty)saveGame();}
function startBossFight(){const savedSun=monsters.find(m=>m.isSun&&m.hp>0);boss=createBoss();monsters=[];if(savedSun){monsters.push(savedSun);eventNotice='☀️ 太阳升起来了!';}
['square','square','square','square','square','square','square','square','square','trapezoid','trapezoid','trapezoid','trapezoid','trapezoid','trapezoid','triangle','triangle','triangle','triangle','hexagon','hexagon','hexagon','kite','kite','octagon','pentagon','isoscelesTrapezoid','isoscelesTrapezoid','fourStar','fourStar','star','star','lShape','lShape','hexStar','hexStar','spiral','spiral','octStar','crescent','arrow'].forEach(t=>{let a=Math.random()*Math.PI*2,d=5+Math.random()*15;let x=Math.max(2,Math.min(WORLD_SIZE-2,boss.x+Math.cos(a)*d));let y=Math.max(2,Math.min(WORLD_SIZE-2,boss.y+Math.sin(a)*d));monsters.push(createMonster(t,x,y));});}
// ==================== 模块6:触摸事件 ====================
canvas.addEventListener('touchstart',e=>{
e.preventDefault();AudioSys.ensure();if(!gameStarted){gameStarted=true;menuOpen=true;return;}
if(menuOpen){let c=getCanvasCoords(e.changedTouches[0].clientX,e.changedTouches[0].clientY);if(c.y>=140&&c.y<=200){menuOpen=false;resetGame();return;}if(c.y>=220&&c.y<=280){menuOpen=false;gameStarted=false;return;}if(c.y>=310&&c.y<=330){goHome();return;}return;}
if(player.dead||isVictory){let c=getCanvasCoords(e.changedTouches[0].clientX,e.changedTouches[0].clientY);if(c.y>=250&&c.y<=290){resetGame();return;}if(c.y>=300&&c.y<=340){goHome();return;}}
for(let i=0;i<e.changedTouches.length;i++){
let t=e.changedTouches[i],c=getCanvasCoords(t.clientX,t.clientY);
if(!shopOpen&&!labOpen&&!phoneOpen&&!pediaOpen){if(Math.hypot(c.x-INTEL_TOGGLE.x,c.y-INTEL_TOGGLE.y)<=INTEL_TOGGLE.r+6){window._intelPanelOpen=!window._intelPanelOpen;continue;}}
if(pediaOpen){handlePediaTap(c);continue;}
if(shopOpen){if(isInsideCircle(c.x,c.y,{x:340,y:60,r:20})){shopOpen=false;continue;}if(c.y>=CANVAS_SIZE-40){shopPage=(shopPage+1)%4;continue;}getPageItems(shopPage).forEach(item=>{if(isInsideRect(c.x,c.y,item))purchaseItem(item);});continue;}
if(labOpen){if(isInsideCircle(c.x,c.y,{x:340,y:60,r:20})){labOpen=false;continue;}if(c.y>=80&&c.y<=110&&c.x>=60&&c.x<=190){labTab='research';continue;}if(c.y>=80&&c.y<=110&&c.x>=210&&c.x<=340){labTab='upgrade';continue;}if(labTab==='research'){let handled=false;labWeapons.forEach((item,idx)=>{let rect={x:60,y:150+idx*45,w:280,h:40};if(isInsideRect(c.x,c.y,rect)&&!weapons[item.index].purchased&&crystals>=item.cost){crystals-=item.cost;weapons[item.index].purchased=true;saveGame();AudioSys.upgrade();handled=true;}});if(!handled){const startY=150+labWeapons.length*45;labSummons.forEach((item,i)=>{const y=startY+i*45;const rect={x:60,y:y,w:280,h:40};if(isInsideRect(c.x,c.y,rect)&&!window.summonUnlocks[item.key]&&crystals>=item.cost){crystals-=item.cost;window.summonUnlocks[item.key]=true;saveGame();AudioSys.upgrade();eventNotice='🔓 已解锁:'+item.name;}});}}else{let list=[{type:'hp',y:150},{type:'support',y:200},{type:'move',y:250},{type:'damage',y:300}];list.forEach(u=>{if(isInsideRect(c.x,c.y,{x:60,y:u.y,w:280,h:45}))purchaseUpgrade(u.type);});}continue;}
if(phoneOpen){if(isInsideCircle(c.x,c.y,{x:340,y:60,r:20})){phoneOpen=false;continue;}if(c.y>=CANVAS_SIZE-40){phonePage=(phonePage+1)%3;continue;}handlePhoneTap(c);continue;}
if(interactAllyIndex>=0&&interactAllyIndex<allies.length){let a=allies[interactAllyIndex];if(a.type==='pentagon'&&energy>=10&&isInsideRect(c.x,c.y,{x:CANVAS_SIZE-90,y:CANVAS_SIZE-160,w:80,h:30})){energy-=10;for(let j=0;j<5;j++){let da=Math.random()*Math.PI*2+j*2.4;allyBullets.push({x:a.x,y:a.y,vx:Math.cos(da)*4,vy:Math.sin(da)*4,r:0.3,dmg:10,life:30,type:'goldenTriangle'});}let child=createMonster('pentagon',a.x,a.y);child.isAlly=true;child.mutation=null;child.gen=1;child.radius=0.6*Math.pow(0.7,1);child.hp=child.maxHp=60;child.speed=3;child.damage=5;child.value=4;child.canSplit=true;child.stateTimer=10;allies.push(child);a.stateTimer=30;continue;}}
if(window.interactTurretIndex>=0&&!shopOpen&&!labOpen&&!phoneOpen&&!pediaOpen){const btn={x:CANVAS_SIZE-120,y:CANVAS_SIZE-200,w:110,h:40};if(isInsideRect(c.x,c.y,btn)){const turret=allies[window.interactTurretIndex];if(turret&&!turret.weaponType)putWeaponInTurret(turret);e.stopImmediatePropagation();return;}}
if(isInsideRect(c.x,c.y,{x:170,y:10,w:80,h:20})){manualMode=!manualMode;continue;}
if(isInsideRect(c.x,c.y,{x:260,y:10,w:60,h:20})){shopOpen=true;continue;}
if(isInsideRect(c.x,c.y,{x:330,y:10,w:60,h:20})){phoneOpen=true;phonePage=0;continue;}
if(isInsideRect(c.x,c.y,{x:10,y:62,w:60,h:20})){pediaOpen=true;pediaSelectedType=null;pediaPage=0;continue;}
if(isInsideRect(c.x,c.y,{x:80,y:62,w:60,h:20})){labOpen=true;continue;}
if(isInsideCircle(c.x,c.y,{x:340,y:240,r:25})){performDash();continue;}
if(manualMode){if(isInsideCircle(c.x,c.y,{x:340,y:270,r:25})){let w=weapons[currentWeaponIndex];if(attackCooldown<=0){if(w.type==='melee')performMelee();else if(['gun','kiteGun','shotgun','paraGun','sniper','lmg','hexGun'].includes(w.type)){if(w.currentMag>0)performShoot();else startReload();}else if(w.type==='blazing'){performShoot();}else if(w.type==='boomerang'){if(w.ready&&!boomerang)performBoomerang();}else if(w.type==='grenade'){if(w.count>0)performGrenade();}else if(w.type==='burn'){if(w.count>0)performBurn();}else if(w.type==='saw'){if(w.currentMag>0)performSaw();else startReload();}else if(w.type==='trap'){if(w.count>0)performTrap();}attackCooldown=w.attackInterval;}continue;}if(isInsideCircle(c.x,c.y,{x:280,y:270,r:25})){startReload();continue;}}
if(c.y>=CANVAS_SIZE-50){if(c.x<30||c.x>=CANVAS_SIZE-30){weaponPage=(weaponPage+1)%Math.ceil(weapons.length/3);continue;}let slot=Math.floor((c.x-30)/((CANVAS_SIZE-60)/3)),idx=weaponPage*3+slot;if(idx>=0&&idx<weapons.length&&weapons[idx].runUnlocked)currentWeaponIndex=idx;continue;}
if(c.x<canvas.width/2&&!moveJoystick.active){moveJoystick.active=true;moveJoystick.touchId=t.identifier;moveJoystick.centerX=c.x;moveJoystick.centerY=c.y;moveJoystick.dx=moveJoystick.dy=0;}else if(c.x>=canvas.width/2&&!aimJoystick.active){aimJoystick.active=true;aimJoystick.touchId=t.identifier;aimJoystick.centerX=c.x;aimJoystick.centerY=c.y;aimJoystick.dx=aimJoystick.dy=0;}
}
});
canvas.addEventListener('touchmove',e=>{e.preventDefault();for(let i=0;i<e.changedTouches.length;i++){let t=e.changedTouches[i],c=getCanvasCoords(t.clientX,t.clientY);if(t.identifier===moveJoystick.touchId&&moveJoystick.active){let dx=c.x-moveJoystick.centerX,dy=c.y-moveJoystick.centerY,d=Math.hypot(dx,dy);if(d>moveJoystick.maxRadius){dx=dx/d*moveJoystick.maxRadius;dy=dy/d*moveJoystick.maxRadius;}moveJoystick.dx=dx;moveJoystick.dy=dy;}if(t.identifier===aimJoystick.touchId&&aimJoystick.active){let dx=c.x-aimJoystick.centerX,dy=c.y-aimJoystick.centerY,d=Math.hypot(dx,dy);if(d>aimJoystick.maxRadius){dx=dx/d*aimJoystick.maxRadius;dy=dy/d*aimJoystick.maxRadius;}aimJoystick.dx=dx;aimJoystick.dy=dy;if(d>5)player.aimAngle=Math.atan2(dy,dx);}}});
function releaseJoystick(e){for(let i=0;i<e.changedTouches.length;i++){let t=e.changedTouches[i];if(t.identifier===moveJoystick.touchId){moveJoystick.active=false;moveJoystick.touchId=null;moveJoystick.dx=moveJoystick.dy=0;}if(t.identifier===aimJoystick.touchId){aimJoystick.active=false;aimJoystick.touchId=null;aimJoystick.dx=aimJoystick.dy=0;}}}
canvas.addEventListener('touchend',releaseJoystick);canvas.addEventListener('touchcancel',releaseJoystick);canvas.addEventListener('pointerdown',()=>AudioSys.ensure(),{passive:true});
canvas.addEventListener('mousemove',e=>{if(!window.pcMode||!gameStarted||player.dead||isVictory)return;const c=getCanvasCoords(e.clientX,e.clientY);const cx=Math.max(0,Math.min(WORLD_SIZE-VIEW_SIZE,player.x-VIEW_SIZE/2));const cy=Math.max(0,Math.min(WORLD_SIZE-VIEW_SIZE,player.y-VIEW_SIZE/2));player.aimAngle=Math.atan2((cy+c.y/UNIT_PIXEL)-player.y,(cx+c.x/UNIT_PIXEL)-player.x);});
// ==================== 初始化 & 游戏循环 ====================
generateStructures();spawnWave();let lastTime=performance.now();let _lastWaveCount=waveCount;let _lastBoss=null;
function gameLoop(t){let dt=Math.min((t-lastTime)/1000,0.1);lastTime=t;if(!gameStarted){update(dt);}else if(!menuOpen&&!player.dead&&!isVictory){update(dt);updateDiscovery();interactAllyIndex=-1;for(let i=0;i<allies.length;i++){let a=allies[i];if(a.type==='pentagon'&&Math.hypot(a.x-player.x,a.y-player.y)<2){interactAllyIndex=i;break;}}window.interactTurretIndex=-1;if(!shopOpen&&!labOpen&&!phoneOpen&&!pediaOpen){for(let i=0;i<allies.length;i++){const a=allies[i];if(!a.isTurret||a.weaponType)continue;if(Math.hypot(a.x-player.x,a.y-player.y)<2){window.interactTurretIndex=i;break;}}}if(waveCount!==_lastWaveCount){_lastWaveCount=waveCount;AudioSys.wave();}if(!_lastBoss&&boss)AudioSys.boss();_lastBoss=boss;}draw();requestAnimationFrame(gameLoop);}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
大功告成!Game Source: 几何特工 - 最终整合版
Creator: BraveRaven69
Libraries: none
Complexity: complex (1169 lines, 194.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: game-braveraven69" to link back to the original. Then publish at arcadelab.ai/publish.