- Fuck it, Quadravol will be lever action. - Tiny cleanup. - Rewrite the weapon replacement system (less of a mess now maybe?). - Some menu fixes. - Minimap zoom increments like in the Common Library. - Add missing sound definition for Safety Tether. (This mostly went unnoticed because it's VERY rare to have it play) - Shift Sparkster x3 (DLC2) to slot 7. This way you can have both it and the Quadravol simultaneously. It would be unfair to not let you hold both "iconic" UnSX weapons at once. - Small lore tweak on Quadravol stance swap. - Fix off-by-one bug in looping palette lights. - Re-do logo shader. Use separate layer textures. - Fix Elemental Coating breaking "End Level" damage sectors. (This will be the last batch of changes before I continue working on menus)
2351 lines
72 KiB
Text
2351 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
|
|
};
|
|
|
|
Class SWWMAchievement
|
|
{
|
|
int baseindex; // order of the achievement in the list
|
|
String basename;
|
|
transient CVar state, progress;
|
|
TextureID icon;
|
|
int maxval;
|
|
bool hasformat;
|
|
}
|
|
|
|
Struct SWWMProjectionData
|
|
{
|
|
swwm_GM_Matrix wtc;
|
|
int viewx, viewy, vieww, viewh;
|
|
}
|
|
|
|
Class SWWMUtility
|
|
{
|
|
// loaded from a file, neater than having a bunch of static arrays
|
|
// filter: excludes unobtained achievements if swwm_filterachievements is 2
|
|
// also excludes the "everything" achievement unless unlocked
|
|
static clearscope void LoadAchievements( out Array<SWWMAchievement> achievements, bool filter = false )
|
|
{
|
|
achievements.Clear();
|
|
let lmp = Wads.FindLump("achievements.lst");
|
|
if ( lmp == -1 ) ThrowAbortException("'achievements.lst' not found");
|
|
String dat;
|
|
Array<String> list, ln;
|
|
bool hide = (filter&&(swwm_filterachievements==2));
|
|
int bidx = 0;
|
|
while ( lmp != -1 )
|
|
{
|
|
dat = Wads.ReadLump(lmp);
|
|
// 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(1) == "#") || (list[i].Left(1) == "") )
|
|
continue;
|
|
ln.Clear();
|
|
list[i].Split(ln,",",0);
|
|
// game filtering
|
|
if ( !(gameinfo.gametype&GAME_DOOM) && (ln[3] ~== "doom") ) continue;
|
|
else if ( !(gameinfo.gametype&GAME_HERETIC) && (ln[3] ~== "heretic") ) continue;
|
|
else if ( !(gameinfo.gametype&GAME_HEXEN) && (ln[3] ~== "hexen") ) continue;
|
|
else if ( !(gameinfo.gametype&GAME_RAVEN) && (ln[3] ~== "raven") ) continue;
|
|
else if ( !(gameinfo.gametype&(GAME_DOOM|GAME_HERETIC)) && (ln[3] ~== "nothexen") ) continue;
|
|
let ac = new("SWWMAchievement");
|
|
ac.baseindex = bidx;
|
|
ac.basename = ln[0];
|
|
ac.maxval = ln[1].ToInt();
|
|
ac.state = CVar.FindCVar("swwm_achievement_"..ac.basename);
|
|
// if filtering, always hide the full completion achievement until it's unlocked
|
|
if ( filter && (ac.basename == "everything") && (ac.state.GetInt() <= 0) )
|
|
{
|
|
ac.Destroy();
|
|
continue;
|
|
}
|
|
if ( !ac.state ) ThrowAbortException("could not find cvar 'swwm_achievement_"..ac.basename.."'");
|
|
if ( ac.maxval )
|
|
{
|
|
ac.progress = CVar.FindCVar("swwm_progress_"..ac.basename);
|
|
if ( !ac.progress ) ThrowAbortException("could not find cvar 'swwm_progress_"..ac.basename.."'");
|
|
// special case for maxval
|
|
if ( ac.basename == "allcoll" )
|
|
{
|
|
int nc = 0;
|
|
for ( int i=0; i<AllActorClasses.Size(); i++ )
|
|
{
|
|
let c = (Class<SWWMCollectible>)(AllActorClasses[i]);
|
|
if ( !c || (c == 'SWWMCollectible') ) continue;
|
|
let def = GetDefaultByType(c);
|
|
// check that we can collect it in this IWAD
|
|
if ( !def.ValidGame() ) continue;
|
|
nc++;
|
|
}
|
|
ac.maxval = nc;
|
|
}
|
|
}
|
|
else ac.progress = null;
|
|
// hide away achievements at 0%
|
|
if ( hide && (ac.state.GetInt() <= 0) && (!ac.progress || (ac.progress.GetInt() <= 0)) )
|
|
{
|
|
ac.Destroy();
|
|
continue;
|
|
}
|
|
ac.hasformat = (ln[2]~=="yes");
|
|
ac.icon = TexMan.CheckForTexture("graphics/Achievements/Achievement"..ac.basename..".png",TexMan.Type_Any);
|
|
// fallback icon if one is not found
|
|
if ( !ac.icon.IsValid() ) ac.icon = TexMan.CheckForTexture("graphics/Achievements/DefaultAchievement.png",TexMan.Type_Any);
|
|
achievements.Push(ac);
|
|
bidx++;
|
|
}
|
|
lmp = Wads.FindLump("achievements.lst",lmp+1);
|
|
}
|
|
}
|
|
// achievement helpers
|
|
static clearscope void MarkAchievement( Name mvar, PlayerInfo p = null )
|
|
{
|
|
if ( !p || (p != players[consoleplayer]) ) return;
|
|
let cv = CVar.FindCVar(mvar);
|
|
if ( !cv )
|
|
{
|
|
if ( developer >= 2 ) Console.Printf("MarkAchievement: CVar '"..mvar.."' not found");
|
|
return;
|
|
}
|
|
if ( cv.GetInt() < 1 ) cv.SetInt(1);
|
|
}
|
|
static clearscope void AchievementProgress( Name pvar, int val, PlayerInfo p = null )
|
|
{
|
|
if ( !p || (p != players[consoleplayer]) ) return;
|
|
let cv = CVar.FindCVar(pvar);
|
|
if ( !cv )
|
|
{
|
|
if ( developer >= 2 ) Console.Printf("AchievementProgress: CVar '"..pvar.."' not found");
|
|
return;
|
|
}
|
|
int cval = cv.GetInt();
|
|
if ( val <= cval ) return;
|
|
cv.SetInt(val);
|
|
}
|
|
static clearscope void AchievementProgressInc( Name pvar, int inc, PlayerInfo p = null )
|
|
{
|
|
if ( !p || (p != players[consoleplayer]) ) return;
|
|
let cv = CVar.FindCVar(pvar);
|
|
if ( !cv )
|
|
{
|
|
if ( developer >= 2 ) Console.Printf("AchievementProgressInc: CVar '"..pvar.."' not found");
|
|
return;
|
|
}
|
|
int cval = cv.GetInt();
|
|
cv.SetInt(cval+inc);
|
|
}
|
|
static clearscope void AchievementProgressIncDouble( Name pvar, double inc, PlayerInfo p = null )
|
|
{
|
|
if ( !p || (p != players[consoleplayer]) ) return;
|
|
let cv = CVar.FindCVar(pvar);
|
|
if ( !cv )
|
|
{
|
|
if ( developer >= 2 ) Console.Printf("AchievementProgressIncDouble: CVar '"..pvar.."' not found");
|
|
return;
|
|
}
|
|
double cval = cv.GetFloat();
|
|
cv.SetFloat(cval+inc);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// thanks zscript
|
|
static clearscope double fract( double a )
|
|
{
|
|
return (a<0)?(a+floor(a)):(a-floor(a));
|
|
}
|
|
|
|
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 )
|
|
{
|
|
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 )
|
|
s.AppendCharacter(0x2C); // comma
|
|
}
|
|
}
|
|
static clearscope String ThousandsNum( int n )
|
|
{
|
|
String nstr = String.Format("%d",n);
|
|
ThousandsStr(nstr);
|
|
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 String BlockBar( int a, int b, int width, int acol, int bcol )
|
|
{
|
|
String str = "";
|
|
int blocks = clamp(int(a/double(b)*width),0,width);
|
|
int eblocks = width-blocks;
|
|
if ( blocks )
|
|
{
|
|
str.AppendCharacter(0x1C);
|
|
str.AppendCharacter(0x41+acol);
|
|
}
|
|
for ( int i=0; i<blocks; i++ ) str.AppendCharacter(0x258F);
|
|
if ( eblocks )
|
|
{
|
|
str.AppendCharacter(0x1C);
|
|
str.AppendCharacter(0x41+bcol);
|
|
}
|
|
for ( int i=0; i<eblocks; i++ ) str.AppendCharacter(0x258F);
|
|
if ( blocks || eblocks )
|
|
{
|
|
str.AppendCharacter(0x1C);
|
|
str.AppendCharacter(0x2D);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
static clearscope void ObscureText( out String str, int seed )
|
|
{
|
|
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 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;
|
|
}
|
|
|
|
static clearscope bool IsExitLine( Line l )
|
|
{
|
|
if ( (l.special == Exit_Normal) || (l.special == Exit_Secret) || (l.special == Teleport_EndGame) || (l.special == Teleport_NewMap) )
|
|
return true;
|
|
// E1M8 compat
|
|
if ( (l.special == ACS_Execute) && (l.Args[0] == -Int('E1M8_KNOCKOUT')) )
|
|
return true;
|
|
// spooktober™
|
|
if ( ((l.special == ACS_Execute) || (l.special == ACS_ExecuteAlways)) && (l.Args[0] == -Int('MapFadeOut')) )
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
if ( p < 0 )
|
|
{
|
|
r = q/p;
|
|
if ( r > t1 ) return false;
|
|
else if ( r > t0 ) t0 = r;
|
|
}
|
|
else if ( p > 0 )
|
|
{
|
|
r = q/p;
|
|
if ( r < t0 ) return false;
|
|
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 int Round100( double x )
|
|
{
|
|
return int(ceil(x/100.)*100.);
|
|
}
|
|
|
|
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( Actor a, String defstr = "" )
|
|
{
|
|
// look up any fun tag services
|
|
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 )
|
|
{
|
|
// 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;
|
|
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;
|
|
// 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,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 ( (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) ) 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;
|
|
// 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 ndmg = a.DamageMobj(Source,Instigator,dmg,(DamageType=='')?Source.DamageType:DamageType,DMG_EXPLOSION,atan2(-dir.y,-dir.x));
|
|
if ( a && !(flags&DE_NOBLEED) ) a.TraceBleed((ndmg>0)?ndmg:dmg,Source);
|
|
if ( (flags&DE_HOWL) && a && (a.Health > 0) && a.bISMONSTER && !Random[DoBlast](0,3) ) a.Howl();
|
|
if ( (!a || (a.Health <= 0)) && (!(flags&DE_COUNTENEMIES) || hostile) && (!(flags&DE_COUNTSTEALTH) || inactive) ) nkill++;
|
|
}
|
|
}
|
|
return nhit, nkill;
|
|
}
|
|
|
|
static play bool InPlayerFOV( PlayerInfo p, Actor a, double maxdist = 0. )
|
|
{
|
|
double vfov = p.fov*.5;
|
|
double hfov = atan(Screen.GetAspectRatio()*tan(vfov));
|
|
let mo = p.camera;
|
|
if ( !mo ) return false;
|
|
Vector3 pp;
|
|
if ( !mo.CheckSight(a,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) return false;
|
|
if ( mo is 'PlayerPawn' ) pp = mo.Vec2OffsetZ(0,0,PlayerPawn(mo).player.viewz);
|
|
else pp = mo.Vec3Offset(0,0,mo.CameraHeight);
|
|
Vector3 sc = level.SphericalCoords(pp,a.pos,(mo.angle,mo.pitch));
|
|
if ( (abs(sc.x) > hfov) || (abs(sc.y) > vfov) ) return false;
|
|
if ( (maxdist > 0.) && (sc.z > maxdist) ) return false;
|
|
return true;
|
|
}
|
|
|
|
// 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.CameraHeight);
|
|
Vector3 sc = level.SphericalCoords(pp,a.pos,(mo.angle,mo.pitch));
|
|
if ( (abs(sc.x) > hfov) || (abs(sc.y) > vfov) ) return false;
|
|
if ( (maxdist > 0.) && (sc.z > maxdist) ) return false;
|
|
return true;
|
|
}
|
|
|
|
static clearscope bool CheatsDisabled( int p = -1 )
|
|
{
|
|
if ( cl_blockcheats || ((G_SkillPropertyInt(SKILLP_DisableCheats) || netgame || deathmatch) && !sv_cheats) )
|
|
{
|
|
if ( (p != -1) && (p == consoleplayer) )
|
|
{
|
|
Console.Printf("\cxSORRY NOTHING\c-");
|
|
S_StartSound("misc/trombone",CHAN_VOICE,CHANF_UI);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// 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 'PusherProjectile' ) 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;
|
|
}
|
|
|
|
// 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 )
|
|
{
|
|
// no gendered string alt
|
|
if ( StringTable.Localize("$SWWM_SELLEXTRA_FEM") == "SWWM_SELLEXTRA_FEM" )
|
|
return false;
|
|
if ( i is 'DeepImpact' ) return true;
|
|
if ( i is 'FistGun' ) 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 'EMPCarbine' ) return true;
|
|
if ( i is 'CandyGun' ) return true;
|
|
if ( i is 'RayKhom' ) return true;
|
|
if ( i is 'GrandLance' ) return true;
|
|
if ( i is 'RayKhom' ) 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.hcredits = 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 'GrandLance') || (target is 'RayKhom') )
|
|
return true;
|
|
if ( (target is 'GoldShell') || (target is 'YnykronAmmo') || (target is 'GrandAmmo') || (target is 'GrandSpear') || (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 'GrandLance') || (target is 'RayKhom') )
|
|
return true;
|
|
if ( (target is 'GoldShell') || (target is 'YnykronAmmo') || (target is 'GrandAmmo') || (target is 'GrandSpear') || (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 b;
|
|
return Random[Replacements](0,1)?a:b;
|
|
}
|
|
static private Class<Weapon> PickTrio( Class<Weapon> a, Class<Weapon> b, Class<Weapon> c )
|
|
{
|
|
if ( ItemExists(a,mapstart:true) )
|
|
{
|
|
if ( ItemExists(b,mapstart:true) )
|
|
return c;
|
|
return Random[Replacements](0,1)?b:c;
|
|
}
|
|
if ( ItemExists(b,mapstart:true) )
|
|
{
|
|
if ( ItemExists(c,mapstart:true) )
|
|
return a;
|
|
return Random[Replacements](0,1)?a:c;
|
|
}
|
|
if ( ItemExists(c,mapstart:true) )
|
|
Random[Replacements](0,1)?a:b;
|
|
switch ( Random[Replacements](0,2) )
|
|
{
|
|
case 0:
|
|
return a;
|
|
case 1:
|
|
return b;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// melee weapons + extra slot 2 guns (50% chance)
|
|
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';
|
|
bool hasmelee = (!CheckNeedsItem('PusherWeapon')/*||!CheckNeedsItem('ItamexHammer')||!CheckNeedsItem('FistGun')*/);
|
|
bool gunexists = ItemExists('ExplodiumGun',worldonly:true)/*||ItemExists('PlasmaBlast',worldonly:true)*/;
|
|
// if the player already has a melee weapon, 50% chance to spawn either slot 2 weapon as long as one doesn't exist in the map already
|
|
if ( hasmelee && !gunexists && Random[Replacements](0,1) )
|
|
return PickSWWMSlot2();
|
|
//return PickTrio('PusherWeapon','ItamexHammer','FistGun');
|
|
//return PickPair('PusherWeapon','ItamexHammer');
|
|
return 'PusherWeapon';
|
|
}
|
|
// pistol spawn, pretty simple
|
|
static Class<Weapon> PickSWWMSlot2()
|
|
{
|
|
//return PickPair('ExplodiumGun','PlasmaBlast');
|
|
return 'ExplodiumGun';
|
|
}
|
|
// shotgun spawn
|
|
static Class<Weapon> PickSWWMSlot3()
|
|
{
|
|
//return PickPair('Spreadgun','PuntzerBeta');
|
|
return 'Spreadgun';
|
|
}
|
|
// super shotgun spawn
|
|
static Class<Weapon> PickSWWMSlot4()
|
|
{
|
|
//return PickPair('Wallbuster','PuntzerGamma');
|
|
return 'Wallbuster';
|
|
}
|
|
// chaingun spawn
|
|
static Class<Weapon> PickSWWMSlot5()
|
|
{
|
|
//return PickPair('Eviscerator','HeavyMahSheenGun');
|
|
return 'Eviscerator';
|
|
}
|
|
// rocket launcher spawn
|
|
static Class<Weapon> PickSWWMSlot6()
|
|
{
|
|
//return PickTrio('Hellblazer','Quadravol','ModernSparkster');
|
|
//return PickPair('Hellblazer','Quadravol');
|
|
return 'Hellblazer';
|
|
}
|
|
// first plasma rifle spawn
|
|
static Class<Weapon> PickSWWMSlot7()
|
|
{
|
|
//return PickPair('Sparkster','BlackfireIgniter');
|
|
return 'Sparkster';
|
|
}
|
|
// second plasma rifle spawn
|
|
static Class<Weapon> PickSWWMSlot8()
|
|
{
|
|
//return PickPair('SilverBullet','EMPCarbine');
|
|
return 'SilverBullet';
|
|
}
|
|
// first bfg spawn
|
|
static Class<Weapon> PickSWWMSlot9()
|
|
{
|
|
//return PickTrio('CandyGun','RayKhom','MisterRifle');
|
|
//return PickPair('CandyGun','RayKhom');
|
|
return 'CandyGun';
|
|
}
|
|
// second bfg spawn (each weapon can only exist once)
|
|
static Class<Weapon> PickSWWMSlot0( bool fallback = true )
|
|
{
|
|
/*if ( ItemExists('Ynykron',mapstart:true) )
|
|
{
|
|
if ( ItemExists('GrandLance',mapstart:true) )
|
|
{
|
|
if ( ItemExists('RafanKos',null,true) )
|
|
return fallback?PickSWWMSlot9():null;
|
|
return 'RafanKos';
|
|
}
|
|
if ( ItemExists('RafanKos',mapstart:true) )
|
|
return 'GrandLance';
|
|
return Random[Replacements](0,1)?'GrandLance':'RafanKos';
|
|
}
|
|
if ( ItemExists('GrandLance',mapstart:true) )
|
|
{
|
|
if ( ItemExists('RafanKos',mapstart:true) )
|
|
return 'Ynykron';
|
|
return Random[Replacements](0,1)?'Ynykron':'RafanKos';
|
|
}
|
|
if ( ItemExists('RafanKos',mapstart:truee) )
|
|
return Random[Replacements](0,1)?'Ynykron':'GrandLance';
|
|
switch ( Random[Replacements](0,2) )
|
|
{
|
|
case 0:
|
|
return 'Ynykron';
|
|
case 1:
|
|
return 'GrandLance';
|
|
}
|
|
return 'RafanKos';*/
|
|
/*if ( ItemExists('Ynykron',mapstart:true) )
|
|
{
|
|
if ( ItemExists('GrandLance',mapstart:true) )
|
|
return fallback?PickSWWMSlot9():null;
|
|
return 'GrandLance';
|
|
}
|
|
if ( ItemExists('GrandLance',mapstart:true) )
|
|
return 'Ynykron';
|
|
return Random[Replacements](0,1)?'Ynykron':'GrandLance';*/
|
|
if ( ItemExists('Ynykron',mapstart:true) ) return fallback?PickSWWMSlot9():null;
|
|
return 'Ynykron';
|
|
}
|
|
// either plasma rifle spawn
|
|
static Class<Weapon> PickDoomSlot6()
|
|
{
|
|
bool hasslot7 = (!CheckNeedsItem('Sparkster')/*||!CheckNeedsItem('BlackfireIgniter')*/);
|
|
bool hasslot8 = (!CheckNeedsItem('SilverBullet')/*||!CheckNeedsItem('EMPCarbine')*/);
|
|
// 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('RayKhom')||!CheckNeedsItem('MortalRifle')*/);
|
|
bool hasslot0 = (!CheckNeedsItem('Ynykron')/*||!CheckNeedsItem('GrandLance')||!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 ShinemapDebugSphere : Actor
|
|
{
|
|
override bool Used( Actor user )
|
|
{
|
|
if ( CurState.NextState )
|
|
SetState(CurState.NextState);
|
|
else SetState(SpawnState);
|
|
return true;
|
|
}
|
|
override void Tick() {}
|
|
Default
|
|
{
|
|
RenderStyle "Add";
|
|
Radius 16;
|
|
Height 48;
|
|
}
|
|
States
|
|
{
|
|
Spawn:
|
|
XZW1 A -1 Bright NoDelay A_SetRenderStyle(1.,STYLE_Add);
|
|
XZW1 B -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 C -1 Bright A_SetRenderStyle(1.,STYLE_Add);
|
|
XZW1 D -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 E -1 Bright A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 F -1 A_SetRenderStyle(1.,STYLE_Add);
|
|
XZW1 G -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 H -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 I -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 J -1 Bright A_SetRenderStyle(1.,STYLE_Add);
|
|
XZW1 K -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 L -1 Bright A_SetRenderStyle(1.,STYLE_Add);
|
|
XZW1 M -1 Bright A_SetRenderStyle(1.,STYLE_Add);
|
|
XZW1 N -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 O -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 P -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 Q -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 R -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 S -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
XZW1 T -1 A_SetRenderStyle(1.,STYLE_Normal);
|
|
Loop;
|
|
}
|
|
}
|