While in the latter case this may result in longer loops, it also reduces GC thrashing by not needing to allocate an iterator every time. This also simplifies the DoBlast code as there is no longer a need to manually traverse portals vertically.
403 lines
14 KiB
Text
403 lines
14 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);
|
|
S_StartSound("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);
|
|
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*swwm_mm_zoom;
|
|
// still about as expensive as using a BlockThingsIterator, but without the need to allocate one every tic
|
|
foreach ( s:level.Sectors ) for ( Actor a=s.thinglist; a; a=a.snext )
|
|
{
|
|
Vector2 rv = a.pos.xy-players[consoleplayer].Camera.pos.xy;
|
|
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') )
|
|
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) )
|
|
continue;
|
|
if ( (a is 'Chancebox') && (a.CurState != a.SpawnState) )
|
|
continue;
|
|
SWWMSimpleTracker.Track(self,a);
|
|
}
|
|
// we need to refer to the suckables array to find missiles
|
|
foreach ( a:suckableactors )
|
|
{
|
|
if ( !a || !a.bMISSILE ) continue;
|
|
Vector2 rv = a.pos.xy-players[consoleplayer].Camera.pos.xy;
|
|
if ( max(abs(rv.x)-a.radius,abs(rv.y)-a.radius) > viewdist )
|
|
continue;
|
|
if ( !level.allmap && !(a.target && a.target.IsFriend(players[consoleplayer].mo)) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
|
|
continue;
|
|
SWWMSimpleTracker.Track(self,a);
|
|
}
|
|
foreach ( a:beams )
|
|
{
|
|
if ( !a ) continue;
|
|
Vector2 rv = a.pos.xy-players[consoleplayer].Camera.pos.xy;
|
|
double rad = SWWMUtility.IsYBeam(a)?(a.scale.y*cos(a.pitch-90)):(a.speed*cos(a.pitch));
|
|
if ( max(abs(rv.x)-rad,abs(rv.y)-rad) > viewdist )
|
|
continue;
|
|
if ( !level.allmap && !(a.target && a.target.IsFriend(players[consoleplayer].mo)) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
|
|
continue;
|
|
SWWMSimpleTracker.Track(self,a);
|
|
}
|
|
// oh boy here we go
|
|
int thisgroup = players[consoleplayer].Camera.CurSector.portalgroup;
|
|
for ( int i=0; i<level.GetPortalGroupCount(); i++ )
|
|
{
|
|
if ( i == thisgroup ) continue;
|
|
Vector2 relpos = players[consoleplayer].Camera.pos.xy+level.GetDisplacement(thisgroup,i);
|
|
// still about as expensive as using a BlockThingsIterator, but without the need to allocate one every tic
|
|
foreach ( s:level.Sectors ) for ( Actor a=s.thinglist; a; a=a.snext )
|
|
{
|
|
Vector2 rv = a.pos.xy-relpos;
|
|
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') )
|
|
continue;
|
|
if ( !level.allmap && !(deathmatch && (a is 'Inventory') && !a.bDROPPED) && !a.IsFriend(players[consoleplayer].mo) && !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) )
|
|
continue;
|
|
if ( (a is 'Chancebox') && (a.CurState != a.SpawnState) )
|
|
continue;
|
|
SWWMSimpleTracker.Track(self,a);
|
|
}
|
|
// we need to refer to the suckables array to find missiles
|
|
foreach ( a:suckableactors )
|
|
{
|
|
if ( !a || !a.bMISSILE ) continue;
|
|
Vector2 rv = a.pos.xy-relpos;
|
|
if ( max(abs(rv.x)-a.radius,abs(rv.y)-a.radius) > viewdist )
|
|
continue;
|
|
if ( !level.allmap && !(a.target && a.target.IsFriend(players[consoleplayer].mo)) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
|
|
continue;
|
|
SWWMSimpleTracker.Track(self,a);
|
|
}
|
|
foreach ( a:beams )
|
|
{
|
|
if ( !a ) continue;
|
|
Vector2 rv = a.pos.xy-relpos;
|
|
double rad = SWWMUtility.IsYBeam(a)?(a.scale.y*cos(a.pitch-90)):(a.speed*cos(a.pitch));
|
|
if ( max(abs(rv.x)-rad,abs(rv.y)-rad) > viewdist )
|
|
continue;
|
|
if ( !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;
|
|
}
|
|
}
|
|
}
|