KF2-StartWave/StartWave/Classes/StartWave.uc
2023-05-20 19:48:47 +03:00

516 lines
16 KiB
Ucode

class StartWave extends KFMutator
config(StartWave);
/*********************************************************************************************************
* Config properties
*********************************************************************************************************/
/** The wave that the match should start on. It is clamped between wave 1 and the boss wave. */
var config int StartWave;
/** The duration of the 'initial' trader (before the first wave). */
var config int InitialTraderTime;
/** The duration of standard trader (between waves). */
var config int TraderTime;
/** The starting dosh of players. */
var config int Dosh;
/** Whether an 'initial' trader time should occur before the first wave. */
var config bool bStartWithTrader;
/** Log level. */
var config E_LogLevel LogLevel;
/**
* The boss override index. For the default boss list, 0-Hans, 1-Patty, 2-King FP, 3-Abomination. Negative
* values can be used to keep the boss spawn random.
*/
var config int Boss;
/*********************************************************************************************************
* Instance variables
*********************************************************************************************************/
/** Used to determine whether the initial trader is still open / hasn't been closed. */
var bool bInitialTrader;
//These are used to determine whether everything in OverrideTimer has been done.
/** Whether the difficulty settings have been overriden. */
var bool bOverridenDifficultySettings;
/** Whether the trader duration has been overriden. */
var bool bOverridenTraderDuration;
function InitMutator(string Options, out string ErrorMessage)
{
//This needs to be called first since KFMutator.InitMutator sets the MyKFGI reference.
Super.InitMutator(Options, ErrorMessage);
//Parse options entered via the launch command.
//We further restrict StartWave later when we know the maximum wave number for the selected game length.
StartWave = Max(class'GameInfo'.static.GetIntOption(Options, "StartWave", StartWave), 1);
InitialTraderTime = Max(class'GameInfo'.static.GetIntOption(Options, "InitialTraderTime",
InitialTraderTime), 1);
TraderTime = Max(class'GameInfo'.static.GetIntOption(Options, "TraderTime", TraderTime), 1);
Dosh = Max(class'GameInfo'.static.GetIntOption(Options, "Dosh", Dosh), 0);
Boss = class'GameInfo'.static.GetIntOption(Options, "Boss", Boss);
bStartWithTrader = GetBoolOption(Options, "bStartWithTrader", bStartWithTrader);
LogLevel = E_LogLevel(class'GameInfo'.static.GetIntOption(Options, "LogLevel", LogLevel));
//DEBUG
`Log_Debug("StartWave: " $ StartWave);
`Log_Debug("InitialTraderTime: " $ InitialTraderTime);
`Log_Debug("TraderTime: " $ TraderTime);
`Log_Debug("Dosh: " $ Dosh);
`Log_Debug("Boss: " $ Boss);
`Log_Debug("bStartWithTrader: " $ bStartWithTrader);
bOverridenDifficultySettings = false;
bOverridenTraderDuration = false;
SetTimer(0.1, false, nameof(OverrideTimer));
//Override the boss with the boss corresponding to the specified boss index. -1 signifies random.
if(Boss != -1)
{
SetTimer(0.1, false, nameof(OverrideBoss));
}
CheckForceInitialTrader();
//We only care if this is the 'initial' trader time if we start with the trader active.
bInitialTrader = bStartWithTrader;
//If we want to start with the trader active or alter the starting wave number.
if(bStartWithTrader || StartWave > 1)
{
`Log_Debug("Calling StartWaveTimer() to alter the start wave or activate the trader initially.");
SetTimer(0.2, false, nameof(StartWaveTimer));
}
//If we will need to alter TimeBetweenWaves for later activations of the trader.
if(bStartWithTrader && InitialTraderTime != TraderTime)
{
`Log_Debug("Calling UpdateTraderDurationTimer() to alter the trader duration later.");
SetTimer(1, true, nameof(UpdateTraderDurationTimer));
}
}
/** Allows for handling player input in the console by the mutator. */
function Mutate(string MutateString, PlayerController Sender)
{
local array<string> CommandBreakdown;
if(MutateString == "")
{
return;
}
//Split the string on the space character.
ParseStringIntoArray(MutateString, CommandBreakdown, " ", true);
//The CheatManager check is equivalent to checking if cheats are enabled for that player.
if(CommandBreakdown.Length > 1 && CommandBreakdown[0] == "setwave" && Sender.CheatManager != None &&
MyKFGI.GetLivingPlayerCount() > 0)
{
//The setwave command should be: mutate setwave WaveNum bSkipTraderTime
//where WaveNum is an integer (or byte) and bSkipTraderTime is a bool.
if(CommandBreakdown.Length == 2)
{
SetWave(int(CommandBreakdown[1]), Sender);
}
else
{
SetWave(int(CommandBreakdown[1]), Sender, bool(CommandBreakdown[2]));
}
}
Super.Mutate(MutateString, Sender);
}
/** Jumps to the specified wave, NewWaveNum, with trader time iff bSkipTraderTime is false. */
function SetWave(int NewWaveNum, PlayerController PC, optional bool bSkipTraderTime)
{
if(NewWaveNum < 1)
{
`Log_Error("SetWave: new wave num must be > 0.");
return;
}
if(KFGameInfo_Endless(MyKFGI) != None)
{
//Jump straight to the final wave if the specified wave number is higher than wave max.
if(NewWaveNum > 254)
{
NewWaveNum = 254;
}
}
else
{
//Jump straight to the boss wave if the specified wave number is higher than wave max.
if(NewWaveNum > KFGameInfo_Survival(MyKFGI).WaveMax)
{
NewWaveNum = KFGameInfo_Survival(MyKFGI).WaveMax+1;
}
}
KFGameInfo_Survival(MyKFGI).WaveNum = NewWaveNum - 1;
//Kill all zeds currently alive.
PC.ConsoleCommand("KillZeds");
//Clear any current objectives.
MyKFGI.MyKFGRI.DeactivateObjective();
if(bSkipTraderTime)
{
//Go to some unused state so that PlayingWave.BeginState is called when we go to PlayingWave.
MyKFGI.GotoState('TravelTheWorld');
UpdateEndlessDifficulty();
//Go to PlayingWave to start the new wave.
MyKFGI.GotoState('PlayingWave');
}
else
{
//Go to trader time before starting the new wave.
MyKFGI.GotoState('TraderOpen');
UpdateEndlessDifficulty();
}
MyKFGI.ResetAllPickups();
}
/**
* Since the difficulty in Endless scales with the wave number, we need to update the difficulty when
* jumping between wave numbers to match the expected difficulty.
*/
function UpdateEndlessDifficulty()
{
local KFGameInfo_Endless Endless;
local int i;
Endless = KFGameInfo_Endless(MyKFGI);
if(Endless == None)
{
return;
}
//Reflects the difficulty update in KFGameInfo_Endless.SetWave.
Endless.bIsInHoePlus = false;
Endless.ResetDifficulty();
Endless.SpawnManager.GetWaveSettings(Endless.SpawnManager.WaveSettings);
Endless.UpdateGameSettings();
//Don't bother iterating for i=0-4, no difficulty increment can occur.
for(i = 5; i < Endless.WaveNum; ++i)
{
//Simulate the death of a boss. The difficulty is incremented after each boss round.
if(i % 5 == 0)
{
Endless.IncrementDifficulty();
}
//This should happen at the end of each wave (if we're in HoE+). The check is handled internally.
//We do this after the simulation of a boss death so that bIsInHoePlus can be set first.
Endless.HellOnEarthPlusRoundIncrement();
}
}
/** Checks whether we should force the initial trader, regardless of the config/command value. */
function CheckForceInitialTrader()
{
//Force the initial trader for compatibility with holdout maps. Otherwise, zeds spawn in the wrong room.
if(!bStartWithTrader && StartWave > 1)
{
bStartWithTrader = true;
InitialTraderTime = 1.0;
}
}
/** Overrides the boss to spawn if a valid boss index has been specified. */
function OverrideBoss()
{
local bool bHalt;
local byte MaxIters, i, MaxSameIters, PrevIndex, SameIters;
//We need a valid KFGRI reference as we use its public BossIndex field.
if(MyKFGI.MyKFGRI == None)
{
SetTimer(0.2, false, nameof(OverrideBoss));
return;
}
//Unfortunately, we cannot directly set the boss index since KFGameInfo.BossIndex is protected. The only
//way we can affect BossIndex is through KFGameInfo.SetBossIndex which randomly chooses a value in the
//valid range. So we have to continue calling SetBossIndex until the desired index has been chosen. We
//can verify this by checking KFGameReplicationInfo.BossIndex because that is public.
i = 0;
MaxIters = 100;
//Since some events/maps could force a specific boss to be spawned (see KFGameInfo.SetBossIndex), we
//should check whether the index hasn't changed after several iterations. If it stays the same for a
//while we assume the index is forced, in which case we can't do anything about it.
SameIters = 0;
MaxSameIters = 10;
PrevIndex = MyKFGI.MyKFGRI.BossIndex;
bHalt = Boss < 0 || MyKFGI.MyKFGRI.BossIndex == Boss;
while(!bHalt)
{
++i;
//Randomly select a new boss.
MyKFGI.SetBossIndex();
//Track whether the boss index is changing.
if(MyKFGI.MyKFGRI.BossIndex == PrevIndex)
{
++SameIters;
}
else
{
SameIters = 0;
PrevIndex = MyKFGI.MyKFGRI.BossIndex;
}
//Halt if we have the desired index or we have tried enough times.
bHalt = MyKFGI.MyKFGRI.BossIndex == Boss || SameIters >= MaxSameIters || i >= MaxIters;
}
if(MyKFGI.MyKFGRI.BossIndex == Boss)
{
`Log_Debug("Successfully overrode boss index to" @ Boss @ "after" @ i @ "attempts.");
}
else
{
`Log_Debug("Failed to override boss index after" @ i @ "attempts.");
}
}
/** Overrides difficulty settings and trader duration when possible. */
function OverrideTimer()
{
local KFGameInfo_Survival KFGI_Surv;
local KFGameInfo_Endless KFGI_Endl;
local KFGameDifficulty_Endless KFGD_Endl;
local int i;
//If we've overriden what we need to, don't call this timer again.
if(bOverridenDifficultySettings && bOverridenTraderDuration)
{
`Log_Debug("All settings have been overriden.");
return;
}
if(!bOverridenDifficultySettings && MyKFGI.DifficultyInfo != None)
{
`Log_Debug("Overriding difficulty settings...");
bOverridenDifficultySettings = true;
//Override starting dosh.
MyKFGI.DifficultyInfo.Normal.StartingDosh = Dosh;
MyKFGI.DifficultyInfo.Hard.StartingDosh = Dosh;
MyKFGI.DifficultyInfo.Suicidal.StartingDosh = Dosh;
MyKFGI.DifficultyInfo.HellOnEarth.StartingDosh = Dosh;
KFGI_Endl = KFGameInfo_Endless(MyKFGI);
if (KFGI_Endl != None)
{
KFGD_Endl = KFGameDifficulty_Endless(KFGI_Endl.DifficultyInfo);
if (KFGD_Endl != None)
{
for (i = 0; i < KFGD_Endl.CurrentDifficultyScaling.Difficulties.length; ++i)
{
KFGD_Endl.CurrentDifficultyScaling.Difficulties[i].StartingDosh = Dosh;
}
}
}
`Log_Debug("Starting dosh has been set to:" @ Dosh @ "dosh.");
//We need to set the difficulty settings again - normally done in KFGameInfo.InitGame - to apply
//these changes, since this happens after InitGame is executed.
MyKFGI.DifficultyInfo.SetDifficultySettings(MyKFGI.GameDifficulty);
}
//Set the starting wave number.
if(!bOverridenTraderDuration)
{
KFGI_Surv = KFGameInfo_Survival(MyKFGI);
if(KFGI_Surv != None)
{
//We require the SpawnManager to be set, because this signifies that InitSpawnManager has been
//executed, which sets WaveMax.
if(MyKFGI.SpawnManager != None)
{
`Log_Debug("Overriding trader duration...");
bOverridenTraderDuration = true;
//Since InitSpawnManager has been executed, then PreBeginPlay must have been executed. This
//means that PostBeginPlay will have been executed as well since it happens straight after.
//Now we can override TimeBetweenWaves.
KFGI_Surv.TimeBetweenWaves = bInitialTrader ? InitialTraderTime : TraderTime;
`Log_Debug("Trader duration has been set to:" @ KFGI_Surv.TimeBetweenWaves @ "seconds.");
}
else
{
`Log_Debug("MyKFGI.SpawnManager hasn't been set yet. Calling StartWaveTimer again.");
//We don't know WaveMax yet, so we need to wait longer.
SetTimer(0.1, false, nameof(StartWaveTimer));
return;
}
}
else
{
`Log_Warn("The game mode does not extend KFGameInfo_Survival. Most features of this mutator are not compatible with non-wave-based game modes.");
}
}
//If the difficulty info isn't set yet, wait.
SetTimer(0.1, false, nameof(OverrideTimer));
}
function StartWaveTimer()
{
local KFGameInfo_Survival KFGI_Surv;
local PlayerController PC;
//We need to wait for the wave to be active, as this will signify that StartMatch has been executed.
if(!MyKFGI.IsWaveActive())
{
//If the wave isn't active yet (probably still in lobby), wait.
SetTimer(0.1, false, nameof(StartWaveTimer));
return;
}
KFGI_Surv = KFGameInfo_Survival(MyKFGI);
if(KFGI_Surv == None)
{
return;
}
`Log_Debug("Clearing the current wave.");
//Clear the current wave.
foreach WorldInfo.AllControllers(class'PlayerController', PC)
{
if (KFDemoRecSpectator(PC) == none)
{
PC.ConsoleCommand("KillZeds");
break;
}
}
//Set the starting wave number.
//Keep the assignments separated so that we can used the restricted StartWave later if we want.
StartWave = Min(StartWave, KFGI_Surv.WaveMax);
//We need to subtract 1 because when the state is eventually reset to PlayingWave, this will be
//incremented by 1.
KFGI_Surv.WaveNum = StartWave - 1;
`Log_Debug("WaveNum set to:" @ KFGI_Surv.WaveNum);
if(bStartWithTrader)
{
`Log_Debug("Switching to state: TraderOpen.");
//We need to update GRI's WaveNum and update the HUD element that shows the last wave.
MyKFGI.MyKFGRI.WaveNum = KFGI_Surv.WaveNum;
MyKFGI.MyKFGRI.UpdateHUDWaveCount();
//Start with the trader active.
MyKFGI.GotoState('TraderOpen', 'Begin');
}
else
{
`Log_Debug("Switching to state: PlayingWave.");
//Start with a wave as usual - but our StartWave number will be used.
MyKFGI.GotoState('PlayingWave');
}
//Since we've updated the wave number, we need to update the game settings (which includes the
//current wave number).
MyKFGI.UpdateGameSettings();
bInitialTrader = false;
}
/** Updates the trader duration. Waits until the initial trader has closed. */
function UpdateTraderDurationTimer()
{
//If the initial trader has already been opened, and the wave is now active.
if(!bInitialTrader && MyKFGI.IsWaveActive())
{
if(KFGameInfo_Survival(MyKFGI) != None)
{
`Log_Debug("Updating trader duration to" @ TraderTime @ "seconds.");
//We can update TimeBetweenWaves to be the TraderTime we specified in the launch command.
KFGameInfo_Survival(MyKFGI).TimeBetweenWaves = TraderTime;
}
else
{
`Log_Warn("The game mode does not extend KFGameInfo_Survival. Most features of this mutator are not compatible with non-wave-based game modes.");
}
//We don't need to call this timer again.
ClearTimer(nameof(UpdateTraderDurationTimer));
}
}
/**
* @brief Gets a bool from the launch command if available.
*
* @param Options - options passed in via the launch command
* @param ParseString - the variable we are looking for
* @param CurrentValue - the current value of the variable
* @return bool value of the option we are looking for
*/
static function bool GetBoolOption(string Options, string ParseString, bool CurrentValue)
{
local string InOpt;
//Find the value associated with this variable in the launch command.
InOpt = class'GameInfo'.static.ParseOption(Options, ParseString);
if(InOpt != "")
{
return bool(InOpt);
}
//If a value for this variable was not specified in the launch command, return the original value.
return CurrentValue;
}
/**
* @brief Gets a string from the launch command if available.
*
* @param Options - options passed in via the launch command
* @param ParseString - the variable we are looking for
* @param CurrentValue - the current value of the variable
* @return string value of the option we are looking for
*/
static function string GetStringOption(string Options, string ParseString, string CurrentValue)
{
local string InOpt;
//Find the value associated with this variable in the launch command.
InOpt = class'GameInfo'.static.ParseOption(Options, ParseString);
if(InOpt != "")
{
return InOpt;
}
//If a value for this variable was not specified in the launch command, return the original value.
return CurrentValue;
}