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