//============================================================================= // 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 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, 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(DamageType) != none ) { AddRandomDialogOption( Killer, class(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 ) { 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(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(DamageType) != none ) { AddRandomDialogOption( Player, class(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 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 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 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 PRIsInObj ) { local array< KFPawn_Human > PawnsInObj; GetHumanPawnsInObj( PawnsInObj, PRIsInObj ); PlayObjectiveDialogOnRandomPlayers( PawnsInObj, `OBJ_Accept ); } function PlayDeclineObjectiveDialog( array PRIsInObj ) { local array< KFPawn_Human > PawnsInObj; GetHumanPawnsInObj( PawnsInObj, PRIsInObj ); PlayObjectiveDialogOnRandomPlayers( PawnsInObj, `OBJ_Decline ); } function PlayWinObjectiveDialog( array PRIsInObj ) { local array< KFPawn_Human > PawnsInObj; GetHumanPawnsInObj( PawnsInObj, PRIsInObj ); PlayObjectiveDialogOnRandomPlayers( PawnsInObj, `OBJ_Win, `OBJ_WinResp ); } function PlayLoseObjectiveDialog( array 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 out_PawnsInObj, const out array 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 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 DmgType ) { local int NumOptions, BestOptionID; BestOptionID = -1; AddRandomDialogOption( Hans, `BOSS_KillBase, NumOptions, BestOptionID ); if( class(DmgType) != none ) { AddRandomDialogOption( Hans, class(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 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 DmgType ) { local int NumOptions, BestOptionID; BestOptionID = -1; AddRandomDialogOption( Patty, `BOSS_KillBase, NumOptions, BestOptionID ); if( class(DmgType) != none ) { AddRandomDialogOption( Patty, class(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' }