massfront-playtest / src /hazards.js
CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
23 kB
;
;
/* ============================================================================
MAP-EXCLUSIVE ENVIRONMENTAL HAZARDS
----------------------------------------------------------------------------
Every map already looked different. None of them PLAYED differently β€” the
terrain changed the pathing and nothing else, so map choice was decoration.
Each map now owns one hazard nobody else gets, drawn from what that place
actually is:
Vanguard Valley DUST FRONT a squall rolls across the open highway country,
blinding and slowing everything inside it
Highland Scar ROCKSLIDE the cliffs shed; fighting on steep ground has
a cost the plains do not charge
Shattered Isles SQUALL an ocean storm earths itself through the
biggest piece of metal under the cell
Relic Basin RELIC PULSE the thing in the crater is not inert, and the
centre of the map is the richest ground on it
Three rules the whole file obeys:
* every hazard hits ALL THREE TEAMS. A hazard that only inconveniences the
player is a difficulty setting wearing a costume.
* every hazard is telegraphed before it lands, with enough warning to walk
out. Damage from nowhere is not difficulty, it is noise.
* every hazard scales with the difficulty setting, because the whole reason
Easy was unplayable is that the map's third parties did not.
============================================================================ */
const HAZ={ map:'', mode:'', t:0, warn:0, cells:[], front:null, phase:0, count:0, faults:[], vision:[], lava:[] };
/* Shown on the map cards in Battle Setup, so the hazard is part of choosing a
battlefield rather than a surprise thirty seconds in. */
const MAPHAZ={
vanguard:{em:'πŸŒͺ', nm:'DUST FRONTS', ds:'Squalls cross the valley β€” sight and speed cut inside'},
highland:{em:'β›°', nm:'FAULT COLLAPSE',ds:'Marked shelves crack, telegraph, then collapse into impassable scars'},
isles: {em:'⚑', nm:'SQUALL LINES', ds:'Lightning earths through the largest massed force'},
crater: {em:'β—ˆ', nm:'RELIC PULSES', ds:'The basin floor blacks out systems and drains grids'},
};
const MF_HAZARD_PROFILES={
dust:{em:'πŸŒͺ',nm:'ASH FRONT',ds:'Abrasive fronts cut sight and movement across exposed ground',mode:'vanguard'},
storm:{em:'🌩',nm:'TEMPEST CELLS',ds:'Charged weather blinds advancing formations',mode:'vanguard'},
calm:{em:'β—Ž',nm:'CLEAR SKIES',ds:'Working civic weather β€” no battlefield tax on the opening build',mode:'calm'},
spores:{em:'β—Œ',nm:'SPORE BLOOM',ds:'Dense alien spores conceal units and slow movement',mode:'vanguard'},
heat:{em:'♨',nm:'THERMAL SURGE',ds:'Heat distortion sweeps exposed routes and drains combat tempo',mode:'heat'},
collapse:{em:'β›°',nm:'TERRAIN COLLAPSE',ds:'Telegraphed shelves fracture into impassable scars',mode:'highland'},
eruption:{em:'πŸŒ‹',nm:'MAGMA ERUPTION',ds:'Unstable vents rupture, then leave dangerous molten ground',mode:'eruption'},
meteor:{em:'β˜„',nm:'ORBITAL DEBRIS',ds:'Non-organic space weather telegraphs destructive impact zones',mode:'meteor'},
squall:{em:'⚑',nm:'SQUALL LINES',ds:'Electrical storms earth through concentrated forces',mode:'isles'},
whiteout:{em:'❄',nm:'WHITEOUT',ds:'Frozen fronts erase vision before slamming massed units',mode:'whiteout'},
flood:{em:'≋',nm:'FLOOD SURGE',ds:'Rising channels isolate formations before a violent surge',mode:'flood'},
/* Dead MAPDEFS string. Live sites use `heat`; keep the alias so a leftover
key still runs the thermal tick instead of falling through to dust. */
solray_corridor:{em:'πŸŒ…',nm:'SOLAR FOCUS',ds:'Heat distortion sweeps exposed routes and drains combat tempo',mode:'heat'},
pulse:{em:'β—ˆ',nm:'RELIC PULSES',ds:'Buried systems black out units and drain local grids',mode:'crater'}
};
function mapHazardKey(map){
const D=(typeof MAPDEFS!=='undefined'&&MAPDEFS[map])?MAPDEFS[map]:null;
return (D&&D.hazard)||({vanguard:'dust',highland:'collapse',isles:'squall',crater:'pulse'}[map])||'storm';
}
function mapHazardDef(map){
const key=mapHazardKey(map);
const P=MF_HAZARD_PROFILES[key]||MF_HAZARD_PROFILES.storm;
const legacy=MAPHAZ[map];
/* Legacy MAPHAZ rows have copy and no mode. Homeworld sites have a
profile key and no MAPHAZ row. Merge so UI and hazTick always agree. */
if(legacy) return {em:legacy.em,nm:legacy.nm,ds:legacy.ds,mode:P.mode||map};
return P;
}
function mapHazardMode(map){ return (mapHazardDef(map).mode)||map; }
function hazDiff(){ return (typeof diffLvl==='function')?diffLvl():1; }
/* Long gaps on Easy, and a much smaller bite. */
function hazEvery(base){
const p=typeof battlefieldPresetDef==='function'?battlefieldPresetDef():{haz:1};
const fac=typeof mapHomeFac==='function'?mapHomeFac():'';
/* Nova homeworlds are beginner theatres: weather exists, it does not bully
the opening. Brood worlds bite sooner. */
const world=fac==='nova'?1.85:fac==='horde'?0.82:1;
return base*[1.85,1.3,1][hazDiff()]*(p.haz||1)*world;
}
function hazDmg(base){ return base*[0.45,0.75,1][hazDiff()]; }
function hazReset(){
HAZ.map=(typeof curMap!=='undefined')?curMap:'';
HAZ.mode=mapHazardMode(HAZ.map);
HAZ.cells.length=0; HAZ.front=null; HAZ.phase=0; HAZ.warn=0; HAZ.count=0;
HAZ.faults.length=0; HAZ.vision.length=0; HAZ.lava.length=0;
/* Nothing happens for the first stretch of any match. A hazard during the
opening build is just a tax on not knowing the map yet. */
HAZ.t=hazEvery([150,120,95][hazDiff()]);
if(HAZ.mode==='calm') HAZ.t=1e9;
if(typeof uhaz!=='undefined') uhaz.fill(0);
if(HAZ.mode==='highland') hazSeedFaults();
}
function hazName(){
const D=mapHazardDef(HAZ.map);
return (D&&D.nm)||'';
}
/* Apply the blind/slow debuff β€” see uhaz in sim.js, read by movement and by
weapon range. Buildings are immune; they were not going anywhere anyway. */
function hazBlind(x,y,r,secs){
const r2=r*r;
for(let i=0;i<unitHigh;i++){
if(!ualive[i]) continue;
if(dist2(ux[i],uy[i],x,y)<r2 && uhaz[i]<secs) uhaz[i]=secs;
}
}
/* Damage is attributed to team 2 rather than a fourth "environment" team: the
damage accumulator is a three-slot array and a stray index 3 would quietly
poison it with NaN. Nothing reads the attacker for kill credit here β€” the
attacker index is -1 β€” so the only effect is that the hive gets blamed for
the weather, which is close enough to true on this planet. */
function hazHurt(x,y,r,dmg){
const r2=r*r;
for(let i=0;i<unitHigh;i++){
if(!ualive[i]) continue;
const d2=dist2(ux[i],uy[i],x,y);
if(d2<r2) dealDamage(i,dmg*(1-Math.sqrt(d2)/r*0.55),2,-1);
}
/* damageBld takes an INDEX into blds, not the object, and it is the only path
that handles death properly β€” rubble, salvage, territory rebuild. */
for(let b=0;b<blds.length;b++){
const B=blds[b];
if(!B||!B.alive) continue;
const d2=dist2(B.x,B.y,x,y);
if(d2<r2) damageBld(b,dmg*(1-Math.sqrt(d2)/r*0.55)*0.6,2);
}
}
/* ---- VANGUARD VALLEY β€” the dust front -------------------------------------
A band that crosses the whole map on a fixed heading. It does no damage at
all: it takes away sight and speed, which on open highway country is the
thing that actually decides fights. Armies caught mid-march arrive late and
half-blind; armies that waited lose tempo instead. */
function hazFrontTick(dt){
const F=HAZ.front;
if(!F) return;
F.p+=F.sp*dt;
const span=Math.hypot(MAP,MAP);
/* Sample points along the band and blind everything near them. */
const cx=MAP/2+F.dx*(F.p-span*0.5), cy=MAP/2+F.dy*(F.p-span*0.5);
F.cx=cx; F.cy=cy;
const px=-F.dy, py=F.dx;
for(let k=-6;k<=6;k++){
const sx=cx+px*k*(MAP/13), sy=cy+py*k*(MAP/13);
if(sx<-200||sy<-200||sx>MAP+200||sy>MAP+200) continue;
hazBlind(sx,sy,F.w*0.62,1.1);
/* Whiteout copy promises a slam after the blind. Dust fronts stay
sight/speed only β€” damage here is the ice, not the ash. */
if(HAZ.mode==='whiteout'&&(tick+k)%6===0) hazHurt(sx,sy,F.w*0.42,hazDmg(18));
if(typeof addParticle==='function' && (tick+k)%2===0 && perfScale>0.35){
for(let n=0;n<2;n++)
addParticle(1, sx+rr(-F.w*0.5,F.w*0.5), sy+rr(-F.w*0.45,F.w*0.45),
F.dx*26+rr(-8,8), F.dy*26+rr(-8,8), 1.5+Math.random(),
52+Math.random()*40, 168,150,116);
}
}
if(F.p>span+F.w){ HAZ.front=null; }
}
/* ---- HIGHLAND SCAR β€” rockslides -------------------------------------------
Sited by slope, not at random: the hazard belongs to the cliffs, so holding
the high ground is a real trade rather than a free advantage. */
function hazPickSteep(){
let best=null, bs=-1;
for(let t=0;t<40;t++){
const x=rr(220,MAP-220), y=rr(220,MAP-220);
if(typeof hAt!=='function') break;
const h=hAt(x,y);
if(h<0.02) continue; // not underwater
const s=Math.abs(hAt(x+34,y)-h)+Math.abs(hAt(x,y+34)-h)
+Math.abs(hAt(x-34,y)-h)+Math.abs(hAt(x,y-34)-h);
if(s>bs){ bs=s; best=[x,y]; }
}
return bs>0.012?best:null;
}
/* Fault shelves are authored by the live heightfield, not by screen-space
decoration. Dormant shelves remain visible before they arm, giving players
a route-planning choice; the normal hazard warning is still the final clear
signal to evacuate. */
function hazSeedFaults(){
const want=5;
for(let tries=0;HAZ.faults.length<want&&tries<90;tries++){
const p=hazPickSteep(); if(!p) continue;
if(typeof farFromStartZones==='function'&&!farFromStartZones(p[0],p[1],430)) continue;
let clear=true;
for(const F of HAZ.faults) if(dist2(p[0],p[1],F.x,F.y)<280*280){ clear=false; break; }
if(clear) HAZ.faults.push({x:p[0],y:p[1],r:105+Math.random()*28,state:0,at:0});
}
}
/* Fog sources ask this at their own position. Unit debuffs and actual storm
geometry now reduce line of sight, rather than merely lowering weapon range
while the fog map continued to reveal everything. */
function hazVisionMult(x,y,unitIdx){
let m=1;
if(unitIdx>=0&&typeof uhaz!=='undefined'&&uhaz[unitIdx]>0) m=0.44;
const F=HAZ.front;
if(F&&F.cx!=null){
const along=Math.abs((x-F.cx)*F.dx+(y-F.cy)*F.dy);
if(along<F.w*0.64) m=Math.min(m,0.42);
}
const now=typeof stats!=='undefined'?stats.t:0;
for(const V of HAZ.vision){
if(V.until<=now) continue;
if(dist2(x,y,V.x,V.y)<V.r*V.r) m=Math.min(m,V.m||0.45);
}
return m;
}
/* Low ground just above the water table β€” floodplains and channels β€” without
calling deformTerrain / stampWaterRipple. Those belong to the crater-water
sibling; this strike is a combat tax, not a mesh rewrite. */
function hazPickChannel(){
const wh=typeof WATER_H==='number'?WATER_H:0.335;
let best=null, bs=1e9;
if(typeof hAt!=='function') return null;
for(let t=0;t<50;t++){
const x=rr(200,MAP-200), y=rr(200,MAP-200);
const h=hAt(x,y);
if(h<wh-0.008||h>wh+0.048) continue;
const score=Math.abs(h-wh);
if(score<bs){ bs=score; best=[x,y]; }
}
return best;
}
/* ---- SHATTERED ISLES β€” squall lightning -----------------------------------
Earths through the largest mass under the cell. Massing an army in one place
on an ocean map is exactly what you want to do and exactly what gets hit. */
function hazPickMass(){
let best=null, bs=0;
const step=Math.max(1,unitHigh/220|0);
for(let i=0;i<unitHigh;i+=step){
if(!ualive[i]||uteam[i]===2) continue;
let n=0;
for(let j=0;j<unitHigh;j+=step) if(ualive[j]&&dist2(ux[i],uy[i],ux[j],uy[j])<170*170) n++;
if(n>bs){ bs=n; best=[ux[i],uy[i]]; }
}
for(const B of bldLive){
if(!B.alive||B.team===2||B.prog<1) continue;
let n=1.5;
for(const C of bldLive) if(C.alive&&dist2(B.x,B.y,C.x,C.y)<200*200) n+=1.2;
if(n>bs){ bs=n; best=[B.x,B.y]; }
}
return bs>=3?best:null;
}
function hazTick(dt){
if(typeof running==='undefined'||!running||demoMode||gameEnded) return;
if(HAZ.map!==curMap) hazReset();
const D=hazDiff();
const now=typeof stats!=='undefined'?stats.t:0;
for(let i=HAZ.vision.length-1;i>=0;i--) if(HAZ.vision[i].until<=now) HAZ.vision.splice(i,1);
/* Eruptions leave readable, persistent danger instead of becoming a single
orange explosion that players can immediately forget. Damage pulses are
bounded and the pool expires, so it cannot permanently wall off a route. */
for(let i=HAZ.lava.length-1;i>=0;i--){
const L=HAZ.lava[i]; L.life-=dt; L.pulse-=dt;
if(L.life<=0){ HAZ.lava.splice(i,1); continue; }
if(L.pulse<=0){ L.pulse=.78; hazHurt(L.x,L.y,L.r,hazDmg(34)); }
if(typeof addParticle==='function'&&perfScale>.32&&Math.random()<dt*9){
const a=Math.random()*TAU, rr0=Math.sqrt(Math.random())*L.r;
addParticle(Math.random()<.42?0:1,L.x+Math.cos(a)*rr0,L.y+Math.sin(a)*rr0,
rr(-3,3),rr(-18,-5),.5+Math.random()*.7,8+Math.random()*14,255,104,30);
}
}
if(HAZ.front) hazFrontTick(dt);
/* Telegraph phase: markers are on the ground, damage has not landed yet. */
if(HAZ.warn>0){
HAZ.warn-=dt;
for(const c of HAZ.cells){
if(typeof addParticle==='function' && (tick%3)===0 && perfScale>0.35)
addParticle(0, c[0]+rr(-c[2],c[2]), c[1]+rr(-c[2],c[2]), 0,-14, .5, 8,
c[3][0],c[3][1],c[3][2]);
}
if(HAZ.warn<=0){ hazStrike(); HAZ.cells.length=0; }
return;
}
HAZ.t-=dt;
if(HAZ.t>0) return;
if(HAZ.mode==='vanguard'||HAZ.mode==='whiteout'||HAZ.mode==='spore_bloom'||HAZ.mode==='spores'){
HAZ.t=hazEvery(115+Math.random()*50);
const a=Math.random()*TAU;
HAZ.front={dx:Math.cos(a),dy:Math.sin(a),p:0,
sp:118+Math.random()*40, w:[300,400,480][D]};
HAZ.count++;
const Dfn=mapHazardDef(HAZ.map);
toast((Dfn&&Dfn.em||'πŸŒͺ')+' '+(Dfn&&Dfn.nm||'FRONT')+' rolling in β€” sight and speed cut inside it');
sfx('alarm');
}
else if(HAZ.mode==='calm'){
HAZ.t=1e9;
}
else if(HAZ.mode==='heat'){
HAZ.t=hazEvery(90+Math.random()*35);
HAZ.cells.length=0;
const n=[2,3,4][D];
for(let k=0;k<n;k++){
const p=[rr(200,MAP-200),rr(200,MAP-200)];
HAZ.cells.push([p[0],p[1],90+Math.random()*30,[255,180,60]]);
}
HAZ.warn=3.5; HAZ.phase=5;
for(const c of HAZ.cells) mmPing(c[0],c[1]);
toast('πŸŒ… SOLAR FOCUS BEAM DISPATCHED β€” clear the illuminated corridor');
sfx('alarm');
}
else if(HAZ.mode==='highland'){
HAZ.t=hazEvery(95+Math.random()*40);
HAZ.cells.length=0;
const n=[1,2,3][D];
for(let k=0;k<n;k++){
let F=HAZ.faults.find(q=>q.state===0);
if(!F){ hazSeedFaults(); F=HAZ.faults.find(q=>q.state===0); }
if(F){
F.state=1; F.at=now;
const c=[F.x,F.y,F.r,[220,176,98]]; c.fault=F; HAZ.cells.push(c);
}
}
if(!HAZ.cells.length){ HAZ.t=20; return; }
HAZ.warn=4.8; HAZ.phase=1;
for(const c of HAZ.cells) mmPing(c[0],c[1]);
toast('β›° FAULT COLLAPSE β€” cracked shelves are giving way, clear the marked ground');
sfx('alarm');
}
else if(HAZ.mode==='eruption'){
HAZ.t=hazEvery(82+Math.random()*38);
HAZ.cells.length=0;
const n=[1,2,3][D];
for(let k=0;k<n;k++){
const p=hazPickSteep()||[rr(240,MAP-240),rr(240,MAP-240)];
HAZ.cells.push([p[0],p[1],86+Math.random()*24,[255,92,28]]);
}
HAZ.warn=4.4; HAZ.phase=4;
for(const c of HAZ.cells) mmPing(c[0],c[1]);
toast('πŸŒ‹ MAGMA PRESSURE RISING β€” evacuate the marked vents');
sfx('alarm');
}
else if(HAZ.mode==='flood'){
HAZ.t=hazEvery(85+Math.random()*35);
HAZ.cells.length=0;
const n=[2,3,4][D];
for(let k=0;k<n;k++){
const p=hazPickChannel()||[rr(220,MAP-220),rr(220,MAP-220)];
HAZ.cells.push([p[0],p[1],88+Math.random()*24,[70,190,220]]);
}
HAZ.warn=3.8; HAZ.phase=7;
for(const c of HAZ.cells) mmPing(c[0],c[1]);
toast('≋ FLOOD SURGE β€” channels are rising, clear the marked low ground');
sfx('alarm');
}
else if(HAZ.mode==='isles'){
HAZ.t=hazEvery(80+Math.random()*35);
HAZ.cells.length=0;
const n=[2,3,5][D];
for(let k=0;k<n;k++){
const p=hazPickMass();
if(p) HAZ.cells.push([clamp(p[0]+rr(-70,70),40,MAP-40),
clamp(p[1]+rr(-70,70),40,MAP-40),70,[150,225,255]]);
}
if(!HAZ.cells.length){ HAZ.t=18; return; }
HAZ.warn=2.6; HAZ.phase=2;
mmPing(HAZ.cells[0][0],HAZ.cells[0][1]);
toast('⚑ Squall line overhead β€” lightning earthing through massed armour');
sfx('alarm');
}
else if(HAZ.mode==='meteor'){
HAZ.t=hazEvery(88+Math.random()*40);
HAZ.cells.length=0;
const n=[2,3,4][D];
for(let k=0;k<n;k++){
const p=[rr(220,MAP-220),rr(220,MAP-220)];
HAZ.cells.push([p[0],p[1],80+Math.random()*28,[210,230,255]]);
}
HAZ.warn=3.8; HAZ.phase=6;
for(const c of HAZ.cells) mmPing(c[0],c[1]);
toast('β˜„ ORBITAL DEBRIS INBOUND β€” clear the marked impact lanes');
sfx('alarm');
}
else if(HAZ.mode==='crater'){
HAZ.t=hazEvery(125+Math.random()*45);
HAZ.cells=[[MAP/2,MAP/2,[300,380,460][D],[200,160,255]]];
HAZ.warn=4.0; HAZ.phase=3;
mmPing(MAP/2,MAP/2);
toast('β—ˆ The relic is waking β€” pulse building at the basin floor');
sfx('alarm');
}
else {
/* Unknown mode used to wait 60s forever and never strike. Treat it as a
front so an authored MAPDEFS.hazard the profiles missed still plays. */
HAZ.mode='vanguard'; HAZ.t=0;
}
}
function hazStrike(){
const D=hazDiff();
HAZ.count++;
if(HAZ.phase===1){ // rockslide
for(const c of HAZ.cells){
if(c.fault){ c.fault.state=2; c.fault.at=stats.t; }
spawnExplosion(c[0],c[1],c[2]*0.5,2);
hazHurt(c[0],c[1],c[2],hazDmg(210));
hazBlind(c[0],c[1],c[2]*1.2,2.2);
HAZ.vision.push({x:c[0],y:c[1],r:c[2]*1.2,until:stats.t+3.2,m:0.48});
/* A collapse is terrain, not a decal: the deeper bowl is processed by
the existing deformation queue, re-lights the mesh and updates the
passability grid if the shelf tears below the water table. */
if(typeof deformTerrain==='function') deformTerrain(c[0],c[1],c[2]*0.78,0.105);
if(typeof addParticle==='function')
for(let k=0;k<14;k++)
addParticle(1,c[0]+rr(-c[2]*0.6,c[2]*0.6),c[1]+rr(-c[2]*0.6,c[2]*0.6),
rr(-14,14),rr(-24,-6),1.8,44+Math.random()*40, 150,136,116);
}
shake=Math.max(shake,5);
}
else if(HAZ.phase===2){ // lightning
for(const c of HAZ.cells){
spawnExplosion(c[0],c[1],c[2]*0.7,2);
hazHurt(c[0],c[1],c[2],hazDmg(260));
hazBlind(c[0],c[1],c[2]*1.4,1.6); // EMP dazzle
HAZ.vision.push({x:c[0],y:c[1],r:c[2]*1.45,until:stats.t+2.4,m:0.36});
if(typeof addParticle==='function')
for(let k=0;k<10;k++)
addParticle(0,c[0]+rr(-c[2]*0.5,c[2]*0.5),c[1]+rr(-c[2]*0.5,c[2]*0.5),
0,rr(-40,-10),.45,10, 190,240,255);
}
shake=Math.max(shake,4);
if(typeof flashScreen==='function'&&HAZ.cells.length>2) flashScreen();
}
else if(HAZ.phase===3){ // relic pulse
const c=HAZ.cells[0];
spawnExplosion(c[0],c[1],c[2]*0.4,2);
hazHurt(c[0],c[1],c[2],hazDmg(150));
hazBlind(c[0],c[1],c[2]*1.15,4.5); // long systems blackout
HAZ.vision.push({x:c[0],y:c[1],r:c[2]*1.15,until:stats.t+5.2,m:0.32});
/* It eats power too β€” the basin punishes anyone who parks their grid on it. */
for(let t=0;t<2;t++) if(typeof resE!=='undefined') resE[t]=Math.max(0,resE[t]*0.88);
if(typeof addParticle==='function')
for(let k=0;k<20;k++){
const a=Math.random()*TAU;
addParticle(0,c[0]+Math.cos(a)*c[2]*0.5,c[1]+Math.sin(a)*c[2]*0.5,
Math.cos(a)*70,Math.sin(a)*70,.7,13, 200,150,255);
}
shake=Math.max(shake,7);
toast('β—ˆ RELIC PULSE β€” systems down across the basin');
}
else if(HAZ.phase===4){ // magma eruption
for(const c of HAZ.cells){
spawnExplosion(c[0],c[1],c[2]*.75,2);
hazHurt(c[0],c[1],c[2]*1.08,hazDmg(320));
hazBlind(c[0],c[1],c[2]*1.3,2.4);
HAZ.vision.push({x:c[0],y:c[1],r:c[2]*1.35,until:stats.t+4,m:.5});
HAZ.lava.push({x:c[0],y:c[1],r:c[2]*.82,life:34+Math.random()*14,pulse:.2});
if(typeof deformTerrain==='function') deformTerrain(c[0],c[1],c[2]*.65,.055);
if(typeof addParticle==='function') for(let k=0;k<28;k++){
const a=Math.random()*TAU;
addParticle(k%3?0:1,c[0]+Math.cos(a)*rr(0,c[2]*.5),c[1]+Math.sin(a)*rr(0,c[2]*.5),
Math.cos(a)*rr(12,55),Math.sin(a)*rr(12,55),.6+Math.random(),
10+Math.random()*22,255,92,24);
}
}
shake=Math.max(shake,11);
if(typeof flashScreen==='function') flashScreen();
toast('πŸŒ‹ MAGMA ERUPTION β€” molten ground remains lethal');
if(typeof sfx==='function'&&HAZ.cells.length) sfx('boom',HAZ.cells[0][0],HAZ.cells[0][1],2.2);
}
else if(HAZ.phase===5){ // thermal / solray
for(const c of HAZ.cells){
spawnExplosion(c[0],c[1],c[2]*0.55,2);
hazHurt(c[0],c[1],c[2],hazDmg(180));
hazBlind(c[0],c[1],c[2]*1.15,2.8);
HAZ.vision.push({x:c[0],y:c[1],r:c[2]*1.2,until:stats.t+3.4,m:0.4});
}
shake=Math.max(shake,4);
toast('♨ THERMAL SURGE β€” exposed ground is blinding');
}
else if(HAZ.phase===6){ // orbital debris
for(const c of HAZ.cells){
spawnExplosion(c[0],c[1],c[2]*0.7,2);
hazHurt(c[0],c[1],c[2],hazDmg(240));
hazBlind(c[0],c[1],c[2]*1.25,1.8);
HAZ.vision.push({x:c[0],y:c[1],r:c[2]*1.3,until:stats.t+2.6,m:0.38});
if(typeof addParticle==='function')
for(let k=0;k<12;k++)
addParticle(0,c[0]+rr(-c[2]*0.5,c[2]*0.5),c[1]+rr(-c[2]*0.5,c[2]*0.5),
0,rr(-50,-12),.5,12, 200,230,255);
}
shake=Math.max(shake,6);
if(typeof flashScreen==='function') flashScreen();
toast('β˜„ DEBRIS STRIKE β€” non-organic weather from orbit');
}
else if(HAZ.phase===7){ // flood surge
for(const c of HAZ.cells){
spawnExplosion(c[0],c[1],c[2]*0.55,2);
hazHurt(c[0],c[1],c[2],hazDmg(190));
hazBlind(c[0],c[1],c[2]*1.35,3.6);
HAZ.vision.push({x:c[0],y:c[1],r:c[2]*1.4,until:stats.t+4.2,m:0.40});
if(typeof addParticle==='function')
for(let k=0;k<14;k++)
addParticle(1,c[0]+rr(-c[2]*0.55,c[2]*0.55),c[1]+rr(-c[2]*0.55,c[2]*0.55),
rr(-10,10),rr(-28,-4),1.2,28+Math.random()*22, 70,190,220);
}
shake=Math.max(shake,5);
toast('≋ SURGE HIT β€” low ground is isolated');
}
HAZ.phase=0;
}