🎮ArcadeLab

几何特工 - V37

by SilverOtter29
5321 lines237.2 KB
▶ Play
<!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>几何特工 - V37</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;

// ==================== 模块1:音效系统 ====================
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);}
  };
})();

// ==================== 模块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}];

// ==================== 模块1:存档系统 ====================
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();

// ==================== 模块1:变异系统 ====================
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;
}

// ==================== 模块1:怪物创建 ====================
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:24,color:'#ffdd44',stroke:'#cc8800',value:25,isSun:true,_sunShootTimer:2,_sunTeleportTimer:3,_sunHealTimer:1,_sunDotTimer:1,_sunTeleportWarning:null}
 };

 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;
 }
 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};}

// ==================== 模块1:粒子/飘字 ====================
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});
}

// ==================== 模块1:伤害函数 ====================
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;
}
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;
}

// ==================== 模块1:食物/强化效果 ====================
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);}

// ← 继续粘下一段
// ==================== 模块2:墙体 ====================
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;
}

// ==================== 模块2: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();}

// ==================== 模块2:移动 ====================
function moveToward(e,tx,ty,dt,isP,isT){
 const dx=tx-e.x,dy=ty-e.y;
 const 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)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;
const 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;
const PARA_DEFLECT_MIN=0.20;
const PARA_DEFLECT_MAX=0.40;
const 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;
}

// ==================== 模块2:目标决策 ====================
function findEnemyTarget(m){
 // 太阳优先(12格内)
 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};
}

// ==================== 模块2:结构系统 ====================
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();
}

// ==================== 模块3:波次系统 ====================
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
 }[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(){
 // 缺席计数
 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');

 pool.sort(()=>Math.random()-0.5);
 let currentValue=0;
 for(let i=0;i<pool.length&&currentValue<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!';
 }
}

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'});
}
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);
}

// ← 继续粘下一段
// ==================== 模块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();
  const dmg=w.damage*dm;
  attackEffectTimer=0.3;
  const rs=w.range*w.range;
  const arc=w.arcAngle/2;
  monsters.forEach(m=>{
   const dx=m.x-player.x,dy=m.y-player.y;
   const ds=dx*dx+dy*dy,d=Math.sqrt(ds);
   const 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;
   const 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];
 const 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();
}

// ==================== 模块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);
 }
}

// ==================== 模块4:钻石 ====================
const DIAMOND_SPLIT_INTERVAL=8;
const DIAMOND_MAX_SPLIT=10;
const DIAMOND_SPLIT_RADIUS=9;
const 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;
const HEART_HOSTILE_LIFESTEAL=1.0;
const HEART_ALLY_HEAL=3;
const HEART_ALLY_HEAL_INTERVAL=3;
const HEART_CONVERT_INTERVAL=25;
const HEART_CONVERT_RANGE=20;
const HEART_CONFUSE_TIME=5;
const HEART_CONFUSE_CHANGE_MIN=0.15;
const HEART_CONFUSE_CHANGE_MAX=0.40;
const HEART_COUNTER_DMG=7;
const HEART_COUNTER_COOLDOWN=0.5;
const 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:'太阳'
};

function getHeartConvertChance(heartValue,targetValue){
 const diff=targetValue-heartValue;
 if(diff<=-2)return 1.00;
 if(diff<=1)return 0.70;
 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')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;
const DODEC_HP_PER_VALUE=5;
const DODEC_DMG_PER_VALUE=0.5;
const DODEC_SPD_PER_10=0.75;
const DODEC_AREA_PER_VALUE=0.03;

function absorbFragmentForDodec(m,f){
 if(m._dodecAbsorbed>=DODEC_MAX_ABSORB)return false;
 const canAbsorb=Math.min(f.value,DODEC_MAX_ABSORB-m._dodecAbsorbed);
 if(canAbsorb<=0)return false;
 m._dodecAbsorbed+=canAbsorb;
 const baseHp=m.maxHp,baseDmg=m.damage,baseSpeed=m.speed,baseRadius=m.radius;
 m.maxHp=m.hp=Math.round(baseHp+canAbsorb*DODEC_HP_PER_VALUE);
 m.damage=baseDmg+canAbsorb*DODEC_DMG_PER_VALUE;
 m.speed=baseSpeed+Math.floor(canAbsorb/10)*DODEC_SPD_PER_10;
 m.radius=baseRadius*Math.sqrt(1+canAbsorb*DODEC_AREA_PER_VALUE);
 // 价值同步(盟友吸收时消耗)
 f.value-=canAbsorb;
 return true;
}
function updateDodecagon(m,dt){
 // 20格内是否有玩家
 const pd=Math.hypot(player.x-m.x,player.y-m.y);
 if(pd>=20){
  // 找最近碎片
  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&&nd>m.radius+0.5){
   moveToward(m,nearest.x,nearest.y,dt,false,false);
  } else if(nearest&&nd<=m.radius+0.5){
   // 吸收
   if(m._dodecAbsorbed<DODEC_MAX_ABSORB){
    const before=m._dodecAbsorbed;
    absorbFragmentForDodec(m,nearest);
    if(m.isAlly){
     // 盟友形态:能量还是归玩家
     energy+=Math.min(nearest.value,(before+ (nearest.value)) - m._dodecAbsorbed < 0 ? nearest.value : nearest.value);
    }
    // 简单处理:把碎片移除,能量给玩家
    const absorbed=m._dodecAbsorbed-before;
    energy+=absorbed;
    const idx=fragments.indexOf(nearest);
    if(idx>=0)fragments.splice(idx,1);
   }
  }
 } else {
  // 追击玩家
  moveWithPathfinding(m,player.x,player.y,dt,false,false);
 }
 m.stuckTimer=0;
}

