423 lines
12 KiB
Ucode
423 lines
12 KiB
Ucode
//=============================================================================
|
|
// 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
|
|
} |