swwmgz_m/zscript/swwm_thinkers.zsc
Marisa Kirisame c81fa6fcf1 Clear out files for unimplemented languages.
Refactored and finetuned mklang code.
Fix autouse behavior of health items.
Stats tracking for headpats, kisses, enemies befriended.
Split mission texts into a separate language file.
2021-02-08 18:48:09 +01:00

1277 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, parries, pparries, pats,
befriend, smooch;
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;
}
private Class<Weapon> WeaponFromInflictor( Actor inflictor, Name damagetype )
{
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
return which;
}
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 = WeaponFromInflictor(inflictor,damagetype);
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[6];
int xscore[6];
String xstr[6];
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<6; 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;
}
}