//============================================================================= // KFAIController_ZedMatriarch //============================================================================= // AI controller class for the matriarch. //============================================================================= // Killing Floor 2 // Copyright (C) 2016 Tripwire Interactive LLC // - Dan Weiss //============================================================================= class KFAIController_ZedMatriarch extends KFAIController_ZedBoss; enum EMatriarchAttacksByRange { EMatriarchAttacksByRange_SweepingClaw, EMatriarchAttacksByRange_LightningStorm, EMatriarchAttacksByRange_WarningSiren, EMatriarchAttacksByRange_TeslaBlast, EMatriarchAttacksByRange_PlasmaCannon, EMatriarchAttacksByRange_ScorpionWhip }; /** Cached reference to patriarch pawn */ var KFPawn_ZedMatriarch MyMatPawn; /** Whether attack evaluation is enabled or not */ var bool bCanEvaluateAttacks; /** How long to wait before evaluating special attacks */ var float GlobalCooldownTimer; var vector2d GlobalCooldownTimeRange_Melee; var vector2d GlobalCooldownTimeRange_LightningStorm; var vector2d GlobalCooldownTimeRange_WarningSiren; var vector2d GlobalCooldownTimeRange_TeslaBlast; var vector2d GlobalCooldownTimeRange_PlasmaCannon; var vector2d GlobalCooldownTimeRange_ScorpionWhip; /* Player Targeting */ var vector2d ReevaluateEnemiesTimeRange; var transient float ReevaluateEnemiesTimer; var array PlayerDamages; var Pawn CurrentTargetPawn; var bool bLogTargeting; /* Special Attacks */ var float SweepingClawCooldown, LastSweepingClawTime; var float TeslaBlastCooldown, LastTeslaBlastTime; var float PlasmaCannonCooldown, LastPlasmaCannonTime; var float LightningStormCooldown, LastLightningStormTime; var float WarningSirenCooldown, LastWarningSirenTime; var float ScorpionWhipCooldown, LastScorpionWhipTime; var config bool bRandomMoves; /** Set MyPatPawn to avoid casting */ event Possess( Pawn inPawn, bool bVehicleTransition ) { super.Possess( inPawn, bVehicleTransition ); if (KFPawn_ZedMatriarch(inPawn) != none) { MyMatPawn = KFPawn_ZedMatriarch(inPawn); } else { `warn( GetFuncName()$"() attempting to possess "$inPawn$", but it's not a KFPawn_ZedMatriarch class! MyMatPawn variable will not be valid." ); } // Wait a bit before evaluating special attacks GlobalCooldownTimer = 2.5f + fRand(); // Start evaluating on next tick bCanEvaluateAttacks = true; if (CommandList == none || CommandList.class != class'AICommand_BossTheatrics') { MyMatPawn.ActivateShield(); } SetReevaluateEnemiesTimer(); } simulated function SetReevaluateEnemiesTimer() { ReevaluateEnemiesTimer = RandRange(ReevaluateEnemiesTimeRange.X, ReevaluateEnemiesTimeRange.Y); if (bLogTargeting) { `log(self$"::SetReevaluateEnemiesTimer - ReevaluateEnemiesTimer: "$ReevaluateEnemiesTimer); } } simulated function ReevaluateEnemies() { local int MaxPlayerDamage, i; local KFPlayerController KFPC, MaxKFPC; if (bLogTargeting) { for (i = 0; i < PlayerDamages.Length; ++i) { `log(self$"::ReevaluateEnemies - PlayerDamages "$i$": "$PlayerDamages[i]); } } MaxPlayerDamage = 0; // find most damaging player foreach WorldInfo.AllControllers(class'KFPlayerController', KFPC) { if (KFPawn(KFPC.Pawn).IsAliveAndWell()) { if (PlayerDamages.Length > KFPC.PlayerNum && PlayerDamages[KFPC.PlayerNum] > MaxPlayerDamage) { MaxPlayerDamage = PlayerDamages[KFPC.PlayerNum]; MaxKFPC = KFPC; } } else { // mark invalid player as untargetable PlayerDamages[MaxKFPC.PlayerNum] = 0; } } if (bLogTargeting) { `log(self$"::ReevaluateEnemies - Max damage player: "$MaxKFPC); } // target most damaging player if (MaxKFPC != none) { CurrentTargetPawn = MaxKFPC.Pawn; ChangeEnemy(MaxKFPC.Pawn, false); // mark most damaging player as untargetable now PlayerDamages[MaxKFPC.PlayerNum] = 0; } SetReevaluateEnemiesTimer(); } simulated function Tick(FLOAT DeltaTime) { super.Tick(DeltaTime); EvaluateAttacks(DeltaTime); } event SeePlayer(Pawn Seen); event bool FindNewEnemy() { local bool Result; Result = super.FindNewEnemy(); CurrentTargetPawn = Enemy; return Result; } /** Evaluates whether or not the Matriarch can do a special attack */ function EvaluateAttacks( float DeltaTime ) { local float DistToTargetSq; local array PossibleMoves; local EMatriarchAttacksByRange ChosenMove; if (!bCanEvaluateAttacks) { return; } ReevaluateEnemiesTimer -= DeltaTime; if (MyMatPawn.IsDoingSpecialMove() || (CommandList != none && GetActiveCommand().IsA('AICommand_SpecialMove'))) { return; } if (MyMatPawn.bShouldTaunt) { MyMatPawn.bShouldTaunt = false; MyMatPawn.DoSpecialMove(SM_Taunt, true,, class'KFSM_Matriarch_Taunt'.static.PackSMFlags(MyMatPawn, TAUNT_Standard)); return; } if (ReevaluateEnemiesTimer <= 0.f) { ReevaluateEnemies(); } GlobalCooldownTimer -= DeltaTime; if (GlobalCooldownTimer > 0.f) { return; } // Trace from worldinfo, open doors ignore traces from zeds if (Enemy != none && WorldInfo.FastTrace(Enemy.Location, Pawn.Location, , true)) { DistToTargetSq = VSizeSq(Enemy.Location - Pawn.Location); PossibleMoves = GetPossibleMovesByRange(DistToTargetSq); if (PossibleMoves.Length > 0) { if (!bRandomMoves) { ChosenMove = PossibleMoves[0]; } else { ChosenMove = PossibleMoves[Rand(PossibleMoves.Length)]; } switch (ChosenMove) { //case EMatriarchAttacksByRange_SweepingClaw: // class'AICommand_Matriarch_SweepingClaw'.static.SweepingClaw(self); // break; case EMatriarchAttacksByRange_LightningStorm: class'AICommand_Matriarch_LightningStorm'.static.LightningStorm(self); break; case EMatriarchAttacksByRange_WarningSiren: class'AICommand_Matriarch_WarningSiren'.static.WarningSiren(self); break; case EMatriarchAttacksByRange_TeslaBlast: class'AICommand_Matriarch_TeslaBlast'.static.TeslaBlast(self); break; case EMatriarchAttacksByRange_PlasmaCannon: class'AICommand_MatriarchPlasmaCannon'.static.PlasmaCannonAttack(self); break; case EMatriarchAttacksByRange_ScorpionWhip: class'AICommand_Matriarch_ScorpionWhip'.static.ScorpionWhip(self); break; }; GlobalCooldownTimer = 1000000.f; } else { GlobalCooldownTimer = 0.5f; } } else { GlobalCooldownTimer = 0.5f; } } function array GetPossibleMovesByRange(float DistToTargetSq) { local array PossibleMoves; //if (CanUseSweepingClaw(DistToTargetSq)) //{ // PossibleMoves.AddItem(EMatriarchAttacksByRange_SweepingClaw); //} if (CanUseLightningStorm(DistToTargetSq)) { PossibleMoves.AddItem(EMatriarchAttacksByRange_LightningStorm); } if (CanUseWarningSiren(DistToTargetSq)) { PossibleMoves.AddItem(EMatriarchAttacksByRange_WarningSiren); } if (CanUseTeslaBlast(DistToTargetSq)) { PossibleMoves.AddItem(EMatriarchAttacksByRange_TeslaBlast); } if (CanUsePlasmaCannon(DistToTargetSq)) { PossibleMoves.AddItem(EMatriarchAttacksByRange_PlasmaCannon); } if (CanUseScorpionWhip(DistToTargetSq)) { PossibleMoves.AddItem(EMatriarchAttacksByRange_ScorpionWhip); } return PossibleMoves; } //function bool CanUseSweepingClaw(float DistToTargetSq) //{ // if (!MyMatPawn.CanUseSweepingClaw()) // { // return false; // } // // if (DistToTargetSq > Square(class'KFSM_Matriarch_SweepingClaw'.default.MaxVictimDistance)) // { // return false; // } // // if (`TimeSince(LastSweepingClawTime) < SweepingClawCooldown) // { // return false; // } // // return true; //} function bool CanUseTeslaBlast(float DistToTargetSq) { if (!MyMatPawn.CanUseTeslaBlast()) { return false; } if (DistToTargetSq > Square(class'KFSM_Matriarch_TeslaBlast'.default.MaxVictimDistance)) { return false; } if (`TimeSince(LastTeslaBlastTime) < TeslaBlastCooldown) { return false; } return true; } function bool CanUsePlasmaCannon(float DistToTargetSq) { if (!MyMatPawn.CanUsePlasmaCannon()) { return false; } if (DistToTargetSq > Square(class'KFSM_Matriarch_PlasmaCannon'.default.MaxVictimDistance)) { return false; } if (`TimeSince(LastPlasmaCannonTime) < PlasmaCannonCooldown) { return false; } return true; } function bool CanUseLightningStorm(float DistToTargetSq) { if (!MyMatPawn.CanUseLightningStorm()) { return false; } if (DistToTargetSq > Square(class'KFSM_Matriarch_LightningStorm'.default.MaxVictimDistance)) { return false; } if (`TimeSince(LastLightningStormTime) < LightningStormCooldown) { return false; } return true; } function bool CanUseWarningSiren(float DistToTargetSq) { if (!MyMatPawn.CanUseWarningSiren()) { return false; } if (DistToTargetSq > Square(class'KFSM_Matriarch_WarningSiren'.default.MaxVictimDistance)) { return false; } if (`TimeSince(LastWarningSirenTime) < WarningSirenCooldown) { return false; } return true; } function bool CanUseScorpionWhip(float DistToTargetSq) { if (!MyMatPawn.CanUseScorpionWhip()) { return false; } if (DistToTargetSq > Square(class'KFSM_Matriarch_ScorpionWhip'.default.MaxRange)) { return false; } if (`TimeSince(LastScorpionWhipTime) < ScorpionWhipCooldown) { return false; } return true; } //function DoStrike() //{ // if (CanUseSweepingClaw(0)) // { // class'AICommand_Matriarch_SweepingClaw'.static.SweepingClaw(self); // } // else // { // super.DoStrike(); // } //} function NotifySpecialMoveStarted(KFSpecialMove SM) { super.NotifySpecialMoveStarted(SM); if (MyMatPawn.Role == ROLE_Authority) { MyMatPawn.SetShieldUp(false); MyMatPawn.SetCloaked(false); } } /** Special move has ended, whether cleanly or aborted */ function NotifySpecialMoveEnded(KFSpecialMove SM) { super.NotifySpecialMoveEnded(SM); switch (SM.Handle) { case 'KFSM_MeleeAttack': GlobalCooldownTimer = RandRange(GlobalCooldownTimeRange_Melee.X, GlobalCooldownTimeRange_Melee.Y); break; case 'KFSM_Matriarch_TeslaBlast': GlobalCooldownTimer = RandRange(GlobalCooldownTimeRange_TeslaBlast.X, GlobalCooldownTimeRange_TeslaBlast.Y); break; case 'KFSM_Matriarch_PlasmaCannon': GlobalCooldownTimer = RandRange(GlobalCooldownTimeRange_PlasmaCannon.X, GlobalCooldownTimeRange_PlasmaCannon.Y); break; case 'KFSM_Matriarch_LightningStorm': GlobalCooldownTimer = RandRange(GlobalCooldownTimeRange_LightningStorm.X, GlobalCooldownTimeRange_LightningStorm.Y); break; case 'KFSM_Matriarch_WarningSiren': GlobalCooldownTimer = RandRange(GlobalCooldownTimeRange_WarningSiren.X, GlobalCooldownTimeRange_WarningSiren.Y); break; case 'KFSM_Matriarch_ScorpionWhip': GlobalCooldownTimer = RandRange(GlobalCooldownTimeRange_ScorpionWhip.X, GlobalCooldownTimeRange_ScorpionWhip.Y); break; default: GlobalCooldownTimer = 0.5f; break; }; EvaluateSprinting(); if (SM.Handle == 'KFSM_Zed_Boss_Theatrics') { MyMatPawn.ActivateShield(); } else { MyMatPawn.SetShieldUp(true); MyMatPawn.SetCloaked(true); } if (CurrentTargetPawn != none && Enemy != CurrentTargetPawn) { ChangeEnemy(CurrentTargetPawn, false); } } /** Evaluate if we should start/stop sprinting, and then set the sprinting flag */ function EvaluateSprinting() { if (ShouldSprint()) { MyKFPawn.SetSprinting(true); } else { MyKFPawn.SetSprinting(false); } } /** Timer function called during latent moves that determines whether NPC should sprint or stop sprinting */ function bool ShouldSprint() { local float DistToEnemy; if (MyKFPawn != none && MyKFPawn.IsAliveAndWell() && Enemy != none && Enemy.IsAliveAndWell()) { DistToEnemy = VSize(Enemy.Location - Pawn.Location); if (DistToEnemy > SprintWithinEnemyRange.X && DistToEnemy < SprintWithinEnemyRange.Y) { return true; } else if (!FastTrace(Enemy.Location, Pawn.Location,, true)) { return true; } } return false; } function NotifyTakeHit(Controller InstigatedBy, vector HitLocation, int Damage, class damageType, vector Momentum) { super.NotifyTakeHit(InstigatedBy, Hitlocation, Damage, damageType, Momentum); if (Damage > 0) { if (PlayerDamages.Length <= InstigatedBy.PlayerNum) { PlayerDamages[InstigatedBy.PlayerNum] = Damage; } else { PlayerDamages[InstigatedBy.PlayerNum] += Damage; } if (bLogTargeting) { `log(self$"::NotifyTakeHit - player "$InstigatedBy.PlayerNum$" add "$Damage$" damage ("$PlayerDamages[InstigatedBy.PlayerNum]$")"); } } } function NotifyKilled(Controller Killer, Controller Killed, pawn KilledPawn, class damageType) { if (GetIsInZedVictoryState()) { return; } if (self == Killer && Killed.GetTeamNum() != GetTeamNum()) { `DialogManager.PlayMattyKilledDialog(MyKFPawn); } else if (Killed.GetTeamNum() == GetTeamNum()) { `DialogManager.PlayMattyMinionKilledDialog(MyKFPawn); } super.NotifyKilled( Killer, Killed, KilledPawn, damageType ); } function EnterZedVictoryState() { super.EnterZedVictoryState(); bCanEvaluateAttacks = false; KFWeapon(MyMatPawn.Weapon).GotoState( 'Inactive' ); MyMatPawn.ClearTimer(nameof(MyMatPawn.Timer_TickDialog), MyMatPawn); } /********************************************************************************************* * Pathfinding **********************************************************************************************/ function bool AmIAllowedToSuicideWhenStuck() { return false; } /********************************************************************************************* * Bump **********************************************************************************************/ function bool DoHeavyZedBump( Actor Other, vector HitNormal ) { local int BumpEffectDamage; local KFPawn_Monster BumpedMonster; /** If we bumped into a glass window, break it */ if (Other.bCanBeDamaged && KFFracturedMeshGlass(Other) != none) { KFFracturedMeshGlass(Other).BreakOffAllFragments(); return true; } BumpedMonster = KFPawn_Monster(Other); if (BumpedMonster == none || !BumpedMonster.IsAliveAndWell() || BumpedMonster.ZedBumpDamageScale <= 0) { return false; } if (MyKFPawn == none || !MyKFPawn.IsAliveAndWell()) { return false; } BumpEffectDamage = ZedBumpEffectThreshold * BumpedMonster.ZedBumpDamageScale * (MyKFPawn.bIsSprinting ? 2 : 1); // If the Bumped Zed is near death, play either a knockdown or an immediate obliteration if (BumpedMonster.Health - BumpEffectDamage <= 0) { BumpedMonster.TakeDamage(BumpEffectDamage, self, BumpedMonster.Location, vect(0, 0, 0), MyKFPawn.GetBumpAttackDamageType()); BumpedMonster.Knockdown(, vect(1, 1, 1), Pawn.Location, 1000, 100); return true; } else { // otherwise deal damage and stumble the zed BumpedMonster.TakeDamage(BumpEffectDamage, self, BumpedMonster.Location, vect(0,0,0), MyKFPawn.GetBumpAttackDamageType()); BumpedMonster.DoSpecialMove(SM_Stumble,,, class'KFSM_Stumble'.static.PackBodyHitSMFlags(BumpedMonster, HitNormal)); return true; } return false; } event bool NotifyBump(Actor Other, vector HitNormal) { local KFPawn_Human BumpedHuman; BumpedHuman = KFPawn_Human(Other); if (BumpedHuman != none && BumpedHuman != CurrentTargetPawn && !MyMatPawn.IsDoingSpecialMove() && !IsZero(MyMatPawn.Velocity) && HitNormal dot vector(Rotation) < -0.7) { AbortCommand(CommandList); // Set our new enemy ChangeEnemy(BumpedHuman, false); //SetEnemyMoveGoal(self, true); EnableMeleeRangeEventProbing(); // Restart default command BeginCombatCommand(GetDefaultCommand(), "Restarting default command"); return true; } return super.NotifyBump(Other, HitNormal); } defaultproperties { Steering=none DefaultCommandClass=class'KFGameContent.AICommand_Base_Matriarch' MeleeCommandClass=class'KFGameContent.AICommand_Base_Matriarch' //SweepingClawCooldown=10.f TeslaBlastCooldown=5.f PlasmaCannonCooldown=7.f LightningStormCooldown=8.f WarningSirenCooldown=10.f ScorpionWhipCooldown=12.f SprintWithinEnemyRange=(X=1500.f,Y=1000000000.f) ReevaluateEnemiesTimeRange=(X=8.f, Y=10.f) bLogTargeting=false bCanDoHeavyBump=true GlobalCooldownTimeRange_Melee=(X=0.0, Y=0.0) GlobalCooldownTimeRange_LightningStorm=(X=2.5, Y=2.5) GlobalCooldownTimeRange_WarningSiren=(X=2.5, Y=2.5) GlobalCooldownTimeRange_TeslaBlast=(X=2.5, Y=2.5) GlobalCooldownTimeRange_PlasmaCannon=(X=2.5, Y=2.5) GlobalCooldownTimeRange_ScorpionWhip=(X=0.0, Y=0.0) }