// 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 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 w; int kills; } Class MonsterKill { Class 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 wstats; Array mstats; Array lstats; Array > alreadygot; int favweapon; // these two are used for mission updates Array clustervisit; Array secretdone; // hackaround for stuff getting lost Array > 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 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 WeaponFromInflictor( Actor inflictor, Name damagetype ) { Class 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; } void PreloadLevelStats() { // pre-adds all unvisited levels from the current cluster int nlevels = LevelInfo.GetLevelInfoCount(); for ( int i=0; i 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 which = WeaponFromInflictor(inflictor,damagetype); if ( !which ) return; for ( int i=0; i s.hiscore) ) s.hiscore = c.credits; // append to hud for ( int i=0; i 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 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)(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_BRCALBUM" ) text = "SWWM_LORETXT_BRCALBUM2"; // comment about kirin's song 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_IBUKIMILK" ) text = "SWWM_LORETXT_IBUKIMILK3"; // tasted by kirin 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() || (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_IBUKIMILK" ) text = "SWWM_LORETXT_IBUKIMILK2"; // tasted by demo else if ( text ~== "SWWM_LORETXT_NANA" ) text = "SWWM_LORETXT_NANA2"; // demo met nana 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 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