// HUT HUT HUT FOOTBALL
class Splot_Football : Splot_Weapon
{
Default
{
Weapon.SelectionOrder 3500;
Weapon.Kickback 100;
Inventory.PickupMessage "$GOTFOOTBALL";
Tag "$TAGFOOTBALL";
Splot_Weapon.HudIcon "PSKNZ0";
Weapon.SlotNumber 1;
Weapon.SlotPriority 20;
}
int steppy;
bool tackletime;
double oldangle;
double propelfactor;
double damagemulti;
double playerrealvel;
double additionalradius;
double additionalhitscan;
bool crashDetector;
double oldfov;
double newfov;
double dashalpha;
States
{
Ready:
PSKN A 1 A_WeaponReady;
Loop;
Deselect:
PSKN # 0
{
A_StartSound("weapons/shared/switch",32);
invoker.oldfov = 1;
A_ClearOverlays(-2,-2);
A_ZoomFactor(1,ZOOM_INSTANT|ZOOM_NOSCALETURNING);
}
PSKN # 1 A_SmoothLower;
goto Deselect+1;
Select:
PSKN A 1 A_SmoothRaise;
Loop;
Fire:
PSKN B 0
{
A_Stop();
A_StartSound("weapons/sports/football/vo", CHAN_VOICE, CHANF_OVERLAP);
vel.z = 4;
invoker.oldfov = 1;
}
PSKN BBBBB 1
{
A_WeaponOffset(random(-1,1),random(31,33),WOF_INTERPOLATE);
}
PSKN B 4
{
A_WeaponOffset(0,32,WOF_INTERPOLATE);
// Yo, you wanna see a disgusting hack? Check out CheckFrozen() in
// the player class! >
This hack lets us fuck with player input
// (ie. disable movement buttons, scale down turning) in the name of
// both weapon balance and not bugging out when the nightmare fuck
// storm we lovingly call the Flipper reel is enabled.
A_GiveInventory("Splot_ChargeToken", 1);
}
TNT1 A 0
{
A_Overlay(-2,"Overlay.Rush");
A_OverlayOffset(-2,0,-32);
A_OverlayFlags(-2,PSPF_ALPHA|PSPF_RENDERSTYLE,1);
A_OverlayRenderstyle(-2,STYLE_Add);
A_OverlayAlpha(-2,0);
invoker.dashalpha = 0;
}
goto Charge;
Charge:
PSKN C 1
{
// Figure out how much to propel the player based on
// the reel effect and speed powerups
// Only scale if the player's reel effect speed is greater
// than their default speed, so we can give the player a chance
// during Mini Mode
// We can change this if we ever add a slowdown reel for some
// horrible reason
if (self.speed > self.default.speed)
{
invoker.propelfactor = self.speed / self.default.speed;
}
else
{
invoker.propelfactor = 1;
}
// If we have a speed powerup, factor it in
let speedpowerup = self.FindInventory("PowerSpeed");
if (speedpowerup)
{
invoker.propelfactor *= speedpowerup.speed**1.125;
}
// Damage multiplier starts at 1 and scales up based on
// how the player's speed has been modified
invoker.damagemulti = max(1,invoker.propelfactor);
// Boost the damage multiplier based on actual speed.
// This factors in things that increase the player's speed
// beyond direct modification through reels and powerups.
// Specifically, this deals with friction.
// Under the Grease reel, the Football will get DANGEROUS.
invoker.playerrealvel = sqrt(vel.x**2 + vel.y**2);
invoker.damagemulti *= max(1,invoker.playerrealvel/(32.*invoker.damagemulti));
invoker.additionalradius = max(0,invoker.playerrealvel-32);
// Make the rush effect slowly fade in if we're going absurdly fast
if (invoker.damagemulti >= invoker.propelfactor*2.25)
{
invoker.dashalpha = min(0.5,(invoker.damagemulti-(invoker.propelfactor*2.25))/9.);
invoker.additionalhitscan = (invoker.damagemulti-(invoker.propelfactor*2.25))*24;
}
else if (invoker.dashalpha > 0)
{
invoker.dashalpha = 0;
invoker.additionalhitscan = 0;
}
A_OverlayAlpha(-2,invoker.dashalpha);
// Set the FOV while we have the original damage multiplier
invoker.newfov = max(0.55,((invoker.oldfov*13)+(0.85/(invoker.damagemulti**0.25)))/14.);
invoker.oldfov = invoker.newfov;
A_ZoomFactor(invoker.newfov,ZOOM_NOSCALETURNING);
// And then lastly, make the damage exponential!
invoker.damagemulti = invoker.damagemulti**1.8;
// Even more damage if we're out of control! AAAAAAAAAAAA
if (invoker.dashalpha > 0)
{
invoker.damagemulti = invoker.damagemulti**1.5;
}
// Propel the player forward, but propel them slower when in the air
// to avoid them suddenly flinging forward at TFC Conc Medic speeds.
if (pos.z == floorz || (pos.z != floorz && self.bFLY))
{
A_Recoil(-3*invoker.propelfactor);
} else {
A_Recoil(-1);
}
// User a ThinkerIterator to decide who's within range to hit
ThinkerIterator it = ThinkerIterator.Create("Actor");
Actor mo;
while ( (mo = Actor(it.Next ())) )
{
if (!mo.bSHOOTABLE || !self.CheckSight(mo) || mo.health <= 0 || mo == self || (mo.bBLASTED && mo.vel.z != 0))
{ // Must be shootable, viewable, alive, not the player, and not in the air from being blasted
continue;
}
if (self.Distance2D(mo) > self.radius*5.4 + mo.radius + invoker.additionalradius)
{ // Out of range
continue;
}
if (mo.pos.z > self.pos.z + self.height || mo.pos.z + mo.height < self.pos.z)
{ // Outside player's height range
continue;
}
// Now that we know a target is nearby, we can calculate angles and pitches.
double angletotarget = self.AngleTo(mo);
double anglediff = AbsAngle(self.angle,angletotarget);
double pitchtotarget = atan2((self.pos.z - mo.pos.z), sqrt(((self.pos.x-mo.pos.x)**2)+((self.pos.y-mo.pos.y)**2)));
// If it's a boss, invulnerable, or unmoveable, don't even bother
// unless it's a close direct hit.
// DONTBLAST is included in this so that they don't rack up a ton
// of hits at the same time from a glancing blow (they'll keep
// getting hit over and over since they're not being blasted)
if ((mo.bBOSS || mo.bNODAMAGE || mo.bDONTBLAST || (!mo.bISMONSTER && !mo.bCANBLAST)) && (anglediff > 20 || self.Distance2D(mo) > self.radius*3.4 + mo.radius + invoker.additionalradius))
{
continue;
}
if (anglediff > 40)
{ // outside a spread of 80 degrees around the player's angle
continue;
}
double angletoshoot = DeltaAngle(self.angle,angletotarget);
double pitchtoshoot = DeltaAngle(self.pitch,pitchtotarget);
A_FireBullets (angletoshoot, pitchtotarget, -1, (random(6,12)*invoker.damagemulti), "Splot_ChargePuff", FBF_NOPITCH|FBF_NORANDOM|FBF_NOFLASH|FBF_EXPLICITANGLE, self.radius*5.4 + mo.radius + invoker.additionalradius, null);
invoker.tackletime = true;
// If we made it this far and it's one of those special classes,
// end immediately.
if (mo.bBOSS || mo.bNODAMAGE || mo.bDONTBLAST || (!mo.bISMONSTER && !mo.bCANBLAST))
{
return A_Jump(256,"End");
}
else
{
// With those out of the way... it's blasting time.
Splot_BlastActor(mo,255,10*(invoker.damagemulti**(1./1.75)),"Splot_LandingFX", false);
}
// Stop after the blast if the monster is HUGE
if (mo.mass >= 1500)
{
return A_Jump(256,"End");
}
}
invoker.steppy++;
A_WeaponOffset(random(-2,2),random(32,34),WOF_INTERPOLATE);
// Every four tics, play a footstep sound if we're on the ground.
if (invoker.steppy >= 4 && pos.z == floorz)
{
A_StartSound("weapons/sports/football/chargestep");
A_Quake (6,2,0,96,0);
invoker.steppy = 0;
}
// Our new hack!
// See if the previous projectile returned false.
// If so, end things!
if (invoker.crashDetector)
{
invoker.crashDetector = false;
return A_Jump(256,"End");
}
// If we continue... fire another probe!
actor hackyProbe = Spawn("Splot_Football_HackyProjectile",pos);
hackyProbe.speed = max(16,sqrt(self.vel.x**2 + self.vel.y**2));
hackyProbe.vel.x = cos(angle)*hackyProbe.speed;
hackyProbe.vel.y = sin(angle)*hackyProbe.speed;
hackyProbe.target = self;
hackyProbe.master = invoker;
// We'll also use a foot-based LineTrace to detect slopes
// Massive thanks to Kodi and Phantombeta for teaching me how to do
// this linetrace stuff!
FLineTraceData SurfaceData; // For checking your feet.
LineTrace(angle, self.radius*2, 0, TRF_BLOCKSELF|TRF_SOLIDACTORS, offsetz: self.MaxStepHeight, data:SurfaceData);
// only count floors if the slope is steeper than 45 degrees (ie., height is greater than length)
if (SurfaceData.HitType == SurfaceData.TRACE_HitFloor && self.MaxStepHeight > SurfaceData.Distance)
return A_Jump(256,"End");
// If we're in minimode, check for real-tiny wall bits. You know, like stairs.
if (CountInv("Slot1_MiniMode_Token"))
{
LineTrace(angle, self.radius*2, 0, TRF_BLOCKSELF|TRF_SOLIDACTORS, offsetz: self.MaxStepHeight, data:SurfaceData);
if (SurfaceData.HitType == SurfaceData.TRACE_HitWall)
return A_Jump(256,"End");
}
return A_Jump(0,null); // This keeps the compiler from shanking me.
// Man, this weapon has a lot of comments.
}
loop;
End:
PSLM A 1
{
// Re-enable controls.
A_TakeInventory("Splot_ChargeToken", 1);
// Fire off a final attack for good measure to bust up any
// geometry that we hit. Based on testing, these will probably
// never hit a normal monster (apart from bosses), but if it does,
// honestly, it'll feel cool.
actor lateHit = Spawn("Splot_Football_LateHit",pos);
lateHit.speed = max(16,sqrt(self.vel.x**2 + self.vel.y**2));
lateHit.vel.x = cos(angle)*lateHit.speed;
lateHit.vel.y = sin(angle)*lateHit.speed;
lateHit.target = self;
lateHit.master = invoker;
A_ClearOverlays(-2,-2);
invoker.dashalpha = 0;
invoker.additionalhitscan = 0;
invoker.oldfov = 1;
A_ZoomFactor(1,ZOOM_INSTANT|ZOOM_NOSCALETURNING);
A_WeaponOffset(0,32);
A_StartSound("weapons/sports/football/chargeslam",44);
A_Quake (6,8,0,96,0);
A_Stop();
A_Recoil(4);
vel.z = 8;
}
PSLM ACEGIKMOQ 1;
TNT1 A 7 A_WeaponOffset(0,90);
// If we switched weapons while moving (eg., picked one up),
// let the player switch here
TNT1 A 1 A_WeaponReady(WRF_NOBOB|WRF_NOFIRE);
// Check if we should play Madden sound
// Either way, reset the tackle stat
PSKN A 0
{
if (invoker.tackletime && random(0,4) == 0)
{
A_StartSound("weapons/sports/football/maddenend",CHAN_WEAPON);
}
invoker.tackletime = false;
}
PSKN A 1 A_WeaponOffset(0,80,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,70,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,60,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,55,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,49,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,44,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,39,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,35,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,33,WOF_INTERPOLATE);
PSKN A 1 A_WeaponOffset(0,32,WOF_INTERPOLATE);
goto Ready;
Overlay.Rush:
RUSH AB 2;
Loop;
Spawn:
PSKN Z -1;
Stop;
}
// Copypasted from the repulsor powerup, and altered to not effect players or missiles.
// Graceful? Fuck no. Works better than the previous method? Yeop.
action private void Splot_BlastActor (Actor victim, double strength, double speed, Class<Actor> blasteffect, bool dontdamage)
{
if (!victim.SpecialBlastHandling (self, strength))
{
return;
}
double ang = AngleTo(victim);
Vector2 move;
if (victim.bBOSS || victim.bNODAMAGE || victim.bDONTBLAST || (!victim.bISMONSTER && !victim.bCANBLAST))
{
return;
}
else
{
move = AngleToVector(ang, speed);
}
victim.Vel.XY = move;
// Spawn blast puff
ang -= 180.;
Vector3 spawnpos = victim.Vec3Offset(
(victim.radius + 1) * cos(ang),
(victim.radius + 1) * sin(ang),
(victim.Height / 2) - victim.Floorclip);
Actor mo = Spawn (blasteffect, spawnpos, ALLOW_REPLACE);
if (mo)
{
mo.Vel.XY = victim.Vel.XY;
}
if (!victim.bBLASTED || (victim.bBLASTED && victim.vel.z != 0))
{
victim.A_StartSound("weapons/sports/football/tackle", CHAN_AUTO, 0, 1.0, ATTN_NORM, frandom(1.0, 1.25));
}
victim.Vel.Z = random(8.,20.) + ( 30.*speed / victim.Mass );
if (victim.player)
{
// Players handled automatically
}
else if (!dontdamage)
{
victim.bBlasted = true;
}
if (victim.bTouchy)
{ // Touchy objects die when blasted
victim.bArmed = false; // Disarm
victim.DamageMobj(self, self, victim.health, 'ChargeSlam', DMG_FORCED|DMG_EXPLOSION);
}
}
}
// Hack borrowed from Mjolnir
// As we get better at learning ZScript, we can go back and fix
// our previous attempts, which are truly the lost and the damned.
class Splot_Football_HackyProjectile : Actor
{
Default
{
Radius 16;
Height 56;
Speed 8;
+NOBLOCKMAP
+NOGRAVITY
+SKYEXPLODE
+NOEXPLODEFLOOR
+STEPMISSILE
+DONTSPLASH
+BLOCKASPLAYER
Projectile;
}
int realRadius;
int realHeight;
States
{
Spawn:
TNT1 A 1;
Loop;
Crash:
Death:
TNT1 A 0
{
bNOINTERACTION = true;
FailState();
}
TNT1 A 3; // hang here as a failsafe
Stop;
XDeath:
TNT1 A 3; // hang here as a failsafe
Stop;
}
override void PostBeginPlay()
{
Super.PostBeginPlay();
if (target)
{
realRadius = target.radius;
realHeight = target.height;
if (realHeight != height || realRadius != radius)
{
if (A_SetSize(realRadius,realHeight,1))
A_SetSize(realRadius,realHeight,0);
else
FailState();
}
}
else
FailState();
}
// Get all our movements out at once to simulate a hitscan
override void Tick()
{
for (int i = 0; i < 2; i++)
{
Super.Tick();
}
self.destroy(); // SUCCESS STATE. Return nothing
}
override int SpecialMissileHit(Actor victim)
{
if (target && victim == target) // Don't hit the player
{
return 1;
}
// Treat only solid, non-shootable actors as obstacles
if (!victim.bSHOOTABLE && victim.bSOLID)
{
return 0;
}
// Otherwise, go ahead
return 1;
}
void FailState()
{
if (master)
{
let footbaww = Splot_Football(master);
footbaww.crashDetector = true;
}
self.destroy();
}
}
class Splot_Football_LateHit : Actor
{
Default
{
Radius 16;
Height 56;
Speed 8;
DamageFunction GetSackDamage();
+NOBLOCKMAP
+NOGRAVITY
+SKYEXPLODE
+NOEXPLODEFLOOR
+STEPMISSILE
+DONTSPLASH
Projectile;
}
int realRadius;
int realHeight;
States
{
Spawn:
TNT1 A 1;
Loop;
Crash:
Death:
XDeath:
TNT1 A 2; // wait for cleanup
Stop;
}
override void PostBeginPlay()
{
Super.PostBeginPlay();
if (target)
{
realRadius = target.radius;
realHeight = target.height;
if (realHeight != height || realRadius != radius)
{
if (A_SetSize(realRadius,realHeight,1))
A_SetSize(realRadius,realHeight,0);
}
}
}
// Get all our movements out at once to simulate a hitscan
override void Tick()
{
for (int i = 0; i < 2; i++)
{
Super.Tick();
}
self.destroy();
}
int GetSackDamage()
{
int sackMultiplier = 1;
if (master)
{
let master = Splot_Football(master);
sackMultiplier *= master.damagemulti;
}
return 100*sackMultiplier;
}
}
class Splot_ChargeToken : Splot_WeaponToken
{
override void AttachToOwner (actor owner)
{
let player = owner.player;
player.cheats = player.cheats + CF_TOTALLYFROZEN;
Super.AttachToOwner(owner);
}
override void DetachFromOwner()
{
let player = owner.player;
player.cheats = player.cheats - CF_TOTALLYFROZEN;
Super.DetachFromOwner();
}
}
class Splot_ChargePuff : Actor
{
Default
{
+NOBLOCKMAP
+NOGRAVITY
+PUFFONACTORS
+RANDOMIZE
+ZDOOMTRANS
+NOEXTREMEDEATH
RenderStyle "Translucent";
Alpha 0.5;
VSpeed 1;
Mass 5;
damagetype "ChargeSlam";
}
States
{
Spawn:
PUFF A 0;
PUFF A 0;
PUFF A 4 Bright;
PUFF BCD 4;
Stop;
}
}