//============================================================================= // 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 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 }