//============================================================================= // KFPawn_ZedHansBase //============================================================================= // Base Hans Class. Put here so classes in KFGame could reference // Hans properties, and if any native code is needed //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // John "Ramm-Jaeger" Gibson //============================================================================= class KFPawn_ZedHansBase extends KFPawn_MonsterBoss abstract; /** Cached AI controller */ var KFAIController_Hans MyHansController; /** If true, MkB42s are unholstered and aiming (also see bIsTargeting) */ var repnotify bool bGunsEquipped; /********************************************************************************************* * Battle phases **********************************************************************************************/ /** Info for hans battle phases */ struct HansBattlePhaseInfo { /** Whether or not we can do the Frenzy attack this battle phase */ var bool bCanFrenzy; /** Whether or not to desire sprinting behavior this battle phase */ var bool bSprintingBehavior; /** Info on how often we can toss any grenades in this battle phase */ var float GlobalOffensiveNadePhaseCooldown; /** Whether or not we can use a regular nerve gas toss this battle phase */ var bool bCanTossNerveGas; /** Info on how often we can toss Nerve Gas Nades in this battle phase */ var float NerveGasTossPhaseCooldown; /** Whether or not we can use a nerve gas barrage this battle phase */ var bool bCanBarrageNerveGas; /** Info on how often we can barrage Nerve Gas Nades in this battle phase */ var float NerveGasBarragePhaseCooldown; /** Whether or not we can use the MKB assault rifle this battle phase */ var bool bCanUseGuns; /** Info on how often we can use guns in this battle phase */ var float GunAttackPhaseCooldown; /** How long Hans will keep his guns out when he pulls them out in this battle phase */ var float GunAttackLengthPhase; /** Whether or not we can use a regular grenade this battle phase */ var bool bCanTossGrenade; /** Info on how often we can toss HENades in this battle phase */ var float HENadeTossPhaseCooldown; /** Whether or not we can use grenade barrage this battle phase */ var bool bCanBarrageGrenades; /** Info on how often we can barrage HENades in this battle phase */ var float HENadeBarragePhaseCooldown; /** Whether Hans can move when throwing grenades, per difficulty level */ var array bCanMoveWhileThrowingGrenades; /** Heal threshold (MaxHealth * HealThreshold) per difficulty level */ var array HealThresholds; /** Heal amount Health + MaxHealth*(MaxHealth * HealAmount) per difficulty level */ var array HealAmounts; /** Shield health per difficulty level */ var array MaxShieldHealth; structdefaultproperties { bSprintingBehavior=true GlobalOffensiveNadePhaseCooldown=8 NerveGasTossPhaseCooldown=10 NerveGasBarragePhaseCooldown=20 GunAttackPhaseCooldown=20 GunAttackLengthPhase=15 HENadeTossPhaseCooldown=10 HENadeBarragePhaseCooldown=20 } }; /** Configuration for the Hans battle phases */ var array BattlePhases; /** Whether or not we've healed this battle phase */ var bool bHealedThisPhase; /** Amount of health healed this phase (cumulative) */ var float AmountHealedThisPhase; /** In the mode to hunt players and try and heal */ var repnotify bool bInHuntAndHealMode; /** How much to reduce Hans' damage he receives when he is in hunt and heal mode */ var() float HuntAndHealModeDamageReduction; /** How much to scale incap powers by when healing */ var float IncapPowerScaleWhenHealing; /** Amount of health shield has remaining */ var float ShieldHealth; var float ShieldHealthMax; var float ShieldHealthScale; /** Replicated shield health percentage */ var repnotify byte ShieldHealthPctByte; /** Number of times we've bumped into enemies when trying to heal */ var int NumHuntAndHealEnemyBumps; /** Last time we bumped into an enemy */ var float LastHuntAndHealEnemyBumpTime; /********************************************************************************************* * Grenade Throwing **********************************************************************************************/ /** Grenade tossing */ var class ActiveGrenadeClass; /** Grenade tossing */ var class ExplosiveGrenadeClass; var const class SeasonalExplosiveGrenadeClasses[5]; /** Grenade tossing */ var class NerveGasGrenadeClass; var const class SeasonalNerveGasGrenadeClasses[5]; /** Grenade tossing */ var class SmokeGrenadeClass; var const class SeasonalSmokeGrenadeClasses[5]; /** Socket where grenade spawns when Hans throws with his right hand */ var name RightHandSocketName; /** Socket where grenade spawns when Hans throws with his left hand */ var name LeftHandSocketName; /** Grenade accuracy TODO: Make this based on skill level for AI*/ var vector GrenadeTossSpread; /** Waiting to do a smoke grenade barrage as soon as the guns are put away */ var bool bPendingSmokeGrenadeBarrage; /** How many grenades have been tossed this barrage */ var int BarrageTossCount; /** The current grenade attack is a barrage */ var bool bDoingBarrage; /** Info on how often we can tossed any damaging grenades */ var float GlobalOffensiveNadeCooldown; var float LastOffensiveNadeTime; /** Info on how often we can toss HENades */ var float HENadeTossCooldown; var float LastHENadeTossTime; /** Info on how often we can barrage HENades */ var float HENadeBarrageCooldown; var float LastHENadeBarrageTime; /** Info on how often we can toss Nerve Gas Nades */ var float NerveGasTossCooldown; var float LastNerveGasTossTime; /** Info on how often we can barrage Nerve Gas Nades */ var float NerveGasBarrageCooldown; var float LastNerveGasBarrageTime; /** Info on how often we can toss Smoke Nades */ var float SmokeTossCooldown; var float LastSmokeTossTime; replication { // Replicated to ALL if ( bNetDirty ) bGunsEquipped, bInHuntAndHealMode, ShieldHealthPctByte; } simulated event ReplicatedEvent( name VarName ) { switch( VarName ) { case nameof( bInHuntAndHealMode ): SetHuntAndHealMode( bInHuntAndHealMode ); break; case nameOf( ShieldHealthPctByte ): UpdateShieldColor(); break; default: super.ReplicatedEvent( VarName ); } } /** Called from Possessed event when this controller has taken control of a Pawn */ function PossessedBy( Controller C, bool bVehicleTransition ) { Super.PossessedBy( C, bVehicleTransition ); MyHansController = KFAIController_Hans( C ); SetPhaseCooldowns( 0 ); ExplosiveGrenadeClass = SeasonalExplosiveGrenadeClasses[class'KFGameEngine'.static.GetSeasonalEventID()]; NerveGasGrenadeClass = SeasonalNerveGasGrenadeClasses[class'KFGameEngine'.static.GetSeasonalEventID()]; SmokeGrenadeClass = SeasonalSmokeGrenadeClasses[class'KFGameEngine'.static.GetSeasonalEventID()]; } /** Increment Hans to the next battle phase */ function IncrementBattlePhase() { CurrentBattlePhase++; bHealedThisPhase = true; AmountHealedThisPhase = 0; SetPhaseCooldowns( CurrentBattlePhase - 1 ); bForceNetUpdate = true; } /** Set the correct phase based cooldown for this battle phase */ function SetPhaseCooldowns( int BattlePhase ) { GlobalOffensiveNadeCooldown = BattlePhases[BattlePhase].GlobalOffensiveNadePhaseCooldown; HENadeTossCooldown = BattlePhases[BattlePhase].HENadeTossPhaseCooldown; HENadeBarrageCooldown = BattlePhases[BattlePhase].HENadeBarragePhaseCooldown; NerveGasTossCooldown = BattlePhases[BattlePhase].NerveGasTossPhaseCooldown; NerveGasBarrageCooldown = BattlePhases[BattlePhase].NerveGasBarragePhaseCooldown; if( MyHansController != none ) { MyHansController.ShootingCooldown = BattlePhases[BattlePhase].GunAttackPhaseCooldown; MyHansController.MaxGunAttackLength = BattlePhases[BattlePhase].GunAttackLengthPhase; // Reset summon flag MyHansController.bSummonedThisPhase = false; } } /** Reduce damage when in hunt and heal mode */ function AdjustDamage(out int InDamage, out vector Momentum, Controller InstigatedBy, vector HitLocation, class DamageType, TraceHitInfo HitInfo, Actor DamageCauser) { Super.AdjustDamage(InDamage, Momentum, InstigatedBy, HitLocation, DamageType, HitInfo, DamageCauser); // Update shield health before scaling damage UpdateShieldHealth( InDamage ); // Reduce damage when in hunt and heal mode if( bInHuntAndHealMode && ShieldHealth > 0 ) { InDamage = Max( InDamage * HuntAndHealModeDamageReduction, 1 ); } } /** Reduce affliction/incap strength when healing */ simulated function AdjustAffliction( out float AfflictionPower ) { super.AdjustAffliction( AfflictionPower ); if( bInHuntAndHealMode ) { AfflictionPower *= IncapPowerScaleWhenHealing; } } /** Updates shield health and shield health percent */ function UpdateShieldHealth( optional int Damage=-1 ) { // If shield has been depleted already, or no damage was taken, don't update if( ShieldHealth == 0 || Damage == 0 ) { return; } // Damage the shield if applicable if( ShieldHealth > 0 && Damage > 0 ) { ShieldHealth = Max( ShieldHealth - Damage, 0 ); } // Shield health depleted, break shield if( ShieldHealth == 0 ) { ShieldHealthPctByte = 0; } else { ShieldHealthPctByte = FloatToByte( fClamp(ShieldHealth / ShieldHealthMax, 0.f, 1.f) ); } // Update color based on percentage of health remaining UpdateShieldColor(); // Need to ensure shield gets broken on dedicated servers if( WorldInfo.NetMode == NM_DedicatedServer && bInHuntAndHealMode && ShieldHealthPctByte == 0 ) { BreakShield(); } } /** Breaks the shield */ simulated function BreakShield() { if( Role == ROLE_Authority ) { // Cancel any flee in progress if( MyHansController != none ) { MyHansController.CancelFlee(); } // Cancel hunt and heal mode SetHuntAndHealMode( false ); // Cancel any active special moves if( IsDoingSpecialMove() ) { EndSpecialMove(); } // Knock down Knockdown( Velocity != vect(0,0,0) ? -Velocity*2 : 3*(-vector(Rotation) * GroundSpeed), vect(1,1,1),,,,, Location ); } } /** Can we frenzy attack in this phase */ function bool CanFrenzyInThisPhase() { return BattlePhases[CurrentBattlePhase-1].bCanFrenzy; } /** If true Hans will favor sprinting in this phase. Even if it's false he may sprint under certain circumstances, but when it's true he'll try and sprint almost all the time */ function bool DesireSprintingInThisPhase() { return BattlePhases[CurrentBattlePhase-1].bSprintingBehavior; } /** Called by PawnAnimInfo when determining whether an attack with bSpecializedMode TRUE can be performed */ function bool ShouldPlaySpecialMeleeAnims() { // make all of Han's melee attacks available when he can frenzy return CanFrenzyInThisPhase(); } /** Only allow blocking when in melee */ function bool CanBlock() { return !bGunsEquipped && !bInHuntAndHealMode && super.CanBlock(); } /** Has the offensive grenade cooldown time elapsed */ function bool OffensiveGrenadeCooldownComplete() { if( LastOffensiveNadeTime == 0 || `TimeSince(LastOffensiveNadeTime) > GlobalOffensiveNadeCooldown ) { return true; } return false; } /** Can we toss nerve gas in this phase */ function bool CanTossNerveGasInThisPhase() { if( BattlePhases[CurrentBattlePhase -1].bCanTossNerveGas && !bInHuntAndHealMode && `TimeSince(LastNerveGasTossTime) > 0 && `TimeSince(LastNerveGasTossTime) > NerveGasTossCooldown && OffensiveGrenadeCooldownComplete() ) { return true; } return false; } /** Can we barrage nerve gas in this phase */ function bool CanBarrageNerveGasInThisPhase() { if( BattlePhases[CurrentBattlePhase -1].bCanBarrageNerveGas && !bInHuntAndHealMode && `TimeSince(LastNerveGasBarrageTime) > 0 && `TimeSince(LastNerveGasBarrageTime) > NerveGasBarrageCooldown && OffensiveGrenadeCooldownComplete() ) { return true; } return false; } /** Can we use guns in this phase */ function bool CanUseGunsInThisPhase() { return BattlePhases[CurrentBattlePhase -1].bCanUseGuns; } /** Can we do a standard grenade toss in this phase */ function bool CanTossGrenadeInThisPhase() { if( BattlePhases[CurrentBattlePhase -1].bCanTossGrenade && !bInHuntAndHealMode && `TimeSince(LastHENadeTossTime) > 0 && `TimeSince(LastHENadeTossTime) > HENadeTossCooldown && OffensiveGrenadeCooldownComplete() ) { return true; } return false; } /** Can we do a grenade barrage in this phase */ function bool CanGrenadeBarrageInThisPhase() { if( BattlePhases[CurrentBattlePhase -1].bCanBarrageGrenades && !bInHuntAndHealMode && `TimeSince(LastHENadeBarrageTime) > 0 && `TimeSince(LastHENadeBarrageTime) > HENadeBarrageCooldown && OffensiveGrenadeCooldownComplete() ) { return true; } return false; } /** Can we do a regular smoke toss in this phase */ function bool CanSmokeTossInThisPhase() { return bInHuntAndHealMode && `TimeSince(LastSmokeTossTime) > 0 && `TimeSince(LastSmokeTossTime) > SmokeTossCooldown; } /** Return true if busy throwing grenade(s) or using AICommand_ThrowGrenade */ function bool IsThrowingGrenade() { return false; } /** Implemented in subclass, put here to be accessible from KFGame.u */ function bool CacheGrenadeThrowLocation( optional bool bLeftHand ) { return false; } function SetActiveGrenadeClassGrenade() { ActiveGrenadeClass=ExplosiveGrenadeClass; } function SetActiveGrenadeClassSmoke() { ActiveGrenadeClass=SmokeGrenadeClass; } function SetActiveGrenadeClassNerveGas() { ActiveGrenadeClass=NerveGasGrenadeClass; } /** Returns TRUE if all conditions are met to allow movement while throwing grenades */ simulated function bool CanMoveWhileThrowingGrenades() { local KFGameReplicationInfo KFGRI; // See if this battle phase allows it KFGRI = KFGameReplicationInfo( WorldInfo.GRI ); if( KFGRI != none && BattlePhases[CurrentBattlePhase-1].bCanMoveWhileThrowingGrenades[KFGRI.GetModifiedGameDifficulty()] ) { return true; } return bInHuntAndHealMode || LocalIsOnePlayerLeftInTeamGame(); } function PlayDrawGunsDialog() { `DialogManager.PlayHansDrawGunsDialog( self ); } function PlayGrenadeDialog( bool bBarrage ) { switch( ActiveGrenadeClass ) { case ExplosiveGrenadeClass: `DialogManager.PlayHansNadeDialog( self, bBarrage ); break; case SmokeGrenadeClass: `DialogManager.PlayHansSmokeDialog( self, false ); break; case NerveGasGrenadeClass: `DialogManager.PlayHansGasDialog( self, bBarrage ); break; }; } /** Sets variables related to "hunt and heal" mode */ simulated function SetHuntAndHealMode( bool bOn ) { local KFGameInfo KFGI; local float HealthMod; local float HeadHealthMod; bInHuntAndHealMode = bOn; if( Role == ROLE_Authority ) { if( bInHuntAndHealMode ) { SetTimer(0.25f, true, nameof(HuntAndHealBump)); // Use health/player count to scale shield health HealthMod = 1.f; KFGI = KFGameInfo( WorldInfo.Game ); if( KFGI != none ) { KFGI.DifficultyInfo.GetAIHealthModifier( self, KFGI.GetModifiedGameDifficulty(), KFGI.GetLivingPlayerCount(), HealthMod, HeadHealthMod ); } // Initialize shield health ShieldHealthMax = BattlePhases[CurrentBattlePhase-1].MaxShieldHealth[WorldInfo.Game.GetModifiedGameDifficulty()] * HealthMod * ShieldHealthScale; ShieldHealth = ShieldHealthMax; UpdateShieldHealth(); // Initialize bump variables NumHuntAndHealEnemyBumps = 0; LastHuntAndHealEnemyBumpTime = WorldInfo.TimeSeconds; // Stumble if( IsDoingSpecialMove() ) { EndSpecialMove(); } DoSpecialMove( SM_Stumble,,, class'KFSM_Stumble'.static.PackBodyHitSMFlags(self, vector(Rotation)) ); // Inform AI of phase change if( MyHansController != none ) { MyHansController.NextBattlePhase(); } // Increment battle phase IncrementBattlePhase(); } else { ClearTimer(nameof(HuntAndHealBump)); `DialogManager.PlayHansBattlePhaseDialog( self, CurrentBattlePhase ); } } if( !bOn ) { bHealedThisPhase = false; SetTimer( 0.1, false, nameOf(Timer_ResetShieldHealthPct) ); } } function SetShieldScale(float InScale) { ShieldHealthScale = InScale; } /** Sets shield health pct back to 0 for next phase */ function Timer_ResetShieldHealthPct() { ShieldHealthPctByte = 0; } /** Returns the amount that Hans should heal for this phase */ function float GetHealAmountForThisPhase() { return float(HealthMax) * BattlePhases[CurrentBattlePhase-2].HealAmounts[WorldInfo.Game.GetModifiedGameDifficulty()]; } /** Summons boss minions depending on difficulty and battle phase */ function SummonMinions() { local int Skill; local int DifficultyIndex; local KFAIWaveInfo MinionWave; local KFAISpawnManager SpawnManager; local KFGameInfo MyKFGameInfo; MyKFGameInfo = KFGameInfo( WorldInfo.Game ); if( MyHansController != none ) { Skill = MyHansController.Skill; } // Determine which summon squad to spawn by difficulty if( Skill == class'KFGameDifficultyInfo'.static.GetDifficultyValue(0) ) // Normal { DifficultyIndex = 0; } else if( Skill <= class'KFGameDifficultyInfo'.static.GetDifficultyValue(1) ) // Hard { DifficultyIndex = 1; } else if( Skill <= class'KFGameDifficultyInfo'.static.GetDifficultyValue(2) ) // Suicidal { DifficultyIndex = 2; } else // Hell on Earth { DifficultyIndex = 3; } // Force frustration mode on MyKFGameInfo.GetAIDirector().bForceFrustration = true; // Spawn our minions MinionWave = GetWaveInfo(CurrentBattlePhase, DifficultyIndex); SpawnManager = MyKFGameInfo.SpawnManager; if ( MinionWave != none && SpawnManager != none ) { SpawnManager.SummonBossMinions( MinionWave.Squads, GetNumMinionsToSpawn() ); } } /** Used to clear DOTs, such as when Hans is healing */ simulated function ClearDOTs() { DamageOverTimeArray.Length = 0; } /** If Hans repeatedly bumps into players during his hunt and heal phase, just heal */ simulated event Bump( Actor Other, PrimitiveComponent OtherComp, Vector HitNormal ) { local KFPawn KFP; if( !bGunsEquipped ) { if( Role == ROLE_Authority && bInHuntAndHealMode && MyHansController != none && NumHuntAndHealEnemyBumps >= 0 && !IsDoingSpecialMove() && Other.GetTeamNum() != GetTeamNum() && MyHansController.FindCommandOfClass(class'AICommand_Attack_Grab') == none ) { KFP = KFPawn( Other ); if( KFP != none ) { if( `TimeSince(LastHuntAndHealEnemyBumpTime) > 1.f ) { ++NumHuntAndHealEnemyBumps; LastHuntAndHealEnemyBumpTime = WorldInfo.TimeSeconds; // If we've bumped into players enough times, just heal off of them if( NumHuntAndHealEnemyBumps > 0 ) { NumHuntAndHealEnemyBumps = -1; MyHansController.CancelFlee( false ); MyKFAIC.DoGrabAttack( KFP ); return; } } } } } super.Bump( Other, OtherComp, HitNormal ); } // Knock other zeds the heck out of the way when I want to attack my enemy!!! function HuntAndHealBump() { local KFPawn_Monster KFPM; local vector ClosestPoint; local float ClosestDist; if( MyKFAIC == none || MyKFAIC.Enemy == none /*|| IsDoingSpecialMove(SM_GrappleAttack)*/ || VSizeSq(Location - MyKFAIC.Enemy.Location) > 250000 ) // 5 meters { return; } foreach WorldInfo.AllPawns( class'KFPawn_Monster', KFPM, Location, 300.f ) { // Heavy bump guys that are between us and our prey! if( KFPM != self && KFPM.IsAliveAndWell() ) { ClosestDist = PointDistToSegment(KFPM.Location, Location, MyKFAIC.Enemy.Location, ClosestPoint); if( ClosestDist < GetCollisionRadius() * 1.5 ) { MyKFAIC.DoHeavyZedBump(KFPM, Normal(KFPM.Location - Location)); } } } } /** returns whether we need to turn to fire at the specified location and angle*/ function bool NeedToTurnEx(vector targ, optional float Angle = 0.93) { local vector LookDir, AimDir; LookDir = Vector(Rotation); LookDir.Z = 0; LookDir = Normal(LookDir); AimDir = targ - Location; AimDir.Z = 0; AimDir = Normal(AimDir); return ((LookDir Dot AimDir) < Angle); } /** AnimNotify which starts during Hans's AoE melee attack */ simulated function ANIMNOTIFY_AoENerveGas(){} /** Overridden to prevent blood while invulnerable */ simulated function ApplyBloodDecals(int HitZoneIndex, vector HitLocation, vector HitDirection, name HitZoneName, name HitBoneName, class DmgType, bool bIsDismemberingHit, bool bWasObliterated) { if( !bInHuntAndHealMode ) { super.ApplyBloodDecals( HitZoneIndex, HitLocation, HitDirection, HitZoneName, HitBoneName, DmgType, bIsDismemberingHit, bWasObliterated ); } // else do nothing, leave no blood because hans is mostly invulnerable } /** Updates the color of the shield based on its health */ simulated function UpdateShieldColor(); /** Detaches the shield PSC */ simulated function DetachShieldFX(); DefaultProperties { ShieldHealthScale=1.f }