1
0
KF2-Dev-Scripts/KFGame/Classes/KFDialogManager.uc
2020-12-13 18:01:13 +03:00

2999 lines
90 KiB
Ucode

//=============================================================================
// KFDialogManager
//=============================================================================
// Manager class for player dialog. Tracks delays, cooldowns, active dialog, etc.
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Jeff Robinson
//=============================================================================
class KFDialogManager extends Actor
dependson(KFPawnVoiceGroupEventData)
config(Game);
`include(KFGame/KFGameDialog.uci)
var bool bEnabled;
var config bool bLogDialog;
/** Information about dialog that has been spoken and is cooling down */
struct DialogCoolDownInfo
{
var int EventID;
var KFPawn Speaker;
var float EndTime;
var vector Location;
};
/** Event info for events that are cooling down */
var array< DialogCoolDownInfo > CoolDowns;
/** We only update one player pawn per "spotted" update, so we need to remember who's next */
var transient Pawn NextSpotterPawn;
/** Last time we checked if there should be "spotted" dialog */
var transient float LastSpotUpdateTime;
struct DelayedDialogInfo
{
var KFPawn Speaker;
var int EventID;
var float EndTime;
};
/** Event info for events that have a delay */
var array< DelayedDialogInfo > DelayedDialog;
/** Last time we checked if there should be idle dialog */
var float LastIdleUpdateTime;
/** Dialog at this priority level and above are considered "urgent" and will interrupt dialog of lower priority */
var byte InterruptPriorityThreshold;
/** Dialog at this priority level and below can be interrupted by any higher-priority dialog, disregarding InterruptPriorityThreshold */
var byte InterruptedByAnyPriorityThreshold;
/** Set at beginning of wave and beginning of trader time */
var bool bIsTraderTime;
// Variables to control dialog
// welding
var int WeldAboutToBreakThreshold;
// healing
var float NeedMoreHealingPctThreshold;
var float NeedNoMoreHealingPctThreshold;
// dosh
var int CaughtSomeDoshAmt;
var int CaughtMuchDoshAmt;
// reload/ammo
var int NumZedsForPressureReload;
var float LowSpareAmmoPctThreshold;
// kill zed
var int NumKillsForOnARoll;
var float CloseCallKillHealthPctThreshold;
var int NumZedsInAreaForMassacre;
var float AreaRadiusForMassacre;
var float TimeLimitForMassacre;
// zed damage
var int NumHitsForDeadHorse;
var float TimeBetweenHitsForDeadHorse;
var float TimeForContinuousDamageThreshold;
var float TimeBetweenHitsForContinuousDamage;
// player damage
var float PlayerHealthPctForNearDeath;
var float PlayerTakeDamageStreakInterval;
var float PlayerTakeDamageStreakPctForScream;
// spot zed
var float IdleTimeforSpottingZed;
var int SpotLargeHordeNumZeds;
var float ZedAlmostDeadHealthPctThreshold;
var float SprintTowardZedDuration;
// situational/idle
var float IdleTimeForSituationalDialog;
var int IdleLowDoshThreshold;
var int IdleHighDoshThreshold;
var int IdleLowAmmoPctThreshold;
var int IdleHighAmmoPctThreshold;
// sprinting
var float TimeUntilStartSprintPanting;
// global AkEvents
var AkEvent StopBreathingAkEvent;
/** Same as defined in KFPawn_HUman. This one should always match that one. */
delegate OnFinishedDialog( const out DialogResponseInfo ResponseInfo );
/*****************************************************************************
* Utility functions for getting event data
*****************************************************************************/
/** Gets the radius within which the event can occur */
function float GetEventRadius( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
if( EventDataClass != none )
{
return EventDataClass.default.Events[EventID].Radius;
}
`warn("KFDialogManager::GetEventRadius - EventDataClass is none, using probably useless default value");
return 0.f;
}
/** Get the event radius squared */
function float GetEventRadiusSq( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
local float Rad;
Rad = GetEventRadius( EventID, EventDataClass );
return Rad * Rad;
}
/** Gets the field-of-view within which the event can occur */
function float GetEventFOV( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
if( EventDataClass != none )
{
return EventDataClass.default.Events[EventID].FOV;
}
`warn("KFDialogManager::GetEventFOV - EventDataClass is none, using probably useless default value");
return 0.f;
}
/** Gets the priority of the event */
function byte GetEventPriority( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
if( EventDataClass != none )
{
return EventDataClass.default.Events[EventID].Priority;
}
`warn("KFDialogManager::GetEventPriority - EventDataClass is none, using probably useless default value");
return 255;
}
/** Gets the cooldown of the event */
function float GetEventCoolDownTime( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
if( EventDataClass != none )
{
return EventDataClass.default.Events[EventID].CoolDownTime;
}
`warn("KFDialogManager::GetEventCoolDownTime - EventDataClass is none, using probably useless default value");
return 0.f;
}
/** Gets the radius of effect of the cooldown of the event */
function float GetEventCoolDownRadius( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
if( EventDataClass != none )
{
return EventDataClass.default.Events[EventID].CoolDownRadius;
}
`warn("KFDialogManager::GetEventCoolDownRadius - EventDataClass is none, using probably useless default value");
return 0.f;
}
/** Gets the cooldown radius squared */
function float GetEventCoolDownRadiusSq( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
local float CoolDownRadius;
CoolDownRadius = GetEventCoolDownRadius( EventID, EventDataClass );
return CoolDownRadius * CoolDownRadius;
}
/** Gets the cooldown category of the event */
function int GetEventCoolDownCategory( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
if( EventDataClass != none )
{
if( EventDataClass.default.Events.Length > EventID )
{
return EventDataClass.default.Events[EventID].CoolDownCategory;
}
else
{
return 255;
}
}
`warn("KFDialogManager::GetEventCoolDownCategory - EventDataClass is none, using probably useless default value");
return 255;
}
/** Gets the cooldown category of the event */
function bool GetEventIsOnlyPlayedLocally( int EventID, class< KFPawnVoiceGroupEventData > EventDataClass )
{
if( EventDataClass != none )
{
return EventDataClass.default.Events[EventID].bOnlyPlayLocally;
}
`warn("KFDialogManager::GetEventIsOnlyPlayedLocally - EventDataClass is none, using probably useless default value");
return true;
}
/*****************************************************************************
* Utility functions for checking pawn validigy, proximity, FOV, and LOS
*****************************************************************************/
function bool PawnIsValidPlayer( KFPawn_Human KFPH )
{
// gotta be human!
if( KFPH.GetTeamNum() != 0 )
{
return false;
}
// gotta be alive!
if( !KFPH.IsAliveAndWell() )
{
return false;
}
if( KFPH.VoiceGroupArch == none || KFPH.VoiceGroupArch.default.EventDataClass == none )
{
return false;
}
return true;
}
function bool PawnCanSpotPawn( Pawn P1, Pawn P2, float MaxDistanceSq, float FOVCosine, optional bool CheckLOS, optional bool DoubleCheckLOS )
{
// check distance
if( VSizeSq(P2.Location - P1.Location) >= MaxDistanceSq )
{
return false;
}
if( !ActorWithinPawnFOV(P2, P1, FOVCosine) )
{
return false;
}
// check LOS
return !CheckLOS || PawnWithinPawnLOS(P2, P1, DoubleCheckLOS);
}
function bool PawnCanSpotActor( Pawn P, Actor A, float MaxDistanceSq, float FOVCosine, optional bool CheckLOS )
{
// check distance
if( VSizeSq(A.Location - P.Location) >= MaxDistanceSq )
{
return false;
}
if( !ActorWithinPawnFOV(A, P, FOVCosine) )
{
return false;
}
// check LOS
if( CheckLOS && !FastTrace(P.GetPawnViewLocation(), A.Location) )
{
return false;
}
return true;
}
function bool ActorWithinPawnFOV( Actor A, Pawn P, float FOVCosine )
{
local vector PtoA, PFacing;
PFacing = vector( P.GetViewRotation() );
PtoA = Normal( A.Location - P.Location );
// check FOV
if( PtoA dot PFacing < FOVCosine )
{
return false;
}
return true;
}
function bool ActorWithinPawnRadius( Actor A, Pawn P, float Radius )
{
if( VSizeSq(A.Location - P.Location) > (Radius * Radius) )
{
return false;
}
return true;
}
function bool PawnWithinPawnLOS( Pawn P1, Pawn P2, optional bool bDoubleCheckLOS )
{
if( !FastTrace(P2.GetPawnViewLocation(), P1.GetPawnViewLocation()) )
{
if( !bDoubleCheckLOS || !FastTrace(P1.GetPawnViewLocation(), P2.Location) )
{
return false;
}
}
return true;
}
function bool PlayerCanSpotEnemy( KFPawn_Human Player, KFPawn_Monster Enemy, optional float RadiusSq = -1.f, optional float FOVCosine = -1.f )
{
local class< KFPawnVoiceGroupEventData > EventDataClass;
EventDataClass = Player.GetVoiceGroupEventDataClass();
if( RadiusSq < 0 )
{
RadiusSq = GetEventRadiusSq(`SPOTZ_Generic, EventDataClass);
}
if( FOVCosine < 0 )
{
FOVCosine = GetEventFOV(`SPOTZ_Generic, EventDataClass);
}
return PawnCanSpotPawn(Player, Enemy, RadiusSq, FOVCosine, true, true );
}
/*****************************************************************************
* Start real dialog manager functionality
*****************************************************************************/
function SetTraderTime( bool bTraderTime )
{
bIsTraderTime = bTraderTime;
}
event Tick( float DeltaTime )
{
if( !bEnabled || !WorldInfo.GRI.bMatchHasBegun )
{
return;
}
if( DelayedDialog.Length > 0 )
{
UpdateDelayedDialog();
}
if( `TimeSince(LastSpotUpdateTime) > 0.25 )
{
CheckSpottedDialog();
LastSpotUpdateTime = WorldInfo.TimeSeconds;
}
if( `TimeSince(LastIdleUpdateTime) > 0.5 )
{
CheckIdleDialog();
LastIdleUpdateTime = WorldInfo.TimeSeconds;
}
}
/** Updates delayed dialog, plays delayed dialog when delay time has elapsed */
function UpdateDelayedDialog()
{
local int i;
for( i = 0; i < DelayedDialog.Length; ++i )
{
if( DelayedDialog[i].EndTime < WorldInfo.TimeSeconds )
{
PlayDialogEvent( DelayedDialog[i].Speaker, DelayedDialog[i].EventID );
DelayedDialog.Remove( i--, 1 );
}
}
}
/** Checks if dialog should be considered to be "cooling down" */
function bool DialogIsCoolingDown( KFPawn KFP, int EventID, int EventCoolDownCategory )
{
local int i, CoolDownID, iCoolDownCategory;
local class< KFPawnVoiceGroupEventData > EventDataClass;
EventDataClass = KFP.GetVoiceGroupEventDataClass();
for( i = 0; i < CoolDowns.Length; ++i )
{
CoolDownID = CoolDowns[i].EventID;
iCoolDownCategory = GetEventCoolDownCategory(CoolDownID, EventDataClass);
// check cooldown for:
// - same event
// - any event, in the case that EventID is an idle/situational event (don't remark about the weather unless no one around you has talked for a while)
// - any event, in the case that CoolDownID is a response event (don't talk over responses... it gets confusing)
// - any event in the same cooldown category
if( EventID == CoolDownID ||
EventCoolDownCategory == ECC_Situ ||
iCoolDownCategory == ECC_Resp ||
(EventCoolDownCategory != 255 && EventCoolDownCategory == iCoolDownCategory) )
{
// instead of removing on tick, remove on demand
if( CoolDowns[i].EndTime <= WorldInfo.TimeSeconds )
{
CoolDowns.Remove( i--, 1 );
continue;
}
if( KFP == Cooldowns[i].Speaker )
{
return true;
}
if( VSizeSq(CoolDowns[i].Location - KFP.Location) < GetEventCoolDownRadiusSq(CoolDownID, EventDataClass) )
{
return true;
}
}
}
return false;
}
/** Checks if a dialog event can be played based on priority, cooldown, etc. */
function bool DialogEventCanBePlayed( KFPawn KFP, const out DialogEventInfo EventInfo )
{
if( KFP.IsSpeaking() )
{
// event isn't allowed to interrupt anything
if( EventInfo.Priority > InterruptPriorityThreshold && KFP.CurrDialogPriority < InterruptedByAnyPriorityThreshold )
{
`log(KFP.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" can't be played for "$Right(KFP.VoiceGroupArch.Name,Len(KFP.VoiceGroupArch.Name)-InStr(KFP.VoiceGroupArch.Name, "_")-1)$" - Already speaking "$KFP.VoiceGroupArch.static.GetEventName(KFP.CurrDialogEventID)$" (priority too low to cause interupt)", bLogDialog, 'BattleChatter');
return false;
}
// event is lower priority than dialog being spoken, so it can't interrupt it
if( EventInfo.Priority > KFP.CurrDialogPriority )
{
`log(KFP.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" can't be played for "$Right(KFP.VoiceGroupArch.Name,Len(KFP.VoiceGroupArch.Name)-InStr(KFP.VoiceGroupArch.Name, "_")-1)$" - Already speaking "$KFP.VoiceGroupArch.static.GetEventName(KFP.CurrDialogEventID)$" (lower priority)", bLogDialog, 'BattleChatter');
return false;
}
// event is same priority and isn't allowed to interrupt equal priority dialog
if( EventInfo.Priority == KFP.CurrDialogPriority && !EventInfo.bCanInterruptEqualPriority )
{
`log(KFP.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" can't be played for "$Right(KFP.VoiceGroupArch.Name,Len(KFP.VoiceGroupArch.Name)-InStr(KFP.VoiceGroupArch.Name, "_")-1)$" - Already speaking "$KFP.VoiceGroupArch.static.GetEventName(KFP.CurrDialogEventID)$" (same priority, not allowed to interrupt same priority)", bLogDialog, 'BattleChatter');
return false;
}
}
// don't re-play dialog in cool-down
if( DialogIsCoolingDown(KFP, EventInfo.EventID, EventInfo.CoolDownCategory) )
{
`log(KFP.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" can't be played for "$Right(KFP.VoiceGroupArch.Name,Len(KFP.VoiceGroupArch.Name)-InStr(KFP.VoiceGroupArch.Name, "_")-1)$" - Cooling down", bLogDialog, 'BattleChatter');
return false;
}
// don't play if it would be out of context with respect to number of players in the game
if( !EventInfo.bCanPlayAlone && KFGameInfo(WorldInfo.Game).GetLivingPlayerCount() < 2 )
{
`log(KFP.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" can't be played for "$Right(KFP.VoiceGroupArch.Name,Len(KFP.VoiceGroupArch.Name)-InStr(KFP.VoiceGroupArch.Name, "_")-1)$" - Alone", bLogDialog, 'BattleChatter');
return false;
}
// only allow dialog in the ECC_TTime category to be played during trader time
if( EventInfo.CoolDownCategory == ECC_TTime && !bIsTraderTime )
{
`log(KFP.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" can't be played for "$Right(KFP.VoiceGroupArch.Name,Len(KFP.VoiceGroupArch.Name)-InStr(KFP.VoiceGroupArch.Name, "_")-1)$" - Trader time only", bLogDialog, 'BattleChatter');
return false;
}
// only allow high-pri dialog (mostly pain/damages sounds) if being grabbed by Hans
if( KFP.SpecialMove == SM_HansGrappleVictim && EventInfo.Priority > InterruptPriorityThreshold )
{
`log(KFP.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" can't be played for "$Right(KFP.VoiceGroupArch.Name,Len(KFP.VoiceGroupArch.Name)-InStr(KFP.VoiceGroupArch.Name, "_")-1)$" - Being grabbed by Hans", bLogDialog, 'BattleChatter');
return false;
}
return true;
}
/** Play dialog from given speaker and dialog anim on given speaker */
function PlayDialogEvent( KFPawn Speaker, int EventID )
{
local DialogCoolDownInfo CoolDownInfo;
local DialogEventInfo EventInfo;
local AkEvent EventAudioCue;
local name SpeakerName;
if( !bEnabled || Speaker == none )
{
return;
}
if( EventID < 0 || Speaker.VoiceGroupArch == none )
{
return;
}
EventInfo = Speaker.VoiceGroupArch.static.GetDialogEventInfo(EventID);
if( EventInfo.Delay > 0 )
{
if( AddDelayedDialogEvent(Speaker, EventID, EventInfo.Delay) )
{
return;
}
}
if( !DialogEventCanBePlayed(Speaker, EventInfo) )
{
return;
}
if( FRand() > EventInfo.Chance )
{
return;
}
EventAudioCue = Speaker.VoiceGroupArch.static.GetDialogAkEvent( EventID, bIsTraderTime );
if( EventAudioCue == none )
{
`log("KFDialogManager::PlayDialogEvent - event "$Speaker.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" for speaker "$Speaker$" has no audio, bailing out", bLogDialog);
return;
}
if( EventAudioCue.Duration <= 0 )
{
`log("KFDialogManager::PlayDialogEvent - event "$Speaker.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" for speaker "$Speaker$" has invalid duration, bailing out", bLogDialog);
return;
}
SpeakerName = Speaker.VoiceGroupArch.Name;
if( Speaker.IsSpeaking() )
{
`log("Interrupting "$Speaker.VoiceGroupArch.static.GetEventName(Speaker.CurrDialogEventID)$" for "$Right(SpeakerName,Len(SpeakerName)-InStr(SpeakerName, "_")-1), bLogDialog, 'BattleChatter');
// interrupt current dialog
Speaker.StopDialog();
}
`log("Playing "$Speaker.VoiceGroupArch.static.GetEventName(EventInfo.EventID)$" for "$Right(SpeakerName,Len(SpeakerName)-InStr(SpeakerName, "_")-1), bLogDialog, 'BattleChatter');
Speaker.CurrDialogEventID = EventInfo.EventID;
Speaker.CurrDialogPriority = EventInfo.Priority;
// manage cool down info
if( EventInfo.CoolDownTime > 0.f )
{
CoolDownInfo.EventID = EventInfo.EventID;
CoolDownInfo.Speaker = Speaker;
CoolDownInfo.Location = Speaker.Location;
CoolDownInfo.EndTime = WorldInfo.TimeSeconds + EventInfo.CoolDownTime;
CoolDowns.AddItem( CoolDownInfo );
}
if( EventInfo.bOnlyPlayLocally )
{
if( Speaker.IsLocallyControlled() )
{
KFPlayerController(Speaker.Controller).ClientHearDialog( Speaker, EventAudioCue, EventInfo.bCanBeMinimized );
}
}
else
{
Speaker.PlayDialog( EventAudioCue, EventInfo.bCanBeMinimized );
}
// keep track of end-of-event with a timer (duration is max duration of all permutations of dialog)
SetTimer( EventAudioCue.Duration, false, nameof(Speaker.EndOfDialogTimer), Speaker );
}
/** Checks if a given player is speaking or is set to speak a given line of dialog */
function bool PlayerIsPlayingDialogEvent( KFPawn_Human Player, int EventID )
{
local int DelayedDialogIdx;
if( Player.IsPlayingDialogEvent(EventiD) )
{
// the player is currently saying the line
return true;
}
DelayedDialogIdx = DelayedDialog.Find('EventID', EventID);
if( DelayedDialogIdx != INDEX_NONE && DelayedDialog[DelayedDialogIdx].Speaker == Player )
{
// the player will start saying the line soon, so consider him to be speaking already
return true;
}
return false;
}
/** Delegate for all responses that only require basic information and logic */
function BasicResponseDelegate( const out DialogResponseInfo RespInfo )
{
if( !RespInfo.Speaker.bDeleteMe && RespInfo.Speaker.IsAliveAndWell() )
{
PlayDialogEvent( RespInfo.Speaker, RespInfo.EventID );
}
}
/** Plays or sets up a basic (requires no special information or logic) response to a given line of dialog */
function PlayBasicDialogResponse( KFPawn_Human RespondToPawn, int RespondingToID, int ResponseID, optional KFPawn_Human Responder, optional KFPawn Target, optional bool bPlayOnlyAsResponse )
{
if( Responder == none )
{
Responder = FindPlayerResponder( RespondToPawn, ResponseID, Target );
}
if( Responder == none )
{
return;
}
// if Speaker is speaking the dialog Responder is waiting for, then wait for it to finish
if( PlayerIsPlayingDialogEvent(RespondToPawn, RespondingToID) )
{
RespondToPawn.SetDialogResponseDelegate( Responder, BasicResponseDelegate, ResponseID, RespondingToID );
}
// otherwise just play the "response" right away, if we're allowed to do so
else if( !bPlayOnlyAsResponse )
{
PlayDialogEvent( Responder, ResponseID );
}
}
/** Finds a nearby player to respond to a given line of dialog */
function KFPawn_Human FindPlayerResponder( KFPawn_Human Speaker, int ResponseEventID, optional KFPawn Target )
{
local KFPawn_Human Responder;
local class< KFPawnVoiceGroupEventData > EventDataClass;
EventDataClass = Speaker.GetVoiceGroupEventDataClass();
foreach WorldInfo.AllPawns( class'KFPawn_Human', Responder )
{
if( Responder == Speaker || !PawnIsValidPlayer(Responder) )
{
continue;
}
if( ActorWithinPawnRadius(Speaker, Responder, GetEventRadius(ResponseEventID, EventDataClass))
&& (Target == none || (ActorWithinPawnFOV(Target, Responder, GetEventFOV(ResponseEventID, EventDataClass)) && PawnWithinPawnLOS(Target, Responder)))
)
{
return Responder;
}
}
return none;
}
/** Plays or sets up a response that requires customized information or logic */
function PlayCustomDialogResponse( KFPawn_Human Speaker, KFPawn Responder, int RespondingToID, delegate<OnFinishedDialog> CustomDelegate, optional bool bPlayOnlyAsResponse )
{
// if Speaker is speaking the dialog Responder is waiting for, then wait for it to finish
if( PlayerIsPlayingDialogEvent(Speaker, RespondingToID) )
{
Speaker.SetDialogResponseDelegate( Responder, CustomDelegate, -1, RespondingToID );
}
// otherwise just play the "response" right away, if we're allowed to do so
else if( !bPlayOnlyAsResponse )
{
Speaker.SetDialogResponseDelegate( Responder, none, -1, RespondingToID );
CustomDelegate( Speaker.DlgRespInfo );
}
}
/** Simulates adding elements to an array and picking a random element (without using an array) */
function AddRandomDialogOption( KFPawn Speaker, int OptionID, out int NumOptions, out int BestOptionID )
{
local DialogEventInfo NewOptionEventInfo, BestOptionEventInfo;
local int WeightCount;
if( OptionID < 0 || Speaker.VoiceGroupArch == none )
{
return;
}
if( BestOptionID >= 0 )
{
NewOptionEventInfo = Speaker.VoiceGroupArch.static.GetDialogEventInfo(OptionID);
BestOptionEventInfo = Speaker.VoiceGroupArch.static.GetDialogEventInfo(BestOptionID);
if( NewOptionEventInfo.Priority < BestOptionEventInfo.Priority )
{
NumOptions = 0;
}
else if( NewOptionEventInfo.Priority > BestOptionEventInfo.Priority )
{
return;
}
}
NumOptions++;
WeightCount = NewOptionEventInfo.Weight;
while( WeightCount-- > 0 )
{
if( Frand() <= 1.f / float(NumOptions) )
{
BestOptionID = OptionID;
}
}
}
/** Adds an entry to the delayed dialog array */
function bool AddDelayedDialogEvent( KFPawn Speaker, int EventID, float Delay )
{
local int DelayedDialogIdx;
DelayedDialogIdx = DelayedDialog.Find('EventID', EventID);
if( DelayedDialogIdx == INDEX_NONE )
{
DelayedDialog.Add( 1 );
DelayedDialog[ DelayedDialog.Length - 1 ].Speaker = Speaker;
DelayedDialog[ DelayedDialog.Length - 1 ].EventID = EventID;
DelayedDialog[ DelayedDialog.Length - 1 ].EndTime = WorldInfo.TimeSeconds + Delay;
return true;
}
else if( DelayedDialog[DelayedDialogIdx].Speaker != Speaker )
{
return true;
}
return false;
}
/************************************************
* Door Dialog
************************************************/
function PlayWeldDialog( KFPawn Speaker, KFDoorActor Door, KFPawn Welder )
{
local int WeldDialogID, WeldRespDialogID;
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
if( Door.UnderAttack() )
{
WeldDialogID = `ACT_WeldDoorUnderAttack;
WeldRespDialogID = `ACT_WeldDoorUnderAttackResp;
}
else
{
WeldDialogID = `ACT_WeldDoor;
WeldRespDialogID = `ACT_WeldDoorResp;
}
if( Speaker == Welder )
{
// if door is now welded in full, play finished dialog
if( Door.WeldIntegrity >= Door.MaxWeldIntegrity )
{
PlayDialogEvent( KFPawn_Human(Speaker), `ACT_WeldDoorFinish );
}
// else play general "welding" dialog, but only when the player first starts welding
else if( !Door.BeingWelded() )
{
PlayDialogEvent( KFPawn_Human(Speaker), WeldDialogID );
}
// else if door is under attack and about to break, play "about to break" dialog
else if( WeldDialogID == `ACT_WeldDoorUnderAttack && Door.WeldIntegrity < WeldAboutToBreakThreshold )
{
PlayDialogEvent( KFPawn_Human(Speaker), `ACT_WeldDoorAboutToBreak );
}
}
else
{
PlayBasicDialogResponse( KFPawn_Human(Speaker), WeldDialogID, WeldRespDialogID, KFPawn_Human(Welder) );
}
}
function PlayUnweldDialog( KFPawn Speaker, KFDoorActor Door, KFPawn Unwelder )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
if( Speaker == Unwelder )
{
// if door is now unwelded in full, play finished dialog
if( Door.WeldIntegrity <= 0 )
{
PlayDialogEvent( KFPawn_Human(Speaker), `ACT_UnweldDoorFinish );
}
// else play unwelding dialog, but only when the player first starts unwelding
else if( !Door.BeingUnwelded() )
{
PlayDialogEvent( KFPawn_Human(Speaker), `ACT_UnweldDoor );
}
}
}
function PlayDoorTakeDamageDialog( KFDoorActor Door )
{
local KFPawn_Human KFPH;
local bool bDoorWeldBroken;
local class< KFPawnVoiceGroupEventData > EventDataClass;
bDoorWeldBroken = Door.WeldIntegrity <= 0;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
EventDataClass = KFPH.GetVoiceGroupEventDataClass();
//@todo: should probably check LOS, but that doesn't seem to be reliable with door actors...
if( !PawnCanSpotActor(KFPH, Door, GetEventRadiusSq(`ACT_SpotDoorAttacked, EventDataClass), GetEventFOV(`ACT_SpotDoorAttacked, EventDataClass)) )
{
continue;
}
if( bDoorWeldBroken )
{
PlayDialogEvent( KFPH, `ACT_SpotDoorBreak );
}
else
{
PlayDialogEvent( KFPH, `ACT_SpotDoorAttacked );
}
}
}
/************************************************
* Heal Dialog
************************************************/
function PlayHealMissDialog( KFPawn Healer, KFPawn IntendedHealee )
{
local KFPawn_Human KFPH, KFPH_Healee;
if( Healer.VoiceGroupArch == none || Healer.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
KFPH = KFPawn_Human(Healer);
if( KFPH == none )
{
return;
}
KFPH_Healee = KFPawn_Human(IntendedHealee);
if( KFPH_Healee == none )
{
return;
}
PlayDialogEvent( KFPH, `ACT_HealMiss );
PlayCustomDialogResponse( KFPawn_Human(Healer), IntendedHealee, `ACT_HealMiss, PlayHealMissResponse, true );
}
function PlayHealMissResponse( const out DialogResponseInfo RespInfo )
{
if( !RespInfo.Speaker.bIsMoving && ActorWithinPawnRadius(RespInfo.Speaker, RespInfo.RespondingToPawn, GetEventRadius(`ACT_HealMissResp, RespInfo.Speaker.GetVoiceGroupEventDataClass())) )
{
PlayDialogEvent( RespInfo.Speaker, `ACT_HealMissResp );
}
}
function PlayHealingDialog( KFPawn Healer, KFPawn Healee, float HealeeHealthPct )
{
local KFPawn_Human KFPH;
if( Healer.VoiceGroupArch == none || Healer.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
KFPH = KFPawn_Human(Healer);
if( KFPH == none )
{
// not an act of healing by a player
return;
}
if( KFPH != Healee )
{
PlayDialogEvent( KFPH, `ACT_HealSomeone );
if( HealeeHealthPct < NeedMoreHealingPctThreshold )
{
PlayBasicDialogResponse( KFPH, `ACT_HealSomeone, `ACT_HealSomeoneRespLT50pct, KFPawn_Human(Healee) );
}
else if( HealeeHealthPct >= NeedNoMoreHealingPctThreshold )
{
PlayBasicDialogResponse( KFPH, `ACT_HealSomeone, `ACT_HealSomeoneRespMT75pct, KFPawn_Human(Healee) );
}
}
else
{
PlayDialogEvent( KFPH, `ACT_HealSelf );
}
}
/************************************************
* Dosh Dialog
************************************************/
function PlayDoshCaughtDialog( KFPawn_Human Speaker )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
if( Speaker.DoshCaughtStreakAmt >= CaughtMuchDoshAmt )
{
PlayDialogEvent( Speaker, `ACT_DoshAtSomeoneRespLots );
}
else if( Speaker.DoshCaughtStreakAmt >= CaughtSomeDoshAmt )
{
PlayDialogEvent( Speaker, `ACT_DoshAtSomeoneRespSome );
}
}
function PlayDoshTossDialog( KFPawn Speaker )
{
local Actor HitActor;
local vector HitLocation, HitNormal;
local vector TraceStart, TraceEnd;
TraceEnd = Speaker.GetPawnViewLocation();
TraceStart = TraceEnd + vector(Speaker.GetViewRotation()) * 500;
HitActor = Trace( HitLocation, HitNormal, TraceEnd, TraceStart, true );
if( HitActor != Speaker && KFPawn_Human(HitActor) != none )
{
PlayDialogEvent( Speaker, `ACT_DoshAtSomeone );
}
else
{
PlayDialogEvent( Speaker, `ACT_DoshAtGround );
}
}
/************************************************
* Weapon Dialog
************************************************/
function PlayReloadDialog( KFPawn Speaker )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
if( Speaker.IsSurrounded(true, NumZedsForPressureReload, GetEventRadius(`ACT_ReloadUnderPres, Speaker.GetVoiceGroupEventDataClass())) )
{
PlayDialogEvent( Speaker, `ACT_ReloadUnderPres );
}
else
{
PlayDialogEvent( Speaker, `ACT_Reload );
}
}
function PlayAmmoDialog( KFPawn Speaker, float SpareAmmoLeftPct )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
if( SpareAmmoLeftPct <= LowSpareAmmoPctThreshold )
{
PlayDialogEvent( Speaker, `ACT_AmmoLow );
PlayBasicDialogResponse( KFPawn_Human(Speaker), `ACT_AmmoLow, `ACT_AmmoLowResp, /*Responder*/, /*Target*/, true );
}
}
function PlayDropWeaponDialog( KFPawn Speaker )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
PlayDialogEvent( Speaker, `ACT_DropWeap );
}
function PlayMeleeAttackDialog( KFPawn Speaker, bool bIsHeavyAttack )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
if( bIsHeavyAttack )
{
PlayDialogEvent( Speaker, `ACT_AttackHeavy );
}
else
{
PlayDialogEvent( Speaker, `ACT_AttackLight );
}
}
function PlayIronsightsDialog( KFPawn Speaker )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
PlayDialogEvent( Speaker, `ACT_Ironsights );
}
function PlayJumpDialog( KFPawn Speaker )
{
if( Speaker.VoiceGroupArch == none || Speaker.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
PlayDialogEvent( Speaker, `ACT_Jump );
}
/************************************************
* Sprint Dialog
************************************************/
function PlaySprintPantingDialog( KFPawn_Human Speaker, bool bNewSprintStatus )
{
if( bNewSprintStatus && !Speaker.bIsSprinting )
{
Speaker.SprintStartTime = WorldInfo.TimeSeconds;
}
else if ( !bNewSprintStatus )
{
StopBreathingDialog(Speaker);
}
if( `TimeSince(Speaker.SprintStartTime) > TimeUntilStartSprintPanting )
{
Speaker.SprintStartTime = WorldInfo.TimeSeconds;
PlayDialogEvent( Speaker, `ACT_Sprint );
}
}
/**
* A stop event for sprinting and ironsight breathing. The breathing sounds also have a max duration,
* so even though this event is not perfect (changing controllers, etc...) it won't break anything.
*/
function StopBreathingDialog(KFPawn Speaker)
{
if( Speaker.IsSpeaking() )
{
if ( Speaker.CurrDialogEventID == `ACT_Sprint || Speaker.CurrDialogEventID == `ACT_Ironsights )
{
KFPlayerController(Speaker.Controller).ClientHearDialog( Speaker, StopBreathingAkEvent, 0 );
}
}
}
/************************************************
* Level Up Dialog
************************************************/
function PlayLevelUpDialog( KFPlayerController LevellerKFPC )
{
local KFPawn_Human Leveller, Commenter;
local int CommentEventID;
local int LevellerLevel, CommenterLevel;
LevellerLevel = KFPlayerReplicationInfo( LevellerKFPC.PlayerReplicationInfo ).GetActivePerkLevel();
if( LevellerLevel < 5 )
{
CommentEventID = `ACT_CommentLvlLT10;
}
else if( LevellerLevel < 15 )
{
CommentEventID = `ACT_CommentLvlLT25;
}
else if( LevellerLevel < 25 )
{
CommentEventID = `ACT_CommentLvlLT50;
}
else
{
CommentEventID = `ACT_CommentLvlMax;
}
Leveller = KFPawn_Human( LevellerKFPC.Pawn );
Commenter = FindPlayerResponder( Leveller, CommentEventID );
if( Commenter != none )
{
PlayDialogEvent( Commenter, CommentEventID );
CommenterLevel = KFPlayerReplicationInfo( KFPlayerController(Commenter.Controller).PlayerReplicationInfo ).GetActivePerkLevel();
if( CommenterLevel > LevellerLevel )
{
PlayBasicDialogResponse( Commenter, CommentEventID, `ACT_CommentLvlUpRespHigher, Leveller, Commenter, true );
}
else if( CommenterLevel < LevellerLevel )
{
PlayBasicDialogResponse( Commenter, CommentEventID, `ACT_CommentLvlUpRespLower, Leveller, Commenter, true );
}
}
}
/************************************************
* KillZed Dialog
************************************************/
function PlayKilledZedDialog( KFPawn_Human Killer, KFPawn_Monster Zed, class<DamageType> DamageType, bool bWasKnockedDown )
{
local int NumOptions, BestOptionID;
if( Killer.VoiceGroupArch == none || Killer.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
BestOptionID = -1;
CheckOnARollDialog( Killer, NumOptions, BestOptionID );
CheckMassacreDialog( Killer, Zed, NumOptions, BestOptionID );
CheckCloseCallKillDialog( Killer, Zed, NumOptions, BestOptionID );
CheckKillAsLastAliveDialog( Killer, Zed, NumOptions, BestOptionID );
CheckKilledWithFavoriteWeaponDialog( Killer, NumOptions, BestOptionID );
AddRandomDialogOption( Killer, Zed.GetKillerDialogID(), NumOptions, BestOptionID );
if( class<KFDamageType>(DamageType) != none )
{
AddRandomDialogOption( Killer, class<KFDamageType>(DamageType).static.GetKillerDialogID(), NumOptions, BestOptionID );
}
if( bWasKnockedDown )
{
AddRandomDialogOption( Killer, `KILL_KnockedDown, NumOptions, BestOptionID );
}
PlayDialogEvent( Killer, BestOptionID );
PlayBasicKilledResponse( Killer, Zed, BestOptionID );
}
/** Adds "on a roll" random dialog if player has killed enough zeds without taking damage */
function CheckOnARollDialog( KFPawn_Human Speaker, out int out_NumOptions, out int out_BestOptionID )
{
Speaker.UpdateKillStreak();
if( Speaker.ZedsKilledStreakAmt >= NumKillsForOnARoll )
{
AddRandomDialogOption( Speaker, `KILL_OnARoll, out_NumOptions, out_BestOptionID );
}
}
/** Adds "massacre" random dialog if enough zeds have been killed in a certain amount of time and within a certain radius */
function CheckMassacreDialog( KFPawn_Human Killer, KFPawn_Monster KilledZed, out int out_NumOptions, out int out_BestOptionID )
{
local int i, MassacreCount;
local KFGoreManager KFGM;
local KFPawn Corpse;
local float AreaRadiusSq;
KFGM = KFGoreManager(WorldInfo.MyGoreEffectManager);
if( KFGM == none )
{
return;
}
AreaRadiusSq = AreaRadiusForMassacre * AreaRadiusForMassacre;
// check corpses for proximity to KilledZed
for( i = 0; i < KFGM.CorpsePool.Length && MassacreCount < NumZedsInAreaForMassacre; ++i )
{
Corpse = KFGM.CorpsePool[i];
// don't count KilledZed toward massacre
if( Corpse == KilledZed )
{
continue;
}
// only check zeds that have died within the time limit for a "massacre"
if( `TimeSince(Corpse.TimeOfDeath) > TimeLimitForMassacre )
{
continue;
}
// only check zeds that are within the radius limit for a "massacre"
if( VSizeSq(Corpse.Location - KilledZed.Location) > AreaRadiusSq )
{
continue;
}
++MassacreCount;
}
if( MassacreCount >= NumZedsInAreaForMassacre )
{
AddRandomDialogOption( Killer, `KILL_Massacre, out_NumOptions, out_BestOptionID );
}
}
/** Add "close call" random dialog if player killed zed right before zed killed him */
function CheckCloseCallKillDialog( KFPawn_Human Killer, KFPawn_Monster Zed, out int out_NumOptions, out int out_BestOptionID )
{
if( Killer.LastHitBy == Zed.MyKFAIC
&& (float(Killer.Health) / float(Killer.HealthMax) <= CloseCallKillHealthPctThreshold) )
{
AddRandomDialogOption( Killer, `KILL_CloseCall, out_NumOptions, out_BestOptionID );
}
}
function CheckKillAsLastAliveDialog( KFPawn_Human Killer, KFPawn_Monster Zed, out int out_NumOptions, out int out_BestOptionID )
{
if( KFGameInfo(WorldInfo.Game).GetLivingPlayerCount() == 1 )
{
AddRandomDialogOption( Killer, `KILL_AsLastPlayer, out_NumOptions, out_BestOptionID );
}
}
function CheckKilledWithFavoriteWeaponDialog( KFPawn_Human Killer, out int out_NumOptions, out int out_BestOptionID )
{
local int FavoriteWeaponIndex;
local KFCharacterInfo_Human KFCI;
KFCI = KFCharacterInfo_Human( Killer.CharacterArch );
if( KFCI == none )
{
return;
}
FavoriteWeaponIndex = KFCI.GetFavoriteWeaponIndexOf( Killer.Weapon );
if( FavoriteWeaponIndex < 0 )
{
return;
}
AddRandomDialogOption( Killer, `KILL_Fave1 + FavoriteWeaponIndex, out_NumOptions, out_BestOptionID );
}
/** Set up response for selected "kill zed" dialog */
function PlayBasicKilledResponse( KFPawn_Human Killer, KFPawn_Monster KilledZed, int KilledEventID )
{
local int ResponseEventID;
local bool bPlayOnlyAsResponse;
switch( KilledEventID )
{
case `KILL_OnARoll:
ResponseEventID = `KILL_OnARollResp;
bPlayOnlyAsResponse = true;
break;
case `KILL_Massacre:
ResponseEventID = `KILL_MassacreResp;
bPlayOnlyAsResponse = true;
break;
case `KILL_Slashing:
ResponseEventID = `KILL_SlashingResp;
break;
case `KILL_Blunt:
ResponseEventID = `KILL_BluntResp;
break;
case `KILL_Ballistic:
ResponseEventID = `KILL_BallisticResp;
break;
case `KILL_Explosive:
ResponseEventID = `KILL_ExplosiveResp;
break;
case `KILL_Fire:
ResponseEventID = `KILL_FireResp;
break;
default:
return; // do nothing
};
PlayBasicDialogResponse( Killer, KilledEventID, ResponseEventID, /*Responder*/, KilledZed, bPlayOnlyAsResponse );
}
/************************************************
* Damage Zed Dialog
************************************************/
function PlayDamagedZedDialog( KFPawn_Human Damager, KFPawn_Monster Zed, class<DamageType> DamageType )
{
local int NumOptions, BestOptionID;
if( Damager.VoiceGroupArch == none || Damager.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
BestOptionID = -1;
if( Zed.IsDoingSpecialMove(SM_Knockdown) )
{
AddRandomDialogOption( Damager, `DAMZ_Knockdown, NumOptions, BestOptionID );
}
else if( Zed.IsDoingSpecialMove(SM_Stumble) )
{
AddRandomDialogOption( Damager, `DAMZ_Stumble, NumOptions, BestOptionID );
}
else if( Zed.IsDoingSpecialMove(SM_Stunned) )
{
AddRandomDialogOption( Damager, `DAMZ_Stun, NumOptions, BestOptionID );
}
if( class<KFDamageType>(DamageType) != none )
{
AddRandomDialogOption( Damager, class< KFDamageType >( DamageType ).static.GetDamagerDialogID(), NumOptions, BestOptionID );
}
PlayDialogEvent( Damager, BestOptionID );
}
/** Play "over and over" random dialog if player deals enough consecutive damage to a given zed
* NOTE: this needs to be called before zed's LastHitBy and LastPainTime are set for current hit */
function PlayDamageZedContinuousDialog( KFPawn_Human Damager, KFPawn_Monster Zed )
{
Damager.UpdateContinuousDamage( Zed, TimeBetweenHitsForContinuousDamage );
if( `TimeSince(Damager.InitialContinousDamageTime) >= TimeForContinuousDamageThreshold )
{
PlayDialogEvent( Damager, `DAMZ_OverAndOver );
}
}
/** Add "dead horse" random dialog if player hits a corpse repeatedly */
function PlayBeatDeadHorseDialog( KFPawn_Human Speaker, KFPawn_Monster DeadZed )
{
if( DeadZed.DeadHorseHitStreakAmt < NumHitsForDeadHorse && `TimeSince(DeadZed.LastDeadHorseHitTime) < TimeBetweenHitsForDeadHorse )
{
DeadZed.UpdateDeadHorseStreak( true );
}
else
{
DeadZed.UpdateDeadHorseStreak( false );
}
if( DeadZed.DeadHorseHitStreakAmt >= NumHitsForDeadHorse )
{
PlayDialogEvent( Speaker, `KILL_BeatDeadHorse );
PlayBasicDialogResponse( Speaker, `KILL_BeatDeadHorse, `KILL_BeatDeadHorseResp, /*Responder*/, DeadZed );
}
}
/************************************************
* Player Damage Dialog
************************************************/
function PlayPlayerDamageDialog( KFPawn_Human Player, class< DamageType > DamageType, int Damage )
{
local int NumOptions, BestOptionID;
if( Player.VoiceGroupArch == none || Player.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
BestOptionID = -1;
if( (float(Player.Health) / float(Player.HealthMax) < PlayerHealthPctForNearDeath) )
{
AddRandomDialogOption( Player, `DAMP_NearDeath, NumOptions, BestOptionID );
}
else
{
CheckPlayerDamageGeneric( Player, Damage, NumOptions, BestOptionID );
}
if( class<KFDamageType>(DamageType) != none )
{
AddRandomDialogOption( Player, class<KFDamageType>(DamageType).static.GetDamageeDialogID(), NumOptions, BestOptionID );
// if player has been blinded (due to toxic damage, though berserker doesn't get blinded), add "blinded" random option
if( ClassIsChildOf(DamageType, class'KFDT_Toxic') && !ClassIsChildOf(Player.GetPerk().Class, class'KFPerk_Berserker') )
{
AddRandomDialogOption( Player, `DAMP_Blinded, NumOptions, BestOptionID );
}
}
PlayDialogEvent( Player, BestOptionID );
// if playing "blinded" dialog, prepare for "blinded" dialog response
if( BestOptionID == `DAMP_Blinded )
{
PlayBasicDialogResponse( Player, `DAMP_Blinded, `DAMP_BlindedResp, /*Responder*/, /*Target*/, true );
}
}
/** Add generic damage vocalizations depending on how hard the player was hit */
function CheckPlayerDamageGeneric( KFPawn_Human Player, int Damage, out int out_NumOptions, out int out_BestOptionID )
{
local float DamageTakenStreakPct;
Player.UpdateDamageTakenStreak( Damage, PlayerTakeDamageStreakInterval );
DamageTakenStreakPct = float( Player.DamageTakenStreakAmt ) / float( Player.HealthMax );
if( DamageTakenStreakPct >= PlayerTakeDamageStreakPctForScream )
{
AddRandomDialogOption( Player, `DAMP_Scream, out_NumOptions, out_BestOptionID );
}
else
{
AddRandomDialogOption( Player, `DAMP_Grunt, out_NumOptions, out_BestOptionID );
}
}
function PlayPlayerGrabbedDialog( KFPawn_Human Speaker )
{
if( Speaker != none )
{
PlayDialogEvent( Speaker, `SPOTZ_GrabbedMe );
}
}
function PlayPlayerGrabbedByPatriarchDialog( KFPawn_Human Speaker )
{
if( Speaker != none )
{
PlayDialogEvent( Speaker, `SPOTZ_PulledMeIn );
}
}
/************************************************
* Player Death Dialog
************************************************/
function PlayPlayerDeathDialog( KFPawn_Human Player )
{
local KFPawn_Human KFPH;
local float MaxDistanceSq;
if( Player.VoiceGroupArch == none || Player.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
PlayDialogEvent( Player, `DAMP_Dying );
MaxDistanceSq = GetEventRadiusSq( `DAMP_SpotTeamDeathLast, Player.GetVoiceGroupEventDataClass() );
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
if( PawnCanSpotPawn(KFPH, Player, MaxDistanceSq, GetEventFOV(`DAMP_SpotTeamDeathLast, KFPH.GetVoiceGroupEventDataClass()), true) )
{
// player just saw only other player die
if( KFGameInfo(WorldInfo.Game).GetLivingPlayerCount() == 1 )
{
PlayDialogEvent( KFPH, `DAMP_SpotTeamDeathLast );
}
// player just saw female teammate die
else if( KFCharacterInfo_Human(KFPH.CharacterArch).bIsFemale )
{
PlayDialogEvent( KFPH, `DAMP_SpotTeamDeathF );
}
// player just saw male teammate die
else
{
PlayDialogEvent( KFPH, `DAMP_SpotTeamDeathM );
}
}
}
}
/************************************************
* Spot Zed Dialog
************************************************/
/** Compares priority of two events */
function bool CheckSpottedDialogPriority( KFPawn_Human Spotter, int CheckEventID, int BestEventID )
{
local DialogEventInfo CheckEventInfo, BestEventInfo;
if( BestEventID < 0 )
{
return true;
}
CheckEventInfo = Spotter.VoiceGroupArch.static.GetDialogEventInfo(CheckEventID);
BestEventInfo = Spotter.VoiceGroupArch.static.GetDialogEventInfo(BestEventID);
if( CheckEventInfo.Priority > BestEventInfo.Priority )
{
return false;
}
else
{
return true;
}
}
/** Called when a zed spots a player (line of sight check has already passed).
* We use this in reverse... when a zed spots a player, then check if the player spots the zed
*/
function CheckSpotMonsterDialog( Pawn Spotter, KFPawn_Monster Spotted )
{
local int SpottedDialogEventID, BestSpottedEventID;
local KFPawn_Human KFPHSpotter;
local class<KFPawnVoiceGroupEventData> EventDataClass;
BestSpottedEventID = -1;
Spotted.MyKFAIC.bWasVisibleToEnemy = Spotted.MyKFAIC.bIsVisibleToEnemy;
KFPHSpotter = KFPawn_Human(Spotter);
if (KFPHSpotter == none)
{
return;
}
EventDataClass = KFPHSpotter.GetVoiceGroupEventDataClass();
// if zed is within FOV
if( ActorWithinPawnFOV(Spotted, Spotter, GetEventFOV(`SPOTZ_Generic, EventDataClass)) )
{
Spotted.MyKFAIC.bIsVisibleToEnemy = true;
// if zed just entered a player's view
if( !Spotted.MyKFAIC.bWasVisibleToEnemy && Spotted.MyKFAIC.bIsVisibleToEnemy )
{
// if zed is within "surprise" (really close) range
if( ActorWithinPawnRadius(Spotted, Spotter, GetEventRadius(`SPOTZ_Surprise, EventDataClass)) )
{
PlayDialogEvent( KFPHSpotter, `SPOTZ_Surprise );
}
else if( ActorWithinPawnRadius(Spotted, Spotter, GetEventRadius(`SPOTZ_Generic, EventDataClass)) )
{
SpottedDialogEventID = Spotted.GetSpotterDialogID();
if( CheckSpottedDialogPriority(KFPHSpotter, SpottedDialogEventID, BestSpottedEventID) )
{
BestSpottedEventID = SpottedDialogEventID;
}
PlayDialogEvent( KFPHSpotter, BestSpottedEventID );
}
}
}
else
{
Spotted.MyKFAIC.bIsVisibleToEnemy = false;
}
}
/** Check all of the things that can be spotted and pick the best one to comment on.
* Called from Tick.
*/
function CheckSpottedDialog()
{
local Pawn P;
local KFPawn_Human KFPH;
local KFPawn_Monster BestMonster;
local int SpottedCount, SpottedEventID;
local int NumOptions, BestOptionID;
SpottedEventID = -1;
BestOptionID = -1;
// we only check one human pawn per call to CheckSpottedDialog, so we cache the next pawn we need to check.
// if that pawn is invalid, reset it.
if( NextSpotterPawn == none || NextSpotterPawn.bDeleteMe || !NextSpotterPawn.IsAliveAndWell() )
{
NextSpotterPawn = WorldInfo.PawnList;
}
// check what each player can spot
for( P = NextSpotterPawn; P != none; P = P.NextPawn )
{
KFPH = KFPawn_Human(P);
if( KFPH == none || !PawnIsValidPlayer(KFPH) )
{
continue;
}
// only call out zeds if you haven't been engaged with zeds for a little while
if( KFPH.TimeSpentIdling() > IdleTimeForSpottingZed )
{
// FindBestSpottedMonster checks proximity, FOV, and dialog priority, but NOT line-of-sight.
// We check line of sight only against the BestMonster returned by FindBestSpottedMonster for performance's sake.
BestMonster = FindBestSpottedMonster( KFPH, SpottedCount, SpottedEventID );
if( BestMonster != none && FastTrace(KFPH.GetPawnViewLocation(), BestMonster.Location) )
{
AddRandomDialogOption( KFPH, SpottedEventID, NumOptions, BestOptionID );
if( float(BestMonster.Health) / float(BestMonster.HealthMax) <= ZedAlmostDeadHealthPctThreshold )
{
AddRandomDialogOption( KFPH, `SPOTZ_AlmostDead, NumOptions, BestOptionID );
}
if( SpottedCount >= SpotLargeHordeNumZeds )
{
AddRandomDialogOption( KFPH, `SPOTZ_Horde, NumOptions, BestOptionID );
}
// check if BestMonster is "behind" any teammates
if( SpotEnemyBehindPlayer(KFPH, BestMonster) )
{
AddRandomDialogOption( KFPH, `SPOTZ_Behind, NumOptions, BestOptionID );
}
}
}
// SprintTowardZedStartTime is set/reset in FindBestSpottedMonster, called above
if( KFPH.SprintTowardZedStartTime > 0 && `TimeSince(KFPH.SprintTowardZedStartTime) >= SprintTowardZedDuration )
{
AddRandomDialogOption( KFPH, `DAMZ_SprintToward, NumOptions, BestOptionID );
}
// only check for SpotItem and SpotAmmo dialog if we haven't already added higher priority dialog (like SpotZed)
if( BestOptionID < 0 || GetEventPriority(BestOptionID, KFPH.GetVoiceGroupEventDataClass()) >= 5 )
{
CheckSpotPickupsDialog( KFPH, NumOptions, BestOptionID );
}
PlayDialogEvent( KFPH, BestOptionID );
PlayBasicSpottedResponse(KFPH, BestMonster, BestOptionID);
// since we're only checking one pawn per call, cache the next pawn in line and break
NextSpotterPawn = KFPH.NextPawn;
break;
}
}
/** Finds the best zed to comment on based on proximity, field of view, and zed dialog priority */
function KFPawn_Monster FindBestSpottedMonster( KFPawn_Human Spotter, out int NumSpotted, out int BestSpottedEventID )
{
local Pawn MP;
local KFPawn_Monster KFPM, BestMonster;
local int SpottedDialogEventID;
local bool bSprintingTowardZed;
local class<KFPawnVoiceGroupEventData> EventDataClass;
EventDataClass = Spotter.GetVoiceGroupEventDataClass();
for( MP = WorldInfo.PawnList; MP != none; MP = MP.NextPawn )
{
if( !MP.IsAliveAndWell() || MP.GetTeamNum() == 0 )
{
continue;
}
KFPM = KFPawn_Monster( MP );
if( KFPM == none )
{
// should never happen, right?
continue;
}
if( KFPM.bIsHeadless )
{
continue;
}
if( VSizeSq(KFPM.Location - Spotter.Location) >= GetEventRadiusSq(`SPOTZ_Generic, EventDataClass) )
{
continue;
}
if( ActorWithinPawnFOV(KFPM, Spotter, GetEventFOV(`SPOTZ_Generic, EventDataClass)) )
{
NumSpotted++;
SpottedDialogEventID = KFPM.GetSpotterDialogID();
// check priority of spotted dialog event for this zed against priority of best-so-far zed
if( CheckSpottedDialogPriority(Spotter, SpottedDialogEventID, BestSpottedEventID) )
{
BestSpottedEventID = SpottedDialogEventID;
BestMonster = KFPM;
}
// check if player is charging toward a zed
if( Spotter.bIsSprinting && !bSprintingTowardZed && ActorWithinPawnFOV(KFPM, Spotter, GetEventFOV(`DAMZ_SprintToward, EventDataClass)) )
{
bSprintingTowardZed = true;
if( Spotter.SprintTowardZedStartTime < 0 )
{
Spotter.SprintTowardZedStartTime = WorldInfo.TimeSeconds;
}
}
}
}
// reset sprint-toward-zed start time if not sprinting toward zed
if( !bSprintingTowardZed )
{
Spotter.SprintTowardZedStartTime = -1;
}
return BestMonster;
}
/** Checks if given zed is within spotter's view and right behind another player */
function bool SpotEnemyBehindPlayer( KFPawn_Human Spotter, KFPawn_Monster Enemy )
{
local Pawn OtherPlayer;
local vector FacingDir, ToEnemyDir, ToPlayerDir;
local float EventRadiusSq;
local class<KFPawnVoiceGroupEventData> EventDataClass;
EventDataClass = Spotter.GetVoiceGroupEventDataClass();
for( OtherPlayer = WorldInfo.PawnList; OtherPlayer != none; OtherPlayer = OtherPlayer.NextPawn )
{
if( !OtherPlayer.IsAliveAndWell() || OtherPlayer.GetTeamNum() != 0 || OtherPlayer == Spotter )
{
continue;
}
// check if the player is within view
ToPlayerDir = OtherPlayer.Location - Spotter.Location;
if( VSizeSq(ToPlayerDir) >= GetEventRadiusSq(`SPOTZ_Generic, EventDataClass) )
{
continue;
}
FacingDir = vector( Spotter.Rotation );
ToPlayerDir = Normal( ToPlayerDir );
if( ToPlayerDir dot FacingDir < GetEventFOV(`SPOTZ_Generic, EventDataClass) )
{
continue;
}
EventRadiusSq = GetEventRadiusSq(`SPOTZ_Behind, EventDataClass);
// check if Enemy is within "behind view" of the player
ToEnemyDir = Enemy.Location - OtherPlayer.Location;
if( VSizeSq(ToEnemyDir) >= EventRadiusSq )
{
continue;
}
FacingDir = vector( OtherPlayer.Rotation );
ToEnemyDir = Normal( ToEnemyDir );
if( ToEnemyDir dot FacingDir <= -GetEventFOV(`SPOTZ_Behind, EventDataClass) )
{
// only consider behind if we can actually see the player whom Enemy is behind
if( FastTrace(Spotter.GetPawnViewLocation(), OtherPlayer.GetPawnViewLocation()) )
{
return true;
}
// for efficiency's sake, break out after we find one good candidate, even if we don't succeed trace
break;
}
}
return false;
}
/** Checks if Spotter can see any pickups in the map */
function CheckSpotPickupsDialog( KFPawn_Human Spotter, out int out_NumOptions, out int out_BestOptionID )
{
local int i;
local KFGameInfo KFGI;
local bool bSpottedWeapon, bSpottedArmor, bSpottedAmmo;
local class< KFPawnVoiceGroupEventData > EventDataClass;
KFGI = KFGameInfo( WorldInfo.Game );
if( KFGI == none )
{
// should never happen, right?
return;
}
EventDataClass = Spotter.GetVoiceGroupEventDataClass();
for( i = 0; i < KFGI.AllPickupFactories.Length; ++i )
{
// ignore factories without visible pickups
if( KFGI.AllPickupFactories[i].bPickupHidden )
{
continue;
}
// ignore weapon factories if a weapon was already spotted
if( KFGI.AllPickupFactories[i].CurrentPickupIsWeapon() && bSpottedWeapon )
{
continue;
}
// ignore armor factories if armor was already spotted
if( KFGI.AllPickupFactories[i].CurrentPickupIsArmor() && bSpottedArmor )
{
continue;
}
// ignore ammo factories if ammo was already spotted
if( KFGI.AllPickupFactories[i].CurrentPickupIsAmmo() && bSpottedAmmo )
{
continue;
}
if( !PawnCanSpotActor(Spotter, KFGI.AllPickupFactories[i], GetEventRadiusSq(`ACT_SpotWeap, EventDataClass), GetEventFOV(`ACT_SpotWeap, EventDataClass), true) )
{
continue;
}
if( KFGI.AllPickupFactories[i].CurrentPickupIsWeapon() )
{
AddRandomDialogOption( Spotter, `ACT_SpotWeap, out_NumOptions, out_BestOptionID );
bSpottedWeapon = true;
}
else if( KFGI.AllPickupFactories[i].CurrentPickupIsArmor() )
{
AddRandomDialogOption( Spotter, `ACT_SpotArmor, out_NumOptions, out_BestOptionID );
bSpottedArmor = true;
}
else if( KFGI.AllPickupFactories[i].CurrentPickupIsAmmo() )
{
AddRandomDialogOption( Spotter, `ACT_SpotAmmo, out_NumOptions, out_BestOptionID );
bSpottedAmmo = true;
}
// break out early if we've added all possible random dialog for pickups
if( bSpottedWeapon && bSpottedArmor && bSpottedAmmo )
{
break;
}
}
}
/** Checks if any player can spot a zed that just cloaked/uncloaked */
function PlaySpotCloakDialog( KFPawn_Monster Cloaker, bool bNewCloaking )
{
local KFPawn_Human KFPH;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
if( PlayerCanSpotEnemy(KFPH, Cloaker) )
{
if( bNewCloaking )
{
PlayDialogEvent( KFPH, `SPOTZ_JustCloaked );
}
else
{
PlayDialogEvent( KFPH, `SPOTZ_JustUncloaked );
}
}
}
}
/** Checks if any player can spot a zed that just became enraged */
function PlaySpotEnragedDialog( KFPawn_Monster EnragedEnemy )
{
local KFPawn_Human KFPH;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
if( PlayerCanSpotEnemy(KFPH, EnragedEnemy) )
{
PlayDialogEvent( KFPH, `SPOTZ_RagingAtMe );
}
}
}
/** Checks if player being charged can spot the zed charging him */
function PlaySpotChargeDialog( KFPawn_Monster ChargingEnemy, KFPawn TargetPlayer )
{
local KFPawn_Human KFPH;
KFPH = KFPawn_Human( TargetPlayer );
if( KFPH == none )
{
return;
}
if( KFPH.VoiceGroupArch == none || KFPH.VoiceGroupArch.default.EventDataClass == none )
{
return;
}
if( PlayerCanSpotEnemy(KFPH, ChargingEnemy) )
{
PlayDialogEvent( KFPH, `SPOTZ_ChargingAtMe );
}
}
/** Checks if any player can spot a zed about to shoot a rocket */
function PlaySpotRocketsDialog( KFPawn_Monster Spotted )
{
local KFPawn_Human KFPH;
local class< KFPawnVoiceGroupEventData > EventDataClass;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
EventDataClass = KFPH.GetVoiceGroupEventDataClass();
if( PlayerCanSpotEnemy(KFPH, Spotted, GetEventRadiusSq(`SPOTZ_ShootingRockets, EventDataClass), GetEventFOV(`SPOTZ_ShootingRockets, EventDataClass)) )
{
PlayDialogEvent( KFPH, `SPOTZ_ShootingRockets );
}
}
}
/** Checks if any player can spot a zed about to throw a grenade */
function PlaySpotGrenadeDialog( KFPawn_Monster Spotted )
{
local KFPawn_Human KFPH;
local class< KFPawnVoiceGroupEventData > EventDataClass;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
EventDataClass = KFPH.GetVoiceGroupEventDataClass();
if( PlayerCanSpotEnemy(KFPH, Spotted, GetEventRadiusSq(`SPOTZ_ThrowGrenade, EventDataClass), GetEventFOV(`SPOTZ_ThrowGrenade, EventDataClass)) )
{
PlayDialogEvent( KFPH, `SPOTZ_ThrowGrenade );
}
}
}
/** Checks if a player being shot at can see shooter */
function PlayBeingShotAtDialog( KFPawn_Human Target, KFPawn_Monster Shooter )
{
local class< KFPawnVoiceGroupEventData > EventDataClass;
if( Target == none )
{
return;
}
EventDataClass = Target.GetVoiceGroupEventDataClass();
if( PlayerCanSpotEnemy(Target, Shooter, GetEventRadiusSq(`SPOTZ_ShootingAtMe, EventDataClass), GetEventFOV(`SPOTZ_ShootingAtMe, EventDataClass)) )
{
PlayDialogEvent( Target, `SPOTZ_ShootingAtMe );
}
}
/** Checks if any player can spot a zed about to heal */
function PlaySpotZedHealingDialog( KFPawn_Monster Spotted )
{
local KFPawn_Human KFPH;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
if( PlayerCanSpotEnemy(KFPH, Spotted) )
{
PlayDialogEvent( KFPH, `SPOTZ_Healing );
}
}
}
/** Checks if any player can spot a zed that is running away */
function PlaySpotRunAwayDialog( KFPawn_Monster Spotted )
{
local KFPawn_Human KFPH;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
if( PlayerCanSpotEnemy(KFPH, Spotted) )
{
PlayDialogEvent( KFPH, `SPOTZ_RunningAway );
}
}
}
/** Checks if player who could see boss can no longer see boss */
function PlayLoseSightOfBossDialog( KFPawn_Monster Boss, KFPawn_Human Speaker )
{
local class< KFPawnVoiceGroupEventData > EventDataClass;
EventDataClass = Speaker.GetVoiceGroupEventDataClass();
if( ActorWithinPawnFOV(Boss, Speaker, GetEventFOV(`SPOTZ_NotSeen, EventDataClass)) )
{
PlayDialogEvent( Speaker, `SPOTZ_NotSeen );
}
}
/** Sets up basic response for spoken "spotted" dialog */
function PlayBasicSpottedResponse( KFPawn_Human Spotter, KFPawn_Monster Spotted, int SpottedEventID )
{
local int ResponseEventID;
if( Spotted == none )
{
return;
}
switch( SpottedEventID )
{
case `SPOTZ_Siren:
ResponseEventID = `SPOTZ_FemaleResp;
break;
case `SPOTZ_Fleshpound:
case `SPOTZ_Scrake:
case `SPOTZ_BossGeneric: // ?
case `SPOTZ_BossFemale: // ?
ResponseEventID = `SPOTZ_BigZedResp;
break;
case `SPOTZ_Generic:
ResponseEventID =`SPOTZ_GenericResp;
break;
default:
return; // do nothing
};
PlayBasicDialogResponse( Spotter, SpottedEventID, ResponseEventID, /*Responder*/, Spotted, true );
}
/************************************************
* Situational Dialog
************************************************/
/** Checks if any player should play idle dialog */
function CheckIdleDialog()
{
local KFPawn_Human KFPH;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
if( KFPH.TimeSpentIdling() > IdleTimeForSituationalDialog )
{
PlaySituationalDialog( KFPH );
}
}
}
/** Plays idle/situational dialog for a given speaker to a target player */
function PlaySituationalDialog( KFPawn_Human Speaker )
{
local KFPawn_Human Target;
local class< KFPawnVoiceGroupEventData > EventDataClass;
EventDataClass = Speaker.GetVoiceGroupEventDataClass();
foreach WorldInfo.AllPawns( class'KFPawn_Human', Target )
{
if( Target == Speaker || !PawnIsValidPlayer(Target) )
{
continue;
}
if( PawnCanSpotPawn(Speaker, Target, GetEventRadiusSq(`SITU_General, EventDataClass), GetEventFOV(`SITU_General, EventDataClass)) )
{
break;
}
}
PlayRandomSituationalDialog( Speaker, Target );
}
/** Calculates situational dialog options, then plays a random one */
function PlayRandomSituationalDialog( KFPawn_Human Speaker, KFPawn_Human Target )
{
local int NumPlayers, NumLivingPlayers;
local int NumOptions, BestOptionID;
NumPlayers = WorldInfo.Game.NumPlayers;
NumLivingPlayers = KFGameInfo( WorldInfo.Game ).GetLivingPlayerCount();
BestOptionID = -1;
// add option for environmental dialog (dark, cold, etc.) if player is within certain volumes
if( Speaker.EnvironmentDialogEventID > 0 )
{
AddRandomDialogOption( Speaker, Speaker.EnvironmentDialogEventID, NumOptions, BestOptionID );
}
// if speaker has someone to speak to, play conversational dialog
if( Target != none )
{
if( Target.MyKFWeapon != none && Target.MyKFWeapon.GetMaxAmmoAmount(0) > 0 )
{
AddRandomDialogOption( Speaker, `SITU_AmmoCheck, NumOptions, BestOptionID );
}
AddRandomDialogOption( Speaker, `SITU_HealthCheck, NumOptions, BestOptionID );
AddRandomDialogOption( Speaker, `SITU_General, NumOptions, BestOptionID );
if( NumLivingPlayers == NumPlayers )
{
AddRandomDialogOption( Speaker, `SITU_AllAlive, NumOptions, BestOptionID );
}
else if( NumLivingPlayers > NumPlayers / 2 )
{
AddRandomDialogOption( Speaker, `SITU_MostAlive, NumOptions, BestOptionID );
}
else if( NumLivingPlayers == NumPlayers / 2 )
{
AddRandomDialogOption( Speaker, `SITU_HalfAlive, NumOptions, BestOptionID );
}
if( Speaker.PlayerReplicationInfo.Score >= IdleHighDoshThreshold )
{
AddRandomDialogOption( Speaker, `SITU_LotsOfDosh, NumOptions, BestOptionID );
}
else if( Speaker.PlayerReplicationInfo.Score <= IdleLowDoshThreshold )
{
AddRandomDialogOption( Speaker, `SITU_LittleDosh, NumOptions, BestOptionID );
}
PlayDialogEvent( Speaker, BestOptionID );
PlayCustomDialogResponse( Speaker, Target, BestOptionID, PlayRandomSituationalResponse, true );
}
// otherwise talk to self
else if( NumPlayers > 1 && NumLivingPlayers == 1 )
{
AddRandomDialogOption( Speaker, `SITU_TalkSelf, NumOptions, BestOptionID );
}
PlayDialogEvent( Speaker, BestOptionID );
}
/** Plays response to random situational dialog */
function PlayRandomSituationalResponse( const out DialogResponseInfo RespInfo )
{
local int ResponseID, ResponderDoshAmt;
local float ResponderHealthPct, ResponderAmmoPCt;
ResponseID = -1;
switch( RespInfo.RespondingToID )
{
case `SITU_HealthCheck:
ResponderHealthPct = RespInfo.Speaker.GetHealthPercentage();
if( ResponderHealthPct >= NeedNoMoreHealingPctThreshold )
{
ResponseID = `SITU_HealthCheckRespHigh;
}
else if( ResponderHealthPct < NeedMoreHealingPctThreshold )
{
ResponseID = `SITU_HealthCheckRespLow;
}
break;
case `SITU_AmmoCheck:
ResponderAmmoPCt = RespInfo.Speaker.MyKFWeapon.GetAmmoPercentage();
if( ResponderAmmoPct >= 0.f )
{
if( ResponderAmmoPct > IdleHighAmmoPctThreshold)
{
ResponseID = `SITU_AmmoCheckRespHigh;
}
else if( ResponderAmmoPCt < IdleLowAmmoPctThreshold)
{
ResponseID = `SITU_AmmoCheckRespLow;
}
}
break;
case `SITU_AllAlive:
ResponseID = `SITU_AllAliveResp;
break;
case `SITU_HalfAlive:
ResponseID = `SITU_HalfAliveResp;
break;
case `SITU_MostAlive:
ResponseID = `SITU_MostAliveResp;
break;
case `SITU_LotsOfDosh:
ResponderDoshAmt = RespInfo.Speaker.PlayerReplicationInfo.Score;
if( ResponderDoshAmt > IdleHighDoshThreshold)
{
ResponseID = `SITU_LotsOfDoshRespLots;
}
else if( ResponderDoshAmt < IdleLowDoshThreshold )
{
ResponseID = `SITU_LotsOfDoshRespLittle;
}
break;
case `SITU_LittleDosh:
ResponderDoshAmt = RespInfo.Speaker.PlayerReplicationInfo.Score;
if( ResponderDoshAmt < IdleLowDoshThreshold )
{
ResponseID = `SITU_LittleDoshResp;
}
break;
};
if( ResponseID >= 0 )
{
PlayDialogEvent( RespInfo.Speaker, ResponseID );
}
}
/** Plays the "wave start" dialog from a random pawn */
function PlayWaveStartDialog( bool bBossWave )
{
local int RandomPlayer;
local KFPawn_Human KFPH;
RandomPlayer = rand( KFGameInfo(WorldInfo.Game).GetLivingPlayerCount() );
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( PawnIsValidPlayer(KFPH) && (RandomPlayer-- == 0) )
{
break;
}
}
if( bBossWave )
{
PlayDialogEvent( KFPH, `SITU_WaveStartBoss );
}
else
{
PlayDialogEvent( KFPH, `SITU_WaveStartGeneral );
}
}
function PlayEnvironmentalDialog( KFPawn_Human Speaker, int EventID )
{
PlayDialogEvent( Speaker, EventID );
}
/************************************************
* Objective Dialog
************************************************/
function PlayAcceptObjectiveDialog( array<PlayerReplicationInfo> PRIsInObj )
{
local array< KFPawn_Human > PawnsInObj;
GetHumanPawnsInObj( PawnsInObj, PRIsInObj );
PlayObjectiveDialogOnRandomPlayers( PawnsInObj, `OBJ_Accept );
}
function PlayDeclineObjectiveDialog( array<PlayerReplicationInfo> PRIsInObj )
{
local array< KFPawn_Human > PawnsInObj;
GetHumanPawnsInObj( PawnsInObj, PRIsInObj );
PlayObjectiveDialogOnRandomPlayers( PawnsInObj, `OBJ_Decline );
}
function PlayWinObjectiveDialog( array<PlayerReplicationInfo> PRIsInObj )
{
local array< KFPawn_Human > PawnsInObj;
GetHumanPawnsInObj( PawnsInObj, PRIsInObj );
PlayObjectiveDialogOnRandomPlayers( PawnsInObj, `OBJ_Win, `OBJ_WinResp );
}
function PlayLoseObjectiveDialog( array<PlayerReplicationInfo> PRIsInObj )
{
local array< KFPawn_Human > PawnsInObj;
GetHumanPawnsInObj( PawnsInObj, PRIsInObj );
PlayObjectiveDialogOnRandomPlayers( PawnsInObj, `OBJ_Lose, `OBJ_LoseResp );
}
/** Fills an "out" array with pawns that are in an objective given the PRIs in the objective */
function GetHumanPawnsInObj( out array<KFPawn_Human> out_PawnsInObj, const out array<PlayerReplicationInfo> PRIsInObj )
{
local KFPawn_Human KFPH;
local int PRIIdx;
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( PawnIsValidPlayer(KFPH) )
{
PRIIdx = PRIsInObj.Find( KFPH.PlayerReplicationInfo );
if( PRIIdx != INDEX_None )
{
out_PawnsInObj.AddItem( KFPH );
}
}
}
}
/** Plays dialog for random player within an objective */
function PlayObjectiveDialogOnRandomPlayers( array<KFPawn_Human> PawnsInObj, int EventID, optional int ResponseID = -1 )
{
local int RandIdx;
local KFPawn_Human Speaker, Responder;
RandIdx = Rand( PawnsInObj.Length );
Speaker = PawnsInObj[RandIdx];
PawnsInObj.Remove( RandIdx, 1 );
if( Speaker != none )
{
PlayDialogEvent( Speaker, EventID );
}
if( ResponseID < 0 )
{
return;
}
RandIdx = Rand( PawnsInObj.Length );
Responder = PawnsInObj[RandIdx];
if( Responder != none )
{
PlayBasicDialogResponse( Speaker, EventID, ResponseID, Responder,, true );
}
}
function PlayDefendObjDialog( byte PrevObjProgress, byte CurrObjProgress )
{
local KFPawn_Human KFPH;
// check obj progress
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
{
if( !PawnIsValidPlayer(KFPH) )
{
continue;
}
if( CurrObjProgress == 0 )
{
PlayDialogEvent( KFPH, `OBJ_DefendAStart );
}
else if( PrevObjProgress < 100 && CurrObjProgress >= 100 )
{
PlayDialogEvent( KFPH, `OBJ_Win );
}
else if( PrevObjProgress < 50 && CurrObjProgress >= 50 )
{
PlayDialogEvent( KFPH, `OBJ_DefendAAlmostSecured );
}
}
}
/************************************************
* Favorite Weapons Dialog
************************************************/
function PlaySwitchToFavoriteWeaponDialog( KFPawn_Human Speaker )
{
local int FavoriteWeaponIndex;
local KFCharacterInfo_Human KFCI;
if( Speaker == none || Speaker.Weapon == none )
{
// wut?
return;
}
KFCI = KFCharacterInfo_Human( Speaker.CharacterArch );
if( KFCI == none )
{
return;
}
FavoriteWeaponIndex = KFCI.GetFavoriteWeaponIndexOf( Speaker.Weapon );
if( FavoriteWeaponIndex < 0 )
{
return;
}
PlayDialogEvent( Speaker, `ACT_SelectFaveWeap1 + FavoriteWeaponIndex );
}
/************************************************
* Voice Command Dialog
************************************************/
function PlayVoiceCommandDialog( KFPawn Speaker, int CommandIndex )
{
switch( CommandIndex )
{
case 0:
PlayDialogEvent( Speaker, `COMM_RequestHeal);
break;
case 1:
PlayDialogEvent( Speaker, `COMM_RequestDosh);
break;
case 2:
PlayDialogEvent( Speaker, `COMM_RequestHelp);
break;
case 3:
PlayDialogEvent( Speaker, `COMM_InsultZeds);
break;
case 4:
PlayDialogEvent( Speaker, `COMM_FollowMe);
break;
case 5:
PlayDialogEvent( Speaker, `COMM_GetToTrader);
break;
case 6:
PlayDialogEvent( Speaker, `COMM_ConfirmGeneric);
break;
case 7:
PlayDialogEvent( Speaker, `COMM_DenyGeneric);
break;
case 9:
PlayDialogEvent(Speaker, `COMM_ThankYou);
break;
};
}
/************************************************
* Common Boss Dialog
************************************************/
function PlayBossMonologue( KFPawn Boss, byte MonologueType )
{
if( ETheatricType(MonologueType) == THEATRIC_Entrance )
{
PlayDialogEvent( Boss, `BOSS_Intro );
}
else if( ETheatricType(MonologueType) == THEATRIC_Victory )
{
PlayDialogEvent( Boss, `BOSS_Victory );
}
}
function PlayBossLoseSightOfDialog( KFPawn_Monster BossSpeaker, KFPawn_Human Target )
{
local class< KFPawnVoiceGroupEventData > EventDataClass;
EventDataClass = BossSpeaker.GetVoiceGroupEventDataClass();
if( ActorWithinPawnFOV(Target, BossSpeaker, GetEventFOV(`BOSS_LoseTarget, EventDataClass)) )
{
PlayDialogEvent( BossSpeaker, `BOSS_LoseTarget );
}
}
function PlayBossChallengeDialog( KFPawn Boss )
{
PlayDialogEvent( Boss, `BOSS_Challenge );
}
function PlayBossGrabDialog( KFPawn Boss )
{
PlayDialogEvent( Boss, `BOSS_Grab );
}
function PlayBossHealDialog( KFPawn Boss )
{
PlayDialogEvent( Boss, `BOSS_Heal );
}
function PlayBossTakeDamageDialog( KFPawn Boss )
{
if( float(Boss.Health) / float(Boss.HealthMax) < 0.25f )
{
PlayDialogEvent( Boss, `BOSS_TakeDmg_LT25 );
}
else
{
PlayDialogEvent( Boss, `BOSS_TakeDmgBase );
}
}
function PlayBossDeathDialog( KFPawn Boss )
{
PlayDialogEvent( Boss, `BOSS_Death );
}
function PlayBossLeapedDialog( KFPawn Boss )
{
PlayDialogEvent( Boss, `BOSS_Jump );
}
function PlayBossLandedDialog( KFPawn Boss )
{
PlayDialogEvent( Boss, `BOSS_Land );
}
/************************************************
* Hans Dialog
************************************************/
function PlayHansTickDialog( KFPawn Hans )
{
local int NumPlayers, NumLivingPlayers, NumOptions, BestOptionID;
local float PlayersAlivePct, DistToTargetSq, MaxDistToTargetSq;
local KFPawn_ZedHansBase HansBase;
BestOptionID = -1;
if( Hans.bIsSprinting && Hans.MyKFAIC.Enemy != none )
{
DistToTargetSq = VSizeSq(Hans.Location - Hans.MyKFAIC.Enemy.Location);
MaxDistToTargetSq = GetEventRadiusSq( `HANS_SprintToward, Hans.GetVoiceGroupEventDataClass() );
if( DistToTargetSq <= MaxDistToTargetSq )
{
AddRandomDialogOption( Hans, `HANS_SprintToward, NumOptions, BestOptionID );
}
}
NumPlayers = WorldInfo.Game.NumPlayers;
NumLivingPlayers = KFGameInfo( WorldInfo.Game ).GetLivingPlayerCount();
if( NumLivingPlayers > 0 )
{
HansBase = KFPawn_ZedHansBase( Hans );
if( HansBase != none && HansBase.bInHuntAndHealMode )
{
AddRandomDialogOption( Hans, `HANS_HuntTaunt, NumOptions, BestOptionID );
}
PlayersAlivePct = float(NumLivingPlayers) / float(NumPlayers);
if( PlayersAlivePct >= 0.5 )
{
AddRandomDialogOption( Hans, `BOSS_TauntBase, NumOptions, BestOptionID );
}
else if( PlayersAlivePct >= 0.25 )
{
AddRandomDialogOption( Hans, `BOSS_Taunt_LT50, NumOptions, BestOptionID );
}
else
{
AddRandomDialogOption( Hans, `BOSS_Taunt_LT25, NumOptions, BestOptionID );
// @todo: hook up taunt for unfrozen German soldier
}
}
PlayDialogEvent( Hans, BestOptionID );
}
function PlayHansDrawGunsDialog( KFPawn Hans )
{
PlayDialogEvent( Hans, `HANS_DrawGuns );
}
function PlayHansNadeDialog( KFPawn Hans, bool bBarrage )
{
if( bBarrage )
{
PlayDialogEvent( Hans, `HANS_NadeBarrage );
}
else
{
PlayDialogEvent( Hans, `HANS_NadeToss );
}
}
function PlayHansSmokeDialog( KFPawn Hans, bool bBarrage )
{
if( bBarrage )
{
PlayDialogEvent( Hans, `HANS_SmokeBarrage );
}
else
{
PlayDialogEvent( Hans, `HANS_SmokeToss );
}
}
function PlayHansGasDialog( KFPawn Hans, bool bBarrage )
{
if( bBarrage )
{
PlayDialogEvent( Hans, `HANS_GasBarrage );
}
else
{
PlayDialogEvent( Hans, `HANS_GasToss );
}
}
function PlayHansKilledDialog( KFPawn Hans, class<DamageType> DmgType )
{
local int NumOptions, BestOptionID;
BestOptionID = -1;
AddRandomDialogOption( Hans, `BOSS_KillBase, NumOptions, BestOptionID );
if( class<KFDamageType>(DmgType) != none )
{
AddRandomDialogOption( Hans, class<KFDamageType>(DmgType).static.GetKillerDialogID(), NumOptions, BestOptionID );
}
// @todo: figure out a way to make this less hard-coded? (see also KFAIController::DoStrike)
if( Hans.GetSpecialMoveTag() == 'Frenzy_Lunge' )
{
AddRandomDialogOption( Hans, `HANS_KillFrenzy, NumOptions, BestOptionID );
}
PlayDialogEvent( Hans, BestOptionID );
}
function PlayHansDamagePlayerDialog( KFPawn Hans, class<DamageType> DmgType )
{
if( DmgType.Name == 'KFDT_Ballistic_HansAK12' ) // Hans' MkB42s use this damage type at the moment. @todo: fix this if/when necessary
{
PlayDialogEvent( Hans, `HANS_DmgGuns );
}
}
function PlayHansFrenzyDialog( KFPawn Hans )
{
PlayDialogEvent( Hans, `HANS_Frenzy );
}
function PlayHansAOEDialog( KFPawn Hans )
{
PlayDialogEvent( Hans, `HANS_AOE );
}
function PlayHansBattlePhaseDialog( KFPawn Hans, int CurrBattlePhase )
{
if( !Hans.IsAliveAndWell() )
{
return;
}
switch( CurrBattlePhase )
{
case 2:
PlayDialogEvent( Hans, `HANS_NextBattlePhase1 );
break;
case 3:
PlayDialogEvent( Hans, `HANS_NextBattlePhase2 );
break;
case 4:
PlayDialogEvent( Hans, `HANS_NextBattlePhase3 );
break;
};
}
/************************************************
* Patriarch Dialog
************************************************/
function PlayPatriarchTickDialog( KFPawn Patty )
{
local int NumPlayers, NumLivingPlayers, NumOptions, BestOptionID;
local float PlayersAlivePct;
BestOptionID = -1;
NumPlayers = WorldInfo.Game.NumPlayers;
NumLivingPlayers = KFGameInfo( WorldInfo.Game ).GetLivingPlayerCount();
if( NumLivingPlayers > 0 )
{
PlayersAlivePct = float(NumLivingPlayers) / float(NumPlayers);
if( PlayersAlivePct >= 0.5 )
{
AddRandomDialogOption( Patty, `BOSS_TauntBase, NumOptions, BestOptionID );
}
else if( PlayersAlivePct >= 0.25 )
{
AddRandomDialogOption( Patty, `BOSS_Taunt_LT50, NumOptions, BestOptionID );
}
else
{
AddRandomDialogOption( Patty, `BOSS_Taunt_LT25, NumOptions, BestOptionID );
}
}
PlayDialogEvent( Patty, BestOptionID );
}
function PlayPattyMinigunWarnDialog( KFPawn Patty )
{
PlayDialogEvent( Patty, `PATTY_MinigunWarn );
}
function PlayPattyMinigunAttackDialog( KFPawn Patty )
{
PlayDialogEvent( Patty, `PATTY_MinigunAttack );
}
function PlayPattyTentaclePullDialog( KFPawn Patty )
{
PlayDialogEvent( Patty, `PATTY_TentaclePull );
}
function PlayPattyChildKilledDialog( KFPawn Patty )
{
PlayDialogEvent( Patty, `PATTY_ChildKilled );
}
function PlayPattyKilledDialog( KFPawn Patty, class<DamageType> DmgType )
{
local int NumOptions, BestOptionID;
BestOptionID = -1;
AddRandomDialogOption( Patty, `BOSS_KillBase, NumOptions, BestOptionID );
if( class<KFDamageType>(DmgType) != none )
{
AddRandomDialogOption( Patty, class<KFDamageType>(DmgType).static.GetKillerDialogID(), NumOptions, BestOptionID );
}
PlayDialogEvent( Patty, BestOptionID );
}
function PlayPattyBattlePhaseDialog( KFPawn Patty, int CurrBattlePhase )
{
if( !Patty.IsAliveAndWell() )
{
return;
}
switch( CurrBattlePhase )
{
case 1:
PlayDialogEvent( Patty, `PATTY_NextBattlePhase1 );
break;
case 2:
PlayDialogEvent( Patty, `PATTY_NextBattlePhase2 );
break;
case 3:
PlayDialogEvent( Patty, `PATTY_NextBattlePhase3 );
break;
};
}
function PlayPattyWhirlwindDialog( KFPawn Patty )
{
PlayDialogEvent( Patty, `PATTY_WhirlwindAttack );
}
/************************************************
* Matriarch Dialog
************************************************/
function PlayMatriarchBattlePhaseDialog(KFPawn Matriarch, int CurrentBattlePhase)
{
if (!Matriarch.IsAliveAndWell())
{
return;
}
switch (CurrentBattlePhase)
{
case 1:
PlayDialogEvent(Matriarch, `MATTY_NextBattlePhase1);
break;
case 2:
PlayDialogEvent(Matriarch, `MATTY_NextBattlePhase2);
break;
case 3:
PlayDialogEvent(Matriarch, `MATTY_NextBattlePhase3);
break;
};
}
function PlayMatriarchTickDialog(KFPawn Matriarch, int ArmorZoneStatus, int CurrentBattlePhase)
{
switch (CurrentBattlePhase)
{
case 0:
PlayDialogEvent(Matriarch, `MATTY_Taunt_Phase1);
break;
case 1:
PlayDialogEvent(Matriarch, `MATTY_Taunt_Phase2);
break;
case 2:
PlayDialogEvent(Matriarch, `MATTY_Taunt_Phase3);
break;
case 3:
PlayDialogEvent(Matriarch, `MATTY_Taunt_Phase4);
break;
};
}
function PlayMatriarchExplodeArmorDialog(KFPawn Matriarch, name ArmorZoneName)
{
switch (ArmorZoneName)
{
case 'head':
PlayDialogEvent(Matriarch, `MATTY_TauntPilotCompartDestroyed);
break;
case 'claw':
PlayDialogEvent(Matriarch, `MATTY_TauntPowerClawDestroyed);
break;
};
}
function PlayMattyKilledDialog(KFPawn Matriarch)
{
if (Matriarch.IsDoingSpecialMove(SM_Custom1))
{
PlayDialogEvent(Matriarch, `MATTY_SweepingClawKill);
}
else if (Matriarch.IsDoingSpecialMove(SM_StandAndShootAttack))
{
PlayDialogEvent(Matriarch, `MATTY_TeslaBlastKill);
}
else if (Matriarch.IsDoingSpecialMove(SM_HoseWeaponAttack))
{
PlayDialogEvent(Matriarch, `MATTY_PlasmaCannonKill);
}
else if (Matriarch.IsDoingSpecialMove(SM_Custom2))
{
PlayDialogEvent(Matriarch, `MATTY_LightningStormKill);
}
else if (Matriarch.IsDoingSpecialMove(SM_GrappleAttack))
{
PlayDialogEvent(Matriarch, `MATTY_ScorpionWhipKill);
}
else if (Matriarch.IsDoingSpecialMove(SM_SonicAttack))
{
PlayDialogEvent(Matriarch, `MATTY_WarningSirenKill);
}
}
function PlayMattyMinionKilledDialog(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_DeadEDAR);
}
function PlayMatriarchSweepingClawEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_SweepingClawAttack);
}
function PlayMatriarchTeslaBlastEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_TeslaBlastWarning);
}
function PlayMatriarchPlasmaCannonEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_PlasmaCannonWarning);
}
function PlayMatriarchLightningStormEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_LightningStormWarning);
}
function PlayMatriarchScorpionWhipEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_ScorpionWhipWarning);
}
function PlayMatriarchWarningSirenEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_WarningSirenWarning);
}
function PlayMatriarchShieldUpEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_ShieldActivate);
}
function PlayMatriarchCloakedEvent(KFPawn Matriarch)
{
PlayDialogEvent(Matriarch, `MATTY_Cloak);
}
DefaultProperties
{
bEnabled=true
InterruptPriorityThreshold=2
InterruptedByAnyPriorityThreshold=7
WeldAboutToBreakThreshold=60
NeedMoreHealingPctThreshold=0.75
NeedNoMoreHealingPctThreshold=0.75
CaughtSomeDoshAmt=50
CaughtMuchDoshAmt=250
NumZedsForPressureReload=1
LowSpareAmmoPctThreshold=0.15
NumKillsForOnARoll=6
CloseCallKillHealthPctThreshold=0.3
NumHitsForDeadHorse=3
TimeBetweenHitsForDeadHorse=1.0
TimeForContinuousDamageThreshold=3.0
TimeBetweenHitsForContinuousDamage=0.75
PlayerHealthPctForNearDeath=0.2
PlayerTakeDamageStreakInterval=1.0
PlayerTakeDamageStreakPctForScream=0.25
IdleTimeforSpottingZed=5.f
SpotLargeHordeNumZeds=6
ZedAlmostDeadHealthPctThreshold=0.3
SprintTowardZedDuration=1.5
NumZedsInAreaForMassacre=9
AreaRadiusForMassacre=2500.0
TimeLimitForMassacre=30.0
IdleTimeForSituationalDialog=7.0
IdleLowDoshThreshold=350
IdleHighDoshThreshold=3500
IdleLowAmmoPctThreshold=0.2
IdleHighAmmoPctThreshold=0.3
TimeUntilStartSprintPanting=3.0
// global ak events
StopBreathingAkEvent=AkEvent'WW_GLO_Runtime.Stop_Breathing'
}