//============================================================================= // KFAIController_ZedFleshpoundKing //============================================================================= // AI Controller for the Fleshpound King //============================================================================= // Killing Floor 2 // Copyright (C) 2017 Tripwire Interactive LLC // - Matt 'Squirrlz' Farber //============================================================================= class KFAIController_ZedFleshpoundKing extends KFAIController_ZedFleshpound; const MaxPhases = 4; /** Cached pawn reference */ var KFPawn_ZedFleshpoundKing MyPawn; /** Next time we should attempt to do a beam attack */ var protected float NextBeamCheckTime; /** Last time we ticked the beam attack check code */ var transient protected float LastBeamTickTime; /** How long to wait after the first time seeing a player before we should attempt to check for a beam attack */ var const vector2d BeamAttackWaitTimeRange; /** Minimum angle a target must be from the fleshpound's rotation when starting the chest beam attack */ var const float MinChestBeamTargetAngle; /** Maximum height offset a target can be from the fleshpound to start the chest beam attack*/ var const float MaxChestBeamHeightOffset; /** Current battle phase, 0 = start, indexes into PhaseThresholds*/ var int CurrentPhase; /** Health percentage cutoffs for the current fight phase */ var const float PhaseThresholds[MaxPhases]; /** Phase at which the chest beam can start */ var const int ChestBeamMinPhase; event Possess(Pawn inPawn, bool bVehicleTransition) { local KFGameReplicationInfo KFGRI; super.Possess(inPawn, bVehicleTransition); // Play entrance animation KFGRI = KFGameReplicationInfo(WorldInfo.GRI); if (KFGRI != none && KFGRI.WaveNum >= KFGRI.WaveMax) { class'AICommand_BossTheatrics'.static.DoTheatrics(self, THEATRIC_Entrance); } MyPawn = KFPawn_ZedFleshpoundKing(inPawn); } function bool AmIAllowedToSuicideWhenStuck() { return false; } /** We have seen a player -- start checking for beam attack */ event SeePlayer( Pawn Seen ) { super.SeePlayer( Seen ); if( NextBeamCheckTime == 0.f ) { NextBeamCheckTime = WorldInfo.TimeSeconds + RandRange( BeamAttackWaitTimeRange.X, BeamAttackWaitTimeRange.Y ); } } /** Tick our chest beam attack */ simulated event Tick( float DeltaTime ) { super.Tick( DeltaTime ); //Don't attack while we're in theatrics if (CommandList != none && CommandList.class == class'AICommand_BossTheatrics') { return; } if( NextBeamCheckTime > 0.f && `TimeSince(LastBeamTickTime) > 0.2f && WorldInfo.TimeSeconds >= NextBeamCheckTime ) { LastBeamTickTime = WorldInfo.TimeSeconds; CheckForBeamAttack(); } } /** Decides if we should attempt a beam attack */ function CheckForBeamAttack() { local Pawn P, BestTarget; local float MaxBeamTargetDistSQ, Dist, DotAngle, Rating, BestRating; local vector Projection, PawnDir; // Don't allow beam attack during special moves if( MyKFPawn.IsDoingSpecialMove() ) { return; } // Don't allow beam when not in the correct phase if (CurrentPhase < ChestBeamMinPhase) { return; } // Don't allow beam while enraged if (RagePlugin.bIsEnraged) { return; } // Subtract 15% from the max beam length when choosing targets due to wind up time MaxBeamTargetDistSQ = Square( class'KFSM_FleshpoundKing_ChestBeam'.default.MaxBeamLength * 0.85f ); PawnDir = vector( MyKFPawn.Rotation ); BestRating = -1.f; foreach WorldInfo.AllPawns( class'Pawn', P ) { // Only target enemies if( P.GetTeamNum() == MyKFPawn.GetTeamNum() || !P.IsAliveAndWell() ) { continue; } // Only target things that are targetable if (!P.CanAITargetThisPawn(MyKFPawn.Controller)) { continue; } Projection = P.Location - MyKFPawn.Location; Dist = VSizeSQ( Projection ); // Distance check if( Dist > MaxBeamTargetDistSQ ) { continue; } // Within field of view and not behind geometry and within allowed height offset DotAngle = PawnDir dot Normal(Projection); if( DotAngle > MinChestBeamTargetAngle && MyKFPawn.FastTrace(P.Location, MyKFPawn.Location) && Abs(P.Location.Z - MyKFPawn.Location.Z) < MaxChestBeamHeightOffset ) { // Lower ratings are better (smaller distance and closer to center of pawn's rotation) Rating = Dist * ( 1.f - DotAngle ); if( BestRating == -1.f || Rating < BestRating ) { BestRating = Rating; BestTarget = P; } } } // Target check if( BestTarget != none ) { if( BestTarget != Enemy ) { ChangeEnemy( BestTarget, false ); } // Start blasting! NextBeamCheckTime = 0.f; class'AICommand_FleshpoundKing_ChestBeamAttack'.static.ChestBeamAttack( self ); } } function NotifyTakeHit(Controller InstigatedBy, vector HitLocation, int Damage, class damageType, vector Momentum) { local float CurrentHealth; super.NotifyTakeHit(InstigatedBy, HitLocation, Damage, damageType, Momentum); //Can still transition if (MyPawn != none && CurrentPhase + 1 < MaxPhases) { CurrentHealth = float(MyPawn.Health) / float(MyPawn.HealthMax); if (CurrentHealth < PhaseThresholds[CurrentPhase + 1]) { TransitionToPhase(CurrentPhase + 1); } } } /** Handle any updates caused by a phase transition */ function TransitionToPhase(int NewPhase) { CurrentPhase = NewPhase; MyPawn.CurrentPhase = NewPhase; MyPawn.SpawnSubWave(); switch (NewPhase) { case 3: MyPawn.ActivateShield(); break; } } function NotifySpecialMoveEnded( KFSpecialMove SM ) { super.NotifySpecialMoveEnded( SM ); if (SM.Handle == 'KFSM_Zed_Boss_Theatrics') { //Start initial phase if theatrics just finished TransitionToPhase(0); } // This ensures that we don't attempt to do a beam attack immediately after a special move ends if( NextBeamCheckTime < WorldInfo.TimeSeconds + 2.0f ) { NextBeamCheckTime = WorldInfo.TimeSeconds + 2.0f + fRand(); } } /** Whether enemy switch commands can be run */ function bool CanSwitchEnemies() { return (MyKFPawn != none && MyKFPawn.SpecialMove != SM_HoseWeaponAttack); } /** Overridden to stop retargeting enemies when firing the chest beam */ event bool FindNewEnemy() { if( !CanSwitchEnemies() ) { return false; } return super.FindNewEnemy(); } /** Overridden to stop retargeting enemies when firing the chest beam */ event bool SetEnemy( Pawn NewEnemy ) { if( !CanSwitchEnemies() ) { return false; } return super.SetEnemy( NewEnemy ); } /** Overridden to stop retargeting enemies when firing the chest beam */ event ChangeEnemy( Pawn NewEnemy, optional bool bCanTaunt=true ) { if( !CanSwitchEnemies() ) { return; } super.ChangeEnemy( NewEnemy, bCanTaunt ); } /** Forces a target change, bypasses our custom ChangeEnemy() */ function ForceTargetChange( Pawn NewEnemy ) { super.ChangeEnemy( NewEnemy, false ); } state ZedVictory { Begin: Sleep(0.1f); if (MyKFPawn != none) { class'AICommand_BossTheatrics'.static.DoTheatrics(self, THEATRIC_Victory, -1); } } DefaultProperties { BeamAttackWaitTimeRange=(X=1.5f, Y=3.0f) MinChestBeamTargetAngle=0.77f MaxChestBeamHeightOffset=64.f PhaseThresholds[0]=1 PhaseThresholds[1]=0.65 //0.75 PhaseThresholds[2]=0.5 PhaseThresholds[3]=0.2 //0.25 //0.1 ChestBeamMinPhase=2 SpawnRagedChance(`DIFFICULTY_NORMAL)=1.f SpawnRagedChance(`DIFFICULTY_HARD)= 1.f SpawnRagedChance(`DIFFICULTY_SUICIDAL)= 1.f SpawnRagedChance(`DIFFICULTY_HELLONEARTH)= 1.f }