// Wallbuster effects Class BustedQuake : SWWMNonInteractiveActor { override void PostBeginPlay() { if ( (special1 < 3) || (special1 > 6) ) { A_StartSound("wallbuster/smallbust",CHAN_VOICE,CHANF_OVERLAP,min(1.,special1*.32),1./max(1.,special1*.35),1.-special1*.05); A_StartSound("wallbuster/smallbust",CHAN_VOICE,CHANF_OVERLAP,min(1.,special1*.32),1./max(1.,special1*.35),1.-special1*.05); } if ( special1 >= 3 ) { A_StartSound("wallbuster/bigbust",CHAN_VOICE,CHANF_OVERLAP,min(1.,special1*.35),1./max(1.,special1*.35),1.-special1*.01); A_StartSound("wallbuster/bigbust",CHAN_VOICE,CHANF_OVERLAP,min(1.,special1*.35),1./max(1.,special1*.35),1.-special1*.01); } A_QuakeEx(special1,special1,special1,20+special1*5,0,300+special1*90,"",QF_RELATIVE|QF_SCALEDOWN|QF_3D,rollIntensity:special1*.1); A_AlertMonsters(swwm_uncapalert?0:2500,AMF_EMITFROMTARGET); } States { Spawn: TNT1 A 700; Stop; } } Class BustPoint { Vector2 pos; } // Bustin' makes me feel good Class BusterWall : Thinker { Sector hitsector; swwm_PolyobjectHandle hitpoly; int accdamage; Array acchits; int hitplane; bool busted; Vector3 bustdir; int busttics, delay, bustmax; double cutheight; // cached Vector3 boundsmin, boundsmax, step; Array polygrid; override void Tick() { if ( busted ) { busttics++; if ( busttics > bustmax ) { Destroy(); return; } SpawnDebris(); return; } // fade out damage if ( delay > 0 ) { delay--; return; } accdamage = int(accdamage*.9-5); if ( accdamage <= 0 ) { Destroy(); return; } } private void SpawnDebris( bool initial = false ) { if ( hitpoly ) { SpawnDebrisPoly(initial); return; } double x, y, z; for ( z=boundsmin.z; z (bustmax/2)) ) continue; int numpt = Random[Wallbuster](-4,1); for ( int i=0; i (bustmax/2)) ) continue; int numpt = Random[Wallbuster](-4,1); for ( int i=0; i= ceil ) faketracer.Results.Tier = TIER_Upper; else if ( (a.pos.z+a.Height) <= flor ) faketracer.Results.Tier = TIER_Lower; } } else return false; // nothing busted return Bust(faketracer.Results,accdamage,a.target,x,a.pos.z+a.Height/2.); } static bool BustLinetrace( FLineTraceData d, int accdamage, Actor instigator, Vector3 x, double hitz ) { LineTracer faketracer = new("LineTracer"); faketracer.Results.HitType = d.HitType; faketracer.Results.HitSector = d.HitSector; faketracer.Results.HitLine = d.HitLine; faketracer.Results.ffloor = d.Hit3DFloor; faketracer.Results.Side = d.LineSide; faketracer.Results.Tier = (d.LinePart==Side.Top)?TIER_UPPER:(d.LinePart==Side.Bottom)?TIER_LOWER:TIER_Middle; return Bust(faketracer.Results,accdamage,instigator,x,hitz); } static bool BustPolyobj( swwm_PolyobjectHandle p, int accdamage, Actor instigator, Vector3 x ) { let ti = ThinkerIterator.Create("BusterWall",STAT_USER); BusterWall iter, bust = null; while ( iter = BusterWall(ti.Next()) ) { if ( iter.hitpoly != p ) continue; bust = iter; break; } bool mnew = false; if ( !bust ) { bust = new("BusterWall"); bust.ChangeStatNum(STAT_USER); bust.hitpoly = p; bust.accdamage = 0; bust.bustdir = x; mnew = true; } // multiply damage if ( instigator ) accdamage = instigator.GetModifiedDamage('Wallbust',accdamage,false,instigator,instigator,0); bust.delay = max(bust.delay,5+min(20,accdamage>>4)); bust.accdamage += accdamage; bust.acchits.Push(accdamage); bust.bustdir = (bust.bustdir+x)*.5; // skip if already busted if ( bust.busted ) return true; // not enough total damage if ( bust.accdamage < 100 ) return false; // estimate polyobject volume Vector3 a = (32767,32767,32767), b = (-32768,-32768,-32768); foreach ( l:p.Lines ) { if ( l.v1.p.x < a.x ) a.x = l.v1.p.x; if ( l.v2.p.x < a.x ) a.x = l.v2.p.x; if ( l.v1.p.y < a.y ) a.y = l.v1.p.y; if ( l.v2.p.y < a.y ) a.y = l.v2.p.y; if ( l.v1.p.x > b.x ) b.x = l.v1.p.x; if ( l.v2.p.x > b.x ) b.x = l.v2.p.x; if ( l.v1.p.y > b.y ) b.y = l.v1.p.y; if ( l.v2.p.y > b.y ) b.y = l.v2.p.y; Sector s = level.PointInSector(l.v1.p); double fz = s.floorplane.ZAtPoint(l.v1.p); double cz = s.ceilingplane.ZAtPoint(l.v1.p); if ( fz < a.z ) a.z = fz; if ( cz > b.z ) b.z = cz; s = level.PointInSector(l.v2.p); fz = s.floorplane.ZAtPoint(l.v2.p); cz = s.ceilingplane.ZAtPoint(l.v2.p); if ( fz < a.z ) a.z = fz; if ( cz > b.z ) b.z = cz; } double girthitude = (b.x-a.x)*(b.y-a.y)*(b.z-a.z); // do a grid check to approximate "real" volume double ystep = (b.y-a.y)/64.; double xstep = (b.x-a.x)/64.; int inspot = 0, allspot = 0; for ( double y=a.y; y<=b.y; y+=ystep ) for ( double x=a.x; x<=b.x; x+=xstep ) { allspot++; if ( !SWWMUtility.PointInPolyobj((x,y),p) ) continue; inspot++; } if ( allspot <= 0 ) return false; // huh??? girthitude = (girthitude*inspot)/allspot; // too huge if ( (girthitude > 16777216) || (max(b.z-a.z,max(b.x-a.x,b.y-a.y)) > 1024) ) return false; // not strong enough to bust if ( bust.accdamage < girthitude/300. ) return false; // report bust if ( Instigator && Instigator.player ) { let s = SWWMStats.Find(Instigator.player); if ( s ) s.busts++; SWWMUtility.AchievementProgressInc("bustin",1,Instigator.player); if ( (Instigator is 'Demolitionist') && !Random[DemoLines](0,3) && (int(girthitude**.15) >= 3) ) { if ( gametic > Demolitionist(Instigator).lastbust ) SWWMHandler.AddOneliner("bustkill",2,50); Demolitionist(Instigator).lastbust = gametic+350; } } // call hit fx for devastation sigil (if any) AngeryPower as = instigator?AngeryPower(instigator.FindInventory("AngeryPower")):null; if ( as ) as.DoHitFX(); bust.busted = true; bust.busttics = 0; bust.bustmax = min(30,int(12+girthitude**.1)); // quakin' let q = Actor.Spawn("BustedQuake",(p.LastPos.x,p.LastPos.y,(b.z+a.z)/2)); q.special1 = clamp(int(girthitude**.15),1,9); // "precache" the grid for busting effects bust.boundsmin = a; bust.boundsmax = b; bust.step = (clamp((b.x-a.x)/4.,2.,32.),clamp((b.y-a.y)/4.,2.,32.),clamp((b.z-a.z)/4.,2.,32.)); for ( double y=a.y; y<=b.y; y+=bust.step.y ) for ( double x=a.x; x<=b.x; x+=bust.step.x ) { if ( !SWWMUtility.PointInPolyobj((x,y),p) ) continue; let g = new("BustPoint"); g.pos = (x,y); bust.polygrid.Push(g); } // stop any polyobject movement level.ExecuteSpecial(Polyobj_Stop,instigator,p.StartLine,Line.Front,p.PolyobjectNum); if ( p.Mirror ) level.ExecuteSpecial(Polyobj_Stop,instigator,p.Mirror.StartLine,Line.Front,p.Mirror.PolyobjectNum); // send it to the shadow realm (and ensure it stays there) if ( !p.FindEffector("SWWMBustedPolyobj") ) { let yeet = new("SWWMBustedPolyobj"); yeet.whomstdve = instigator; p.AddEffector(yeet); } bust.SpawnDebris(true); // damnums Vector3 bcenter = (bust.boundsmin+bust.boundsmax)*.5; foreach ( hit:bust.acchits ) SWWMDamNum.Spawn(hit,level.Vec3Offset(bcenter,SWWMUtility.Vec3FromAngles(FRandom[ScoreBits](0,360),FRandom[ScoreBits](-90,90))*8.),'Wallbust'); return true; } static bool Bust( TraceResults d, int accdamage, Actor instigator, Vector3 x, double hitz ) { // we can't blow up 3D floors if ( d.ffloor ) return false; Sector hs = d.HitSector; int hp; if ( d.HitType == TRACE_HitWall ) { // check if it's a polyobject line, if so, switch to the other bust method swwm_PolyObjectHandle p; if ( SWWMUtility.IsPolyLine(d.HitLine,p) ) return BustPolyobj(p,accdamage,instigator,x); // no busting the goat if ( IsIOSWall(d.HitLine) ) return false; // onesided wall? no bust if ( !d.HitLine.sidedef[1] ) return false; // sector is opposite of side hit hs = d.HitLine.sidedef[!d.Side].sector; // what part we hit? if ( d.Tier == TIER_Upper ) hp = 1; // ceiling else if ( d.Tier == TIER_Lower ) hp = 0; // floor else return false; // middle ignored } else if ( d.HitType == TRACE_HitCeiling ) { // no busting the goat foreach ( l:hs.Lines ) { if( IsIOSWall(l) ) return false; } hp = 1; } else if ( d.HitType == TRACE_HitFloor ) { // no busting the goat foreach ( l:hs.Lines ) { if( IsIOSWall(l) ) return false; } hp = 0; } else return false; // this isn't a valid hit, needs to be world geometry bool bBustable = (level.GetUDMFInt(level.UDMF_Sector,hs.Index(),'BUSTABLE')>0); bool bUnbustable = (level.GetUDMFInt(level.UDMF_Sector,hs.Index(),'UNBUSTABLE')>0); // Check if it's a door if ( !swwm_cbtall && (bUnbustable || (!bBustable && !SWWMUtility.IsDoorSector(hs,hp))) ) return false; let ti = ThinkerIterator.Create("BusterWall",STAT_USER); BusterWall iter, bust = null; while ( iter = BusterWall(ti.Next()) ) { if ( (iter.hitsector != hs) || (iter.hitplane != hp) ) continue; bust = iter; break; } bool mnew = false; if ( !bust ) { bust = new("BusterWall"); bust.ChangeStatNum(STAT_USER); bust.hitsector = hs; bust.accdamage = 0; bust.hitplane = hp; bust.bustdir = x; mnew = true; } // multiply damage if ( instigator ) accdamage = instigator.GetModifiedDamage('Wallbust',accdamage,false,instigator,instigator,0); bust.delay = max(bust.delay,5+min(20,accdamage>>4)); bust.accdamage += accdamage; bust.acchits.Push(accdamage); bust.bustdir = (bust.bustdir+x)*.5; double extracut = FRandom[Wallbuster](.01,.04)*bust.accdamage; // is this actually sticking out? double thisheight, othersheight, partheight, cutheight; if ( hp ) { thisheight = hs.FindLowestCeilingPoint(); othersheight = hs.FindHighestCeilingSurrounding(); if ( (thisheight-othersheight) >= 0. ) return false; cutheight = min(hitz+extracut,othersheight+4); } else { thisheight = hs.FindHighestFloorPoint(); othersheight = hs.FindLowestFloorSurrounding(); if ( (thisheight-othersheight) <= 0. ) return false; cutheight = max(hitz-extracut,othersheight-4); } if ( hp ) bust.cutheight = mnew?cutheight:max(bust.cutheight,cutheight); else bust.cutheight = mnew?cutheight:min(bust.cutheight,cutheight); partheight = abs(thisheight-bust.cutheight); // skip if we don't cut off enough if ( partheight <= 0. ) return false; // skip if already busted if ( bust.busted ) return true; // not enough total damage if ( bust.accdamage < 100 ) return false; // estimate sector volume Vector2 a = (32767,32767), b = (-32768,-32768); foreach ( l:hs.Lines ) { if ( l.v1.p.x < a.x ) a.x = l.v1.p.x; if ( l.v2.p.x < a.x ) a.x = l.v2.p.x; if ( l.v1.p.y < a.y ) a.y = l.v1.p.y; if ( l.v2.p.y < a.y ) a.y = l.v2.p.y; if ( l.v1.p.x > b.x ) b.x = l.v1.p.x; if ( l.v2.p.x > b.x ) b.x = l.v2.p.x; if ( l.v1.p.y > b.y ) b.y = l.v1.p.y; if ( l.v2.p.y > b.y ) b.y = l.v2.p.y; } double girthitude = (b.x-a.x)*(b.y-a.y)*partheight; // do a grid check to approximate "real" volume, useful for diagonal doors double ystep = (b.y-a.y)/64.; double xstep = (b.x-a.x)/64.; int inspot = 0, allspot = 0; for ( double y=a.y; y<=b.y; y+=ystep ) for ( double x=a.x; x<=b.x; x+=xstep ) { allspot++; if ( level.PointInSector((x,y)) == hs ) inspot++; } if ( allspot <= 0 ) return false; // huh??? girthitude = (girthitude*inspot)/allspot; // too huge if ( (girthitude > 16777216) || (max(partheight,max(b.x-a.x,b.y-a.y)) > 1024) ) return false; // not strong enough to bust if ( bust.accdamage < girthitude/300. ) return false; // report bust if ( Instigator && Instigator.player ) { let s = SWWMStats.Find(Instigator.player); if ( s ) s.busts++; SWWMUtility.AchievementProgressInc("bustin",1,Instigator.player); if ( (Instigator is 'Demolitionist') && !Random[DemoLines](0,3) && (int(girthitude**.15) >= 3) ) { if ( gametic > Demolitionist(Instigator).lastbust ) SWWMHandler.AddOneliner("bustkill",2,50); Demolitionist(Instigator).lastbust = gametic+350; } } // call hit fx for devastation sigil (if any) AngeryPower as = instigator?AngeryPower(instigator.FindInventory("AngeryPower")):null; if ( as ) as.DoHitFX(); bust.busted = true; bust.busttics = 0; bust.bustmax = min(30,int(12+girthitude**.1)); // shush hs.flags |= Sector.SECF_SILENTMOVE; // filler texture TextureID rubble = TexMan.CheckForTexture("ASHWALL2"); // equivalents for other iwads if ( !rubble.IsValid() ) rubble = TexMan.CheckForTexture("ASHWALL"); if ( !rubble.IsValid() ) rubble = TexMan.CheckForTexture("LOOSERCK"); if ( !rubble.IsValid() ) rubble = TexMan.CheckForTexture("WASTE03"); if ( !rubble.IsValid() ) rubble = TexMan.CheckForTexture("textures/DefaultTexture.png"); // a fun little fallback should none of those exist // activate all shoot/use specials (not locked) associated with this sector's two-sided lines foreach ( l:hs.Lines ) { int locknum = SWWMUtility.GetLineLock(l); if ( locknum && (!instigator || !instigator.CheckKeys(locknum,false,true)) ) continue; if ( !l.sidedef[1] ) continue; int away = 0; if ( l.sidedef[0].sector == hs ) away = 1; l.Activate(instigator,away,SPAC_Use); l.Activate(instigator,away,SPAC_Impact); } // if this is a broken crusher, we need to clear that now that we've busted it SWWMCrusherBroken.Remove(hp?null:hs,hp?hs:null); // quakin' let q = Actor.Spawn("BustedQuake",(hs.centerspot.x,hs.centerspot.y,thisheight)); q.special1 = clamp(int(girthitude**.15),1,9); if ( hp ) { // remove any current movers if ( hs.CeilingData ) hs.CeilingData.Destroy(); // blow up that ceiling hs.MoveCeiling(abs(partheight),bust.cutheight,0,1,false); bust.boundsmin = (a.x,a.y,thisheight)+(1,1,1); bust.boundsmax = (b.x,b.y,bust.cutheight)-(1,1,1); // prevent any further ceiling movement level.CreateCeiling(hs,Ceiling.ceilRaiseByValue,null,0.,0.,1.); hs.StopSoundSequence(CHAN_VOICE); } else { // remove any current movers if ( hs.FloorData ) hs.FloorData.Destroy(); // blow up that floor hs.MoveFloor(abs(partheight),abs(bust.cutheight),0,-1,false,true); bust.boundsmin = (a.x,a.y,bust.cutheight)+(1,1,1); bust.boundsmax = (b.x,b.y,thisheight)-(1,1,1); // prevent any further floor movement level.CreateFloor(hs,Floor.floorLowerByValue,null,0.,1.); hs.StopSoundSequence(CHAN_WEAPON); } bust.step = (clamp((b.x-a.x)/4.,2.,32.),clamp((b.y-a.y)/4.,2.,32.),clamp(partheight/4.,2.,32.)); bust.SpawnDebris(true); hs.SetTexture(hp,rubble); hs.SetXScale(hp,1.); hs.SetYScale(hp,1.); hs.SetAngle(hp,0.); foreach ( l:hs.Lines ) { if ( !l.sidedef[1] ) { if ( hp && !(l.flags&Line.ML_DONTPEGBOTTOM) ) l.sidedef[0].AddTextureYOffset(1,-partheight); // shift down else if ( !hp && (l.flags&Line.ML_DONTPEGBOTTOM) ) l.sidedef[0].AddTextureYOffset(1,partheight); // shift up continue; } int away = 0; if ( l.sidedef[0].sector == hs ) away = 1; for ( int j=0; j<2; j++ ) { if ( l.sidedef[j].GetTexture(hp?0:2).IsValid() ) { if ( j == away ) { if ( hp && !(l.flags&Line.ML_DONTPEGTOP) ) l.sidedef[j].AddTextureYOffset(0,-partheight); // shift down else if ( !hp && !(l.flags&Line.ML_DONTPEGBOTTOM) ) l.sidedef[j].AddTextureYOffset(2,partheight); // shift up } } else { l.sidedef[j].SetTexture(hp?0:2,rubble); l.sidedef[j].SetTextureXScale(hp?0:2,1.); l.sidedef[j].SetTextureYScale(hp?0:2,1.); } } } // damnums Vector3 bcenter = (bust.boundsmin+bust.boundsmax)*.5; foreach( hit:bust.acchits ) SWWMDamNum.Spawn(hit,level.Vec3Offset(bcenter,SWWMUtility.Vec3FromAngles(FRandom[ScoreBits](0,360),FRandom[ScoreBits](-90,90))*8.),'Wallbust'); return true; } }