几何特工 - V37
by SilverOtter295321 lines237.2 KB
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>几何特工 - 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&¤tValue<poolBudget;i++){
let t=pool[i],v=getValue(t);
if(currentValue+v<=poolBudget){
let p=getSpawnPos(),m=createMonster(t,p.x,p.y);
if(m){monsters.push(m);spawnedThisWave.push(t);currentValue+=v;}
}
}
while(currentValue<poolBudget){
let p=getSpawnPos(),m=createMonster('square',p.x,p.y);
if(m){monsters.push(m);spawnedThisWave.push('square');currentValue++;}
}
generateStructures();
// 斩草除根
if(waveCount!==13&&waveCount>=5&&!massMutationWave&&!speedBattleWave&&!specialEvent&&Math.random()<0.10){
monsters=[];
boss=createExterminateBoss();
_exterminateMode=true;
eventNotice='⚠️ 斩草除根!独自面对Boss!';
}
}
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.