swwmgz_m/zscript/utility/swwm_utility_blast.zsc

171 lines
7.2 KiB
Text

// 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;
int nhit = 0, nkill = 0;
bool haskilled = false;
Array<Actor> hitlist;
hitlist.Clear();
// gather first
foreach ( s:level.Sectors ) for ( Actor a=s.thinglist; a; a=a.snext )
{
// early checks for self and ignored actor (usually the instigator)
if ( (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;
hitlist.Push(a);
}
foreach ( a:hitlist )
{
if ( !a ) continue; // this can happen, yes
// 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);
}
else 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++;
}
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;
}
}