// Misc. Utility 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 }; const FallbackTag = "AWESOME IT'S PENIS"; // used on tag processing, please don't mind the actual string used) Class SWWMUtility { // thanks zscript static clearscope double fract( double a ) { return a-floor(a); } // not sure if I should use this, looks a bit ugly static clearscope void ThousandsStr( out String s ) { String nstr = s; s.Truncate(0); int len = nstr.CodePointCount(); int t = len; if ( nstr.Left(1) == "-" ) t++; for ( int i=0, pos=0; i=0; i-- ) { int d = int(val/(10**i))%10; str.AppendCharacter(digs[d]); } return str; } static clearscope void BeautifyClassName( out String str ) { String workstr = str; str.Truncate(0); workstr.Replace("_"," "); int len = workstr.CodePointCount(); for ( int i=0, pos=0; i 0 ) return -asin(diff.z/dist); return 0; } static clearscope int GetLineLock( Line l ) { int locknum = l.locknumber; if ( !locknum ) { // check the special switch ( l.special ) { case FS_Execute: locknum = l.Args[2]; break; case Door_LockedRaise: case Door_Animated: locknum = l.Args[3]; break; case ACS_LockedExecute: case ACS_LockedExecuteDoor: case Generic_Door: locknum = l.Args[4]; break; } } return locknum; } static clearscope bool IsExitLine( Line l ) { if ( (l.special == Exit_Normal) || (l.special == Exit_Secret) || (l.special == Teleport_EndGame) || (l.special == Teleport_NewMap) ) return true; return false; } // how the fuck is this not available to ZScript? // copied from P_PointOnLineSidePrecise() static clearscope int PointOnLineSide( Vector2 p, Line l ) { if ( !l ) return 0; return (((p.y-l.v1.p.y)*l.delta.x+(l.v1.p.x-p.x)*l.delta.y) > double.epsilon); } // haha another one // copied from BoxOnLineSide() static clearscope int BoxOnLineSide( double top, double bottom, double left, double right, Line l ) { if ( !l ) return 0; int p1, p2; if ( l.delta.x == 0 ) { // ST_VERTICAL: p1 = (right < l.v1.p.x); p2 = (left < l.v1.p.x); if ( l.delta.y < 0 ) { p1 ^= 1; p2 ^= 1; } } else if ( l.delta.y == 0 ) { // ST_HORIZONTAL: p1 = (top > l.v1.p.y); p2 = (bottom > l.v1.p.y); if ( l.delta.x < 0 ) { p1 ^= 1; p2 ^= 1; } } else if ( (l.delta.x*l.delta.y) >= 0 ) { // ST_POSITIVE: p1 = PointOnLineSide((left,top),l); p2 = PointOnLineSide((right,bottom),l); } else { // ST_NEGATIVE: p1 = PointOnLineSide((right,top),l); p2 = PointOnLineSide((left,bottom),l); } return (p1==p2)?p1:-1; } // wrapper static clearscope int ActorOnLineSide( Actor a, Line l ) { double box[4]; box[0] = a.pos.y+a.radius; box[1] = a.pos.y-a.radius; box[2] = a.pos.x-a.radius; box[3] = a.pos.x+a.radius; return BoxOnLineSide(box[0],box[1],box[2],box[3],l); } // box intersection check, for collision detection static clearscope bool BoxIntersect( Actor a, Actor b, Vector3 ofs = (0,0,0), int pad = 0 ) { Vector3 diff = level.Vec3Diff(level.Vec3Offset(a.pos,ofs),b.pos); if ( (abs(diff.x) > (a.radius+b.radius+pad)) || (abs(diff.y) > (a.radius+b.radius+pad)) ) return false; if ( (diff.z > a.height+pad) || (diff.z < -(b.height+pad)) ) return false; return true; } // extruded box intersection check, useful when checking things that might be hit along a path static clearscope bool ExtrudeIntersect( Actor a, Actor b, Vector3 range, int steps, int pad = 0 ) { if ( steps <= 0 ) return BoxIntersect(a,b,pad:pad); double step = 1./steps; for ( double i=step; i<=1.; i+=step ) { if ( BoxIntersect(a,b,range*i,pad) ) return true; } return false; } // sphere intersection check, useful for proximity detection static clearscope bool SphereIntersect( Actor a, Vector3 p, double radius ) { Vector3 ap = p+level.Vec3Diff(p,a.pos); // portal-relative actor position Vector3 amin = ap+(-a.radius,-a.radius,0), amax = ap+(a.radius,a.radius,a.height); double distsq = 0.; if ( p.x < amin.x ) distsq += (amin.x-p.x)**2; if ( p.x > amax.x ) distsq += (p.x-amax.x)**2; if ( p.y < amin.y ) distsq += (amin.y-p.y)**2; if ( p.y > amax.y ) distsq += (p.y-amax.y)**2; if ( p.z < amin.z ) distsq += (amin.z-p.z)**2; if ( p.z > amax.z ) distsq += (p.z-amax.z)**2; return (distsq <= (radius**2)); } // THANKS FOR NOT GIVING US ANY OTHER WAY TO CHECK IF A LOCK NUMBER IS VALID static clearscope bool IsValidLockNum( int l ) { if ( (l < 1) || (l > 255) ) return true; Array valid; valid.Clear(); for ( int i=0; i lines; lines.Clear(); data.Split(lines,"\n"); for ( int j=0; j spl; spl.Clear(); lines[j].Split(spl," ",TOK_SKIPEMPTY); // check game string (if any) if ( spl.Size() > 2 ) { if ( (spl[2] ~== "DOOM") && !(gameinfo.gametype&GAME_Doom) ) continue; else if ( (spl[2] ~== "HERETIC") && !(gameinfo.gametype&GAME_Heretic) ) continue; else if ( (spl[2] ~== "HEXEN") && !(gameinfo.gametype&GAME_Hexen) ) continue; else if ( (spl[2] ~== "STRIFE") && !(gameinfo.gametype&GAME_Strife) ) continue; else if ( (spl[2] ~== "CHEX") && !(gameinfo.gametype&GAME_Chex) ) continue; } valid.Push(spl[1].ToInt()); } } } for ( int i=0; i= 10) && (l.special <= 13)) || (!part && (l.special >= 20) && (l.special <= 25)) || (!part && (l.special == 28)) || ((l.special >= 29) && (l.special <= 30)) || (!part && (l.special >= 35) && (l.special <= 37)) || (part && (l.special >= 40) && (l.special <= 45)) || (!part && (l.special == 46)) || (part && (l.special == 47)) || (!part && (l.special >= 60) && (l.special <= 68)) || (part && (l.special == 69)) || ((l.special >= 94) && (l.special <= 96)) || (part && (l.special == 97)) || (!part && (l.special == 99)) || (part && (l.special == 104)) || (part && (l.special >= 105) && (l.special <= 106)) || (part && (l.special >= 168) && (l.special <= 169)) || (!part && (l.special == 172)) || (part && (l.special >= 192) && (l.special <= 199)) || (!part && (l.special == 200)) || (part && (l.special >= 201) && (l.special <= 202)) || (!part && (l.special == 203)) || (part && (l.special == 205)) || (!part && (l.special >= 206) && (l.special <= 207)) || (!part && (l.special == 228)) || (!part && (l.special >= 230) && (l.special <= 231)) || (!part && (l.special >= 238) && (l.special <= 242)) || ((l.special >= 245) && (l.special <= 247)) || (part && (l.special == 249)) || (!part && (l.special >= 250) && (l.special <= 251)) || (part && (l.special >= 251) && (l.special <= 255)) || (!part && (l.special >= 256) && (l.special <= 261)) || (part && (l.special >= 262) && (l.special <= 269)) || (!part && (l.special == 275)) || (part && (l.special == 276)) || (!part && (l.special == 279)) || (part && (l.special == 280)) ) { let si = level.CreateSectorTagIterator(l.Args[0],l); int idx; while ( (idx = si.Next()) != -1 ) if ( level.Sectors[idx] == s ) return true; } } let ti = ThinkerIterator.Create("Actor"); Actor a; while ( a = Actor(ti.Next()) ) { if ( !a.special || !a.Args[0] ) continue; if ( (part && (a.special >= 10) && (a.special <= 13)) || (!part && (a.special >= 20) && (a.special <= 25)) || (!part && (a.special == 28)) || ((a.special >= 29) && (a.special <= 30)) || (!part && (a.special >= 35) && (a.special <= 37)) || (part && (a.special >= 40) && (a.special <= 45)) || (!part && (a.special == 46)) || (part && (a.special == 47)) || (!part && (a.special >= 60) && (a.special <= 68)) || (part && (a.special == 69)) || ((a.special >= 94) && (a.special <= 96)) || (part && (a.special == 97)) || (!part && (a.special == 99)) || (part && (a.special == 104)) || (part && (a.special >= 105) && (a.special <= 106)) || (part && (a.special >= 168) && (a.special <= 169)) || (!part && (a.special == 172)) || (part && (a.special >= 192) && (a.special <= 199)) || (!part && (a.special == 200)) || (part && (a.special >= 201) && (a.special <= 202)) || (!part && (a.special == 203)) || (part && (a.special == 205)) || (!part && (a.special >= 206) && (a.special <= 207)) || (!part && (a.special == 228)) || (!part && (a.special >= 230) && (a.special <= 231)) || (!part && (a.special >= 238) && (a.special <= 242)) || ((a.special >= 245) && (a.special <= 247)) || (part && (a.special == 249)) || (!part && (a.special >= 250) && (a.special <= 251)) || (part && (a.special >= 251) && (a.special <= 255)) || (!part && (a.special >= 256) && (a.special <= 261)) || (part && (a.special >= 262) && (a.special <= 269)) || (!part && (a.special == 275)) || (part && (a.special == 276)) || (!part && (a.special == 279)) || (part && (a.special == 280)) ) { let si = level.CreateSectorTagIterator(a.Args[0]); int idx; while ( (idx = si.Next()) != -1 ) if ( level.Sectors[idx] == s ) return true; } } return false; } // because GetTag() returns the localized string, we need to do things the hard way static clearscope String GetFunTag( Actor a, String defstr = "" ) { if ( a is 'SWWMMonster' ) return SWWMMonster(a).GetFunTag(defstr); int ntags = 1; String basetag = ""; switch ( a.GetClassName() ) { // Doom case 'ZombieMan': case 'StealthZombieMan': basetag = "ZOMBIE"; break; case 'ShotgunGuy': case 'StealthShotgunGuy': basetag = "SHOTGUN"; break; case 'ChaingunGuy': case 'StealthChaingunGuy': basetag = "HEAVY"; break; case 'DoomImp': case 'StealthDoomImp': basetag = "IMP"; break; case 'Demon': case 'StealthDemon': basetag = "DEMON"; break; case 'Spectre': basetag = "SPECTRE"; break; case 'LostSoul': basetag = "LOST"; break; case 'Cacodemon': case 'StealthCacodemon': basetag = "CACO"; break; case 'HellKnight': case 'StealthHellKnight': basetag = "HELL"; break; case 'BaronOfHell': case 'StealthBaron': basetag = "BARON"; break; case 'Arachnotron': case 'StealthArachnotron': basetag = "ARACH"; break; case 'PainElemental': basetag = "PAIN"; break; case 'Revenant': case 'StealthRevenant': basetag = "REVEN"; break; case 'Fatso': case 'StealthFatso': basetag = "MANCU"; break; case 'Archvile': case 'StealthArchvile': basetag = "ARCH"; break; case 'SpiderMastermind': basetag = "SPIDER"; break; case 'Cyberdemon': basetag = "CYBER"; break; case 'BossBrain': basetag = "BOSSBRAIN"; break; case 'WolfensteinSS': basetag = "WOLFSS"; break; case 'CommanderKeen': case 'SWWMHangingKeen': basetag = "KEEN"; break; case 'MBFHelperDog': basetag = "DOG"; break; // Heretic case 'Chicken': basetag = "CHICKEN"; break; case 'Beast': basetag = "BEAST"; break; case 'Clink': basetag = "CLINK"; break; case 'Sorcerer1': case 'Sorcerer2': basetag = "DSPARIL"; break; case 'HereticImp': case 'HereticImpLeader': basetag = "HERETICIMP"; break; case 'Ironlich': basetag = "IRONLICH"; break; case 'Knight': case 'KnightGhost': basetag = "BONEKNIGHT"; break; case 'Minotaur': case 'MinotaurFriend': basetag = "MINOTAUR"; break; case 'Mummy': case 'MummyGhost': basetag = "MUMMY"; break; case 'MummyLeader': case 'MummyLeaderGhost': basetag = "MUMMYLEADER"; break; case 'Snake': basetag = "SNAKE"; break; case 'Wizard': basetag = "WIZARD"; break; // Hexen case 'FireDemon': basetag = "FIREDEMON"; break; case 'Demon1': case 'Demon1Mash': case 'Demon2': case 'Demon2Mash': basetag = "DEMON1"; break; case 'Ettin': case 'EttinMash': basetag = "ETTIN"; break; case 'Centaur': case 'CentaurMash': basetag = "CENTAUR"; break; case 'CentaurLeader': basetag = "SLAUGHTAUR"; break; case 'Bishop': basetag = "BISHOP"; break; case 'IceGuy': basetag = "ICEGUY"; break; case 'Serpent': case 'SerpentLeader': basetag = "SERPENT"; break; case 'Wraith': case 'WraithBuried': basetag = "WRAITH"; break; case 'Dragon': basetag = "DRAGON"; break; case 'Korax': basetag = "KORAX"; break; case 'FighterBoss': basetag = "FBOSS"; break; case 'MageBoss': basetag = "MBOSS"; break; case 'ClericBoss': basetag = "CBOSS"; break; case 'Heresiarch': basetag = "HERESIARCH"; break; } if ( basetag == "" ) return a.GetTag(defstr); String funtag = "FN_"..basetag.."_FUN"; String lfuntag = StringTable.Localize(funtag,false); if ( lfuntag != funtag ) return lfuntag; String nfuntag = "FN_"..basetag.."_FUNN"; String lnfuntag = StringTable.Localize(nfuntag,false); if ( lnfuntag == nfuntag ) return a.GetTag(defstr); ntags = lnfuntag.ToInt(); return StringTable.Localize(String.Format("$FN_%s_FUN%d",basetag,Random[FunTags](1,ntags))); } // Apply full 3D knockback in a specific direction, useful for hitscan static play void DoKnockback( Actor Victim, Vector3 HitDirection, double MomentumTransfer ) { 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,.1*Momentum.length()); Momentum /= Thinker.TICRATE*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 ) { // debug, display radius sphere if ( swwm_debugblast ) { let s = Actor.Spawn("RadiusDebugSphere",Source.pos); s.Scale *= ExplosionRadius; s.SetShade((Damage>0)?"Green":"Blue"); if ( FullDamageRadius > 0. ) { let s = Actor.Spawn("RadiusDebugSphere",Source.pos); s.Scale *= FullDamageRadius; s.SetShade("Red"); } } if ( !(flags&DE_NOSPLASH) ) Source.CheckSplash(ExplosionRadius); double brange = 1./(ExplosionRadius-FullDamageRadius); Actor Instigator = (flags&DE_NOTMISSILE)?Source:Source.target; BlockThingsIterator bi = BlockThingsIterator.Create(Source,ExplosionRadius*2); // test with doubled radius, just to be sure int nhit = 0, nkill = 0; while ( bi.Next() ) { Actor a = bi.Thing; // 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; // can we see it if ( !(flags&DE_THRUWALLS) && !Source.CheckSight(a,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) continue; // intersecting? if ( !SWWMUtility.SphereIntersect(a,Source.pos,ExplosionRadius) ) continue; // calculate factor Vector3 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 = (cos(ang)*cos(pt),sin(ang)*cos(pt),-sin(pt)); } dir /= dist; dist = clamp(dist-FullDamageRadius,0,min(dist,ExplosionRadius)); double 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 /= Thinker.TICRATE*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 nhit++; int dmg = int(Damage*damagescale); if ( dmg <= 0 ) continue; // no harm int ndmg = a.DamageMobj(Source,Instigator,dmg,(DamageType=='')?Source.DamageType:DamageType,DMG_EXPLOSION,atan2(-dir.y,-dir.x)); if ( a && !(flags&DE_NOBLEED) ) a.TraceBleed((ndmg>0)?ndmg:dmg,Source); if ( (flags&DE_HOWL) && a && a.bISMONSTER && !Random[DoBlast](0,3) ) a.Howl(); if ( !a || (a.Health <= 0) ) nkill++; } return nhit, nkill; } static play bool InPlayerFOV( PlayerInfo p, Actor a, double maxdist = 0. ) { double vfov = p.fov*.5; double hfov = atan(Screen.GetAspectRatio()*tan(vfov)); let mo = p.camera; Vector3 pp; if ( !mo.CheckSight(a,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) return false; if ( mo is 'PlayerPawn' ) pp = mo.Vec2OffsetZ(0,0,PlayerPawn(mo).player.viewz); else pp = mo.Vec3Offset(0,0,mo.CameraHeight); Vector3 sc = level.SphericalCoords(pp,a.pos,(mo.angle,mo.pitch)); if ( (abs(sc.x) > hfov) || (abs(sc.y) > vfov) ) return false; if ( (maxdist > 0.) && (sc.z < maxdist) ) return false; return true; } static clearscope bool CheatsDisabled( int p = -1 ) { if ( cl_blockcheats || ((G_SkillPropertyInt(SKILLP_DisableCheats) || netgame || deathmatch) && !sv_cheats) ) { if ( (p != -1) && (p == consoleplayer) ) { Console.Printf("\cxSORRY NOTHING\c-"); S_StartSound("misc/trombone",CHAN_VOICE,CHANF_UI); } return true; } return false; } static clearscope bool IdentifyingDog( Actor a ) { if ( a is 'MBFHelperDog' ) return true; // reminder that mark is a terrible person if ( a.GetClassName() == 'GermanDog' ) return true; if ( a.GetClassName() == '64HellHound' ) return true; // more dogs will be added as found // because all dogs must be pet return false; } static clearscope bool IdentifyingCaco( Actor a ) { if ( a is 'DeadCacodemon' ) return false; if ( a is 'Cacodemon' ) return true; if ( a.Species == 'RLCacodemon' ) return true; // DRLA if ( a.Species == 'Caco' ) return true; // CH return false; } // Друг static clearscope bool IdentifyingDrug( Actor a ) { if ( a is 'Beast' ) return true; return false; } static clearscope bool IdentifyingDoubleBoi( Actor a ) { if ( a is 'Ettin' ) return true; return false; } } Class RadiusDebugSphere : Actor { Default { RenderStyle "AddStencil"; StencilColor "White"; Radius .1; Height 0.; +NOGRAVITY; +NOINTERACTION; } States { Spawn: XZW1 A 1 BRIGHT A_FadeOut(); Wait; } } Class ShinemapDebugSphere : Actor { override bool Used( Actor user ) { if ( CurState.NextState ) SetState(CurState.NextState); else SetState(SpawnState); return true; } override void Tick() {} Default { RenderStyle "Add"; Radius 16; Height 48; } States { Spawn: XZW1 A -1 Bright NoDelay A_SetRenderStyle(1.,STYLE_Add); XZW1 B -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 C -1 Bright A_SetRenderStyle(1.,STYLE_Add); XZW1 D -1 Bright A_SetRenderStyle(1.,STYLE_Normal); XZW1 E -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 F -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 G -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 H -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 I -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 J -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 K -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 L -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 M -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 N -1 A_SetRenderStyle(1.,STYLE_Normal); Loop; } }