//============================================================================= // KFGameDifficulty_Endless //============================================================================= // Manages Zed adjustments used in any game mode with Outbreak waves.. //============================================================================= // Killing Floor 2 // Copyright (C) 2017 Tripwire Interactive LLC //============================================================================= class KFOutbreakEvent extends Object within KFGameInfo; //Overrides for when ResetAllPickups will properly function. PRS_Wave functions like base _Survival. enum PickupResetTime { PRS_Wave, //Start of wave PRS_Trader, //During trader time PRS_WaveAndTrader, //During start of wave and start of trader time PRS_Never, //Disable pickups entirely, works like bDisablePickups }; enum BeefcakeType { EBT_Damage, EBT_Rally, EBT_Scream, EBT_StalkerPoison, }; /** Individual per-class adjustments to make after a zed spawns */ struct StatAdjustments { //Class to adjust var() class ClassToAdjust; //Health percentage scale var() float HealthScale; //Scale for gore health of the head hit zone var() float HeadHealthScale; //Scale for shield for zeds that support this var() float ShieldScale; //Start enraged var() bool bStartEnraged; //Whether or not to explode on death var() bool bExplosiveDeath; //Template to use for explosion var KFGameExplosion ExplosionTemplate; //Class to ignore on explosion var class ExplosionIgnoreClass; /** Amount to scale up per-beefcake application per-type */ var array BeefcakeScaleIncreases; /** Amount to scale up per-beefcake application per-type */ var array BeefcakeHealthIncreases; /** Max Beefcake Scale - This should probably never go > 1.5 for collision reasons */ var() float MaxBeefcake; /** Max beefcake health scale - This can scale forever really since it's not tied to visual scale */ var() float MaxBeefcakeHealth; /** Scale to all damage that has this zed as an instigator */ var() float DamageDealtScale; /** Scale to all damage that has this zed as a victim */ var() float DamageTakenScale; /** Override of the global deflation rate to define a different per-zed rate, LERP between X and Y by player count */ var() Vector2D OverrideDeflationRate; /** Additional sub wave to use when one of this type of zed spawns */ var() KFAIWaveInfo AdditionalSubSpawns; /** 1 to max player count range of how many AI should spawn during the sub wave */ var() Vector2D AdditionalSubSpawnCount; /** Killing the zed should give a different dosh amount than the standard. */ var() int DoshGiven; structdefaultproperties { HealthScale = 1.f; HeadHealthScale = 1.f; ShieldScale = 1.f; bExplosiveDeath = false MaxBeefcake = 1.5 MaxBeefcakeHealth = 1.5 DamageDealtScale = 1.0 DamageTakenScale = 1.0 OverrideDeflationRate = (X = 0.0,Y = 0.0) AdditionalSubSpawnCount = (X = 1,Y = 1) DoshGiven=INDEX_NONE } }; /** Individual property overrides that drive other behavior to allow for * a large amount of variety in our weekly event mode. */ struct WeeklyOverrides { /** Difficulty to use for this event */ var() int EventDifficulty; /** Length of game to use for this event */ var() int GameLength; /** Only allow headshots */ var() bool bHeadshotsOnly; /** Spawn rate multiplier. Modifies how KFAISpawnManager runs logic */ var() float SpawnRateMultiplier; /** How often global damage should occur (0 = Off) */ var() float GlobalDamageTickRate; /** How much damage should be applied by global tick */ var() float GlobalDamageTickAmount; /** How much the cost of ammo should be scaled (Default 1.0) */ var() float GlobalAmmoCostScale; /** If this array is not empty, modifies the pawn's DefaultInventory * array prior to calling in to P.AddDefaultInventory */ var() KFGFxObject_TraderItems SpawnWeaponList; /** If this array is not empty, modifies the trader's list of available weapons */ var() KFGFxObject_TraderItems TraderWeaponList; /** Whether or not grenades are disabled at spawn and for purchase */ var() bool bDisableGrenades; /** If this array is not empty, replaces AIClassList entries with a new spawn class */ var() array SpawnReplacementList; /** Whether or not to use the spawn replacement list in the boss wave*/ var() bool bAllowSpawnReplacementDuringBossWave; /** If this array is not empty, replaces AIClassList entries with a new spawn class */ var() array BossSpawnReplacementList; /** If this array is not empty, properties set in ZedsToAdjust are used in AdjustSpawnedAIPawn */ var() array ZedsToAdjust; /** Whether or not to skip opening of the trader */ var() bool bDisableTraders; /** When to reset pickups */ var() PickupResetTime PickupResetTime; /** Override for the difficulty's item pickup modifier */ var() float OverrideItemPickupModifier ; /** Overrride for the difficulty's ammo pickup modifier */ var() float OverrideAmmoPickupModifier ; /** Overrides for the standard wave scale behavior of WaveNum / WaveMax */ var() array WaveItemPickupModifiers; var() array WaveAmmoPickupModifiers; /** Whether or not to use the override item pickup timings */ var() bool bUseOverrideItemRespawnTime; /** Override timings for item pickup respawn */ var() NumPlayerMods OverrideItemRespawnTime; /** Whether or not to use the override ammo pickup timings */ var() bool bUseOverrideAmmoRespawnTime; /** Override timings for ammo pickup respawn */ var() NumPlayerMods OverrideAmmoRespawnTime; /** Permanent zed time */ var() bool bPermanentZedTime; /** Amount of pawns at which zed time will turn off */ var() int PermanentZedTimeCutoff; /** Amount of time between checks to stay in full slomo. Note: Scaled by zed time dilation */ var() float PermanentZedResetTime; /** Override time dilation for zed time */ var() float OverrideZedTimeSlomoScale; /** Radius to use for kicking players out of partial zed time */ var() float ZedTimeRadius; /** Radius to use specifically against bosses for partial zed time */ var() Float ZedTimeBossRadius; /** Height to use for kicking players out of partial zed time */ var() float ZedTimeHeight; /** Whether or not to use size scale on damage */ var() bool bScaleOnHealth; /** Starting size scale (typically should be 1.0) */ var() float StartingDamageSizeScale; /** Damage size scale at 0 health */ var() float DeadDamageSizeScale; /** Global Override Spawn Derate Time */ var() float OverrideSpawnDerateTime; /** Global Override Teleport Derate Time */ var() float OverrideTeleportDerateTime; /** Global gravity override */ var() float GlobalGravityZ; /** Turn on beef cake mode. We're going full Cartman. Pawn scales up when doing damage or being rallied by an alpha. */ var() bool bUseBeefcakeRules; /** Per-player count percent to scale amount of AI in the wave by */ var() array WaveAICountScale; /** Head size */ var() float ZedSpawnHeadScale; /** Player head size */ var() float PlayerSpawnHeadScale; /** Allow human sprinting */ var() bool bHumanSprintEnabled; /** Cost scale for weapons not on the user's active perk */ var() float OffPerkCostScale; /** Whether or not we should allow ground speed to become sprint if the melee backup is active */ var() bool bBackupMeleeSprintSpeed; /** Additional wave info to use during boss phase */ var() KFAIWaveInfo AdditionalBossWaveInfo; /** Frequency of additional wave spawn */ var() float AdditionalBossWaveFrequency; /** Delay until first wave. This should never be 0, and likely never less than ~5 seconds, it can potentially never spawn boss if it is. */ var() float AdditionalBossWaveStartDelay; /** 1 to max player count range of how many AI should spawn during the additional waves */ var() Vector2D AdditionalBossSpawnCount; /** Whether or not to spawn a continuous wave type every x seconds, or a single spawn every x seconds */ var() bool bContinuousAdditionalBossWave; /** Scale to use to increase or decrease crush damage */ var() float CrushScale; /** Scale to increase or decrease damage while jumping */ var() float JumpDamageScale; /** Amount of jumps the player can take */ var() int NumJumpsAllowed; /** Whether to turn on inflation rules within KFPawn_Monster */ var() bool bUseZedDamageInflation; /** Maximum pawn inflation (0 health) */ var() float ZeroHealthInflation; /** Deflation Percent Per Second */ var() float GlobalDeflationRate; /** Inflation death gravity */ var() float InflationDeathGravity; /** Inflation explosion timer */ var() float InflationExplosionTimer; /** Disable headless mode on a pawn */ var() bool bDisableHeadless; /** Maximum level of perk allowed to be in use. -1 = all off, 4 = level 25 */ var() byte MaxPerkLevel; /** Boom performance optimization - Max booms in one frame (avoids big Demo spikes) */ var() int MaxBoomsPerFrame; /** If another outbreak mode shares the same events, this will link the two to quicker UI lookup */ var() int WeeklyOutbreakId; structdefaultproperties { GameLength = GL_Short SpawnRateMultiplier = 1.0 GlobalAmmoCostScale = 1.0 PickupResetTime = PRS_Wave OverrideItemPickupModifier = -1.0 OverrideAmmoPickupModifier = -1.0 PermanentZedResetTime = 1.0 OverrideZedTimeSlomoScale = 0.2 StartingDamageSizeScale = 1.0 DeadDamageSizeScale = 0.1 OverrideSpawnDerateTime = -1.f; OverrideTeleportDerateTime = -1.f GlobalGravityZ = -1150 //Matches WorldInfo.uc default PlayerSpawnHeadScale = 1.0 ZedSpawnHeadScale = 1.0 bHumanSprintEnabled = true OffPerkCostScale = 1.0 bBackupMeleeSprintSpeed = false AdditionalBossWaveStartDelay = 15 bContinuousAdditionalBossWave = true CrushScale = 1.0 JumpDamageScale = 1.0 NumJumpsAllowed = 1 ZeroHealthInflation = 1.0 GlobalDeflationRate = 0.1 InflationExplosionTimer = 3.0 InflationDeathGravity = -0.1 MaxPerkLevel = 4 bAllowSpawnReplacementDuringBossWave = true WeeklyOutbreakId=INDEX_NONE } }; struct CachedOutbreakInfo { var KFGFxObject_TraderItems TraderItems; var float GameAmmoCostScale; var bool bAllowGrenadePurchase; var bool bTradersEnabled; var byte MaxPerkLevel; var float CachedWorldGravityZ; var float CachedGlobalGravityZ; structdefaultproperties { bTradersEnabled=true, bAllowGrenadePurchase=true GameAmmoCostScale=1.0 } }; /** List of events setup by design */ var array SetEvents; /** List of events that are in progress or have yet to make the cut */ var array TestEvents; /** Current event being used */ var WeeklyOverrides ActiveEvent; /** Stored values of World Info and GRI items incase we need to reset it. */ var CachedOutbreakInfo CachedItems; function SetActiveEvent(int ActiveEventIdx) { `if(`notdefined(ShippingPC)) local string LocalURL; `endif `if(`notdefined(ShippingPC)) //Runtime override by URL options for testing purposes LocalURL = WorldInfo.GetLocalURL(); LocalURL = Split(LocalURL, "?"); //remove map name ActiveEventIdx = GetIntOption(LocalURL, "ActiveEventIdx", ActiveEventIdx); //If our override is out of bounds, see if it's a valid test event if (ActiveEventIdx >= SetEvents.Length) { ActiveEventIdx -= SetEvents.Length; if (ActiveEventIdx <= TestEvents.Length && ActiveEventIdx >= 0) { ActiveEvent = TestEvents[ActiveEventIdx]; } } else { ActiveEvent = SetEvents[ActiveEventIdx]; } `else if(ActiveEventIdx < SetEvents.length) { ActiveEvent = SetEvents[ActiveEventIdx]; } `endif } function ClearActiveEvent() { local WeeklyOverrides EmptyEvent; WorldInfo.WorldGravityZ = CachedItems.CachedWorldGravityZ; WorldInfo.GlobalGravityZ = CachedItems.CachedGlobalGravityZ; if (GameReplicationInfo != none && KFGameReplicationInfo(GameReplicationInfo) != none) { if (ActiveEvent.TraderWeaponList != none) { KFGameReplicationInfo(GameReplicationInfo).TraderItems = CachedItems.TraderItems; } KFGameReplicationInfo(GameReplicationInfo).GameAmmoCostScale = CachedItems.GameAmmoCostScale; KFGameReplicationInfo(GameReplicationInfo).bAllowGrenadePurchase = CachedItems.bAllowGrenadePurchase; KFGameReplicationInfo(GameReplicationInfo).bTradersEnabled = CachedItems.bTradersEnabled; KFGameReplicationInfo(GameReplicationInfo).MaxPerkLevel = CachedItems.MaxPerkLevel; } ActiveEvent = EmptyEvent; } function CacheGRI() { CachedItems.CachedGlobalGravityZ = WorldInfo.GlobalGravityZ; CachedItems.CachedWorldGravityZ = WorldInfo.WorldGravityZ; if (GameReplicationInfo != none && KFGameReplicationInfo(GameReplicationInfo) != none) { if (ActiveEvent.TraderWeaponList != none) { CachedItems.TraderItems = KFGameReplicationInfo(GameReplicationInfo).TraderItems; } CachedItems.GameAmmoCostScale = KFGameReplicationInfo(GameReplicationInfo).GameAmmoCostScale; CachedItems.bAllowGrenadePurchase = KFGameReplicationInfo(GameReplicationInfo).bAllowGrenadePurchase; CachedItems.bTradersEnabled = KFGameReplicationInfo(GameReplicationInfo).bTradersEnabled; CachedItems.MaxPerkLevel = KFGameReplicationInfo(GameReplicationInfo).MaxPerkLevel; } } function UpdateGRI() { CacheGRI(); //This should have just been spawned in the super if (GameReplicationInfo != none && KFGameReplicationInfo(GameReplicationInfo) != none) { if (ActiveEvent.TraderWeaponList != none) { KFGameReplicationInfo(GameReplicationInfo).TraderItems = ActiveEvent.TraderWeaponList; } KFGameReplicationInfo(GameReplicationInfo).GameAmmoCostScale = ActiveEvent.GlobalAmmoCostScale; KFGameReplicationInfo(GameReplicationInfo).bAllowGrenadePurchase = !ActiveEvent.bDisableGrenades; KFGameReplicationInfo(GameReplicationInfo).bTradersEnabled = !ActiveEvent.bDisableTraders; KFGameReplicationInfo(GameReplicationInfo).MaxPerkLevel = ActiveEvent.MaxPerkLevel; } } function ModifyGroundSpeed(KFPawn PlayerPawn, out float GroundSpeed) { local KFWeapon KFW; local KFInventoryManager KFIM; if (ActiveEvent.bBackupMeleeSprintSpeed) { KFW = KFWeapon(PlayerPawn.Weapon); if (KFW == none) { KFIM = KFInventoryManager(PlayerPawn.InvManager); if (KFIM != none && KFIM.PendingWeapon != none) { KFW = KFWeapon(KFIM.PendingWeapon); } } if (KFW != None && KFW.IsMeleeWeapon() && KFW.bIsBackupWeapon) { GroundSpeed = PlayerPawn.default.SprintSpeed; } } } //In our case, this should be better explained as a GameInfo-facing AdjustDamage. Things are being done here that would be incredibly invasive in other classes given the size of our code base. function ReduceDamage(out int Damage, Pawn Injured, Controller InstigatedBy, class DamageType, TraceHitInfo HitInfo) { local int HitZoneIdx; local KFPawn InstigatorPawn; local StatAdjustments ToAdjust; //Some events can be headshot only. Do this only if the incoming damage is against a monster-derived class // and it's one of our custom damage types. Keeps things like crush damage from being scaled to 0. if (ActiveEvent.bHeadshotsOnly && KFPawn_Monster(Injured) != none && ClassIsChildOf(DamageType, class'KFDamageType')) { HitZoneIdx = KFPawn_Monster(Injured).HitZones.Find('ZoneName', HitInfo.BoneName); if (HitZoneIdx != HZI_Head) { Damage = 0; } } if (InstigatedBy != none) { InstigatorPawn = KFPawn(InstigatedBy.Pawn); //Do any instigator-based adjustments here if (InstigatorPawn != None) { if (ActiveEvent.JumpDamageScale != 1.0 && InstigatorPawn.bJumping) { Damage *= ActiveEvent.JumpDamageScale; } } } foreach ActiveEvent.ZedsToAdjust(ToAdjust) { //Injured zed reduction if (Injured.class == ToAdjust.ClassToAdjust) { Damage *= ToAdjust.DamageTakenScale; } if (InstigatorPawn != none && InstigatorPawn.class == ToAdjust.ClassToAdjust) { Damage *= ToAdjust.DamageDealtScale; } } } //Become Cartman function AdjustForBeefcakeRules(Pawn Pawn, optional BeefcakeType Type = EBT_Damage) { local float CurrentScale, PercentIncrease; local KFPawn_Monster KFP; local StatAdjustments StatAdjust; local float IntendedHeadScaling; local float OldHealthMax; KFP = KFPawn_Monster(Pawn); if (KFP != none) { foreach ActiveEvent.ZedsToAdjust(StatAdjust) { if (Pawn.class == StatAdjust.ClassToAdjust) { CurrentScale = KFP.IntendedBodyScale; //Set new body scale based on whether or not we rallied CurrentScale += StatAdjust.BeefcakeScaleIncreases[Type]; CurrentScale = FMin(CurrentScale, StatAdjust.MaxBeefcake); KFP.IntendedBodyScale = CurrentScale; if (StatAdjust.BeefcakeHealthIncreases[Type] > 0.f && !KFP.IsABoss()) { //Adjust their health by the specified amount PercentIncrease = StatAdjust.BeefcakeHealthIncreases[Type]; OldHealthMax = KFP.HealthMax; KFP.HealthMax += KFP.default.Health * PercentIncrease; KFP.HealthMax = Min(KFP.default.Health * StatAdjust.MaxBeefcakeHealth, KFP.HealthMax); //If we haven't capped health max, also adjust health if (OldHealthMax < KFP.HealthMax) { KFP.Health += KFP.default.Health * PercentIncrease; KFP.Health = Min(KFP.Health, KFP.HealthMax); } } //Heads in beefcake are supposed to stay at the original scale. Do that here. IntendedHeadScaling = 1.0 / CurrentScale; KFP.IntendedHeadScale = IntendedHeadScaling; KFP.SetHeadScale(IntendedHeadScaling, KFP.CurrentHeadScale); } } } } function AdjustScoreDamage(Controller InstigatedBy, Pawn DamagedPawn, class damageType) { if (ActiveEvent.bScaleOnHealth) { AdjustPawnScale(DamagedPawn); } } function AdjustPawnScale(Pawn Pawn) { local float ScalePercent; local KFPawn_Monster MonsterPawn; local KFPawn_Human HumanPawn; local int CurrentHealth; MonsterPawn = KFPawn_Monster(Pawn); HumanPawn = KFPawn_Human(Pawn); CurrentHealth = Max(Pawn.Health, 0); if (ActiveEvent.bScaleOnHealth) { if (MonsterPawn != none) { ScalePercent = ActiveEvent.StartingDamageSizeScale - (ActiveEvent.StartingDamageSizeScale - ActiveEvent.DeadDamageSizeScale) * (1 - (float(CurrentHealth) / float(Pawn.HealthMax))); MonsterPawn.IntendedBodyScale = ScalePercent; } //Humans can go > 100 health, only scale down if they fall below the 100 threshold for starting health else if (HumanPawn != none) { if (CurrentHealth > 100) { ScalePercent = ActiveEvent.StartingDamageSizeScale; } else { ScalePercent = ActiveEvent.StartingDamageSizeScale - (ActiveEvent.StartingDamageSizeScale - ActiveEvent.DeadDamageSizeScale) * (1 - (float(CurrentHealth) / 100.0)); } HumanPawn.IntendedBodyScale = ScalePercent; } } } function AdjustMonsterDefaults(out KFPawn_Monster P) { local StatAdjustments ToAdjust; if (P == none) { return; } //Mode globals P.IntendedHeadScale = ActiveEvent.ZedSpawnHeadScale; P.SetHeadScale(P.IntendedHeadScale, P.CurrentHeadScale); P.CrushScale = ActiveEvent.CrushScale; P.bDisableHeadless = ActiveEvent.bDisableHeadless; if (ActiveEvent.bUseZedDamageInflation) { P.bUseDamageInflation = true; P.ZeroHealthInflation = ActiveEvent.ZeroHealthInflation; P.DamageDeflationRate = ActiveEvent.GlobalDeflationRate; P.bDisableGoreMeshWhileAlive = true; P.InflationExplosionTimer = ActiveEvent.InflationExplosionTimer; P.InflateDeathGravity = ActiveEvent.InflationDeathGravity; } //Per class overrides foreach ActiveEvent.ZedsToAdjust(ToAdjust) { if (P.class == ToAdjust.ClassToAdjust) { P.Health *= ToAdjust.HealthScale; P.HealthMax *= ToAdjust.HealthScale; P.HitZones[HZI_HEAD].GoreHealth *= ToAdjust.HeadHealthScale; P.HitZones[HZI_HEAD].MaxGoreHealth = P.HitZones[HZI_HEAD].GoreHealth; P.SetShieldScale(ToAdjust.ShieldScale); if (ToAdjust.bStartEnraged) { //If we aren't using the AI controller's spawn enrage, go into the pawn if (KFAIController(P.Controller) == none || !KFAIController(P.Controller).SpawnEnraged()) { P.SetEnraged(true); } } if (ToAdjust.bExplosiveDeath && ToAdjust.ExplosionTemplate != none) { P.bUseExplosiveDeath = true; } if (ActiveEvent.bUseZedDamageInflation && (ToAdjust.OverrideDeflationRate.X > 0.0f || ToAdjust.OverrideDeflationRate.Y > 0.0f)) { P.DamageDeflationRate = Lerp(ToAdjust.OverrideDeflationRate.X, ToAdjust.OverrideDeflationRate.Y, FMax(NumPlayers, 1) / float(MaxPlayers)); } if (ToAdjust.AdditionalSubSpawns != none) { SpawnManager.SummonBossMinions(ToAdjust.AdditionalSubSpawns.Squads, Lerp(ToAdjust.AdditionalSubSpawnCount.X, ToAdjust.AdditionalSubSpawnCount.Y, FMax(NumPlayers, 1) / float(MaxPlayers))); } } } } function AdjustRestartedPlayer(out KFPawn_Human KFPH) { local KFInventoryManager KFIM; //Do human spawn time things if (KFPH != none) { KFPH.bAllowSprinting = ActiveEvent.bHumanSprintEnabled; KFPH.NumJumpsAllowed = ActiveEvent.NumJumpsAllowed; KFPH.IntendedHeadScale = ActiveEvent.PlayerSpawnHeadScale; KFPH.SetHeadScale(KFPH.IntendedHeadScale, KFPH.CurrentHeadScale); KFPH.bDisableTraderDialog = ActiveEvent.bDisableTraders; KFIM = KFInventoryManager(KFPH.InvManager); if (KFIM != none) { KFIM.OffPerkCostScale = ActiveEvent.OffPerkCostScale; } } } function OnScoreKill(Pawn KilledPawn) { } function ApplyGlobalDamage() { local KFPawn_Human Pawn; foreach WorldInfo.AllPawns(class'KFPawn_Human', Pawn) { Pawn.TakeDamage(OutbreakEvent.ActiveEvent.GlobalDamageTickAmount, none, Pawn.Location, vect(0, 0, 0), class'DmgType_Crushed'); } } function CacheWorldInfo() { if (WorldInfo != none) { CachedItems.CachedGlobalGravityZ = WorldInfo.GlobalGravityZ; CachedItems.CachedWorldGravityZ = WorldInfo.WorldGravityZ; } } function SetWorldInfoOverrides() { CacheWorldInfo(); if (WorldInfo != None) { WorldInfo.GlobalGravityZ = OutbreakEvent.ActiveEvent.GlobalGravityZ; WorldInfo.WorldGravityZ = WorldInfo.GlobalGravityZ; } } function class GetAISpawnOverrirde(EAIType AIType) { local SpawnReplacement Replacement; local float RandF; //Check if our current weekly event has any overrides available foreach ActiveEvent.SpawnReplacementList(Replacement) { if (Replacement.SpawnEntry == AIType) { if (Replacement.PercentChance < 1.f) { RandF = FRand(); //Let loop continue in case we have multiple possible replacements. if (RandF > Replacement.PercentChance) { continue; } } if (Replacement.NewClass.Length > 0) { return Replacement.NewClass[Rand(Replacement.NewClass.Length)]; } } } return AIClassList[AIType]; } // This will convert the provided Outbreak Index into an Id that corresponds to the Weekly Event. static function int GetOutbreakId(int SetEventsIndex); defaultproperties { }