/**************************************************************************** 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; }