IW4-Dump-Files/animscripts/battlechatter.gsc

4588 lines
110 KiB
Plaintext

/****************************************************************************
battleChatter.gsc
Basic concepts: Battle chatter events work on a queue system. Events are
added the AI's queue, and script calls to playBattleChatter(), scattered
throughout the animscripts, give the AI oppurtunities to play the events.
Events have an expiration time; if there are no calls to playBattleChatter
before an event expires, it will not play.
Script calls, usually within animscripts or battleChatter_ai::*Waiter()
functions, call the add*Event(); functions to add a voice event to the
AI's queue.
Since an AI can have multiple events in it's queue at a give time, there
is a priority system in place to help the AI choose which events get added
to the queue and which events it will play. Events with a priority of 1
will always be added to the queue (unless battlechatter is disabled on the
AI)
*****************************************************************************/
#include common_scripts\utility;
#include maps\_utility;
#include animscripts\utility;
#include animscripts\battlechatter_ai;
/****************************************************************************
initialization
*****************************************************************************/
// Initializes the battle chatter system
init_battleChatter()
{
if ( IsDefined( anim.chatInitialized ) && anim.chatInitialized )
return;
SetDvarIfUninitialized( "bcs_enable", "on" );
if ( GetDvar( "bcs_enable", "on" ) == "off" )
{
anim.chatInitialized = false;
anim.player.chatInitialized = false;
return;
}
anim.chatInitialized = true;
anim.player.chatInitialized = false;
SetDvarIfUninitialized( "bcs_filterThreat", "off" );
SetDvarIfUninitialized( "bcs_filterInform", "off" );
SetDvarIfUninitialized( "bcs_filterOrder", "off" );
SetDvarIfUninitialized( "bcs_filterReaction", "off" );
SetDvarIfUninitialized( "bcs_filterResponse", "off" );
SetDvarIfUninitialized( "bcs_forceEnglish", "0" );
// useful if you only have one voice actor's set of aliases and need to test responses
SetDvarIfUninitialized( "bcs_allowsamevoiceresponse", "off" );
SetDvarIfUninitialized( "debug_bcprint", "off" );
SetDvarIfUninitialized( "debug_bcprintdump", "off" );
SetDvarIfUninitialized( "debug_bcprintdumptype", "csv" );
SetDvarIfUninitialized( "debug_bcshowqueue", "off" );
/#
SetDvarIfUninitialized( "debug_bcthreat", "off" );
SetDvarIfUninitialized( "debug_bcresponse", "off" );
SetDvarIfUninitialized( "debug_bcorder", "off" );
SetDvarIfUninitialized( "debug_bcinform", "off" );
SetDvarIfUninitialized( "debug_bcdrawobjects", "off" );
SetDvarIfUninitialized( "debug_bcinteraction", "off" );
#/
anim.bcPrintFailPrefix = "^3***** BCS FAILURE: ";
anim.bcPrintWarnPrefix = "^3***** BCS WARNING: ";
bcs_setup_teams_array();
bcs_setup_countryIDs();
// Player Name IDs:
// - these IDs map to whatever the sound dept. uses for the player name alias
// (ex. "US_name_player_1" would make our id = 1)
anim.playerNameIDs[ "american" ] = "1";
anim.playerNameIDs[ "seal" ] = "1";
anim.playerNameIDs[ "taskforce" ] = "1";
anim.playerNameIDs[ "secretservice" ] = "1";
thread setPlayerBcNameID();
// AI Name IDs
// - update these when the number or ID(s) of voice actors for any nationality changes!
anim.usedIDs = [];
anim.usedIDs[ "russian" ] = [];
anim.usedIDs[ "russian" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "russian" ][ 0 ].count = 0;
anim.usedIDs[ "russian" ][ 0 ].npcID = "0";
anim.usedIDs[ "russian" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "russian" ][ 1 ].count = 0;
anim.usedIDs[ "russian" ][ 1 ].npcID = "1";
anim.usedIDs[ "russian" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "russian" ][ 2 ].count = 0;
anim.usedIDs[ "russian" ][ 2 ].npcID = "2";
anim.usedIDs[ "russian" ][ 3 ] = SpawnStruct();
anim.usedIDs[ "russian" ][ 3 ].count = 0;
anim.usedIDs[ "russian" ][ 3 ].npcID = "3";
anim.usedIDs[ "russian" ][ 4 ] = SpawnStruct();
anim.usedIDs[ "russian" ][ 4 ].count = 0;
anim.usedIDs[ "russian" ][ 4 ].npcID = "4";
anim.usedIDs[ "portuguese" ] = [];
anim.usedIDs[ "portuguese" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "portuguese" ][ 0 ].count = 0;
anim.usedIDs[ "portuguese" ][ 0 ].npcID = "0";
anim.usedIDs[ "portuguese" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "portuguese" ][ 1 ].count = 0;
anim.usedIDs[ "portuguese" ][ 1 ].npcID = "1";
anim.usedIDs[ "portuguese" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "portuguese" ][ 2 ].count = 0;
anim.usedIDs[ "portuguese" ][ 2 ].npcID = "2";
anim.usedIDs[ "shadowcompany" ] = [];
anim.usedIDs[ "shadowcompany" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "shadowcompany" ][ 0 ].count = 0;
anim.usedIDs[ "shadowcompany" ][ 0 ].npcID = "0";
anim.usedIDs[ "shadowcompany" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "shadowcompany" ][ 1 ].count = 0;
anim.usedIDs[ "shadowcompany" ][ 1 ].npcID = "1";
anim.usedIDs[ "shadowcompany" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "shadowcompany" ][ 2 ].count = 0;
anim.usedIDs[ "shadowcompany" ][ 2 ].npcID = "2";
anim.usedIDs[ "shadowcompany" ][ 3 ] = SpawnStruct();
anim.usedIDs[ "shadowcompany" ][ 3 ].count = 0;
anim.usedIDs[ "shadowcompany" ][ 3 ].npcID = "3";
anim.usedIDs[ "british" ] = [];
anim.usedIDs[ "british" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "british" ][ 0 ].count = 0;
anim.usedIDs[ "british" ][ 0 ].npcID = "0";
anim.usedIDs[ "british" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "british" ][ 1 ].count = 0;
anim.usedIDs[ "british" ][ 1 ].npcID = "1";
anim.usedIDs[ "british" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "british" ][ 2 ].count = 0;
anim.usedIDs[ "british" ][ 2 ].npcID = "2";
anim.usedIDs[ "american" ] = [];
anim.usedIDs[ "american" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "american" ][ 0 ].count = 0;
anim.usedIDs[ "american" ][ 0 ].npcID = "0";
anim.usedIDs[ "american" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "american" ][ 1 ].count = 0;
anim.usedIDs[ "american" ][ 1 ].npcID = "1";
anim.usedIDs[ "american" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "american" ][ 2 ].count = 0;
anim.usedIDs[ "american" ][ 2 ].npcID = "2";
anim.usedIDs[ "american" ][ 3 ] = SpawnStruct();
anim.usedIDs[ "american" ][ 3 ].count = 0;
anim.usedIDs[ "american" ][ 3 ].npcID = "3";
anim.usedIDs[ "american" ][ 4 ] = SpawnStruct();
anim.usedIDs[ "american" ][ 4 ].count = 0;
anim.usedIDs[ "american" ][ 4 ].npcID = "4";
anim.usedIDs[ "seal" ] = [];
anim.usedIDs[ "seal" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "seal" ][ 0 ].count = 0;
anim.usedIDs[ "seal" ][ 0 ].npcID = "0";
anim.usedIDs[ "seal" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "seal" ][ 1 ].count = 0;
anim.usedIDs[ "seal" ][ 1 ].npcID = "1";
anim.usedIDs[ "seal" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "seal" ][ 2 ].count = 0;
anim.usedIDs[ "seal" ][ 2 ].npcID = "2";
anim.usedIDs[ "seal" ][ 3 ] = SpawnStruct();
anim.usedIDs[ "seal" ][ 3 ].count = 0;
anim.usedIDs[ "seal" ][ 3 ].npcID = "3";
anim.usedIDs[ "taskforce" ] = [];
anim.usedIDs[ "taskforce" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "taskforce" ][ 0 ].count = 0;
anim.usedIDs[ "taskforce" ][ 0 ].npcID = "0";
anim.usedIDs[ "taskforce" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "taskforce" ][ 1 ].count = 0;
anim.usedIDs[ "taskforce" ][ 1 ].npcID = "1";
anim.usedIDs[ "taskforce" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "taskforce" ][ 2 ].count = 0;
anim.usedIDs[ "taskforce" ][ 2 ].npcID = "2";
anim.usedIDs[ "secretservice" ] = [];
anim.usedIDs[ "secretservice" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "secretservice" ][ 0 ].count = 0;
anim.usedIDs[ "secretservice" ][ 0 ].npcID = "0";
anim.usedIDs[ "secretservice" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "secretservice" ][ 1 ].count = 0;
anim.usedIDs[ "secretservice" ][ 1 ].npcID = "1";
anim.usedIDs[ "secretservice" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "secretservice" ][ 2 ].count = 0;
anim.usedIDs[ "secretservice" ][ 2 ].npcID = "2";
anim.usedIDs[ "secretservice" ][ 3 ] = SpawnStruct();
anim.usedIDs[ "secretservice" ][ 3 ].count = 0;
anim.usedIDs[ "secretservice" ][ 3 ].npcID = "3";
anim.usedIDs[ "arab" ] = [];
anim.usedIDs[ "arab" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "arab" ][ 0 ].count = 0;
anim.usedIDs[ "arab" ][ 0 ].npcID = "0";
anim.usedIDs[ "arab" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "arab" ][ 1 ].count = 0;
anim.usedIDs[ "arab" ][ 1 ].npcID = "1";
anim.usedIDs[ "arab" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "arab" ][ 2 ].count = 0;
anim.usedIDs[ "arab" ][ 2 ].npcID = "2";
anim.usedIDs[ "german" ] = [];
anim.usedIDs[ "german" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "german" ][ 0 ].count = 0;
anim.usedIDs[ "german" ][ 0 ].npcID = "0";
anim.usedIDs[ "german" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "german" ][ 1 ].count = 0;
anim.usedIDs[ "german" ][ 1 ].npcID = "1";
anim.usedIDs[ "german" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "german" ][ 2 ].count = 0;
anim.usedIDs[ "german" ][ 2 ].npcID = "2";
anim.usedIDs[ "italian" ] = [];
anim.usedIDs[ "italian" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "italian" ][ 0 ].count = 0;
anim.usedIDs[ "italian" ][ 0 ].npcID = "0";
anim.usedIDs[ "italian" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "italian" ][ 1 ].count = 0;
anim.usedIDs[ "italian" ][ 1 ].npcID = "1";
anim.usedIDs[ "italian" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "italian" ][ 2 ].count = 0;
anim.usedIDs[ "italian" ][ 2 ].npcID = "2";
anim.usedIDs[ "spanish" ] = [];
anim.usedIDs[ "spanish" ][ 0 ] = SpawnStruct();
anim.usedIDs[ "spanish" ][ 0 ].count = 0;
anim.usedIDs[ "spanish" ][ 0 ].npcID = "0";
anim.usedIDs[ "spanish" ][ 1 ] = SpawnStruct();
anim.usedIDs[ "spanish" ][ 1 ].count = 0;
anim.usedIDs[ "spanish" ][ 1 ].npcID = "1";
anim.usedIDs[ "spanish" ][ 2 ] = SpawnStruct();
anim.usedIDs[ "spanish" ][ 2 ].count = 0;
anim.usedIDs[ "spanish" ][ 2 ].npcID = "2";
init_flavorbursts();
// doesn't impact friendlyfire warnings normally played when battlechatter is on,
// just whether it plays when battlechatter is otherwise turned off
if ( !IsDefined( level.friendlyfire_warnings ) )
{
level.friendlyfire_warnings = false;
}
anim.eventTypeMinWait = [];
anim.eventTypeMinWait[ "threat" ] = [];
anim.eventTypeMinWait[ "response" ] = [];
anim.eventTypeMinWait[ "reaction" ] = [];
anim.eventTypeMinWait[ "order" ] = [];
anim.eventTypeMinWait[ "inform" ] = [];
anim.eventTypeMinWait[ "custom" ] = [];
anim.eventTypeMinWait[ "direction" ] = [];
// If you want to tweak how often battlechatter messages happen,
// this is place to do it.
// A priority of 1 will force an event to be added to the queue, and
// will make it override pre-existing events of the same type.
// times are in milliseconds
if ( IsDefined( level._stealth ) )
{
anim.eventActionMinWait[ "threat" ][ "self" ] = 20000;
anim.eventActionMinWait[ "threat" ][ "squad" ] = 30000;
}
else
{
anim.eventActionMinWait[ "threat" ][ "self" ] = 12500;
anim.eventActionMinWait[ "threat" ][ "squad" ] = 7500;
}
anim.eventActionMinWait[ "threat" ][ "location_repeat" ] = 5000;
anim.eventActionMinWait[ "response" ][ "self" ] = 1000;
anim.eventActionMinWait[ "response" ][ "squad" ] = 1000;
anim.eventActionMinWait[ "reaction" ][ "self" ] = 1000;
anim.eventActionMinWait[ "reaction" ][ "squad" ] = 1000;
anim.eventActionMinWait[ "order" ][ "self" ] = 8000;
anim.eventActionMinWait[ "order" ][ "squad" ] = 10000;
anim.eventActionMinWait[ "inform" ][ "self" ] = 6000;
anim.eventActionMinWait[ "inform" ][ "squad" ] = 8000;
anim.eventActionMinWait[ "custom" ][ "self" ] = 0;
anim.eventActionMinWait[ "custom" ][ "squad" ] = 0;
anim.eventTypeMinWait[ "playername" ] = 15000;
anim.eventTypeMinWait[ "reaction" ][ "casualty" ] = 14000;
anim.eventTypeMinWait[ "reaction" ][ "friendlyfire" ] = 5000;
anim.eventTypeMinWait[ "reaction" ][ "taunt" ] = 30000;
anim.eventTypeMinWait[ "inform" ][ "reloading" ] = 20000;
anim.eventTypeMinWait[ "inform" ][ "killfirm" ] = 15000;
anim.eventPriority[ "threat" ][ "infantry" ] = 0.5;
anim.eventPriority[ "threat" ][ "vehicle" ] = 0.7;
anim.eventPriority[ "response" ][ "ack" ] = 0.9;
anim.eventPriority[ "response" ][ "exposed" ] = 0.8;
anim.eventPriority[ "response" ][ "callout" ] = 0.9;
anim.eventPriority[ "response" ][ "echo" ] = 0.9;
anim.eventPriority[ "reaction" ][ "casualty" ] = 0.5;
anim.eventPriority[ "reaction" ][ "friendlyfire" ] = 1.0;
anim.eventPriority[ "reaction" ][ "taunt" ] = 0.9;
anim.eventPriority[ "order" ][ "action" ] = 0.3;
anim.eventPriority[ "order" ][ "move" ] = 0.3;
anim.eventPriority[ "order" ][ "displace" ] = 0.5;
anim.eventPriority[ "inform" ][ "attack" ] = 0.9;
anim.eventPriority[ "inform" ][ "incoming" ] = 0.9;
anim.eventPriority[ "inform" ][ "reloading" ] = 0.2;
anim.eventPriority[ "inform" ][ "suppressed" ] = 0.2;
anim.eventPriority[ "inform" ][ "killfirm" ] = 0.7;
anim.eventPriority[ "custom" ][ "generic" ] = 1.0;
anim.eventDuration[ "threat" ][ "infantry" ] = 1000;
anim.eventDuration[ "threat" ][ "vehicle" ] = 1000;
anim.eventDuration[ "response" ][ "exposed" ] = 2000;
anim.eventDuration[ "response" ][ "callout" ] = 2000;
anim.eventDuration[ "response" ][ "echo" ] = 2000;
anim.eventDuration[ "response" ][ "ack" ] = 1750;
anim.eventDuration[ "reaction" ][ "casualty" ] = 2000;
anim.eventDuration[ "reaction" ][ "friendlyfire" ] = 1000;
anim.eventDuration[ "reaction" ][ "taunt" ] = 2000;
anim.eventDuration[ "order" ][ "action" ] = 3000;
anim.eventDuration[ "order" ][ "move" ] = 3000;
anim.eventDuration[ "order" ][ "displace" ] = 3000;
anim.eventDuration[ "inform" ][ "attack" ] = 1000;
anim.eventDuration[ "inform" ][ "incoming" ] = 1500;
anim.eventDuration[ "inform" ][ "reloading" ] = 1000;
anim.eventDuration[ "inform" ][ "suppressed" ] = 2000;
anim.eventDuration[ "inform" ][ "killfirm" ] = 2000;
anim.eventDuration[ "custom" ][ "generic" ] = 1000;
// event chances are in % out of 100
anim.eventChance[ "response" ][ "exposed" ] = 75;
anim.eventChance[ "response" ][ "reload" ] = 65;
anim.eventChance[ "response" ][ "callout" ] = 75;
anim.eventChance[ "response" ][ "callout_negative" ] = 20;
anim.eventChance[ "response" ][ "order" ] = 40;
anim.eventChance[ "moveEvent" ][ "coverme" ] = 70;
anim.eventChance[ "moveEvent" ][ "ordertoplayer" ] = 10;
// flavor burst transmission tweakables
anim.fbt_desiredDistMax = 620;// try to keep fbts within this range from the player
anim.fbt_waitMin = 12;// time to wait between transmissions
anim.fbt_waitMax = 24;
anim.fbt_lineBreakMin = 2;// time to wait between lines
anim.fbt_lineBreakMax = 5;
anim.moveOrigin = Spawn( "script_origin", ( 0, 0, 0 ) );
// how many units from the player dudes can be and still chatter or be chattered about
if ( !IsDefined( level.bcs_maxTalkingDistFromPlayer ) )
{
level.bcs_maxTalkingDistFromPlayer = 1500;
}
if ( !IsDefined( level.bcs_maxThreatDistFromPlayer ) )
{
level.bcs_maxThreatDistFromPlayer = 2500;
}
// set up location triggers
maps\_bcs_location_trigs::bcs_location_trigs_init();
Assert( IsDefined( anim.bcs_locations ) );
anim.locationLastCalloutTimes = [];
// how long after starting some scripted dialogue will we wait before chattering? (milliseconds)
anim.scriptedDialogueBufferTime = 4000;
// how long before we can chatter about a threat again?
anim.bcs_threatResetTime = 3000;
/#
if ( GetDvar( "debug_bcdrawobjects" ) == "on" )
thread bcDrawObjects();
#/
anim.squadCreateFuncs[ anim.squadCreateFuncs.size ] = ::init_squadBattleChatter;
anim.squadCreateStrings[ anim.squadCreateStrings.size ] = "::init_squadBattleChatter";
foreach ( team in anim.teams )
{
anim.isTeamSpeaking[ team ] = false;
anim.isTeamSaying[ team ][ "threat" ] = false;
anim.isTeamSaying[ team ][ "order" ] = false;
anim.isTeamSaying[ team ][ "reaction" ] = false;
anim.isTeamSaying[ team ][ "response" ] = false;
anim.isTeamSaying[ team ][ "inform" ] = false;
anim.isTeamSaying[ team ][ "custom" ] = false;
}
bcs_setup_chatter_toggle_array();
// which nationalities can do flavor burst transmissions?
if ( !IsDefined( anim.flavorburstVoices ) )
{
anim.flavorburstVoices = [];
anim.flavorburstVoices[ "american" ] = true;
anim.flavorburstVoices[ "shadowcompany" ] = true;
anim.flavorburstVoices[ "seal" ] = false;
anim.flavorburstVoices[ "taskforce" ] = false;
anim.flavorburstVoices[ "secretservice" ] = false;
anim.flavorburstVoices[ "british" ] = false;
}
bcs_setup_flavorburst_toggle_array();
anim.lastTeamSpeakTime = [];
anim.lastNameSaid = [];
anim.lastNameSaidTime = [];
foreach ( team in anim.teams )
{
anim.lastTeamSpeakTime[ team ] = -50000;// so it doesnt pause if nobody has ever spoken
anim.lastNameSaid[ team ] = "none";
anim.lastNameSaidTime[ team ] = -100000;
}
// how long we'll wait before allowing use of a certain AI name again
anim.lastNameSaidTimeout = 10000;
for ( index = 0; index < anim.squadIndex.size; index++ )
{
if ( IsDefined( anim.squadIndex[ index ].chatInitialized ) && anim.squadIndex[ index ].chatInitialized )
continue;
anim.squadIndex[ index ] init_squadBattleChatter();
}
/*----------- THREAT CALLOUT CHANCES -----------
- anim.threatCallouts[] is indexed by the types of possible threat callouts for this AI,
and holds %chance weights that help determine if that the AI will
try to use that type of threat callout to alert players to a threat.
- These are matched against the values of self.allowedCallouts[] for each AI, to determine
whether the AI can use a particular kind of callout - not all nationalities get all callouts.
self.allowedCallouts gets set up for each AI in battlechatter_ai::init_aiBattleChatter.
- higher numbers mean a higher chance!
- zero values do not get considered.
- 100+ values are prioritized above everything except other 100+ values.
- chances are dicerolled against one another to find a winner, like this:
if( RandomInt( threatCallouts[ "player_yourclock" ] ) > RandomInt( threatCallouts[ "ai_yourclock ] ) )
rough priorities:
1. RPG
2. exposed
3. player_obvious
3. "catch-all":
1. player_your_clock
2. player_contact_clock
3. player_target_clock
4. ai_your_clock
5. player_cardinal
4. object (aka landmark):
1. player_object_yourclock
1. player_object_clock
2. ai_object_yourclock
5. location
1. player_location
2. ai_location
3. generic_location
-------------------------------------------------*/
anim.threatCallouts = [];
// RPG/exposed
anim.threatCallouts[ "rpg" ] = 100;// "RPG!"
anim.threatCallouts[ "exposed" ] = 25;// "Got a tango in the open!"
// "obvious" callouts
anim.threatCallouts[ "player_obvious" ] = 40;// "Player! Light 'em up!"( for 12 o'clock threats )
// player-relative callouts
anim.threatCallouts[ "player_yourclock" ] = 30;// "Player! Contact at your 10 o'clock!"
anim.threatCallouts[ "player_contact_clock" ] = 25;// "Player! Contact at 10 o'clock!"
anim.threatCallouts[ "player_target_clock" ] = 25;// "Player! Target, 10 o'clock!"
anim.threatCallouts[ "player_cardinal" ] = 20;// "Player! Contact, northwest!"
// "catch-all" callouts
anim.threatCallouts[ "ai_yourclock" ] = 25;// "Peas! Contact at your 10 o'clock!"
anim.threatCallouts[ "ai_contact_clock" ] = 20;
anim.threatCallouts[ "ai_target_clock" ] = 20;
anim.threatCallouts[ "ai_cardinal" ] = 10;
/* DEPRECATED
// object (aka landmark) callouts
anim.threatCallouts[ "player_object_yourclock" ] = 100;// "Player! Movement by the dumpster at your 10 o'clock!"
anim.threatCallouts[ "player_object_clock" ] = 100;// "Player! Movement by the dumpster at 10 o'clock!"
anim.threatCallouts[ "ai_object_yourclock" ] = 95;// "Peas! Movement by the dumpster at your 10 o'clock!"
*/
// location callouts
anim.threatCallouts[ "player_location" ] = 95;// ( Player - relative ) "Contact! 2nd floor window on the left!"
anim.threatCallouts[ "ai_location" ] = 100;// ( AI - relative ) "Contact! 2nd floor window on the left!"
anim.threatCallouts[ "generic_location" ] = 90;
anim.lastTeamThreatCallout = [];
anim.lastTeamThreatCalloutTime = [];
foreach ( team in anim.teams )
{
anim.lastTeamThreatCallout[ team ] = undefined;
anim.lastTeamThreatCalloutTime[ team ] = undefined;
}
anim.teamThreatCalloutLimitTimeout = 20000;
level notify( "battlechatter initialized" );
anim notify( "battlechatter initialized" );
}
bcs_setup_teams_array()
{
if ( !IsDefined( anim.teams ) )
{
anim.teams = [];
anim.teams[ anim.teams.size ] = "axis";
anim.teams[ anim.teams.size ] = "allies";
anim.teams[ anim.teams.size ] = "team3";
anim.teams[ anim.teams.size ] = "neutral";
}
}
bcs_setup_countryIDs()
{
if ( !IsDefined( anim.countryIDs ) )
{
anim.countryIDs[ "british" ] = "UK";
anim.countryIDs[ "american" ] = "US";
anim.countryIDs[ "seal" ] = "NS";
anim.countryIDs[ "taskforce" ] = "TF";
anim.countryIDs[ "secretservice" ] = "SS";
anim.countryIDs[ "russian" ] = "RU";
anim.countryIDs[ "arab" ] = "AB";
anim.countryIDs[ "german" ] = "GE";
anim.countryIDs[ "spanish" ] = "SP";
anim.countryIDs[ "italian" ] = "IT";
anim.countryIDs[ "portuguese" ] = "PG";
anim.countryIDs[ "shadowcompany" ] = "SC";
}
}
bcs_setup_chatter_toggle_array()
{
bcs_setup_teams_array();
if ( !IsDefined( level.battlechatter ) )
{
level.battlechatter = [];
foreach ( team in anim.teams )
{
set_battlechatter_variable( team, true );
}
}
}
bcs_setup_flavorburst_toggle_array()
{
bcs_setup_teams_array();
if ( !IsDefined( level.flavorbursts ) )
{
level.flavorbursts = [];
foreach ( team in anim.teams )
{
level.flavorbursts[ team ] = true;
}
}
}
init_flavorbursts()
{
// flavor burst transmission aliases
// update these as new transmissions are recorded
// (format of the fbt aliases is "FB_US_7_11",
// where the first number is what you put below)
anim.flavorbursts[ "american" ] = [];
numBursts = 41;
us = [];
if ( level.script == "roadkill" || level.script == "trainer" )
{
// we don't want to hear chatter about US locales when we're operating in another country
// (these correspond to sequence numbers in the aliases)
us[ us.size ] = 13;
us[ us.size ] = 15;
us[ us.size ] = 16;
us[ us.size ] = 19;
us[ us.size ] = 20;
us[ us.size ] = 30;
us[ us.size ] = 31;
us[ us.size ] = 33;
us[ us.size ] = 38;
}
for ( i = 0; i < numBursts; i++ )
{
// don't include US-specific aliases, if applicable
if ( us.size )
{
foundOne = false;
foreach ( sequenceNum in us )
{
if ( sequenceNum == i )
{
foundOne = true;
break;
}
}
if ( foundOne )
{
continue;
}
}
anim.flavorbursts[ "american" ][ i ] = string( i + 1 );
}
anim.flavorbursts[ "shadowcompany" ] = [];
numBursts = 9;
for ( i = 1; i <= numBursts; i++ )
{
anim.flavorbursts[ "shadowcompany" ][ i ] = string( i + 1 );
}
anim.flavorburstsUsed = [];
}
shutdown_battleChatter()
{
anim.countryIDs = undefined;
anim.eventTypeMinWait = undefined;
anim.eventActionMinWait = undefined;
anim.eventTypeMinWait = undefined;
anim.eventPriority = undefined;
anim.eventDuration = undefined;
anim.moveOrigin = undefined;
anim.scriptedDialogueBufferTime = undefined;
anim.bcs_threatResetTime = undefined;
anim.locationLastCalloutTimes = undefined;
anim.usedIDs = undefined;
anim.flavorburstsUsed = undefined;
anim.lastTeamThreatCallout = undefined;
anim.lastTeamThreatCalloutTime = undefined;
anim.lastNameSaidTimeout = undefined;
anim.lastNameSaid = undefined;
anim.lastNameSaidTime = undefined;
anim.chatInitialized = false;
anim.player.chatInitialized = false;
level.battlechatter = undefined;
for ( i = 0; i < anim.squadCreateFuncs.size; i++ )
{
if ( anim.squadCreateStrings[ i ] != "::init_squadBattleChatter" )
continue;
if ( i != ( anim.squadCreateFuncs.size - 1 ) )
{
anim.squadCreateFuncs[ i ] = anim.squadCreateFuncs[ anim.squadCreateFuncs.size - 1 ];
anim.squadCreateStrings[ i ] = anim.squadCreateStrings[ anim.squadCreateStrings.size - 1 ];
}
anim.squadCreateFuncs[ anim.squadCreateFuncs.size - 1 ] = undefined;
anim.squadCreateStrings[ anim.squadCreateStrings.size - 1 ] = undefined;
}
level notify( "battlechatter disabled" );
anim notify( "battlechatter disabled" );
}
// initializes battlechatter data that resides in the squad manager
// this is done to keep the squad management system free from clutter
init_squadBattleChatter()
{
squad = self;
// tweakables
squad.numSpeakers = 0;
squad.maxSpeakers = 1;
// non tweakables
squad.nextSayTime = GetTime() + 50;
squad.nextSayTimes[ "threat" ] = GetTime() + 50;
squad.nextSayTimes[ "order" ] = GetTime() + 50;
squad.nextSayTimes[ "reaction" ] = GetTime() + 50;
squad.nextSayTimes[ "response" ] = GetTime() + 50;
squad.nextSayTimes[ "inform" ] = GetTime() + 50;
squad.nextSayTimes[ "custom" ] = GetTime() + 50;
squad.nextTypeSayTimes[ "threat" ] = [];
squad.nextTypeSayTimes[ "order" ] = [];
squad.nextTypeSayTimes[ "reaction" ] = [];
squad.nextTypeSayTimes[ "response" ] = [];
squad.nextTypeSayTimes[ "inform" ] = [];
squad.nextTypeSayTimes[ "custom" ] = [];
squad.isMemberSaying[ "threat" ] = false;
squad.isMemberSaying[ "order" ] = false;
squad.isMemberSaying[ "reaction" ] = false;
squad.isMemberSaying[ "response" ] = false;
squad.isMemberSaying[ "inform" ] = false;
squad.isMemberSaying[ "custom" ] = false;
squad.lastDirection = "";
squad.memberAddFuncs[ squad.memberAddFuncs.size ] = ::addToSystem;
squad.memberAddStrings[ squad.memberAddStrings.size ] = "::addToSystem";
squad.memberRemoveFuncs[ squad.memberRemoveFuncs.size ] = ::removeFromSystem;
squad.memberRemoveStrings[ squad.memberRemoveStrings.size ] = "::removeFromSystem";
squad.squadUpdateFuncs[ squad.squadUpdateFuncs.size ] = ::initContact;
squad.squadUpdateStrings[ squad.squadUpdateStrings.size ] = "::initContact";
squad.fbt_firstBurst = true;
squad.fbt_lastBursterID = undefined;
for ( i = 0; i < anim.squadIndex.size; i++ )
squad thread initContact( anim.squadIndex[ i ].squadName );
squad thread squadThreatWaiter();
squad thread squadOfficerWaiter();
squad thread squadFlavorBurstTransmissions();
squad.chatInitialized = true;
squad notify( "squad chat initialized" );
}
// initializes battlechatter data that resides in the squad manager
// this is done to keep the squad management system free from clutter
shutdown_squadBattleChatter()
{
squad = self;
// tweakables
squad.numSpeakers = undefined;
squad.maxSpeakers = undefined;
// non tweakables
squad.nextSayTime = undefined;
squad.nextSayTimes = undefined;
squad.nextTypeSayTimes = undefined;
squad.isMemberSaying = undefined;
squad.fbt_firstBurst = undefined;
squad.fbt_lastBursterID = undefined;
for ( i = 0; i < squad.memberAddFuncs.size; i++ )
{
if ( squad.memberAddStrings[ i ] != "::addToSystem" )
continue;
if ( i != ( squad.memberAddFuncs.size - 1 ) )
{
squad.memberAddFuncs[ i ] = squad.memberAddFuncs[ squad.memberAddFuncs.size - 1 ];
squad.memberAddStrings[ i ] = squad.memberAddStrings[ squad.memberAddStrings.size - 1 ];
}
squad.memberAddFuncs[ squad.memberAddFuncs.size - 1 ] = undefined;
squad.memberAddStrings[ squad.memberAddStrings.size - 1 ] = undefined;
}
for ( i = 0; i < squad.memberRemoveFuncs.size; i++ )
{
if ( squad.memberRemoveStrings[ i ] != "::removeFromSystem" )
continue;
if ( i != ( squad.memberRemoveFuncs.size - 1 ) )
{
squad.memberRemoveFuncs[ i ] = squad.memberRemoveFuncs[ squad.memberRemoveFuncs.size - 1 ];
squad.memberRemoveStrings[ i ] = squad.memberRemoveStrings[ squad.memberRemoveStrings.size - 1 ];
}
squad.memberRemoveFuncs[ squad.memberRemoveFuncs.size - 1 ] = undefined;
squad.memberRemoveStrings[ squad.memberRemoveStrings.size - 1 ] = undefined;
}
for ( i = 0; i < squad.squadUpdateFuncs.size; i++ )
{
if ( squad.squadUpdateStrings[ i ] != "::initContact" )
continue;
if ( i != ( squad.squadUpdateFuncs.size - 1 ) )
{
squad.squadUpdateFuncs[ i ] = squad.squadUpdateFuncs[ squad.squadUpdateFuncs.size - 1 ];
squad.squadUpdateStrings[ i ] = squad.squadUpdateStrings[ squad.squadUpdateStrings.size - 1 ];
}
squad.squadUpdateFuncs[ squad.squadUpdateFuncs.size - 1 ] = undefined;
squad.squadUpdateStrings[ squad.squadUpdateStrings.size - 1 ] = undefined;
}
for ( i = 0; i < anim.squadIndex.size; i++ )
squad shutdownContact( anim.squadIndex[ i ].squadName );
squad.chatInitialized = false;
}
bcsEnabled()
{
return anim.chatInitialized;
}
bcsDebugWaiter()
{
lastState = GetDvar( "bcs_enable", "on" );
while ( 1 )
{
state = GetDvar( "bcs_enable", "on" );
if ( state != lastState )
{
switch( state )
{
case "on":
if ( !anim.chatInitialized )
enableBattleChatter();
break;
case "off":
if ( anim.chatInitialized )
disableBattleChatter();
break;
}
lastState = state;
}
wait( 1.0 );
}
}
enableBattleChatter()
{
init_battleChatter();
anim.player thread animscripts\battleChatter_ai::addToSystem();
ai = GetAIArray();
for ( i = 0; i < ai.size; i++ )
{
ai[ i ] addToSystem();
}
}
disableBattleChatter()
{
shutdown_battleChatter();
ai = GetAIArray();
for ( i = 0; i < ai.size; i++ )
{
if ( IsDefined( ai[ i ].squad ) && ai[ i ].squad.chatInitialized )
ai[ i ].squad shutdown_squadBattleChatter();
ai[ i ] removeFromSystem();
}
}
// sets the player name to use when playing player-relative phrases
// these are used to make an alias that looks like: US_1_name_player_US_1
setPlayerBcNameID( overrideNameID, overrideCountryID )
{
if ( IsDefined( overrideNameID ) && IsDefined( overrideCountryID ) )
{
level.player.bcNameID = overrideNameID;
level.player.bcCountryID = overrideCountryID;
return;
}
while ( !IsDefined( level.campaign ) )
{
wait( 0.1 );
}
nationality = level.campaign;
nameID = anim.playerNameIDs[ nationality ];
countryID = anim.countryIDs[ nationality ];
if ( IsDefined( nameID ) )
{
level.player.bcNameID = nameID;
}
if ( IsDefined( countryID ) )
{
level.player.bcCountryID = countryID;
}
}
/****************************************************************************
processing
*****************************************************************************/
playBattleChatter()
{
if ( !IsAlive( self ) )
{
return;
}
// battlechatter system is totally turned off (as opposed to dormant)
if ( !bcsEnabled() )
{
return;
}
// he's doing a scripted animation
if ( self._animActive > 0 )
{
return;
}
// he's already saying a battlechatter line
if ( IsDefined( self.isSpeaking ) && self.isSpeaking )
{
return;
}
// an ally is doing scripted dialogue
if ( self.team == "allies" && IsDefined( anim.scriptedDialogueStartTime ) )
{
if ( ( anim.scriptedDialogueStartTime + anim.scriptedDialogueBufferTime ) > GetTime() )
{
return;
}
}
// hacky! friendlyfire warnings have greatly reduced requirements for whether they can play
if ( self friendlyfire_warning() )
{
return;
}
if ( !isdefined( self.battleChatter ) || !self.battleChatter )
{
return;
}
if ( self.team == "allies" && GetDvarInt( "bcs_forceEnglish", 0 ) )
{
return;
}
if ( anim.isTeamSpeaking[ self.team ] )
{
return;
}
self endon( "death" );
// self thread debugQueueEvents();
// self thread debugPrintEvents();
event = self getHighestPriorityEvent();
if ( !isdefined( event ) )
{
return;
}
switch( event )
{
case "custom":
self thread playCustomEvent();
break;
case "response":
self thread playResponseEvent();
break;
case "order":
self thread playOrderEvent();
break;
case "threat":
self thread playThreatEvent();
break;
case "reaction":
self thread playReactionEvent();
break;
case "inform":
self thread playInformEvent();
break;
}
}
//// threat events functions
playThreatEvent()
{
self endon( "death" );
self endon( "removed from battleChatter" );
self endon( "cancel speaking" );
self.curEvent = self.chatQueue[ "threat" ];
threat = self.chatQueue[ "threat" ].threat;
if ( !IsAlive( threat ) )
{
return;
}
if ( threatWasAlreadyCalledOut( threat ) && !IsPlayer( threat ) )
{
return;
}
anim thread lockAction( self, "threat" );
/#
if ( GetDvar( "debug_bcinteraction" ) == "on" )
animscripts\utility::showDebugLine( self.origin + ( 0, 0, 50 ), threat.origin + ( 0, 0, 50 ), ( 1, 0, 0 ), 1.5 );
#/
success = false;
switch( self.chatQueue[ "threat" ].eventType )
{
case "infantry":
if ( IsPlayer( threat ) || !isdefined( threat GetTurret() ) )
{
success = self threatInfantry( threat );
}
else
{
// if we ever want emplacement callouts again, put one here
}
break;
case "dog":
success = self threatDog( threat );
break;
case "vehicle":
break;
}
self notify( "done speaking" );
if ( !success )
{
return;
}
if ( !IsAlive( threat ) )
{
return;
}
threat.calledOut[ self.squad.squadName ] = SpawnStruct();
threat.calledOut[ self.squad.squadName ].spotter = self;
threat.calledOut[ self.squad.squadName ].threatType = self.chatQueue[ "threat" ].eventType;
threat.calledOut[ self.squad.squadName ].expireTime = GetTime() + anim.bcs_threatResetTime;
if ( IsDefined( threat.squad ) )
{
self.squad.squadList[ threat.squad.squadName ].calledOut = true;
}
}
threatWasAlreadyCalledOut( threat )
{
if ( IsDefined( threat.calledOut ) && IsDefined( threat.calledOut[ self.squad.squadName ] ) )
{
// maybe we can talk about him again if he was previously called out
if ( threat.calledOut[ self.squad.squadName ].expireTime < GetTime() )
{
return true;
}
}
return false;
}
threatInfantry( threat, forceDetail )
{
self endon( "cancel speaking" );
chatPhrase = self createChatPhrase();
chatPhrase.master = true;
chatPhrase.threatEnt = threat;
// figure out what kind of callout we want to do
callout = self getThreatInfantryCalloutType( threat );
if ( !IsDefined( callout ) || ( IsDefined( callout ) && !IsDefined( callout.type ) ) )
{
/*
printStr = anim.bcPrintFailPrefix + "Couldn't find a threat callout type using getThreatInfantryCalloutType. ";
if( IsDefined( threat ) && IsDefined( threat.classname ) )
{
printStr += "Threat classname was: " + threat.classname;
}
else if( IsDefined( threat ) && !IsDefined( threat.classname ) )
{
printStr += "Threat didn't have a classname!";
}
else if( !IsDefined( threat ) )
{
printStr += "Threat wasn't defined!";
}
PrintLn( printStr );
*/
return false;
}
switch( callout.type )
{
case "rpg":
chatPhrase threatInfantryRPG( threat );
break;
case "exposed":
// not getting enough variety when we count on AIs to see the targets themselves
// before responding, so do a simpler diceroll check that we can control better
//
// - check for callout.responder because it's not required
// for this callout, just nice to have
doResponse = self doExposedCalloutResponse( callout.responder );
// if we can say their name, do it
if ( doResponse && self canSayName( callout.responder ) )
{
chatPhrase addNameAlias( callout.responder.bcName );
chatPhrase.lookTarget = callout.responder;
}
// set up the exposed line to play
chatPhrase threatInfantryExposed( threat );
// add a response event, if we decided to do it
if ( doResponse )
{
if ( RandomInt( 100 ) < anim.eventChance[ "response" ][ "callout_negative" ] )
{
callout.responder addResponseEvent( "callout", "neg", self, 0.9 );
}
else
{
callout.responder addResponseEvent( "exposed", "acquired", self, 0.9 );
}
}
break;
case "player_obvious":
chatPhrase addPlayerNameAlias();
chatPhrase addThreatObviousAlias();
break;
case "player_yourclock":
chatPhrase addPlayerNameAlias();
chatPhrase addThreatCalloutAlias( "yourclock", callout.playerClockDirection );
break;
case "player_contact_clock":
chatPhrase addPlayerNameAlias();
chatPhrase addThreatCalloutAlias( "contactclock", callout.playerClockDirection );
break;
case "player_target_clock":
chatPhrase addPlayerNameAlias();
chatPhrase addThreatCalloutAlias( "targetclock", callout.playerClockDirection );
break;
case "player_cardinal":
chatPhrase addPlayerNameAlias();
cardinalDirection = getDirectionCompass( level.player.origin, threat.origin );
normalizedDirection = normalizeCompassDirection( cardinalDirection );
if ( normalizedDirection == "impossible" )
{
return false;
}
chatPhrase addThreatCalloutAlias( "cardinal", normalizedDirection );
break;
case "ai_yourclock":
AssertEx( IsDefined( callout.responder ), "we should have found a valid responder in order to do an ai_yourclock callout!" );
angles = getRelativeAngles( callout.responder );
if ( self canSayName( callout.responder ) )
{
chatPhrase addNameAlias( callout.responder.bcName );
chatPhrase.lookTarget = callout.responder;
}
chatPhrase addThreatCalloutAlias( "yourclock", callout.responderClockDirection );
chatPhrase addCalloutResponseEvent( self, callout, threat );
break;
case "ai_contact_clock":
relativeGuy = self;
if ( self.team == "allies" )
{
// make it player relative for allied callouts
relativeGuy = level.player;
}
// for axis, make it relative to a responder if we have one
else if ( IsDefined( callout.responder )
&& RandomInt( 100 ) < anim.eventChance[ "response" ][ "callout" ] )
{
relativeGuy = callout.responder;
}
angles = getRelativeAngles( relativeGuy );
clockDirection = getDirectionFacingClock( angles, relativeGuy.origin, threat.origin );
if ( IsDefined( callout.responder ) && self canSayName( callout.responder ) )
{
chatPhrase addNameAlias( callout.responder.bcName );
chatPhrase.lookTarget = callout.responder;
}
chatPhrase addThreatCalloutAlias( "contactclock", clockDirection );
chatPhrase addCalloutResponseEvent( self, callout, threat );
break;
case "ai_target_clock":
relativeGuy = self;
if ( self.team == "allies" )
{
// make it player relative for allied callouts
relativeGuy = level.player;
}
// for axis, make it relative to a responder if we have one
else if ( IsDefined( callout.responder )
&& RandomInt( 100 ) < anim.eventChance[ "response" ][ "callout" ] )
{
relativeGuy = callout.responder;
}
angles = getRelativeAngles( relativeGuy );
clockDirection = getDirectionFacingClock( angles, relativeGuy.origin, threat.origin );
if ( IsDefined( callout.responder ) && self canSayName( callout.responder ) )
{
chatPhrase addNameAlias( callout.responder.bcName );
chatPhrase.lookTarget = callout.responder;
}
chatPhrase addThreatCalloutAlias( "targetclock", clockDirection );
chatPhrase addCalloutResponseEvent( self, callout, threat );
break;
case "ai_cardinal":
relativeGuy = self;
if ( self.team == "allies" )
{
relativeGuy = level.player;
}
cardinalDirection = getDirectionCompass( relativeGuy.origin, threat.origin );
normalizedDirection = normalizeCompassDirection( cardinalDirection );
if ( normalizedDirection == "impossible" )
{
return false;
}
chatPhrase addThreatCalloutAlias( "cardinal", normalizedDirection );
break;
/* DEPRECATED
case "player_object_yourclock":
chatPhrase addPlayerNameAlias();
success = chatPhrase addThreatCalloutLandmarkAlias( callout.landmark, callout.playerClockDirection, true );
if ( !success )
{
return false;
}
break;
case "player_object_clock":
chatPhrase addPlayerNameAlias();
success = chatPhrase addThreatCalloutLandmarkAlias( callout.landmark, callout.playerClockDirection );
if ( !success )
{
return false;
}
break;
case "ai_object_yourclock":
// SRS TODO add when we have obj_your_[clocknum] aliases
break;
*/
case "generic_location":
Assert( IsDefined( callout.location ) );
success = chatPhrase threatInfantry_doCalloutLocation( callout, level.player, threat );
if ( !success )
{
return false;
}
break;
case "player_location":
Assert( IsDefined( callout.location ) );
chatPhrase addPlayerNameAlias();
success = chatPhrase threatInfantry_doCalloutLocation( callout, level.player, threat );
if ( !success )
{
return false;
}
break;
case "ai_location":
Assert( IsDefined( callout.location ) );
AssertEx( IsDefined( callout.responder ), "we should have found a valid responder in order to do an ai_location callout!" );
if ( self canSayName( callout.responder ) )
{
chatPhrase addNameAlias( callout.responder.bcName );
chatPhrase.lookTarget = callout.responder;
}
success = chatPhrase threatInfantry_doCalloutLocation( callout, self, threat );
if ( !success )
{
return false;
}
// the last alias in the soundaliases array is always the one with the actual location info
index = chatPhrase.soundaliases.size - 1;
alias = chatPhrase.soundaliases[ index ];
// if the location alias is a "report" we'll have an "echo" to go with it
if ( IsCalloutTypeReport( alias ) )
{
callout.responder addResponseEvent( "callout", "echo", self, 0.9, alias );
}
else if ( IsCalloutTypeQA( alias, self ) )
{
callout.responder addResponseEvent( "callout", "QA", self, 0.9, alias, callout.location );
}
// otherwise do a generic response
else
{
if ( RandomInt( 100 ) < anim.eventChance[ "response" ][ "callout_negative" ] )
{
callout.responder addResponseEvent( "callout", "neg", self, 0.9 );
}
else
{
callout.responder addResponseEvent( "exposed", "acquired", self, 0.9 );
}
}
break;
}
setLastCalloutType( callout.type );
self playPhrase( chatPhrase );
return true;
}
doExposedCalloutResponse( responder )
{
if ( !IsDefined( responder ) )
{
return false;
}
if ( responder.countryID != "US" && responder.countryID != "NS" && responder.countryID != "TF" )
{
return false;
}
if ( RandomInt( 100 ) > anim.eventChance[ "response" ][ "exposed" ] )
{
return false;
}
return true;
}
// self = a chatPhrase
// refEnt = the player or an AI, they will determine where the threat is in worldspace relative to themselves
threatInfantry_doCalloutLocation( callout, refEnt, threat )
{
success = self addThreatCalloutLocationAlias( callout.location );
return success;
}
// self = a chatPhrase
addCalloutResponseEvent( respondTo, callout, threat )
{
if ( !IsDefined( callout.responder ) )
{
return;
}
if ( RandomInt( 100 ) > anim.eventChance[ "response" ][ "callout" ] )
{
return;
}
modifier = "affirm";
// make sure that the guy can't actually see the enemy while we do the diceroll
if ( !callout.responder CanSee( threat ) && RandomInt( 100 ) < anim.eventChance[ "response" ][ "callout_negative" ] )
{
modifier = "neg";
}
callout.responder addResponseEvent( "callout", modifier, respondTo, 0.9 );
}
// figures out what kind of callout to do for infantry threats
// - uses chances from anim.threatCallouts[], matched against callout types in self.allowedCallouts[]
getThreatInfantryCalloutType( threat )
{
// get info about the AI
location = threat GetLocation();
selfClockDirection = getDirectionFacingClock( self.angles, self.origin, threat.origin );
// try to find a responder
responder = self getResponder( 64, 1024, "response" );
responderClockDirection = undefined;
if ( IsDefined( responder ) )
{
responderClockDirection = getDirectionFacingClock( responder.angles, responder.origin, threat.origin );
}
// get relative info
playerCanSeeThreat = false;
if ( self.team == level.player.team )
{
playerCanSeeThreat = player_can_see_ai( threat );
}
threatInPlayerFOV = level.player pointInFov( threat.origin );
threatInFrontArc = level.player entInFrontArc( threat );
playerClockDirection = getDirectionFacingClock( level.player.angles, level.player.origin, threat.origin );
// now, figure out all possible kinds of callouts that this AI can say
self.possibleThreatCallouts = [];
// is it an RPG?
if ( !IsPlayer( threat ) && threat usingRocketLauncher() )
{
self addPossibleThreatCallout( "rpg" );
}
// is the threat exposed?
if ( threat IsExposed() )
{
self addPossibleThreatCallout( "exposed" );
}
// player-relatives: can the player see the threat?
if ( threatInFrontArc && self canSayPlayerName() )
{
// guys right in front of you either get "obvious" callouts or nothing
if ( playerClockDirection == "11"
|| playerClockDirection == "12"
|| playerClockDirection == "1" )
{
if ( playerCanSeeThreat )
{
self addPossibleThreatCallout( "player_obvious" );
}
}
else
{
self addPossibleThreatCallout( "player_yourclock" );
self addPossibleThreatCallout( "player_contact_clock" );
self addPossibleThreatCallout( "player_target_clock" );
self addPossibleThreatCallout( "player_cardinal" );
}
}
// can another AI whose name we can say see the threat?
if ( IsDefined( responder ) && self canSayName( responder ) )
{
self addPossibleThreatCallout( "ai_yourclock" );
}
// catch-alls
// don't want allies to do 12 o'clock callouts if it's not a "your" callout
if ( enemy_team_name() || ( selfClockDirection != "12" ) )
{
self addPossibleThreatCallout( "ai_contact_clock" );
self addPossibleThreatCallout( "ai_target_clock" );
}
self addPossibleThreatCallout( "ai_cardinal" );
/* DEPRECATED
// is the threat in a landmark trigger?
if ( IsDefined( landmark ) )
{
// only call out landmarks at 10 or 2
if ( playerClockDirection == "10" || playerClockDirection == "2" )
{
if ( self canSayPlayerName() )
{
self addPossibleThreatCallout( "player_object_yourclock" );
}
self addPossibleThreatCallout( "player_object_clock" );
}
if ( IsDefined( responder ) && self canSayName( responder ) )
{
if ( responderClockDirection == "10" || responderClockDirection == "2" )
{
self addPossibleThreatCallout( "ai_object_yourclock" );
}
}
}
*/
// is the threat in a location trigger?
if ( IsDefined( location ) )
{
cannedResponse = location GetCannedResponse( self );
// locations with responses are the best
if ( IsDefined( cannedResponse ) )
{
// if we have an accompanying response line and a responder, don't tell the player because it's cool to have the AI respond
if ( IsDefined( responder ) )
{
self addPossibleThreatCallout( "ai_location" );
}
else
{
/#
debugstring = anim.bcPrintWarnPrefix + "Calling out a location at origin " + location.origin + " with a canned response, but there are no AIs able to respond.";
#/
if ( self canSayPlayerName() )
{
self addPossibleThreatCallout( "player_location" );
}
self addPossibleThreatCallout( "generic_location" );
}
}
// otherwise do whichever
else
{
if ( IsDefined( responder ) )
{
self addPossibleThreatCallout( "ai_location" );
}
if ( self canSayPlayerName() )
{
self addPossibleThreatCallout( "player_location" );
}
// last ditch effort!
self addPossibleThreatCallout( "generic_location" );
}
}
if ( !self.possibleThreatCallouts.size )
{
return undefined;
}
// now figure out which of the possible threat callouts we're actually going to use
best = getWeightedChanceRoll( self.possibleThreatCallouts, anim.threatCallouts );
callout = SpawnStruct();
callout.type = best;
callout.responder = responder;
callout.responderClockDirection = responderClockDirection;
//callout.landmark = landmark; // DEPRECATED
callout.playerClockDirection = playerClockDirection;
if ( IsDefined( location ) )
{
callout.location = location;
}
//println( "CALLOUT: " + callout.type );
return callout;
}
// determines whether this kind of location has an alias that could do a canned response
GetCannedResponse( speaker )
{
cannedResponseAlias = undefined;
aliases = self.locationAliases;
foreach ( alias in aliases )
{
// always do a "QA" type callout if we can, since it's cooler
if ( IsCalloutTypeQA( alias, speaker ) && !IsDefined( self.qaFinished ) )
{
cannedResponseAlias = alias;
break;
}
// it's ok that we always choose the last one because we randomize them earlier
if ( IsCalloutTypeReport( alias ) )
{
cannedResponseAlias = alias;
}
}
return cannedResponseAlias;
}
IsCalloutTypeReport( alias )
{
return IsSubStr( alias, "_report" );
}
// tells us whether a given alias can start a back-and-forth conversation about the location
IsCalloutTypeQA( alias, speaker )
{
// first try to see if it's fully constructed
if ( IsSubStr( alias, "_qa" ) && SoundExists( alias ) )
{
return true;
}
// otherwise, maybe we have to add prefix/suffix info
tryQA = speaker GetQACalloutAlias( alias, 0 );
if ( SoundExists( tryQA ) )
{
return true;
}
return false;
}
GetQACalloutAlias( basealias, lineIndex )
{
alias = self.countryID + "_" + self.npcID + "_co_";
alias += basealias;
alias += "_qa" + lineIndex;
return alias;
}
addAllowedThreatCallout( threatType )
{
self.allowedCallouts[ self.allowedCallouts.size ] = threatType;
}
addPossibleThreatCallout( threatType )
{
allowed = false;
foreach ( calloutType in self.allowedCallouts )
{
if ( calloutType == threatType )
{
if ( !self calloutTypeWillRepeat( threatType ) )
{
allowed = true;
}
break;
}
}
if ( !allowed )
{
return;
}
self.possibleThreatCallouts[ self.possibleThreatCallouts.size ] = threatType;
}
calloutTypeWillRepeat( threatType )
{
if ( !IsDefined( anim.lastTeamThreatCallout[ self.team ] ) )
{
return false;
}
if ( !IsDefined( anim.lastTeamThreatCalloutTime[ self.team ] ) )
{
return false;
}
lastThreat = anim.lastTeamThreatCallout[ self.team ];
lastCalloutTime = anim.lastTeamThreatCalloutTime[ self.team ];
timeout = anim.teamThreatCalloutLimitTimeout;
if ( ( threatType == lastThreat ) && ( GetTime() - lastCalloutTime < timeout ) )
{
return true;
}
return false;
}
setLastCalloutType( type )
{
anim.lastTeamThreatCallout[ self.team ] = type;
anim.lastTeamThreatCalloutTime[ self.team ] = GetTime();
}
// returns a member of possibleValues[], determined by dicerolling it against all the other
// members of possibleValues[].
// - chances are provided for each possible value by the values
// in chancesForValues[], which is indexed by possibleValues, so we can match them up
getWeightedChanceRoll( possibleValues, chancesForValues )
{
best = undefined;
bestRoll = -1;// only want to roll once per value so store this off
foreach ( value in possibleValues )
{
// don't consider it if the chance is 0
if ( chancesForValues[ value ] <= 0 )
{
continue;
}
thisRoll = RandomInt( chancesForValues[ value ] );
// if the best is 100+...
if ( IsDefined( best ) && ( chancesForValues[ best ] >= 100 ) )
{
// ...and the new challenger isn't at that level, keep going
if ( chancesForValues[ value ] < 100 )
{
continue;
}
}
// otherwise, if the new challenger is 100+...
else if ( ( chancesForValues[ value ] >= 100 ) )
{
// he wins automatically
best = value;
bestRoll = thisRoll;
}
// otherwise, everyone else rolls against each other, or 100+'s roll against each other
else if ( thisRoll > bestRoll )
{
best = value;
bestRoll = thisRoll;
}
}
return best;
}
threatDog( threat, forceDetail )
{
self endon( "cancel speaking" );
chatPhrase = self createChatPhrase();
chatPhrase.master = true;
chatPhrase.threatEnt = threat;
// SRS 10/27/08: updated to be more generic until we have actual dog aliases
chatPhrase addThreatAlias( "dog", "generic" );
self playPhrase( chatPhrase );
return true;
}
threatInfantryExposed( threat )
{
exposedVariants = [];
exposedVariants = array_add( exposedVariants, "open" );
exposedVariants = array_add( exposedVariants, "breaking" );
// only allies get these variants - except Russians, who are usually enemies so we didn't record extras for them
if ( self.owner.team == "allies" && self.owner.countryID != "RU" )
{
exposedVariants = array_add( exposedVariants, "oscarmike" );
exposedVariants = array_add( exposedVariants, "movement" );
}
exposedVariant = exposedVariants[ RandomInt( exposedVariants.size ) ];
self addThreatExposedAlias( exposedVariant );
}
threatInfantryRPG( threat )
{
self addThreatAlias( "rpg" );
}
//// reaction events functions
playReactionEvent()
{
self endon( "death" );
self endon( "removed from battleChatter" );
self.curEvent = self.chatQueue[ "reaction" ];
reactTo = self.chatQueue[ "reaction" ].reactTo;
modifier = self.chatQueue[ "reaction" ].modifier;
anim thread lockAction( self, "reaction" );
switch( self.chatQueue[ "reaction" ].eventType )
{
case "casualty":
self reactionCasualty( reactTo, modifier );
break;
case "taunt":
self reactionTaunt( reactTo, modifier );
break;
case "friendlyfire":
self reactionFriendlyFire( reactTo, modifier );
break;
}
self notify( "done speaking" );
}
reactionCasualty( reactTo, modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
chatPhrase addReactionAlias( "casualty", "generic" );
self playPhrase( chatPhrase );
}
reactionTaunt( reactTo, modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
if ( IsDefined( modifier ) && modifier == "hostileburst" )
{
chatPhrase addHostileBurstAlias();
}
else
{
chatPhrase addTauntAlias( "taunt", "generic" );
}
self playPhrase( chatPhrase );
}
reactionFriendlyFire( reactTo, modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
chatPhrase addCheckFireAlias();
self playPhrase( chatPhrase );
}
playResponseEvent()
{
self endon( "death" );
self endon( "removed from battleChatter" );
self.curEvent = self.chatQueue[ "response" ];
modifier = self.chatQueue[ "response" ].modifier;
respondTo = self.chatQueue[ "response" ].respondTo;
if ( !IsAlive( respondTo ) )
{
return;
}
// if he's responding to a "follow" order, make sure that he's actually moving
if ( self.chatQueue[ "response" ].modifier == "follow" && self.a.state != "move" )
{
return;
}
anim thread lockAction( self, "response" );
/#
if ( GetDvar( "debug_bcinteraction" ) == "on" )
animscripts\utility::showDebugLine( self.origin + ( 0, 0, 50 ), respondTo.origin + ( 0, 0, 50 ), ( 1, 1, 0 ), 1.5 );
#/
switch( self.chatQueue[ "response" ].eventType )
{
case "exposed":
self responseThreatExposed( respondTo, modifier );
break;
case "callout":
self responseThreatCallout( respondTo, modifier );
break;
case "ack":
self responseGeneric( respondTo, modifier );
break;
}
self notify( "done speaking" );
}
responseThreatExposed( respondTo, modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
if ( !IsAlive( respondTo ) )
{
return;
}
chatPhrase = self createChatPhrase();
// these aliases look different from regular responses,
// so construct them using addThreatExposedAlias()
chatPhrase addThreatExposedAlias( modifier );
chatPhrase.lookTarget = respondTo;
chatPhrase.master = true;
self playPhrase( chatPhrase );
}
responseThreatCallout( respondTo, modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
if ( !IsAlive( respondTo ) )
{
return;
}
chatPhrase = self createChatPhrase();
success = false;
if ( modifier == "echo" )
{
success = chatPhrase addThreatCalloutEcho( self.curEvent.reportAlias, respondTo );
}
else if ( modifier == "QA" )
{
success = chatPhrase addThreatCalloutQA_NextLine( respondTo, self.curEvent.reportAlias, self.curEvent.location );
}
else
{
success = chatPhrase addThreatCalloutResponseAlias( modifier );
}
if ( !success )
{
return;
}
chatPhrase.lookTarget = respondTo;
chatPhrase.master = true;
self playPhrase( chatPhrase );
}
responseGeneric( respondTo, modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
if ( !IsAlive( respondTo ) )
{
return;
}
type = self.chatQueue[ "response" ].eventType;
chatPhrase = self createChatPhrase();
chatPhrase addResponseAlias( type, modifier );
chatPhrase.lookTarget = respondTo;
chatPhrase.master = true;
self playPhrase( chatPhrase );
}
//// order events functions
playOrderEvent()
{
self endon( "death" );
self endon( "removed from battleChatter" );
self.curEvent = self.chatQueue[ "order" ];
modifier = self.chatQueue[ "order" ].modifier;
orderTo = self.chatQueue[ "order" ].orderTo;
anim thread lockAction( self, "order" );
switch( self.chatQueue[ "order" ].eventType )
{
case "action":
self orderAction( modifier, orderTo );
break;
case "move":
self orderMove( modifier, orderTo );
break;
case "displace":
self orderDisplace( modifier );
break;
}
self notify( "done speaking" );
}
orderAction( modifier, orderTo )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
self tryOrderTo( chatPhrase, orderTo );
chatPhrase addOrderAlias( "action", modifier );
self playPhrase( chatPhrase );
}
orderMove( modifier, orderTo )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
/#
if ( GetDvar( "debug_bcinteraction" ) == "on" && IsDefined( orderTo ) )
animscripts\utility::showDebugLine( self.origin + ( 0, 0, 50 ), orderTo.origin + ( 0, 0, 50 ), ( 0, 1, 0 ), 1.5 );
#/
self tryOrderTo( chatPhrase, orderTo );
chatPhrase addOrderAlias( "move", modifier );
self playPhrase( chatPhrase );
}
orderDisplace( modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
chatPhrase addOrderAlias( "displace", modifier );
self playPhrase( chatPhrase, true );
}
tryOrderTo( chatPhrase, orderTo )
{
if ( RandomInt( 100 ) > anim.eventChance[ "response" ][ "order" ] )
{
// only return if the orderTo guy isn't the player
if ( !IsDefined( orderTo ) || ( IsDefined( orderTo ) && !IsPlayer( orderTo ) ) )
{
return;
}
}
if ( IsDefined( orderTo ) && IsPlayer( orderTo ) && IsDefined( level.player.bcNameID ) )
{
chatPhrase addPlayerNameAlias();
chatPhrase.lookTarget = level.player;
}
else if ( IsDefined( orderTo ) && self canSayName( orderTo ) )
{
chatPhrase addNameAlias( orderTo.bcName );
chatPhrase.lookTarget = orderTo;
orderTo addResponseEvent( "ack", "yes", self, 0.9 );
}
else
{
// if we can't specifically respond to someone, throw a notify out there
// and hope that someone is around to catch it
level notify( "follow order", self );
}
}
//// inform events functions
playInformEvent()
{
self endon( "death" );
self endon( "removed from battleChatter" );
self.curEvent = self.chatQueue[ "inform" ];
modifier = self.chatQueue[ "inform" ].modifier;
anim thread lockAction( self, "inform" );
switch( self.chatQueue[ "inform" ].eventType )
{
case "incoming":
self informIncoming( modifier );
break;
case "attack":
self informAttacking( modifier );
break;
case "reloading":
self informReloading( modifier );
break;
case "suppressed":
self informSuppressed( modifier );
break;
case "killfirm":
self informKillfirm( modifier );
break;
}
self notify( "done speaking" );
}
informReloading( modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
chatPhrase addInformAlias( "reloading", modifier );
self playPhrase( chatPhrase );
}
informSuppressed( modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
chatPhrase addInformAlias( "suppressed", modifier );
self playPhrase( chatPhrase );
}
informIncoming( modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
if ( modifier == "grenade" )
chatPhrase.master = true;
chatPhrase addInformAlias( "incoming", modifier );
self playPhrase( chatPhrase );
}
informAttacking( modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
chatPhrase addInformAlias( "attack", modifier );
self playPhrase( chatPhrase );
}
informKillfirm( modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
chatPhrase = self createChatPhrase();
chatPhrase addInformAlias( "killfirm", modifier );
self playPhrase( chatPhrase );
}
//// custom events functions
playCustomEvent()
{
self endon( "death" );
self endon( "removed from battleChatter" );
self.curEvent = self.chatQueue[ "custom" ];
anim thread lockAction( self, self.curEvent.type, true );
self playPhrase( self.customChatPhrase );
self notify( "done speaking" );
self.customChatEvent = undefined;
self.customChatPhrase = undefined;
}
/****************************************************************************
utility
*****************************************************************************/
playPhrase( chatPhrase, noSound )
{
anim endon( "battlechatter disabled" );
self endon( "death" );
if ( IsDefined( noSound ) )
{
return;
}
// if ( GetDvar( "bcs_stealth" ) != "" && self.voice == "british" )
if ( IsDefined( level._stealth ) && ( self voice_is_british_based() ) )
{
for ( i = 0; i < chatPhrase.soundAliases.size; i++ )
chatPhrase.soundAliases[ i ] = chatPhrase.soundAliases[ i ] + "_s";
}
if ( self battleChatter_canPrint() || self battleChatter_canPrintDump() )
{
bcAliases = [];
foreach ( alias in chatPhrase.soundAliases )
{
bcAliases[ bcAliases.size ] = alias;
}
if ( self battleChatter_canPrint() )
{
self battleChatter_print( bcAliases );
}
if ( self battleChatter_canPrintDump() )
{
bcDescriptor = self.curEvent.eventAction + "_" + self.curEvent.eventType;
if ( IsDefined( self.curEvent.modifier ) )
{
bcDescriptor += ( "_" + self.curEvent.modifier );
}
self thread battleChatter_printDump( bcAliases, bcDescriptor );
}
}
for ( i = 0; i < chatPhrase.soundAliases.size; i++ )
{
// if battlechatter is turned off and this isn't a friendly fire event, don't keep talking
if ( !self.battleChatter )
{
if ( !is_friendlyfire_event( self.curEvent ) )
{
continue;
}
// hacky! passing false here - don't check the typelimit since we set it early for friendlyfire
else if ( !self can_say_friendlyfire( false ) )
{
continue;
}
}
if ( self._animActive > 0 )
{
continue;
}
if ( isFiltered( self.curEvent.eventAction ) )
{
wait( 0.85 );
continue;
}
if ( !SoundExists( chatPhrase.soundAliases[ i ] ) )
{
/#
PrintLn( anim.bcPrintFailPrefix + "Tried to play an alias that doesn't exist: '" + chatPhrase.soundAliases[ i ] + "'." );
#/
continue;
}
startTime = GetTime();
if ( chatPhrase.master && self.team == "allies" )
{
self thread maps\_anim::anim_facialFiller( chatPhrase.soundAliases[ i ], chatPhrase.lookTarget );
self PlaySoundAsMaster( chatPhrase.soundAliases[ i ], chatPhrase.soundAliases[ i ], true );
self waittill( chatPhrase.soundAliases[ i ] );
}
else
{
self thread maps\_anim::anim_facialFiller( chatPhrase.soundAliases[ i ], chatPhrase.lookTarget );
if ( GetDvarInt( "bcs_forceEnglish", 0 ) )
{
self PlaySoundAsMaster( chatPhrase.soundAliases[ i ], chatPhrase.soundAliases[ i ], true );
}
else
{
self PlaySound( chatPhrase.soundAliases[ i ], chatPhrase.soundAliases[ i ], true );
}
self waittill( chatPhrase.soundAliases[ i ] );
}
if ( GetTime() < startTime + 250 )
{
// This could mean the alias points to a 'null.wav', or that PlaySound() failed for some other reason.
//println( anim.bcPrintFailPrefix + "alias exists but sound didn't play: " + chatPhrase.soundAliases[i] );
}
}
// animscripts\shared::LookAtStop();
self notify( "playPhrase_done" );
self doTypeLimit( self.curEvent.eventAction, self.curEvent.eventType );
}
is_friendlyfire_event( curEvent )
{
if ( !IsDefined( curEvent.eventAction ) || !IsDefined( curEvent.eventType ) )
{
return false;
}
if ( curEvent.eventAction == "reaction" && curEvent.eventType == "friendlyfire" )
{
return true;
}
return false;
}
isSpeakingFailSafe( eventAction )
{
self endon( "death" );
wait( 25 );
self clearIsSpeaking( eventAction );
}
clearIsSpeaking( eventAction )
{
self.isSpeaking = false;
self.chatQueue[ eventAction ].expireTime = 0;
self.chatQueue[ eventAction ].priority = 0.0;
self.nextSayTimes[ eventAction ] = GetTime() + anim.eventActionMinWait[ eventAction ][ "self" ];
}
lockAction( speaker, eventAction, customEvent )
{
anim endon( "battlechatter disabled" );
Assert( !speaker.isSpeaking );
squad = speaker.squad;
team = speaker.team;
speaker.isSpeaking = true;
speaker thread isSpeakingFailSafe( eventAction );
squad.isMemberSaying[ eventAction ] = true;
squad.numSpeakers++;
anim.isTeamSpeaking[ team ] = true;
anim.isTeamSaying[ team ][ eventAction ] = true;
message = speaker waittill_any_return( "death", "done speaking", "cancel speaking" );
squad.isMemberSaying[ eventAction ] = false;
squad.numSpeakers--;
anim.isTeamSpeaking[ team ] = false;
anim.isTeamSaying[ team ][ eventAction ] = false;
if ( message == "cancel speaking" )
{
return;
}
anim.lastTeamSpeakTime[ team ] = GetTime();
if ( IsAlive( speaker ) )
{
speaker clearIsSpeaking( eventAction );
}
squad.nextSayTimes[ eventAction ] = GetTime() + anim.eventActionMinWait[ eventAction ][ "squad" ];
}
updateContact( squadName, member )
{
if ( GetTime() - self.squadList[ squadName ].lastContact > 10000 )
{
isInContact = false;
for ( i = 0; i < self.members.size; i++ )
{
if ( self.members[ i ] != member && IsAlive( self.members[ i ].enemy ) && IsDefined( self.members[ i ].enemy.squad ) && self.members[ i ].enemy.squad.squadName == squadName )
isInContact = true;
}
if ( !isInContact )
{
self.squadList[ squadName ].firstContact = GetTime();
self.squadList[ squadName ].calledOut = false;
}
}
self.squadList[ squadName ].lastContact = GetTime();
}
canSay( eventAction, eventType, priority, modifier )
{
self endon( "death" );
self endon( "removed from battleChatter" );
if ( IsPlayer( self ) )
{
return false;
}
if ( Distance( level.player.origin, self.origin ) > level.bcs_maxTalkingDistFromPlayer )
{
return false;
}
// our battlechatter is disabled
if ( !isdefined( self.battlechatter ) || !self.battlechatter )
return( false );
if ( IsDefined( priority ) && priority >= 1 )
return( true );
// we're not allowed to call out a threat now, and won't be able to before it expires
if ( ( GetTime() + anim.eventActionMinWait[ eventAction ][ "self" ] ) < self.nextSayTimes[ eventAction ] )
return( false );
// the squad is not allowed to call out a threat yet and won't be able to before it expires
if ( ( GetTime() + anim.eventActionMinWait[ eventAction ][ "squad" ] ) < self.squad.nextSayTimes[ eventAction ] )
return( false );
if ( IsDefined( eventType ) && typeLimited( eventAction, eventType ) )
return( false );
if ( IsDefined( eventType ) && anim.eventPriority[ eventAction ][ eventType ] < self.bcs_minPriority )
return( false );
if ( self voice_is_british_based() )
return quietFilter( eventAction, eventType, modifier );
return( true );
}
quietFilter( action, type, modifier )
{
if ( !isDefined( modifier ) )
modifier = "";
if ( !isDefined( type ) )
return false;
switch( action )
{
case "order":
if ( type == "action" && modifier == "coverme" )
return true;
break;
case "threat":
if ( type == "infantry" || type == "dog" || type == "rpg" )
return true;
break;
case "inform":
if ( type == "attack" && modifier == "grenade" )
return true;
else if ( type == "incoming" && modifier == "grenade" )
return true;
else if ( type == "reloading" && modifier == "generic" )
return true;
break;
case "reaction":
if ( type == "casualty" && modifier == "generic" )
return true;
break;
default:
return false;
}
return false;
}
getHighestPriorityEvent()
{
best = undefined;
bestpriority = -999999999;
if ( self isValidEvent( "custom" ) )
{
// don't have to check priority because this is the first if
best = "custom";
bestpriority = self.chatQueue[ "custom" ].priority;
}
if ( self isValidEvent( "response" ) )
{
if ( self.chatQueue[ "response" ].priority > bestpriority )
{
best = "response";
bestpriority = self.chatQueue[ "response" ].priority;
}
}
if ( self isValidEvent( "order" ) )
{
if ( self.chatQueue[ "order" ].priority > bestpriority )
{
best = "order";
bestpriority = self.chatQueue[ "order" ].priority;
}
}
if ( self isValidEvent( "threat" ) )
{
if ( self.chatQueue[ "threat" ].priority > bestpriority )
{
best = "threat";
bestpriority = self.chatQueue[ "threat" ].priority;
}
}
if ( self isValidEvent( "inform" ) )
{
if ( self.chatQueue[ "inform" ].priority > bestpriority )
{
best = "inform";
bestpriority = self.chatQueue[ "inform" ].priority;
}
}
if ( self isValidEvent( "reaction" ) )
{
if ( self.chatQueue[ "reaction" ].priority > bestpriority )
{
best = "reaction";
bestpriority = self.chatQueue[ "reaction" ].priority;
}
}
return best;
}
getTargettingAI( threat )
{
squad = self.squad;
targettingAI = [];
for ( index = 0; index < squad.members.size; index++ )
{
if ( IsDefined( squad.members[ index ].enemy ) && squad.members[ index ].enemy == threat )
targettingAI[ targettingAI.size ] = squad.members[ index ];
}
if ( !isdefined( targettingAI[ 0 ] ) )
return( undefined );
targettingSpeaker = undefined;
for ( index = 0; index < targettingAI.size; index++ )
{
if ( targettingAI[ index ] canSay( "response" ) )
return( targettingSpeaker );
}
return( getClosest( self.origin, targettingAI ) );
}
getQueueEvents()
{
queueEvents = [];
queueEventStates = [];
queueEvents[ 0 ] = "custom";
queueEvents[ 1 ] = "response";
queueEvents[ 2 ] = "order";
queueEvents[ 3 ] = "threat";
queueEvents[ 4 ] = "inform";
for ( i = queueEvents.size - 1; i >= 0; i-- )
{
for ( j = 1; j <= i; j++ )
{
if ( self.chatQueue[ queueEvents[ j - 1 ] ].priority < self.chatQueue[ queueEvents[ j ] ].priority )
{
strTemp = queueEvents[ j - 1 ];
queueEvents[ j - 1 ] = queueEvents[ j ];
queueEvents[ j ] = strTemp;
}
}
}
validEventFound = false;
for ( i = 0; i < queueEvents.size; i++ )
{
eventState = self getEventState( queueEvents[ i ] );
if ( eventState == " valid" && !validEventFound )
{
validEventFound = true;
queueEventStates[ i ] = "g " + queueEvents[ i ] + eventState + " " + self.chatQueue[ queueEvents[ i ] ].priority;
}
else if ( eventState == " valid" )
{
queueEventStates[ i ] = "y " + queueEvents[ i ] + eventState + " " + self.chatQueue[ queueEvents[ i ] ].priority;
}
else
{
if ( self.chatQueue[ queueEvents[ i ] ].expireTime == 0 )
queueEventStates[ i ] = "b " + queueEvents[ i ] + eventState + " " + self.chatQueue[ queueEvents[ i ] ].priority;
else
queueEventStates[ i ] = "r " + queueEvents[ i ] + eventState + " " + self.chatQueue[ queueEvents[ i ] ].priority;
}
}
return queueEventStates;
}
getEventState( strAction )
{
strState = "";
if ( self.squad.isMemberSaying[ strAction ] )
strState += " playing";
if ( GetTime() > self.chatQueue[ strAction ].expireTime )
strState += " expired";
if ( GetTime() < self.squad.nextSayTimes[ strAction ] )
strState += " cantspeak";
if ( strState == "" )
strState = " valid";
return( strState );
}
isFiltered( strAction )
{
if ( GetDvar( "bcs_filter" + strAction, "off" ) == "on" || GetDvar( "bcs_filter" + strAction, "off" ) == "1" )
return( true );
return( false );
}
isValidEvent( strAction )
{
if ( !self.squad.isMemberSaying[ strAction ] &&
!anim.isTeamSaying[ self.team ][ strAction ] &&
GetTime() < self.chatQueue[ strAction ].expireTime &&
GetTime() > self.squad.nextSayTimes[ strAction ] )
{
// redundant?
if ( !typeLimited( strAction, self.chatQueue[ strAction ].eventType ) )
return( true );
}
return( false );
}
typeLimited( strAction, strType )
{
if ( !isdefined( anim.eventTypeMinWait[ strAction ][ strType ] ) )
return( false );
if ( !isdefined( self.squad.nextTypeSayTimes[ strAction ][ strType ] ) )
return( false );
if ( GetTime() > self.squad.nextTypeSayTimes[ strAction ][ strType ] )
return( false );
return( true );
}
doTypeLimit( strAction, strType )
{
if ( !isdefined( anim.eventTypeMinWait[ strAction ][ strType ] ) )
return;
self.squad.nextTypeSayTimes[ strAction ][ strType ] = GetTime() + anim.eventTypeMinWait[ strAction ][ strType ];
}
bcIsSniper()
{
if ( IsPlayer( self ) )
return false;
if ( self isExposed() )
return false;
return IsSniperRifle( self.weapon );
}
isExposed()
{
// if he's too far away, the disadvantage of his exposed state
// is negated by contact distance
if ( Distance( self.origin, level.player.origin ) > 1500 )
{
return false;
}
// if he's in a "location" that'll be a better way to find him
if ( IsDefined( self GetLocation() ) )
{
return false;
}
node = self bcGetClaimedNode();
// if he doesn't have a claimed node, he's not in cover
if ( !IsDefined( node ) )
{
return true;
}
// if the node is cover or conceal, he's not exposed
if ( !self isNodeCoverOrConceal() )
{
return false;
}
return true;
}
isNodeCoverOrConceal()
{
node = self.node;
if ( !IsDefined( node ) )
{
return false;
}
if ( IsSubStr( node.type, "Cover" ) || IsSubStr( node.type, "Conceal" ) )
{
return true;
}
return false;
}
squadHasOfficer( squad )
{
if ( squad.officerCount > 0 )
{
return true;
}
else
{
return false;
}
}
isOfficer()
{
fullRank = self getRank();
if ( !isdefined( fullRank ) )
return false;
if ( fullRank == "sergeant"
|| fullRank == "lieutenant"
|| fullRank == "captain"
|| fullRank == "sergeant" )
{
return true;
}
return false;
}
bcGetClaimedNode()
{
if ( IsPlayer( self ) )
return self.node;
else
return self GetClaimedNode();
}
enemy_team_name()
{
if ( self IsBadGuy() )
return true;
else
return false;
}
getName()
{
if ( enemy_team_name() )
{
name = self.ainame;
}
else if ( self.team == "allies" )
{
name = self.name;
}
else
{
name = undefined;
}
if ( !isdefined( name ) || self voice_is_british_based() )
{
return( undefined );
}
// check to see if this is a name with two parts, like "Sgt. Peas" or "Agent Smith"
tokens = StrTok( name, " " );
if ( tokens.size < 2 )
{
return( name );
}
Assert( tokens.size > 1 );
return( tokens[ 1 ] );
}
getRank()
{
return self.airank;
}
getClosestFriendlySpeaker( strAction )
{
speakers = self getSpeakers( strAction, self.team );
speaker = getClosest( self.origin, speakers );
return( speaker );
}
getSpeakers( strAction, team )
{
speakers = [];
soldiers = GetAIArray( team );
for ( i = 0; i < soldiers.size; i++ )
{
if ( soldiers[ i ] == self )
{
continue;
}
if ( !soldiers[ i ] canSay( strAction ) )
{
continue;
}
speakers[ speakers.size ] = soldiers[ i ];
}
return( speakers );
}
// see if self can find someone to respond to him
getResponder( distMin, distMax, eventType )
{
responder = undefined;
if ( !IsDefined( eventType ) )
{
eventType = "response";
}
soldiers = array_randomize( self.squad.members );
for ( i = 0; i < soldiers.size; i++ )
{
if ( soldiers[ i ] == self )
{
continue;
}
if ( !IsAlive( soldiers[ i ] ) )
{
continue;
}
if ( Distance( self.origin, soldiers[ i ].origin ) > distMin
&& Distance( self.origin, soldiers[ i ].origin ) < distMax
&& !self isUsingSameVoice( soldiers[ i ] )
&& soldiers[ i ] canSay( eventType ) )
{
responder = soldiers[ i ];
// prioritize for guys whose names we know how to say
if ( self canSayName( responder ) )
{
break;
}
}
}
return responder;
}
getLocation()
{
myLocations = self get_all_my_locations();
myLocations = array_randomize( myLocations );
if ( myLocations.size )
{
// give us new ones first
foreach ( location in myLocations )
{
if ( !location_called_out_ever( location ) )
{
return location;
}
}
// otherwise just get a valid one
foreach ( location in myLocations )
{
if ( !location_called_out_recently( location ) )
{
return location;
}
}
}
return undefined;
}
get_all_my_locations()
{
allLocations = anim.bcs_locations;
myLocations = [];
foreach ( location in allLocations )
{
if ( self IsTouching( location ) && IsDefined( location.locationAliases ) )
{
myLocations[ myLocations.size ] = location;
}
}
return myLocations;
}
is_in_callable_location()
{
myLocations = self get_all_my_locations();
foreach ( location in myLocations )
{
if ( !location_called_out_recently( location ) )
{
return true;
}
}
return false;
}
location_called_out_ever( location )
{
lastCalloutTime = location_get_last_callout_time( location );
if ( !IsDefined( lastCalloutTime ) )
{
return false;
}
return true;
}
location_called_out_recently( location )
{
lastCalloutTime = location_get_last_callout_time( location );
if ( !IsDefined( lastCalloutTime ) )
{
return false;
}
nextCalloutTime = lastCalloutTime + anim.eventActionMinWait[ "threat" ][ "location_repeat" ];
if ( GetTime() < nextCalloutTime )
{
return true;
}
return false;
}
location_add_last_callout_time( location )
{
anim.locationLastCalloutTimes[ location.classname ] = GetTime();
}
location_get_last_callout_time( location )
{
if ( IsDefined( anim.locationLastCalloutTimes[ location.classname ] ) )
{
return anim.locationLastCalloutTimes[ location.classname ];
}
return undefined;
}
// if AI is on a cover node, we want to use the node angles to determine threats relative
// to him; this eliminates false callouts in case the AI's cover animation turns him
// away from the threats
getRelativeAngles( ent )
{
Assert( IsDefined( ent ) );
angles = ent.angles;
if ( !IsPlayer( ent ) )
{
node = ent bcGetClaimedNode();
if ( IsDefined( node ) )
{
angles = node.angles;
}
}
return angles;
}
sideIsLeftRight( side )
{
if ( side == "left" || side == "right" )
{
return true;
}
return false;
}
/* DEPRECATED but this is a cool function, we should keep it around somewhere
getDirectionReferenceSide( vOrigin, vPoint, vReference )
{
anglesToReference = VectorToAngles( vReference - vOrigin );
anglesToPoint = VectorToAngles( vPoint - vOrigin );
angle = anglesToReference[ 1 ] - anglesToPoint[ 1 ];
angle += 360;
angle = Int( angle ) % 360;
if ( angle > 180 )
angle -= 360;
if ( angle > 2 && angle < 45 )
side = "right";
else if ( angle < - 2 && angle > - 45 )
side = "left";
else
{
if ( Distance( vOrigin, vPoint ) < Distance( vOrigin, vReference ) )
side = "front";
else
side = "rear";
}
return( side );
}
*/
getDirectionFacingFlank( vOrigin, vPoint, vFacing )
{
anglesToFacing = VectorToAngles( vFacing );
anglesToPoint = VectorToAngles( vPoint - vOrigin );
angle = anglesToFacing[ 1 ] - anglesToPoint[ 1 ];
angle += 360;
angle = Int( angle ) % 360;
if ( angle > 315 || angle < 45 )
direction = "front";
else if ( angle < 135 )
direction = "right";
else if ( angle < 225 )
direction = "rear";
else
direction = "left";
return( direction );
}
// takes output from getDirectionCompass and normalizes it to the convention
// used by the soundaliases
normalizeCompassDirection( direction )
{
Assert( IsDefined( direction ) );
new = undefined;
switch( direction )
{
case "north":
new = "n";
break;
case "northwest":
new = "nw";
break;
case "west":
new = "w";
break;
case "southwest":
new = "sw";
break;
case "south":
new = "s";
break;
case "southeast":
new = "se";
break;
case "east":
new = "e";
break;
case "northeast":
new = "ne";
break;
case "impossible":
new = "impossible";
break;
default:
AssertMsg( "Can't normalize compass direction " + direction );
return;
}
Assert( IsDefined( new ) );
return new;
}
getDirectionCompass( vOrigin, vPoint )
{
angles = VectorToAngles( vPoint - vOrigin );
angle = angles[ 1 ];
northYaw = GetNorthYaw();
angle -= northYaw;
if ( angle < 0 )
angle += 360;
else if ( angle > 360 )
angle -= 360;
if ( angle < 22.5 || angle > 337.5 )
direction = "north";
else if ( angle < 67.5 )
direction = "northwest";
else if ( angle < 112.5 )
direction = "west";
else if ( angle < 157.5 )
direction = "southwest";
else if ( angle < 202.5 )
direction = "south";
else if ( angle < 247.5 )
direction = "southeast";
else if ( angle < 292.5 )
direction = "east";
else if ( angle < 337.5 )
direction = "northeast";
else
direction = "impossible";
return( direction );
}
// takes a getDirectionFacingClock value and, if it's in the "front arc" (10-2 on the clock face),
// will return it normalized to 10, 12, or 2. Otherwise, returns undefined.
getFrontArcClockDirection( direction )
{
AssertEx( IsDefined( direction ) );
faDirection = "undefined";
if ( direction == "10" || direction == "11" )
{
faDirection = "10";
}
else if ( direction == "12" )
{
faDirection = direction;
}
else if ( direction == "1" || direction == "2" )
{
faDirection = "2";
}
return faDirection;
}
// gets a clock direction from a "viewer" to a "target"
getDirectionFacingClock( viewerAngles, viewerOrigin, targetOrigin )
{
forward = AnglesToForward( viewerAngles );
vFacing = VectorNormalize( forward );
anglesToFacing = VectorToAngles( vFacing );
anglesToPoint = VectorToAngles( targetOrigin - viewerOrigin );
angle = anglesToFacing[ 1 ] - anglesToPoint[ 1 ];
angle += 360;
angle = Int( angle ) % 360;
if ( angle > 345 || angle < 15 )
{
direction = "12";
}
else if ( angle < 45 )
{
direction = "1";
}
else if ( angle < 75 )
{
direction = "2";
}
else if ( angle < 105 )
{
direction = "3";
}
else if ( angle < 135 )
{
direction = "4";
}
else if ( angle < 165 )
{
direction = "5";
}
else if ( angle < 195 )
{
direction = "6";
}
else if ( angle < 225 )
{
direction = "7";
}
else if ( angle < 255 )
{
direction = "8";
}
else if ( angle < 285 )
{
direction = "9";
}
else if ( angle < 315 )
{
direction = "10";
}
else
{
direction = "11";
}
return( direction );
}
getVectorRightAngle( vDir )
{
return( vDir[ 1 ], 0 - vDir[ 0 ], vDir[ 2 ] );
}
getVectorArrayAverage( avAngles )
{
vDominantDir = ( 0, 0, 0 );
for ( i = 0; i < avAngles.size; i++ )
vDominantDir += avAngles[ i ];
return( vDominantDir[ 0 ] / avAngles.size, vDominantDir[ 1 ] / avAngles.size, vDominantDir[ 2 ] / avAngles.size );
}
addNameAlias( name )
{
self.soundAliases[ self.soundAliases.size ] =
self.owner.countryID + "_" + self.owner.npcID + "_name_" + name;
anim.lastNameSaid[ self.owner.team ] = name;
anim.lastNameSaidTime[ self.owner.team ] = GetTime();
}
addPlayerNameAlias()
{
if ( !self.owner canSayPlayerName() )
{
return;
}
anim.lastPlayerNameCallTime = GetTime();
nameAlias = self.owner.countryID + "_" + self.owner.npcID + "_name_player_" + level.player.bcCountryID + "_" + level.player.bcNameID;
self.soundAliases[ self.soundAliases.size ] = nameAlias;
self.lookTarget = level.player;
}
addRankAlias( name )
{
self.soundAliases[ self.soundAliases.size ] = self.owner.countryID + "_" + self.owner.npcID + "_rank_" + name;
}
canSayName( ai )
{
// axis don't use names
if ( enemy_team_name() )
{
return false;
}
if ( !IsDefined( ai.bcName ) )
{
return false;
}
// SRE'd so added this defensive mechanism
if ( !IsDefined( ai.countryID ) )
{
return false;
}
// don't want to cross the streams for AI names in mixed-nationality squads
if ( self.countryID != ai.countryID )
{
return false;
}
// make sure we don't say this guy's name too frequently
if ( self nameSaidRecently( ai ) )
{
return false;
}
nameAlias = self.countryID + "_" + self.npcID + "_name_" + ai.bcName;
if ( SoundExists( nameAlias ) )
{
return true;
}
return false;
}
nameSaidRecently( ai )
{
if ( ( anim.lastNameSaid[ self.team ] == ai.bcName ) && ( ( GetTime() - anim.lastNameSaidTime[ self.team ] ) < anim.lastNameSaidTimeout ) )
{
return true;
}
return false;
}
canSayPlayerName()
{
if ( enemy_team_name() )
{
return false;
}
if ( !IsDefined( level.player.bcNameID ) || !IsDefined( level.player.bcCountryID ) )
{
return false;
}
if ( player_name_called_recently() )
{
return false;
}
nameAlias = self.countryID + "_" + self.npcID + "_name_player_" + level.player.bcCountryID + "_" + level.player.bcNameID;
if ( SoundExists( nameAlias ) )
{
return true;
}
return false;
}
player_name_called_recently()
{
if ( !IsDefined( anim.lastPlayerNameCallTime ) )
{
return false;
}
if ( GetTime() - anim.lastPlayerNameCallTime >= anim.eventTypeMinWait[ "playername" ] )
{
return false;
}
return true;
}
isUsingSameVoice( otherguy )
{
/#
if ( GetDvar( "bcs_allowsamevoiceresponse" ) == "on" )
{
return false;
}
#/
if ( ( IsString( self.npcID ) && IsString( otherguy.npcID ) ) && ( self.npcID == otherguy.npcID ) )
{
return true;
}
else if ( ( !isString( self.npcID ) && !isString( otherguy.npcID ) ) && ( self.npcID == otherguy.npcID ) )
{
return true;
}
else
{
return false;
}
}
// format: US_1_threat_[type], with optional _[modifier]
addThreatAlias( type, modifier )
{
Assert( IsDefined( type ) );
threat = self.owner.countryID + "_" + self.owner.npcID + "_threat_" + type;
// not all threat aliases use modifiers anymore
if ( IsDefined( modifier ) )
{
threat += ( "_" + modifier );
}
self.soundAliases = array_add( self.soundAliases, threat );
return true;
}
// format: US_1_exposed_[type]
addThreatExposedAlias( type )
{
Assert( IsDefined( type ) );
alias = self.owner.countryID + "_" + self.owner.npcID + "_exposed_" + type;
self.soundAliases[ self.soundAliases.size ] = alias;
return true;
}
// format: US_1_order_action_suppress
addThreatObviousAlias()
{
// just using the order_action_suppress aliases
alias = self.owner.countryID + "_" + self.owner.npcID + "_order_action_suppress";
self.soundAliases[ self.soundAliases.size ] = alias;
return true;
}
// format: [reportAlias]_echo ("_echo" replaces "_report" in the reportAlias)
addThreatCalloutEcho( reportAlias, respondTo )
{
Assert( IsDefined( reportAlias ) );
alias = self createEchoAlias( reportAlias, respondTo );
if ( !SoundExists( alias ) )
{
/#
PrintLn( anim.bcPrintFailPrefix + "Can't find echo alias '" + alias + "'." );
#/
// TODO maybe output to data csv/txt file later
return false;
}
self.soundAliases[ self.soundAliases.size ] = alias;
return true;
}
// format: US_1_resp_ack_co_gnrc_[affirm/neg]
addThreatCalloutResponseAlias( modifier )
{
Assert( IsDefined( modifier ) );
alias = self.owner.countryID + "_" + self.owner.npcID + "_resp_ack_co_gnrc_" + modifier;
if ( !SoundExists( alias ) )
{
/#
PrintLn( anim.bcPrintFailPrefix + "Can't find callout response alias '" + alias + "'." );
#/
// TODO maybe output to data csv/txt file later
return false;
}
self.soundAliases[ self.soundAliases.size ] = alias;
return true;
}
addThreatCalloutQA_NextLine( respondTo, prevLine, location )
{
Assert( IsDefined( respondTo ) && IsDefined( prevLine ) );
// figure out the partial alias so we can reconstruct it later
// this is easier than parsing out the prevLine to just get the meat
partialAlias = undefined;
foreach ( str in location.locationAliases )
{
if ( IsSubStr( prevLine, str ) )
{
partialAlias = str;
break;
}
}
Assert( IsDefined( partialAlias ) );
// now try to construct the new string
prefix = self.owner.countryID + "_" + self.owner.npcID + "_co_";
lastChar = GetSubStr( prevLine, prevLine.size - 1, prevLine.size );
Assert( string_is_single_digit_integer( lastChar ) );
nextIndex = Int( lastChar ) + 1;
qaAlias = prefix + partialAlias + "_qa" + nextIndex;
if ( !SoundExists( qaAlias ) )
{
// finish up the conversation with a yes/no response
if ( RandomInt( 100 ) < anim.eventChance[ "response" ][ "callout_negative" ] )
{
respondTo addResponseEvent( "callout", "neg", self.owner, 0.9 );
}
else
{
respondTo addResponseEvent( "exposed", "acquired", self.owner, 0.9 );
}
// from now on we'll use the base alias to refer to it since we talked about it already
location.qaFinished = true;
return false;
}
// keep the QA conversation going, potentially
respondTo addResponseEvent( "callout", "QA", self.owner, 0.9, qaAlias, location );
self.soundAliases[ self.soundAliases.size ] = qaAlias;
return true;
}
// takes a soundalias that ends with "_report" and returns a variant of it that ends with "_echo"
createEchoAlias( reportAlias, respondTo )
{
reportSuffix = "_report";
echoSuffix = "_echo";
// make sure that we're responding in the responder's countryID and voice
echoPrefix = self.owner.countryID + "_" + self.owner.npcID + "_";
AssertEx( IsSubStr( reportAlias, reportSuffix ), "reportAlias doesn't have substring '" + reportSuffix + "', so it doesn't look like an eligible report alias." );
reportSuffixStartIndex = reportAlias.size - reportSuffix.size;// figure out where the end of this baseAlias is
// some guys have longer npcIDs than others, so we have to allow the base prefix to be variable length
oldPrefix = self.owner.countryID + "_" + respondTo.npcID + "_";
oldPrefixLength = oldPrefix.size;
baseAlias = GetSubStr( reportAlias, oldPrefixLength, reportSuffixStartIndex );// start 5 spots in to eliminate the original countryID and npcID info
// construct the final alias
echoAlias = echoPrefix + baseAlias + echoSuffix;
return echoAlias;
}
// format: US_1_callout_[contactclock/targetclock/yourclock/cardinal]_[modifier]
addThreatCalloutAlias( type, modifier )
{
Assert( IsDefined( type ) && IsDefined( modifier ) );
alias = self.owner.countryID + "_" + self.owner.npcID + "_callout_" + type + "_" + modifier;
self.soundAliases[ self.soundAliases.size ] = alias;
return true;
}
// "landmarks" are aka "objects" in the soundaliases
// format: US_1_callout_obj_[landmark]_your(optional)_[frontArcDirection]
// - isRelative dictates if we will add the "your" to the string
addThreatCalloutLandmarkAlias( landmark, frontArcDirection, isRelative )
{
Assert( IsDefined( landmark ) && IsDefined( frontArcDirection ) );
landmarkStr = landmark.script_landmark;
if ( !IsDefined( isRelative ) )
{
isRelative = false;
}
alias = self.owner.countryID + "_" + self.owner.npcID + "_callout_obj_" + landmarkStr;
if ( isRelative )
{
alias += "_y";
}
alias += "_" + frontArcDirection;
if ( !SoundExists( alias ) )
{
/#
PrintLn( anim.bcPrintFailPrefix + "Can't find sound alias '" + alias + "'. Does landmark '" + landmarkStr + "' have callout references in the battlechatter csv for nationality '" + self.owner.countryID + "'?" );
#/
// TODO maybe output to data csv/txt file later
return false;
}
self.soundAliases[ self.soundAliases.size ] = alias;
return true;
}
// format: US_1_co_[location.locationAliases[idx]]
// -- oldstyle format: US_1_callout_loc_[location](optional if floor exists)_[floor](optional if location exists)_[left/right](optional)_report(optional)
addThreatCalloutLocationAlias( location )
{
Assert( IsDefined( location ) && IsDefined( location.locationAliases ) );
finalAlias = undefined;
// some triggers have more than one alias set up
locationAliases = location.locationAliases;
Assert( locationAliases.size );
locAlias = locationAliases[ 0 ];
if ( locationAliases.size > 1 )
{
// prefer aliases that make the AIs talk more
responseAlias = undefined;
responseAlias = location GetCannedResponse( self.owner );
if ( IsDefined( responseAlias ) )
{
locAlias = responseAlias;
}
else
{
// otherwise just randomize it
locAlias = random( locationAliases );
}
}
alias = undefined;
// see if this is a QA conversation that hasn't been finished
if ( !IsDefined( location.qaFinished ) && IsCalloutTypeQA( locAlias, self.owner ) )
{
alias = self.owner GetQACalloutAlias( locAlias, 0 );
}
else
{
// standard prefix
prefix = self.owner.countryID + "_" + self.owner.npcID + "_";
// this separates oldstyle location callouts so we can use some older assets
// - in the future we'll just add the rest of the prefix string above
if ( !IsSubStr( locAlias, "callout" ) )
{
prefix += "co_";// the newstyle standard
}
alias = prefix + locAlias;
}
if ( SoundExists( alias ) )
{
finalAlias = alias;
}
if ( !IsDefined( finalAlias ) )
{
/#
printStr = anim.bcPrintFailPrefix + "Couldn't find a location callout alias for data:";
if ( IsDefined( location ) )
{
printStr += " location=" + locAlias;
}
if ( IsDefined( alias ) )
{
printStr += " finalAlias=" + alias;
}
printStr += ". Are you sure that there is an alias to support it?";
PrintLn( printStr );
// TODO maybe output to data csv/txt file later
#/
return false;
}
location_add_last_callout_time( location );
self.soundAliases[ self.soundAliases.size ] = finalAlias;
return true;
}
addInformAlias( type, modifier )
{
Assert( IsDefined( type ) && IsDefined( modifier ) );
alias = self.owner.countryID + "_" + self.owner.npcID + "_inform_" + type + "_" + modifier;
self.soundAliases[ self.soundAliases.size ] = alias;
}
addResponseAlias( type, modifier )
{
Assert( IsDefined( type ) && IsDefined( modifier ) );
alias = self.owner.countryID + "_" + self.owner.npcID + "_response_" + type + "_" + modifier;
self.soundAliases[ self.soundAliases.size ] = alias;
return( true );
}
addReactionAlias( type, modifier )
{
Assert( IsDefined( type ) && IsDefined( modifier ) );
reaction = self.owner.countryID + "_" + self.owner.npcID + "_reaction_" + type + "_" + modifier;
self.soundAliases[ self.soundAliases.size ] = reaction;
return( true );
}
addCheckFireAlias()
{
reaction = self.owner.countryID + "_" + self.owner.npcID + "_check_fire";
self.soundAliases[ self.soundAliases.size ] = reaction;
return true;
}
addTauntAlias( type, modifier )
{
Assert( IsDefined( type ) && IsDefined( modifier ) );
reaction = self.owner.countryID + "_" + self.owner.npcID + "_taunt";
self.soundAliases[ self.soundAliases.size ] = reaction;
return( true );
}
// format: GE_1_hostile_burst
addHostileBurstAlias()
{
burst = self.owner.countryID + "_" + self.owner.npcID + "_hostile_burst";
self.soundAliases[ self.soundAliases.size ] = burst;
return true;
}
// format: US_1_order_move_follow (etc.)
addOrderAlias( type, modifier )
{
Assert( IsDefined( type ) && IsDefined( modifier ) );
order = self.owner.countryID + "_" + self.owner.npcID + "_order_" + type + "_" + modifier;
self.soundAliases[ self.soundAliases.size ] = order;
return true;
}
initContact( squadName )
{
if ( !isdefined( self.squadList[ squadName ].calledOut ) )
self.squadList[ squadName ].calledOut = false;
if ( !isdefined( self.squadList[ squadName ].firstContact ) )
self.squadList[ squadName ].firstContact = 2000000000;
if ( !isdefined( self.squadList[ squadName ].lastContact ) )
self.squadList[ squadName ].lastContact = 0;
}
shutdownContact( squadName )
{
self.squadList[ squadName ].calledOut = undefined;
self.squadList[ squadName ].firstContact = undefined;
self.squadList[ squadName ].lastContact = undefined;
}
createChatEvent( eventAction, eventType, priority )
{
chatEvent = SpawnStruct();
chatEvent.owner = self;
chatEvent.eventType = eventType;
chatEvent.eventAction = eventAction;
if ( IsDefined( priority ) )
chatEvent.priority = priority;
else
chatEvent.priority = anim.eventPriority[ eventAction ][ eventType ];
chatEvent.expireTime = GetTime() + anim.eventDuration[ eventAction ][ eventType ];
return chatEvent;
}
createChatPhrase()
{
chatPhrase = SpawnStruct();
chatPhrase.owner = self;
chatPhrase.soundAliases = [];
chatPhrase.master = false;
return chatPhrase;
}
pointInFov( origin )
{
forward = AnglesToForward( self.angles );
normalVec = VectorNormalize( origin - self.origin );
dot = VectorDot( forward, normalVec );
return dot > 0.766;// fov = 80
}
// the "front arc" goes from 9 to 3 on a clock face - the front 180 degrees
entInFrontArc( ent )
{
direction = getDirectionFacingClock( self.angles, self.origin, ent.origin );
if ( direction == "9"
|| direction == "10"
|| direction == "11"
|| direction == "12"
|| direction == "1"
|| direction == "2"
|| direction == "3" )
{
return true;
}
return false;
}
/****************************************************************************
flavor burst transmissions
*****************************************************************************/
// self = the squad
squadFlavorBurstTransmissions()
{
anim endon( "battlechatter disabled" );
self endon( "squad_deleting" );
if ( self.team != "allies" )
{
// hackish, don't need it to be more complicated for now though
if ( level.script != "af_caves" )
{
return;
}
}
// wait until an AI gets put in the squad
while ( self.memberCount <= 0 )
{
wait( 0.5 );
}
// don't do regular waits if we're coming back from flavorbursts being disabled
burstingWasPaused = false;
while ( IsDefined( self ) )
{
// make sure at least one of the guys in the squad can burst
if ( !squadCanBurst( self ) )
{
burstingWasPaused = true;
wait( 1 );
continue;
}
else if ( self.fbt_firstBurst )
{
if ( !burstingWasPaused )
{
wait( RandomFloat( anim.fbt_waitMin ) );
}
if ( burstingWasPaused )
{
burstingWasPaused = false;
}
self.fbt_firstBurst = false;
}
else
{
if ( !burstingWasPaused )
{
wait( RandomFloatRange( anim.fbt_waitMin, anim.fbt_waitMax ) );
}
if ( burstingWasPaused )
{
burstingWasPaused = false;
}
}
burster = getBurster( self );
if ( !IsDefined( burster ) )
{
continue;
}
nationality = burster.voice;
burstID = getFlavorBurstID( self, nationality );
aliases = getFlavorBurstAliases( nationality, burstID );
foreach ( i, alias in aliases )
{
// see if we need to migrate our burster
if ( !burster canDoFlavorBurst() || Distance( level.player.origin, burster.origin ) > anim.fbt_desiredDistMax )
{
for ( j = 0; j < self.members.size; j++ )
{
burster = getBurster( self );
if ( !IsDefined( burster ) )
{
continue;
}
// to continue the burst transmission, we want to stick with the same
// nationality - this is in case we have a squad of mixed nationalities
if ( burster.voice == nationality )
{
break;
}
}
// if we can't find a new burster of the same nationality,
// quit this transmission
if ( !IsDefined( burster ) || burster.voice != nationality )
{
break;
}
}
// play the burst
self thread playFlavorBurstLine( burster, alias );
self waittill( "burst_line_done" );
if ( i != ( aliases.size - 1 ) )
{
wait( RandomFloatRange( anim.fbt_lineBreakMin, anim.fbt_lineBreakMax ) );
}
}
}
}
squadCanBurst( squad )
{
foundOne = false;
foreach ( member in squad.members )
{
if ( member canDoFlavorBurst() )
{
foundOne = true;
break;
}
}
return foundOne;
}
canDoFlavorBurst()
{
canDo = false;
if ( !IsPlayer( self )
&& IsAlive( self )
&& self.classname != "actor_enemy_dog"
&& level.flavorbursts[ self.team ]
&& self voiceCanBurst()
&& self.flavorbursts )
{
canDo = true;
}
return canDo;
}
voiceCanBurst()
{
if ( IsDefined( anim.flavorburstVoices[ self.voice ] ) && anim.flavorburstVoices[ self.voice ] )
{
return true;
}
return false;
}
getBurster( squad )
{
burster = undefined;
// prioritize by player proximity
// for some reason, get_array_of_farthest returns the closest at index 0
squadMembers = get_array_of_farthest( level.player.origin, squad.members );
foreach ( guy in squadMembers )
{
if ( guy canDoFlavorBurst() )
{
burster = guy;
if ( !IsDefined( squad.fbt_lastBursterID ) )
{
break;
}
// try not to play it off the last guy we played it off of
if ( IsDefined( squad.fbt_lastBursterID ) && squad.fbt_lastBursterID == burster.unique_id )
{
continue;
}
}
}
if ( IsDefined( burster ) )
{
// store the ent's unique ID because the ent could be gone by the time we check again
squad.fbt_lastBursterID = burster.unique_id;
}
return burster;
}
getFlavorBurstID( squad, nationality )
{
bursts = array_randomize( anim.flavorbursts[ nationality ] );
// if we used all of the flavor bursts already, reset
if ( anim.flavorburstsUsed.size >= bursts.size )
{
anim.flavorburstsUsed = [];
}
burstID = undefined;
foreach ( burst in bursts )
{
burstID = burst;
if ( !flavorBurstWouldRepeat( burstID ) )
{
break;
}
}
anim.flavorburstsUsed[ anim.flavorburstsUsed.size ] = burstID;
return burstID;
}
flavorBurstWouldRepeat( burstID )
{
if ( !anim.flavorburstsUsed.size )
{
return false;
}
foundIt = false;
foreach ( usedBurst in anim.flavorburstsUsed )
{
if ( usedBurst == burstID )
{
foundIt = true;
break;
}
}
return foundIt;
}
getFlavorBurstAliases( nationality, burstID, startingLine )
{
if ( !IsDefined( startingLine ) )
{
startingLine = 1;
}
burstLine = startingLine;
aliases = [];
while ( 1 )
{
alias = "FB_" + anim.countryIDs[ nationality ] + "_" + burstID + "_" + burstLine;
burstLine++;
if ( SoundExists( alias ) )
{
aliases[ aliases.size ] = alias;
}
else
{
break;
}
}
return aliases;
}
playFlavorBurstLine( burster, alias )
{
anim endon( "battlechatter disabled" );
/#
if ( GetDvar( "bcs_fbt_debug" ) == "on" )
{
self thread flavorBurstLineDebug( burster, alias );
}
#/
// make a separate origin to play the sound off of so that mission dialogue doesn't get cut off when played on this guy at the same time
soundOrg = Spawn( "script_origin", burster.origin );
soundOrg LinkTo( burster );
soundOrg PlaySound( alias, alias, true );
soundOrg waittill( alias );
soundOrg Delete();
if ( IsDefined( self ) )
self notify( "burst_line_done" );
}
flavorBurstLineDebug( burster, alias )
{
self endon( "burst_line_done" );
while ( 1 )
{
Print3d( burster GetTagOrigin( "j_spinelower" ), alias, ( 1, 1, 1 ), 1, 0.5 );
wait( 0.05 );
}
}
/****************************************************************************
debugging functions
*****************************************************************************/
/*
debugPrintEvents()
{
if ( !isalive( self ) )
return;
if ( GetDvar( "debug_bcshowqueue" ) != self.team && GetDvar( "debug_bcshowqueue" ) != "all" )
return;
self endon( "death" );
self notify( "debugPrintEvents" );
self endon( "debugPrintEvents" );
queueEvents = self getQueueEvents();
colors[ "g" ] = ( 0, 1, 0 );
colors[ "y" ] = ( 1, 1, 0 );
colors[ "r" ] = ( 1, 0, 0 );
colors[ "b" ] = ( 0, 0, 0 );
while ( 1 )
{
aboveHead = self GetShootAtPos() + ( 0, 0, 10 );
for ( i = 0; i < queueEvents.size; i++ )
{
Print3d( aboveHead, queueEvents[ i ], colors[ queueEvents[ i ][ 0 ] ], 1, 0.5 ); // origin, text, RGB, alpha, scale
aboveHead += ( 0, 0, 5 );
}
wait 0.05;
}
}
debugQueueEvents()
{
if ( GetDvar( "debug_bcresponse" ) == "on" )
self thread printQueueEvent( "response" );
if ( GetDvar( "debug_bcthreat" ) == "on" )
self thread printQueueEvent( "threat" );
if ( GetDvar( "debug_bcinform" ) == "on" )
self thread printQueueEvent( "inform" );
if ( GetDvar( "debug_bcorder" ) == "on" )
self thread printQueueEvent( "order" );
}
printAboveHead( string, duration, offset )
{
self endon( "death" );
if ( !isdefined( offset ) )
offset = ( 0, 0, 0 );
for ( i = 0; i < ( duration * 2 ); i++ )
{
if ( !isalive( self ) )
return;
aboveHead = self GetShootAtPos() + ( 0, 0, 10 ) + offset;
Print3d( aboveHead, string, ( 1, 0, 0 ), 1, 0.5 ); // origin, text, RGB, alpha, scale
wait 0.05;
}
}
printQueueEvent( eventAction )
{
time = GetTime();
if ( self.chatQueue[ eventAction ].expireTime > 0 && !isdefined( self.chatQueue[ eventAction ].printed ) )
{
Print( "QUEUE EVENT " + eventAction + "_" + self.chatQueue[ eventAction ].eventType + ": " );
if ( time > self.chatQueue[ eventAction ].expireTime )
PrintLn( "^2 missed by " + ( time - self.chatQueue[ eventAction ].expireTime ) + "ms" );
else
PrintLn( "slack of " + ( self.chatQueue[ eventAction ].expireTime - time ) + "ms" );
self.chatQueue[ eventAction ].printed = true;
}
}
*/
battleChatter_canPrint()
{
/#
if ( GetDebugDvar( "debug_bcprint" ) == self.team || GetDebugDvar( "debug_bcprint" ) == "all" )
return( true );
#/
return( false );
}
battleChatter_canPrintDump()
{
/#
if ( GetDebugDvar( "debug_bcprintdump" ) == self.team || GetDebugDvar( "debug_bcprintdump" ) == "all" )
{
return true;
}
#/
return false;
}
// SRS 10/16/08: this used to be unnecessarily covered with two functions
battleChatter_print( aliases )
{
if ( aliases.size <= 0 )
{
AssertMsg( "battleChatter_print(): the aliases array is empty." );
return;
}
if ( !self battleChatter_canPrint() )
{
return;
}
colorPrefix = "^5 ";// allies
if ( enemy_team_name() )
{
colorPrefix = "^6 ";
}
// print to the console
Print( colorPrefix );
foreach ( alias in aliases )
{
Print( alias );
}
PrintLn( "" );
}
// optionally dumps info out to files for examination later
battleChatter_printDump( aliases, descriptor )
{
/#
if ( !self battleChatter_canPrintDump() )
{
return;
}
if ( aliases.size <= 0 )
{
AssertMsg( "battleChatter_printDump(): the aliases array is empty." );
return;
}
dumpType = GetDvar( "debug_bcprintdumptype" );
if ( dumpType != "csv" && dumpType != "txt" )
{
return;
}
// do this early, in case the file writing hangs for a bit of time
secsSinceLastDump = -1;
if ( IsDefined( level.lastDumpTime ) )
{
secsSinceLastDump = ( GetTime() - level.lastDumpTime ) / 1000;
}
level.lastDumpTime = GetTime();// reset
// -- CSV dumps help the audio dept optimize where they spend their time --
if ( dumpType == "csv" )
{
// only 1 write at a time
if ( !flag_exist( "bcs_csv_dumpFileWriting" ) )
{
flag_init( "bcs_csv_dumpFileWriting" );
}
// open the file, if it's not already open
if ( !IsDefined( level.bcs_csv_dumpFile ) )
{
filePath = "scriptgen/battlechatter/bcsDump_" + level.script + ".csv";
level.bcs_csv_dumpFile = OpenFile( filePath, "write" );
}
// dump a new line for each sound
// format: levelname,countryID,npcID,aliasType
foreach ( alias in aliases )
{
aliasType = getAliasTypeFromSoundalias( alias );
dumpString = level.script + ","
+ self.countryID + ","
+ self.npcID + ","
+ aliasType;
battleChatter_printDumpLine( level.bcs_csv_dumpFile, dumpString, "bcs_csv_dumpFileWriting" );
}
}
// -- TXT dumps help the design dept tweak distributions and timing --
else if ( dumpType == "txt" )
{
AssertEx( IsDefined( descriptor ), "battlechatter print dumps of type 'txt' require a descriptor!" );
if ( !flag_exist( "bcs_txt_dumpFileWriting" ) )
{
flag_init( "bcs_txt_dumpFileWriting" );
}
if ( !IsDefined( level.bcs_txt_dumpFile ) )
{
filePath = "scriptgen/battlechatter/bcsDump_" + level.script + ".txt";
level.bcs_txt_dumpFile = OpenFile( filePath, "write" );
}
name = self.name;
if ( enemy_team_name() )
{
name = self.ainame;
}
// format: (2.3 secs) US_1 order_move_follow: US_1_threat_rpg_generic, US_1_landmark_near_cargocontainer, US_1_direction_relative_north
dumpString = "(" + secsSinceLastDump + " secs) ";
dumpString += name + " " + descriptor + ": ";
foreach ( i, alias in aliases )
{
dumpString += alias;
if ( i != ( aliases.size - 1 ) )
{
dumpString += ", ";
}
}
battleChatter_printDumpLine( level.bcs_txt_dumpFile, dumpString, "bcs_txt_dumpFileWriting" );
}
#/
}
getAliasTypeFromSoundalias( alias )
{
// get the prefix and make sure it matches as we'd expect
prefix = self.countryID + "_" + self.npcID + "_";
AssertEx( IsSubStr( alias, prefix ), "didn't find expected prefix info in alias '" + alias + "' with substr test of '" + prefix + "'." );
// figure out the alias type by removing the prefix
aliasType = GetSubStr( alias, prefix.size, alias.size );
return aliasType;
}
battleChatter_printDumpLine( file, str, controlFlag )
{
if ( flag( controlFlag ) )
{
flag_wait( controlFlag );
}
flag_set( controlFlag );
FPrintLn( file, str );
flag_clear( controlFlag );
}
bcDrawObjects()
{
for ( i = 0; i < anim.bcs_locations.size; i++ )
{
locationAliases = anim.bcs_locations[ i ].locationAliases;
if ( !IsDefined( locationAliases ) )
{
continue;
}
locationStr = "";
foreach ( alias in locationAliases )
{
locationStr += alias;
}
thread drawBCObject( "Location: " + locationStr, anim.bcs_locations[ i ] GetOrigin(), ( 0, 0, 8 ), ( 1, 1, 1 ) );
}
}
drawBCObject( string, origin, offset, color )
{
while ( true )
{
if ( Distance( level.player.origin, origin ) > 2048 )
{
wait( 0.1 );
continue;
}
Print3d( origin + offset, string, color, 1, 0.75 ); // origin, text, RGB, alpha, scale
wait 0.05;
}
}
drawBCDirections( landmark, offset, color )
{
landmarkOrigin = landmark GetOrigin();
while ( true )
{
if ( Distance( level.player.origin, landmarkOrigin ) > 2048 )
{
wait( 0.1 );
continue;
}
compass = getDirectionCompass( level.player.origin, landmarkOrigin );
compass = normalizeCompassDirection( compass );
clock = getDirectionFacingClock( level.player.angles, level.player.origin, landmarkOrigin );
string = compass + ", " + clock + ":00";
Print3d( landmarkOrigin + offset, string, color, 1, 0.75 ); // origin, text, RGB, alpha, scale
wait 0.05;
}
}
resetNextSayTimes( team, action )
{
soldiers = GetAIArray( team );
for ( index = 0; index < soldiers.size; index++ )
{
soldier = soldiers[ index ];
if ( !isAlive( soldier ) )
continue;
if ( !isDefined( soldier.battlechatter ) )
continue;
soldier.nextSayTimes[ action ] = GetTime() + 350;
soldier.squad.nextSayTimes[ action ] = GetTime() + 350;
}
}
voice_is_british_based()
{
self endon( "death" );
if ( self.voice == "british" || self.voice == "spanish" || self.voice == "italian" || self.voice == "german" )
return true;
else
return false;
}
friendlyfire_warning()
{
if ( !self can_say_friendlyfire() )
{
return false;
}
// since we're skipping a lot of the normal bcs checks, multiple guys can potentially say this at the same time, so do the typelimit earlier than usual
self doTypeLimit( "reaction", "friendlyfire" );
self thread playReactionEvent();
return true;
}
can_say_friendlyfire( checkTypeLimit )
{
if ( IsDefined( self.friendlyfire_warnings_disable ) )
{
return false;
}
if ( !IsDefined( self.chatQueue ) )
{
return false;
}
// do we have a reaction event in our queue?
if ( !IsDefined( self.chatQueue[ "reaction" ] ) || !IsDefined( self.chatQueue[ "reaction" ].eventType ) )
{
return false;
}
// is it a friendlyfire reaction?
if ( self.chatQueue[ "reaction" ].eventType != "friendlyfire" )
{
return false;
}
// has it expired?
if ( GetTime() > self.chatQueue[ "reaction" ].expireTime )
{
return false;
}
if ( !IsDefined( checkTypeLimit ) )
{
checkTypeLimit = true;
}
if ( checkTypeLimit )
{
// is it too early to do another one yet?
if ( IsDefined( self.squad.nextTypeSayTimes[ "reaction" ][ "friendlyfire" ] ) )
{
if ( GetTime() < self.squad.nextTypeSayTimes[ "reaction" ][ "friendlyfire" ] )
{
return false;
}
}
}
return true;
}