- #wrap 去除外框限制,竖屏/方形屏占满全屏(逻辑分辨率动态:宽240、高按屏比 clamp[280,640]) - 横屏 letterbox:中间 3:4 竖屏显示区域居中,UI 收紧压缩 - viewport-fit=cover + safe-area-inset 适配刘海屏/底部横条 - 星星背景按视口面积重建,保持星空密度 - resize/orientationchange 自动重新适配,对局中旋转位置自动修正
636 lines
35 KiB
JavaScript
636 lines
35 KiB
JavaScript
"use strict";
|
||
/* ================= 全局状态 ================= */
|
||
const cv=$("game"),ctx=cv.getContext("2d");
|
||
ctx.imageSmoothingEnabled=false;
|
||
let W=240,H=320;
|
||
/* 动态视口适配(手机/折叠屏/平板):
|
||
竖屏/方形(高>=宽):占满全屏——宽固定 240,高按屏幕比例动态(限幅 [280,640])
|
||
横屏(高<宽):保持标准竖屏 3:4 逻辑分辨率(240x320),CSS 层 letterbox 居中显示
|
||
逻辑分辨率随屏变化,canvas 拉伸铺满游戏区域,像素不变形;所有实体坐标均为相对 W/H,安全。 */
|
||
function fitViewport(){
|
||
const vw=Math.max(1,window.innerWidth), vh=Math.max(1,window.innerHeight);
|
||
if(vh>=vw){
|
||
W=240; H=clamp(Math.round(240*vh/vw),280,640);
|
||
}else{
|
||
W=240; H=320;
|
||
}
|
||
cv.width=W; cv.height=H;
|
||
if(typeof initStars==="function")initStars();
|
||
}
|
||
let G=null,UI="title";
|
||
function newRun(mode,shipKey,diff){
|
||
const ship=SHIPS[shipKey],m=ship.mod,meta=SAVE.m;
|
||
G={mode,diff,ship:shipKey,stage:1,wave:0,waveT:0,betweenT:60,spawning:[],
|
||
score:0,coins:0,kills:0,combo:0,comboT:0,gems:0,level:1,xp:0,xpNeed:BAL.xpNeed(1),
|
||
luck:(meta.luck||0),mutator:null,mutList:[],
|
||
time:0,boss:null,bossN:0,bossKills:0,shake:0,hitstop:0,flash:0,slowmo:0,
|
||
weapons:{vulcan:1},loadout:["vulcan"],subs:{},subLoadout:[],drones:{},droneLoadout:[],
|
||
wt:{},wst:{},sst:{},dst:{},zones:[],zaps:[],novas:[],flares:[],holies:[],shocks:[],bbeams:[],funnels:[],
|
||
u:{fire:0,crit:0,shield:0,nano:0,mag:0,bounty:0,odcore:0,ghost:0},
|
||
enemies:[],pb:[],eb:[],parts:[],picks:[],texts:[],lasers:[],
|
||
banner:null,bannerT:0,odActive:0,
|
||
p:{x:W/2,y:H-46,orbA:0,satA:0,inv:120,focus:false,dead:0}
|
||
};
|
||
if(m.drone){G.drones.gunner=1;G.droneLoadout=["gunner"];} // 女武神自带僚机
|
||
// 机体与局外强化
|
||
const b=BAL.player;
|
||
G.stat={
|
||
spd:b.speed*m.spd, dmgMul:m.dmg,
|
||
maxLives:b.lives+m.lives+(meta.armor||0),
|
||
critC:b.critC+m.crit, dodge:m.dodge, hitR:b.hitR+m.hitR,
|
||
magR:b.magnetR*m.mag*(1+0.15*(meta.mag2||0)), coinMul:1+0.15*(meta.wallet||0),
|
||
shieldMax:m.shield, droneBoost:m.droneBoost, odStart:meta.odstart?50:0
|
||
};
|
||
if(meta.weapon)G.weapons.vulcan=1+meta.weapon; // 机库:初始主武器强化
|
||
G.lives=G.stat.maxLives;
|
||
G.maxHp=BAL.player.hp+(m.hp||0)+15*(meta.hpup||0); G.hp=G.maxHp;
|
||
G.shield=G.stat.shieldMax; G.shieldT=0; G.od=G.stat.odStart;
|
||
applyShipMeta();
|
||
return G;
|
||
}
|
||
function applyShipMeta(){
|
||
// 局外强化对主武器等级的加成由 newRun 处理,这里预留
|
||
}
|
||
/* 派生数值 */
|
||
function pDmgMul(){let d=G.stat.dmgMul*(G.odActive>0?1.25:1);return d;}
|
||
function pFireMul(){let f=1+0.12*G.u.fire; if(G.odActive>0)f*=1.7; return f;}
|
||
function pSpd(){return G.stat.spd*(G.p.focus?BAL.player.focusMul:1)*(G.odActive>0?1.2:1);}
|
||
function pCrit(){return G.stat.critC+0.08*G.u.crit;}
|
||
function pDodge(){return Math.min(0.4,G.stat.dodge+0.1*G.u.ghost);}
|
||
function pMagnet(){return G.stat.magR*(1+0.6*G.u.mag);}
|
||
function pCoinMul(){let c=G.stat.coinMul*(1+0.3*G.u.bounty); if(G.diff===1)c*=BAL.hard.coin; if(G.mutator==="GOLD")c*=2; return c;}
|
||
/* ================= 输入 ================= */
|
||
const KEYS={};
|
||
const GAMEKEYS=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," ","KeyW","KeyA","KeyS","KeyD","KeyX","KeyC","KeyZ","Shift","KeyP","Escape","KeyM","Digit1","Digit2","Digit3"];
|
||
addEventListener("keydown",e=>{
|
||
if(e.target&&(e.target.tagName==="INPUT"||e.target.tagName==="TEXTAREA"))return; // 登陆/注册表单打字不拦截
|
||
if(GAMEKEYS.includes(e.code))e.preventDefault();
|
||
KEYS[e.code]=true;
|
||
AU.init(); if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
|
||
if(e.code==="KeyM"){SAVE.music=!SAVE.music;AU.applyMute();persist();}
|
||
if(e.code==="Escape"&&UI==="demoSim"){exitDemoSim();return;}
|
||
if(e.code==="KeyP"||e.code==="Escape")togglePause();
|
||
if(UI==="cards"&&["Digit1","Digit2","Digit3"].includes(e.code))pickCard(+e.code.slice(-1)-1);
|
||
});
|
||
addEventListener("keyup",e=>KEYS[e.code]=false);
|
||
/* 触屏拖动 */
|
||
let touchId=null,tx=0,ty=0,tox=0,toy=0;
|
||
cv.addEventListener("pointerdown",e=>{
|
||
AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
|
||
if(touchId!==null)return; touchId=e.pointerId;
|
||
const r=cv.getBoundingClientRect();
|
||
tx=(e.clientX-r.left)/r.width*W; ty=(e.clientY-r.top)/r.height*H;
|
||
if(G&&UI==="play"){tox=G.p.x-tx;toy=G.p.y-ty-14;}
|
||
});
|
||
cv.addEventListener("pointermove",e=>{
|
||
if(e.pointerId!==touchId)return;
|
||
const r=cv.getBoundingClientRect();
|
||
tx=(e.clientX-r.left)/r.width*W; ty=(e.clientY-r.top)/r.height*H;
|
||
});
|
||
const endTouch=e=>{if(e.pointerId===touchId)touchId=null;};
|
||
cv.addEventListener("pointerup",endTouch);cv.addEventListener("pointercancel",endTouch);
|
||
|
||
/* ================= 玩家:移动与武器 ================= */
|
||
function updatePlayer(){
|
||
const p=G.p;
|
||
if(p.dead>0){if(G.lives<=0)return;p.dead--;if(p.dead===0){p.x=W/2;p.y=H-46;p.inv=BAL.reviveInv;G.hp=G.maxHp;}return;}
|
||
p.focus=!!KEYS["Shift"];
|
||
let dx=0,dy=0;
|
||
if(KEYS["ArrowLeft"]||KEYS["KeyA"])dx--;
|
||
if(KEYS["ArrowRight"]||KEYS["KeyD"])dx++;
|
||
if(KEYS["ArrowUp"]||KEYS["KeyW"])dy--;
|
||
if(KEYS["ArrowDown"]||KEYS["KeyS"])dy++;
|
||
const sp=pSpd();
|
||
if(touchId!==null){
|
||
const gx=clamp(tx+tox,8,W-8),gy=clamp(ty+toy,20,H-14);
|
||
p.x=lerp(p.x,gx,0.4); p.y=lerp(p.y,gy,0.4);
|
||
}else{
|
||
if(dx&&dy){dx*=0.707;dy*=0.707;}
|
||
p.x=clamp(p.x+dx*sp,8,W-8); p.y=clamp(p.y+dy*sp,20,H-14);
|
||
if(G.demoSim&&!dx&&!dy){ // 自动驾驶:缓慢追踪最近敌人,玩家输入即接管
|
||
const apt=nearestEnemy(p.x,p.y,400);
|
||
const ax=apt?apt.x:W/2+Math.sin(G.time*0.008)*70;
|
||
p.x=lerp(p.x,clamp(ax,16,W-16),0.028);
|
||
p.y=lerp(p.y,H-64+Math.sin(G.time*0.02)*8,0.04);
|
||
}
|
||
}
|
||
if(p.inv>0)p.inv--;
|
||
if(G.odActive>0){G.odActive--; if(G.odActive===0)G.od=0;}
|
||
// 暴走:超频槽充满自动触发(被动)
|
||
if(G.od>=100&&G.odActive<=0&&p.dead<=0){
|
||
G.odActive=(6+G.u.odcore)*60;SFX.od();
|
||
banner("暴走 OVERDRIVE!","#ff9a3d");
|
||
}
|
||
// 护盾回复
|
||
if(G.u.shield>0&&G.shield<shieldCap()){
|
||
G.shieldT++; const need=(46-10*(G.u.shield-1))*60;
|
||
if(G.shieldT>need){G.shield++;G.shieldT=0;SFX.pick();}
|
||
}
|
||
fireWeapons();
|
||
}
|
||
function pbul(x,y,vx,vy,dmg,o){
|
||
if(!isFinite(x)||!isFinite(y)||!isFinite(vx)||!isFinite(vy))return; // NaN 防护
|
||
G.pb.push(Object.assign({x,y,vx,vy,dmg,r:3,t:"v",life:400,pierce:false,hit:null},o||{}));
|
||
}
|
||
/* ================= 武器引擎 ================= */
|
||
function hurtT(t,dmg,crit){if(G.boss&&t===G.boss)hurtBoss(dmg);else hurtEnemy(t,dmg,crit);}
|
||
function nearEnemies(x,y,r,n){
|
||
const rr=r*r,c=[];
|
||
for(const e of G.enemies)if(!e.dead)c.push(e);
|
||
if(G.boss)c.push(G.boss);
|
||
c.sort((a,b)=>dist2(x,y,a.x,a.y)-dist2(x,y,b.x,b.y));
|
||
const out=[];
|
||
for(const e of c){if(out.length>=n)break;if(dist2(x,y,e.x,e.y)<rr)out.push(e);}
|
||
return out;
|
||
}
|
||
function zapVisual(x1,y1,x2,y2,col){
|
||
const pts=[[x1,y1]],seg=4;
|
||
for(let i=1;i<seg;i++){const t2=i/seg;pts.push([lerp(x1,x2,t2)+rnd(-5,5),lerp(y1,y2,t2)+rnd(-5,5)]);}
|
||
pts.push([x2,y2]);
|
||
G.zaps.push({pts,life:7,col});
|
||
}
|
||
function areaDamage(x,y,r,dmg){
|
||
for(const e of G.enemies)if(!e.dead&&dist2(x,y,e.x,e.y)<(r+e.r)*(r+e.r))hurtEnemy(e,dmg,true);
|
||
if(G.boss&&bossHit(x,y,r))hurtBoss(dmg);
|
||
}
|
||
function crossBeams(x,y,len,dmg,rot){ // 圣光十字:只结算伤害,绘制在 render 中(支持旋转)
|
||
rot=rot||0;
|
||
for(let k=0;k<4;k++){
|
||
const ca=rot+k*Math.PI/2,dx=Math.cos(ca),dy=Math.sin(ca);
|
||
for(const e of G.enemies){if(e.dead)continue;
|
||
const t2=clamp((e.x-x)*dx+(e.y-y)*dy,0,len);
|
||
if(dist2(e.x,e.y,x+dx*t2,y+dy*t2)<(e.r+3)*(e.r+3))hurtEnemy(e,dmg,true);}
|
||
if(G.boss){const t2=clamp((G.boss.x-x)*dx+(G.boss.y-y)*dy,0,len);
|
||
if(bossHit(x+dx*t2,y+dy*t2,3))hurtBoss(dmg);}
|
||
}
|
||
}
|
||
/* ================= 差异化爆炸特效 =================
|
||
每种伤害类型:唯一主色+形状语言+动画节奏(参考原神/明日方舟/Cave系) */
|
||
function explodeFx(x,y,type,s){
|
||
s=s||1;
|
||
switch(type){
|
||
case "fire": // 火焰:白闪→火舌上卷→余烬飘落
|
||
G.novas.push({x,y,r:2,maxr:9*s,life:7,col:"#fff4d9"});
|
||
for(let i=0;i<10;i++)G.parts.push({x:x+rnd(-4,4),y:y+rnd(-3,3),vx:rnd(-0.9,0.9),vy:rnd(-1.8,-0.4),life:rndi(14,26),col:pick(["#ff9a3d","#ff6b3d","#ffd23d"]),sz:rndi(1,3),grav:0.05});
|
||
for(let i=0;i<4;i++)G.parts.push({x,y,vx:rnd(-0.4,0.4),vy:rnd(-0.9,-0.3),life:rndi(22,36),col:"#5a5a6a",sz:2});
|
||
break;
|
||
case "ice": // 冰霜:内吸→冰刺放射→碎片缓落
|
||
for(let i=0;i<6;i++)G.parts.push({x:x+rnd(-15,15),y:y+rnd(-15,15),vx:0,vy:0,life:9,col:"#cfeaff",sz:1,suck:{x,y}});
|
||
for(let i=0;i<8;i++){const a=i/8*TAU+rnd(-0.2,0.2);
|
||
G.parts.push({x,y,vx:Math.cos(a)*2.4,vy:Math.sin(a)*2.4,life:rndi(10,18),col:pick(["#7fd8ff","#ffffff"]),sz:2,spark:true});}
|
||
for(let i=0;i<4;i++)G.parts.push({x,y,vx:rnd(-0.8,0.8),vy:rnd(-0.4,0.4),life:rndi(18,30),col:"#7fd8ff",sz:1,grav:0.03});
|
||
break;
|
||
case "elec": // 雷电:闪白→分叉电弧→残留闪烁
|
||
G.novas.push({x,y,r:2,maxr:11*s,life:6,col:"#ffffff"});
|
||
for(let i=0;i<3;i++){const a=rnd(0,TAU);zapVisual(x,y,x+Math.cos(a)*rnd(10,20),y+Math.sin(a)*rnd(10,20),pick(["#ffe95c","#43e8ff"]));}
|
||
break;
|
||
case "boom": // 爆炸:白闪→同心冲击环→碎片+灰烟
|
||
G.novas.push({x,y,r:2,maxr:8*s,life:5,col:"#ffffff"});
|
||
G.novas.push({x,y,r:4,maxr:22*s,life:14,col:"#ff9a3d"});
|
||
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-2.4,2.4),vy:rnd(-2.4,2.4),life:rndi(9,16),col:pick(["#ff9a3d","#ff5566","#fff"]),sz:rndi(1,2),spark:i%2===0});
|
||
for(let i=0;i<4;i++)G.parts.push({x,y,vx:rnd(-0.5,0.5),vy:rnd(-0.9,-0.2),life:rndi(20,34),col:"#5a5a6a",sz:2});
|
||
break;
|
||
case "plasma": // 能量/电浆:青绿光球+电弧
|
||
G.novas.push({x,y,r:3,maxr:18*s,life:12,col:"#43e8ff"});
|
||
for(let i=0;i<10;i++)G.parts.push({x,y,vx:rnd(-1.7,1.7),vy:rnd(-1.7,1.7),life:rndi(12,24),col:pick(["#43e8ff","#5cff9d","#fff"]),sz:rndi(1,2)});
|
||
zapVisual(x,y,x+rnd(-14,14),y+rnd(-14,14),"#43e8ff");
|
||
break;
|
||
case "dark": // 奇点坍缩:万芒内吸→中心炽闪→三重爆环→紫电撕裂→星芒外喷
|
||
for(let i=0;i<26;i++)G.parts.push({x:x+rnd(-44,44),y:y+rnd(-44,44),vx:0,vy:0,life:rndi(8,16),col:pick(["#b05ce8","#7b2fd0","#e0b3ff"]),sz:rndi(1,2),suck:{x,y}});
|
||
G.novas.push({x,y,r:2,maxr:15*s,life:7,col:"#ffffff"});
|
||
G.novas.push({x,y,r:3,maxr:40*s,life:16,col:"#b05ce8"});
|
||
G.novas.push({x,y,r:6,maxr:62*s,life:26,col:"#7b2fd0"});
|
||
for(let i=0;i<6;i++){const a=i/6*TAU+rnd(-0.3,0.3);
|
||
zapVisual(x,y,x+Math.cos(a)*rnd(24,40)*s,y+Math.sin(a)*rnd(24,40)*s,pick(["#b05ce8","#e0b3ff"]));}
|
||
for(let i=0;i<12;i++){const a=rnd(0,TAU);
|
||
G.parts.push({x,y,vx:Math.cos(a)*rnd(1.5,3.4),vy:Math.sin(a)*rnd(1.5,3.4),life:rndi(10,20),col:pick(["#fff","#e0b3ff","#b05ce8"]),sz:1,spark:true});}
|
||
break;
|
||
case "airburst": // 空爆:无冲击环,火花四溅+灰烟翻滚(去圆形区域)
|
||
for(let i=0;i<7;i++)G.parts.push({x,y,vx:rnd(-2.2,2.2),vy:rnd(-2.2,2.2),life:rndi(8,14),col:pick(["#ff9a3d","#ffd23d","#fff"]),sz:rndi(1,2),spark:i%2===0});
|
||
for(let i=0;i<3;i++)G.parts.push({x,y,vx:rnd(-0.5,0.5),vy:rnd(-0.8,-0.2),life:rndi(16,26),col:"#5a5a6a",sz:2});
|
||
break;
|
||
case "executor": // 斩首巡航弹:白炽闪→三重冲击环→火舌上卷→钢片四溅→浓烟
|
||
G.novas.push({x,y,r:3,maxr:15,life:6,col:"#ffffff"});
|
||
G.novas.push({x,y,r:4,maxr:34,life:15,col:"#ff9a3d"});
|
||
G.novas.push({x,y,r:7,maxr:54,life:24,col:"#ff5566"});
|
||
for(let i=0;i<14;i++){const a=rnd(0,TAU);
|
||
G.parts.push({x,y,vx:Math.cos(a)*rnd(1.8,3.6),vy:Math.sin(a)*rnd(1.8,3.6),life:rndi(10,18),col:pick(["#ffd23d","#ff9a3d","#fff"]),sz:rndi(1,2),spark:true});}
|
||
for(let i=0;i<8;i++)G.parts.push({x:x+rnd(-4,4),y,vx:rnd(-1.2,1.2),vy:rnd(-2.4,-0.8),life:rndi(16,28),col:pick(["#ff9a3d","#ff6b3d"]),sz:2,grav:0.04});
|
||
for(let i=0;i<6;i++)G.parts.push({x,y,vx:rnd(-0.7,0.7),vy:rnd(-1,-0.3),life:rndi(24,40),col:"#5a5a6a",sz:2});
|
||
break;
|
||
case "holy": // 神圣:金色光环+光粒上飘
|
||
G.novas.push({x,y,r:3,maxr:24*s,life:16,col:"#ffcf5c"});
|
||
for(let i=0;i<9;i++)G.parts.push({x:x+rnd(-6,6),y:y+rnd(-6,6),vx:rnd(-0.4,0.4),vy:rnd(-1,-0.2),life:rndi(18,32),col:pick(["#ffcf5c","#fff4d9","#fff"]),sz:1});
|
||
break;
|
||
case "poison": // 毒素:绿泡破裂+黏液溅射
|
||
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-1,1),vy:rnd(-1.5,-0.2),life:rndi(14,26),col:pick(["#9dff5c","#5cff9d"]),sz:rndi(1,3),grav:0.06});
|
||
break;
|
||
case "kinetic": // 动能:金属碎片+火花
|
||
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-2.6,2.6),vy:rnd(-2.6,2.6),life:rndi(7,14),col:pick(["#c8d8ff","#8c9ab8","#fff"]),sz:1,spark:true});
|
||
break;
|
||
case "dawnburst": // 黎明重炮:白炽核→四重热环→爆芯闪光→火星条→烟尘→紫烬
|
||
G.novas.push({x,y,r:3,maxr:16*s,life:6,col:"#ffffff"});
|
||
G.novas.push({x,y,r:5,maxr:38*s,life:15,col:"#ffc64a"});
|
||
G.novas.push({x,y,r:8,maxr:56*s,life:24,col:"#ff4a1f"});
|
||
G.novas.push({x,y,r:10,maxr:70*s,life:32,col:"#7a0e0e"});
|
||
for(let i=0;i<40;i++){const a=rnd(0,TAU),sp=rnd(1.5,4);
|
||
G.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:rndi(18,35),col:pick(["#fff","#fff3d0","#ffc64a","#ff8a2e"]),sz:rndi(1,2),spark:i%3===0});}
|
||
for(let i=0;i<18;i++){const a2=rnd(0,TAU),sp2=rnd(2,5);
|
||
G.parts.push({x,y,vx:Math.cos(a2)*sp2,vy:Math.sin(a2)*sp2,life:rndi(20,40),col:"#ff8a2e",sz:1,spark:true});}
|
||
for(let i=0;i<10;i++)G.parts.push({x:x+rnd(-6,6),y,vx:rnd(-0.6,0.6),vy:rnd(-1.1,-0.4),life:rndi(50,80),col:"#6a4636",sz:2});
|
||
for(let i=0;i<6;i++)G.parts.push({x,y,vx:rnd(-0.4,0.4),vy:rnd(-0.8,-0.3),life:rndi(40,70),col:"#c46bff",sz:1});
|
||
break;
|
||
case "protocol": // 协议重炮:白炽核→三层红黑橙环→橙红径向粒子→黑烟余烬→橙电
|
||
G.novas.push({x,y,r:3,maxr:18*s,life:6,col:"#ffffff"});
|
||
G.novas.push({x,y,r:5,maxr:34*s,life:14,col:"#ffd54a"});
|
||
G.novas.push({x,y,r:8,maxr:52*s,life:22,col:"#ff4a1f"});
|
||
for(let i=0;i<30;i++){const a=rnd(0,TAU),sp=rnd(1.6,4.2);
|
||
G.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:rndi(14,28),col:pick(["#fff","#ffd54a","#ff5a2e","#26262e"]),sz:rndi(1,2),spark:i%2===0});}
|
||
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-1,1),vy:rnd(-1.6,-0.5),life:rndi(20,34),col:pick(["#26262e","#ff8a2e"]),sz:2,grav:0.05});
|
||
for(let i=0;i<4;i++){const a3=rnd(0,TAU);zapVisual(x,y,x+Math.cos(a3)*rnd(18,30),y+Math.sin(a3)*rnd(18,30),"#ff8a2e");}
|
||
break;
|
||
case "nebulafx": // 星云绽放:紫蓝双环 + 漩涡星尘
|
||
G.novas.push({x,y,r:3,maxr:26*s,life:12,col:"#b98cff"});
|
||
G.novas.push({x,y,r:6,maxr:40*s,life:20,col:"#5b2bff"});
|
||
for(let i=0;i<14;i++)G.parts.push({x,y,vx:rnd(-1.6,1.6),vy:rnd(-1.6,1.6),life:rndi(14,26),col:pick(["#b98cff","#e0b3ff","#7ce9ff","#fff"]),sz:1,spark:i%2===0});
|
||
break;
|
||
}
|
||
}
|
||
function explodeBullet(b){
|
||
if(b.t==="am"){ // 空爆弹直击:无圈爆炸
|
||
areaDamage(b.x,b.y,b.zone||24,b.dmg);
|
||
explodeFx(b.x,b.y,"airburst",1.3);
|
||
SFX.boom(false);G.shake=Math.max(G.shake,3);
|
||
}else if(b.t==="ex"){ // 斩首巡航弹:华丽殉爆
|
||
areaDamage(b.x,b.y,b.aoe||26,b.dmg);
|
||
explodeFx(b.x,b.y,"executor",1);
|
||
SFX.boom(true);G.shake=Math.max(G.shake,8);
|
||
}else if(b.t==="lm"){ // 巡飞弹:原地火场(kind:fire)+ 分裂4枚小炸弹很快起爆
|
||
areaDamage(b.x,b.y,b.zone||20,b.dmg);
|
||
G.zones.push({x:b.x,y:b.y,r:b.zone||20,dps:b.dmg*0.12,life:b.fire||95,col:"#ff8a2e",kind:"fire"});
|
||
for(let i=0;i<4;i++){const la=-Math.PI/2+(i-1.5)*0.85+rnd(-0.2,0.2);
|
||
pbul(b.x,b.y,Math.cos(la)*2.8,Math.sin(la)*2.8,b.dmg*0.35,{t:"lb",r:2,bt:12+rndi(0,6),aoe:12,trailCol:"#ff9a3d"});}
|
||
explodeFx(b.x,b.y,"fire",1.2);
|
||
SFX.boom(false);G.shake=Math.max(G.shake,3);
|
||
}else if(b.t==="lb"){ // 巡飞弹小炸弹:小范围快爆
|
||
areaDamage(b.x,b.y,b.aoe||12,b.dmg);
|
||
explodeFx(b.x,b.y,"fire",0.6);SFX.boom(false);
|
||
}else if(b.t==="tb"||b.t==="tb2"){
|
||
areaDamage(b.x,b.y,b.aoe||b.zone||24,b.dmg);
|
||
explodeFx(b.x,b.y,"boom",1);
|
||
SFX.boom(false);G.shake=Math.max(G.shake,3);
|
||
}else if(b.t==="mt"){ // 流星火雨:火焰爆炸(修复无伤害bug)
|
||
areaDamage(b.x,b.y,b.aoe||16,b.dmg);
|
||
explodeFx(b.x,b.y,"fire",1);G.shake=Math.max(G.shake,2);SFX.boom(false);
|
||
}else if(b.t==="bb"){
|
||
areaDamage(b.x,b.y,b.aoe,b.dmg);
|
||
explodeFx(b.x,b.y,"plasma",1);SFX.boom(false);
|
||
}else if(b.t==="sg"){
|
||
areaDamage(b.x,b.y,b.aoe,b.dmg);
|
||
explodeFx(b.x,b.y,"dark",1.6);
|
||
G.shake=Math.max(G.shake,8);SFX.boom(true);
|
||
}else if(b.t==="ho"){ // 圣光弹:留下旋转生长的圣光十字
|
||
G.holies.push({x:b.x,y:b.y,len:b.len,dmg:b.dmg,t:0,life:36});
|
||
explodeFx(b.x,b.y,"holy",0.7);SFX.level();}
|
||
else if(b.t==="dw"){ // 黎明重炮:停帧+白闪+巨爆+余震
|
||
areaDamage(b.x,b.y,b.aoe||30,b.dmg);
|
||
explodeFx(b.x,b.y,"dawnburst",1);
|
||
G.hitstop=5;G.shake=Math.max(G.shake,10);
|
||
SFX.boom(true);
|
||
G.shocks.push({x:b.x,y:b.y,t:22,dmg:b.dmg*0.5,r:(b.aoe||30)*0.7});
|
||
}else if(b.t==="ph"){ // 协议重炮:青蓝巨爆
|
||
areaDamage(b.x,b.y,b.aoe||26,b.dmg);
|
||
explodeFx(b.x,b.y,"protocol",1);
|
||
G.shake=Math.max(G.shake,7);SFX.boom(true);
|
||
}else if(b.t==="nb"){ // 星云奥秘:绽放群星(星星弹也有伤害)
|
||
const ns=b.stars||10;
|
||
for(let i=0;i<ns;i++){const a=i/ns*TAU+rnd(-0.15,0.15);
|
||
pbul(b.x,b.y,Math.cos(a)*2.6,Math.sin(a)*2.6,b.dmg*0.3,{t:"st",spr:"star4",r:2,life:44});}
|
||
explodeFx(b.x,b.y,"nebulafx",1);SFX.boom(false);
|
||
}
|
||
}
|
||
function aimShot(x,y,dmg,spd,spr,slow){
|
||
const t=nearestEnemy(x,y,280);let vx=0,vy=-(spd||5);
|
||
if(t){const d=Math.hypot(t.x-x,t.y-y)||1;vx=(t.x-x)/d*(spd||5);vy=(t.y-y)/d*(spd||5);}
|
||
pbul(x,y,vx,vy,dmg,{r:2,spr,slow});
|
||
}
|
||
function fireWeapons(){
|
||
if(G.boss&&G.boss.transT>0)return; // Boss变形转场:全员停火
|
||
const p=G.p,fm=pFireMul(),dm=pDmgMul(),wdm=dm*wMul();
|
||
/* ---------- 主武器 ---------- */
|
||
for(const wk of G.loadout){
|
||
const w=WEAPONS[wk],st=w.lvl(G.weapons[wk]),S=G.wst[wk]=G.wst[wk]||{};
|
||
G.wt[wk]=(G.wt[wk]||0)-1;
|
||
switch(w.beh){
|
||
case "streams":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(2,st.int/fm);
|
||
for(let i=0;i<st.n;i++){const off=i-(st.n-1)/2;
|
||
if(st.skipMid&&Math.abs(off)<=st.skipMid)continue; // 超级霰弹:去掉中央弹丸
|
||
const a=-Math.PI/2+off*(st.fan?st.fan*2/Math.max(1,st.n-1):0.09);
|
||
pbul(p.x+off*(st.gap||6),p.y-8,Math.cos(a)*st.spd,Math.sin(a)*st.spd,st.dmg*wdm,{r:3,spr:w.spr,poison:st.poison,slow:st.slow});}
|
||
SFX.shoot();}
|
||
break;
|
||
case "flame": // 周期性宽幅喷吐:喷射 burst 帧 → 停歇 cd 帧
|
||
if(S.on>0){S.on--;
|
||
for(let i=0;i<st.n;i++)pbul(p.x+rnd(-5,5),p.y-10,rnd(-2.1,2.1),-rnd(2.4,3.4),st.dmg*wdm,{t:"f",life:Math.round(st.range/3),pierce:true,r:3});
|
||
}else{S.t=(S.t||0)-1;if(S.t<=0){S.on=st.burst;S.t=st.cd;SFX.shoot();}}
|
||
break;
|
||
case "tesla":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(10,st.int/fm);
|
||
const tgts=nearEnemies(p.x,p.y,st.range,st.n);
|
||
for(const t of tgts){zapVisual(p.x,p.y-6,t.x,t.y,"#ffe95c");hurtT(t,st.dmg*wdm,true);}
|
||
if(tgts.length)SFX.graze();}
|
||
break;
|
||
case "rail":
|
||
S.c=(S.c||0)+1;
|
||
if(S.c>st.charge*0.35&&G.time%3===0){const a0=rnd(0,TAU),r0=rnd(10,18);
|
||
G.parts.push({x:p.x+Math.cos(a0)*r0,y:p.y-10+Math.sin(a0)*r0,vx:0,vy:0,life:14,col:pick(["#43e8ff","#b98cff","#fff"]),sz:1,suck:{x:p.x,y:p.y-10}});}
|
||
if(S.c>=st.charge){S.c=0;
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"rg",r:5,pierce:true,zone:st.zone,trailCol:"#b98cff"});
|
||
G.novas.push({x:p.x,y:p.y-10,r:2,maxr:16,life:10,col:"#b98cff"});
|
||
G.shake=Math.max(G.shake,5);SFX.boom(false);}
|
||
break;
|
||
case "deathray":
|
||
if(S.beam>0){S.beam--;
|
||
const t=nearestEnemy(p.x,p.y,400);
|
||
if(t){G.lasers.push({x1:p.x,y1:p.y-8,x2:t.x,y2:t.y,w:st.width,life:2,col:"#ff6b9d"});
|
||
if(G.time%2===0)hurtT(t,st.dps*wdm*4,false); // 死光伤害翻倍
|
||
if(G.time%3===0)spawnPart(t.x,t.y,pick(["#ff6b9d","#fff"]),1);}
|
||
}else{S.c=(S.c||0)+1;
|
||
if(S.c>st.charge*0.45&&G.time%2===0){const t2=nearestEnemy(p.x,p.y,400);
|
||
if(t2)G.lasers.push({x1:p.x,y1:p.y-8,x2:t2.x,y2:t2.y,w:1,life:2,col:"#7a2a3a"});}
|
||
if(S.c>=st.charge){S.c=0;S.beam=st.beam;SFX.od();}}
|
||
break;
|
||
case "crescent": // 巨型旋刃:仅前方±30°两个方向,自身旋转,贯穿
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(20,st.int/fm);
|
||
for(const sd of[-1,1]){const ca=-Math.PI/2+sd*Math.PI/6;
|
||
pbul(p.x+sd*6,p.y-8,Math.cos(ca)*st.spd,Math.sin(ca)*st.spd,st.dmg*wdm,{t:"c",spr:"cres",pierce:true,r:6,spin:true,scale:1.8});}
|
||
SFX.shoot();}
|
||
break;
|
||
case "fission":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(3,st.int/fm);
|
||
pbul(p.x,p.y-8,0,-st.spd,st.dmg*wdm,{t:"s",spr:"fiss",split:st.split,splitAt:st.splitAt||26,r:3});
|
||
SFX.shoot();}
|
||
break;
|
||
case "frost":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(4,st.int/fm);
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"fr",spr:"icelance",pierce:true,r:3,freeze:st.freeze,trailCol:"#7fd8ff"});
|
||
SFX.shoot();}
|
||
break;
|
||
case "homing":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(4,st.int/fm);
|
||
for(let i=0;i<st.n;i++)pbul(p.x+(i-(st.n-1)/2)*6,p.y-4,(i-(st.n-1)/2)*0.9,-st.spd,st.dmg*wdm,{t:"m",spr:"msl2",homing:true,r:3,aoe:st.aoe,trailCol:"#e8e8e8"});
|
||
SFX.shoot();}
|
||
break;
|
||
case "loiter": // 巡飞弹:慢攻速+追踪(中速上限5),命中留火场+分裂4枚快爆小炸弹
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(30,st.int/fm);
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"lm",spr:"loms",r:4,zone:st.zone,fire:st.fire,homing:true,maxSpd:5,trailCol:"#ff8a2e"});
|
||
SFX.shoot();}
|
||
break;
|
||
case "bubble":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(6,st.int/fm);
|
||
pbul(p.x,p.y-10,rnd(-0.3,0.3),-st.spd,st.dmg*wdm,{t:"bb",spr:"bub",aoe:st.aoe,pierce:true,life:150,r:6});}
|
||
break;
|
||
case "starfall": // 流星自屏幕底部逆冲而起
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(70,st.int/fm);
|
||
for(let i=0;i<st.n;i++){const mx=clamp(p.x+rnd(-70,70),10,W-10),my=clamp(p.y-rnd(60,160),20,H-20);
|
||
pbul(mx,H+8,rnd(-0.2,0.2),-rnd(4.5,6),st.dmg*wdm,{t:"mt",spr:"met",aoe:st.aoe,ty2:my,r:4,trailCol:"#ff9a3d",rot:true});}
|
||
SFX.boom(false);}
|
||
break;
|
||
case "boom":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(3,st.int/fm);
|
||
for(let i=0;i<st.n;i++){const off=i-(st.n-1)/2;
|
||
pbul(p.x+off*8,p.y-6,off*0.6,-st.spd,st.dmg*wdm,{t:"b",spr:"boomer",pierce:true,back:true,r:4,bt:39});}
|
||
SFX.shoot();}
|
||
break;
|
||
case "sing":
|
||
if(G.wt[wk]<=0){G.wt[wk]=st.int;
|
||
pbul(p.x,p.y-10,0,-1.2,st.dmg*wdm,{t:"sg",spr:"sing",pull:st.pull,aoe:st.aoe,life:st.fuse,r:6});}
|
||
break;
|
||
case "wave":
|
||
S.c=(S.c||0)+1;
|
||
if(S.c>st.charge*0.35&&G.time%4===0){const a0=rnd(0,TAU),r0=rnd(8,16);
|
||
G.parts.push({x:p.x+Math.cos(a0)*r0,y:p.y-10+Math.sin(a0)*r0,vx:0,vy:0,life:12,col:"#43b8ff",sz:1,suck:{x:p.x,y:p.y-10}});}
|
||
if(S.c>=st.charge){S.c=0;
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"wv",pierce:true,r:Math.round(st.width/2)+2,trailCol:"#43b8ff"});
|
||
G.novas.push({x:p.x,y:p.y-10,r:2,maxr:14,life:8,col:"#43b8ff"});
|
||
G.shake=Math.max(G.shake,4);SFX.boom(false);}
|
||
break;
|
||
case "holy":
|
||
if(G.wt[wk]<=0){G.wt[wk]=st.int;
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"ho",spr:"holyo",len:st.len,life:90,r:4});}
|
||
break;
|
||
case "thunder":
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(30,st.int/fm);
|
||
for(let i=0;i<st.n;i++){const sx=clamp(p.x+rnd(-70,70),10,W-10),sy=rnd(20,p.y-40);
|
||
zapVisual(sx,-4,sx,sy,"#ffe95c");zapVisual(sx+rnd(-10,10),-4,sx,sy,"#43e8ff");
|
||
areaDamage(sx,sy,st.aoe,st.dmg*wdm);
|
||
explodeFx(sx,sy,"elec",0.7);}
|
||
G.shake=Math.max(G.shake,3);SFX.boom(false);}
|
||
break;
|
||
case "shuriken": // 电弧手里剑:不停留,一路向前飞行放电
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(40,st.int/fm);
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"shk",spr:"shuri",pierce:true,r:5,spin:true,life:400,pulse:st.pulse,zapDmg:st.zapDmg*wdm});
|
||
SFX.shoot();}
|
||
break;
|
||
case "volt": // 电能机炮:高频连发,命中连锁电弧(连锁在 collide 结算)
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(3,st.int/fm);
|
||
for(let i=0;i<st.n;i++){const off=i-(st.n-1)/2;
|
||
pbul(p.x+off*st.gap,p.y-8,off*0.5,-st.spd,st.dmg*wdm,{t:"vl",spr:"voltb",r:3,chain:st.chain});}
|
||
SFX.shoot();}
|
||
break;
|
||
case "dawn": // 黎明重炮:长蓄力,一炮巨响
|
||
S.c=(S.c||0)+1;
|
||
if(S.c>st.charge*0.3&&G.time%3===0){const a0=rnd(0,TAU),r0=rnd(8,20);
|
||
G.parts.push({x:p.x+Math.cos(a0)*r0,y:p.y-10+Math.sin(a0)*r0,vx:0,vy:0,life:14,col:pick(["#ffc64a","#ff8a2e","#fff"]),sz:1,suck:{x:p.x,y:p.y-10}});}
|
||
if(S.c>=st.charge){S.c=0;
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"dw",spr:"dawnsh",r:5,aoe:st.aoe,trailCol:"#ff8a2e"});
|
||
G.novas.push({x:p.x,y:p.y-10,r:2,maxr:18,life:10,col:"#ffc64a"});
|
||
G.shake=Math.max(G.shake,5);SFX.boom(false);}
|
||
break;
|
||
case "protocol": // 协议武装:红橙连发压制 + 周期蓄能重炮
|
||
S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=Math.max(3,st.int/fm);
|
||
pbul(p.x-4,p.y-8,0,-st.spd,st.dmg*wdm,{spr:"protb",r:3});
|
||
pbul(p.x+4,p.y-8,0,-st.spd,st.dmg*wdm,{spr:"protb",r:3});
|
||
SFX.shoot();}
|
||
S.hc=(S.hc===undefined?st.heavy:S.hc)-1;
|
||
if(S.hc<24&&S.hc>0&&G.time%3===0){const a1=rnd(0,TAU),r1=rnd(6,14);
|
||
G.parts.push({x:p.x+Math.cos(a1)*r1,y:p.y-12+Math.sin(a1)*r1,vx:0,vy:0,life:12,col:pick(["#ff5a2e","#ffd54a"]),sz:1,suck:{x:p.x,y:p.y-12}});}
|
||
if(S.hc<=0){S.hc=st.heavy;
|
||
pbul(p.x,p.y-12,0,-3.6,st.hdmg*wdm,{t:"ph",spr:"protoh",r:6,aoe:st.haoe,trailCol:"#ff5a2e"});
|
||
G.novas.push({x:p.x,y:p.y-12,r:2,maxr:14,life:8,col:"#ff5a2e"});
|
||
G.shake=Math.max(G.shake,4);SFX.boom(false);}
|
||
break;
|
||
case "nebula": // 星云奥秘:缓慢软追踪,命中绽放群星
|
||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(50,st.int/fm);
|
||
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"nb",spr:"neb",r:6,softH:true,stars:st.stars,trailCol:"#b98cff"});
|
||
SFX.shoot();}
|
||
break;
|
||
}
|
||
}
|
||
/* ---------- 副武器 ---------- */
|
||
for(const sk of G.subLoadout){
|
||
const sd=SUBS[sk],lv=G.subs[sk],S=G.sst[sk]=G.sst[sk]||{};
|
||
switch(sd.beh){
|
||
case "broad": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=Math.max(40,(76-6*lv)/fm);
|
||
const bsp=4.5+0.3*lv;
|
||
for(const sd of[-1,1])pbul(p.x+sd*10,p.y-4,sd*bsp*0.707,-bsp*0.707,(14+4*lv)*dm,{spr:"broad",r:3,trailCol:"#c8923e",rot:true});
|
||
SFX.shoot();}
|
||
break;
|
||
case "sweep":
|
||
if(S.on>0){S.on--;
|
||
const swa=-Math.PI/2+Math.sin(G.time*0.055)*0.55,sx1=p.x,sy1=p.y-8;
|
||
const sx2=sx1+Math.cos(swa)*260,sy2=sy1+Math.sin(swa)*260;
|
||
G.lasers.push({x1:sx1,y1:sy1,x2:sx2,y2:sy2,w:3,life:2,col:"#43e8ff"});
|
||
if(G.time%3===0){const sdx=sx2-sx1,sdy=sy2-sy1,sL2=sdx*sdx+sdy*sdy||1;
|
||
for(const e of G.enemies){if(e.dead)continue;
|
||
const t2=clamp(((e.x-sx1)*sdx+(e.y-sy1)*sdy)/sL2,0,1);
|
||
if(dist2(e.x,e.y,sx1+sdx*t2,sy1+sdy*t2)<(e.r+3)*(e.r+3))hurtEnemy(e,(6+2*lv)*dm*0.34,false);}
|
||
if(G.boss){const t2=clamp(((G.boss.x-sx1)*sdx+(G.boss.y-sy1)*sdy)/sL2,0,1);
|
||
if(bossHit(sx1+sdx*t2,sy1+sdy*t2,3))hurtBoss((6+2*lv)*dm*0.34);}}}
|
||
else{S.t=(S.t||0)-1;if(S.t<=0){S.on=34;S.t=Math.max(90,(170-14*lv)/fm);SFX.od();}}
|
||
break;
|
||
case "leech": break; // 寄生电浆:被动附着,在 hurtEnemy 中结算
|
||
case "orbit":
|
||
p.orbA+=0.09;
|
||
for(let i=0;i<lv;i++){const a=p.orbA+i*(TAU/lv),bx=p.x+Math.cos(a)*26,by=p.y+Math.sin(a)*26;
|
||
for(const e of G.enemies){if(e.dead)continue;
|
||
if(dist2(bx,by,e.x,e.y)<(e.r+5)*(e.r+5)&&G.time-(e.bladeT||0)>18){e.bladeT=G.time;hurtEnemy(e,(10+3*lv)*dm,true);}}
|
||
if(G.boss&&bossHit(bx,by,5)&&G.time-(G.boss.bladeT||0)>18){G.boss.bladeT=G.time;hurtBoss((10+3*lv)*dm);}}
|
||
break;
|
||
case "sats": {
|
||
const n=1+lv;p.satA=(p.satA||0)+0.07;
|
||
for(let i=0;i<n;i++){const a=p.satA+i*(TAU/n),sx=p.x+Math.cos(a)*20,sy=p.y+Math.sin(a)*20;
|
||
for(const b of G.eb){if(b.dead)continue;
|
||
if(dist2(sx,sy,b.x,b.y)<40){b.dead=true;spawnPart(b.x,b.y,"#43e8ff",2);SFX.graze();}}}
|
||
break;}
|
||
case "pd": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=Math.max(18,Math.round((16-2*lv)/0.3));
|
||
let best=null,bd=(60+10*lv)*(60+10*lv);const pdMin=15*15; // 不拦截贴身弹
|
||
for(const b of G.eb){if(b.dead)continue;const d=dist2(p.x,p.y,b.x,b.y);if(d<bd&&d>pdMin){bd=d;best=b;}}
|
||
if(best){best.dead=true;zapVisual(p.x,p.y-8,best.x,best.y,"#9fd8ff");SFX.hit();}}
|
||
break;
|
||
case "slowfield": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=(330-30*lv)/fm;
|
||
G.zones.push({x:p.x,y:p.y-70,r:46+8*lv,slow:true,life:160,col:"#43b8ff"});
|
||
SFX.od();}
|
||
break;
|
||
case "flares": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=Math.max(300,Math.round((100-8*lv)*5/fm));
|
||
for(let i=0;i<4;i++){const fa=-Math.PI/2+(i-1.5)*0.36;
|
||
G.flares.push({x:p.x,y:p.y-6,vx:Math.cos(fa)*0.85,vy:Math.sin(fa)*0.85,life:260});}
|
||
SFX.shoot();}
|
||
break;
|
||
case "seeker": {
|
||
if(S.x===undefined){S.x=p.x;S.y=p.y;}
|
||
const t=nearestEnemy(S.x,S.y,300);
|
||
if(t){const d=Math.hypot(t.x-S.x,t.y-S.y)||1;
|
||
if(d>50){S.x+=(t.x-S.x)/d*2.2;S.y+=(t.y-S.y)/d*2.2;}
|
||
else{S.x+=Math.cos(G.time*0.05)*1.4;S.y+=Math.sin(G.time*0.05)*1.4;}
|
||
S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=Math.max(16,(32-2*lv)/fm);
|
||
const a=Math.atan2(t.y-S.y,t.x-S.x);
|
||
pbul(S.x,S.y,Math.cos(a)*5,Math.sin(a)*5,(4+2*lv)*dm,{r:2});}}
|
||
else{S.x=lerp(S.x,p.x,0.05);S.y=lerp(S.y,p.y-26,0.05);}
|
||
break;}
|
||
case "beamdrone": {
|
||
const a=G.time*0.03;S.x=p.x+Math.cos(a)*30;S.y=p.y+Math.sin(a)*30;
|
||
if(S.on>0){S.on--;
|
||
const t=nearestEnemy(S.x,S.y,220);
|
||
if(t){G.lasers.push({x1:S.x,y1:S.y,x2:t.x,y2:t.y,w:3,life:2,col:"#ff6b9d"});
|
||
if(G.time%3===0)hurtT(t,(6+3*lv)*dm*0.34,false);}}
|
||
else{S.t=(S.t||0)-1;if(S.t<=0){S.on=22;S.t=(140-15*lv)/fm;}}
|
||
break;}
|
||
case "frostnova": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=(260-30*lv)/fm;
|
||
const r=50+10*lv;
|
||
G.novas.push({x:p.x,y:p.y,r:6,maxr:r,life:22,col:"#7fd8ff"});
|
||
for(const e of G.enemies)if(!e.dead&&dist2(p.x,p.y,e.x,e.y)<(r+e.r)*(r+e.r)){e.slow=Math.max(e.slow||0,120+30*lv);hurtEnemy(e,(10+5*lv)*dm,true);}
|
||
SFX.od();}
|
||
break;
|
||
case "sticky": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=(230-18*lv)/fm;
|
||
pbul(p.x,p.y-6,rnd(-0.4,0.4),-2.6,(34+12*lv)*dm,{t:"tb",spr:"sticky",fuse:120,aoe:36,r:4});}
|
||
break;
|
||
case "magnetar": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=(320-25*lv)/fm;
|
||
G.novas.push({x:p.x,y:p.y,r:8,maxr:90,life:26,col:"#b05ce8"});
|
||
for(const e of G.enemies)if(!e.dead){e.slow=Math.max(e.slow||0,60);hurtEnemy(e,(12+6*lv)*dm,true);}
|
||
if(G.boss)hurtBoss((12+6*lv)*dm);
|
||
const rr=(70+15*lv)*(70+15*lv);
|
||
for(const b of G.eb)if(dist2(p.x,p.y,b.x,b.y)<rr){b.dead=true;spawnPart(b.x,b.y,"#b05ce8",1);}
|
||
SFX.boom(true);}
|
||
break;
|
||
case "executor": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=(340-30*lv)/fm;
|
||
pbul(p.x,p.y-4,rnd(-0.6,0.6),-2.2,(60+20*lv)*dm,{t:"ex",spr:"cmsl",homing:true,eliteHunt:true,r:4,aoe:26,trailCol:"#ff9a3d"});
|
||
SFX.shoot();}
|
||
break;
|
||
case "airstrike": S.t=(S.t||0)-1; // 副武器:穿透机身,沿途轰炸
|
||
if(S.t<=0){S.t=Math.max(70,(160-10*lv)/fm);
|
||
for(const sd of[-1,1])
|
||
pbul(p.x+sd*8,p.y-2,sd*2.5,0,(16+5*lv)*dm,{t:"am",spr:"amsl",phase:1,pt:0,zone:16,r:3,trailCol:"#9aa4b8",rot:true,pierce:true});
|
||
SFX.shoot();}
|
||
break;
|
||
case "repair": S.t=(S.t||0)-1;
|
||
if(S.t<=0){S.t=(30-4*lv)*60;
|
||
if(G.hp<G.maxHp){G.hp=Math.min(G.maxHp,G.hp+1);
|
||
G.texts.push({x:p.x,y:p.y-12,t:"+1",col:"#5cff9d",life:30});}}
|
||
break;
|
||
case "thorns": break; // 被动反伤,在碰撞中结算
|
||
}
|
||
}
|
||
/* ---------- 僚机 ---------- */
|
||
const dn=G.droneLoadout.length;
|
||
for(let i=0;i<dn;i++){
|
||
const key=G.droneLoadout[i],dd=DRONES[key],lv=G.drones[key],S=G.dst[key]=G.dst[key]||{};
|
||
const a=G.time*0.025+i*(TAU/Math.max(1,dn));
|
||
S.x=p.x+Math.cos(a)*24;S.y=p.y+Math.sin(a)*10+6;
|
||
S.t=(S.t||0)-1;
|
||
const db=G.stat.droneBoost;
|
||
switch(dd.beh){
|
||
case "aim": if(S.t<=0){S.t=Math.max(18,(34-2*lv)/fm);aimShot(S.x,S.y,(4+2*lv)*dm*db,5);}break;
|
||
case "aim2": if(S.t<=0){S.t=Math.max(20,(40-2*lv)/fm);aimShot(S.x-3,S.y,(3+2*lv)*dm*db,5);aimShot(S.x+3,S.y,(3+2*lv)*dm*db,5);}break;
|
||
case "snipe": if(S.t<=0){S.t=Math.max(40,(70-4*lv)/fm);aimShot(S.x,S.y,(15+8*lv)*dm*db,7);}break;
|
||
case "beam":
|
||
if(S.on>0){S.on--;const t=nearestEnemy(S.x,S.y,200);
|
||
if(t){G.lasers.push({x1:S.x,y1:S.y,x2:t.x,y2:t.y,w:3,life:2,col:"#ff6b9d"});
|
||
if(G.time%3===0)hurtT(t,(5+3*lv)*dm*0.34,false);}}
|
||
else if(S.t<=0){S.on=20;S.t=(140-15*lv)/fm;}
|
||
break;
|
||
case "bomb": if(S.t<=0){S.t=(150-12*lv)/fm;pbul(S.x,S.y,0,-2.5,(12+6*lv)*dm*db,{t:"tb2",spr:"bombp",bt:40,aoe:24,r:3});}break;
|
||
case "frostD": if(S.t<=0){S.t=Math.max(24,(44-3*lv)/fm);aimShot(S.x,S.y,(3+2*lv)*dm*db,5,"frost",60+20*lv);}break;
|
||
case "teslaD": if(S.t<=0){S.t=Math.max(30,(55-4*lv)/fm);
|
||
const t=nearestEnemy(S.x,S.y,85);
|
||
if(t){zapVisual(S.x,S.y,t.x,t.y,"#ffe95c");hurtT(t,(6+3*lv)*dm*db,true);}}break;
|
||
case "melee": { // 冲撞僚机:加大环绕半径与判定
|
||
const a2=G.time*0.07+i*(TAU/Math.max(1,dn));
|
||
S.x=p.x+Math.cos(a2)*38;S.y=p.y+Math.sin(a2)*38;
|
||
for(const e of G.enemies)if(!e.dead&&dist2(S.x,S.y,e.x,e.y)<(e.r+10)*(e.r+10)&&G.time-(e.ramT||0)>22){e.ramT=G.time;hurtEnemy(e,(8+4*lv)*dm*db,true);}
|
||
if(G.boss&&bossHit(S.x,S.y,10)&&G.time-(G.boss.ramT||0)>22){G.boss.ramT=G.time;hurtBoss((8+4*lv)*dm*db);}
|
||
break;}
|
||
case "heal": if(S.t<=0){S.t=(40-6*lv)*60;
|
||
if(G.hp<G.maxHp){G.hp=Math.min(G.maxHp,G.hp+2);G.texts.push({x:p.x,y:p.y-12,t:"+2",col:"#5cff9d",life:30});}}break;
|
||
case "block": for(const b of G.eb){if(b.dead)continue;
|
||
if(dist2(S.x,S.y,b.x,b.y)<110){b.dead=true;spawnPart(b.x,b.y,"#43e8ff",2);}}break;
|
||
case "missile": if(S.t<=0){S.t=(90-8*lv)/fm;pbul(S.x,S.y,rnd(-1,1),-2.5,(10+5*lv)*dm*db,{t:"m",spr:"msl",homing:true,r:3});}break;
|
||
case "bladeD": if(S.t<=0){S.t=(80-6*lv)/fm;pbul(S.x,S.y,0,-4,(8+4*lv)*dm*db,{t:"b",spr:"blade",pierce:true,back:true,r:4,bt:34});}break;
|
||
case "sweep": // 扫射僚机:周期性一串弹从左到右/从右到左扫过前方扇区
|
||
if(S.sw>0){S.sw--;
|
||
if(S.sw%2===0){const pr=1-S.sw/14,sa=-Math.PI/2+(pr-0.5)*1.5*(S.dir||1);
|
||
pbul(S.x,S.y,Math.cos(sa)*5.2,Math.sin(sa)*5.2,(3+1.5*lv)*dm*db,{r:2,spr:"swep"});}
|
||
}else if(S.t<=0){S.dir=S.dir===1?-1:1;S.sw=14;S.t=Math.max(50,(110-8*lv)/fm);}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
function nearestEnemy(x,y,maxD){
|
||
let best=null,bd=maxD*maxD;
|
||
for(const e of G.enemies){if(e.dead)continue;const d=dist2(x,y,e.x,e.y);if(d<bd){bd=d;best=e;}}
|
||
if(G.boss){const d=dist2(x,y,G.boss.x,G.boss.y);if(d<bd){best=G.boss;}}
|
||
return best;
|
||
}
|
||
|