// explosion/knockback code enum EDoExplosionFlags { DE_BLAST = 1, // sets BLASTED flag on pushed actors DE_NOBLEED = 2, // does not spawn blood decals on hit DE_NOSPLASH = 4, // like XF_NOSPLASH DE_THRUWALLS = 8, // damages through geometry (no sight check) DE_NOTMISSILE = 16, // instigator is the source itself (normally it'd be its target pointer) DE_EXTRAZTHRUST = 32, // applies a higher Z thrust to enemies on ground DE_HOWL = 64, // 25% chance for hit enemies to howl DE_COUNTENEMIES = 128, // only count hits for hostiles DE_COUNTSTEALTH = 256, // only count hits for inactive monsters DE_COUNTFHKILLS = 512, // only count kills for enemies that were at full health DE_NOHURTFRIEND = 1024, // splash damage will not affect allies DE_CENTERHEIGHT = 2048, // origin of explosion is at the center height of the source actor, rather than its base DE_NONEXPLOSIVE = 4096, // does not count as explosive damage (DMG_EXPLOSION is not passed, and no blast taunts are used) DE_QUADRAVOL = 8192 // splash burn from a Quadravol projectile, so it'll ignite enemies instead of dealing damage }; extend Class SWWMUtility { // Apply full 3D knockback in a specific direction, useful for hitscan static play void DoKnockback( Actor Victim, Vector3 HitDirection, double MomentumTransfer, bool ExtraZThrust = false ) { if ( !Victim ) return; if ( Victim.bDORMANT ) // no dormant knockback return; if ( !Victim.bSHOOTABLE && !Victim.bVULNERABLE ) return; if ( Victim.bDONTTHRUST || (Victim.Mass >= Actor.LARGE_MASS) ) return; Vector3 Momentum = HitDirection*MomentumTransfer; if ( (Victim.pos.z <= Victim.floorz) || !Victim.TestMobjZ() ) Momentum.z = max(Momentum.z,(ExtraZThrust?.4:.1)*Momentum.length()); Momentum /= GameTicRate*max(50,Victim.Mass); Victim.vel += Momentum; } // complete spherical and more accurate replacement of A_Explode // 100% free of the buggery GZDoom's own splash damage has // returns the number of shootables hit/killed static play int, int DoExplosion( Actor Source, double Damage, double MomentumTransfer, double ExplosionRadius, double FullDamageRadius = 0., int flags = 0, Name DamageType = '', Actor ignoreme = null, int dmgflags = 0, Actor realsource = null, Actor realinflictor = null ) { FullDamageRadius = min(FullDamageRadius,ExplosionRadius); // debug, display radius sphere if ( swwm_debugblast ) { let s = Actor.Spawn("RadiusDebugSphere",(flags&DE_CENTERHEIGHT)?Source.Vec3Offset(0,0,Source.height/2):Source.pos); s.Scale *= ExplosionRadius; s.SetShade((Damage>0)?"Green":"Blue"); if ( FullDamageRadius > 0. ) { let s = Actor.Spawn("RadiusDebugSphere",(flags&DE_CENTERHEIGHT)?Source.Vec3Offset(0,0,Source.height/2):Source.pos); s.Scale *= FullDamageRadius; s.SetShade("Red"); } } if ( !(flags&DE_NOSPLASH) ) Source.CheckSplash(ExplosionRadius); double brange; // sanity checks are needed to avoid division by zero or other weirdness if ( ExplosionRadius == FullDamageRadius ) brange = 1.; else brange = 1./(ExplosionRadius-FullDamageRadius); Actor Instigator = realsource?realsource:(flags&DE_NOTMISSILE)?Source:Source.target; int dflg = ((flags&DE_NONEXPLOSIVE)?0:DMG_EXPLOSION)|dmgflags; BlockThingsIterator bi = BlockThingsIterator.Create(Source,ExplosionRadius); int nhit = 0, nkill = 0; bool haskilled = false; Array washit; washit.Clear(); while ( bi.Next() ) { Actor a = bi.Thing; washit.Push(a); // early checks for self and ignored actor (usually the instigator) if ( !a || (a == ignoreme) || (a == Source) ) continue; // can't be affected if ( !a.bSHOOTABLE && !a.bVULNERABLE ) continue; // no blasting if no radius dmg (unless forced) if ( a.bNORADIUSDMG && !Source.bFORCERADIUSDMG ) continue; // check the DONTHARMCLASS/DONTHARMSPECIES flags if ( !a.player && ((Source.bDONTHARMCLASS && (a.GetClass() == Source.GetClass())) || (Source.bDONTHARMSPECIES && (a.GetSpecies() == Source.GetSpecies()))) ) continue; // check friendliness if ( (flags&DE_NOHURTFRIEND) && Instigator && Instigator.IsFriend(a) ) continue; // can we see it if ( !(flags&DE_THRUWALLS) && !Source.CheckSight(a,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) continue; // intersecting? if ( !SphereIntersect(a,Source.pos,ExplosionRadius) ) continue; // calculate factor Vector3 dir; if ( flags&DE_CENTERHEIGHT ) dir = level.Vec3Diff(Source.Vec3Offset(0,0,Source.Height/2),a.Vec3Offset(0,0,a.Height/2)); else dir = level.Vec3Diff(Source.pos,a.Vec3Offset(0,0,a.Height/2)); double dist = dir.length(); // intersecting, randomize direction if ( dir.length() <= double.epsilon ) { double ang = FRandom[DoBlast](0,360); double pt = FRandom[DoBlast](-90,90); dir = Vec3FromAngles(ang,pt); } dir /= dist; dist = clamp(dist-FullDamageRadius,0,min(dist,ExplosionRadius)); double damagescale; if ( ExplosionRadius == FullDamageRadius ) damagescale = 1.; else damagescale = 1.-clamp((dist-a.Radius)*brange,0.,1.); double mm = MomentumTransfer*damagescale; // no knockback if massive/unpushable if ( (abs(mm) > 0.) && !a.bDORMANT && !a.bDONTTHRUST && (a.Mass < Actor.LARGE_MASS) ) { Vector3 Momentum = dir*mm; if ( (a.pos.z <= a.floorz) || !a.TestMobjZ() ) Momentum.z = max(Momentum.z,(flags&DE_EXTRAZTHRUST?.4:.1)*Momentum.length()); Momentum /= GameTicRate*max(50,a.Mass); // prevent tiny things from getting yeeted at warp speed a.vel += Momentum; if ( (flags&DE_BLAST) && a.bCANBLAST && !a.bDONTBLAST ) a.bBLASTED = true; } // hit it bool inactive = (!a.player&&!a.target); bool hostile = (Instigator&&a.IsHostile(Instigator)&&(a.bISMONSTER||a.player)); if ( (!(flags&DE_COUNTENEMIES) || hostile) && (!(flags&DE_COUNTSTEALTH) || inactive) ) nhit++; int dmg = int(Damage*damagescale); if ( flags&DE_QUADRAVOL ) { OnFire.Apply(a,Instigator,dmg); // ignite continue; } if ( dmg <= 0 ) continue; // no harm int oldhp = a.Health; int basehp = a.GetSpawnHealth(); int ndmg = a.DamageMobj(realinflictor?realinflictor:Source,Instigator,dmg,(DamageType=='')?Source.DamageType:DamageType,dflg,atan2(-dir.y,-dir.x)); if ( (ndmg > 0) && a && !(flags&DE_NOBLEED) ) a.TraceBleed(ndmg,Source); if ( (flags&DE_HOWL) && a && (a.Health > 0) && a.bISMONSTER && !Random[DoBlast](0,3) ) a.Howl(); if ( hostile && (!a || (a.Health <= 0)) ) haskilled = true; if ( (flags&DE_COUNTFHKILLS) && (oldhp < basehp) ) continue; // was not at full health if ( (!a || (a.Health <= 0)) && (!(flags&DE_COUNTENEMIES) || hostile) && (!(flags&DE_COUNTSTEALTH) || inactive) ) nkill++; } // traverse portals (needed since BlockThingsIterator can't properly cross sector portals in both vertical directions) int thisgroup = Source.CurSector.portalgroup; for ( int i=0; i 0.) && !a.bDORMANT && !a.bDONTTHRUST && (a.Mass < Actor.LARGE_MASS) ) { Vector3 Momentum = dir*mm; if ( (a.pos.z <= a.floorz) || !a.TestMobjZ() ) Momentum.z = max(Momentum.z,(flags&DE_EXTRAZTHRUST?.4:.1)*Momentum.length()); Momentum /= GameTicRate*max(50,a.Mass); // prevent tiny things from getting yeeted at warp speed a.vel += Momentum; if ( (flags&DE_BLAST) && a.bCANBLAST && !a.bDONTBLAST ) a.bBLASTED = true; } // hit it bool inactive = (!a.player&&!a.target); bool hostile = (Instigator&&a.IsHostile(Instigator)&&(a.bISMONSTER||a.player)); if ( (!(flags&DE_COUNTENEMIES) || hostile) && (!(flags&DE_COUNTSTEALTH) || inactive) ) nhit++; int dmg = int(Damage*damagescale); if ( flags&DE_QUADRAVOL ) { OnFire.Apply(a,Instigator,dmg); // ignite continue; } if ( dmg <= 0 ) continue; // no harm int oldhp = a.Health; int basehp = a.GetSpawnHealth(); int ndmg = a.DamageMobj(realinflictor?realinflictor:Source,Instigator,dmg,(DamageType=='')?Source.DamageType:DamageType,dflg,atan2(-dir.y,-dir.x)); if ( (ndmg > 0) && a && !(flags&DE_NOBLEED) ) a.TraceBleed(ndmg,Source); if ( (flags&DE_HOWL) && a && (a.Health > 0) && a.bISMONSTER && !Random[DoBlast](0,3) ) a.Howl(); if ( hostile && (!a || (a.Health <= 0)) ) haskilled = true; if ( (flags&DE_COUNTFHKILLS) && (oldhp < basehp) ) continue; // was not at full health if ( (!a || (a.Health <= 0)) && (!(flags&DE_COUNTENEMIES) || hostile) && (!(flags&DE_COUNTSTEALTH) || inactive) ) nkill++; } } if ( (Instigator is 'Demolitionist') && haskilled && !(flags&DE_NONEXPLOSIVE) ) { let hnd = SWWMHandler(EventHandler.Find("SWWMHandler")); let demo = Demolitionist(Instigator); if ( hnd && (gametic > demo.lastbang+30) && (gametic > hnd.lastcombat+10) && !Random[DemoLines](0,1) ) demo.lastbang = SWWMHandler.AddOneLiner("blast",2,10); } return nhit, nkill; } } Class RadiusDebugSphere : SWWMNonInteractiveActor { Default { RenderStyle "AddStencil"; StencilColor "White"; } States { Spawn: XZW1 A 1 BRIGHT A_FadeOut(); Wait; } }