// ==================== 模块4:太阳 ====================
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;
  const 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+2);
 }
 // 全图灼伤
 sun._sunDotTimer-=dt;
 if(sun._sunDotTimer<=0){
  sun._sunDotTimer=1;
  for(const m of monsters){if(m!==sun&&m.hp>0)m.hp-=1;}
  if(!player.dead&&player.invincibleTime<=0)player.hp-=1;
  for(const a of allies){if(a.hp>0)a.hp-=1;}
 }
 // 热浪
 sun._sunShootTimer-=dt;
 if(sun._sunShootTimer<=0){
  sun._sunShootTimer=2;
  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};}
  }
  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:24,life:20,
    hitSet:new Set()
   });
  }
 }
 // 传送
 sun._sunTeleportTimer-=dt;
 if(sun._sunTeleportTimer<=0&&!sun._sunTeleportWarning){
  sun._sunTeleportTimer=3;
  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(target)sun._sunTeleportWarning={x:target.x,y:target.y,timer:3};
 }
 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,4,0.5);
   for(const m of monsters){
    if(m===sun||m.hp<=0)continue;
    if(Math.hypot(m.x-tx,m.y-ty)<4)m.hp-=48;
   }
   for(const a of allies){
    if(Math.hypot(a.x-tx,a.y-ty)<4)a.hp-=48;
   }
   if(Math.hypot(player.x-tx,player.y-ty)<4&&player.invincibleTime<=0){
    damagePlayer(48);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:怪物主更新 ====================
const POISON_DMG_MULT=0.5;
const SOLID_QUAD_ALPHA_INV=0.15;
const 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);}
 }
 // NaN 修复
 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;
  }
 }
 // 缴械处理
 const _disabled=[];
 for(const m of monsters){
  if(m._attackDisabledTimer>0){
   m._attackDisabledTimer-=dt;
   if(m._attackDisabledTimer>0){_disabled.push({m,orig:m.damage});m.damage=0;}
  }
 }
 if(boss&&boss._attackDisabledTimer>0){
  boss._attackDisabledTimer-=dt;
  if(boss._attackDisabledTimer>0){_disabled.push({m:boss,orig:boss.damage});boss.damage=0;}
 }
 // 钻石聚合
 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 item of _disabled){item.m.damage=item.orig;}

 // 后处理
 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;
const 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))));
 });
}

// ==================== 模块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;
const MIRROR_REFLECT_DMG=1.0;
const HEXGUN_BLAST_RADIUS=2.5;
const HEXGUN_BLAST_RATIO=0.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;
 }
 // 记录 hexGun 位置
 const hexGunBefore=[];
 for(const b of bullets){
  if(b.type==='hexGun')hexGunBefore.push({b,x:b.x,y:b.y,dmg:b.dmg});
 }
 // blazing 穿透
 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);
 }
 // hexGun 爆炸检测
 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;
   }
  }
 }
 // 盟友子弹
 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(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);
 }
}

// ==================== 模块4:粒子/飘字 ====================
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;
}

// ==================== 模块4:冲刺 ====================
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);
}

// 新盟友 AI(12种)
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;
 const dy=target?target.y-a.y:0;
 const 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;
    const ux=dx/jd,uy=dy/jd;
    const side=Math.random()<0.5?1:-1;
    const 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);
    const 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 pd=Math.hypot(player.x-a.x,player.y-a.y);
   if(pd>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 pd=Math.hypot(player.x-a.x,player.y-a.y);
   if(pd>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(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;
  }
  // buff 过期
  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;}

  // 原版 7 种 AI
  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));
 }
}

// ==================== 模块4:主更新 ====================
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;

 // Boss 入场动画
 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;
  let 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();
  }
 }
}

