So... Remember that one decision I made about avoiding BlockThingsIterator as much as possible? Turns out that was a stupid idea. There ARE situations where it's better to iterate sector thinglists, yes, especially for things that are NOT part of the blockmap, but in other cases, the excess allocations of new iterators are a reasonable price to pay for the lower perf impact in extreme cases, such as maps that have a gazillion sectors with gazillions of things in them (I'm looking at you, UDMF mappers). As a compromise, at least, in situations where the thinglists are needed, I did add a sort of micro-optimization by implementing code to check if a bounding box is inside a sector (would be nice if this was part of GZDoom itself, tho).
390 lines
14 KiB
Text
390 lines
14 KiB
Text
// WorldThing* events (excluding damage)
|
|
|
|
extend Class SWWMHandler
|
|
{
|
|
// prevents revived monsters from spawning in more golden shells
|
|
Array<Actor> alreadygold;
|
|
|
|
// legendary monster markers (for the "has mutated" message)
|
|
Array<Inventory> legtrack;
|
|
|
|
override void WorldThingRevived( WorldEvent e )
|
|
{
|
|
if ( profiling ) ProfileTick();
|
|
// reattach headpats
|
|
if ( SWWMUtility.IdentifyingDog(e.Thing) || SWWMUtility.IdentifyingCaco(e.Thing)
|
|
|| SWWMUtility.IdentifyingDrug(e.Thing) || SWWMUtility.IdentifyingDoubleBoi(e.Thing) )
|
|
{
|
|
// you can pet the dog, and you can also pet the caco (and friends)
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
}
|
|
if ( !(e.Thing is 'PlayerPawn') )
|
|
{
|
|
if ( profiling ) ProfileTock(PT_WORLDTHINGREVIVED);
|
|
return;
|
|
}
|
|
// reset some vars
|
|
if ( e.Thing.playernumber() != -1 )
|
|
{
|
|
multilevel[e.Thing.playernumber()] = 0;
|
|
spreecount[e.Thing.playernumber()] = 0;
|
|
tookdamage[e.Thing.playernumber()] = false;
|
|
lastkill[e.Thing.playernumber()] = int.min;
|
|
}
|
|
// reset uptime since player had just died
|
|
SWWMStats s = SWWMStats.Find(e.Thing.player);
|
|
if ( s ) s.lastspawn = level.totaltime;
|
|
if ( profiling ) ProfileTock(PT_WORLDTHINGREVIVED);
|
|
}
|
|
|
|
private bool HexenMap40()
|
|
{
|
|
if ( level.GetChecksum() ~== "2A6C4235B942467D25FD50D5B313E67A" ) return true; // 1.1
|
|
if ( level.GetChecksum() ~== "1C5DE5A921DEE405E98E7E09D9829387" ) return true; // 1.0
|
|
if ( level.GetChecksum() ~== "EFAFE59092DE5E613562ACF52B86C37F" ) return true; // beta
|
|
return false;
|
|
}
|
|
|
|
static int ShouldSpawnGold()
|
|
{
|
|
int totalneeded = 0;
|
|
// check "free space" in player inventories
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] || !players[i].mo ) continue;
|
|
let cg = players[i].mo.FindInventory("GoldShell");
|
|
if ( cg ) totalneeded += cg.MaxAmount-cg.Amount;
|
|
else totalneeded = GetDefaultByType("GoldShell").MaxAmount;
|
|
}
|
|
// subtract any shells already in the world
|
|
let ti = ThinkerIterator.Create("GoldShell");
|
|
GoldShell g;
|
|
while ( g = GoldShell(ti.Next()) )
|
|
{
|
|
if ( g.Owner ) continue;
|
|
totalneeded -= g.Amount;
|
|
}
|
|
return max(0,totalneeded);
|
|
}
|
|
|
|
override void WorldThingDied( WorldEvent e )
|
|
{
|
|
if ( profiling ) ProfileTick();
|
|
if ( e.Thing.default.bISMONSTER && e.Thing.default.bCOUNTKILL && ((e.Thing.default.bBOSS) || (e.Thing.GetSpawnHealth() >= 1000) || e.Thing.FindInventory("BossMarker")) && (alreadygold.Find(e.Thing) == alreadygold.Size()) )
|
|
{
|
|
// make sure we can't farm drops from revivable enemies
|
|
// (or cause some things to spam-spawn gold shells)
|
|
// (*cough* Touhou Doom *cough*)
|
|
alreadygold.Push(e.Thing);
|
|
// weight drop chance based on total count of this monster type
|
|
// guarantees that maps that get a bit slaughtery won't become easy farms for drops
|
|
int dropweight = 0;
|
|
let ti = ThinkerIterator.Create(e.Thing.GetClass());
|
|
while ( ti.Next() ) dropweight++;
|
|
int minchance = max(1,6-(e.Thing.GetSpawnHealth()/1000));
|
|
dropweight = max(minchance,dropweight/2);
|
|
// make sure the gold shell is "worth spawning", too
|
|
// (also, chance should be reduced as you acquire more shells)
|
|
int gchance = int(ceil(ShouldSpawnGold()/2.));
|
|
if ( !Random[GoldDrop](0,dropweight) && Random[GoldDrop](0,gchance) )
|
|
{
|
|
let g = Actor.Spawn("GoldShell",e.Thing.Vec3Offset(0,0,e.Thing.Height/2.));
|
|
double ang = FRandom[SpareShells](0.,360.);
|
|
g.vel.xy = Actor.AngleToVector(ang,FRandom[SpareShells](.4,.8));
|
|
g.vel.z = FRandom[SpareShells](2.,4.);
|
|
}
|
|
}
|
|
// Korax instakill
|
|
if ( (e.Thing is 'Korax') && !e.Thing.special2 && HexenMap40() )
|
|
{
|
|
e.Thing.special2 = 1;
|
|
// terminate the monster closet scripts, open all the
|
|
// doors ourselves
|
|
level.ExecuteSpecial(ACS_Terminate,e.Thing,null,false,220);
|
|
level.ExecuteSpecial(ACS_Terminate,e.Thing,null,false,220);
|
|
level.ExecuteSpecial(ACS_Terminate,e.Thing,null,false,221);
|
|
level.ExecuteSpecial(ACS_Terminate,e.Thing,null,false,255);
|
|
level.ExecuteSpecial(Door_Open,e.Thing,null,false,10,16);
|
|
level.ExecuteSpecial(Door_Open,e.Thing,null,false,11,16);
|
|
level.ExecuteSpecial(Door_Open,e.Thing,null,false,12,16);
|
|
level.ExecuteSpecial(Door_Open,e.Thing,null,false,13,16);
|
|
level.ExecuteSpecial(Door_Open,e.Thing,null,false,14,16);
|
|
level.ExecuteSpecial(Door_Open,e.Thing,null,false,10,16);
|
|
// keep the portal closed, you can't leave unless you
|
|
// kill everyone else
|
|
let t = new("KoraxYeeted");
|
|
t.ChangeStatNum(Thinker.STAT_USER);
|
|
}
|
|
// Archangelus death
|
|
if ( e.Thing.GetClassName() == "ArchangelusB" )
|
|
{
|
|
// kill all other monsters
|
|
let ti = ThinkerIterator.Create("Actor");
|
|
Actor a;
|
|
while ( a = Actor(ti.Next()) )
|
|
{
|
|
if ( (a != e.Thing) && (a.Health > 0) && (a.bBossSpawned || a.bCOUNTKILL) )
|
|
a.DamageMobj(e.Thing,e.Thing,a.Health,'EndMii',DMG_FORCED|DMG_THRUSTLESS);
|
|
}
|
|
}
|
|
if ( swwm_partytime )
|
|
{
|
|
let pt = Actor.Spawn("PartyTime",e.Thing.pos);
|
|
pt.target = e.Thing;
|
|
}
|
|
if ( profiling ) ProfileTock(PT_WORLDTHINGDIED);
|
|
}
|
|
|
|
private void DoKeyTagFix( Actor a )
|
|
{
|
|
if ( a is 'SWWMKey' ) return; // mod's custom keys are fine
|
|
if ( a is 'RedCard' ) a.SetTag("$T_REDCARD");
|
|
else if ( a is 'BlueCard' ) a.SetTag("$T_BLUECARD");
|
|
else if ( a is 'YellowCard' ) a.SetTag("$T_YELLOWCARD");
|
|
else if ( a is 'RedSkull' ) a.SetTag("$T_REDSKULL");
|
|
else if ( a is 'BlueSkull' ) a.SetTag("$T_BLUESKULL");
|
|
else if ( a is 'YellowSkull' ) a.SetTag("$T_YELLOWSKULL");
|
|
else if ( a is 'KeyYellow' ) a.SetTag("$T_KEYYELLOW");
|
|
else if ( a is 'KeyGreen' ) a.SetTag("$T_KEYGREEN");
|
|
else if ( a is 'KeyBlue' ) a.SetTag("$T_KEYBLUE");
|
|
else if ( a.GetClassName() == 'KeyRed' ) a.SetTag("$T_KEYRED");
|
|
else if ( a is 'KeySteel' ) a.SetTag("$T_KEYSTEEL");
|
|
else if ( a is 'KeyCave' ) a.SetTag("$T_KEYCAVE");
|
|
else if ( a is 'KeyAxe' ) a.SetTag("$T_KEYAXE");
|
|
else if ( a is 'KeyFire' ) a.SetTag("$T_KEYFIRE");
|
|
else if ( a is 'KeyEmerald' ) a.SetTag("$T_KEYEMERALD");
|
|
else if ( a is 'KeyDungeon' ) a.SetTag("$T_KEYDUNGEON");
|
|
else if ( a is 'KeySilver' ) a.SetTag("$T_KEYSILVER");
|
|
else if ( a is 'KeyRusted' ) a.SetTag("$T_KEYRUSTED");
|
|
else if ( a is 'KeyHorn' ) a.SetTag("$T_KEYHORN");
|
|
else if ( a is 'KeySwamp' ) a.SetTag("$T_KEYSWAMP");
|
|
else if ( a is 'KeyCastle' ) a.SetTag("$T_KEYCASTLE");
|
|
}
|
|
|
|
// tempfix keys have no tags
|
|
static void KeyTagFix( Actor a )
|
|
{
|
|
let hnd = SWWMHandler(Find("SWWMHandler"));
|
|
if ( hnd ) hnd.DoKeyTagFix(a);
|
|
}
|
|
|
|
// copies the floatbob of overlapping identical items, so it doesn't look weird
|
|
private void CopyFloatBob( Actor a )
|
|
{
|
|
for ( Actor t=a.CurSector.thinglist; t; t=t.snext )
|
|
{
|
|
if ( (t == a) || !(t is 'Inventory') || !(t.spawnpoint ~== a.spawnpoint) ) continue;
|
|
a.floatbobphase = t.floatbobphase;
|
|
a.angle = t.angle; // also copy angle
|
|
break;
|
|
}
|
|
}
|
|
|
|
private bool SuppressMultiItem( WorldEvent e )
|
|
{
|
|
// quick checks
|
|
if ( !G_SkillPropertyInt(SKILLP_SpawnMulti) || multiplayer || sv_alwaysspawnmulti )
|
|
return false;
|
|
// is it coop inventory? suppress it
|
|
if ( (e.Thing.spawnflags&(MTF_COOPERATIVE|MTF_DEATHMATCH)) && !(e.Thing.spawnflags&MTF_SINGLE) && ((e.Thing is 'Inventory') || (e.Thing is 'MaceSpawner')) )
|
|
{
|
|
e.Thing.ClearCounters();
|
|
e.Thing.Destroy();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
override void WorldThingDestroyed( WorldEvent e )
|
|
{
|
|
if ( profiling ) ProfileTick();
|
|
// for gibber throttling
|
|
if ( e.Thing is 'mkBloodDrop' ) blods_realcnt--;
|
|
else if ( e.Thing is 'mkFlyingGib' ) meats_realcnt--;
|
|
if ( profiling ) ProfileTock(PT_WORLDTHINGDESTROYED);
|
|
}
|
|
|
|
override void WorldThingSpawned( WorldEvent e )
|
|
{
|
|
if ( profiling ) ProfileTick();
|
|
// for gibber throttling
|
|
if ( e.Thing is 'mkBloodDrop' ) blods_realcnt++;
|
|
else if ( e.Thing is 'mkFlyingGib' ) meats_realcnt++;
|
|
if ( !e.Thing || SuppressMultiItem(e) )
|
|
{
|
|
if ( profiling ) ProfileTock(PT_WORLDTHINGSPAWNED);
|
|
return;
|
|
}
|
|
IWantDieSpawn(e);
|
|
if ( (e.Thing is 'TeleportDest') || (e.Thing is 'BossTarget') )
|
|
{
|
|
let d = Actor.Spawn("SWWMTeleportDest",e.Thing.pos);
|
|
d.bNOGRAVITY = e.Thing.bNOGRAVITY;
|
|
}
|
|
if ( e.Thing is 'Inventory' )
|
|
{
|
|
CopyFloatBob(e.Thing);
|
|
if ( Inventory(e.Thing).PickupFlash is 'SWWMPickupFlash' )
|
|
{
|
|
let p = Actor.Spawn(Inventory(e.Thing).PickupFlash,e.Thing.pos);
|
|
p.target = e.Thing;
|
|
p.SetStateLabel("Pickup");
|
|
}
|
|
// for notification
|
|
if ( e.Thing.GetClassName() == "LDLegendaryMonsterTransformed" )
|
|
legtrack.Push(Inventory(e.Thing));
|
|
}
|
|
if ( swwm_doomfall && e.Thing.bISMONSTER && !e.Thing.bBOSS )
|
|
e.Thing.bFALLDAMAGE = true;
|
|
if ( e.Thing is 'Key' )
|
|
{
|
|
DoKeyTagFix(e.Thing);
|
|
SWWMInterest.Spawn(self,thekey:Key(e.Thing));
|
|
}
|
|
if ( indoomvacation == -1 ) indoomvacation = SWWMUtility.InDoomVacation();
|
|
if ( inultdoom2 == -1 ) inultdoom2 = SWWMUtility.IsUltDoom2();
|
|
if ( e.Thing.GetClass() == 'Pig' )
|
|
e.Thing.SetTag("$FN_PIG"); // missing in gzdoom
|
|
// nashgore compat
|
|
if ( (e.Thing.GetClassName() == "NashGoreFootprint")
|
|
|| (e.Thing.GetClassName() == "NashGoreFootprintLeft")
|
|
|| (e.Thing.GetClassName() == "NashGoreFootprintRight") )
|
|
e.Thing.A_ChangeModel("",0,"","",0,"models/nashgore/Footprint","DemoFootprint.png");
|
|
// eviternity stuff
|
|
else if ( (e.Thing.GetClassName() == "Archangelus")
|
|
|| (e.Thing.GetClassName() == "ArchangelusA")
|
|
|| (e.Thing.GetClassName() == "ArchangelusB") )
|
|
e.Thing.SetTag("$FN_ANGEL");
|
|
else if ( e.Thing.GetClassName() == "AstralCaco" )
|
|
e.Thing.SetTag("$FN_ASTRAL");
|
|
else if ( e.Thing.GetClassName() == "Annihilator" )
|
|
e.Thing.SetTag("$FN_ANNIHIL");
|
|
else if ( e.Thing.GetClassName() == "FormerCaptain" )
|
|
e.Thing.SetTag("$FN_FCAPTAIN");
|
|
else if ( e.Thing.GetClassName() == "NightmareDemon" )
|
|
e.Thing.SetTag("$FN_NDEMON");
|
|
// doom vacation stuff
|
|
else if ( indoomvacation )
|
|
{
|
|
if ( e.Thing.GetClassName() == "Babe" )
|
|
{
|
|
e.Thing.bSHOOTABLE = false; // no hurt
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = .2;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "CommanderKeen" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = .4;
|
|
HeadpatTracker(hp).angfix = 5;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "BBChair" )
|
|
{
|
|
e.Thing.bUSESPECIAL = false;
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = .2;
|
|
HeadpatTracker(hp).angfix = 15;
|
|
HeadpatTracker(hp).patstate = e.Thing.MeleeState;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "EvilEye" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = .1;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "HeadCandles" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).angfix = 20;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "HeartColumn" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = -.3;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "Meat2" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = .6;
|
|
HeadpatTracker(hp).angfix = -15;
|
|
HeadpatTracker(hp).dvacationarghack = true;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "Meat3" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = .6;
|
|
HeadpatTracker(hp).angfix = 20;
|
|
HeadpatTracker(hp).dvacationarghack = true;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "LegsBabe" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = -1.5;
|
|
HeadpatTracker(hp).angfix = 20;
|
|
HeadpatTracker(hp).dvacationarghack = true;
|
|
}
|
|
else if ( e.Thing.GetClassName() == "Meat4" )
|
|
{
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
HeadpatTracker(hp).heightfix = .6;
|
|
HeadpatTracker(hp).angfix = 15;
|
|
HeadpatTracker(hp).dvacationarghack = true;
|
|
}
|
|
}
|
|
else if ( inultdoom2 && (e.Thing.GetClassName() == "WolfensteinSS") )
|
|
{
|
|
e.Thing.SetTag("$FN_ELITEZOMBIE");
|
|
//e.Thing.Obituary = "$OB_ELITEZOMBIE";
|
|
e.Thing.SeeSound = "grunt/sight";
|
|
e.Thing.AttackSound = "grunt/attack";
|
|
e.Thing.PainSound = "grunt/pain";
|
|
e.Thing.DeathSound = "grunt/death";
|
|
e.Thing.ActiveSound = "grunt/active";
|
|
}
|
|
else if ( ccloaded && (e.Thing.GetClassName() == "CCards_Token_Glitched") )
|
|
{
|
|
if ( !gdat.cclilithonce ) SendInterfaceEvent(consoleplayer,"swwmsetdialogue.LILITH");
|
|
gdat.cclilithonce = true;
|
|
}
|
|
if ( SWWMUtility.IdentifyingDog(e.Thing) || SWWMUtility.IdentifyingCaco(e.Thing)
|
|
|| SWWMUtility.IdentifyingDrug(e.Thing) || SWWMUtility.IdentifyingDoubleBoi(e.Thing) )
|
|
{
|
|
// you can pet the dog, and you can also pet the caco (and friends)
|
|
let hp = Actor.Spawn("HeadpatTracker",e.Thing.pos);
|
|
hp.target = e.Thing;
|
|
}
|
|
// vanilla blood color changes
|
|
if ( (e.Thing.GetClass() == "BaronOfHell") || (e.Thing.GetClass() == "HellKnight") || (e.Thing.GetClass() == "Bishop") || (e.Thing.GetClass() == "Korax") )
|
|
{
|
|
let gb = Actor.Spawn("GreenBloodReference");
|
|
e.Thing.CopyBloodColor(gb);
|
|
gb.Destroy();
|
|
}
|
|
else if ( e.Thing.GetClass() == "Cacodemon" )
|
|
{
|
|
let bb = Actor.Spawn("BlueBloodReference");
|
|
e.Thing.CopyBloodColor(bb);
|
|
bb.Destroy();
|
|
}
|
|
else if ( (e.Thing.GetClass() == "Wizard") || (e.Thing.GetClass() == "Heresiarch") || (e.Thing.GetClass() == "Sorcerer2") )
|
|
{
|
|
let pb = Actor.Spawn("PurpleBloodReference");
|
|
e.Thing.CopyBloodColor(pb);
|
|
pb.Destroy();
|
|
}
|
|
else if ( e.Thing.GetClass() == "LostSoul" )
|
|
e.Thing.bNOBLOOD = true;
|
|
VanillaBossSpawn(e);
|
|
if ( profiling ) ProfileTock(PT_WORLDTHINGSPAWNED);
|
|
}
|
|
}
|