// 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 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 }; enum EExitType { ET_Normal, ET_Secret, ET_EndGame, ET_NewMap, }; Struct SWWMProjectionData { swwm_GM_Matrix wtc; int viewx, viewy, vieww, viewh; } Class SWWMUtility { // achievement helpers static clearscope void MarkAchievement( String mvar, PlayerInfo p = null ) { if ( !p || (p != players[consoleplayer]) ) return; let hnd = SWWMStaticHandler(StaticEventHandler.Find("SWWMStaticHandler")); if ( !hnd ) return; String val = hnd.achievementstate.At(mvar); if ( val == "" ) { if ( developer >= 2 ) Console.Printf("MarkAchievement: achievement '"..mvar.."' not found"); return; } if ( val == "2" ) return; hnd.achievementstate.Insert(mvar,"1"); } static clearscope int GetAchievementProgress( String pvar, PlayerInfo p = null ) { if ( !p || (p != players[consoleplayer]) ) return 0; let hnd = SWWMStaticHandler(StaticEventHandler.Find("SWWMStaticHandler")); if ( !hnd ) return 0; String pval = hnd.achievementprogress.At(pvar); if ( pval == "" ) { if ( developer >= 2 ) Console.Printf("AchievementProgress: achievement '"..pvar.."' not found"); return 0; } return pval.ToInt(); } static clearscope void AchievementProgress( String pvar, int val, PlayerInfo p = null ) { if ( !p || (p != players[consoleplayer]) ) return; let hnd = SWWMStaticHandler(StaticEventHandler.Find("SWWMStaticHandler")); if ( !hnd ) return; String pval = hnd.achievementprogress.At(pvar); if ( pval == "" ) { if ( developer >= 2 ) Console.Printf("AchievementProgress: achievement '"..pvar.."' not found"); return; } if ( val <= pval.ToInt() ) return; hnd.achievementprogress.Insert(pvar,String.Format("%d",val)); } static clearscope void AchievementProgressInc( String pvar, int inc, PlayerInfo p = null ) { if ( !p || (p != players[consoleplayer]) ) return; let hnd = SWWMStaticHandler(StaticEventHandler.Find("SWWMStaticHandler")); if ( !hnd ) return; String pval = hnd.achievementprogress.At(pvar); if ( pval == "" ) { if ( developer >= 2 ) Console.Printf("AchievementProgress: achievement '"..pvar.."' not found"); return; } hnd.achievementprogress.Insert(pvar,String.Format("%d",pval.ToInt()+inc)); } static clearscope void AchievementProgressIncDouble( String pvar, double inc, PlayerInfo p = null ) { if ( !p || (p != players[consoleplayer]) ) return; let hnd = SWWMStaticHandler(StaticEventHandler.Find("SWWMStaticHandler")); if ( !hnd ) return; String pval = hnd.achievementprogress.At(pvar); if ( pval == "" ) { if ( developer >= 2 ) Console.Printf("AchievementProgress: achievement '"..pvar.."' not found"); return; } hnd.achievementprogress.Insert(pvar,String.Format("%g",pval.ToDouble()+inc)); } // for bitfields static clearscope void AchievementProgressOr( String pvar, int val, PlayerInfo p = null ) { if ( !p || (p != players[consoleplayer]) ) return; let hnd = SWWMStaticHandler(StaticEventHandler.Find("SWWMStaticHandler")); if ( !hnd ) return; String pval = hnd.achievementprogress.At(pvar); if ( pval == "" ) { if ( developer >= 2 ) Console.Printf("AchievementProgress: achievement '"..pvar.."' not found"); return; } hnd.achievementprogress.Insert(pvar,String.Format("%d",pval.ToInt()|val)); } // gets the names of all mod cvars static clearscope void GetCVars( out Array cvarlist ) { cvarlist.Clear(); let lmp = Wads.CheckNumForFullname("cvarinfo.base"); if ( lmp == -1 ) ThrowAbortException("'cvarinfo.base' not found"); String dat = Wads.ReadLump(lmp); Array list, ln; // fucking Windows dat.Replace("\r",""); list.Clear(); dat.Split(list,"\n"); for ( int i=0; i which ) { if ( !swwm_weapontooltips ) return; CVar v = CVar.FindCVar('swwm_tooltipshown'); String tt = v.GetString(); Array wpn; tt.Split(wpn,","); for ( int i=0; i=1.3)?1.333333:aspect; double fovy = 2.*atan(tan(clamp(fov,5,170)/2.)/fovratio); // world→clip matrix swwm_GM_Matrix view = swwm_GM_Matrix.view(viewpos,angle,pitch,roll); swwm_GM_Matrix perp = swwm_GM_Matrix.perspective(fovy,aspect,5,65535); d.wtc = perp.multiplyMatrix(view); // screen coord data int sblocks = CVar.FindCVar('screenblocks').GetInt(); int viewx, viewy, vieww, viewh; [viewx, viewy, vieww, viewh] = Screen.GetViewWindow(); int sh = Screen.GetHeight(); int h = sh; if ( sblocks < 10 ) h = (sblocks*sh/10)&~7; int bottom = sh-(h+viewy-((h-viewh)/2)); d.viewx = viewx; d.viewy = sh-bottom-h; d.vieww = vieww; d.viewh = h; } static clearscope Vector3 ProjectPoint( SWWMProjectionData d, Vector3 worldpos ) { return d.wtc.multiplyVector3(worldpos).asVector3(); } static clearscope Vector2 NDCToViewport( SWWMProjectionData d, Vector3 ndc ) { return (d.viewx,d.viewy)+(((ndc.x+1)*d.vieww)/2,((-ndc.y+1)*d.viewh)/2); } // checks if a point is inside the viewport static clearscope bool TestScreenBounds( SWWMProjectionData d, Vector2 vpos ) { return ((vpos.x == clamp(vpos.x,d.viewx,d.viewx+d.vieww)) && (vpos.y == clamp(vpos.y,d.viewy,d.viewy+d.viewh))); } // less code duplication static clearscope void AdjustClean_1( out double x, out double y ) { x = (x-160)*CleanXFac_1+(Screen.GetWidth()*.5); y = (y-100)*CleanYFac_1+(Screen.GetHeight()*.5); } static clearscope void AdjustClean_1x( out double x ) { x = (x-160)*CleanXFac_1+(Screen.GetWidth()*.5); } static clearscope void AdjustClean_1y( out double y ) { y = (y-100)*CleanYFac_1+(Screen.GetHeight()*.5); } // Vector/Axis utility functions static clearscope Vector3 Vec3FromAngles( double angle, double pitch ) { return (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch)); } static clearscope Vector3 CircleOffset( Vector3 y, Vector3 z, double angle, double radius ) { return y*cos(angle)*radius+z*sin(angle)*radius; } static clearscope Vector3 ConeSpread( Vector3 x, Vector3 y, Vector3 z, double angle, double spread ) { return (x+y*cos(angle)*spread+z*sin(angle)*spread).unit(); } static clearscope Vector3 RotateVector3( Vector3 v, double angle ) { Vector2 v2d = Actor.RotateVector(v.xy,angle); return (v2d.x,v2d.y,v.z); } static clearscope Vector3 AngleToVector3( double angle, double length = 1. ) { Vector2 v2d = Actor.AngleToVector(angle,length); return (v2d.x,v2d.y,0); } // thanks zscript static clearscope double fract( double a ) { return (a<0)?(a+floor(a)):(a-floor(a)); } static clearscope double lerp( double a, double b, double theta ) { return a*(1.-theta)+b*theta; } static clearscope Vector3 LerpVector3( Vector3 a, Vector3 b, double theta ) { return a*(1.-theta)+b*theta; } static clearscope Vector2 LerpVector2( Vector2 a, Vector2 b, double theta ) { return a*(1.-theta)+b*theta; } static clearscope Color LerpColor( Color a, Color b, double theta ) { Color c = Color( int(a.a*(1.-theta)+b.a*theta), int(a.r*(1.-theta)+b.r*theta), int(a.g*(1.-theta)+b.g*theta), int(a.b*(1.-theta)+b.b*theta) ); return c; } // bit ugly, but it works static clearscope void ThousandsStr( out String s, int col = Font.CR_UNDEFINED, String colstr = "" ) { if ( (col < Font.CR_UNDEFINED) || (col >= Font.NUM_TEXT_COLORS) ) ThrowAbortException("col parameter out of range, use colstr for non-standard font colors."); 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 ) nstr = String.Format("%0*d",digits,n); else nstr = String.Format("%d",n); ThousandsStr(nstr,col,colstr); return nstr; } // this can probably be simplified, but I'm lazy static clearscope Vector3 HSVtoRGB( Vector3 hsv ) { Vector3 p; p.x = abs(fract(hsv.x+1.)*6.-3.); p.y = abs(fract(hsv.x+(2./3.))*6.-3.); p.z = abs(fract(hsv.x+(1./3.))*6.-3.); Vector3 p2; p2.x = (1.-hsv.y)+clamp(p.x-1.,0.,1.)*hsv.y; p2.y = (1.-hsv.y)+clamp(p.y-1.,0.,1.)*hsv.y; p2.z = (1.-hsv.y)+clamp(p.z-1.,0.,1.)*hsv.y; return p2*hsv.z; } static clearscope void StripColor( out String str ) { int len = str.CodePointCount(); for ( int i=0, pos=0; i=0; i-- ) { int d = int(val/IntPow(10,i))%10; str.AppendCharacter(digs[d]); } return str; } static clearscope void ObscureText( out String str, int seed, bool alnum = false ) { int len = str.CodePointCount(); String newstr = ""; for ( int i=0, pos=0; i= 10 ) sd += 7; newstr.AppendCharacter(sd+48); } else newstr.AppendCharacter((abs(seed)%95)+32); } str = newstr; } 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; } // return if a line is an exit, and additionally the type of exit static clearscope bool, int IsExitLine( Line l ) { if ( l.special == Exit_Secret ) return true, ET_Secret; if ( l.special == Exit_Normal ) return true, ET_Normal; if ( l.special == Teleport_EndGame ) return true, ET_EndGame; if ( l.special == Teleport_NewMap ) return true, ET_NewMap; // E1M8 compat if ( (l.special == ACS_Execute) && (l.Args[0] == -Int('E1M8_KNOCKOUT')) ) return true, ET_Normal; // spooktober™ if ( ((l.special == ACS_Execute) || (l.special == ACS_ExecuteAlways)) && (l.Args[0] == -Int('MapFadeOut')) ) { if ( level.levelnum == 1 ) { let lv = levelinfo.FindLevelByNum(l.Args[2]); if ( lv && lv.mapname.Left(6) ~== "SECRET" ) return true, ET_Secret; else return true, ET_NewMap; } return true, ET_Normal; } return false, ET_Normal; } static clearscope bool IsTeleportLine( Line l, bool all = false ) { // must be two-sided and crossable if ( !l.sidedef[1] || !(l.Activation&(SPAC_Cross|SPAC_MCross|SPAC_PCross|SPAC_AnyCross)) ) return false; // filter lines that aren't player-activated (unless checking all) if ( !all && !(l.Activation&SPAC_PlayerActivate) ) return false; // typical teleports if ( (l.special == Teleport) || (l.special == Teleport_NoStop) ) return true; // if checking all, also include sneaky teleports if ( all && ((l.special == Teleport_Line) || (l.special == Teleport_NoFog)) ) return true; // exits are included too if ( IsExitLine(l) ) 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)*(amin.x-p.x); if ( p.x > amax.x ) distsq += (p.x-amax.x)*(p.x-amax.x); if ( p.y < amin.y ) distsq += (amin.y-p.y)*(amin.y-p.y); if ( p.y > amax.y ) distsq += (p.y-amax.y)*(p.y-amax.y); if ( p.z < amin.z ) distsq += (amin.z-p.z)*(amin.z-p.z); if ( p.z > amax.z ) distsq += (p.z-amax.z)*(p.z-amax.z); return (distsq <= (radius*radius)); } // hitscan exit point calculation given actor, entry point and direction static clearscope Vector3 TraceExit( Actor a, Vector3 p, Vector3 d ) { 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); Vector3 tmax, div = (1./d.x,1./d.y,1./d.z); if ( div.x < 0 ) tmax.x = (amin.x-p.x)*div.x; else tmax.x = (amax.x-p.x)*div.x; if ( div.y < 0 ) tmax.y = (amin.y-p.y)*div.y; else tmax.y = (amax.y-p.y)*div.y; if ( div.z < 0 ) tmax.z = (amin.z-p.z)*div.z; else tmax.z = (amax.z-p.z)*div.z; return level.Vec3Offset(p,min(min(tmax.x,tmax.y),tmax.z)*d); } // Liang-Barsky line clipping static clearscope bool, Vector2, Vector2 LiangBarsky( Vector2 minclip, Vector2 maxclip, Vector2 v0, Vector2 v1 ) { double t0 = 0., t1 = 1.; double xdelta = v1.x-v0.x; double ydelta = v1.y-v0.y; double p, q, r; for ( int i=0;i<4; i++ ) { switch ( i ) { case 0: p = -xdelta; q = -(minclip.x-v0.x); break; case 1: p = xdelta; q = (maxclip.x-v0.x); break; case 2: p = -ydelta; q = -(minclip.y-v0.y); break; case 3: p = ydelta; q = (maxclip.y-v0.y); break; } if ( (p == 0.) && (q<0.) ) return false, (0,0), (0,0); if ( p < 0 ) { r = q/p; if ( r > t1 ) return false, (0,0), (0,0); else if ( r > t0 ) t0 = r; } else if ( p > 0 ) { r = q/p; if ( r < t0 ) return false, (0,0), (0,0); else if ( r < t1 ) t1 = r; } } Vector2 ov0 = v0+(xdelta,ydelta)*t0; Vector2 ov1 = v0+(xdelta,ydelta)*t1; return true, ov0, ov1; } static clearscope bool IsValidLockNum( int l ) { if ( (l < 1) || (l > 255) ) return true; return SWWMCachedLockInfo.IsValidLock(l); } static clearscope Color GetLockColor( int l ) { return SWWMCachedLockInfo.GetLockColor(l); } // Thanks to ZZYZX and Nash static play void SetToSlopeSpecific( Actor a, double dang, SecPlane plane, bool flipnorm ) { Vector3 fnormal; if ( flipnorm ) fnormal = -plane.Normal; else fnormal = plane.Normal; vector2 fnormalp1 = ((fnormal.x != 0) || (fnormal.y != 0))?(fnormal.x,fnormal.y).Unit():(0,0); vector2 fnormalp2 = ((fnormal.x,fnormal.y).Length(),fnormal.z); double fang = atan2(fnormalp1.y,fnormalp1.x); // floor angle (not pitch!) double fpitch = atan2(fnormalp2.x,fnormalp2.y); // floor pitch double ddiff1 = sin(fang-dang); double ddiff2 = cos(fang-dang); a.pitch = fpitch*ddiff2; a.roll = -fpitch*ddiff1; a.angle = dang; } static play void SetToSlope( Actor a, double dang, bool ceil = false ) { Sector sect; SecPlane plane; Vector3 fnormal; bool flipnorm; if ( ceil ) { sect = a.CeilingSector; plane = sect.ceilingplane; flipnorm = true; fnormal = -sect.ceilingplane.Normal; } else { sect = a.FloorSector; plane = sect.floorplane; flipnorm = false; fnormal = sect.floorplane.Normal; } // find closest 3d floor for its normal F3DFloor ff; 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; } } 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; } // try to be as thorough as possible in checking if DEHACKED has altered this actor static clearscope bool CheckDehackery( Class cls ) { let def = GetDefaultByType(cls); for ( State s=def.SpawnState; s; s=s.NextState ) { if ( s.bDEHACKED ) return true; // keep checking until we hit a loop, just in case if ( s.NextState && (s.DistanceTo(s.NextState) <= 0) ) break; } for ( State s=def.SeeState; s; s=s.NextState ) { if ( s.bDEHACKED ) return true; // keep checking until we hit a loop, just in case if ( s.NextState && (s.DistanceTo(s.NextState) <= 0) ) break; } for ( State s=def.MissileState; s; s=s.NextState ) { if ( s.bDEHACKED ) return true; // keep checking until we hit a loop, just in case if ( s.NextState && (s.DistanceTo(s.NextState) <= 0) ) break; } for ( State s=def.MeleeState; s; s=s.NextState ) { if ( s.bDEHACKED ) return true; // keep checking until we hit a loop, just in case if ( s.NextState && (s.DistanceTo(s.NextState) <= 0) ) break; } return false; } // because GetTag() returns the localized string, we need to do things the hard way static play String GetFunTag( SWWMHandler hnd, Actor a, String defstr = "" ) { // look up fun tag services if available for ( int i=0; i= 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 ( a && !(flags&DE_NOBLEED) ) a.TraceBleed((ndmg>0)?ndmg:dmg,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) let hnd = SWWMHandler(EventHandler.Find("SWWMHandler")); if ( !hnd || (hnd.psectors.Size() <= 1) ) { if ( (Instigator is 'Demolitionist') && haskilled && !(flags&DE_NONEXPLOSIVE) ) { let demo = Demolitionist(Instigator); if ( (gametic > demo.lastbang+30) && (!hnd || (gametic > hnd.lastcombat+10)) && !Random[DemoLines](0,1) ) demo.lastbang = SWWMHandler.AddOneLiner("blast",2,10); } return nhit, nkill; } 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 ( a && !(flags&DE_NOBLEED) ) a.TraceBleed((ndmg>0)?ndmg:dmg,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 demo = Demolitionist(Instigator); if ( (gametic > demo.lastbang+30) && (gametic > hnd.lastcombat+10) && !Random[DemoLines](0,1) ) demo.lastbang = SWWMHandler.AddOneLiner("blast",2,10); } 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; if ( !mo ) return false; Vector3 pp; if ( !a.CheckSight(mo,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.GetCameraHeight()); 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; } // ui-friendly version without CheckSight call static clearscope bool InPlayerFOVSimple( PlayerInfo p, Actor a, double maxdist = 0. ) { double vfov = p.fov*.5; double hfov = atan(Screen.GetAspectRatio()*tan(vfov)); let mo = p.camera; if ( !mo ) return false; Vector3 pp; if ( mo is 'PlayerPawn' ) pp = mo.Vec2OffsetZ(0,0,PlayerPawn(mo).player.viewz); else pp = mo.Vec3Offset(0,0,mo.GetCameraHeight()); 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; } // IsZeroDamage() can lead to some false negatives, we have to account for that static play bool ValidProjectile( Actor a ) { if ( !a.bMISSILE ) return false; if ( a is 'AirBullet' ) return true; if ( a is 'ExplodiumMagProj' ) return true; if ( a is 'CorrosiveFlechette' ) return true; if ( a is 'TheBall' ) return true; if ( a is 'EvisceratorChunk' ) return true; if ( a is 'EvisceratorProj' ) return true; if ( a is 'HellblazerMissile' ) return true; if ( a is 'QuadProj' ) return true; if ( a is 'BigBiospark' ) return true; if ( a is 'BiosparkBall' ) return true; if ( a is 'BiosparkCore' ) return true; if ( a is 'CandyGunProj' ) return true; if ( a is 'CandyMagProj' ) return true; if ( a is 'MisterGrenade' ) return true; if ( a is 'LoveHeart' ) return true; if ( !a.IsZeroDamage() ) return true; return false; } // Is this a beam projectile? (speed == length) static play bool IsBeamProj( Actor a ) { if ( a is 'SaltBeam' ) return true; if ( a is 'BiosparkBeam' ) return true; if ( a is 'BiosparkArc' ) return true; if ( a is 'CandyBeam' ) return true; if ( a is 'YnykronBeam' ) return true; if ( a is 'YnykronLightningArc' ) return true; if ( a is 'YnykronAltBeam' ) return true; if ( a is 'MykradvoTendril' ) return true; if ( a is 'MisterRailBeam' ) return true; return false; } // is this a YBeam type? (real pitch is pitch-90, scale.y == length) static play bool IsYBeam( Actor a ) { if ( a is 'MisterRailBeam' ) return true; return false; } static clearscope bool IdentifyingDog( Actor a ) { if ( a is 'MBFHelperDog' ) return true; if ( a is 'SWWMDog' ) return true; if ( a.GetClassName() == 'GermanDog' ) return true; // brote dote if ( a.GetClassName() == '64HellHound' ) return true; // brote dote 64 if ( a.GetClassName() == 'AbyssDemon2' ) return true; // CH if ( a.GetClassName() == 'WHOLETTHEDOGSOUT' ) return true; // CH // 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 and others if ( a.Species == 'Cacodemon' ) return true; // Beautiful Doom if ( a.GetClassName() == 'AstralCaco' ) return true; // Eviternity if ( a.GetParentClass().GetClassName() == 'LEG_BaseCaco' ) return true; // LEGION 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; } // used to "substitute" a monster class for another so killcount stats are merged // e.g.: "stealth" monsters and their non-stealth counterparts, // or "HereticImp" and "HereticImpLeader", which have the same exact tag, // and would result in an odd "duplication" of monster names static play Class MergeMonster( SWWMHandler hnd, Class a ) { // see if any services can resolve this first for ( int i=0; i rescls = res; return rescls; } // stealth monsters, the worst thing ever invented if ( a == 'StealthArachnotron' ) return 'Arachnotron'; if ( a == 'StealthArchvile' ) return 'Archvile'; if ( a == 'StealthBaron' ) return 'BaronOfHell'; if ( a == 'StealthCacodemon' ) return 'Cacodemon'; if ( a == 'StealthChaingunGuy' ) return 'ChaingunGuy'; if ( a == 'StealthDemon' ) return 'Demon'; if ( a == 'StealthHellKnight' ) return 'HellKnight'; if ( a == 'StealthDoomImp' ) return 'DoomImp'; if ( a == 'StealthFatso' ) return 'Fatso'; if ( a == 'StealthRevenant' ) return 'Revenant'; if ( a == 'StealthShotgunGuy' ) return 'ShotgunGuy'; if ( a == 'StealthZombieMan' ) return 'ZombieMan'; // heretic monsters if ( a == 'Sorcerer2' ) return 'Sorcerer1'; if ( a == 'HereticImpLeader' ) return 'HereticImp'; if ( a == 'KnightGhost' ) return 'Knight'; if ( a == 'MummyGhost' ) return 'Mummy'; if ( a == 'MummyLeaderGhost' ) return 'MummyLeader'; // hexen monsters if ( a == 'CentaurMash' ) return 'Centaur'; if ( a == 'Demon1Mash' ) return 'Demon1'; if ( a == 'Demon2' ) return 'Demon1'; if ( a == 'Demon2Mash' ) return 'Demon1'; if ( a == 'EttinMash' ) return 'Ettin'; if ( a == 'SerpentLeader' ) return 'Serpent'; if ( a == 'WraithBuried' ) return 'Wraith'; return a; } // the stupidest thing ever, it's called BlockingLine but it's not always blocking us static play bool BlockingLineIsBlocking( Actor a, int blockflags = Line.ML_BLOCKEVERYTHING, Line testline = null ) { Line l = testline?testline:a.BlockingLine; // not blocked if ( !l ) return false; // one-sided always blocking if ( !l.sidedef[1] ) return true; // same for block everything lines if ( l.flags&blockflags ) return true; // lower and upper bounds hit? double afloor = l.frontsector.floorplane.ZAtPoint(a.pos.xy), bfloor = l.backsector.floorplane.ZAtPoint(a.pos.xy), aceil = l.frontsector.ceilingplane.ZAtPoint(a.pos.xy), bceil = l.backsector.ceilingplane.ZAtPoint(a.pos.xy); if ( (min(a.pos.z+a.height,a.ceilingz) > min(aceil,bceil)) || (max(a.pos.z,a.floorz) < max(afloor,bfloor)) ) return true; // solid 3d floor bounds hit? for ( int i=0; i ceil) && (a.pos.z < floor) ) return true; } for ( int i=0; i ceil) && (a.pos.z < floor) ) return true; } return false; } static play Vector3 UseLinePos( Line l ) { Vector3 al, ah, bl, bh; if ( !l.sidedef[1] ) { // just the whole line al = (l.v1.p,l.frontsector.floorplane.ZatPoint(l.v1.p)); ah = (l.v1.p,l.frontsector.ceilingplane.ZatPoint(l.v1.p)); bl = (l.v2.p,l.frontsector.floorplane.ZatPoint(l.v2.p)); bh = (l.v2.p,l.frontsector.ceilingplane.ZatPoint(l.v2.p)); return (al+ah+bl+bh)*.25; } SecPlane highestfloor, lowestfloor, lowestceiling, highestceiling; if ( (l.frontsector.floorplane.ZatPoint(l.v1.p) > l.backsector.floorplane.ZatPoint(l.v1.p)) && (l.frontsector.floorplane.ZatPoint(l.v2.p) > l.backsector.floorplane.ZatPoint(l.v2.p)) ) { highestfloor = l.frontsector.floorplane; lowestfloor = l.backsector.floorplane; } else { highestfloor = l.backsector.floorplane; lowestfloor = l.frontsector.floorplane; } if ( (l.frontsector.ceilingplane.ZatPoint(l.v1.p) < l.backsector.ceilingplane.ZatPoint(l.v1.p)) && (l.frontsector.ceilingplane.ZatPoint(l.v2.p) < l.backsector.ceilingplane.ZatPoint(l.v2.p)) ) { lowestceiling = l.frontsector.ceilingplane; highestceiling = l.backsector.ceilingplane; } else { lowestceiling = l.backsector.ceilingplane; highestceiling = l.frontsector.ceilingplane; } // try to guess what the part that triggers this is if ( l.Activation&SPAC_Cross ) { // pick the "intersection" al = (l.v1.p,highestfloor.ZatPoint(l.v1.p)); ah = (l.v1.p,lowestceiling.ZatPoint(l.v1.p)); bl = (l.v2.p,highestfloor.ZatPoint(l.v2.p)); bh = (l.v2.p,lowestceiling.ZatPoint(l.v2.p)); return (al+ah+bl+bh)*.25; } // check if lower part available al = (l.v1.p,lowestfloor.ZatPoint(l.v1.p)); ah = (l.v1.p,highestfloor.ZatPoint(l.v1.p)); bl = (l.v2.p,lowestfloor.ZatPoint(l.v2.p)); bh = (l.v2.p,highestfloor.ZatPoint(l.v2.p)); if ( ((al-ah).length() > 0) && ((bl-bh).length() > 0) ) return (al+ah+bl+bh)*.25; // check if upper part available al = (l.v1.p,lowestceiling.ZatPoint(l.v1.p)); ah = (l.v1.p,highestceiling.ZatPoint(l.v1.p)); bl = (l.v2.p,lowestceiling.ZatPoint(l.v2.p)); bh = (l.v2.p,highestceiling.ZatPoint(l.v2.p)); if ( ((al-ah).length() > 0) && ((bl-bh).length() > 0) ) return (al+ah+bl+bh)*.25; // check for 3d floors bool floorfound = false; Vector3 fal, fah, fbl, fbh; for ( int i=0; i ah.z) && (fbh.z > bh.z) && (fal.z > al.z) && (fbl.z > bl.z) ) continue; al = fal; ah = fah; bl = fbl; bh = fbh; floorfound = true; } if ( floorfound ) return (al+ah+bl+bh)*.25; for ( int i=0; i ah.z) && (fbh.z > bh.z) && (fal.z > al.z) && (fbl.z > bl.z) ) continue; al = fal; ah = fah; bl = fbl; bh = fbh; floorfound = true; } if ( floorfound ) return (al+ah+bl+bh)*.25; // check for midtex if ( !l.sidedef[0].GetTexture(1).IsNull() ) { double ofs = l.sidedef[0].GetTextureYOffset(1); Vector2 siz = TexMan.GetScaledSize(l.sidedef[0].GetTexture(1)); Vector2 tofs = TexMan.GetScaledOffset(l.sidedef[0].GetTexture(1)); ofs += tofs.y; ofs *= l.sidedef[0].GetTextureYScale(1); siz.y *= l.sidedef[0].GetTextureYScale(1); if ( l.flags&Line.ML_DONTPEGBOTTOM ) { al = (l.v1.p,highestfloor.ZAtPoint(l.v1.p)+ofs); bl = (l.v2.p,highestfloor.ZAtPoint(l.v2.p)+ofs); ah = al+(0,0,siz.y); bh = bl+(0,0,siz.y); } else { ah = (l.v1.p,lowestceiling.ZAtPoint(l.v1.p)+ofs); bh = (l.v2.p,lowestceiling.ZAtPoint(l.v2.p)+ofs); al = ah-(0,0,siz.y); bl = bh-(0,0,siz.y); } return (al+ah+bl+bh)*.25; } if ( !l.sidedef[1].GetTexture(1).IsNull() ) { double ofs = l.sidedef[1].GetTextureYOffset(1); Vector2 siz = TexMan.GetScaledSize(l.sidedef[1].GetTexture(1)); Vector2 tofs = TexMan.GetScaledOffset(l.sidedef[1].GetTexture(1)); ofs += tofs.y; ofs *= l.sidedef[1].GetTextureYScale(1); siz.y *= l.sidedef[1].GetTextureYScale(1); if ( l.flags&Line.ML_DONTPEGBOTTOM ) { al = (l.v1.p,highestfloor.ZAtPoint(l.v1.p)+ofs); bl = (l.v2.p,highestfloor.ZAtPoint(l.v2.p)+ofs); ah = al+(0,0,siz.y); bh = bl+(0,0,siz.y); } else { ah = (l.v1.p,lowestceiling.ZAtPoint(l.v1.p)+ofs); bh = (l.v2.p,lowestceiling.ZAtPoint(l.v2.p)+ofs); al = ah-(0,0,siz.y); bl = bh-(0,0,siz.y); } return (al+ah+bl+bh)*.25; } // just use the intersection al = (l.v1.p,highestfloor.ZatPoint(l.v1.p)); ah = (l.v1.p,lowestceiling.ZatPoint(l.v1.p)); bl = (l.v2.p,highestfloor.ZatPoint(l.v2.p)); bh = (l.v2.p,lowestceiling.ZatPoint(l.v2.p)); return (al+ah+bl+bh)*.25; } // get how much a sector's physical position is offset by portals static Vector2 PortalDisplacement( Sector a, Sector b ) { if ( a.portalgroup == b.portalgroup ) return (0,0); // ez // we can't access level.displacements, so we gotta improvise Vector2 pdisp = b.centerspot-a.centerspot; Vector2 vdisp = level.Vec2Diff(a.centerspot,b.centerspot); return pdisp-vdisp; } // shorthand for some of these checks (these are generally used by the mission briefing system) static bool IsKnownMap() { if ( (gameinfo.gametype&GAME_DOOM) && (IsKnownCustomWAD() || CheckMD5List("vanilladoom.lst")) ) return true; if ( (gameinfo.gametype&GAME_HERETIC) && CheckMD5List("vanillaheretic.lst") ) return true; if ( (gameinfo.gametype&GAME_HEXEN) && CheckMD5List("vanillahexen.lst") ) return true; return false; } // to be filled static bool IsKnownCustomWAD() { if ( gameinfo.gametype&GAME_DOOM ) { if ( IsEviternity() ) return true; if ( IsUltDoom2() ) return true; } return false; } // detect ultimate doom 2 static bool IsUltDoom2() { return CheckMD5List("ultdoom2.lst"); } // detect eviternity (naive method) static bool IsEviternity() { for ( int i=0; i list; list.Clear(); dat.Split(list,"\n"); for ( int i=0; i i ) { if ( i is 'MagAmmo' ) return StringTable.Localize("$T_"..GetDefaultByType((Class)(i)).PickupTag.."S"); if ( i is 'SWWMAmmo' ) return StringTable.Localize("$T_"..GetDefaultByType((Class)(i)).PickupTag.."S"); return GetDefaultByType(i).GetTag(); } static bool, TextureID DefaceTexture( TextureID checkme ) { String tn = TexMan.GetName(checkme); // special case: alt texture names in Doom 2 In Spain Only if ( (tn ~== "MARBFAC2") || (tn ~== "SP_MAR01") ) return true, TexMan.CheckForTexture("defaced_MARBFAC2",TexMan.Type_Any); if ( (tn ~== "MARBFAC3") || (tn ~== "SP_MAR02")) return true, TexMan.CheckForTexture("defaced_MARBFAC3",TexMan.Type_Any); if ( (tn ~== "MARBFAC4") || (tn ~== "SP_MAR03") ) return true, TexMan.CheckForTexture("defaced_MARBFAC4",TexMan.Type_Any); if ( (tn ~== "MARBFACE") || (tn ~== "SP_MAR04") ) return true, TexMan.CheckForTexture("defaced_MARBFACE",TexMan.Type_Any); if ( (tn ~== "ZZWOLF2") ) return true, TexMan.CheckForTexture("defaced_ZZWOLF2",TexMan.Type_Any); if ( (tn ~== "ZZWOLF3") ) return true, TexMan.CheckForTexture("defaced_ZZWOLF3",TexMan.Type_Any); if ( (tn ~== "ZZWOLF4") ) return true, TexMan.CheckForTexture("defaced_ZZWOLF4",TexMan.Type_Any); if ( (tn ~== "ZZWOLF6") ) return true, TexMan.CheckForTexture("defaced_ZZWOLF6",TexMan.Type_Any); if ( (tn ~== "ZZWOLF7") ) return true, TexMan.CheckForTexture("defaced_ZZWOLF7",TexMan.Type_Any); if ( (tn ~== "ZZWOLF12") ) return true, TexMan.CheckForTexture("defaced_ZZWOLF12",TexMan.Type_Any); if ( (tn ~== "ZZWOLF13") ) return true, TexMan.CheckForTexture("defaced_ZZWOLF13",TexMan.Type_Any); return false, checkme; } // iterate through polyobjects and see if this line is part of one (returning which, if any) static bool IsPolyLine( Line l, out swwm_PolyobjectHandle o ) { let pi = swwm_PolyobjectIterator.Create(); swwm_PolyobjectHandle p; while ( p = pi.Next() ) { if ( p.Lines.Find(l) >= p.Lines.Size() ) continue; o = p; return true; } o = null; return false; } // checks if the specified world coordinate is inside the polyobject // this check is very naive but it should handle most "normal" shapes // (yeah, sorry if you somehow want to play this mod with lilith.pk3) static bool PointInPolyobj( Vector2 p, swwm_PolyobjectHandle o ) { // first pass, find which vertex out of all lines is closest Vertex v = o.StartLine.v1; double dist = (v.p-p).length(); for ( int i=0; i= 4) && (level.mapname.Mid(0,1) == "E") && (level.mapname.ByteAt(1) >= 0x30) && (level.mapname.ByteAt(1) < 0x40) && (level.mapname.Mid(2,1) == "M") && (level.mapname.ByteAt(3) >= 0x30) && (level.mapname.ByteAt(3) < 0x40) ) return true; return false; } static bool IsVipItem( Actor target ) { if ( (target is 'Chancebox') && (target.CurState==target.SpawnState) ) return true; if ( target is 'SWWMCollectible' ) return true; if ( (target is 'Ynykron') || (target is 'RafanKos') ) return true; if ( (target is 'GoldShell') || (target is 'YnykronAmmo') || (target is 'UltimatePod') || (target is 'UltimateAmmo') ) return true; if ( (target is 'Mykradvo') || (target is 'AngerySigil') || (target is 'DivineSprite') ) return true; if ( target is 'PuzzleItem' ) return true; return false; } // used by the store static bool IsVipItemClass( Class target ) { if ( (target is 'Ynykron') || (target is 'RafanKos') ) return true; if ( (target is 'GoldShell') || (target is 'YnykronAmmo') || (target is 'UltimatePod') || (target is 'UltimateAmmo') ) return true; if ( (target is 'Mykradvo') || (target is 'AngerySigil') || (target is 'DivineSprite') ) return true; return false; } static bool IsScoreItem( Actor target ) { if ( target is 'Key' ) return true; if ( target is 'HammerspaceEmbiggener' ) return true; return target.bCOUNTITEM; } // check that all players can get enough of this if needed // multi: check for multiple copies, not just single instances // (useful e.g. for dual wieldable weapons) static bool CheckNeedsItem( Class itm, bool multi = false ) { int np = 0; for ( int i=0; i0); } // checks if instances of a certain item exist // skipme: optionally, ignore checking for one specific instance // (useful to check if we're the only copy of an item) // mapstart: this function is being called during map load, so we // should also check STAT_TRAVELLING inventory // worldonly: only checks for items that are placed in the world // ownedonly: only checks for items that are owned by players // (note that this is mutually exclusive with worldonly) static bool ItemExists( Class itm, Inventory skipme = null, bool mapstart = false, bool worldonly = false, bool ownedonly = false ) { let ti = ThinkerIterator.Create(itm); Inventory i; while ( i = Inventory(ti.Next()) ) { if ( i == skipme ) continue; if ( worldonly && i.Owner ) continue; if ( ownedonly && (!i.Owner || !i.Owner.player) ) continue; return true; } if ( worldonly || !mapstart ) return false; ti = ThinkerIterator.Create(itm,Thinker.STAT_TRAVELLING); while ( i = Inventory(ti.Next()) ) { if ( i == skipme ) continue; if ( ownedonly && (!i.Owner || !i.Owner.player) ) continue; return true; } return false; } // multi-weapon spawn stuff static private Class PickPair( Class a, Class b ) { if ( !ItemExists(a,mapstart:true) ) return a; if ( !ItemExists(b,mapstart:true) ) return b; return Random[Replacements](0,1)?a:b; } // melee weapon + extra slot 2 guns static Class PickSWWMSlot1() { // [GROSS HACK] default to a hammer if there are no players // (this genuinely can happen, if player starts were placed AFTER the item) int np = 0; for ( int i=0; i PickSWWMSlot2() { // as they are dual-wieldable, there should be a 50% chance for spares to also appear if needed if ( Random[Replacements](0,1) && !CheckNeedsItem('ExplodiumGun') && CheckNeedsItem('ExplodiumGun',true) ) return 'ExplodiumGun'; if ( Random[Replacements](0,1) && !CheckNeedsItem('PlasmaBlast') && CheckNeedsItem('PlasmaBlast',true) ) return 'PlasmaBlast'; return PickPair('ExplodiumGun','PlasmaBlast'); } // shotgun spawn static Class PickSWWMSlot3() { return PickPair('Spreadgun','PuntzerBeta'); } // super shotgun spawn static Class PickSWWMSlot4() { return PickPair('Wallbuster','PuntzerGamma'); } // chaingun spawn static Class PickSWWMSlot5() { return PickPair('Eviscerator','HeavyMahSheenGun'); } // rocket launcher spawn static Class PickSWWMSlot6() { return PickPair('Hellblazer','Quadravol'); } // first plasma rifle spawn static Class PickSWWMSlot7() { return PickPair('Sparkster','ModernSparkster'); } // second plasma rifle spawn static Class PickSWWMSlot8() { return PickPair('SilverBullet','RayKhom'); } // first bfg spawn static Class PickSWWMSlot9() { // 25% chance to still drop another candy gun if it's not at max capacity if ( !Random[Replacements](0,3) && ItemExists('CandyGun') && CheckNeedsItem('CandyGunSpares',true) ) return 'CandyGun'; return PickPair('CandyGun','MisterRifle'); } // second bfg spawn (each weapon can only exist once) static Class PickSWWMSlot0( bool fallback = true ) { if ( ItemExists('Ynykron',mapstart:true) ) { if ( ItemExists('RafanKos',mapstart:true) ) return fallback?PickSWWMSlot9():null; return 'RafanKos'; } if ( ItemExists('RafanKos',mapstart:true) ) return 'Ynykron'; return Random[Replacements](0,1)?'Ynykron':'RafanKos'; } // either plasma rifle spawn static Class PickDoomSlot6() { bool hasslot7 = (!CheckNeedsItem('Sparkster')||!CheckNeedsItem('ModernSparkster')); bool hasslot8 = (!CheckNeedsItem('SilverBullet')||!CheckNeedsItem('RayKhom')); // if the player already has a slot 7 weapon... if ( hasslot7 ) { // ... and also has a slot 8 weapon, 33% chance of a slot 8 spawn // otherwise, guaranteed slot 8 spawn if ( hasslot8 && Random[Replacements](0,2) ) return PickSWWMSlot7(); else return PickSWWMSlot8(); } // otherwise, always spawn a slot 7 weapon first return PickSWWMSlot7(); } // either bfg spawn static Class PickDoomSlot7() { bool hasslot9 = (!CheckNeedsItem('CandyGun')||!CheckNeedsItem('MisterRifle')); bool hasslot0 = (!CheckNeedsItem('Ynykron')||!CheckNeedsItem('RafanKos')); let rep = PickSWWMSlot0(false); // if the player already has a slot 9 weapon (and a slot 0 weapon can still spawn)... if ( hasslot9 && rep ) { // ... and also has a slot 0 weapon already, 33% chance of a slot 0 spawn // otherwise, guaranteed slot 0 spawn if ( hasslot0 && Random[Replacements](0,2) ) return PickSWWMSlot9(); else return rep; } // otherwise, always spawn a slot 9 weapon first return PickSWWMSlot9(); } // either shotgun spawn (also used for Heretic) static Class PickDoomSlot3() { // always slot 3 after map start, prevents shotgun guys from dropping wallbusters, which is weird af if ( level.maptime ) return PickSWWMSlot3(); bool hasslot3 = (!CheckNeedsItem('Spreadgun')||!CheckNeedsItem('PuntzerBeta')); bool hasslot4 = (!CheckNeedsItem('Wallbuster')||!CheckNeedsItem('PuntzerGamma')); // if the player already has a slot 3 weapon... if ( hasslot3 ) { // ... and also has a slot 4 weapon, 33% chance of a slot 4 spawn // otherwise, guaranteed slot 4 spawn if ( hasslot4 && Random[Replacements](0,2) ) return PickSWWMSlot3(); return PickSWWMSlot4(); } // otherwise, always spawn a slot 3 weapon first return PickSWWMSlot3(); } // what RandomSpawner does, basically (simplified for items) static play void TransferItemProp( Actor a, Actor b, bool bundlehack = false ) { if ( bundlehack ) { b.spawnpoint = b.pos; b.spawnangle = int(b.angle); } else { b.spawnpoint = a.spawnpoint; b.spawnangle = a.spawnangle; b.angle = a.angle; b.pitch = a.pitch; b.roll = a.roll; } b.special = a.special; b.FloatBobPhase = a.FloatBobPhase; // important for ( int i=0; i<5; i++ ) b.args[i] = a.args[i]; b.special1 = a.special1; b.special2 = a.special2; b.spawnflags = a.spawnflags&~MTF_SECRET; b.HandleSpawnFlags(); b.spawnflags = a.spawnflags; b.bCountSecret = a.spawnflags&MTF_SECRET; b.ChangeTid(a.tid); b.vel = b.vel; b.master = b.master; b.tracer = b.tracer; b.target = b.target; b.bDROPPED = a.bDROPPED; } } Class RadiusDebugSphere : Actor { Default { RenderStyle "AddStencil"; StencilColor "White"; Radius .1; Height 0.; +NOGRAVITY; +NOINTERACTION; } States { Spawn: XZW1 A 1 BRIGHT A_FadeOut(); Wait; } } Class EnvmapDebugSphere : Actor { override bool Used( Actor user ) { if ( CurState.NextState ) SetState(CurState.NextState); else SetState(SpawnState); return true; } override void Tick() {} Default { RenderStyle "Normal"; Radius 16; Height 48; } States { Spawn: XZW1 A -1 NoDelay A_SetRenderStyle(1.,STYLE_Normal); XZW1 B -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 C -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 D -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 E -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 F -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 G -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 H -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 I -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 J -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 K -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 L -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 M -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 N -1 A_SetRenderStyle(1.,STYLE_Add); XZW1 O -1 Bright A_SetRenderStyle(1.,STYLE_Normal); XZW1 P -1 Bright A_SetRenderStyle(1.,STYLE_Normal); XZW1 Q -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 R -1 Bright A_SetRenderStyle(1.,STYLE_Normal); XZW1 S -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 T -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 U -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 V -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 W -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 X -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 Y -1 A_SetRenderStyle(1.,STYLE_Normal); XZW1 Z -1 A_SetRenderStyle(1.,STYLE_Normal); XZW2 A -1 A_SetRenderStyle(1.,STYLE_Normal); XZW2 B -1 A_SetRenderStyle(1.,STYLE_Normal); XZW2 C -1 A_SetRenderStyle(1.,STYLE_Normal); XZW2 D -1 A_SetRenderStyle(1.,STYLE_Add); XZW2 E -1 Bright A_SetRenderStyle(1.,STYLE_Normal); XZW2 F -1 Bright A_SetRenderStyle(1.,STYLE_Add); Loop; } }