// 波次卡死兜底
let _stuckTimer=0;
const _updateForStuck=update;
update=function(dt){
 _updateForStuck(dt);
 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:实验室 ====================
function purchaseUpgrade(type){
 let cost=upgradeCosts[type];
 if(upgrades[type]>=upgradeMax)return;
 if(energy<cost)return;
 energy-=cost;
 upgrades[type]++;
 if(type==='hp')player.hp+=20;
 AudioSys.upgrade();
}

const labSummons=[
 {key:'hexStar',name:'六角星召唤',cost:3,price:30}
];

// ==================== 模块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 pediaCloseBtn={x:340,y:60,r:20};
const pediaPrevBtn={x:40,y:365,r:18};
const 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:130,w:280,h:50,action:'airstrike'},
  {x:60,y:190,w:280,h:50,action:'sun'}
 ];
 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.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:'"我照亮的,不止是你们。"'},
 boss:{name:'正七边形Boss',color:'#cc99ff',desc:'生命777,速度2.5。多种技能,343进入二阶段。',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);
 ctx.fill();ctx.stroke();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();
}

// ==================== 模块6:绘制墙体 ====================
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();
}

// ==================== 模块6:绘制怪物 ====================
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 {
   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==='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;
   const wy=(m._sunTeleportWarning.y-cy)*UNIT_PIXEL;
   const 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 drawAllies(cx,cy){
 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);}
 });
}

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:绘制子弹/特效 ====================
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'},boss:{n:'B',c:'#cc99ff'}
};
const INTEL_PANEL={x:250,y:75,w:145,h:18};
const 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();
 ctx.fillStyle='#ff3333';monsters.forEach(m=>{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 {
   // 第3页:航道轰炸 + 太阳
   ctx.fillStyle='#ff9999';ctx.fillRect(60,130,280,50);
   ctx.fillStyle='#fff';ctx.font='14px sans-serif';
   ctx.fillText(airStrikeCooldown>0?`轨道打击冷却 ${Math.ceil(airStrikeCooldown)}s`:`轨道打击 ${Math.round(50*discount)}能量`,70,160);
   // 太阳
   ctx.fillStyle=_sunNextWave?'#ffaa44':'#ffdd66';
   ctx.fillRect(60,190,280,50);
   ctx.strokeStyle='#cc8800';ctx.lineWidth=2;ctx.strokeRect(60,190,280,50);
   ctx.fillStyle='#442200';ctx.font='bold 13px sans-serif';
   if(_sunNextWave){ctx.fillText('☀️ 太阳已在下一波安排',75,221);}
   else{ctx.fillText(`☀️ 破晓之时 ${Math.round(50*discount)}能量(下一波召唤太阳)`,72,221);}
  }
  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);
  }
 }
}

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;

 // 破晓之时保留解锁
 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;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(){
 boss=createBoss();monsters=[];
 ['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(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});

// ==================== 初始化 & 游戏循环 ====================
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;}
  }
  if(waveCount!==_lastWaveCount){_lastWaveCount=waveCount;AudioSys.wave();}
  if(!_lastBoss&&boss)AudioSys.boss();
  _lastBoss=boss;
 }

 draw();
 requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
