swwmgz_m/zscript/swwm_thinkers.zsc

1270 lines
35 KiB
Text

// various stat tracking thinkers and others
// weapon spawn mgmt
Class SWWMPairedSpawns : Thinker
{
// 0 - spawn former, 1 - spawn latter, 2 - random
int spreadbuster; // spreadgun/wallbuster pair (Ethereal Crossbow)
int sparksilver; // sparkster/silverbullet pair (Plasma Rifle, Skull Rod)
int candyfury; // candygun/ynykron pair (BFG9000, Mace)
// when the DLC weaponset lands, this will also have to account for more weapon pairs (or quartets)
private static SWWMPairedSpawns Instance()
{
let s = SWWMPairedSpawns(ThinkerIterator.Create("SWWMPairedSpawns",STAT_STATIC).Next());
if ( !s )
{
s = new("SWWMPairedSpawns");
s.ChangeStatNum(STAT_STATIC);
}
return s;
}
static Class<Actor> PickSpreadBuster()
{
let s = Instance();
if ( s.spreadbuster == 0 )
{
s.spreadbuster = 1;
return 'Spreadgun';
}
if ( s.spreadbuster == 1 )
{
s.spreadbuster = 2;
return 'Wallbuster';
}
return Random[Replacements](0,2)?'Spreadgun':'Wallbuster';
}
static Class<Actor> PickSparkSilver()
{
let s = Instance();
if ( s.sparksilver == 0 )
{
s.sparksilver = 1;
return 'Sparkster';
}
if ( s.sparksilver == 1 )
{
s.sparksilver = 2;
return 'SilverBullet';
}
return Random[Replacements](0,2)?'Sparkster':'SilverBullet';
}
static Class<Actor> PickCandyFury()
{
let s = Instance();
if ( s.candyfury == 0 )
{
s.candyfury = 1;
return 'CandyGun';
}
if ( s.candyfury == 1 )
{
s.candyfury = 2;
return 'Ynykron';
}
return Random[Replacements](0,2)?'CandyGun':'Ynykron';
}
}
// Stats
Class WeaponUsage
{
Class<Weapon> w;
int kills;
}
Class MonsterKill
{
Class<Actor> m;
int kills;
}
Class LevelStat
{
bool hub;
String levelname, mapname;
int kcount, ktotal;
int icount, itotal;
int scount, stotal;
int time, par, suck;
}
Class SWWMStats : Thinker
{
PlayerInfo myplayer;
int lastspawn, dashcount, boostcount, stompcount, airtime, kills,
deaths, damagedealt, hdamagedealt, damagetaken, hdamagetaken,
mkill, hiscore, hhiscore, topdealt, toptaken, skill, wponch,
busts, buttslams, secrets, items;
double grounddist, airdist, swimdist, fuelusage, topspeed, teledist;
Array<WeaponUsage> wstats;
Array<MonsterKill> mstats;
Array<LevelStat> lstats;
Array<Class<Weapon> > alreadygot;
int favweapon;
// these two are used for mission updates
Array<int> clustervisit;
Array<bool> secretdone;
// [Strife] preserve previous mission logs
String oldlogtext;
Array<String> questbacklog;
// hackaround for stuff getting lost
Array<Class<SWWMCollectible> > ownedcollectibles;
// for pistol start info (to avoid it within hubs)
int lastcluster;
bool GotWeapon( Class<Weapon> which )
{
for ( int i=0; i<alreadygot.Size(); i++ )
{
if ( alreadygot[i] == which ) return true;
}
alreadygot.Push(which);
return false;
}
void AddDamageDealt( int dmg )
{
int upper = dmg/1000000000;
int lower = dmg%1000000000;
if ( hdamagedealt+upper > 999999999 ) hdamagedealt = 999999999;
else hdamagedealt += upper;
damagedealt += lower;
if ( damagedealt > 999999999 )
{
upper = damagedealt/1000000000;
lower = damagedealt%1000000000;
if ( hdamagedealt+upper > 999999999 ) hdamagedealt = 999999999;
else hdamagedealt += upper;
damagedealt = lower;
}
}
void AddDamageTaken( int dmg )
{
int upper = dmg/1000000000;
int lower = dmg%1000000000;
if ( hdamagetaken+upper > 999999999 ) hdamagetaken = 999999999;
else hdamagetaken += upper;
damagetaken += lower;
if ( damagetaken > 999999999 )
{
upper = damagetaken/1000000000;
lower = damagetaken%1000000000;
if ( hdamagetaken+upper > 999999999 ) hdamagetaken = 999999999;
else hdamagetaken += upper;
damagetaken = lower;
}
}
void AddLevelStats()
{
let ls = new("LevelStat");
ls.hub = !!(level.clusterflags&level.CLUSTER_HUB);
ls.levelname = level.levelname;
int iof = ls.levelname.IndexOf(" - by: ");
if ( iof != -1 ) ls.levelname.Truncate(iof);
ls.mapname = level.mapname;
ls.kcount = level.killed_monsters;
ls.ktotal = level.total_monsters;
ls.icount = level.found_items;
ls.itotal = level.total_items;
ls.scount = level.found_secrets;
ls.stotal = level.total_secrets;
ls.time = level.maptime;
ls.par = level.partime;
ls.suck = level.sucktime;
lstats.Push(ls);
}
void AddWeaponKill( Actor inflictor, Actor victim, Name damagetype )
{
if ( victim )
{
bool found = false;
for ( int i=0; i<mstats.Size(); i++ )
{
if ( mstats[i].m != victim.GetClass() ) continue;
found = true;
mstats[i].kills++;
break;
}
if ( !found )
{
let ms = new("MonsterKill");
ms.m = victim.GetClass();
ms.kills = 1;
mstats.Push(ms);
}
}
Class<Weapon> which = myplayer.ReadyWeapon?myplayer.ReadyWeapon.GetClass():null;
if ( inflictor is 'Weapon' ) which = Weapon(inflictor).GetClass();
if ( which is 'DualExplodiumGun' ) which = 'ExplodiumGun'; // don't credit sister weapon
// properly credit some projectiles to their respective gun
if ( inflictor is 'AirBullet' ) which = 'DeepImpact';
else if ( inflictor is 'PusherProjectile' ) which = 'PusherWeapon';
else if ( (inflictor is 'ExplodiumMagArm') || (inflictor is 'ExplodiumMagProj') || (inflictor is 'ExplodiumBulletImpact') ) which = 'ExplodiumGun';
else if ( (inflictor is 'DragonBreathArm') || ((inflictor is 'SaltImpact') && !inflictor.Args[0]) || ((inflictor is 'SaltBeam') && !inflictor.Args[1]) || (inflictor is 'CorrodeDebuff') || (inflictor is 'CorrosiveFlechette') || ((inflictor is 'TheBall') && !inflictor.special1) || (inflictor is 'GoldenImpact') || (inflictor is 'GoldenSubImpact') || (inflictor is 'GoldenSubSubImpact') ) which = 'Spreadgun';
else if ( ((inflictor is 'SaltImpact') && inflictor.Args[0]) || ((inflictor is 'SaltBeam') && inflictor.Args[1]) || ((inflictor is 'TheBall') && inflictor.special1) ) which = 'Wallbuster';
else if ( (inflictor is 'EvisceratorChunk') || (inflictor is 'EvisceratorProj') ) which = 'Eviscerator';
else if ( (inflictor is 'HellblazerRavagerArm') || (inflictor is 'HellblazerWarheadArm') ) which = 'Hellblazer';
else if ( (inflictor is 'BigBiospark') || (inflictor is 'BiosparkBall') || (inflictor is 'BiosparkBeamImpact') || (inflictor is 'BiosparkComboImpact') || (inflictor is 'BiosparkComboImpactSub') || (inflictor is 'BiosparkBeam') || (inflictor is 'BiosparkArc') ) which = 'Sparkster';
else if ( (inflictor is 'CandyBeam') || (inflictor is 'CandyPop') || (inflictor is 'CandyMagArm') || (inflictor is 'CandyGunProj') || (inflictor is 'CandyMagProj') || (inflictor is 'CandyBulletImpact') ) which = 'CandyGun';
else if ( (inflictor is 'YnykronBeam') || (inflictor is 'YnykronImpact') || (inflictor is 'YnykronSingularity') || (inflictor is 'YnykronCloud') || (inflictor is 'YnykronVoidBeam') || (inflictor is 'YnykronLightningArc') || (inflictor is 'YnykronLightningImpact') ) which = 'Ynykron';
else if ( (inflictor is 'Demolitionist') || (inflictor is 'DemolitionistShockwave') || (inflictor is 'DemolitionistRadiusShockwave') || (inflictor is 'SWWMGesture')
|| (inflictor is 'SWWMItemGesture') ) which = 'SWWMWeapon'; // hack to assume Demolitionist as weapon
else if ( inflictor is 'BigPunchSplash' )
{
// guess from damagetype
if ( (damagetype == 'Jump') || (damagetype == 'Dash') || (damagetype == 'Buttslam') || (damagetype == 'GroundPound') )
which = 'SWWMWeapon';
else if ( damagetype == 'Love' )
which = 'SWWMGesture';
// others are just weapon melee, so keep the readyweapon
}
else if ( inflictor is 'FroggyChair' )
which = 'SWWMItemGesture'; // more gross hacks
if ( damagetype == 'Falling' )
which = 'Weapon'; // the gross hacks continue
if ( !which ) return;
for ( int i=0; i<wstats.Size(); i++ )
{
if ( wstats[i].w != which ) continue;
wstats[i].kills++;
if ( (favweapon == -1) || (wstats[favweapon].kills < wstats[i].kills) ) favweapon = i;
return;
}
let ws = new("WeaponUsage");
ws.w = which;
ws.kills = 1;
wstats.Push(ws);
if ( (favweapon == -1) || (wstats[favweapon].kills < ws.kills) )
favweapon = wstats.Size()-1;
}
static clearscope SWWMStats Find( PlayerInfo p )
{
let ti = ThinkerIterator.Create("SWWMStats",STAT_STATIC);
SWWMStats t;
while ( t = SWWMStats(ti.Next()) )
{
if ( t.myplayer != p ) continue;
return t;
}
return null;
}
}
// Scoring
Class SWWMCredits : Thinker
{
PlayerInfo myplayer;
int credits, hcredits;
static void Give( PlayerInfo p, int amount, int hamount = 0 )
{
let c = Find(p);
if ( !c ) return;
if ( c.credits+amount < c.credits ) c.credits = int.max;
else c.credits += amount;
while ( c.credits > 999999999 )
{
c.credits -= 1000000000;
c.hcredits++;
}
if ( (c.hcredits+hamount < c.hcredits) || (c.hcredits+hamount > 999999999) ) c.hcredits = 999999999;
else c.hcredits += hamount;
let s = SWWMStats.Find(p);
if ( s && ((c.hcredits > s.hhiscore) || ((c.credits > s.hiscore) && (c.hcredits >= s.hhiscore))) )
{
s.hiscore = c.credits;
s.hhiscore = c.hcredits;
}
}
static clearscope bool CanTake( PlayerInfo p, int amount, int hamount = 0 )
{
let c = Find(p);
if ( !c ) return false;
int req = amount, hreq = hamount;
while ( req > 999999999 )
{
req -= 1000000000;
hreq++;
}
// waaaaay too much
if ( (c.hcredits-hreq < 0) || (c.hcredits-hreq > c.hcredits) ) return false;
// too much!
if ( ((c.credits-amount < 0) || (c.credits-amount > c.credits)) && (c.hcredits-hreq <= 0) ) return false;
return true;
}
static bool Take( PlayerInfo p, int amount, int hamount = 0 )
{
let c = Find(p);
if ( !c ) return false;
int req = amount, hreq = hamount;
while ( req > 999999999 )
{
req -= 1000000000;
hreq++;
}
// waaaaay too much
if ( (c.hcredits-hreq < 0) || (c.hcredits-hreq > c.hcredits) ) return false;
// too much!
if ( ((c.credits-amount < 0) || (c.credits-amount > c.credits)) && (c.hcredits-hreq <= 0) ) return false;
c.hcredits -= hreq;
c.credits -= req;
while ( c.credits < 0 )
{
c.credits += 1000000000;
c.hcredits--;
}
return true;
}
static clearscope int, int Get( PlayerInfo p )
{
let c = Find(p);
if ( !c ) return 0;
return c.credits, c.hcredits;
}
static clearscope SWWMCredits Find( PlayerInfo p )
{
let ti = ThinkerIterator.Create("SWWMCredits",STAT_STATIC);
SWWMCredits t;
while ( t = SWWMCredits(ti.Next()) )
{
if ( t.myplayer != p ) continue;
return t;
}
return null;
}
}
// Trading history between players
Class SWWMTrade
{
int timestamp, type, amt;
String other;
Class<Inventory> what;
}
Class SWWMTradeHistory : Thinker
{
PlayerInfo myplayer;
Array<SWWMTrade> ent;
static void RegisterSend( PlayerInfo p, PlayerInfo other, Class<Inventory> what, int amt )
{
let th = Find(p);
if ( !th ) return;
SWWMTrade t = new("SWWMTrade");
t.timestamp = level.totaltime;
t.type = 0;
t.other = other.GetUserName();
t.what = what;
t.amt = amt;
th.ent.Push(t);
}
static void RegisterReceive( PlayerInfo p, PlayerInfo other, Class<Inventory> what, int amt )
{
let th = Find(p);
if ( !th ) return;
SWWMTrade t = new("SWWMTrade");
t.timestamp = level.totaltime;
t.type = 1;
t.other = other.GetUserName();
t.what = what;
t.amt = amt;
th.ent.Push(t);
}
static clearscope SWWMTradeHistory Find( PlayerInfo p )
{
let ti = ThinkerIterator.Create("SWWMTradeHistory",STAT_STATIC);
SWWMTradeHistory th;
while ( th = SWWMTradeHistory(ti.Next()) )
{
if ( th.myplayer != p ) continue;
return th;
}
return Null;
}
}
// Lore holder
enum ELoreTab
{
LORE_ITEM,
LORE_PEOPLE,
LORE_LORE // lol
};
Class SWWMLore
{
String tag, text, assoc;
int tab;
bool read;
}
Class SWWMLoreLibrary : Thinker
{
PlayerInfo myplayer;
Array<SWWMLore> ent;
int lastaddtic;
static bool PreVerify( String ref )
{
// restrictions
if ( !(gameinfo.gametype&(GAME_Raven|GAME_Strife)) )
{
if ( ref ~== "Parthoris" ) return true;
if ( ref ~== "Sidhe" ) return true;
if ( ref ~== "SerpentRiders" ) return true;
}
if ( !(gameinfo.gametype&(GAME_Hexen|GAME_Strife)) )
{
if ( ref ~== "Cronos" ) return true;
if ( ref ~== "Kirin" ) return true; // not met
if ( ref ~== "Fabricator" ) return true; // not yet introduced
if ( ref ~== "Administrators" ) return true; // not met
}
if ( !(gameinfo.gametype&GAME_Strife) )
{
if ( ref ~== "TheOrder" ) return true;
if ( ref ~== "TheFront" ) return true;
}
// check if entry is for a collectible
for ( int i=0; i<AllActorClasses.Size(); i++ )
{
let c = (Class<SWWMCollectible>)(AllActorClasses[i]);
if ( !c || (c == 'SWWMCollectible') ) continue;
let def = GetDefaultByType(c);
// skip if we match and it's not for this game
if ( (c.GetClassName() == ref) && !(gameinfo.gametype&def.avail) )
return true;
}
ref = ref.MakeUpper();
String tag = String.Format("SWWM_LORETAG_%s",ref);
String tab = String.Format("SWWM_LORETAB_%s",ref);
String text = String.Format("SWWM_LORETXT_%s",ref);
// check that it's valid
if ( StringTable.Localize(tag,false) == tag ) return true;
if ( StringTable.Localize(tab,false) == tab )
{
Console.Printf("Entry \"%s\" defines no tab.",ref);
return true;
}
if ( StringTable.Localize(text,false) == text )
{
Console.Printf("Entry \"%s\" defines no text.",ref);
return true;
}
return false;
}
bool DirectAdd( String ref )
{
if ( PreVerify(ref) ) return true;
return InternalAdd(ref);
}
private bool InternalAdd( String ref )
{
ref = ref.MakeUpper();
String tag = String.Format("SWWM_LORETAG_%s",ref);
String tab = String.Format("SWWM_LORETAB_%s",ref);
String text = String.Format("SWWM_LORETXT_%s",ref);
String assoc = String.Format("SWWM_LOREREL_%s",ref);
// redirects
if ( gameinfo.gametype&GAME_Strife )
{
if ( text ~== "SWWM_LORETXT_KIRIN" )
text = "SWWM_LORETXT_KIRIN2"; // married alakir
else if ( text ~== "SWWM_LORETXT_SERPENTRIDERS" )
text = "SWWM_LORETXT_SERPENTRIDERS3"; // all riders gone
}
if ( gameinfo.gametype&(GAME_Hexen|GAME_Strife) )
{
if ( text ~== "SWWM_LORETXT_SAYA" )
text = "SWWM_LORETXT_SAYA3"; // married kirin
else if ( text ~== "SWWM_LORETXT_ANARUKON" )
text = "SWWM_LORETXT_ANARUKON2"; // comments from miyamoto-xanai wedding
else if ( text ~== "SWWM_LORETXT_HELL" )
text = "SWWM_LORETXT_HELL3"; // met father nostros during the wedding
else if ( text ~== "SWWM_LORETXT_NANA" )
text = "SWWM_LORETXT_NANA3"; // stuff that happened at the wedding
else if ( text ~== "SWWM_LORETXT_GHOULHUNT" )
text = "SWWM_LORETXT_GHOULHUNT2"; // met anthon anderken during the wedding
else if ( text ~== "SWWM_LORETXT_RAGEKIT" )
text = "SWWM_LORETXT_RAGEKIT2"; // kirin's reactions to demo using this item
else if ( text ~== "SWWM_LORETXT_SANKAIDERIHA" )
text = "SWWM_LORETXT_SANKAIDERIHA2"; // comments about kirin
else if ( text ~== "SWWM_LORETXT_SERPENTRIDERS" )
text = "SWWM_LORETXT_SERPENTRIDERS2"; // defeated d'sparil
else if ( text ~== "SWWM_LORETXT_XANIMEN" )
text = "SWWM_LORETXT_XANIMEN2"; // footnote about nuoma
else if ( text ~== "SWWM_LORETXT_ZANAVETH2" )
text = "SWWM_LORETXT_ZANAVETH22"; // met at wedding
else if ( text ~== "SWWM_LORETXT_YNYKRON" )
text = "SWWM_LORETXT_YNYKRON2"; // confirmed to harm (but not kill) gods
else if ( text ~== "SWWM_LORETXT_AKARIPROJECT" )
text = "SWWM_LORETXT_AKARIPROJECT3"; // mentions kirin
else if ( text ~== "SWWM_LORETXT_GODS" )
text = "SWWM_LORETXT_GODS2"; // beyond gods
}
if ( gameinfo.gametype&(GAME_Raven|GAME_Strife) )
{
if ( text ~== "SWWM_LORETXT_SAYA" )
text = "SWWM_LORETXT_SAYA2"; // dating demo
else if ( text ~== "SWWM_LORETXT_AKARILABS" )
text = "SWWM_LORETXT_AKARILABS2"; // demo won, akari project announced
else if ( text ~== "SWWM_LORETXT_DEMOLITIONIST" )
text = "SWWM_LORETXT_DEMOLITIONIST2"; // demo rewarded with maidbot frame
else if ( text ~== "SWWM_LORETXT_DOOMGUY" )
text = "SWWM_LORETXT_DOOMGUY2"; // he gone
else if ( text ~== "SWWM_LORETXT_UAC" )
text = "SWWM_LORETXT_UAC2"; // uac "reformed"
else if ( text ~== "SWWM_LORETXT_HELL" )
text = "SWWM_LORETXT_HELL2"; // invasion was a thing of the past
else if ( text ~== "SWWM_LORETXT_NANA" )
text = "SWWM_LORETXT_NANA2"; // demo met nana
else if ( text ~== "SWWM_LORETXT_ZANAVETH3" )
text = "SWWM_LORETXT_ZANAVETH32"; // iagb happened
else if ( text ~== "SWWM_LORETXT_BIGSHOT" )
text = "SWWM_LORETXT_BIGSHOT2"; // predictions about crimes_m
else if ( text ~== "SWWM_LORETXT_AKARIPROJECT" )
text = "SWWM_LORETXT_AKARIPROJECT2"; // fiction becomes reality
}
// check if existing
for ( int i=0; i<ent.Size(); i++ )
{
if ( ent[i].tag != "$"..tag ) continue;
return true;
}
SWWMLore e = new("SWWMLore");
e.tag = "$"..tag;
if ( StringTable.Localize(e.tag) == "" )
{
Console.Printf("Entry \"%s\" has an empty tag.",ref);
return true;
}
String ttab = StringTable.Localize(tab,false);
if ( ttab ~== "People" ) e.tab = LORE_PEOPLE;
else if ( ttab ~== "Lore" ) e.tab = LORE_LORE;
else if ( ttab ~== "Item" ) e.tab = LORE_ITEM;
else
{
Console.Printf("Entry \"%s\" has an incorrect tab setting of \"%s\".",ref,ttab);
return true;
}
e.text = "$"..text;
if ( StringTable.Localize(e.text) == "" )
{
Console.Printf("Entry \"%s\" has empty text.",ref);
return true;
}
e.assoc = "$"..assoc;
e.read = false;
// "new lore" message
if ( (level.maptime > 0) && (gametic > lastaddtic) && (myplayer == players[consoleplayer]) && (!menuactive || (menuactive == Menu.OnNoPause)) && (myplayer.mo is 'Demolitionist') )
Console.Printf(StringTable.Localize("$SWWM_NEWLORE"));
lastaddtic = gametic;
ent.Push(e);
return true;
}
static void Add( PlayerInfo p, String ref )
{
if ( PreVerify(ref) ) return;
SWWMLoreLibrary ll = Find(p);
if ( !ll )
{
ll = new("SWWMLoreLibrary");
ll.ChangeStatNum(STAT_STATIC);
ll.myplayer = p;
}
ll.InternalAdd(ref);
}
void MarkRead( int idx )
{
if ( (idx < 0) || (idx >= ent.Size()) ) return;
if ( !ent[idx].read )
{
ent[idx].read = true;
// add associated entries
Array<String> rel;
rel.Clear();
String assocstr = StringTable.Localize(ent[idx].assoc);
assocstr.Split(rel,";",0);
for ( int i=0; i<rel.Size(); i++ )
{
if ( (rel[i] != "") && !DirectAdd(rel[i]) )
Console.Printf("Related entry \"%s\" not found, please update LANGUAGE.txt",rel[i]);
}
}
}
clearscope int FindEntry( String tag )
{
for ( int i=0; i<ent.Size(); i++ )
{
if ( ent[i].tag ~== tag )
return i;
}
return -1;
}
static clearscope SWWMLoreLibrary Find( PlayerInfo p )
{
let ti = ThinkerIterator.Create("SWWMLoreLibrary",STAT_STATIC);
SWWMLoreLibrary ll;
while ( ll = SWWMLoreLibrary(ti.Next()) )
{
if ( ll.myplayer != p ) continue;
return ll;
}
return Null;
}
}
Enum EScoreObjType
{
ST_Score,
ST_Damage,
ST_Health,
ST_Armor
};
// floating scores
Class SWWMScoreObj : Thinker
{
int xcnt;
int xtcolor[5];
int xscore[5];
String xstr[5];
int tcolor;
int score;
Vector3 pos;
int lifespan, initialspan;
int starttic, seed, seed2;
SWWMScoreObj prev, next;
bool damnum;
Actor acc;
static SWWMScoreObj Spawn( int score, Vector3 pos, int type = ST_Score, Actor acc = null, int tcolor = -1 )
{
let hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
if ( !hnd ) return null;
let o = new("SWWMScoreObj");
o.ChangeStatNum(STAT_USER);
o.score = score;
o.pos = pos;
o.lifespan = o.initialspan = 60;
if ( !hnd.numcolor_scr ) hnd.numcolor_scr = CVar.GetCVar('swwm_numcolor_scr',players[consoleplayer]);
if ( !hnd.numcolor_bonus ) hnd.numcolor_bonus = CVar.GetCVar('swwm_numcolor_bonus',players[consoleplayer]);
if ( !hnd.numcolor_dmg ) hnd.numcolor_dmg = CVar.GetCVar('swwm_numcolor_dmg',players[consoleplayer]);
if ( !hnd.numcolor_hp ) hnd.numcolor_hp = CVar.GetCVar('swwm_numcolor_hp',players[consoleplayer]);
if ( !hnd.numcolor_ap ) hnd.numcolor_ap = CVar.GetCVar('swwm_numcolor_ap',players[consoleplayer]);
if ( tcolor != -1 ) o.tcolor = tcolor;
else switch ( type )
{
case ST_Score:
o.tcolor = hnd.numcolor_scr.GetInt();
break;
case ST_Damage:
o.tcolor = hnd.numcolor_dmg.GetInt();
break;
case ST_Health:
o.tcolor = hnd.numcolor_hp.GetInt();
break;
case ST_Armor:
o.tcolor = hnd.numcolor_ap.GetInt();
break;
}
o.starttic = level.maptime;
o.seed = Random[ScoreBits]();
o.seed2 = Random[ScoreBits]();
o.damnum = (type > ST_Score);
o.xcnt = 0;
for ( int i=0; i<5; i++ ) o.xtcolor[i] = hnd.numcolor_bonus.GetInt();
o.acc = acc;
if ( o.damnum )
{
o.next = hnd.damnums;
if ( hnd.damnums ) hnd.damnums.prev = o;
hnd.damnums = o;
hnd.damnums_cnt++;
}
else
{
o.next = hnd.scorenums;
if ( hnd.scorenums ) hnd.scorenums.prev = o;
hnd.scorenums = o;
hnd.scorenums_cnt++;
}
return o;
}
override void OnDestroy()
{
let hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
if ( hnd )
{
if ( damnum )
{
hnd.damnums_cnt--;
if ( !prev ) hnd.damnums = next;
}
else
{
hnd.scorenums_cnt--;
if ( !prev ) hnd.scorenums = next;
}
if ( !prev )
{
if ( next ) next.prev = null;
}
else
{
prev.next = next;
if ( next ) next.prev = prev;
}
}
Super.OnDestroy();
}
override void Tick()
{
lifespan--;
if ( lifespan <= 0 ) Destroy();
}
}
enum EInterestType
{
INT_Key,
INT_Exit
};
Class SWWMInterest : Thinker
{
int type;
Key trackedkey;
Line trackedline;
Vector3 pos;
SWWMInterest prev, next;
String keytag;
static SWWMInterest Spawn( Vector3 pos = (0,0,0), Key thekey = null, Line theline = null )
{
let hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
if ( !hnd ) return null;
if ( (!thekey && !theline) || (thekey && theline) ) return null;
let i = new("SWWMInterest");
i.ChangeStatNum(STAT_USER);
i.trackedkey = thekey;
i.trackedline = theline;
if ( thekey )
{
i.type = INT_Key;
i.keytag = thekey.GetTag();
}
else if ( theline ) i.type = INT_Exit;
else
{
i.Destroy();
return null;
}
i.pos = thekey?thekey.Vec3Offset(0,0,thekey.height/2):pos;
i.next = hnd.intpoints;
if ( hnd.intpoints ) hnd.intpoints.prev = i;
hnd.intpoints = i;
hnd.intpoints_cnt++;
return i;
}
override void OnDestroy()
{
let hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
if ( hnd )
{
hnd.intpoints_cnt--;
if ( !prev )
{
hnd.intpoints = next;
if ( next ) next.prev = null;
}
else
{
prev.next = next;
if ( next ) next.prev = prev;
}
}
Super.OnDestroy();
}
override void Tick()
{
// update
if ( (type == INT_Key) && (!trackedkey || trackedkey.Owner) ) Destroy();
else if ( trackedkey ) pos = trackedkey.Vec3Offset(0,0,trackedkey.height/2);
}
}
Class SWWMItemSense : Thinker
{
Inventory item;
String tag;
int updated;
bool scoreitem;
Demolitionist parent;
SWWMItemSense prev, next;
Vector3 pos;
static SWWMItemSense Spawn( Demolitionist parent, Inventory item )
{
if ( !parent || !item ) return null;
// only refresh the updated time if existing
for ( SWWMItemSense s=parent.itemsense; s; s=s.next )
{
if ( s.item != item ) continue;
s.updated = level.maptime+35;
s.pos = item.Vec3Offset(0,0,item.height);
return s;
}
let i = new("SWWMItemSense");
i.ChangeStatNum(STAT_USER);
i.item = item;
i.scoreitem = item.bCOUNTITEM;
i.parent = parent;
i.updated = level.maptime+35;
i.UpdateTag();
i.pos = item.Vec3Offset(0,0,item.height);
i.next = parent.itemsense;
if ( parent.itemsense ) parent.itemsense.prev = i;
parent.itemsense = i;
parent.itemsense_cnt++;
return i;
}
void UpdateTag()
{
if ( !item ) return;
// certain ammo types use the pickup message as it's amount-aware
if ( (item is 'RedShell') || (item is 'GreenShell')
|| (item is 'WhiteShell') || (item is 'BlueShell')
|| (item is 'BlackShell') || (item is 'PurpleShell')
|| (item is 'GoldShell') || (item is 'SMW05Ammo')
|| (item is 'SheenAmmo') )
tag = item.PickupMessage();
else tag = item.GetTag();
}
override void OnDestroy()
{
if ( parent )
{
parent.itemsense_cnt--;
if ( !prev )
{
parent.itemsense = next;
if ( next ) next.prev = null;
}
else
{
prev.next = next;
if ( next ) next.prev = prev;
}
}
Super.OnDestroy();
}
override void Tick()
{
if ( !parent )
{
Destroy();
return;
}
// expire
if ( level.maptime > updated+70 ) Destroy();
}
}
// enemy combat tracker
Class SWWMCombatTracker : Thinker
{
Actor mytarget;
String mytag;
int updated, lasthealth, maxhealth;
DynamicValueInterpolator intp;
Vector3 pos, prevpos, oldpos, oldprev;
PlayerInfo myplayer;
SWWMCombatTracker prev, next;
bool legged, mutated;
int tcnt;
double height;
transient CVar funtags, maxdist, damagebars;
bool funtag;
int mxdist, dbar;
bool bBOSS, bFRIENDLY;
bool firsthit;
void UpdateTag()
{
// only the first tracker accesses the CVar, saves on perf
if ( !prev )
{
if ( !funtags ) funtags = CVar.GetCVar('swwm_funtags',players[consoleplayer]);
funtag = funtags.GetBool();
}
if ( next ) next.funtag = funtag;
if ( mytarget && (mytarget.player || mytarget.bISMONSTER || (mytarget is 'BossBrain') || (mytarget is 'SWWMHangingKeen') || (mytarget is 'Demolitionist')) )
{
String realtag = funtag?SWWMUtility.GetFunTag(mytarget,FallbackTag):mytarget.GetTag(FallbackTag);
if ( realtag == FallbackTag )
{
realtag = mytarget.GetClassName();
SWWMUtility.BeautifyClassName(realtag);
}
mytag = mytarget.player?(mytarget.player.mo!=mytarget)?String.Format(StringTable.Localize("$FN_VOODOO"),mytarget.player.GetUserName()):mytarget.player.GetUserName():realtag;
}
else mytag = "";
}
static SWWMCombatTracker Spawn( Actor target )
{
let hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
if ( !hnd ) return null;
SWWMCombatTracker t;
for ( t=hnd.trackers; t; t=t.next )
{
if ( t.mytarget != target ) continue;
return t;
}
t = new("SWWMCombatTracker");
t.ChangeStatNum(STAT_USER);
t.mytarget = target;
t.UpdateTag();
if ( target.player )
{
t.lasthealth = target.health;
t.maxhealth = target.default.health;
}
else t.lasthealth = t.maxhealth = target.health;
t.updated = int.min;
t.height = target.height;
t.pos = level.Vec3Offset(target.pos,(0,0,t.height));
t.prevpos = level.Vec3Offset(target.prev,(0,0,t.height));
t.oldpos = target.pos;
t.oldprev = target.prev;
t.intp = DynamicValueInterpolator.Create(t.lasthealth,.5,1,100);
t.myplayer = target.player;
t.next = hnd.trackers;
t.bBOSS = target.bBOSS;
t.bFRIENDLY = target.bFRIENDLY;
if ( hnd.trackers )
{
hnd.trackers.prev = t;
// propagate cvar values
t.mxdist = hnd.trackers.mxdist;
t.dbar = hnd.trackers.dbar;
}
hnd.trackers = t;
hnd.trackers_cnt++;
return t;
}
override void OnDestroy()
{
let hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
if ( hnd )
{
hnd.trackers_cnt--;
if ( !prev )
{
hnd.trackers = next;
if ( next ) next.prev = null;
}
else
{
prev.next = next;
if ( next ) next.prev = prev;
}
}
Super.OnDestroy();
}
override void Tick()
{
// only the first tracker accesses the CVars, saves on perf
if ( !prev )
{
if ( !damagebars ) damagebars = CVar.GetCVar('swwm_damagetarget',players[consoleplayer]);
if ( !maxdist ) maxdist = CVar.GetCVar('swwm_maxtargetdist',players[consoleplayer]);
dbar = damagebars.GetInt();
mxdist = maxdist.GetInt();
}
if ( next )
{
next.dbar = dbar;
next.mxdist = mxdist;
}
// is target gone or dead?
if ( !mytarget || (mytarget.Health <= 0) )
{
// we're done
if ( updated > level.maptime ) updated = level.maptime;
lasthealth = 0;
prevpos = pos; // prevent stuttering
intp.Update(lasthealth);
if ( level.maptime > updated+35 ) Destroy();
return;
}
// don't update dormant targets
if ( mytarget.bDORMANT )
return;
// in deathmatch, don't update for hostile players
if ( deathmatch && mytarget.player && (mytarget.player.mo == mytarget) )
{
if ( teamplay && (mytarget.player.GetTeam() != players[consoleplayer].GetTeam()) )
return;
else if ( !teamplay ) return;
}
// only update height/position while alive
bool heightchanged = false;
if ( height != mytarget.height ) heightchanged = true;
height = mytarget.height;
if ( heightchanged || (mytarget.pos != oldpos) || (mytarget.prev != oldprev) )
{
oldpos = mytarget.pos;
oldprev = mytarget.prev;
pos = level.Vec3Offset(mytarget.pos,(0,0,height));
prevpos = level.Vec3Offset(mytarget.prev,(0,0,height));
}
tcnt++;
if ( (tcnt == 1) && !mytarget.player )
{
// post-spawn health inflation check
if ( lasthealth > maxhealth )
{
maxhealth = lasthealth;
intp.Reset(lasthealth);
}
}
if ( (tcnt == 6) && !mytarget.player )
{
// legendoom check
for ( Inventory i=mytarget.inv; i; i=i.inv )
{
if ( i.GetClassName() != "LDLegendaryMonsterToken" ) continue;
legged = true;
// adjust for health inflation
if ( lasthealth > maxhealth )
{
maxhealth = lasthealth;
intp.Reset(lasthealth);
}
}
}
if ( legged && !mutated )
{
// check inventory regularly to mark as mutated
for ( Inventory i=mytarget.inv; i; i=i.inv )
{
if ( i.GetClassName() != "LDLegendaryMonsterTransformed" ) continue;
mutated = true;
Console.Printf(StringTable.Localize("$SWWM_LTFORM"),mytag);
}
}
bFRIENDLY = mytarget.bFRIENDLY;
if ( mytarget.Health < lasthealth ) firsthit = true;
lasthealth = mytarget.Health;
intp.Update(lasthealth);
// special update conditions
if ( dbar )
{
if ( (dbar == 2) && (lasthealth >= maxhealth) )
return;
else if ( (dbar == 1) && !firsthit )
return;
}
if ( (mytarget.bISMONSTER || mytarget.player) && !mytarget.bINVISIBLE && !mytarget.bCORPSE )
{
bool straifu = false;
if ( (gameinfo.gametype&GAME_Strife) && (!mytarget.bINCOMBAT && !mytarget.bJUSTATTACKED) || (mytarget is 'Beggar') || (mytarget is 'Peasant') )
straifu = true;
// players (but not voodoo dolls), always visible
if ( mytarget.player && (mytarget.player.mo == mytarget) ) updated = level.maptime+35;
// friendlies within a set distance
else if ( mytarget.bFRIENDLY && ((mxdist <= 0) || (mytarget.Vec3To(players[consoleplayer].Camera).length() < mxdist)) && players[consoleplayer].Camera.CheckSight(mytarget,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) updated = level.maptime+35;
// enemies within a set distance that have us as target
else if ( !straifu && mytarget.target && (mytarget.target.Health > 0) && (mytarget.target.player == players[consoleplayer]) && ((mxdist <= 0) || (mytarget.Vec3To(players[consoleplayer].Camera).length() < mxdist)) && players[consoleplayer].Camera.CheckSight(mytarget,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) updated = level.maptime+70;
// any visible enemies within one quarter of the set distance
else if ( ((mxdist <= 0) || (mytarget.Vec3To(players[consoleplayer].Camera).length() < (mxdist/4))) && players[consoleplayer].Camera.CheckSight(mytarget,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) updated = level.maptime;
}
else if ( (mytarget is 'BossBrain') || (mytarget is 'SWWMHangingKeen') )
{
// special stuff, only if visible
if ( ((mxdist <= 0) || (mytarget.Vec3To(players[consoleplayer].Camera).length() < (mxdist/4))) && players[consoleplayer].Camera.CheckSight(mytarget,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) updated = level.maptime;
}
}
}
// Korax instakill handler
Class UglyBoyGetsFuckedUp : Thinker
{
bool wedone;
override void Tick()
{
if ( wedone ) return;
if ( level.killed_monsters < level.total_monsters )
{
// stop portal door
int sidx = level.CreateSectorTagIterator(145).Next();
if ( sidx == -1 ) return;
Sector door = level.Sectors[sidx];
let ti = ThinkerIterator.Create("SectorEffect");
SectorEffect se;
while ( se = SectorEffect(ti.Next()) )
{
if ( se.GetSector() != door ) continue;
se.Destroy();
door.StopSoundSequence(CHAN_VOICE);
}
return;
}
wedone = true;
level.ExecuteSpecial(Door_Open,null,null,false,145,8);
Destroy();
}
}
// Track last damage source to blame fall damage on
Class SWWMWhoPushedMe : Thinker
{
Actor tracked, instigator;
static void SetInstigator( Actor b, Actor whomst )
{
if ( !b || !whomst ) return;
let ti = ThinkerIterator.Create("SWWMWhoPushedMe",STAT_INFO);
SWWMWhoPushedMe ffd;
while ( ffd = SWWMWhoPushedMe(ti.Next()) )
{
if ( ffd.tracked != b ) continue;
ffd.instigator = whomst;
return;
}
ffd = new("SWWMWhoPushedMe");
ffd.ChangeStatNum(STAT_INFO);
ffd.tracked = b;
ffd.instigator = whomst;
}
static Actor RecallInstigator( Actor b )
{
if ( !b ) return null;
let ti = ThinkerIterator.Create("SWWMWhoPushedMe",STAT_INFO);
SWWMWhoPushedMe ffd;
while ( ffd = SWWMWhoPushedMe(ti.Next()) )
{
if ( ffd.tracked != b ) continue;
Actor whomst = ffd.instigator;
ffd.Destroy();
return whomst;
}
return null;
}
}
Class SWWMDamageAccumulator : Thinker
{
Actor victim, inflictor, source;
Array<Int> amounts;
int total;
Name type;
bool dontgib;
int flags;
override void Tick()
{
Super.Tick();
// so many damn safeguards in this
if ( !victim )
{
Destroy();
return;
}
int gibhealth = victim.GetGibHealth();
// おまえはもう死んでいる
if ( (victim.health-total <= gibhealth) && !dontgib )
{
// safeguard for inflictors that have somehow ceased to exist, which apparently STILL CAN HAPPEN
if ( inflictor ) inflictor.bEXTREMEDEATH = true;
else type = 'Extreme';
}
// make sure accumulation isn't reentrant
if ( inflictor && (inflictor is 'EvisceratorChunk') ) inflictor.bAMBUSH = true;
// 何?
for ( int i=0; i<amounts.Size(); i++ )
{
if ( !victim ) break;
victim.DamageMobj(inflictor,source,amounts[i],type,DMG_THRUSTLESS|flags);
}
// clean up
if ( inflictor )
{
if ( inflictor is 'EvisceratorChunk' ) inflictor.bAMBUSH = false;
inflictor.bEXTREMEDEATH = false;
}
Destroy();
}
static void Accumulate( Actor victim, int amount, Actor inflictor, Actor source, Name type, bool dontgib = false, int flags = 0 )
{
if ( !victim ) return;
let ti = ThinkerIterator.Create("SWWMDamageAccumulator",STAT_USER);
SWWMDamageAccumulator a, match = null;
while ( a = SWWMDamageAccumulator(ti.Next()) )
{
if ( a.victim != victim ) continue;
match = a;
break;
}
if ( !match )
{
match = new("SWWMDamageAccumulator");
match.ChangeStatNum(STAT_USER);
match.victim = victim;
match.amounts.Clear();
}
match.amounts.Push(amount);
match.total += amount;
match.inflictor = inflictor;
match.source = source;
match.type = type;
match.dontgib = dontgib;
match.flags = flags;
}
static clearscope int GetAmount( Actor victim )
{
let ti = ThinkerIterator.Create("SWWMDamageAccumulator",STAT_USER);
SWWMDamageAccumulator a, match = null;
while ( a = SWWMDamageAccumulator(ti.Next()) )
{
if ( a.victim != victim ) continue;
return a.total;
}
return 0;
}
}