1
0
KF2-Dev-Scripts/KFGame/Classes/KFTraderDialogManager.uc

783 lines
19 KiB
Ucode
Raw Permalink Normal View History

2020-12-13 15:01:13 +00:00
//=============================================================================
// KFTraderDialogManager
//=============================================================================
// Manager class for trader dialog
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Jeff Robinson
//=============================================================================
class KFTraderDialogManager extends Actor
dependson(KFTraderVoiceGroupBase);
`include(KFGame/KFGameDialog.uci)
var bool bEnabled;
struct TraderDialogCoolDownInfo
{
var int EventID;
var int Priority;
var float EndTime;
};
var transient array< TraderDialogCoolDownInfo > CoolDowns;
/** The voice class contains the actual AkEvents to play (allows us to swap out different voices) */
var transient class<KFTraderVoiceGroupBase> TraderVoiceGroupClass;
/** The event currently being spoken by the trader */
var transient TraderDialogEventInfo ActiveEventInfo;
/** variables used to determine what should be said by trader */
var float FewZedsDeadPct;
var float ManyZedsDeadPct;
var float FarFromTraderDistance;
var float NeedHealPct;
var float TeammateNeedsHealPct;
var int ShareDoshAmount;
var int LowDoshAmount;
/** Delay trader open dialog a short time so we don't overlap wave end sounds */
var float WaveClearDialogDelay;
/** Event to play at end of delay. Cached after MatchStats are recieved, but before they are reset */
var int DelayedWaveClearEventID;
/** Used as SetTimer callback. Set by PlayDialog. */
simulated function EndOfDialogTimer()
{
local TraderDialogCoolDownInfo CoolDownInfo;
CoolDownInfo.EventID = ActiveEventInfo.EventID;
CoolDownInfo.Priority = ActiveEventInfo.Priority;
CoolDownInfo.EndTime = WorldInfo.TimeSeconds + ActiveEventInfo.CoolDown;
CoolDowns.AddItem( CoolDownInfo );
ActiveEventInfo.AudioCue = none;
}
/** Whether given event is cooling down */
simulated function bool DialogIsCoolingDown( int EventID )
{
local int CoolDownIndex;
CoolDownIndex = CoolDowns.Find('EventID', EventID);
if( CoolDownIndex == INDEX_NONE )
{
return false;
}
if( CoolDowns[CoolDownIndex].EndTime <= WorldInfo.TimeSeconds )
{
CoolDowns.Remove( CoolDownIndex, 1 );
return false;
}
return true;
}
/** Whether the dialog should play, given the percent chance. */
simulated function bool ShouldDialogPlay(int EventID)
{
local float PercentChance;
PercentChance = TraderVoiceGroupClass.default.DialogEvents[EventID].Chance;
if (PercentChance >= 1.f)
{
return true;
}
if (FRand() <= PercentChance)
{
return true;
}
return false;
}
/** Plays dialog event and replicates it as necessary */
simulated function PlayDialog( int EventID, Controller C, bool bInterrupt = false )
{
local KFPawn_Human KFPH;
// safeguard - don't play audio on dedicated server because we can't anyway
if( WorldInfo.NetMode == NM_DedicatedServer )
{
`log("Failed to play dialog id " $ EventId $ ". Tried to play on dedicated server.");
return;
}
if (C == none)
{
`log("Failed to play dialog id " $ EventId $ ". Controller is null.");
return;
}
// safeguard - only play trader dialog for local players
if( !C.IsLocalController() )
{
`log("Failed to play dialog id " $ EventId $ ". Controller is not local");
return;
}
if( !bEnabled || TraderVoiceGroupClass == none )
{
`log("Failed to play dialog id " $ EventId $ ". Manager is disabled or trader voice group class is none.");
return;
}
if( EventID < 0 || EventID >= `TRADER_DIALOG_COUNT )
{
`log("Failed to play dialog id " $ EventId $ ". Event id is outside of range of 0 and " $ (`TRADER_DIALOG_COUNT - 1));
return;
}
if( C.Pawn == none || !C.Pawn.IsAliveAndWell() )
{
`log("Failed to play dialog id " $ EventId $ ". Controller's pawn is either null or dead.");
return;
}
// don't interrupt active dialog
if( ActiveEventInfo.AudioCue != none && !bInterrupt )
{
`log("Failed to play dialog id " $ EventId $ ". There is already another audio cue going and we can't interrupt.");
return;
}
// don't speak same dialog again too soon
if( DialogIsCoolingDown(EventID) )
{
`log("Failed to play dialog id " $ EventId $ ". Event id is still cooling down.");
return;
}
if (!ShouldDialogPlay(EventID))
{
`log("Failed to play dialog id " $ EventId $ ". Failed random chance to play.");
return;
}
KFPH = KFPawn_Human( C.Pawn );
if( KFPH != none )
{
if (bInterrupt)
{
KFPH.StopTraderDialog();
}
ActiveEventInfo = TraderVoiceGroupClass.default.DialogEvents[ EventID ];
//`log("Play Dialog" @ ActiveEventInfo.EventID @ ActiveEventInfo.AudioCue);
KFPH.PlayTraderDialog( ActiveEventInfo.AudioCue );
SetTimer( ActiveEventInfo.AudioCue.Duration, false, nameof(EndOfDialogTimer) );
}
}
/** Plays a synced dialog event for every player in the game.
Called on server.
*/
static function PlayGlobalDialog( int EventID, WorldInfo WI, bool bInterrupt = false )
{
local Controller C;
local KFPlayerController KFPC;
foreach WI.AllControllers(class'Controller', C)
{
if( C.bIsPlayer )
{
KFPC = KFPlayerController( C );
if( KFPC == none )
{
continue;
}
if( KFPC.IsLocalController() )
{
//`log("Play Global Dialog " $ EventID $ ": Play Trader Dialog");
KFPC.PlayTraderDialog( EventID, bInterrupt );
}
else
{
//`log("Play Global Dialog " $ EventID $ ": Play Client Trader Dialog");
KFPC.ClientPlayTraderDialog( EventID, bInterrupt );
}
}
}
}
/** Plays wave progress event for every player in the game.
Called on server.
*/
static function PlayGlobalWaveProgressDialog( int ZedsRemaining, int ZedsTotal, WorldInfo WI )
{
local float ZedDeadPct, PrevZedDeadPct;
if ( ZedsTotal == 0 )
return;
ZedDeadPct = 1.f - (float(ZedsRemaining) / float(ZedsTotal));
PrevZedDeadPct = 1.f - (float(ZedsRemaining+1) / float(ZedsTotal));
if( PrevZedDeadPct < default.ManyZedsDeadPct && ZedDeadPct >= default.ManyZedsDeadPct )
{
PlayGlobalDialog( `TRAD_Wave80pctDead, WI );
}
else if( PrevZedDeadPct < default.FewZedsDeadPct && ZedDeadPct >= default.FewZedsDeadPct )
{
PlayGlobalDialog( `TRAD_Wave20pctDead, WI );
}
}
/** Plays dialog when trader time begins */
simulated function PlayOpenTraderDialog( int WaveNum, int WaveMax, Controller C )
{
local KFGameReplicationInfo KFGRI;
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
if( KFGRI != none && KFGRI.IsBossWaveNext() )
{
PlayDialog( `TRAD_FinalShopWave, C );
}
else if (KFGRI != none && KFGRI.bEndlessMode && KFGRI.IsBossWave())
{
PlayDialog(`TRAD_PATTY_KILLBOSS_1 + Clamp((WaveNum / 5), 0, 14), C);
}
else
{
PlayDialog( `TRAD_WaveLastZedDies, C );
}
}
/** Plays dialog when player enters trader trigger.
Called on server.
*/
static function PlayApproachTraderDialog( Controller C )
{
local KFPlayerController KFPC;
KFPC = KFPlayerController( C );
if( KFPC == none )
{
return;
}
if( KFPC.IsLocalController() )
{
KFPC.PlayTraderDialog( `TRAD_PlayerArrive );
}
else
{
KFPC.ClientPlayTraderDialog( `TRAD_PlayerArrive );
}
}
/** Plays dialog when trader time ends */
simulated function PlayCloseTraderDialog( Controller C )
{
local KFGameReplicationInfo KFGRI;
KFGRI = KFGameReplicationInfo(C.WorldInfo.GRI);
if(KFGRI == none || !KFGRI.bEndlessMode)
{
// Only play trader close dialog for non-endless game modes.
PlayDialog( `TRAD_Close, C );
}
}
/** Modifies "BestOptionID" based on "NumOptions" to represent a random event */
simulated function AddRandomOption( int OptionID, out byte NumOptions, out int BestOptionID )
{
local TraderDialogEventInfo NewOptionEventInfo, BestOptionEventInfo;
if( OptionID < 0 || TraderVoiceGroupClass == none )
{
return;
}
if( BestOptionID >= 0 )
{
NewOptionEventInfo = TraderVoiceGroupClass.default.DialogEvents[OptionID];
BestOptionEventInfo = TraderVoiceGroupClass.default.DialogEvents[BestOptionID];
if( NewOptionEventInfo.Priority < BestOptionEventInfo.Priority )
{
NumOptions = 0;
}
else if( NewOptionEventInfo.Priority > BestOptionEventInfo.Priority )
{
return;
}
}
NumOptions++;
if( Frand() <= 1.f / float(NumOptions) )
{
BestOptionID = OptionID;
}
}
/** called once per second from KFGameReplicationInfo Timer(), which is simulated */
simulated function PlayTraderTickDialog( int RemainingTime, Controller C, WorldInfo WI )
{
local Pawn P;
local KFPawn_Human KFPH, Teammate;
local KFGameReplicationInfo KFGRI;
local KFPlayerController KFPC;
local KFMapInfo KFMI;
local int BestOptionID;
local byte NumOptions;
if( !default.bEnabled )
{
return;
}
if( RemainingTime == 30 )
{
PlayDialog( `TRAD_30SecLeft, C );
return;
}
if( RemainingTime == 10 )
{
PlayDialog( `TRAD_10SecLeft, C );
return;
}
KFPC = KFPlayerController( C );
if( KFPC == none )
{
return;
}
if( KFPC.bClientTraderMenuOpen )
{
return;
}
KFPH = KFPawn_Human( C.Pawn );
if( KFPH == none )
{
return;
}
BestOptionID = INDEX_NONE;
if( KFPH.GetHealthPercentage() < default.NeedHealPct )
{
// need healing
AddRandomOption( `TRAD_NeedToHeal, NumOptions, BestOptionID );
}
KFGRI = KFGameReplicationInfo(WI.GRI);
if( KFGRI != none && KFGRI.OpenedTrader != none )
{
KFMI = KFMapInfo( WorldInfo.GetMapInfo() );
if( (KFMI == none || KFMI.SubGameType != ESGT_Descent)
&& VSize(KFGRI.OpenedTrader.Location - KFPH.Location) >= default.FarFromTraderDistance )
{
// far from trader
AddRandomOption( `TRAD_PlayerFarAway, NumOptions, BestOptionID );
}
}
for( P = WI.PawnList; P != none; P = P.NextPawn )
{
if( KFPH == P )
{
continue;
}
if( P.GetTeamNum() != 0 )
{
continue;
}
if( !P.IsAliveAndWell() )
{
continue;
}
Teammate = KFPawn_Human( P );
if( Teammate == none )
{
continue;
}
if( Teammate.GetHealthPercentage() < default.TeammateNeedsHealPct )
{
// teammates needs healing
AddRandomOption( `TRAD_TeamNeedsHeal, NumOptions, BestOptionID );
break;
}
}
if(BestOptionID != INDEX_NONE)
{
PlayDialog( BestOptionID, C );
}
}
/** Plays dialog when trader time begins based on performance last wave */
simulated function PlayBeginTraderTimeDialog( KFPlayerController KFPC )
{
if( !default.bEnabled )
{
return;
}
if( KFPC.PWRI.bDiedDuringWave )
{
PlayPlayerDiedLastWaveDialog( KFPC );
}
else
{
PlayPlayerSurvivedLastWaveDialog( KFPC );
}
}
/** Called from PlayBeginTraderTimeDialog if player died last wave */
simulated function PlayPlayerDiedLastWaveDialog( KFPlayerController KFPC )
{
local int BestOptionID;
local byte NumOptions;
BestOptionID = -1;
// killed by specific zed last wave
if( KFPC.PWRI.ClassKilledByLastWave != none )
{
AddRandomOption( KFPC.PWRI.ClassKilledByLastWave.static.GetTraderAdviceID(), NumOptions, BestOptionID );
}
if( KFPC.MatchStats.DeathStreak >= 3 )
{
// keep dying
AddRandomOption( `TRAD_KeepDying, NumOptions, BestOptionID );
}
else
{
// died last wave
AddRandomOption( `TRAD_DiedLastWave, NumOptions, BestOptionID );
}
PlayWaveClearDialog(BestOptionID, KFPC);
}
/** Called from PlayBeginTraderTimeDialog if player survived last wave */
simulated function PlayPlayerSurvivedLastWaveDialog( KFPlayerController KFPC )
{
local int BestOptionID;
local byte NumOptions;
local KFPawn_Human KFPH;
BestOptionID = -1;
KFPH = KFPawn_Human( KFPC.Pawn );
if( KFPH == none )
{
return;
}
if( KFPC.MatchStats.GetHealGivenInWave() >= 200 )
{
// good job healing
AddRandomOption( `TRAD_GoodJobHeal, NumOptions, BestOptionID );
}
if( KFPC.MatchStats.SurvivedStreak >= 3 )
{
// didn't die multiple waves
AddRandomOption( `TRAD_SurviveMultWaves, NumOptions, BestOptionID );
}
if (KFPC.MatchStats.GetDamageTakenInWave() == 0)
{
// no damage taken
AddRandomOption(`TRAD_NoDamage, NumOptions, BestOptionID);
}
if (KFPH.HealthMax != KFPH.Health)
{
if (KFPC.MatchStats.GetDamageTakenInWave() < KFPH.HealthMax / 2)
{
// less than half damage taken
AddRandomOption(`TRAD_LT50PctDamTaken, NumOptions, BestOptionID);
}
if (KFPC.MatchStats.GetDamageTakenInWave() >= 300)
{
// took lots of damage but didn't die
AddRandomOption(`TRAD_HighDmgSurvivor, NumOptions, BestOptionID);
}
if (KFPC.MatchStats.GetDamageTakenInWave() >= 100 && KFPC.MatchStats.GetHealReceivedInWave() >= 100)
{
// took lots of damage and got healed a lot
AddRandomOption(`TRAD_HighDmgHighHeal, NumOptions, BestOptionID);
}
}
if( KFPC.PWRI.bKilledFleshpoundLastWave )
{
// killed fp
AddRandomOption( `TRAD_KilledFleshpound, NumOptions, BestOptionID );
}
if( KFPC.PWRI.bKilledScrakeLastWave )
{
// killed scrake
AddRandomOption( `TRAD_KilledScrake, NumOptions, BestOptionID );
}
if( KFPH.PlayerReplicationInfo.Score < default.LowDoshAmount )
{
// low dosh
AddRandomOption( `TRAD_LittleDosh, NumOptions, BestOptionID );
}
// Team based dialog options (skip in solo)
if ( !IsSoloHumanPlayer() )
{
if( KFPC.PWRI.bKilledMostZeds )
{
// killed most zeds
AddRandomOption( `TRAD_KilledMost, NumOptions, BestOptionID );
}
if( KFPC.PWRI.bEarnedMostDosh )
{
// earned most dosh last wave
AddRandomOption( `TRAD_EarnMostDosh, NumOptions, BestOptionID );
}
if( KFPC.PWRI.bBestTeammate )
{
AddRandomOption( `TRAD_BestTeamPlayer, NumOptions, BestOptionID );
}
if( KFPH.PlayerReplicationInfo.Score >= default.ShareDoshAmount )
{
// you're rich, share dosh
AddRandomOption( `TRAD_ShareDosh, NumOptions, BestOptionID );
}
if( KFPC.PWRI.bAllSurvivedLastWave )
{
// no one died
AddRandomOption( `TRAD_NoOneDies, NumOptions, BestOptionID );
}
else if( KFPC.PWRI.bSomeSurvivedLastWave )
{
// a view teammates died
AddRandomOption( `TRAD_SomeDie, NumOptions, BestOptionID );
}
else if( KFPC.PWRI.bOneSurvivedLastWave )
{
// whole team died except you
AddRandomOption( `TRAD_OnlySurvivor, NumOptions, BestOptionID );
}
}
PlayWaveClearDialog(BestOptionID, KFPC);
}
/** Helper function to count human team players on the client */
function bool IsSoloHumanPlayer()
{
local int i, NumHumans;
local PlayerReplicationInfo PRI;
// trivial case
if ( WorldInfo.NetMode == NM_StandAlone )
{
return true;
}
// Count human team members by adding up active PRI's. TeamInfo.Size not replicated?
for ( i = 0; i < WorldInfo.GRI.PRIArray.Length && NumHumans < 2; i++)
{
PRI = WorldInfo.GRI.PRIArray[i];
if ( PRI != None && !PRI.bOnlySpectator && PRI.GetTeamNum() == 0 )
{
NumHumans++;
}
}
return (NumHumans == 1);
}
/**
* Trigger a delayed dialog event when trader opens. Delay should be long enough to bypass
* wave end sounds. Need to cache the EventId, because MatchStats may not have valid data
* by the time the timer expires.
*/
simulated function PlayWaveClearDialog( int EventID, Controller C )
{
if( C != None && C.IsLocalController() )
{
DelayedWaveClearEventID = EventID;
SetTimer(WaveClearDialogDelay, false, nameof(WaveClearDialogTimer));
}
}
/** Actually play sound after timer expires */
simulated function WaveClearDialogTimer()
{
PlayDialog( DelayedWaveClearEventID, WorldInfo.GetALocalPlayerController() );
}
/** Plays dialog when player "uses" trader and opens trader menu */
simulated function PlayOpenTraderMenuDialog( KFPlayerController KFPC )
{
local KFPawn_Human KFPH;
local KFGameReplicationInfo KFGRI;
local float ArmorPct, AmmoMax, AmmoPct;
local int BestOptionID;
local byte NumOptions;
BestOptionID = -1;
KFPH = KFPawn_Human( KFPC.Pawn );
if( KFPH != none )
{
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
if (KFGRI == none || KFGRI.WaveNum > 1)
{
ArmorPct = float(KFPH.Armor) / float(KFPH.MaxArmor);
if (ArmorPct <= 0.f)
{
// no armor
AddRandomOption(`TRAD_NoArmor, NumOptions, BestOptionID);
}
else if (ArmorPct < 0.3f)
{
// low armor
AddRandomOption(`TRAD_LowArmor, NumOptions, BestOptionID);
}
}
// only play low ammo dialog if the weapon uses ammo
AmmoMax = float(KFPH.MyKFWeapon.GetMaxAmmoAmount(0));
if( AmmoMax > 0 )
{
AmmoPct = KFPH.MyKFWeapon.GetAmmoPercentage(0);
if( AmmoPct < 0.3f )
{
// low ammo
AddRandomOption( `TRAD_LowAmmo, NumOptions, BestOptionID );
}
}
if( !KFPH.MyKFWeapon.HasAmmo(4) )
{
// no grenades
AddRandomOption( `TRAD_NoNades, NumOptions, BestOptionID );
}
}
PlayDialog( BestOptionID, KFPC );
}
/** Plays dialog related to current objective */
simulated function PlayObjectiveDialog( Controller C, int ObjDialogID )
{
PlayDialog( ObjDialogID, C );
}
function PlayLastManStandingDialog( WorldInfo WI )
{
PlayGlobalDialog( `TRAD_LastManStanding, WI );
}
/** Plays dialog if selected item in trader menu is unobtainable */
simulated function PlaySelectItemDialog( Controller C, bool bTooExpensive, bool bTooHeavy )
{
local int BestOptionID;
local byte NumOptions;
BestOptionID = -1;
if( bTooExpensive )
{
AddRandomOption( `TRAD_ClickTooExpensive, NumOptions, BestOptionID );
}
if( bTooHeavy )
{
AddRandomOption( `TRAD_ClickTooHeavy, NumOptions, BestOptionID );
}
PlayDialog( BestOptionID, C );
}
static function BroadcastEndlessStartWaveDialog(int WaveNum, int ModeIndex, WorldInfo WI)
{
local int SpecialWaveEventId, NormalWaveEventId;
local Controller C;
local KFPlayerController KFPC;
SpecialWaveEventId = INDEX_NONE;
NormalWaveEventId = INDEX_NONE;
if (ModeIndex != INDEX_NONE)
{
SpecialWaveEventId = `TRAD_SPECIALWAVE_1 + (ModeIndex % 3);
}
if (WaveNum > 100)
{
NormalWaveEventId = `TRAD_PATTY_WAVE_100Plus;
}
else
{
NormalWaveEventId = `TRAD_PATTY_WAVE_1 + (WaveNum - 1);
}
// Play specific global dialog
// We can't just call "PlayGlobalDialog" because we need to do some extra logic client-side, where the trader voice group lives
foreach WI.AllControllers(class'Controller', C)
{
if( C.bIsPlayer )
{
KFPC = KFPlayerController( C );
if( KFPC == none )
{
continue;
}
if( KFPC.IsLocalController() )
{
KFPC.PlayTraderEndlessWaveStartDialog(SpecialWaveEventId, NormalWaveEventId);
}
else
{
KFPC.ClientPlayTraderEndlessWaveStartDialog(SpecialWaveEventId, NormalWaveEventId);
}
}
}
}
static function PlayFirstWaveStartDialog(WorldInfo WI)
{
PlayGlobalDialog(`TRAD_FirstWave, WI);
}
defaultproperties
{
bEnabled=true
FewZedsDeadPct=0.2
ManyZedsDeadPct=0.8
FarFromTraderDistance=50000
NeedHealPct=0.5
TeammateNeedsHealPct=0.5
ShareDoshAmount=2000
LowDoshAmount=200
WaveClearDialogDelay=7.f
}