swwmgz_m/zscript/swwm_thinkers_player.zsc
Marisa the Magician 2aa0ea4680 More work towards Legacy of Rust support (with caveats).
As of this commit, do not consider the experience when playing that new
expansion to be complete. I've only partially written some of the mission texts
and rudimentarily enhanced some boss fights.

Currently there is one major limitation in that the intermission texts cannot
be replaced, as they're hardcoded inside the UMAPINFO. I don't know if I can
work around that.
2025-08-20 16:47:24 +02:00

765 lines
25 KiB
Text

// player-specific thinkers
Class SWWMStaticThinker : Thinker abstract
{
// shell class to iterate easier on sanity checks
// all subclasses should be guaranteed to have a statnum of STAT_STATIC
// if they don't, something is very wrong
}
// Dedicated mission log (for custom maps)
Class SWWMMissionLog : SWWMStaticThinker
{
Array<String> entries;
bool clockset;
int year, month, day, hour, minute;
String tz;
static clearscope SWWMMissionLog Get()
{
let ml = SWWMMissionLog(ThinkerIterator.Create('SWWMMissionLog',STAT_STATIC).Next());
return ml;
}
static play void AddLog( String str )
{
let ml = SWWMMissionLog(ThinkerIterator.Create('SWWMMissionLog',STAT_STATIC).Next());
if ( !ml )
{
ml = new('SWWMMissionLog');
ml.ChangeStatNum(STAT_STATIC);
}
if ( ml.entries.Find(str) < ml.entries.Size() ) return;
ml.entries.Push(str);
Console.Printf(StringTable.Localize("$SWWM_NEWMISSION"));
}
static play void SetClock( int year, int month, int day, int hour, int minute, String tz = "JST" )
{
let ml = SWWMMissionLog(ThinkerIterator.Create('SWWMMissionLog',STAT_STATIC).Next());
if ( !ml )
{
ml = new('SWWMMissionLog');
ml.ChangeStatNum(STAT_STATIC);
}
ml.clockset = true;
ml.year = year;
ml.month = month;
ml.day = day;
ml.hour = hour;
ml.minute = minute;
ml.tz = tz;
}
}
// Stats
Class WeaponUsage
{
Class<Weapon> w;
int kills;
}
Class MonsterKill
{
Class<Actor> m;
int kills;
}
Class LevelStat
{
bool hub, visited;
int cluster;
String levelname, mapname;
int kcount, ktotal;
int icount, itotal;
int scount, stotal;
int time, par, suck;
}
Class SWWMStats : SWWMStaticThinker
{
PlayerInfo myplayer;
int lastspawn, dashcount, boostcount, stompcount, airtime, kills,
deaths, damagedealt, hdamagedealt, damagetaken, hdamagetaken,
mkill, hiscore, 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;
// hackaround for stuff getting lost
Array<Class<SWWMCollectible> > ownedcollectibles;
int plushuses;
// for pistol start info (to avoid it within hubs)
int lastcluster;
// for trash removal achievement
int nazicleanup;
// stored for hexen
int puzzlecnt, realpuzzlecnt;
// easter eggs
int silveregg, quadegg, deepegg;
bool oldcheat;
bool gotyorick;
// caching
SWWMHandler hnd;
bool GotWeapon( Class<Weapon> which )
{
foreach ( a:alreadygot )
{
if ( a == which ) return true;
}
alreadygot.Push(which);
return false;
}
void CleanGotWeapons()
{
// clean up the list of weapons we don't have anymore
// so the obtain lines play again
for ( int i=0; i<alreadygot.Size(); i++ )
{
// special case for dual weapons
String cn = alreadygot[i].GetClassName();
if ( (cn.Left(4) ~== "Dual") )
{
int ss = myplayer.mo.CountInv(GetDefaultByType(alreadygot[i]).SisterWeaponType);
if ( ss == 2 ) continue;
alreadygot.Delete(i);
i--;
continue;
}
if ( myplayer.mo.FindInventory(alreadygot[i]) ) continue;
alreadygot.Delete(i);
i--;
}
}
private Class<Weapon> WeaponFromInflictor( Actor inflictor, Name damagetype )
{
Class<Weapon> which = myplayer.ReadyWeapon?myplayer.ReadyWeapon.GetClass():null;
if ( inflictor is 'SWWMPuff' ) inflictor = inflictor.master; // special puffs transfer real inflictor through master pointer
if ( inflictor is 'Weapon' ) which = Weapon(inflictor).GetClass();
if ( which is 'DualExplodiumGun' ) which = 'ExplodiumGun'; // don't credit sister weapon
if ( inflictor && inflictor.FindInventory('ParriedBuff') ) which = 'DoomWeapon'; // gross hack
// properly credit some projectiles to their respective gun
else if ( inflictor is 'AirBullet' ) which = 'DeepImpact';
else if ( (inflictor is 'HammerShockwave') || (inflictor is 'HammerRadiusShockwave') ) which = 'ItamexHammer';
else if ( (inflictor is 'ExplodiumMagArm') || (inflictor is 'ExplodiumMagProj') || (inflictor is 'ExplodiumBulletImpact') ) which = 'ExplodiumGun';
else if ( (inflictor is 'GoldenSubImpact') || (inflictor is 'GoldenSubSubImpact') ) which = 'Spreadgun';
else if ( (inflictor is 'EvisceratorChunk') || (inflictor is 'EvisceratorProj') ) which = 'Eviscerator';
else if ( inflictor is 'SheenTrail' ) which = 'HeavyMahSheenGun';
else if ( (inflictor is 'HellblazerMissile') || (inflictor is 'HellblazerArm') ) which = 'Hellblazer';
else if ( (inflictor is 'QuadProj') || (inflictor is 'QuadExplArm') || (inflictor is 'OnFire') ) which = 'Quadravol';
else if ( (inflictor is 'BigBiospark') || (inflictor is 'BiosparkBall') || (inflictor is 'BiosparkBeamImpact') || (inflictor is 'BiosparkComboImpact') || (inflictor is 'BiosparkComboImpactSub') || (inflictor is 'BiosparkBeam') || (inflictor is 'BiosparkArc') || (inflictor is 'BiosparkCore') ) which = 'Sparkster';
else if ( (inflictor is 'SilverAirRip') || (inflictor is 'SilverImpact') ) which = 'SilverBullet';
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 'MisterBulletImpact') || (inflictor is 'MisterPop') || (inflictor is 'MisterFuzzy') || (inflictor is 'MisterGrenade') || (inflictor is 'MisterRailBeam') ) which = 'MisterRifle';
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;
}
}
private LevelStat FindLevelStats( String mapname )
{
foreach ( ls:lstats )
{
if ( ls.mapname != mapname ) continue;
return ls;
}
return null;
}
// we doin' that thing again, yup
int ClusterRemap( int clus, int levelnum )
{
if ( SWWMUtility.IsEviternityTwo() )
{
// clusters in eviternity 2 have to be remapped
if ( clus == 5 ) return 1;
if ( (clus == 6) || (clus == 13) ) return 2;
if ( (clus == 7) || (clus == 14) ) return 3;
if ( (clus == 8) || (clus == 15) ) return 4;
if ( (clus == 9) || (clus == 16) ) return 5;
if ( (clus == 10) || (clus == 17) ) return 6;
if ( (clus == 11) || (clus == 12) || (clus == 18) || (clus == 19) ) return 7;
}
else if ( SWWMUtility.IsEviternity() )
{
// we have to do some heavy lifting here because episodes don't match clusters
if ( levelnum <= 0 ) return clus;
if ( levelnum <= 5 ) return 1;
if ( levelnum <= 10 ) return 2;
if ( levelnum <= 15 ) return 3;
if ( levelnum <= 20 ) return 4;
if ( levelnum <= 25 ) return 5;
if ( levelnum <= 30 ) return 6;
if ( levelnum <= 31 ) return 7;
if ( levelnum <= 32 ) return 8;
}
else if ( SWWMUtility.IsLegacyOfRust() )
{
// legacy of rust has its quirks, as with umapinfo there are
// technically no clusters
if ( levelnum <= 0 ) return clus;
if ( (levelnum <= 7) || (levelnum == 15) ) return 28;
else if ( (levelnum <= 14) || (levelnum == 16) ) return 29;
}
return clus;
}
void PreloadLevelStats()
{
// pre-adds all unvisited levels from the current cluster
int nlevels = LevelInfo.GetLevelInfoCount();
int ourcluster = ClusterRemap(level.cluster,level.levelnum);
for ( int i=0; i<nlevels; i++ )
{
let li = LevelInfo.GetLevelInfo(i);
if ( !li.isValid() || !LevelInfo.MapExists(li.mapname)
|| FindLevelStats(li.mapname) )
continue;
int theircluster = ClusterRemap(li.cluster,li.levelnum);
if ( theircluster != ourcluster )
continue;
let ls = new('LevelStat');
// we can automatically assume that all levels from the same cluster will have this flag as well
ls.hub = !!(level.clusterflags&level.CLUSTER_HUB);
ls.visited = false;
ls.cluster = theircluster;
ls.levelname = li.LookupLevelName();
// level name may contain trailing whitespace due to DEHACKED nonsense, so strip it
ls.levelname.StripRight();
if ( li.authorname == "" ) // the author name might be part of the level name, try to strip it
{
int iof;
if ( ((iof = ls.levelname.RightIndexOf(" - by: ")) != -1) || ((iof = ls.levelname.RightIndexOf(" - by ")) != -1) || ((iof = ls.levelname.RightIndexOf(" - ")) != -1) )
ls.levelname.Truncate(iof);
}
ls.mapname = li.mapname;
// append after last entry whose cluster is lower or equal to our own
// or just push otherwise
int lastent = -1, i = 0;
foreach ( ls:lstats )
{
if ( ls.cluster <= li.cluster )
lastent = i;
i++;
}
if ( lastent != -1 ) lstats.Insert(lastent+1,ls);
else lstats.Push(ls);
}
}
void AddLevelStats()
{
// overwrite any existing entry
let ls = FindLevelStats(level.mapname);
if ( ls )
{
ls.visited = true;
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;
return;
}
ls = new('LevelStat');
ls.hub = !!(level.clusterflags&level.CLUSTER_HUB);
ls.visited = true;
ls.cluster = level.cluster;
ls.levelname = level.levelname;
// level name may contain trailing whitespace due to DEHACKED nonsense, so strip it
ls.levelname.StripRight();
if ( level.authorname == "" ) // the author name might be part of the level name, try to strip it
{
int iof;
if ( ((iof = ls.levelname.RightIndexOf(" - by: ")) != -1) || ((iof = ls.levelname.RightIndexOf(" - by ")) != -1) || ((iof = ls.levelname.RightIndexOf(" - ")) != -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;
// append to last entry whose cluster is lower or equal to our own
// or just push otherwise
int lastent = -1, i = 0;
foreach ( ls:lstats )
{
if ( ls.cluster <= level.cluster )
lastent = i;
i++;
}
if ( lastent != -1 ) lstats.Insert(lastent,ls);
else lstats.Push(ls);
}
void AddWeaponKill( Actor inflictor, Actor victim, Name damagetype )
{
if ( victim )
{
if ( !hnd ) hnd = SWWMHandler(EventHandler.Find('SWWMHandler'));
Class<Actor> mvictim = SWWMUtility.MergeMonster(hnd,victim.GetClass());
bool found = false;
foreach ( ms:mstats )
{
if ( ms.m != mvictim ) continue;
found = true;
ms.kills++;
break;
}
if ( !found )
{
let ms = new('MonsterKill');
ms.m = mvictim;
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 : SWWMStaticThinker
{
PlayerInfo myplayer;
int credits;
static void Give( PlayerInfo p, int amount )
{
let c = Find(p);
if ( !c ) return;
// safeguard
if ( amount < 0 ) ThrowAbortException("SWWMCredits.Give() called with negative amount");
if ( c.credits+amount < c.credits ) c.credits = 999999999;
else c.credits = min(999999999,c.credits+amount);
let s = SWWMStats.Find(p);
if ( s && (c.credits > s.hiscore) ) s.hiscore = c.credits;
// append to hud
for ( int i=0; i<MAXPLAYERS; i++ )
{
if ( players[i] != p ) continue;
EventHandler.SendInterfaceEvent(i,"swwmhudgivescore",amount);
break;
}
SWWMLoreLibrary.Add(p,"ScoreSystem");
}
static clearscope bool CanTake( PlayerInfo p, int amount )
{
let c = Find(p);
if ( !c ) return false;
// safeguard
if ( amount < 0 ) ThrowAbortException("SWWMCredits.CanTake() called with negative amount");
// too much!
if ( (amount > 999999999) || (c.credits-amount < 0) || (c.credits-amount > c.credits) ) return false;
return true;
}
static bool Take( PlayerInfo p, int amount )
{
let c = Find(p);
if ( !c ) return false;
// safeguard
if ( amount < 0 ) ThrowAbortException("SWWMCredits.Take() called with negative amount");
// too much!
if ( (amount > 999999999) || (c.credits-amount < 0) || (c.credits-amount > c.credits) ) return false;
c.credits -= amount;
// append to hud
for ( int i=0; i<MAXPLAYERS; i++ )
{
if ( players[i] != p ) continue;
EventHandler.SendInterfaceEvent(i,"swwmhudtakescore",amount);
break;
}
return true;
}
static clearscope int Get( PlayerInfo p )
{
let c = Find(p);
if ( !c ) return 0;
return c.credits;
}
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;
}
}
// Lore holder
enum ELoreTab
{
LORE_ITEM,
LORE_ENEMY,
LORE_PEOPLE,
LORE_LORE // lol
};
Class SWWMLore
{
String tag, text, assoc;
TextureID img;
int tab;
bool read;
}
Class SWWMLoreLibrary : SWWMStaticThinker
{
PlayerInfo myplayer;
Array<SWWMLore> ent;
int lastaddtic;
static bool PreVerify( String ref )
{
// restrictions
let mlog = SWWMMissionLog.Get();
if ( !(gameinfo.gametype&GAME_Raven) && (!mlog || ((mlog.year < 2171) && (mlog.month < 3))) )
{
if ( ref ~== "Parthoris" ) return true;
if ( ref ~== "Sidhe" ) return true;
if ( ref ~== "SerpentRiders" ) return true;
}
if ( !(gameinfo.gametype&GAME_Hexen) && (!mlog || ((mlog.year < 2171) && (mlog.month < 4))) )
{
if ( ref ~== "Cronos" ) return true;
if ( ref ~== "Kirin" ) return true; // not met
if ( ref ~== "Alakir" ) return true; // likewise
if ( ref ~== "Fabricator" ) return true; // not yet introduced
if ( ref ~== "Administrators" ) return true; // not met
}
// check if entry is for a collectible
foreach ( cls:AllActorClasses )
{
let c = (Class<SWWMCollectible>)(cls);
if ( !c || (c == 'SWWMCollectible') ) continue;
let def = GetDefaultByType(c);
// skip if we match and it's not for this game
if ( (c.GetClassName() == ref) && !def.ValidGame() )
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 )
{
if ( developer >= 2 ) Console.Printf("Entry \"%s\" defines no tab.",ref);
return true;
}
if ( StringTable.Localize(text,false) == text )
{
if ( developer >= 2 ) 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
let mlog = SWWMMissionLog.Get();
if ( (gameinfo.gametype&GAME_Hexen) || (mlog && (mlog.year >= 2171) && (mlog.month >= 4)) )
{
if ( text ~== "SWWM_LORETXT_AKARIPROJECT" )
text = "SWWM_LORETXT_AKARIPROJECT3"; // mentions kirin
else if ( text ~== "SWWM_LORETXT_ANARUKON" )
text = "SWWM_LORETXT_ANARUKON2"; // comments from miyamoto-xanai wedding
else if ( text ~== "SWWM_LORETXT_DEMOLITIONIST" )
text = "SWWM_LORETXT_DEMOLITIONIST3"; // married to kirin
else if ( text ~== "SWWM_LORETXT_DEMONINVASION" )
text = "SWWM_LORETXT_DEMONINVASION4"; // meeting with azkhan
else if ( text ~== "SWWM_LORETXT_GHOULHUNT" )
text = "SWWM_LORETXT_GHOULHUNT2"; // met anthon anderken during the wedding
else if ( text ~== "SWWM_LORETXT_GODS" )
text = "SWWM_LORETXT_GODS2"; // beyond gods
else if ( text ~== "SWWM_LORETXT_HELL" )
text = "SWWM_LORETXT_HELL4"; // met father nostros during the wedding
else if ( text ~== "SWWM_LORETXT_MAIDBOT" )
text = "SWWM_LORETXT_MAIDBOT2"; // married to kirin
else if ( text ~== "SWWM_LORETXT_NANA" )
text = "SWWM_LORETXT_NANA3"; // stuff that happened at the wedding
else if ( text ~== "SWWM_LORETXT_NOVOSKHANA" )
text = "SWWM_LORETXT_NOVOSKHANA2"; // met up with the empress
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_SAYA" )
text = "SWWM_LORETXT_SAYA3"; // married kirin
else if ( text ~== "SWWM_LORETXT_SAFETYTETHER" )
text = "SWWM_LORETXT_SAFETYTETHER2"; // we're in cronos now
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_YNYKRON" )
text = "SWWM_LORETXT_YNYKRON2"; // confirmed to harm (but not kill) gods
else if ( text ~== "SWWM_LORETXT_ZANAVETH2" )
text = "SWWM_LORETXT_ZANAVETH22"; // met at wedding
else if ( text ~== "SWWM_LORETXT_MADCAT" )
text = "SWWM_LORETXT_MADCAT3"; // demolition quest released
else if ( text ~== "SWWM_LORETXT_MARISA" )
text = "SWWM_LORETXT_MARISA3"; // post-wedding update
}
if ( (gameinfo.gametype&GAME_Raven) || (mlog && (mlog.year >= 2171) && (mlog.month >= 3)) )
{
if ( text ~== "SWWM_LORETXT_AKARIPROJECT" )
text = "SWWM_LORETXT_AKARIPROJECT2"; // fiction becomes reality
else if ( text ~== "SWWM_LORETXT_ARCHDEMONS" )
text = "SWWM_LORETXT_ARCHDEMONS3"; // archdemon generals assassinated
else if ( text ~== "SWWM_LORETXT_DECADEMECH" )
text = "SWWM_LORETXT_DECADEMECH2"; // extra info
else if ( text ~== "SWWM_LORETXT_DEMONINVASION" )
text = "SWWM_LORETXT_DEMONINVASION3"; // events of doom 64 and such
else if ( text ~== "SWWM_LORETXT_DOOMGUY" )
text = "SWWM_LORETXT_DOOMGUY3"; // he gone
else if ( text ~== "SWWM_LORETXT_HELL" )
text = "SWWM_LORETXT_HELL3"; // invasion was a thing of the past
else if ( text ~== "SWWM_LORETXT_UAC" )
text = "SWWM_LORETXT_UAC3"; // events of doom 64 and more
else if ( text ~== "SWWM_LORETXT_MADCAT" )
text = "SWWM_LORETXT_MADCAT2"; // interstellar demolitionist released
else if ( text ~== "SWWM_LORETXT_MARISA" )
text = "SWWM_LORETXT_MARISA2"; // post-invasion update
}
if ( (gameinfo.gametype&GAME_Raven) || SWWMUtility.IsEviternity() || SWWMUtility.IsEviternityTwo() || SWWMUtility.IsLegacyOfRust() || (mlog && (mlog.year >= 2150) && (mlog.month >= 5)) )
{
if ( text ~== "SWWM_LORETXT_AKARILABS" )
text = "SWWM_LORETXT_AKARILABS2"; // demo won, akari project announced
else if ( text ~== "SWWM_LORETXT_ARCHDEMONS" )
text = "SWWM_LORETXT_ARCHDEMONS2"; // demon invasion repelled
else if ( text ~== "SWWM_LORETXT_BIGSHOT" )
text = "SWWM_LORETXT_BIGSHOT2"; // predictions about crimes_m
else if ( text ~== "SWWM_LORETXT_DEMOLITIONIST" )
text = "SWWM_LORETXT_DEMOLITIONIST2"; // demo rewarded with maidbot frame
else if ( text ~== "SWWM_LORETXT_DEMONINVASION" )
text = "SWWM_LORETXT_DEMONINVASION2"; // victory against invasion
else if ( text ~== "SWWM_LORETXT_DOOMGUY" )
text = "SWWM_LORETXT_DOOMGUY2"; // decommissioned
else if ( text ~== "SWWM_LORETXT_GENERICCUBE" )
text = "SWWM_LORETXT_GENERICCUBE2"; // info from mykka
else if ( text ~== "SWWM_LORETXT_HELL" )
text = "SWWM_LORETXT_HELL2"; // events of tnt/plutonia
else if ( text ~== "SWWM_LORETXT_NANA" )
text = "SWWM_LORETXT_NANA2"; // demo met nana
else if ( text ~== "SWWM_LORETXT_RIKA" )
text = "SWWM_LORETXT_RIKA2"; // saw demo's maidbot frame
else if ( text ~== "SWWM_LORETXT_SAYA" )
text = "SWWM_LORETXT_SAYA2"; // dating demo
else if ( text ~== "SWWM_LORETXT_UAC" )
text = "SWWM_LORETXT_UAC2"; // uac "reformed"
else if ( text ~== "SWWM_LORETXT_VOICEBOX" )
text = "SWWM_LORETXT_VOICEBOX2"; // remarks on demo's voice as a maidbot
else if ( text ~== "SWWM_LORETXT_ZANAVETH3" )
text = "SWWM_LORETXT_ZANAVETH32"; // iagb happened
}
// check if existing
foreach ( e:ent )
{
if ( e.tag != "$"..tag ) continue;
return true;
}
SWWMLore e = new('SWWMLore');
e.tag = "$"..tag;
if ( StringTable.Localize(e.tag) == "" )
{
if ( developer >= 2 ) 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 if ( ttab ~== "Enemy" ) e.tab = LORE_ENEMY;
else
{
if ( developer >= 2 ) Console.Printf("Entry \"%s\" has an incorrect tab setting of \"%s\".",ref,ttab);
return true;
}
e.text = "$"..text;
if ( StringTable.Localize(e.text) == "" )
{
if ( developer >= 2 ) Console.Printf("Entry \"%s\" has empty text.",ref);
return true;
}
e.assoc = "$"..assoc;
e.read = false;
// image for certain entries
e.img = TexMan.CheckForTexture("graphics/KBase/PFP_"..ref..".png");
// "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 ( deathmatch || 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);
foreach ( r:rel )
{
if ( r == "" ) continue;
bool rslt = DirectAdd(r);
if ( (developer >= 2) && !rslt )
Console.Printf("Related entry \"%s\" not found.",r);
}
}
}
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;
}
}