//============================================================================= // KFWeap_Blunt_Pulverizer //============================================================================= // A sledgehammer combined with a makeshift shotgun //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC //============================================================================= class KFWeap_Blunt_Pulverizer extends KFWeap_MeleeBase; const ShootAnim_L = 'HardFire_L'; const ShootAnim_R = 'HardFire_R'; const ShootAnim_F = 'HardFire_F'; const ShootAnim_B = 'HardFire_B'; var bool bWasTimeDilated; /** Explosion actor class to spawn */ var class ExplosionActorClass; var() KFGameExplosion ExplosionTemplate; /** If true, use an alternate set of explosion effects */ var bool bAltExploEffects; var KFImpactEffectInfo AltExploEffects; var transient Actor BlastAttachee; /** Spawn location offset to improve cone hit detection */ var transient float BlastSpawnOffset; /** If set, heavy attack button has been released during the attack */ var transient bool bPulverizerFireReleased; var bool bFriendlyFireEnabled; var class NukeExplosionActorClass; var float StartingDamageRadius; replication { if (bNetInitial) bFriendlyFireEnabled; } simulated event PreBeginPlay() { Super.PreBeginPlay(); /** Initially check whether friendly fire is on or not. */ if(Role == ROLE_Authority && KFGameInfo(WorldInfo.Game).FriendlyFireScale != 0.f) { bFriendlyFireEnabled = true; } if (ExplosionTemplate != none) { StartingDamageRadius = ExplosionTemplate.DamageRadius; } } /** Healing charge doesn't count as ammo for purposes of inventory management (e.g. switching) */ simulated function bool HasAnyAmmo() { // Special ammo is stored in the default firemode (heal darts are separate) if (HasSpareAmmo() || AmmoCount[DEFAULT_FIREMODE] >= AmmoCost[CUSTOM_FIREMODE]) { return true; } return false; } simulated event bool HasAmmo(byte FireModeNum, optional int Amount) { // Default fire mode either has ammo to trigger the heal or needs to return true to still allow a basic swing if (FireModeNum == DEFAULT_FIREMODE) { return true; } return super.HasAmmo(FireModeNum, Amount); } /** Pulverizer should be able to interrupt its reload state with any melee attack */ simulated function bool CanOverrideMagReload(byte FireModeNum) { return FireModeNum != RELOAD_FIREMODE; } /** Explosion Actor version */ simulated function CustomFire() { local KFExplosionActor ExploActor; local vector SpawnLoc; local rotator SpawnRot; if ( Instigator.Role < ROLE_Authority ) { return; } // On local player or server, we cache off our time dilation setting here if (WorldInfo.NetMode == NM_ListenServer || WorldInfo.NetMode == NM_DedicatedServer || Instigator.Controller != None) { bWasTimeDilated = WorldInfo.TimeDilation < 1.f; } PrepareExplosionTemplate(); SetExplosionActorClass(); SpawnLoc = Instigator.GetWeaponStartTraceLocation(); SpawnRot = GetPulverizerAim(SpawnLoc); // nudge backwards to give a wider code near the player SpawnLoc += vector(SpawnRot) * BlastSpawnOffset; // explode using the given template ExploActor = Spawn(ExplosionActorClass, self,, SpawnLoc, SpawnRot,, true); if (ExploActor != None) { ExploActor.InstigatorController = Instigator.Controller; ExploActor.Instigator = Instigator; // Force the actor we collided with to get hit again (when DirectionalExplosionAngleDeg is small) // This is only necessary on server since GetEffectCheckRadius() will be zero on client ExploActor.Attachee = BlastAttachee; ExplosionTemplate.bFullDamageToAttachee = true; // enable muzzle location sync ExploActor.bReplicateInstigator = true; ExploActor.SetSyncToMuzzleLocation(true); ExploActor.Explode(ExplosionTemplate, vector(SpawnRot)); } // tell remote clients that we fired, to trigger effects in third person IncrementFlashCount(); if ( bDebug ) { DrawDebugCone(SpawnLoc, vector(SpawnRot), ExplosionTemplate.DamageRadius, ExplosionTemplate.DirectionalExplosionAngleDeg * DegToRad, ExplosionTemplate.DirectionalExplosionAngleDeg * DegToRad, 16, MakeColor(64,64,255,0), TRUE); } } // for nukes && concussive force simulated protected function PrepareExplosionTemplate() { local KFPlayerReplicationInfo InstigatorPRI; local KFPlayerController KFPC; local KFPerk InstigatorPerk; if (bWasTimeDilated) { InstigatorPRI = KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo); if (InstigatorPRI != none) { if (InstigatorPRI.bNukeActive) { ExplosionTemplate = class'KFPerk_Demolitionist'.static.GetNukeExplosionTemplate(); ExplosionTemplate.Damage = ExplosionTemplate.Damage * class'KFPerk_Demolitionist'.static.GetNukeDamageModifier(); ExplosionTemplate.DamageRadius = ExplosionTemplate.DamageRadius * class'KFPerk_Demolitionist'.static.GetNukeRadiusModifier(); ExplosionTemplate.DamageFalloffExponent = ExplosionTemplate.DamageFalloffExponent; } else if (InstigatorPRI.bConcussiveActive && AltExploEffects != none) { ExplosionTemplate.ExplosionEffects = AltExploEffects; ExplosionTemplate.ExplosionSound = class'KFPerk_Demolitionist'.static.GetConcussiveExplosionSound(); } } } else { // When copying the default, we are assigning a reference that can change the default // So since we are changing the DamageRadius, save that default off and re-apply it here ExplosionTemplate = default.ExplosionTemplate; ExplosionTemplate.DamageRadius = StartingDamageRadius; } // Change the radius and damage based on the perk if (Owner.Role == ROLE_Authority) { KFPC = KFPlayerController(Instigator.Controller); if (KFPC != none) { InstigatorPerk = KFPC.GetPerk(); ExplosionTemplate.DamageRadius *= InstigatorPerk.GetAoERadiusModifier(); } } } simulated protected function SetExplosionActorClass() { local KFPlayerReplicationInfo InstigatorPRI; if (bWasTimeDilated && Instigator != none) { InstigatorPRI = KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo); if (InstigatorPRI != none) { if (InstigatorPRI.bNukeActive) { ExplosionActorClass = NukeExplosionActorClass; return; } } } ExplosionActorClass = default.ExplosionActorClass; } /** Called by CustomFire when shotgun blast is fired */ simulated function Rotator GetPulverizerAim( vector StartFireLoc ) { local Rotator R; R = GetAdjustedAim(StartFireLoc); // Adjust cone fire angle based on swing direction switch (MeleeAttackHelper.CurrentAttackDir) { case DIR_Left: R.Yaw += 5461; break; case DIR_Right: R.Yaw -= 5461; break; case DIR_Forward: R.Pitch -= 2048; break; case DIR_Backward: R.Pitch += 2048; break; } return R; } /** Don't play a shoot anim when FireAmmunition is called */ simulated function name GetWeaponFireAnim(byte FireModeNum) { // Adjust cone fire angle based on swing direction switch (MeleeAttackHelper.CurrentAttackDir) { case DIR_Forward: case DIR_ForwardLeft: case DIR_ForwardRight: return ShootAnim_F; case DIR_Backward: case DIR_BackwardLeft: case DIR_BackwardRight: return ShootAnim_B; case DIR_Left: return ShootAnim_L; case DIR_Right: return ShootAnim_R; } return ''; } /** Called on the server alongside PulverizerFired */ reliable server private function ServerBeginPulverizerFire(Actor HitActor, optional vector HitLocation) { // Ignore if too far away (something went wrong!) if ( VSizeSq2D(HitLocation - Instigator.Location) > Square(500) ) { `log("ServerBeginPulverizerFire outside of range!"); return; } BlastAttachee = HitActor; SendToFiringState(CUSTOM_FIREMODE); } /** Called when altfire melee attack hits a target and there is ammo left */ simulated function BeginPulverizerFire() { SendToFiringState(CUSTOM_FIREMODE); } /** Skip calling StillFiring/PendingFire to fix log warning */ simulated function bool ShouldRefire() { if ( CurrentFireMode == CUSTOM_FIREMODE ) return false; return Super.ShouldRefire(); } /** Override to allow for two different states associated with RELOAD_FIREMODE */ simulated function SendToFiringState(byte FireModeNum) { // Ammo needs to be synchronized on client/server for this to work! if ( FireModeNum == RELOAD_FIREMODE && !Super(KFWeapon).CanReload() ) { SetCurrentFireMode(FireModeNum); GotoState('WeaponUpkeep'); return; } Super.SendToFiringState(FireModeNum); } /** Always allow reload and choose the correct state in SendToFiringState() */ simulated function bool CanReload(optional byte FireModeNum) { return true; } /** Debugging */ `if(`notdefined(ShippingPC)) exec function ToggleWeaponDebug() { bDebug = !bDebug; } `endif /********************************************************************************************* * State MeleeHeavyAttacking * This is the alt-fire Melee State. *********************************************************************************************/ simulated state MeleeHeavyAttacking { /** Reset bPulverizerFireReleased */ simulated event BeginState(Name PreviousStateName) { Super.BeginState(PreviousStateName); bPulverizerFireReleased = false; } /** Set bPulverizerFireReleased to ignore NotifyMeleeCollision */ simulated function StopFire(byte FireModeNum) { Super.StopFire(FireModeNum); bPulverizerFireReleased = true; } /** Network: Local Player */ simulated function NotifyMeleeCollision(Actor HitActor, optional vector HitLocation) { local KFPawn Victim; // If fire button is being held down, try firing pulverizer if ( Instigator != None && Instigator.IsLocallyControlled() /*&& !bPulverizerFireReleased*/ ) { // only detonate when the pulverizer hits a pawn so that level geometry doesn't get in the way if ( HitActor.bWorldGeometry ) { return; } Victim = KFPawn(HitActor); if ( Victim == None || (!bFriendlyFireEnabled && Victim.GetTeamNum() == Instigator.GetTeamNum()) || (Victim.bPlayedDeath && `TimeSince(Victim.TimeOfDeath) > 0.f) ) { return; } if ( AmmoCount[0] >= AmmoCost[CUSTOM_FIREMODE] && !IsTimerActive(nameof(BeginPulverizerFire)) ) { BlastAttachee = HitActor; // need to delay one frame, since this is called from AnimNotify SetTimer(0.001f, false, nameof(BeginPulverizerFire)); if ( Role < ROLE_Authority ) { if( HitActor.bTearOff && Victim != none ) { Victim.TakeRadiusDamage(Instigator.Controller, ExplosionTemplate.Damage, ExplosionTemplate.DamageRadius, ExplosionTemplate.MyDamageType, ExplosionTemplate.MomentumTransferScale, Location, true, (Owner != None) ? Owner : self); } else { ServerBeginPulverizerFire(HitActor, HitLocation); } } } } } } simulated state Active { /** * Called from Weapon:Active.BeginState when HasAnyAmmo (which is overridden above) returns false. */ simulated function WeaponEmpty() { local int i; // Copied from Weapon:Active.BeginState where HasAnyAmmo returns true. // Basically, pretend the weapon isn't empty in this case. for (i=0; i