2349 lines
72 KiB
Text
2349 lines
72 KiB
Text
// 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
|
|
};
|
|
|
|
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<String> cvarlist )
|
|
{
|
|
cvarlist.Clear();
|
|
let lmp = Wads.CheckNumForFullname("cvarinfo.base");
|
|
if ( lmp == -1 ) ThrowAbortException("'cvarinfo.base' not found");
|
|
String dat = Wads.ReadLump(lmp);
|
|
Array<String> list, ln;
|
|
// fucking Windows
|
|
dat.Replace("\r","");
|
|
list.Clear();
|
|
dat.Split(list,"\n");
|
|
for ( int i=0; i<list.Size(); i++ )
|
|
{
|
|
if ( (list[i].Length() == 0) || (list[i].Left(2) == "//") || (list[i].Left(1) == "") )
|
|
continue;
|
|
int eq = list[i].IndexOf("=");
|
|
if ( eq == -1 ) continue;
|
|
list[i].Truncate(eq);
|
|
ln.Clear();
|
|
list[i].Split(ln," ",0);
|
|
for ( int j=0; j<ln.Size(); j++ )
|
|
{
|
|
if ( (ln[j].Left(5) != "swwm_") ) continue;
|
|
cvarlist.Push(ln[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// sends
|
|
static clearscope void SendTooltip( Class<SWWMWeapon> which )
|
|
{
|
|
if ( !swwm_weapontooltips ) return;
|
|
CVar v = CVar.FindCVar('swwm_tooltipshown');
|
|
String tt = v.GetString();
|
|
Array<String> wpn;
|
|
tt.Split(wpn,",");
|
|
for ( int i=0; i<wpn.Size(); i++ )
|
|
{
|
|
if ( wpn[i] == which.GetClassName() ) return;
|
|
}
|
|
if ( tt == "" ) tt = which.GetClassName();
|
|
else tt = tt..","..which.GetClassName();
|
|
v.SetString(tt);
|
|
// this is kinda hacky but hey, as long as it works
|
|
Console.MidPrint(null,"swwmwpntooltip."..which.GetClassName());
|
|
v = CVar.FindCVar('swwm_tooltipnote');
|
|
if ( v.GetBool() ) return;
|
|
v.SetBool(true);
|
|
Console.Printf(StringTable.Localize("$SWWM_TTNOTE"));
|
|
}
|
|
|
|
// gutamatics caching
|
|
static clearscope void PrepareProjData( out SWWMProjectionData d, Vector3 viewpos, double angle, double pitch, double roll, double fov )
|
|
{
|
|
double aspect = Screen.GetAspectRatio();
|
|
// vertical fov
|
|
double fovratio = (aspect>=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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// not sure if I should use this, looks a bit ugly
|
|
static clearscope void ThousandsStr( out String s, int col = -1 )
|
|
{
|
|
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<len; i++ )
|
|
{
|
|
int ch;
|
|
[ch, pos] = nstr.GetNextCodePoint(pos);
|
|
s.AppendCharacter(ch);
|
|
t = (t-1)%3;
|
|
if ( (pos < len) && !t )
|
|
{
|
|
if ( col != -1 )
|
|
{
|
|
s.AppendCharacter(0x1C);
|
|
s.AppendCharacter(0x61+col);
|
|
}
|
|
s.AppendCharacter(0x2C);
|
|
if ( col != -1 )
|
|
{
|
|
s.AppendCharacter(0x1C);
|
|
s.AppendCharacter(0x2D);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static clearscope String ThousandsNum( int n, int col = -1, int digits = 0 )
|
|
{
|
|
String nstr;
|
|
if ( digits > 0 ) nstr = String.Format("%0*d",digits,n);
|
|
else nstr = String.Format("%d",n);
|
|
ThousandsStr(nstr,col);
|
|
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<len; i++ )
|
|
{
|
|
int remlen = 0;
|
|
int cplen = 0;
|
|
int ch, nxt;
|
|
[ch, nxt] = str.GetNextCodePoint(pos);
|
|
if ( ch != 0x1C )
|
|
{
|
|
pos = nxt;
|
|
continue;
|
|
}
|
|
remlen++;
|
|
cplen++;
|
|
[ch, nxt] = str.GetNextCodePoint(pos+remlen);
|
|
if ( ch == 0x5B )
|
|
{
|
|
int ch2;
|
|
do
|
|
{
|
|
[ch2, nxt] = str.GetNextCodePoint(pos+remlen);
|
|
remlen += nxt-(pos+remlen);
|
|
cplen++;
|
|
}
|
|
while ( ch2 != 0x5D );
|
|
}
|
|
remlen++;
|
|
str.Remove(pos,remlen);
|
|
len -= cplen;
|
|
i--;
|
|
}
|
|
}
|
|
|
|
static clearscope String SuperscriptNum( int val )
|
|
{
|
|
// unicode is fun
|
|
static const int digs[] = {0x2070,0x00B9,0x00B2,0x00B3,0x2074,0x2075,0x2076,0x2077,0x2078,0x2079};
|
|
String str = "";
|
|
int digits = int(Log10(val));
|
|
for ( int i=digits; i>=0; i-- )
|
|
{
|
|
int d = int(val/(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<len; i++ )
|
|
{
|
|
seed = ((seed<<1)*35447+(seed/87));
|
|
int ch;
|
|
[ch, pos] = str.GetNextCodePoint(pos);
|
|
if ( (ch == 0x20) || (ch == 0x09) || (ch == 0x0A) )
|
|
newstr.AppendCharacter(ch);
|
|
else if ( alnum )
|
|
{
|
|
int sd = abs(seed%36);
|
|
if ( sd >= 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<len; i++ )
|
|
{
|
|
int cp1;
|
|
[cp1, pos] = workstr.GetNextCodePoint(pos);
|
|
str.AppendCharacter(cp1);
|
|
if ( i < len-1 )
|
|
{
|
|
int cp2 = workstr.GetNextCodePoint(pos);
|
|
// this looks awkward, but I have to also account for non-letter characters
|
|
// uppercase after lowercase
|
|
if ( (String.CharUpper(cp1) != cp1) && (String.CharLower(cp2) != cp2) )
|
|
str.AppendCharacter(0x20);
|
|
// uppercase after non-letter
|
|
else if ( (String.CharUpper(cp1) == cp1) && (String.CharLower(cp1) == cp1) && (String.CharLower(cp2) != cp2) )
|
|
str.AppendCharacter(0x20);
|
|
// non-letter after lowercase
|
|
else if ( (String.CharUpper(cp1) != cp1) && (String.CharLower(cp2) == cp2) && (String.CharUpper(cp2) == cp2) )
|
|
str.AppendCharacter(0x20);
|
|
}
|
|
}
|
|
}
|
|
|
|
static double PitchTo( Actor a, Actor b, double hfact = 1. )
|
|
{
|
|
if ( !a || !b ) return 0;
|
|
Vector3 thispos = a.player?a.Vec2OffsetZ(0,0,a.player.viewz):a.Vec3Offset(0,0,a.missileheight);
|
|
Vector3 otherpos = b.Vec3Offset(0,0,b.height*hfact);
|
|
Vector3 diff = level.Vec3Diff(thispos,otherpos);
|
|
double dist = diff.length();
|
|
if ( dist > 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)**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));
|
|
}
|
|
|
|
// 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<sect.Get3DFloorCount(); i++ )
|
|
{
|
|
if ( !(sect.Get3DFloor(i).flags&F3DFloor.FF_SOLID) ) continue;
|
|
if ( !ceil && !(sect.Get3DFloor(i).top.ZAtPoint(a.pos.xy) ~== a.floorz) ) continue;
|
|
else if ( ceil && !(sect.Get3DFloor(i).top.ZAtPoint(a.pos.xy) ~== a.ceilingz) ) continue;
|
|
ff = sect.Get3DFloor(i);
|
|
break;
|
|
}
|
|
if ( ff )
|
|
{
|
|
if ( ceil )
|
|
{
|
|
plane = ff.bottom;
|
|
flipnorm = false;
|
|
fnormal = ff.bottom.Normal;
|
|
}
|
|
else
|
|
{
|
|
plane = ff.top;
|
|
flipnorm = true;
|
|
fnormal = -ff.top.Normal;
|
|
}
|
|
}
|
|
SetToSlopeSpecific(a,dang,plane,flipnorm);
|
|
}
|
|
|
|
static clearscope bool IsDoorSector( Sector s, int part )
|
|
{
|
|
// super-easy mode: check for boss special sectors
|
|
if ( (level.mapname ~== "E1M8") || (level.mapname ~== "E2M8") || (level.mapname ~== "E3M8")
|
|
|| (level.mapname ~== "E4M6") || (level.mapname ~== "E4M8") || (level.mapname ~== "E5M8")
|
|
|| (level.mapname ~== "MAP07") )
|
|
{
|
|
let si = level.CreateSectorTagIterator(666);
|
|
int idx;
|
|
while ( (idx = si.Next()) != -1 )
|
|
if ( level.Sectors[idx] == s )
|
|
return true;
|
|
if ( level.mapname ~== "MAP07" )
|
|
{
|
|
let si2 = level.CreateSectorTagIterator(667);
|
|
while ( (idx = si.Next()) != -1 )
|
|
if ( level.Sectors[idx] == s )
|
|
return true;
|
|
}
|
|
}
|
|
// moderate: see if it's a busted crusher, we need to be able to break those in case they cause a softlock
|
|
let ti = ThinkerIterator.Create("SWWMCrusherBroken",Thinker.STAT_USER);
|
|
SWWMCrusherBroken cb;
|
|
while ( cb = SWWMCrusherBroken(ti.Next()) )
|
|
{
|
|
if ( (part == 0) && (cb.fsec == s) ) return true;
|
|
if ( (part == 1) && (cb.csec == s) ) return true;
|
|
}
|
|
// hard mode: look for all lines/actors with movement specials referencing us
|
|
for ( int i=0; i<level.Lines.Size(); i++ )
|
|
{
|
|
Line l = level.Lines[i];
|
|
if ( !l.special ) continue;
|
|
if ( (part && (l.special >= 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<Actor> 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
|
|
if ( hnd.funtagsv )
|
|
{
|
|
let si = ServiceIterator.Find("FunTagService");
|
|
Service sv;
|
|
String res;
|
|
while ( sv = si.Next() )
|
|
{
|
|
res = sv.GetString("GetFunTag",objectArg:a);
|
|
if ( res == "" ) continue;
|
|
si.Destroy();
|
|
return res;
|
|
}
|
|
si.Destroy();
|
|
}
|
|
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 'SWWMBossBrain':
|
|
basetag = "BOSSBRAIN";
|
|
break;
|
|
case 'WolfensteinSS':
|
|
if ( IsUltDoom2() )
|
|
{
|
|
basetag = "ELITEZOMBIE";
|
|
break;
|
|
}
|
|
// ensure it's not being replaced
|
|
if ( CheckDehackery('WolfensteinSS') ) break;
|
|
case 'SWWMSS':
|
|
basetag = "WOLFSS";
|
|
break;
|
|
case 'SWWMHangingKeen':
|
|
basetag = "KEEN";
|
|
break;
|
|
case 'MBFHelperDog':
|
|
case 'SWWMDog':
|
|
basetag = "DOG";
|
|
break;
|
|
case 'SWWMGuard':
|
|
basetag = "WOLFGUARD";
|
|
break;
|
|
case 'SWWMHans':
|
|
basetag = "WOLFHANS";
|
|
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;
|
|
case 'Pig':
|
|
basetag = "PIG";
|
|
break;
|
|
// eviternity
|
|
case 'ArchangelusA':
|
|
case 'ArchangelusB':
|
|
basetag = "ANGEL";
|
|
break;
|
|
case 'AstralCaco':
|
|
basetag = "ASTRAL";
|
|
break;
|
|
case 'Annihilator':
|
|
basetag = "ANNIHIL";
|
|
break;
|
|
case 'FormerCaptain':
|
|
basetag = "FCAPTAIN";
|
|
break;
|
|
case 'NightmareDemon':
|
|
basetag = "NDEMON";
|
|
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, 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 )
|
|
{
|
|
// 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);
|
|
int nhit = 0, nkill = 0;
|
|
bool haskilled = false;
|
|
Array<Actor> 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 ( !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 /= 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 ( dmg <= 0 ) continue; // no harm
|
|
int oldhp = a.Health;
|
|
int basehp = a.GetSpawnHealth();
|
|
int ndmg = a.DamageMobj(Source,Instigator,dmg,(DamageType=='')?Source.DamageType:DamageType,DMG_EXPLOSION|dmgflags,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 )
|
|
{
|
|
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<hnd.psectors.Size(); i++ )
|
|
{
|
|
if ( i == thisgroup ) continue;
|
|
Vector2 relpos = Source.pos.xy+SWWMUtility.PortalDisplacement(level.Sectors[hnd.psectors[thisgroup]],level.Sectors[hnd.psectors[i]]);
|
|
bi = BlockThingsIterator.CreateFromPos(relpos.x,relpos.y,Source.pos.z,Source.pos.z+Source.height,ExplosionRadius,false);
|
|
while ( bi.Next() )
|
|
{
|
|
Actor a = bi.Thing;
|
|
// early exit for already processed actors
|
|
if ( washit.Find(a) < washit.Size() )
|
|
continue;
|
|
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 ( !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 /= 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 ( dmg <= 0 ) continue; // no harm
|
|
int oldhp = a.Health;
|
|
int basehp = a.GetSpawnHealth();
|
|
int ndmg = a.DamageMobj(Source,Instigator,dmg,(DamageType=='')?Source.DamageType:DamageType,DMG_EXPLOSION|dmgflags,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 )
|
|
{
|
|
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 '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 'LoveHeart' ) return true;
|
|
if ( a is 'SWWMLightProjectile' ) 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;
|
|
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
|
|
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<Actor> MergeMonster( Class<Actor> a )
|
|
{
|
|
// see if any services can resolve this first
|
|
let si = ServiceIterator.Find("MergeMonsterService");
|
|
Service sv;
|
|
String res;
|
|
while ( sv = si.Next() )
|
|
{
|
|
res = sv.GetString("MergeMonster",stringArg:a.GetClassName());
|
|
if ( res == "" ) continue;
|
|
si.Destroy();
|
|
Class<Actor> rescls = res;
|
|
return rescls;
|
|
}
|
|
si.Destroy();
|
|
// 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<l.frontsector.Get3DFloorCount(); i++ )
|
|
{
|
|
F3DFloor ff = l.frontsector.Get3DFloor(i);
|
|
if ( !(ff.flags&(F3DFloor.FF_EXISTS|F3DFloor.FF_SOLID)) ) continue;
|
|
double floor = ff.top.ZAtPoint(a.pos.xy);
|
|
double ceil = ff.bottom.ZAtPoint(a.pos.xy);
|
|
if ( (a.pos.z+a.height > ceil) && (a.pos.z < floor) )
|
|
return true;
|
|
}
|
|
for ( int i=0; i<l.backsector.Get3DFloorCount(); i++ )
|
|
{
|
|
F3DFloor ff = l.backsector.Get3DFloor(i);
|
|
if ( !(ff.flags&(F3DFloor.FF_EXISTS|F3DFloor.FF_SOLID)) ) continue;
|
|
double floor = ff.top.ZAtPoint(a.pos.xy);
|
|
double ceil = ff.bottom.ZAtPoint(a.pos.xy);
|
|
if ( (a.pos.z+a.height > 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<l.backsector.Get3DFloorCount(); i++ )
|
|
{
|
|
let ff = l.backsector.Get3DFloor(i);
|
|
fal = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
|
|
fah = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
|
|
fbl = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
|
|
fbh = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
|
|
// skip if higher, we'll go with the lowest 3d floor (may not be right, but whatever)
|
|
if ( floorfound && (fah.z > 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<l.frontsector.Get3DFloorCount(); i++ )
|
|
{
|
|
let ff = l.frontsector.Get3DFloor(i);
|
|
fal = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
|
|
fah = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
|
|
fbl = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
|
|
fbh = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
|
|
// skip if higher, we'll go with the lowest 3d floor (may not be right, but whatever)
|
|
if ( floorfound && (fah.z > 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<AllActorClasses.Size(); i++ )
|
|
{
|
|
if ( AllActorClasses[i].GetClassName() != "Archangelus" )
|
|
continue;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// detect doom vacation
|
|
static bool InDoomVacation()
|
|
{
|
|
// cheap, but hey, it should work
|
|
if ( Wads.FindLump("VACABEX") != -1 )
|
|
{
|
|
// just to make sure
|
|
for ( int i=0; i<AllActorClasses.Size(); i++ )
|
|
{
|
|
if ( AllActorClasses[i].GetClassName() != "Babe" )
|
|
continue;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool IsDeathkings()
|
|
{
|
|
if ( (gameinfo.gametype&GAME_HEXEN) && LevelInfo.MapExists("MAP41") )
|
|
{
|
|
let inf = LevelInfo.FindLevelInfo("MAP41");
|
|
if ( inf.cluster == 1 ) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// detect vanilla maps (across all IWAD versions)
|
|
static bool CheckMD5List( String fname )
|
|
{
|
|
String csum = level.GetChecksum();
|
|
let lmp = Wads.CheckNumForFullName(fname);
|
|
if ( lmp == -1 ) return false;
|
|
String dat = Wads.ReadLump(lmp);
|
|
// fucking Windows
|
|
dat.Replace("\r","");
|
|
Array<String> list;
|
|
list.Clear();
|
|
dat.Split(list,"\n");
|
|
for ( int i=0; i<list.Size(); i++ )
|
|
{
|
|
if ( (list[i].Length() == 0) || (list[i].Left(1) == "#") || (list[i].Left(1) == "") )
|
|
continue;
|
|
if ( csum ~== list[i] )
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// gendered languages are a fuck
|
|
static clearscope bool SellFemaleItem( Inventory i, String loc = "SWWM_SELLEXTRA_FEM" )
|
|
{
|
|
// no gendered string alt
|
|
if ( StringTable.Localize("$"..loc) == loc )
|
|
return false;
|
|
if ( i is 'DeepImpact' ) return true;
|
|
if ( i is 'ExplodiumGun' ) return true;
|
|
if ( i is 'Wallbuster' ) return true;
|
|
if ( i is 'HeavyMahSheenGun' ) return true;
|
|
if ( i is 'Quadravol' ) return true;
|
|
if ( i is 'Sparkster' ) return true;
|
|
if ( i is 'CandyGun' ) return true;
|
|
if ( i is 'RayKhom' ) return true;
|
|
if ( i is 'RafanKos' ) return true;
|
|
if ( i is 'HealthNuggetItem' ) return true;
|
|
if ( i is 'ArmorNuggetItem' ) return true;
|
|
if ( i is 'WarArmor' ) return true;
|
|
if ( i is 'FuckingInvinciball' ) return true;
|
|
if ( i is 'SWWMLamp' ) return true;
|
|
if ( i is 'AngerySigil' ) return true;
|
|
return false;
|
|
}
|
|
|
|
// returns the plural tag (if available)
|
|
static clearscope string GetAmmoTag( Inventory i )
|
|
{
|
|
if ( i is 'MagAmmo' ) return StringTable.Localize("$T_"..MagAmmo(i).PickupTag.."S");
|
|
if ( i is 'SWWMAmmo' ) return StringTable.Localize("$T_"..SWWMAmmo(i).PickupTag.."S");
|
|
return i.GetTag();
|
|
}
|
|
// because of zscript fuckery with GetDefaultByType
|
|
static clearscope string GetAmmoTagClass( Class<Inventory> i )
|
|
{
|
|
if ( i is 'MagAmmo' ) return StringTable.Localize("$T_"..GetDefaultByType((Class<MagAmmo>)(i)).PickupTag.."S");
|
|
if ( i is 'SWWMAmmo' ) return StringTable.Localize("$T_"..GetDefaultByType((Class<SWWMAmmo>)(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<o.Lines.Size(); i++ )
|
|
{
|
|
Line l = o.Lines[i];
|
|
double dist2 = (l.v1.p-p).length();
|
|
if ( dist2 < dist )
|
|
{
|
|
v = l.v1;
|
|
dist = dist2;
|
|
}
|
|
dist2 = (l.v2.p-p).length();
|
|
if ( dist2 < dist )
|
|
{
|
|
v = l.v2;
|
|
dist = dist2;
|
|
}
|
|
}
|
|
// second pass, find which two lines share that vertex
|
|
// (in theory there should only be two)
|
|
Line a = null, b = null;
|
|
for ( int i=0; i<o.Lines.Size(); i++ )
|
|
{
|
|
Line l = o.Lines[i];
|
|
if ( (l.v1 == v) || (l.v2 == v) )
|
|
{
|
|
if ( !a ) a = l;
|
|
else if ( !b ) b = l;
|
|
else break;
|
|
}
|
|
}
|
|
// is the point behind both lines?
|
|
return (PointOnLineSide(p,a) && PointOnLineSide(p,b));
|
|
}
|
|
|
|
// full reset of inventory (excluding collectibles, and optionally resetting the score)
|
|
static play void WipeInventory( Actor mo, bool resetscore = false, bool allplayers = false )
|
|
{
|
|
if ( allplayers )
|
|
{
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] || !players[i].mo ) continue;
|
|
WipeInventory(players[i].mo,resetscore,false);
|
|
}
|
|
return;
|
|
}
|
|
PlayerInfo p = mo.player;
|
|
if ( !p || !p.mo ) return;
|
|
SWWMCredits c = SWWMCredits.Find(p);
|
|
if ( resetscore && c ) c.credits = 0;
|
|
Actor last = p.mo;
|
|
while ( last.inv )
|
|
{
|
|
let inv = last.inv;
|
|
if ( !(inv is 'SWWMCollectible') )
|
|
{
|
|
inv.Destroy();
|
|
if ( !inv.bDestroyed ) last = inv;
|
|
}
|
|
else last = inv;
|
|
}
|
|
p.mo.GiveDefaultInventory();
|
|
p.mo.BringUpWeapon();
|
|
p.health = p.mo.Health = p.mo.SpawnHealth();
|
|
}
|
|
|
|
// sets all carried ammo back to zero
|
|
// resets hammerspace capacity
|
|
static play void ResetAmmo( Actor mo )
|
|
{
|
|
PlayerInfo p = mo.player;
|
|
if ( !p || !p.mo ) return;
|
|
for ( Inventory i=p.mo.inv; i; i=i.inv )
|
|
{
|
|
if ( (i is 'Ammo') || (i is 'MagAmmo') )
|
|
{
|
|
i.Amount = 0;
|
|
i.MaxAmount = i.default.MaxAmount;
|
|
}
|
|
if ( i is 'HammerspaceEmbiggener' )
|
|
i.Amount = 0;
|
|
}
|
|
// also gives back any ammo from carried weapons (provided they don't have NOFIRSTGIVE)
|
|
for ( Inventory i=p.mo.inv; i; i=i.inv )
|
|
{
|
|
if ( !(i is 'Weapon') ) continue;
|
|
let w = Weapon(i);
|
|
if ( (w is 'SWWMWeapon') && SWWMWeapon(w).bNOFIRSTGIVE ) continue;
|
|
if ( w.Ammo1 ) w.Ammo1.Amount = min(w.Ammo1.MaxAmount,w.Ammo1.Amount+w.AmmoGive1);
|
|
if ( w.Ammo2 && (w.Ammo2 != w.Ammo1) ) w.Ammo2.Amount = min(w.Ammo2.MaxAmount,w.Ammo2.Amount+w.AmmoGive2);
|
|
}
|
|
}
|
|
|
|
// removes all usable items
|
|
static play void ResetItems( Actor mo )
|
|
{
|
|
PlayerInfo p = mo.player;
|
|
if ( !p || !p.mo ) return;
|
|
Actor last = p.mo;
|
|
while ( last.inv )
|
|
{
|
|
let inv = last.inv;
|
|
if ( !inv.bINVBAR )
|
|
{
|
|
last = inv;
|
|
continue;
|
|
}
|
|
inv.Destroy();
|
|
if ( !inv.bDestroyed ) last = inv;
|
|
}
|
|
}
|
|
|
|
// resets health and removes worn armor
|
|
static play void ResetHealth( Actor mo )
|
|
{
|
|
PlayerInfo p = mo.player;
|
|
if ( !p || !p.mo ) return;
|
|
p.health = p.mo.health = 100;
|
|
for ( Inventory i=p.mo.inv; i; i=i.inv )
|
|
{
|
|
if ( !(i is 'SWWMArmor') ) continue;
|
|
i.Amount = 0;
|
|
}
|
|
}
|
|
|
|
// WHACK
|
|
static play void EndLevelDie( Actor victim )
|
|
{
|
|
victim.DamageMobj(null,null,victim.Health,'EndLevel',DMG_FORCED|DMG_THRUSTLESS);
|
|
}
|
|
|
|
// for Equinox
|
|
static play void SpawnVanillaBossBrain( int tid )
|
|
{
|
|
let ai = Level.CreateActorIterator(tid);
|
|
Actor a;
|
|
while ( a = ai.Next() )
|
|
{
|
|
let bb = a.Spawn("BossBrain",a.pos,NO_REPLACE);
|
|
bb.angle = a.angle;
|
|
}
|
|
}
|
|
|
|
// checks if we're playing in doom 1
|
|
// this is used so we can sometimes replace the shotgun with a SSG slot weapon
|
|
static bool IsDoomOne()
|
|
{
|
|
if ( !(gameinfo.GameType&GAME_DOOM) ) return false;
|
|
// is the map in ExMx format? Then it's likely we're playing a doom 1 map
|
|
if ( (level.mapname.Length() >= 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<Actor> 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<Inventory> itm, bool multi = false )
|
|
{
|
|
int np = 0;
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] ) continue;
|
|
np++;
|
|
}
|
|
int required = np;
|
|
if ( multi ) required *= GetDefaultByType(itm).MaxAmount;
|
|
// subtract all that exist already (either in world or owned)
|
|
let ti = ThinkerIterator.Create(itm);
|
|
Inventory i;
|
|
while ( i = Inventory(ti.Next()) )
|
|
{
|
|
if ( multi ) required -= i.Amount;
|
|
else required--;
|
|
}
|
|
// check travelling inventory separately, as by default iterators don't check anything below STAT_FIRST_THINKING
|
|
ti = ThinkerIterator.Create(itm,Thinker.STAT_TRAVELLING);
|
|
while ( i = Inventory(ti.Next()) )
|
|
{
|
|
if ( multi ) required -= i.Amount;
|
|
else required--;
|
|
}
|
|
return (required>0);
|
|
}
|
|
|
|
// 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
|
|
static bool ItemExists( Class<Inventory> itm, Inventory skipme = null, bool mapstart = false, bool worldonly = false )
|
|
{
|
|
let ti = ThinkerIterator.Create(itm);
|
|
Inventory i;
|
|
while ( i = Inventory(ti.Next()) )
|
|
{
|
|
if ( i == skipme ) continue;
|
|
if ( worldonly && i.Owner ) continue;
|
|
return true;
|
|
}
|
|
if ( worldonly || !mapstart ) return false;
|
|
ti = ThinkerIterator.Create(itm,Thinker.STAT_TRAVELLING);
|
|
while ( i = Inventory(ti.Next()) )
|
|
{
|
|
if ( i == skipme ) continue;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// multi-weapon spawn stuff
|
|
|
|
static private Class<Weapon> PickPair( Class<Weapon> a, Class<Weapon> 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<Weapon> PickSWWMSlot1()
|
|
{
|
|
// so the player can recover it if they decided to drop it in a previous map, or they didn't start with it
|
|
if ( CheckNeedsItem('DeepImpact') ) return 'DeepImpact';
|
|
if ( CheckNeedsItem('ItamexHammer') ) return 'ItamexHammer';
|
|
return PickSWWMSlot2();
|
|
}
|
|
// pistol spawn, pretty simple
|
|
static Class<Weapon> 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<Weapon> PickSWWMSlot3()
|
|
{
|
|
return PickPair('Spreadgun','PuntzerBeta');
|
|
}
|
|
// super shotgun spawn
|
|
static Class<Weapon> PickSWWMSlot4()
|
|
{
|
|
return PickPair('Wallbuster','PuntzerGamma');
|
|
}
|
|
// chaingun spawn
|
|
static Class<Weapon> PickSWWMSlot5()
|
|
{
|
|
return PickPair('Eviscerator','HeavyMahSheenGun');
|
|
}
|
|
// rocket launcher spawn
|
|
static Class<Weapon> PickSWWMSlot6()
|
|
{
|
|
return PickPair('Hellblazer','Quadravol');
|
|
}
|
|
// first plasma rifle spawn
|
|
static Class<Weapon> PickSWWMSlot7()
|
|
{
|
|
return PickPair('Sparkster','ModernSparkster');
|
|
}
|
|
// second plasma rifle spawn
|
|
static Class<Weapon> PickSWWMSlot8()
|
|
{
|
|
return PickPair('SilverBullet','RayKhom');
|
|
}
|
|
// first bfg spawn
|
|
static Class<Weapon> PickSWWMSlot9()
|
|
{
|
|
// 33% chance to still drop another candy gun if it's not at max capacity
|
|
if ( !Random[Replacements](0,2) && ItemExists('CandyGun') && CheckNeedsItem('CandyGunSpares',true) )
|
|
return 'CandyGun';
|
|
return PickPair('CandyGun','MisterRifle');
|
|
}
|
|
// second bfg spawn (each weapon can only exist once)
|
|
static Class<Weapon> 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<Weapon> 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<Weapon> 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<Weapon> 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;
|
|
}
|
|
}
|