//============================================================================= // KFAIController_ZedBloatKing //============================================================================= // Boss variant of the Bloat. //============================================================================= // Killing Floor 2 // Copyright (C) 2017 Tripwire Interactive LLC //============================================================================= class KFAIController_ZedBloatKing extends KFAIController_ZedBloat; /** Cached pawn reference */ var KFPawn_ZedBloatKing BloatPawn; /** How long the Patriarch should wait to start sprinting after losing sight of his enemy */ var float LostSightSprintDelay; /** How often to spawn a new pack of minions. This is done continuously throughout the fight. */ var() const float MinionSpawnTimer[4]; /** The base amount of minions to spawn in a single cycle */ var vector2D NumMinionsToSpawn[4]; /** Wave infos to use for continuous spawn */ var KFAIWaveInfo ContinuousSpawnWaveInfos[4]; /** Timer triggered when a piece of armor is blown off to keep Bloat in enrage */ var float ArmorEnrageTimer; /** Attacks */ var float NextSpecialMoveCheck; var float NextGorgeAttackCheck; var float NextHumanGorgeAttackCheck; /** Retargeting modifications - pulled from Hans */ /** The last time we changed to a new target */ var float LastRetargetTime; /** How long to wait before attempting to find a new target */ var vector2d RetargetWaitTimeRange; /** The actual retarget wait time after last retarget time has been set */ var transient float ActualRetargetWaitTime; event Possess(Pawn inPawn, bool bVehicleTransition) { super.Possess(inPawn, bVehicleTransition); BloatPawn = KFPawn_ZedBloatKing(inPawn); //Init Timers NextSpecialMoveCheck = 0.5f; NextGorgeAttackCheck = class'KFSM_BloatKing_Gorge'.default.GorgeAttackCheckDelay; //Delay start of minion waves a bit to get past boss intro SetTimer(2.f, true, 'StartMinionWaves'); // Initialize retarget time LastRetargetTime = WorldInfo.TimeSeconds; ActualRetargetWaitTime = RandRange(RetargetWaitTimeRange.X, RetargetWaitTimeRange.Y); } simulated function Tick(float DeltaTime) { super.Tick(DeltaTime); EvaluateSpecialMoves(DeltaTime); } function bool AmIAllowedToSuicideWhenStuck() { return false; } function EvaluateSpecialMoves(float DeltaTime) { //Don't attack while we're in theatrics if (CommandList != none && CommandList.class == class'AICommand_BossTheatrics') { return; } NextSpecialMoveCheck -= DeltaTime; NextGorgeAttackCheck -= DeltaTime; NextHumanGorgeAttackCheck -= DeltaTime; if (NextSpecialMoveCheck > 0.f || BloatPawn.IsDoingSpecialMove()) { return; } if (NextGorgeAttackCheck < 0) { if (CanDoGorgeAttack()) { TriggerGorge(); return; } else { NextGorgeAttackCheck = class'KFSM_BloatKing_Gorge'.default.GorgeAttackCheckDelay; } } NextSpecialMoveCheck = 0.5f; } function TriggerGorge(bool bForced = false) { class'AICommand_BloatKing_Gorge'.static.StartGorge(self); //If we aren't forced, trigger cooldown for gorge intended to pull humans if (!bForced) { NextHumanGorgeAttackCheck = class'KFSM_BloatKing_Gorge'.static.GetGorgeCooldown(MyKFPawn, WorldInfo.Game.GetModifiedGameDifficulty()); } } function bool CanDoGorgeAttack() { local KFPawn KFP; local vector ViewDirection, ToTarget; local float DotAngle, ToTargetRange; ViewDirection = Vector(MyKFPawn.Rotation); //Gorge can attack enemy AND friendly units, with different results on contact foreach BloatPawn.GorgeTrigger.TouchingActors(class'KFPawn', KFP) { //Can target enemy and friendly, but they must be alive, and can be filtered by class type if (!KFP.IsAliveAndWell() || !class'KFSM_BloatKing_Gorge'.static.IsValidPullClass(KFP)) { continue; } //Don't allow human-triggered pull if the timer hasn't reset if (NextHumanGorgeAttackCheck > 0) { if (KFP.IsHumanControlled()) { continue; } } ToTarget = KFP.Location - MyKFPawn.Location; ToTargetRange = VSizeSq(ToTarget); //Check distance. If out of range, ignore this potential target. if (KFP.IsHumanControlled() && ToTargetRange > class'KFSM_BloatKing_Gorge'.default.GorgeHumanAttackRangeSq) { continue; } else if (ToTargetRange > class'KFSM_BloatKing_Gorge'.default.GorgeAttackRangeSq) { continue; } // Within field of view and not behind geometry DotAngle = ViewDirection dot Normal(ToTarget); if (DotAngle > class'KFSM_BloatKing_Gorge'.default.GorgeMinAttackAngle && MyKFPawn.FastTrace(KFP.Location, MyKFPawn.Location)) { //Found a target, so we're good to start the move return true; } } //If we've gotten this far, there are no valid targets. Retry later. return false; } event SeePlayer(Pawn Seen) { Super.SeePlayer(Seen); // Evaluate sprinting when visibility changes EvaluateSprinting(); // Reject potential enemy if it's invalid or not on our team, or if it's already my current enemy, or if my pawn is dead or invalid if (Seen == none || !Seen.IsAliveAndWell() || Pawn.IsSameTeam(Seen) || Pawn == none || !Pawn.IsAliveAndWell() || !Seen.CanAITargetThisPawn(self)) { return; } LastEnemySightedTime = WorldInfo.TimeSeconds; } function NotifySpecialMoveEnded(KFSpecialMove SM) { super.NotifySpecialMoveEnded(SM); EvaluateSprinting(); // Retarget if it's been enough time since we changed targets if (SM.Handle == 'KFSM_MeleeAttack' && `TimeSince(LastRetargetTime) > ActualRetargetWaitTime ) { CheckForEnemiesInFOV(3000.f, -1.f, 1.f, true); } } event ChangeEnemy(Pawn NewEnemy, optional bool bCanTaunt = true) { local Pawn OldEnemy; OldEnemy = Enemy; super.ChangeEnemy(NewEnemy, bCanTaunt); if (OldEnemy != Enemy) { LastRetargetTime = WorldInfo.TimeSeconds; ActualRetargetWaitTime = RandRange(RetargetWaitTimeRange.X, RetargetWaitTimeRange.Y); } } /** Evaluate if we should start/stop sprinting, and then set the sprinting flag */ function EvaluateSprinting() { if (MyKFPawn != none && MyKFPawn.IsAliveAndWell() && Enemy != none) { if (ShouldSprint()) { MyKFPawn.SetSprinting(true); } else { MyKFPawn.SetSprinting(false); } } } function bool ShouldSprint() { local float DistToEnemy; local float CurrentSprintDistance; // Don't allow sprinting when blocking attacks if (MyKFPawn.IsDoingSpecialMove(SM_Block)) { return false; } else if (MyKFPawn.IsEnraged()) { return true; } // Sprint if we can't see our enemy else if (LastEnemySightedTime == 0 || `TimeSince(LastEnemySightedTime) > LostSightSprintDelay ) { //`log(self@GetFuncName()$" don't see any enemy should sprint = true! LastEnemySightedTime: "$LastEnemySightedTime$" TimeSince(LastEnemySightedTime): "$`TimeSince(LastEnemySightedTime)); return true; } //Sprint if nobody in range else { DistToEnemy = VSizeSq(Enemy.Location - Pawn.Location); CurrentSprintDistance = Lerp(SprintWithinEnemyRange.X, SprintWithinEnemyRange.Y, MyKFPawn.GetHealthPercentage()); if (DistToEnemy < CurrentSprintDistance * CurrentSprintDistance) { return true; } else { return false; } } return false; } function StartMinionWaves() { local KFGameInfo KFGI; local float TimerIdx; if (CommandList != none && CommandList.class == class'AICommand_BossTheatrics') { `log("*** Still not done with theatrics"); return; } ClearTimer('StartMinionWaves'); KFGI = KFGameInfo(WorldInfo.Game); TimerIdx = Clamp(KFGI.GetModifiedGameDifficulty(), 0, 3); SetTimer(MinionSpawnTimer[TimerIdx], true, 'SpawnMinions'); } //King Bloat spawns minions constantly on a timer throughout the fight, both for player distraction // and to eat in a couple of his abilities. function SpawnMinions() { local KFAIWaveInfo SpawnInfo; local KFGameInfo KFGI; KFGI = KFGameInfo(WorldInfo.Game); SpawnInfo = GetWaveInfo(KFGI.GetModifiedGameDifficulty()); KFGI.SpawnManager.SummonBossMinions(SpawnInfo.Squads, GetNumMinionsToSpawn(), false); //Stop spawning after a couple seconds until the next subwave comes through SetTimer(2.f, true, nameof(PauseBossWave)); } //Once we reach a point where all intended minions for the current spawn cycle are through, pause spawning function PauseBossWave() { local KFGameInfo KFGI; KFGI = KFGameInfo(WorldInfo.Game); if (KFGI.SpawnManager.GetNumAINeeded() <= 0) { StopBossWave(); } } function StopBossWave() { local KFGameInfo KFGI; KFGI = KFGameInfo(WorldInfo.Game); Cleartimer(nameof(PauseBossWave)); KFGI.SpawnManager.StopSummoningBossMinions(); } function KFAIWaveInfo GetWaveInfo(int GameDifficulty) { return ContinuousSpawnWaveInfos[Clamp(GameDifficulty, 0, 3)]; } /** Returns the number of minions to spawn based on number of players */ function byte GetNumMinionsToSpawn() { local int LivingPlayerCount; local float MaxPlayers; local byte NumMinsToSpawn; local KFGameInfo KFGI; KFGI = KFGameInfo(WorldInfo.Game); if (KFGI != none) { LivingPlayerCount = KFGameInfo(WorldInfo.Game).GetLivingPlayerCount(); MaxPlayers = float(WorldInfo.Game.MaxPlayers); NumMinsToSpawn = byte(Lerp(NumMinionsToSpawn[KFGI.GetModifiedGameDifficulty()].X, NumMinionsToSpawn[KFGI.GetModifiedGameDifficulty()].Y, LivingPlayerCount / MaxPlayers)); `log("[KFAIController_ZedBloatKing::GetNumMinionsToSpawn] LivingPlayerCount:" @ LivingPlayerCount @ "Max Players:" @ MaxPlayers @ "NumMinionsToSpawn:" @ NumMinsToSpawn, KFGI.bLogAICount); return NumMinsToSpawn; } //Backup if we're in a weird state return byte(Lerp(NumMinionsToSpawn[0].X, NumMinionsToSpawn[0].Y, fMax(WorldInfo.Game.NumPlayers, 1) / float(WorldInfo.Game.MaxPlayers))); } function SetEnrageTimer() { SetTimer(ArmorEnrageTimer, false, nameof(EndArmorEnrage)); } function StartArmorEnrage() { MyKFPawn.SetEnraged(true); } function EndArmorEnrage() { MyKFPawn.SetEnraged(false); } function PawnDied(Pawn InPawn) { super.PawnDied(InPawn); StopBossWave(); } /** Victory */ function EnterZedVictoryState() { MyKFGameInfo.SpawnManager.StopSummoningBossMinions(); BloatPawn.ClearFartTimer(); super.EnterZedVictoryState(); } state ZedVictory { ignores NotifyTakeHit, NotifyKilled, NotifySpecialMoveEnded, NotifyFleeFinished, SeePlayer, CheckForEnemiesInFOV, EvaluateSprinting, ChangeEnemy, SetEnemy, FindNewEnemy; Begin: Sleep(0.1f); class'AICommand_BossTheatrics'.static.DoTheatrics(self, THEATRIC_Victory, -1); } DefaultProperties { // Bloat King Specific Functionality LostSightSprintDelay=5.0 ArmorEnrageTimer=9.0 //15 25 //10 AggroEnemySwitchWaitTime=7.0f RetargetWaitTimeRange=(X=4.4, Y=5.0) MinionSpawnTimer[0]=70.0 MinionSpawnTimer[1]=65.0 MinionSpawnTimer[2]=65.0 //5 //20 //40.0 MinionSpawnTimer[3]=60.0 NumMinionsToSpawn[0]=(X=5,Y=30) NumMinionsToSpawn[1]=(X=6,Y=36) NumMinionsToSpawn[2]=(X=6,Y=36) //(X=7,Y=17) 5 30 10 60 NumMinionsToSpawn[3]=(X=8,Y=48) ContinuousSpawnWaveInfos[0]=KFAIWaveInfo'GP_Spawning_ARCH.Special.Boss.BloatKing_Normal_WaveInfo' ContinuousSpawnWaveInfos[1]=KFAIWaveInfo'GP_Spawning_ARCH.Special.Boss.BloatKing_Normal_WaveInfo' ContinuousSpawnWaveInfos[2]=KFAIWaveInfo'GP_Spawning_ARCH.Special.Boss.BloatKing_Normal_WaveInfo' ContinuousSpawnWaveInfos[3]=KFAIWaveInfo'GP_Spawning_ARCH.Special.Boss.BloatKing_Normal_WaveInfo' // --------------------------------------------- // --------------------------------------------- // AI / Navigation SprintWithinEnemyRange=(X=2500.f,Y=500.f) //0% health, 100% health }