// common code goes here // extra sound channels for the mod enum ESWWMGZChannels { CHAN_YOUDONEFUCKEDUP = 63200, // exception handler CHAN_DEMOVOICE = 63201, // demolitionist voices CHAN_FOOTSTEP = 63202, // footstep sounds and others CHAN_WEAPONEXTRA = 63203, // additional weapon sounds (usually loops) CHAN_POWERUP = 63204, // powerup sounds CHAN_POWERUPEXTRA = 63205, // additional powerup sounds CHAN_JETPACK = 63206, // jetpack sound CHAN_ITEMEXTRA = 63207, // additional item sounds CHAN_WEAPONEXTRA2 = 63208, // additional weapon sound slot CHAN_WEAPONEXTRA3 = 63209, // additional weapon sound slot (again) CHAN_DAMAGE = 63210, // used for impact/hit sounds CHAN_AMBEXTRA = 63211, // player ambience when submerged CHAN_DEMOVOICEAUX = 63212, // extra channel to make oneliner voices louder CHAN_DEMOVOICEAUX2 = 63213, // how many more channels do I need??? CHAN_DEMOVOICEAUX3 = 63214, // oh god, the loudening CHAN_FUELREGEN = 63215 // sound of fuel regenerating }; const FallbackTag = "AWESOME IT'S PENIS"; // used on tag processing, please don't mind the actual string used) const MaxBouncePerTic = 40; // maximum simultaneous bounces in one tic for a lightweight actor before we consider it's stuck // super-minimal tick override that simply advances states when not frozen Mixin Class SWWMMinimalTick { override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; if ( !CheckNoDelay() || (tics == -1) ) return; if ( tics > 0 ) tics--; while ( !tics ) { if ( !SetState(CurState.NextState) ) return; } } } // + move by velocity Mixin Class SWWMMinimalMovingTick { override void Tick() { prev = pos; if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; SetOrigin(level.Vec3Offset(pos,vel),true); if ( !CheckNoDelay() || (tics == -1) ) return; if ( tics > 0 ) tics--; while ( !tics ) { if ( !SetState(CurState.NextState) ) return; } } } // + check water level after move Mixin Class SWWMMinimalMovingWaterTick { override void Tick() { prev = pos; if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; SetOrigin(level.Vec3Offset(pos,vel),true); UpdateWaterLevel(); if ( !CheckNoDelay() || (tics == -1) ) return; if ( tics > 0 ) tics--; while ( !tics ) { if ( !SetState(CurState.NextState) ) return; } } } // absolutely non-interactive actor that is net-safe for clientside use Class SWWMNonInteractiveActor : Actor { Mixin SWWMMinimalTick; Default { +NOINTERACTION; +NOBLOCKMAP; // needed since we don't use the internal Tick +SYNCHRONIZED; +DONTBLAST; FloatBobPhase 0; Radius .1; Height 0; } } // basic "does nothing" actor, used to remove stuff in CheckReplacement Class SWWMNothing : SWWMNonInteractiveActor { States { Spawn: TNT1 A 1; Stop; } } // mixins for gravity-affected missiles that need correct water behavior Mixin Class SWWMMissileFix { Default { +NOFRICTION; } override void FallAndSink( double grav, double oldfloorz ) { if ( bNOGRAVITY || (waterlevel < 1) ) { Super.FallAndSink(grav,oldfloorz); return; } vel *= .99; if ( pos.z > floorz ) vel.z -= grav*.01; } } Class SWWMDamageAccumulator : Inventory { Actor inflictor, source; Array amounts; int total; Name type; bool dontgib; int flags; override void DoEffect() { Super.DoEffect(); // so many damn safeguards in this if ( !Owner || (Owner.Health <= 0) ) { Destroy(); return; } int gibhealth = Owner.GetGibHealth(); // お前はもう死んでいる if ( (Owner.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'; } // 何? foreach ( dmg:amounts ) { if ( !Owner ) break; Owner.DamageMobj(inflictor,source,dmg,type,DMG_THRUSTLESS|flags); } // clean up if ( inflictor ) inflictor.bEXTREMEDEATH = inflictor.default.bEXTREMEDEATH; Destroy(); } static void Accumulate( Actor victim, int amount, Actor inflictor, Actor source, Name type, bool dontgib = false, int flags = 0 ) { if ( !victim ) return; SWWMDamageAccumulator match = SWWMDamageAccumulator(victim.FindInventory("SWWMDamageAccumulator")); if ( !match ) { match = SWWMDamageAccumulator(Spawn("SWWMDamageAccumulator")); match.AttachToOwner(victim); } 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 match = SWWMDamageAccumulator(victim.FindInventory("SWWMDamageAccumulator")); if ( match ) { if ( match.source && match.source.FindInventory("AngeryPower") ) return (match.total>85899345)?int.max:(match.total*25); return match.total; } return 0; } default { +INVENTORY.UNTOSSABLE; +INVENTORY.UNDROPPABLE; +INVENTORY.UNCLEARABLE; } } // Track last damage source to blame fall damage on Class SWWMWhoPushedMe : Inventory { Actor instigator; int killtimer; static void SetInstigator( Actor b, Actor whomst ) { if ( !b || !whomst ) return; SWWMWhoPushedMe ffd = SWWMWhoPushedMe(b.FindInventory("SWWMWhoPushedMe")); if ( ffd ) { ffd.instigator = whomst; ffd.killtimer = 0; // cancel kill timer return; } ffd = SWWMWhoPushedMe(Spawn("SWWMWhoPushedMe")); ffd.AttachToOwner(b); ffd.instigator = whomst; } static Actor RecallInstigator( Actor b ) { if ( !b ) return null; SWWMWhoPushedMe ffd = SWWMWhoPushedMe(b.FindInventory("SWWMWhoPushedMe")); if ( ffd ) { Actor whomst = ffd.instigator; ffd.Destroy(); return whomst; } return null; } override void OwnerDied() { Super.OwnerDied(); if ( killtimer > 0 ) return; killtimer = 5; } override void DoEffect() { Super.DoEffect(); if ( killtimer <= 0 ) return; killtimer--; if ( killtimer > 0 ) return; Destroy(); } override void ModifyDamage( int damage, Name damageType, out int newdamage, bool passive, Actor inflictor, Actor source, int flags ) { if ( !passive || (damageType != 'Falling') || (killtimer > 0) ) return; killtimer = 5; } default { +INVENTORY.UNTOSSABLE; +INVENTORY.UNDROPPABLE; +INVENTORY.UNCLEARABLE; } } Class SWWMFlyTracker : Inventory { Actor instigator; Vector3 startpos, curpos; double maxdist; int gracepd; static void Track( Actor b, Actor whomst ) { if ( !b || !whomst ) return; SWWMFlyTracker ffd = SWWMFlyTracker(b.FindInventory("SWWMFlyTracker")); if ( ffd ) { ffd.instigator = whomst; return; } ffd = SWWMFlyTracker(Spawn("SWWMFlyTracker")); ffd.AttachToOwner(b); ffd.instigator = whomst; ffd.curpos = ffd.startpos = b.pos; ffd.maxdist = 0; } override void DoEffect() { maxdist = max(maxdist,level.Vec3Diff(startpos,curpos).length()); if ( !Owner || Owner.bFLOAT || Owner.bNOGRAVITY || (Owner.waterlevel > 1) || (Owner.pos.z <= Owner.floorz) || !Owner.TestMobjZ(false) ) { gracepd++; if ( gracepd < 10 ) return; if ( instigator ) SWWMUtility.AchievementProgress("flight",int(maxdist),instigator.player); Destroy(); return; } gracepd = 0; curpos = Owner.pos; } default { +INVENTORY.UNTOSSABLE; +INVENTORY.UNDROPPABLE; +INVENTORY.UNCLEARABLE; } } // very lightweight combat tracker, more Souls-y Class SWWMQuickCombatTracker : Inventory { int lifespan, lasthealth, maxhealth; int laghealth[10]; int cummdamage, cummspan, cummflash; // please do not misread int fadein; // used for the hud double lvheight; // height while alive (used by the hud for positioning) SmoothDynamicValueInterpolator intp; SmoothLinearValueInterpolator intpl; PlayerInfo myplayer; // for multiplayer compatibility (this breaks inventory logic, but alas...) String mytag; void UpdateTag( SWWMHandler hnd ) { if ( Owner && (Owner.player || Owner.bISMONSTER || (Owner is 'BossBrain') || (Owner is 'SWWMHangingKeen') || (Owner is 'PlayerPawn')) ) { String realtag = swwm_funtags?SWWMUtility.GetFunTag(hnd,Owner,FallbackTag):Owner.GetTag(FallbackTag); if ( realtag == FallbackTag ) { realtag = Owner.GetClassName(); SWWMUtility.BeautifyClassName(realtag); } mytag = Owner.player?(Owner.player.mo!=Owner)?multiplayer?String.Format(StringTable.Localize("$FN_VOODOO"),Owner.player.GetUserName()):StringTable.Localize("$FN_VOODOO_NP"):Owner.player.GetUserName():((Owner is 'PlayerPawn')&&(!Owner.player))?StringTable.Localize("$FN_VOODOO_NP"):realtag; } else mytag = ""; } override void AttachToOwner( Actor other ) { // GROSS HACK TO ATTACH TO VOODOO DOLLS if ( other.player && (other.player.mo != other) ) { BecomeItem(); Owner = other; inv = Owner.inv; Owner.inv = self; // we unfortunately can't access this, but hopefully it shouldn't be necessary //InventoryID = Owner.InventoryID++; return; } Super.AttachToOwner(other); } static SWWMQuickCombatTracker Update( SWWMHandler hnd, PlayerInfo p, Actor target, int damage = 0 ) { // no-damage entities get no healthbars if ( target.bNODAMAGE ) return null; // also don't give healthbars to moths if ( target is 'LampMoth' ) return null; // don't give them to boss brains unless visible if ( (target is 'BossBrain') && !p.Camera.CheckSight(target,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) return null; SWWMQuickCombatTracker t = null; for ( Inventory i=target.inv; i; i=i.inv ) { if ( !(i is 'SWWMQuickCombatTracker') ) continue; let tt = SWWMQuickCombatTracker(i); if ( tt.myplayer != p ) continue; t = tt; break; } // must exclude players (unless they're voodoo dolls) // this is mainly for deathmatch so players you hit don't become easy to track when out of view bool realmonster = target.bISMONSTER&&(!target.player||(target.player&&(target.player.mo!=target))); if ( t ) { // re-fade in if ( t.lifespan < 20 ) t.fadein = t.lifespan/4; t.lifespan = max(t.lifespan,realmonster?((damage>0)?700:70):((damage>0)?140:35)); if ( damage > 0 ) { t.cummdamage += damage; t.cummspan = realmonster?120:30; t.cummflash = 15; } return t; } t = SWWMQuickCombatTracker(Spawn("SWWMQuickCombatTracker")); t.myplayer = p; t.AttachToOwner(target); // players always use SpawnHealth if ( target.player ) t.maxhealth = target.SpawnHealth(); else t.maxhealth = max(target.health,target.SpawnHealth()); t.lasthealth = target.health; int prevhealth = min(t.maxhealth,target.health+damage); // guessed health before damage was dealt for ( int i=0; i<10; i++ ) t.laghealth[i] = prevhealth; if ( damage > 0 ) { t.cummdamage = damage; t.cummspan = realmonster?120:30; t.cummflash = 15; } t.intp = SmoothDynamicValueInterpolator.Create(t.lasthealth,.5); t.intpl = SmoothLinearValueInterpolator.Create(t.laghealth[9],max(1,t.maxhealth/50)); t.lifespan = realmonster?((damage>0)?700:70):((damage>0)?140:35); t.fadein = 0; t.lvheight = target.Height; t.UpdateTag(hnd); return t; } // expiration, and interpolator updates override void DoEffect() { Super.DoEffect(); // immediately expire if the owner is gone or has morphed if ( !Owner || Owner.bUnmorphed ) { Destroy(); return; } // cap lifespan if owner is dead if ( Owner.Health <= 0 ) lifespan = min(lifespan,35); else lvheight = Owner.Height; if ( cummflash > 0 ) cummflash--; if ( cummspan > 0 ) { cummspan--; if ( cummspan <= 0 ) cummdamage = 0; } if ( Owner.Health > lasthealth ) { if ( !Owner.player && (Owner.Health > maxhealth) ) maxhealth = Owner.Health; for ( int i=9; i>0; i-- ) laghealth[i] = Owner.Health; } laghealth[0] = lasthealth = Owner.Health; intp.Update(lasthealth); intpl.Update(laghealth[9]); for ( int i=9; i>0; i-- ) laghealth[i] = laghealth[i-1]; if ( fadein < 5 ) fadein++; lifespan = max(0,lifespan-1); // only fully expire if owner dies // (this is so we can preserve our current fun tag) if ( (lifespan <= 0) && (Owner.Health <= 0) ) Destroy(); } override void PreTravelled() { // expire immediately when switching maps Destroy(); } default { +INVENTORY.UNTOSSABLE; +INVENTORY.UNDROPPABLE; +INVENTORY.UNCLEARABLE; } } // fractic-compatible interpolators, with double value Class SmoothLinearValueInterpolator { private double val, oldval, diff; static SmoothLinearValueInterpolator Create( double val, double diff ) { let v = new("SmoothLinearValueInterpolator"); v.oldval = v.val = val; v.diff = diff; return v; } void Reset( double newval ) { oldval = val = newval; } void Update( double newval ) { oldval = val; if ( abs(newval-val) < diff ) val = newval; else if ( val > newval ) val = max(newval,val-diff); else val = min(newval,val+diff); } double GetValue( double fractic = 1. ) { return (val~==oldval)?val:SWWMUtility.Lerp(oldval,val,fractic); } } Class SmoothDynamicValueInterpolator { private double val, oldval, factor, mindiff, maxdiff; static SmoothDynamicValueInterpolator Create( double val, double factor, double mindiff = 1., double maxdiff = 0. ) { let v = new("SmoothDynamicValueInterpolator"); v.oldval = v.val = val; v.factor = factor; v.mindiff = mindiff; v.maxdiff = maxdiff; return v; } void Reset( double newval ) { oldval = val = newval; } void Update( double newval ) { oldval = val; if ( abs(newval-val) < mindiff ) val = newval; else { double diff = (maxdiff>0.)?min(abs(newval-val)*factor,maxdiff):(abs(newval-val)*factor); if ( val > newval ) val = max(newval,val-diff); else val = min(newval,val+diff); } } double GetValue( double fractic = 1. ) { return (val~==oldval)?val:SWWMUtility.Lerp(oldval,val,fractic); } }