// ==================== 整合补丁包 V37 → V47 ====================
(function patchV38toV47(){
  'use strict';

  // ══════════════════════════════════════════════════════════
  // 常量
  // ══════════════════════════════════════════════════════════
  const TURRET_HP = 200;
  const TURRET_RADIUS = 0.4;
  const TURRET_RANGE = 20;
  const TURRET_ALLOWED = new Set(['boomerang','kiteGun','shotgun','saw','paraGun','sniper','lmg','hexGun']);
  const ALLY_INVINCIBLE_TIME = 0.01;
  const CONTACT_CD = 0.5;
  const NOTICE_DURATION = 3.5;

  window.interactTurretIndex = -1;
  window._nextWaveBudgetBonus = 0;
  window.pcMode = false;

  // ══════════════════════════════════════════════════════════
  // ① 图鉴文案
  // ══════════════════════════════════════════════════════════
  if(typeof pediaData !== 'undefined'){
    if(pediaData.spiral) pediaData.spiral.comment = '恶果自食吧你!';
    if(pediaData.octStar) pediaData.octStar.comment = '凝聚了希望之后绝望才显得更加锋利';
    if(pediaData.heart) pediaData.heart.comment = '躁动的感情就是魔鬼!';
    if(pediaData.diamond) pediaData.diamond.comment = '至臻永恒,纯粹流淌,千命葬汝心';
    if(pediaData.smallDiamond) pediaData.smallDiamond.comment = '直到分离,我才看见世界……';
    if(pediaData.solidQuad) pediaData.solidQuad.comment = '“即使多次尝试,但是我们不得不承认,我们无法对从未见过的存在想象建模”';
    if(pediaData.sun) pediaData.sun.comment = '冉冉大日啊!告诉我!这是谁的黄昏?!谁的黎明?!';
    if(pediaData.turret) pediaData.turret.comment = '当时作者正在咖啡厅里写作业,然后她的那只笔落入了奶茶杯……';

    pediaData.zShape = {
      name: 'Z字形',
      color: '#ffdd88',
      desc: '生命70,价值6,主动远离玩家。30秒后若未被击杀便消失,使下一波预算+10。击杀后额外给予4能量。',
      comment: '敌方非但不投降还胆敢还击!请求支援!'
    };
    pediaData.shenShape = {
      name: '申字形',
      color: '#88ddff',
      desc: '生命350,价值12,不可移动。每1秒瞄准玩家发射3发平行子弹,并向垂直方向两端各发射3发。每发伤害7。',
      comment: '“哥们,你这是什么形状?”“汉字形?”“预订象形文字”'
    };
  }

  // ══════════════════════════════════════════════════════════
  // ② 创建怪物(十二边形 / 申字形 / Z字形)
  // ══════════════════════════════════════════════════════════
  const _prevCreateMonster = createMonster;
  createMonster = function(type, x, y){
    if(type === 'shenShape'){
      return _newMonsterBase('shenShape', x, y, {
        radius: 0.7, hp: 350, maxHp: 350, speed: 0, damage: 0,
        color: '#88ddff', stroke: '#3399cc', value: 12,
        shootTimer: 1, isStationary: true
      });
    }
    if(type === 'zShape'){
      return _newMonsterBase('zShape', x, y, {
        radius: 0.6, hp: 70, maxHp: 70, speed: 4, damage: 0,
        color: '#ffdd88', stroke: '#ccaa33', value: 6,
        lifeTimer: 30, escaped: false
      });
    }
    const m = _prevCreateMonster(type, x, y);
    if(m && type === 'dodecagon'){
      m._baseHp = m.maxHp;
      m._baseDmg = m.damage;
      m._baseSpeed = m.speed;
      m._baseRadius = m.radius;
    }
    return m;
  };

  // ══════════════════════════════════════════════════════════
  // ③ 爱心:太阳不可被策反
  // ══════════════════════════════════════════════════════════
  updateHeartAlly = function(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') 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');
        }
      }
    }
  };

  // ══════════════════════════════════════════════════════════
  // ④ 十二边形:吸收修复
  // ══════════════════════════════════════════════════════════
  updateDodecagon = function(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);
  };

  // ══════════════════════════════════════════════════════════
  // ⑤ 申字形 / 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;
    const 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;
  }

  // ══════════════════════════════════════════════════════════
  // ⑥ 炮塔
  // ══════════════════════════════════════════════════════════
  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) return;
    if(!TURRET_ALLOWED.has(w.type)) return;
    if(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;
    const dmg = wd.damage * getPlayerDamageBoostMultiplier();
    const 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();
  }

  // 炮塔敌人接触伤害
  const _prevDamageAlly = damageAlly;
  damageAlly = function(a, dmg){
    if(a && a.isTurret) a._hitFlash = 0.1;
    _prevDamageAlly(a, dmg);
    if(a && !a.isSun && a.hp > 0 && a.invincibleTime !== undefined){
      a.invincibleTime = ALLY_INVINCIBLE_TIME;
    }
  };

  const _prevDamagePlayer = damagePlayer;
  damagePlayer = function(dmg){
    _prevDamagePlayer(dmg);
    if(player.hp > 0) player.invincibleTime = Math.min(player.invincibleTime, 0.5);
  };

  // ══════════════════════════════════════════════════════════
  // ⑦ updateAllies:炮塔独立 + 无敌帧递减
  // ══════════════════════════════════════════════════════════
  const _prevUpdateAllies = updateAllies;
  updateAllies = function(dt){
    // 无敌帧递减
    for(const a of allies){
      if(a.invincibleTime > 0) a.invincibleTime -= dt;
    }
    // 炮塔独立更新
    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);
    }
    // 摘出炮塔后交给原逻辑
    const turrets = [];
    for(let i = allies.length - 1; i >= 0; i--){
      if(allies[i].isTurret){ turrets.push(allies[i]); allies.splice(i, 1); }
    }
    _prevUpdateAllies(dt);
    for(const t of turrets){ if(t.hp > 0) allies.push(t); }
  };

  // ══════════════════════════════════════════════════════════
  // ⑧ updateMonsters:围殴独立 + 申Z逻辑
  // ══════════════════════════════════════════════════════════
  const _prevUpdateMonstersV37 = updateMonsters;
  updateMonsters = function(dt){
    // 围殴独立计算(盟友)
    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 = CONTACT_CD;
          a.invincibleTime = ALLY_INVINCIBLE_TIME;
          break;
        }
      }
    }
    // 屏蔽原逻辑对盟友的伤害(避免重复)
    const saved = [];
    for(const a of allies){ saved.push({a, t: a.invincibleTime}); a.invincibleTime = 999; }

    // 申Z独立更新
    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);
    }

    _prevUpdateMonstersV37(dt);
    for(const it of saved){ if(it.a.invincibleTime >= 999) it.a.invincibleTime = it.t; }
  };

  // ══════════════════════════════════════════════════════════
  // ⑨ Z字形额外奖励 +4
  // ══════════════════════════════════════════════════════════
  const _prevSpawnFragments = spawnFragments;
  spawnFragments = function(type, x, y, value){
    _prevSpawnFragments(type, x, y, value);
    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'});
      }
    }
  };

  // ══════════════════════════════════════════════════════════
  // ⑩ spawnWave:太阳生成 + 申Z投放 + Z预算惩罚
  // ══════════════════════════════════════════════════════════
  const _prevSpawnWaveV37 = spawnWave;
  spawnWave = function(){
    const wasSun = _sunNextWave;
    const bonus = window._nextWaveBudgetBonus;
    _sunNextWave = false;
    window._nextWaveBudgetBonus = 0;

    _prevSpawnWaveV37();

    // 太阳
    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);
      }
    }

    // 申Z投放
    if(waveCount !== 13){
      const target = waveCount <= 5 ? 5+(waveCount-1)*2
                   : waveCount <= 10 ? 13+(waveCount-5)*3
                   : waveCount <= 15 ? 28+(waveCount-10)*4
                   : 48+(waveCount-15)*5;

      if(waveCount >= 7){
        const c = Math.max(1, Math.floor(target / 25));
        for(let i = 0; i < c; i++){
          const p = getSpawnPos();
          const m = createMonster('shenShape', p.x, p.y);
          if(m) monsters.push(m);
        }
      }
      if(waveCount >= 5){
        const c = Math.max(1, Math.floor(target / 30));
        for(let i = 0; i < c; i++){
          const p = getSpawnPos();
          const m = createMonster('zShape', p.x, p.y);
          if(m) monsters.push(m);
        }
      }

      // Z预算惩罚
      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字形的代价:本波敌人更多!';
      }
    }
  };

  // ══════════════════════════════════════════════════════════
  // ⑪ update:字幕清除 + 电脑模式 WASD
  // ══════════════════════════════════════════════════════════
  let _lastNotice = '', _noticeTimer = 0;
  const keys = {};

  const _prevUpdateV37 = update;
  update = function(dt){
    // PC 模式 WASD
    if(window.pcMode && gameStarted && !menuOpen && !player.dead && !isVictory){
      let mx = 0, my = 0;
      if(keys['w']) my -= 1;
      if(keys['s']) my += 1;
      if(keys['a']) mx -= 1;
      if(keys['d']) mx += 1;
      const len = Math.hypot(mx, my);
      if(len > 0){
        moveJoystick.active = true;
        moveJoystick.dx = (mx/len) * moveJoystick.maxRadius;
        moveJoystick.dy = (my/len) * moveJoystick.maxRadius;
      } else if(!moveJoystick.touchId){
        moveJoystick.active = false;
        moveJoystick.dx = 0; moveJoystick.dy = 0;
      }
    }

    _prevUpdateV37(dt);

    // 字幕计时
    if(eventNotice !== _lastNotice){
      _lastNotice = eventNotice;
      _noticeTimer = eventNotice ? NOTICE_DURATION : 0;
    } else if(eventNotice){
      _noticeTimer -= dt;
      if(_noticeTimer <= 0){ eventNotice = ''; _lastNotice = ''; }
    }

    // 交互炮塔检测
    window.interactTurretIndex = -1;
    if(gameStarted && !menuOpen && !player.dead && !isVictory &&
       !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;
        }
      }
    }
  };

  // ══════════════════════════════════════════════════════════
  // ⑫ 绘制:申Z / 炮塔
  // ══════════════════════════════════════════════════════════
  function drawShenShape(sx, sy, size, fill, stroke){
    const half = size/2, boxHalf = half*0.5;
    ctx.save();
    ctx.fillStyle = fill; ctx.strokeStyle = 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();
  }
  function drawZShape(sx, sy, size, fill, stroke){
    const h = size/2;
    ctx.save();
    ctx.strokeStyle = 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();
  }

  const _prevDrawMonstersV37 = drawMonsters;
  drawMonsters = function(cx, cy){
    const specials = [];
    for(let i = monsters.length - 1; i >= 0; i--){
      const m = monsters[i];
      if(m.type === 'shenShape' || m.type === 'zShape'){
        specials.push(m); monsters.splice(i, 1);
      }
    }
    _prevDrawMonstersV37(cx, cy);
    for(const m of specials){
      const sx = (m.x-cx)*UNIT_PIXEL, sy = (m.y-cy)*UNIT_PIXEL, r = m.radius*UNIT_PIXEL;
      if(m.type === 'shenShape') drawShenShape(sx, sy, r*2, m.color, m.stroke);
      else drawZShape(sx, sy, r*1.6, m.color, m.stroke);

      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);

      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._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.3, 0, Math.PI*2); ctx.fill();
        ctx.restore();
      }
    }
    for(const m of specials) monsters.push(m);
  };

  const _prevDrawAlliesV37 = drawAllies;
  drawAllies = function(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); }
    }
    _prevDrawAlliesV37(cx, cy);
    for(const t of turrets) allies.push(t);

    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();
      }
    }
  };

  // ══════════════════════════════════════════════════════════
  // ⑬ UI:手机第三页 + 电脑模式按钮 + 操作提示
  // ══════════════════════════════════════════════════════════
  const PC_BTN = {x: 90, y: 10, w: 70, h: 20};

  const _prevGetPhoneRects = getPhoneButtonRects;
  getPhoneButtonRects = function(){
    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 _prevGetPhoneRects();
  };

  const _prevHandlePhoneTap = handlePhoneTap;
  handlePhoneTap = function(c){
    if(phonePage === 2){
      for(const r of getPhoneButtonRects()){
        if(isInsideRect(c.x, c.y, r)){
          if(r.action === 'airstrike') airStrike();
          if(r.action === 'sun') summonSun();
          if(r.action === 'turret') placeTurret();
          return;
        }
      }
      return;
    }
    _prevHandlePhoneTap(c);
  };

  const _prevDrawUI = drawUI;
  drawUI = function(){
    _prevDrawUI();

    if(phoneOpen && phonePage === 2){
      ctx.fillStyle = 'rgba(0,0,0,0.85)';
      ctx.fillRect(0, 100, CANVAS_SIZE, 180);
      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);
      const disc = getSupportDiscount();

      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*disc)}能量`, 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*disc)}能量(下一波召唤太阳)`, 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*disc)}能量`, 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(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);
        }
      }
    }

    // 电脑模式按钮
    if(!shopOpen && !labOpen && !phoneOpen && !pediaOpen && gameStarted && !menuOpen){
      ctx.save();
      ctx.fillStyle = window.pcMode ? '#66ff88' : 'rgba(255,255,255,0.3)';
      ctx.fillRect(PC_BTN.x, PC_BTN.y, PC_BTN.w, PC_BTN.h);
      ctx.strokeStyle = '#fff'; ctx.lineWidth = 1;
      ctx.strokeRect(PC_BTN.x+0.5, PC_BTN.y+0.5, PC_BTN.w-1, PC_BTN.h-1);
      ctx.fillStyle = '#000'; ctx.font = 'bold 11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(window.pcMode ? '电脑开' : '电脑关', PC_BTN.x + PC_BTN.w/2, PC_BTN.y+14);
      ctx.textAlign = 'left';
      ctx.restore();
    }

    // PC 操作提示
    if(window.pcMode && !shopOpen && !labOpen && !phoneOpen && !pediaOpen &&
       gameStarted && !menuOpen && !player.dead && !isVictory){
      ctx.save();
      ctx.fillStyle = 'rgba(0,0,0,0.45)';
      ctx.fillRect(CANVAS_SIZE-145, CANVAS_SIZE-175, 140, 95);
      ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1;
      ctx.strokeRect(CANVAS_SIZE-145, CANVAS_SIZE-175, 140, 95);
      ctx.fillStyle = '#ffdd66'; ctx.font = 'bold 10px sans-serif';
      ctx.fillText('🖥️ 电脑模式', CANVAS_SIZE-138, CANVAS_SIZE-160);
      ctx.fillStyle = '#fff'; ctx.font = '10px sans-serif';
      const lines = ['WASD 移动    鼠标 瞄准', 'R 冲刺       右键 装填', 'F/左键 开火', 'L 实验室  B 武器  K 支援'];
      for(let i = 0; i < lines.length; i++){
        ctx.fillText(lines[i], CANVAS_SIZE-138, CANVAS_SIZE-142 + i*14);
      }
      ctx.restore();
    }
  };

  // ══════════════════════════════════════════════════════════
  // ⑭ 输入事件
  // ══════════════════════════════════════════════════════════
  // 键盘
  window.addEventListener('keydown', e => {
    const k = e.key.toLowerCase();
    keys[k] = true;
    if(window.pcMode && ['w','a','s','d'].includes(k)) e.preventDefault();
    if(!window.pcMode) return;
    if(!gameStarted || menuOpen || player.dead || isVictory) return;
    if(shopOpen || labOpen || phoneOpen || pediaOpen) return;

    if(k === 'r') performDash();
    if(k === 'f' && manualMode){
      if(attackCooldown <= 0){
        const w = weapons[currentWeaponIndex];
        if(w.type === 'melee') performMelee();
        else if(['gun','kiteGun','shotgun','paraGun','sniper','lmg','hexGun','blazing'].includes(w.type)){
          if(w.currentMag > 0) performShoot(); else startReload();
        }
        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;
      }
    }
    if(k === 'l'){ labOpen = !labOpen; e.preventDefault(); }
    if(k === 'b'){ shopOpen = !shopOpen; e.preventDefault(); }
    if(k === 'k'){ phoneOpen = !phoneOpen; phonePage = 0; e.preventDefault(); }
  });
  window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });

  // 鼠标瞄准
  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);
  });

  // 右键装填
  canvas.addEventListener('contextmenu', e => {
    if(!window.pcMode) return;
    e.preventDefault();
    if(shopOpen || labOpen || phoneOpen || pediaOpen) return;
    if(!gameStarted || player.dead || isVictory) return;
    startReload();
  });

  // 左键开火
  canvas.addEventListener('mousedown', e => {
    if(!window.pcMode || e.button !== 0) return;
    if(shopOpen || labOpen || phoneOpen || pediaOpen) return;
    if(!gameStarted || player.dead || isVictory) return;
    if(manualMode && attackCooldown <= 0){
      const w = weapons[currentWeaponIndex];
      if(w.type === 'melee') performMelee();
      else if(['gun','kiteGun','shotgun','paraGun','sniper','lmg','hexGun','blazing'].includes(w.type)){
        if(w.currentMag > 0) performShoot(); else startReload();
      }
      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;
    }
  });

  // 触摸切换电脑模式 + 炮塔放入
  canvas.addEventListener('touchstart', e => {
    if(!gameStarted || menuOpen || player.dead || isVictory) return;
    if(shopOpen || labOpen || phoneOpen || pediaOpen) return;
    for(let i = 0; i < e.changedTouches.length; i++){
      const t = e.changedTouches[i];
      const c = getCanvasCoords(t.clientX, t.clientY);

      // 电脑模式按钮
      if(isInsideRect(c.x, c.y, PC_BTN)){
        window.pcMode = !window.pcMode;
        if(!window.pcMode){
          for(const k in keys) keys[k] = false;
          moveJoystick.active = false;
          moveJoystick.dx = 0; moveJoystick.dy = 0;
        }
        eventNotice = window.pcMode ? '🖥️ 电脑模式已开启' : '📱 触屏模式已开启';
        e.stopImmediatePropagation();
        return;
      }

      // 炮塔放入
      if(window.interactTurretIndex >= 0){
        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;
        }
      }
    }
  }, true);

  // ══════════════════════════════════════════════════════════
  // ⑮ 重置清空状态
  // ══════════════════════════════════════════════════════════
  const _prevResetGame = resetGame;
  resetGame = function(){
    window._nextWaveBudgetBonus = 0;
    window.interactTurretIndex = -1;
    _prevResetGame();
  };

  const _prevGoHome = goHome;
  goHome = function(){
    window._nextWaveBudgetBonus = 0;
    window.interactTurretIndex = -1;
    _prevGoHome();
  };

})();
// ==================== 补丁 V48:Z字形时长 / 小地图颜色 / 申字形不再传送 ====================
(function patchV48(){
  'use strict';

  // ── ① Z字形 30s → 45s ──
  const _prevCreateMonster = createMonster;
  createMonster = function(type, x, y){
    const m = _prevCreateMonster(type, x, y);
    if(m && type === 'zShape' && m.lifeTimer === 30){
      m.lifeTimer = 45;
    }
    return m;
  };

  // ── ② 申字形不再被判定为"卡住"并传送 ──
  const _prevTeleportUnstuck = teleportUnstuck;
  teleportUnstuck = function(e){
    if(e.isSun || e.isStationary || e.type === 'shenShape') return;
    _prevTeleportUnstuck(e);
  };

  // ── ③ 小地图颜色区分 ──
  const _prevDrawUI = drawUI;
  drawUI = function(){
    _prevDrawUI();

    // 只在战斗 HUD 有效时覆盖小地图
    if(!gameStarted || menuOpen || bossIntro) return;
    if(shopOpen || labOpen || phoneOpen || pediaOpen) return;

    const mapX = CANVAS_SIZE - 70;
    const mapY = 10;
    const mapW = 60;
    const mapH = 60;
    const scx = mapW / WORLD_SIZE;
    const scy = mapH / WORLD_SIZE;

    // 重绘整个小地图(覆盖原来的红点)
    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.lineWidth = 1;
    ctx.strokeRect(mapX, mapY, mapW, mapH);

    // 玩家
    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';      // Z字形:灰色
      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();
    }

    // Boss
    if(boss){
      ctx.fillStyle = '#cc99ff';
      ctx.beginPath();
      ctx.arc(mapX + boss.x * scx, mapY + boss.y * scy, 4, 0, Math.PI * 2);
      ctx.fill();
    }
  };

  // 图鉴描述同步更新
  if(typeof pediaData !== 'undefined' && pediaData.zShape){
    pediaData.zShape.desc = '生命70,价值6,主动远离玩家。45秒后若未被击杀便消失,使下一波预算+10。击杀后额外给予4能量。';
  }

})();
// ==================== 补丁 V50:申Z进池子 ====================
(function patchV50(){
  'use strict';

  const SHEN_MAX_PER_WAVE = 1;   // 每波最多 1 只申字
  const Z_MAX_PER_WAVE = 2;      // 每波最多 2 只 Z字

  const _prevSpawnWaveV50 = spawnWave;
  spawnWave = function(){
    _prevSpawnWaveV50();

    // 特殊波次 / Boss 波不处理(V49 已经清理了追加的申Z)
    if(waveCount === 13) return;
    if(massMutationWave || speedBattleWave || specialEvent || infiniteSurvivalMode) return;

    // 计算这一波目标预算(和原 spawnWave 里的公式一致)
    const target = waveCount <= 5 ? 5 + (waveCount-1)*2
                 : waveCount <= 10 ? 13 + (waveCount-5)*3
                 : waveCount <= 15 ? 28 + (waveCount-10)*4
                 : 48 + (waveCount-15)*5;

    // 决定要放几只(按预算比例,但有上限)
    let shenCount = 0, zCount = 0;
    if(waveCount >= 7) shenCount = Math.min(SHEN_MAX_PER_WAVE, Math.floor(target / 25));
    if(waveCount >= 5) zCount = Math.min(Z_MAX_PER_WAVE, Math.floor(target / 30));
    if(shenCount === 0 && zCount === 0) return;

    // 需要腾出的预算空间
    const needBudget = shenCount * 12 + zCount * 6;

    // 从当前 monsters 里移除等价值的"普通怪"(排除特殊怪/保底怪)
    const EXCLUDE = new Set([
      'heart','diamond','smallDiamond','solidQuad','dodecagon','sun',
      'shenShape','zShape','boss','arrow','pentagon','hexagon','kite',
      'octagon','crescent','octStar','spiral','hexStar'
    ]);
    // 优先移除普通怪,按价值从高到低
    const removable = [];
    for(const m of monsters){
      if(EXCLUDE.has(m.type)) continue;
      if(m.isAlly) continue;
      if(!m.value || m.value <= 0) continue;
      removable.push(m);
    }
    removable.sort((a,b) => b.value - a.value);

    let removedBudget = 0;
    for(const m of removable){
      if(removedBudget >= needBudget) break;
      const idx = monsters.indexOf(m);
      if(idx >= 0){
        monsters.splice(idx, 1);
        removedBudget += m.value;
      }
    }

    // 如果腾出的空间不够,就按比例减少投放
    let actualBudget = Math.min(needBudget, removedBudget);
    let shenLeft = shenCount, zLeft = zCount;
    while(actualBudget >= 6 && (shenLeft > 0 || zLeft > 0)){
      // 优先放价值高的(申字),再放 Z 字
      if(shenLeft > 0 && actualBudget >= 12){
        const p = getSpawnPos();
        const m = createMonster('shenShape', p.x, p.y);
        if(m){ monsters.push(m); spawnedThisWave.push('shenShape'); }
        actualBudget -= 12;
        shenLeft--;
      } else if(zLeft > 0 && actualBudget >= 6){
        const p = getSpawnPos();
        const m = createMonster('zShape', p.x, p.y);
        if(m){ monsters.push(m); spawnedThisWave.push('zShape'); }
        actualBudget -= 6;
        zLeft--;
      } else break;
    }
  };

})();

</script>
</body>
</html>

Game Source: 几何特工 - V37

Creator: SilverOtter29

Libraries: none

Complexity: complex (5321 lines, 237.2 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: v37-silverotter29-mucpbvug" to link back to the original. Then publish at arcadelab.ai/publish.