171 lines
7.2 KiB
Text
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;
|
|
}
|
|
}
|