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).
355 lines
11 KiB
Text
355 lines
11 KiB
Text
// WorldTick functions
|
|
|
|
extend Class SWWMHandler
|
|
{
|
|
transient Array<Actor> combatactors;
|
|
transient Array<Int> combattics;
|
|
transient int highesttic, lastcombat;
|
|
int lastitemcount[MAXPLAYERS];
|
|
transient String curlang;
|
|
transient bool curfuntags;
|
|
SWWMSimpleTracker strackers;
|
|
bool mnotify;
|
|
bool allkills, allitems, allsecrets;
|
|
bool mapclear;
|
|
int mapclearagain, restartmus, startmus;
|
|
double musvol;
|
|
String lastmus;
|
|
int lastorder;
|
|
bool lastloop;
|
|
transient ThinkerIterator cti, qti;
|
|
|
|
private void LangRefresh()
|
|
{
|
|
if ( (language != curlang) || (swwm_funtags != curfuntags) )
|
|
{
|
|
// manually refresh some tags if language has changed
|
|
if ( !qti ) qti = ThinkerIterator.Create("SWWMQuickCombatTracker",Thinker.STAT_INVENTORY);
|
|
else qti.Reinit();
|
|
SWWMQuickCombatTracker qt;
|
|
while ( qt=SWWMQuickCombatTracker(qti.Next()) )
|
|
qt.UpdateTag(self);
|
|
for ( SWWMInterest p=intpoints; p; p=p.next )
|
|
{
|
|
if ( (p.type != INT_Key) || !p.trackedkey ) continue;
|
|
p.keytag = p.trackedkey.GetTag();
|
|
}
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] || !Demolitionist(players[i].mo) ) continue;
|
|
for ( SWWMItemSense s=Demolitionist(players[i].mo).itemsense; s; s=s.next )
|
|
s.UpdateTag();
|
|
}
|
|
}
|
|
curlang = language;
|
|
curfuntags = swwm_funtags;
|
|
}
|
|
|
|
// countable item scoring
|
|
private void ItemCountTrack()
|
|
{
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] ) continue;
|
|
if ( players[i].itemcount > lastitemcount[i] )
|
|
{
|
|
int score = 10*(players[i].itemcount-lastitemcount[i]);
|
|
if ( !deathmatch && !(gameinfo.gametype&GAME_Hexen) && (level.total_items == level.found_items) && !allitems )
|
|
{
|
|
allitems = true;
|
|
if ( i == consoleplayer ) Console.Printf(StringTable.Localize("$SWWM_LASTITEM"),500);
|
|
else Console.Printf(StringTable.Localize("$SWWM_LASTITEMREM"),players[i].GetUserName(),500);
|
|
score += 490;
|
|
SWWMUtility.AchievementProgressInc("allitems",1,players[i]);
|
|
}
|
|
SWWMCredits.Give(players[i],score);
|
|
SWWMScoreObj.SpawnFromHandler(self,score,players[i].mo.Vec3Offset(0,0,players[i].mo.Height/2));
|
|
lastitemcount[i] = players[i].itemcount;
|
|
let s = SWWMStats.Find(players[i]);
|
|
s.items++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// combat tracking
|
|
private void CombatTrack()
|
|
{
|
|
// if players are above 1HP, reset the "one hp kill" counter
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] ) continue;
|
|
if ( players[i].Health != 1 ) onehpspree[i] = 0;
|
|
}
|
|
// prune old entries
|
|
for ( int i=0; i<combatactors.Size(); i++ )
|
|
{
|
|
if ( combattics[i] > highesttic )
|
|
highesttic = combattics[i];
|
|
if ( combatactors[i]
|
|
&& (combatactors[i].Health > 0)
|
|
&& !combatactors[i].bKILLED
|
|
&& !combatactors[i].bCORPSE
|
|
&& (combatactors[i].target == players[consoleplayer].mo)
|
|
&& (combattics[i]+2000 > gametic) )
|
|
continue;
|
|
combatactors.Delete(i);
|
|
combattics.Delete(i);
|
|
i--;
|
|
}
|
|
bool enteredcombat = false;
|
|
// add new entries
|
|
bool bossfound = false;
|
|
// we can use this instead of a thinker iterator as only actors that EXIST physically could count as combatants
|
|
foreach ( s:level.Sectors ) for ( Actor a=s.thinglist; a; a=a.snext )
|
|
{
|
|
if ( !a.player && !a.bISMONSTER ) continue;
|
|
// ignore the dead
|
|
if ( (a.Health <= 0) || a.bKILLED || a.bCORPSE ) continue;
|
|
// ignore if not targetted
|
|
if ( a.target != players[consoleplayer].mo ) continue;
|
|
// ignore friends
|
|
if ( a.IsFriend(players[consoleplayer].mo) ) continue;
|
|
// ignore if player can't see it
|
|
if ( !SWWMUtility.InPlayerFOV(players[consoleplayer],a) ) continue;
|
|
// is it already in?
|
|
bool addme = true;
|
|
for ( int i=0; i<combatactors.Size(); i++ )
|
|
{
|
|
if ( combatactors[i] != a ) continue;
|
|
addme = false;
|
|
combattics[i] = gametic;
|
|
break;
|
|
}
|
|
// add it in
|
|
if ( addme )
|
|
{
|
|
combatactors.Push(a);
|
|
combattics.Push(gametic);
|
|
enteredcombat = true;
|
|
if ( a.bBOSS || a.FindInventory("BossMarker") )
|
|
bossfound = true;
|
|
}
|
|
}
|
|
// be smart, demo-chan, don't shout if you're invisible, or you'll make it worse
|
|
if ( enteredcombat && ((bossfound && (!lastcombat || (gametic > lastcombat+240))) || (!bossfound && (!highesttic || (gametic > highesttic+700)))) && !players[consoleplayer].mo.FindInventory("GhostPower") )
|
|
lastcombat = AddOneliner("fightstart",1,10);
|
|
}
|
|
|
|
private void OneHundredPercentCheck()
|
|
{
|
|
// not in DM
|
|
if ( deathmatch ) return;
|
|
// not in Hexen, due to its fully hub-based nature
|
|
if ( gameinfo.gametype&GAME_Hexen ) return;
|
|
if ( !mapclear && (restartmus > 0) )
|
|
{
|
|
restartmus--;
|
|
if ( restartmus == 0 ) S_ChangeMusic(lastmus,lastorder,lastloop,true);
|
|
return;
|
|
}
|
|
// ignore levels that have NOTHING
|
|
if ( (level.total_secrets <= 0) && (level.total_items <= 0) && (level.total_monsters <= 0) ) return;
|
|
if ( mapclear )
|
|
{
|
|
if ( (musplaying.name != "music/nomusic.ogg") && (musplaying.name != "music/solitary.ogg") )
|
|
{
|
|
lastmus = musplaying.name;
|
|
lastorder = musplaying.baseorder;
|
|
lastloop = musplaying.loop;
|
|
S_ChangeMusic((startmus>0)?"music/nomusic.ogg":"music/solitary.ogg",force:true);
|
|
}
|
|
if ( startmus > 0 ) startmus--;
|
|
else if ( startmus == 0 )
|
|
{
|
|
startmus = -1;
|
|
S_ChangeMusic("music/solitary.ogg",force:true);
|
|
}
|
|
if ( (level.found_secrets < level.total_secrets) || (level.found_items < level.total_items) || (level.killed_monsters < level.total_monsters) )
|
|
{
|
|
restartmus = 25;
|
|
S_ChangeMusic("music/nomusic.ogg",force:true);
|
|
S_StartSound("recordscratch",CHAN_VOICE,CHANF_UI|CHANF_NOPAUSE|CHANF_OVERLAP,1,ATTN_NONE);
|
|
mapclear = false;
|
|
if ( mapclearagain > 1 ) Console.Printf(StringTable.Localize("$SWWM_NOTCLEARAGAIN"));
|
|
else Console.Printf(StringTable.Localize("$SWWM_NOTCLEAR"));
|
|
}
|
|
return;
|
|
}
|
|
if ( (level.found_secrets < level.total_secrets) || (level.found_items < level.total_items) || (level.killed_monsters < level.total_monsters) ) return;
|
|
restartmus = 0;
|
|
mapclear = true;
|
|
if ( mapclearagain ) Console.Printf(StringTable.Localize("$SWWM_ALLCLEARAGAIN"),500);
|
|
else Console.Printf(StringTable.Localize("$SWWM_ALLCLEAR"),5000);
|
|
bool altclear = swwm_altclear;
|
|
S_StartSound(altclear?"misc/yippeee":"misc/wow",CHAN_VOICE,CHANF_UI|CHANF_NOPAUSE|CHANF_OVERLAP,1,ATTN_NONE);
|
|
lastmus = musplaying.name;
|
|
lastorder = musplaying.baseorder;
|
|
lastloop = musplaying.loop;
|
|
S_ChangeMusic("music/nomusic.ogg",force:true);
|
|
startmus = 1050;
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] || !players[i].mo ) continue;
|
|
let f = Actor.Spawn("PartyTime",players[i].mo.pos);
|
|
if ( altclear ) f.bSTANDSTILL = true;
|
|
else f.bAMBUSH = true;
|
|
if ( mapclearagain )
|
|
{
|
|
SWWMCredits.Give(players[i],500);
|
|
SWWMScoreObj.SpawnFromHandler(self,500,players[i].mo.Vec3Offset(0,0,players[i].mo.Height/2));
|
|
}
|
|
else
|
|
{
|
|
SWWMCredits.Give(players[i],5000);
|
|
SWWMScoreObj.SpawnFromHandler(self,5000,players[i].mo.Vec3Offset(0,0,players[i].mo.Height/2));
|
|
}
|
|
}
|
|
mapclearagain++;
|
|
if ( !iwantdie ) return;
|
|
for ( int i=0; i<MAXPLAYERS; i++ )
|
|
{
|
|
if ( !playeringame[i] ) continue;
|
|
let demo = Demolitionist(players[i].mo);
|
|
if ( !demo || !demo.mystats ) continue;
|
|
if ( demo.mystats.deaths > 0 ) return;
|
|
}
|
|
SWWMUtility.MarkAchievement("wantdie",players[consoleplayer]);
|
|
}
|
|
|
|
private void UpdateHUDObjects()
|
|
{
|
|
// score objects
|
|
SWWMScoreObj so = scorenums;
|
|
SWWMScoreObj soprev = null, sonext;
|
|
while ( so )
|
|
{
|
|
sonext = so.next;
|
|
if ( so.Tick() )
|
|
{
|
|
if ( soprev ) soprev.next = sonext;
|
|
else scorenums = sonext;
|
|
so.Destroy();
|
|
}
|
|
else soprev = so;
|
|
so = sonext;
|
|
}
|
|
SWWMDamNum dn = damnums;
|
|
SWWMDamNum dnprev = null, dnnext;
|
|
while ( dn )
|
|
{
|
|
dnnext = dn.next;
|
|
if ( dn.Tick() )
|
|
{
|
|
if ( dnprev ) dnprev.next = dnnext;
|
|
else damnums = dnnext;
|
|
dn.Destroy();
|
|
}
|
|
else dnprev = dn;
|
|
dn = dnnext;
|
|
}
|
|
// interest markers
|
|
SWWMInterest ip = intpoints;
|
|
SWWMInterest ipprev = null, ipnext;
|
|
while ( ip )
|
|
{
|
|
ipnext = ip.next;
|
|
if ( ip.Tick() )
|
|
{
|
|
if ( ipprev ) ipprev.next = ipnext;
|
|
else intpoints = ipnext;
|
|
ip.Destroy();
|
|
}
|
|
else ipprev = ip;
|
|
ip = ipnext;
|
|
}
|
|
}
|
|
|
|
// "simple" tracking (used by the minimap)
|
|
private void SimpleTracking()
|
|
{
|
|
if ( (gamestate != GS_LEVEL) || !swwm_mm_enable )
|
|
{
|
|
while ( strackers )
|
|
{
|
|
SWWMSimpleTracker next = strackers.next;
|
|
strackers.Destroy();
|
|
strackers = next;
|
|
}
|
|
return;
|
|
}
|
|
// update trackers for anything around the player
|
|
double viewdist = SWWMStatusBar.MAPVIEWDIST;
|
|
// still about as expensive as using a BlockThingsIterator, but without the need to allocate one every tic
|
|
int thisgroup = players[consoleplayer].Camera.CurSector.portalgroup;
|
|
foreach ( s:level.Sectors )
|
|
{
|
|
// don't check sectors that aren't within bounds, saves some time
|
|
if ( !BoxInSectorBounds(s,players[consoleplayer].Camera.pos.xy,viewdist,players[consoleplayer].Camera.CurSector.PortalGroup) )
|
|
continue;
|
|
for ( Actor a=s.thinglist; a; a=a.snext )
|
|
{
|
|
Vector2 rv;
|
|
if ( s.portalgroup != thisgroup )
|
|
{
|
|
Vector2 relpos = players[consoleplayer].Camera.pos.xy+level.GetDisplacement(thisgroup,s.portalgroup);
|
|
rv = a.pos.xy-relpos;
|
|
}
|
|
else rv = a.pos.xy-players[consoleplayer].Camera.pos.xy;
|
|
double rad;
|
|
bool isproj = a.bMISSILE&&SWWMUtility.ValidProjectile(a);
|
|
if ( SWWMUtility.IsBeamProj(a) )
|
|
{
|
|
isproj = true;
|
|
rad = SWWMUtility.IsYBeam(a)?(a.scale.y*cos(a.pitch-90)):(a.speed*cos(a.pitch));
|
|
}
|
|
else rad = a.radius;
|
|
if ( max(abs(rv.x)-a.radius,abs(rv.y)-a.radius) > viewdist )
|
|
continue;
|
|
if ( a == players[consoleplayer].Camera )
|
|
continue;
|
|
if ( a is 'GhostTarget' )
|
|
continue;
|
|
if ( !a.player && !a.bSOLID && !a.bSHOOTABLE && !a.bISMONSTER && !a.bFRIENDLY && !(a is 'Inventory') && !(a is 'Chancebox') && !isproj )
|
|
continue;
|
|
if ( !level.allmap && !(deathmatch && (a is 'Inventory') && !a.bDROPPED) && !(a.IsFriend(players[consoleplayer].mo) && !(a.player && (a.player.mo != a))) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
|
|
continue;
|
|
if ( a.bKILLED || (a.Health <= 0) || a.bUnmorphed )
|
|
continue;
|
|
if ( (a is 'Inventory') && (!a.bSPECIAL || Inventory(a).Owner || (a.GetClassName() == 'aas_token')) ) // autoautosave hotfix
|
|
continue;
|
|
if ( (a is 'Chancebox') && (a.CurState != a.SpawnState) )
|
|
continue;
|
|
if ( isproj && !level.allmap && !(a.target && a.target.IsFriend(players[consoleplayer].mo)) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
|
|
continue;
|
|
SWWMSimpleTracker.Track(self,a);
|
|
}
|
|
}
|
|
SWWMSimpleTracker trk = strackers;
|
|
SWWMSimpleTracker prev = null, next;
|
|
while ( trk )
|
|
{
|
|
next = trk.next;
|
|
// minimize lifespan of destroyed targets
|
|
if ( !trk.target ) trk.lastupdate = min(trk.lastupdate,level.maptime);
|
|
else if ( !trk.expired )
|
|
{
|
|
// "last breath" update
|
|
if ( (trk.target.bKILLED || (trk.target.Health <= 0))
|
|
|| ((trk.target is 'Inventory') && (!trk.target.bSPECIAL || Inventory(trk.target).Owner))
|
|
|| ((trk.target is 'Chancebox') && (trk.target.CurState != trk.target.SpawnState))
|
|
|| (trk.target.default.bMISSILE && !trk.target.bMISSILE)
|
|
|| trk.target.bUnmorphed )
|
|
trk.Update();
|
|
}
|
|
// prune expired trackers
|
|
if ( trk.lastupdate+140 < level.maptime )
|
|
{
|
|
if ( !prev ) strackers = trk.next;
|
|
else prev.next = trk.next;
|
|
trk.Destroy();
|
|
}
|
|
else prev = trk;
|
|
trk = next;
|
|
}
|
|
}
|
|
}
|