566 lines
21 KiB
Plaintext
566 lines
21 KiB
Plaintext
#using scripts\shared\ai_shared;
|
|
#using scripts\shared\math_shared;
|
|
#using scripts\shared\spawner_shared;
|
|
#using scripts\shared\ai\archetype_utility;
|
|
|
|
|
|
|
|
|
|
|
|
#namespace Phalanx;
|
|
|
|
function private _AssignPhalanxStance( sentients, stance )
|
|
{
|
|
assert( IsArray( sentients ) );
|
|
|
|
foreach ( index, sentient in sentients )
|
|
{
|
|
if ( IsDefined( sentient) && IsAlive( sentient ) )
|
|
{
|
|
sentient ai::set_behavior_attribute( "phalanx_force_stance", stance );
|
|
}
|
|
}
|
|
}
|
|
|
|
function private _CreatePhalanxTier(
|
|
phalanxType, tier, phalanxPosition, forward, maxTierSize, spawner = undefined )
|
|
{
|
|
sentients = [];
|
|
|
|
if ( !IsSpawner( spawner ) )
|
|
{
|
|
spawner = _GetPhalanxSpawner( tier );
|
|
}
|
|
|
|
positions = _GetPhalanxPositions( phalanxType, tier );
|
|
angles = VectorToAngles( forward );
|
|
|
|
foreach ( index, position in positions )
|
|
{
|
|
if ( index >= maxTierSize )
|
|
{
|
|
break;
|
|
}
|
|
|
|
orientedPos = _RotateVec( position, angles[1] - 90 );
|
|
|
|
navMeshPosition = GetClosestPointOnNavMesh(
|
|
phalanxPosition + orientedPos, 200 );
|
|
|
|
// Make sure the spawner can actually spawn a sentient.
|
|
if ( !( spawner.spawnflags & 64 ) )
|
|
{
|
|
spawner.count++;
|
|
}
|
|
|
|
sentient = spawner spawner::spawn( true, "", navMeshPosition, angles );
|
|
|
|
_InitializeSentient( sentient );
|
|
|
|
// Wait till all sentients have been created.
|
|
{wait(.05);};
|
|
|
|
sentients[ sentients.size ] = sentient;
|
|
}
|
|
|
|
return sentients;
|
|
}
|
|
|
|
// Caps the maximum damage the phalanx takes from a single explosive entity.
|
|
function private _DampenExplosiveDamage(
|
|
inflictor, attacker, damage, flags, meansOfDamage, weapon, point, dir, hitLoc, offsetTime, boneIndex, modelIndex )
|
|
{
|
|
entity = self;
|
|
|
|
isExplosive = IsInArray(
|
|
array(
|
|
"MOD_GRENADE",
|
|
"MOD_GRENADE_SPLASH",
|
|
"MOD_PROJECTILE",
|
|
"MOD_PROJECTILE_SPLASH",
|
|
"MOD_EXPLOSIVE" ),
|
|
meansOfDamage );
|
|
|
|
if ( isExplosive && IsDefined( inflictor ) && IsDefined( inflictor.weapon ) )
|
|
{
|
|
weapon = inflictor.weapon;
|
|
|
|
distanceToEntity = Distance( entity.origin, inflictor.origin );
|
|
|
|
// Linear falloff from grenade distance.
|
|
fractionDistance = 1;
|
|
|
|
if ( weapon.explosionradius > 0 )
|
|
{
|
|
fractionDistance = ( weapon.explosionradius - distanceToEntity ) / weapon.explosionradius;
|
|
}
|
|
|
|
// This causes near exponential damage falloff since the original damage already considers radius at a near linear falloff.
|
|
return Int( Max( damage * fractionDistance, 1 ) );
|
|
}
|
|
|
|
return damage;
|
|
}
|
|
|
|
function private _GetPhalanxPositions( phalanxType, tier )
|
|
{
|
|
switch ( phalanxType )
|
|
{
|
|
case "phanalx_wedge":
|
|
switch( tier )
|
|
{
|
|
case "phalanx_tier1":
|
|
return array( ( 0, 0, 0 ), ( -64, -48, 0 ), ( 64, -48, 0 ), ( -128, -96, 0 ), ( 128, -96, 0 ) );
|
|
case "phalanx_tier2":
|
|
return array( ( -32, -96, 0 ), ( 32, -96, 0 ) );
|
|
case "phalanx_tier3":
|
|
return array( );
|
|
}
|
|
break;
|
|
case "phalanx_reverse_wedge":
|
|
switch( tier )
|
|
{
|
|
case "phalanx_tier1":
|
|
return array( ( -32, 0, 0 ), ( 32, 0, 0 ) );
|
|
case "phalanx_tier2":
|
|
return array( ( 0, -96, 0 ) );
|
|
case "phalanx_tier3":
|
|
return array( );
|
|
}
|
|
break;
|
|
case "phalanx_diagonal_left":
|
|
switch( tier )
|
|
{
|
|
case "phalanx_tier1":
|
|
return array( ( 0, 0, 0 ), ( -48, -64, 0 ), ( -96, -128, 0 ), ( -144, -192, 0 ) );
|
|
case "phalanx_tier2":
|
|
return array( ( 64, 0, 0 ), ( 16, -64, 0 ), ( -48, -128, 0 ), ( -112, -192, 0 ) );
|
|
case "phalanx_tier3":
|
|
return array( );
|
|
}
|
|
break;
|
|
case "phalanx_diagonal_right":
|
|
switch( tier )
|
|
{
|
|
case "phalanx_tier1":
|
|
return array( ( 0, 0, 0 ), ( 48, -64, 0 ), ( 96, -128, 0 ), ( 144, -192, 0 ) );
|
|
case "phalanx_tier2":
|
|
return array( ( -64, 0, 0 ), ( -16, -64, 0 ), ( 48, -128, 0 ), ( 112, -192, 0 ) );
|
|
case "phalanx_tier3":
|
|
return array( );
|
|
}
|
|
break;
|
|
case "phalanx_forward":
|
|
switch( tier )
|
|
{
|
|
case "phalanx_tier1":
|
|
return array( ( 0, 0, 0 ), ( 64, 0, 0 ), ( 128, 0, 0 ), ( 192, 0, 0 ) );
|
|
case "phalanx_tier2":
|
|
return array( ( -32, -64, 0 ), ( 32, -64, 0 ), ( 96, -64, 0 ), ( 160, -64, 0 ) );
|
|
case "phalanx_tier3":
|
|
return array( );
|
|
}
|
|
break;
|
|
case "phalanx_column":
|
|
switch( tier )
|
|
{
|
|
case "phalanx_tier1":
|
|
return array( ( 0, 0, 0 ), ( -64, 0, 0 ), ( 0, -64, 0 ), ( -64, -64, 0 ) );
|
|
case "phalanx_tier2":
|
|
return array( ( 0, -128, 0 ), ( -64, -128, 0 ), ( 0, -192, 0 ), ( -64, -192, 0 ) );
|
|
case "phalanx_tier3":
|
|
return array( );
|
|
}
|
|
break;
|
|
case "phalanx_column_right":
|
|
switch( tier )
|
|
{
|
|
case "phalanx_tier1":
|
|
return array( ( 0, 0, 0 ), ( 0, -64, 0 ), ( 0, -128, 0 ), ( 0, -192, 0 ) );
|
|
case "phalanx_tier2":
|
|
return array( );
|
|
case "phalanx_tier3":
|
|
return array( );
|
|
}
|
|
break;
|
|
default:
|
|
assert( "Unknown phalanx type \"" + phalanxType + "\"." );
|
|
}
|
|
|
|
assert( "Unknown phalanx tier \"" + tier + "\"." );
|
|
}
|
|
|
|
function private _GetPhalanxSpawner( tier )
|
|
{
|
|
spawner = GetSpawnerArray( tier, "targetname" );
|
|
|
|
assert( spawner.size >= 0,
|
|
"No spawners for the phalanx system were found, make sure you include " +
|
|
"the \"game/map_source/_prefabs/ai/robot_phalanx.map\" prefab within your " +
|
|
"map to use the system." );
|
|
assert( spawner.size == 1,
|
|
"Too many spawners for the phalanx system were found, make sure you " +
|
|
"don't include multiple copies of the " +
|
|
"\"game/map_source/_prefabs/ai/robot_phalanx.map\" prefab in your map." );
|
|
|
|
return spawner[0];
|
|
}
|
|
|
|
function private _HaltAdvance( sentients )
|
|
{
|
|
assert( IsArray( sentients ) );
|
|
|
|
foreach ( index, sentient in sentients )
|
|
{
|
|
if ( IsDefined( sentient ) && IsAlive( sentient ) && sentient HasPath() )
|
|
{
|
|
navMeshPosition = GetClosestPointOnNavMesh(
|
|
sentient.origin, 200 );
|
|
|
|
sentient UsePosition( navMeshPosition );
|
|
sentient ClearPath();
|
|
}
|
|
}
|
|
}
|
|
|
|
function private _HaltFire( sentients )
|
|
{
|
|
assert( IsArray( sentients ) );
|
|
|
|
foreach ( index, sentient in sentients )
|
|
{
|
|
if ( IsDefined( sentient ) && IsAlive( sentient ) )
|
|
{
|
|
sentient.ignoreall = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function _InitializeSentient( sentient )
|
|
{
|
|
assert( IsActor( sentient ) );
|
|
|
|
sentient ai::set_behavior_attribute( "phalanx", true );
|
|
if ( sentient.archetype === "human" )
|
|
{
|
|
sentient.allowPain = false;
|
|
}
|
|
sentient SetAvoidanceMask( "avoid none" );
|
|
|
|
if ( (isdefined(sentient.archetype) && ( sentient.archetype == "robot" )) )
|
|
{
|
|
sentient ai::set_behavior_attribute( "move_mode", "marching" );
|
|
sentient ai::set_behavior_attribute( "force_cover", true );
|
|
}
|
|
|
|
AiUtility::AddAIOverrideDamageCallback( sentient, &_DampenExplosiveDamage, true );
|
|
}
|
|
|
|
function private _MovePhalanxTier( sentients, phalanxType, tier, destination, forward )
|
|
{
|
|
positions = _GetPhalanxPositions( phalanxType, tier );
|
|
angles = VectorToAngles( forward );
|
|
|
|
assert( sentients.size <= positions.size,
|
|
"There must be enough positions for the phalanx tier to move to." );
|
|
|
|
foreach ( index, sentient in sentients )
|
|
{
|
|
if ( IsDefined( sentient ) && IsAlive( sentient ) )
|
|
{
|
|
assert( IsVec( positions[ index ] ),
|
|
"Must have a formation position for position(" + index + ") in tier " +
|
|
tier + " of formation " + phalanxType );
|
|
|
|
orientedPos = _RotateVec( positions[ index ], angles[1] - 90 );
|
|
|
|
navMeshPosition = GetClosestPointOnNavMesh(
|
|
destination + orientedPos, 200 );
|
|
|
|
sentient UsePosition( navMeshPosition );
|
|
}
|
|
}
|
|
}
|
|
|
|
function private _PruneDead( sentients )
|
|
{
|
|
liveSentients = [];
|
|
|
|
// Removes dead sentients and keeps living ones with the same array index.
|
|
foreach ( index, sentient in sentients )
|
|
{
|
|
if ( IsDefined( sentient ) && IsAlive( sentient ) )
|
|
{
|
|
liveSentients[ index ] = sentient;
|
|
}
|
|
}
|
|
|
|
return liveSentients;
|
|
}
|
|
|
|
function private _ReleaseSentient( sentient )
|
|
{
|
|
if ( IsDefined( sentient ) && IsAlive( sentient ) )
|
|
{
|
|
sentient ClearUsePosition();
|
|
sentient PathMode( "move delayed", true, RandomFloatRange( 0.5, 1 ) );
|
|
|
|
sentient ai::set_behavior_attribute( "phalanx", false );
|
|
|
|
// Wait a frame to make sure sentients's are released from the phalanx.
|
|
{wait(.05);};
|
|
|
|
if ( sentient.archetype === "human" )
|
|
{
|
|
sentient.allowPain = true;
|
|
}
|
|
sentient SetAvoidanceMask( "avoid all" );
|
|
|
|
AiUtility::RemoveAIOverrideDamageCallback( sentient, &_DampenExplosiveDamage );
|
|
|
|
if ( (isdefined(sentient.archetype) && ( sentient.archetype == "robot" )) )
|
|
{
|
|
sentient ai::set_behavior_attribute( "move_mode", "normal" );
|
|
sentient ai::set_behavior_attribute( "force_cover", false );
|
|
}
|
|
}
|
|
}
|
|
|
|
function private _ReleaseSentients( sentients )
|
|
{
|
|
foreach ( index, sentient in sentients )
|
|
{
|
|
_ResumeFire( sentient );
|
|
_ReleaseSentient( sentient );
|
|
|
|
// Release sentients slowly from their group.
|
|
wait RandomFloatRange( 0.5, 5 );
|
|
}
|
|
}
|
|
|
|
function private _ResumeFire( sentient )
|
|
{
|
|
if ( IsDefined( sentient ) && IsAlive( sentient ) )
|
|
{
|
|
sentient.ignoreall = false;
|
|
}
|
|
}
|
|
|
|
function private _ResumeFireSentients( sentients )
|
|
{
|
|
assert( IsArray( sentients ) );
|
|
|
|
foreach ( index, sentient in sentients )
|
|
{
|
|
_ResumeFire( sentient );
|
|
}
|
|
}
|
|
|
|
function private _RotateVec( vector, angle )
|
|
{
|
|
return ( vector[0] * Cos( angle ) - vector[1] * Sin( angle ),
|
|
vector[0] * Sin( angle ) + vector[1] * Cos( angle ),
|
|
vector[2] );
|
|
}
|
|
|
|
function private _UpdatePhalanxThread( phalanx )
|
|
{
|
|
while ( [[ phalanx ]]->_UpdatePhalanx() )
|
|
{
|
|
wait 1;
|
|
}
|
|
}
|
|
|
|
class Phalanx
|
|
{
|
|
// All tiers of sentients.
|
|
var sentientTiers_;
|
|
|
|
// Total starting sentients in the formation.
|
|
var startSentientCount_;
|
|
// Current count of all sentients in the formation.
|
|
var currentSentientCount_;
|
|
|
|
// Number of sentients that must die for the formation to scatter.
|
|
var breakingPoint_;
|
|
|
|
// Phalanx move positions
|
|
var startPosition_;
|
|
var endPosition_;
|
|
|
|
// Phalanx type
|
|
var phalanxType_;
|
|
|
|
// Mark whether the formation has already been scattered.
|
|
var scattered_;
|
|
|
|
constructor()
|
|
{
|
|
sentientTiers_ = [];
|
|
|
|
startSentientCount_ = 0;
|
|
currentSentientCount_ = 0;
|
|
breakingPoint_ = 0;
|
|
|
|
scattered_ = false;
|
|
}
|
|
|
|
destructor()
|
|
{
|
|
}
|
|
|
|
function private _UpdatePhalanx()
|
|
{
|
|
if ( scattered_ )
|
|
{
|
|
// Terminate the phalanx if someone else has already scattered the formation.
|
|
return false;
|
|
}
|
|
|
|
currentSentientCount_ = 0;
|
|
|
|
// Discard dead sentients from the phalanx.
|
|
foreach ( name, tier in sentientTiers_ )
|
|
{
|
|
sentientTiers_[ name ] = Phalanx::_PruneDead( tier );
|
|
|
|
currentSentientCount_ += sentientTiers_[ name ].size;
|
|
}
|
|
|
|
// Break up the phalanx if enough sentients died.
|
|
if ( currentSentientCount_ <= ( startSentientCount_ - breakingPoint_ ) )
|
|
{
|
|
ScatterPhalanx();
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function HaltFire()
|
|
{
|
|
foreach ( name, tier in sentientTiers_ )
|
|
{
|
|
Phalanx::_HaltFire( tier );
|
|
}
|
|
}
|
|
|
|
function HaltAdvance()
|
|
{
|
|
if ( !scattered_ )
|
|
{
|
|
foreach ( name, tier in sentientTiers_ )
|
|
{
|
|
Phalanx::_HaltAdvance( tier );
|
|
}
|
|
}
|
|
}
|
|
|
|
function Initialize(
|
|
phalanxType,
|
|
origin,
|
|
destination,
|
|
breakingPoint,
|
|
maxTierSize = 10,
|
|
tierOneSpawner = undefined,
|
|
tierTwoSpawner = undefined,
|
|
tierThreeSpawner = undefined )
|
|
{
|
|
assert( IsString( phalanxType ) );
|
|
assert( IsInt( breakingPoint ) );
|
|
assert( IsVec( origin ) );
|
|
assert( IsVec( destination ) );
|
|
|
|
tierSpawners = [];
|
|
tierSpawners[ "phalanx_tier1" ] = tierOneSpawner;
|
|
tierSpawners[ "phalanx_tier2" ] = tierTwoSpawner;
|
|
tierSpawners[ "phalanx_tier3" ] = tierThreeSpawner;
|
|
|
|
maxTierSize = math::clamp( maxTierSize, 1, 10 );
|
|
|
|
forward = VectorNormalize( destination - origin );
|
|
|
|
foreach ( tierName in array( "phalanx_tier1", "phalanx_tier2", "phalanx_tier3" ) )
|
|
{
|
|
sentientTiers_[ tierName ] = Phalanx::_CreatePhalanxTier(
|
|
phalanxType, tierName, origin, forward, maxTierSize, tierSpawners[ tierName ] );
|
|
|
|
startSentientCount_ += sentientTiers_[ tierName ].size;
|
|
}
|
|
|
|
// The first tier facing the enemy always crouches.
|
|
Phalanx::_AssignPhalanxStance( sentientTiers_[ "phalanx_tier1" ], "crouch" );
|
|
|
|
// Assign phalanx positions to all sentient tiers.
|
|
foreach ( name, tier in sentientTiers_ )
|
|
{
|
|
Phalanx::_MovePhalanxTier(
|
|
sentientTiers_[ name ], phalanxType, name, destination, forward );
|
|
}
|
|
|
|
breakingPoint_ = breakingPoint;
|
|
startPosition_ = origin;
|
|
endPosition_ = destination;
|
|
phalanxType_ = phalanxType;
|
|
|
|
// Initiate the main update loop, a single thread that updates the phalanx.
|
|
self thread Phalanx::_UpdatePhalanxThread( self );
|
|
}
|
|
|
|
function ResumeAdvance()
|
|
{
|
|
if ( !scattered_ )
|
|
{
|
|
Phalanx::_AssignPhalanxStance( sentientTiers_[ "phalanx_tier1" ], "stand" );
|
|
|
|
// TODO(David Young 10-21-14): Too hardcoded, waiting for animation to complete.
|
|
wait 1;
|
|
|
|
forward = VectorNormalize( endPosition_ - startPosition_ );
|
|
|
|
Phalanx::_MovePhalanxTier(
|
|
sentientTiers_[ "phalanx_tier1" ], phalanxType_, "phalanx_tier1", endPosition_, forward );
|
|
Phalanx::_MovePhalanxTier(
|
|
sentientTiers_[ "phalanx_tier2" ], phalanxType_, "phalanx_tier2", endPosition_, forward );
|
|
Phalanx::_MovePhalanxTier(
|
|
sentientTiers_[ "phalanx_tier3" ], phalanxType_, "phalanx_tier3", endPosition_, forward );
|
|
|
|
Phalanx::_AssignPhalanxStance( sentientTiers_[ "phalanx_tier1" ], "crouch" );
|
|
}
|
|
}
|
|
|
|
function ResumeFire()
|
|
{
|
|
Phalanx::_ResumeFireSentients( sentientTiers_[ "phalanx_tier1" ] );
|
|
Phalanx::_ResumeFireSentients( sentientTiers_[ "phalanx_tier2" ] );
|
|
Phalanx::_ResumeFireSentients( sentientTiers_[ "phalanx_tier3" ] );
|
|
}
|
|
|
|
function ScatterPhalanx()
|
|
{
|
|
if ( !scattered_ )
|
|
{
|
|
scattered_ = true;
|
|
|
|
Phalanx::_ReleaseSentients( sentientTiers_[ "phalanx_tier1" ] );
|
|
sentientTiers_[ "phalanx_tier1" ] = [];
|
|
|
|
Phalanx::_AssignPhalanxStance( sentientTiers_[ "phalanx_tier2" ], "crouch" );
|
|
|
|
// Settling time for the tier.
|
|
wait RandomFloatRange( 5, 7 );
|
|
|
|
Phalanx::_ReleaseSentients( sentientTiers_[ "phalanx_tier2" ] );
|
|
sentientTiers_[ "phalanx_tier2" ] = [];
|
|
|
|
Phalanx::_AssignPhalanxStance( sentientTiers_[ "phalanx_tier3" ], "crouch" );
|
|
|
|
// Settling time for the tier.
|
|
wait RandomFloatRange( 5, 7 );
|
|
|
|
Phalanx::_ReleaseSentients( sentientTiers_[ "phalanx_tier3" ] );
|
|
sentientTiers_[ "phalanx_tier3" ] = [];
|
|
}
|
|
}
|
|
} |