1734 lines
48 KiB
Plaintext
1734 lines
48 KiB
Plaintext
#include animscripts\Utility;
|
|
#include animscripts\SetPoseMovement;
|
|
#include animscripts\Combat_Utility;
|
|
#include common_scripts\Utility;
|
|
#include maps\_utility;
|
|
#using_animtree( "generic_human" );
|
|
|
|
// ===========================================================
|
|
// AI vs Player melee
|
|
// ===========================================================
|
|
|
|
sqr8 = 8 * 8;
|
|
sqr16 = 16 * 16;
|
|
sqr32 = 32 * 32;
|
|
sqr36 = 36 * 36;
|
|
sqr64 = 64 * 64;
|
|
|
|
MELEE_RANGE = 64;
|
|
MELEE_RANGE_SQ = sqr64;
|
|
MELEE_ACTOR_BOUNDS_RADIUS = 32; // a little bigger than twice the radius of an actor's bounding box
|
|
MELEE_ACTOR_BOUNDS_RADIUS_MINUS_EPSILON = (MELEE_ACTOR_BOUNDS_RADIUS-0.1); // used for asserts
|
|
|
|
CHARGE_RANGE_SQ = 160 * 160;
|
|
CHARGE_RANGE_SQ_VS_PLAYER = 200 * 200;
|
|
|
|
FAILED_INIT_NEXT_MELEE_TIME = 150; // basic IsValid() falure
|
|
FAILED_CHARGE_NEXT_MELEE_TIME = 1500; // charge failures (both standard/aiVSai)
|
|
FAILED_STANDARD_NEXT_MELEE_TIME = 2500; // standard melee failure
|
|
|
|
NOTETRACK_SYNC = "sync";
|
|
NOTETRACK_UNSYNC = "unsync";
|
|
NOTETRACK_ATTACHKNIFE = "attach_knife";
|
|
NOTETRACK_DETACTKNIFE = "detach_knife";
|
|
NOTETRACK_STAB = "stab";
|
|
NOTETRACK_DEATH = "melee_death";
|
|
NOTETRACK_INTERACT = "melee_interact";
|
|
|
|
KNIFE_ATTACK_MODEL = "weapon_parabolic_knife";
|
|
KNIFE_ATTACK_TAG = "TAG_INHAND";
|
|
KNIFE_ATTACK_SOUND = "melee_knife_hit_body";
|
|
KNIFE_ATTACK_FX_NAME = "melee_knife_ai";
|
|
KNIFE_ATTACK_FX_PATH = "impacts/flesh_hit_knife";
|
|
KNIFE_ATTACK_FX_TAG = "TAG_KNIFE_FX";
|
|
|
|
|
|
Melee_Init()
|
|
{
|
|
precacheModel( KNIFE_ATTACK_MODEL );
|
|
level._effect[ KNIFE_ATTACK_FX_NAME ] = loadfx( KNIFE_ATTACK_FX_PATH );
|
|
}
|
|
|
|
Melee_StealthCheck()
|
|
{
|
|
if ( !isdefined( self._stealth ) )
|
|
return false;
|
|
|
|
if ( isdefined( self.ent_flag ) && isdefined( self.ent_flag[ "_stealth_enabled" ] ) && self.ent_flag[ "_stealth_enabled" ] )
|
|
if ( isdefined( self.ent_flag[ "_stealth_attack" ] ) && !self.ent_flag[ "_stealth_attack" ] )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
Melee_TryExecuting()
|
|
{
|
|
// Must have a valid enemy before we try anything
|
|
if ( !isDefined( self.enemy ) )
|
|
return false;
|
|
|
|
if ( isdefined( self.dontmelee ) )
|
|
return false;
|
|
|
|
if ( Melee_StealthCheck() )
|
|
return false;
|
|
|
|
if ( !Melee_AcquireMutex( self.enemy ) )
|
|
return false;
|
|
|
|
Melee_ResetAction();
|
|
if ( !Melee_ChooseAction() )
|
|
{
|
|
Melee_ReleaseMutex( self.enemy );
|
|
return false;
|
|
}
|
|
|
|
self animcustom( ::Melee_MainLoop, ::Melee_EndScript );
|
|
}
|
|
|
|
|
|
// Setup internal melee structure for sanity/cache tracking
|
|
Melee_ResetAction()
|
|
{
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.enemy.melee ) );
|
|
|
|
self.melee.target = self.enemy;
|
|
self.melee.initiated = false;
|
|
self.melee.inProgress = false;
|
|
}
|
|
|
|
|
|
// After succesfully checking for melee, initialize our move
|
|
Melee_ChooseAction()
|
|
{
|
|
if ( !Melee_IsValid() )
|
|
return false;
|
|
|
|
self.melee.initiated = true;
|
|
|
|
if ( Melee_AIvsAI_ChooseAction() )
|
|
{
|
|
self.melee.func = ::Melee_AIvsAI_Main;
|
|
return true;
|
|
}
|
|
|
|
if ( Melee_Standard_ChooseAction() )
|
|
{
|
|
if ( isdefined( self.specialMelee_Standard ) )
|
|
self.melee.func = self.specialMelee_Standard;
|
|
else
|
|
self.melee.func = ::Melee_Standard_Main;
|
|
return true;
|
|
}
|
|
|
|
// Don't try again for a while since we can't start
|
|
self.melee.func = undefined;
|
|
self.nextMeleeCheckTime = gettime() + FAILED_INIT_NEXT_MELEE_TIME;
|
|
self.nextMeleeCheckTarget = self.melee.target;
|
|
return false;
|
|
}
|
|
|
|
|
|
Melee_UpdateAndValidateStartPos()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.startPos ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
|
|
ignoreActors = true;
|
|
|
|
// If the target is too close from the start pos, move the start pos in a way that our traces will succeed.
|
|
distFromTarget2d = distance2d( self.melee.startPos, self.melee.target.origin );
|
|
if ( distFromTarget2d < MELEE_ACTOR_BOUNDS_RADIUS )
|
|
{
|
|
// Calculate the direction from the target to the start pos, and push that start pos a bit
|
|
dirToStartPos2d = vectorNormalize( (self.melee.startPos[0] - self.melee.target.origin[0], self.melee.startPos[1] - self.melee.target.origin[1], 0) );
|
|
self.melee.startPos += dirToStartPos2d * (MELEE_ACTOR_BOUNDS_RADIUS - distFromTarget2d);
|
|
assertex( distance2d( self.melee.startPos, self.melee.target.origin ) >= (MELEE_ACTOR_BOUNDS_RADIUS_MINUS_EPSILON), "Invalid distance to target: " + distance2d( self.melee.startPos, self.melee.target.origin ) + ", should be more than " + (MELEE_ACTOR_BOUNDS_RADIUS_MINUS_EPSILON) );
|
|
ignoreActors = false;
|
|
}
|
|
|
|
// Height-based checks
|
|
floorPos = self getDropToFloorPosition( self.melee.startPos );
|
|
if ( !isDefined( floorPos ) )
|
|
return false;
|
|
|
|
// Point is so far from the ground that we can't reach it, fail
|
|
if ( abs( self.melee.startPos[2] - floorPos[2] ) > (MELEE_RANGE * 0.80) )
|
|
return false;
|
|
|
|
// Point is on another floor / platform, can't get that high
|
|
if ( abs( self.origin[2] - floorPos[2] ) > (MELEE_RANGE * 0.80) )
|
|
return false;
|
|
|
|
// If the point is fine, update its value
|
|
self.melee.startPos = floorPos;
|
|
assertex( distance2d( self.melee.startPos, self.melee.target.origin ) >= (MELEE_ACTOR_BOUNDS_RADIUS_MINUS_EPSILON), "Invalid distance to target: " + distance2d( self.melee.startPos, self.melee.target.origin ) + ", should be more than " + (MELEE_ACTOR_BOUNDS_RADIUS_MINUS_EPSILON) );
|
|
|
|
// Now check whether movement is possible to that point
|
|
|
|
// First check to see if we can reach our start pos
|
|
if ( !self mayMoveToPoint( self.melee.startPos, true, ignoreActors ) )
|
|
return false;
|
|
|
|
// Compute a point that's just outside of the target's bounds. Do a first trace to that point which doesn't
|
|
// ignore actors, and then a second trace which does
|
|
|
|
// if we're going around a corner, the two traces will pick a point to form a 90 angle.
|
|
// otherwise we pick a point right outside of the target's box
|
|
if ( isDefined( self.melee.startToTargetCornerAngles ) )
|
|
{
|
|
// first find the corner based on the angles
|
|
targetToStartPos = self.melee.startPos - self.melee.target.origin;
|
|
cornerDir = anglesToForward( self.melee.startToTargetCornerAngles );
|
|
cornerDirLen = vectorDot( cornerDir, targetToStartPos );
|
|
mayMoveTargetOrigin = self.melee.startPos - (cornerDir * cornerDirLen);
|
|
|
|
// push it out a bit if it's too close to the target
|
|
cornerToTarget = self.melee.target.origin - mayMoveTargetOrigin;
|
|
cornerToTargetLen = distance2d( self.melee.target.origin, mayMoveTargetOrigin );
|
|
if ( cornerToTargetLen < MELEE_ACTOR_BOUNDS_RADIUS )
|
|
mayMoveTargetOrigin -= cornerToTarget * ((MELEE_ACTOR_BOUNDS_RADIUS-cornerToTargetLen)/MELEE_ACTOR_BOUNDS_RADIUS);
|
|
}
|
|
else
|
|
{
|
|
dirToStartPos2d = vectorNormalize( (self.melee.startPos[0] - self.melee.target.origin[0], self.melee.startPos[1] - self.melee.target.origin[1], 0) );
|
|
mayMoveTargetOrigin = self.melee.target.origin + dirToStartPos2d * MELEE_ACTOR_BOUNDS_RADIUS;
|
|
}
|
|
|
|
assert( isDefined( mayMoveTargetOrigin ) );
|
|
|
|
if ( !self mayMoveFromPointToPoint( self.melee.startPos, mayMoveTargetOrigin, true, false ) )
|
|
return false;
|
|
|
|
if ( !self mayMoveFromPointToPoint( mayMoveTargetOrigin, self.melee.target.origin, true, true ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Checks for various self / target conditions. Does not check for pathing issues.
|
|
Melee_IsValid()
|
|
{
|
|
// Must have a target still
|
|
if ( !isDefined( self.melee.target ) )
|
|
return false;
|
|
|
|
target = self.melee.target;
|
|
|
|
if ( isdefined( target.dontMelee ) )
|
|
return false;
|
|
|
|
// Distance check should usually fail
|
|
enemyDistanceSq = distanceSquared( self.origin, target.origin );
|
|
|
|
if ( isdefined( self.meleeChargeDistSq ) )
|
|
chargeDistSq = self.meleeChargeDistSq;
|
|
else if ( isplayer( target ) )
|
|
chargeDistSq = CHARGE_RANGE_SQ_VS_PLAYER;
|
|
else
|
|
chargeDistSq = CHARGE_RANGE_SQ;
|
|
|
|
// Enemy isn't even close enough to initiate
|
|
if ( !self.melee.initiated && (enemyDistanceSq > chargeDistSq) )
|
|
return false;
|
|
|
|
//
|
|
// Self Checks
|
|
//
|
|
|
|
// Don't charge if we're about to die
|
|
if ( !isAlive( self ) )
|
|
return false;
|
|
|
|
// Don't melee on the first frame ...
|
|
if ( isDefined( self.a.noFirstFrameMelee ) && (self.a.scriptStartTime >= gettime() + 50) )
|
|
return false;
|
|
|
|
// Prevent doing checks too often on the same target
|
|
if ( isDefined( self.nextMeleeCheckTime ) && isDefined( self.nextMeleeCheckTarget ) && (gettime() < self.nextMeleeCheckTime) && ( self.nextMeleeCheckTarget == target ) )
|
|
return false;
|
|
|
|
// Can't melee if we're not standing or crouching
|
|
if ( isdefined( self.a.onback ) || (self.a.pose == "prone") )
|
|
return false;
|
|
|
|
// can't melee while sidearm is out. need animations for this.
|
|
// we rely on main loop to put away sidearm if necessary.
|
|
if ( usingSidearm() )
|
|
return false;
|
|
|
|
// don't melee charge with a grenade in range, unless you have a shield
|
|
if ( isDefined( self.grenade ) && ( self.frontShieldAngleCos == 1 ) )
|
|
return false;
|
|
|
|
//
|
|
// Enemy checks
|
|
//
|
|
|
|
if ( !isAlive( target ) )
|
|
return false;
|
|
|
|
// no melee on enemies that are flagged as such
|
|
if ( isDefined( target.dontAttackMe ) || (isDefined( target.ignoreMe ) && target.ignoreMe) )
|
|
return false;
|
|
|
|
// no meleeing virtual targets
|
|
if ( !isAI( target ) && !isPlayer( target ) )
|
|
return false;
|
|
|
|
if ( isAI( target ) )
|
|
{
|
|
// special state, can't allow meleeing
|
|
if ( target isInScriptedState() )
|
|
return false;
|
|
|
|
// crawling/dying
|
|
if ( target doingLongDeath() || target.delayedDeath )
|
|
return false;
|
|
}
|
|
|
|
// Check if our enemy is in a proper pose to get melee'd
|
|
if ( isPlayer( target ) )
|
|
enemyPose = target getStance();
|
|
else
|
|
enemyPose = target.a.pose;
|
|
|
|
if ( (enemyPose != "stand") && (enemyPose != "crouch") )
|
|
return false;
|
|
|
|
// Disable melee completely when both targets are invulnerable
|
|
if ( isDefined( self.magic_bullet_shield ) && isDefined( target.magic_bullet_shield ) )
|
|
return false;
|
|
|
|
// don't melee charge with a grenade in range of the enemy
|
|
if ( isDefined( target.grenade ) )
|
|
return false;
|
|
|
|
//
|
|
// Position Checks
|
|
//
|
|
|
|
// Have extra tolerance when already in progress, since some animations twist the origin quite a bit ( for example standard melee )
|
|
if ( self.melee.inProgress )
|
|
yawThreshold = 110;
|
|
else
|
|
yawThreshold = 60;
|
|
|
|
yawToEnemy = AngleClamp180( self.angles[ 1 ] - GetYaw( target.origin ) );
|
|
if ( abs( yawToEnemy ) > yawThreshold )
|
|
return false;
|
|
|
|
// Enemy is already close enough to melee.
|
|
if ( enemyDistanceSq <= MELEE_RANGE_SQ )
|
|
return true;
|
|
|
|
// if we already started, but no longer in melee range, fail/abort
|
|
if ( self.melee.inProgress )
|
|
return false;
|
|
|
|
// we can't melee from our position and need to charge, but failed a charge recently on the same target ; fail
|
|
if ( isDefined( self.nextMeleeChargeTime ) && isDefined( self.nextMeleeChargeTarget ) && (gettime() < self.nextMeleeChargeTime) && (self.nextMeleeChargeTarget == target) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
Melee_StartMovement()
|
|
{
|
|
self.melee.playingMovementAnim = true;
|
|
self.a.movement = "run";
|
|
}
|
|
|
|
Melee_StopMovement()
|
|
{
|
|
self clearanim( %body, 0.2 );
|
|
self.melee.playingMovementAnim = undefined;
|
|
self.a.movement = "stop";
|
|
self orientMode( "face default" );
|
|
}
|
|
|
|
|
|
Melee_MainLoop()
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "end_melee" );
|
|
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.func ) );
|
|
|
|
while( true )
|
|
{
|
|
prevFunc = self.melee.func;
|
|
|
|
[[ self.melee.func ]]();
|
|
|
|
// No more melee actions available, or no new ones, finish
|
|
if ( !isDefined( self.melee.func ) || (prevFunc == self.melee.func) )
|
|
break;
|
|
}
|
|
}
|
|
|
|
Melee_Standard_DelayStandardCharge( target )
|
|
{
|
|
if ( !isDefined ( target ) )
|
|
return;
|
|
|
|
self.nextMeleeStandardChargeTime = getTime() + FAILED_STANDARD_NEXT_MELEE_TIME;
|
|
self.nextMeleeStandardChargeTarget = target;
|
|
}
|
|
|
|
Melee_Standard_CheckTimeConstraints()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
|
|
// out of range and too early to do a standard melee
|
|
targetDistSq = distanceSquared( self.melee.target.origin, self.origin );
|
|
if ( (targetDistSq > MELEE_RANGE_SQ) && isDefined( self.nextMeleeStandardChargeTime ) && isDefined( self.nextMeleeStandardChargeTarget ) && (getTime() < self.nextMeleeStandardChargeTime) && (self.nextMeleeStandardChargeTarget == self.melee.target) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
Melee_Standard_ChooseAction()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
|
|
if ( isDefined( self.melee.target.magic_bullet_shield ) )
|
|
return false;
|
|
|
|
if ( !Melee_Standard_CheckTimeConstraints() )
|
|
return false;
|
|
|
|
if ( isdefined( self.melee.target.specialMeleeChooseAction ) )
|
|
return false;
|
|
|
|
return Melee_Standard_UpdateAndValidateTarget();
|
|
}
|
|
|
|
Melee_Standard_ResetGiveUpTime()
|
|
{
|
|
if ( isdefined( self.meleeChargeDistSq ) )
|
|
chargeDistSq = self.meleeChargeDistSq;
|
|
else if ( isplayer( self.melee.target ) )
|
|
chargeDistSq = CHARGE_RANGE_SQ_VS_PLAYER;
|
|
else
|
|
chargeDistSq = CHARGE_RANGE_SQ;
|
|
|
|
if ( distanceSquared( self.origin, self.melee.target.origin ) > chargeDistSq )
|
|
self.melee.giveUpTime = gettime() + 3000;
|
|
else
|
|
self.melee.giveUpTime = gettime() + 1000;
|
|
}
|
|
|
|
Melee_Standard_Main()
|
|
{
|
|
self animMode( "zonly_physics" );
|
|
|
|
Melee_Standard_ResetGiveUpTime();
|
|
|
|
while ( true )
|
|
{
|
|
assert( isdefined( self.melee.target ) );
|
|
|
|
// first, charge forward if we need to; get into place to play the melee animation
|
|
if ( !Melee_Standard_GetInPosition() )
|
|
{
|
|
// if we couldn't get in place to melee, don't try to charge for a little while and abort
|
|
self.nextMeleeChargeTime = getTime() + FAILED_CHARGE_NEXT_MELEE_TIME;
|
|
self.nextMeleeChargeTarget = self.melee.target;
|
|
break;
|
|
}
|
|
|
|
if ( !isdefined( self.melee.target ) )
|
|
break;
|
|
|
|
assert( (self.a.pose == "stand") || (self.a.pose == "crouch") );
|
|
|
|
self animscripts\battleChatter_ai::evaluateMeleeEvent();
|
|
|
|
self orientMode( "face point", self.melee.target.origin );
|
|
self setflaggedanimknoballrestart( "meleeanim", %melee_1, %body, 1, .2, 1 );
|
|
self.melee.inProgress = true;
|
|
|
|
// If the attack loop returns false, we need to stop this melee
|
|
if( !Melee_Standard_PlayAttackLoop() )
|
|
{
|
|
// Since getting here means that we've done a melee but our attack is no longer valid, delay before we can do a standard attack again.
|
|
Melee_Standard_DelayStandardCharge( self.melee.target );
|
|
break;
|
|
}
|
|
}
|
|
|
|
self animMode( "none" );
|
|
}
|
|
|
|
|
|
Melee_Standard_PlayAttackLoop()
|
|
{
|
|
while ( true )
|
|
{
|
|
self waittill( "meleeanim", note );
|
|
|
|
if ( note == "end" )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( note == "stop" )
|
|
{
|
|
// check if it's worth continuing with another melee.
|
|
// and see if we could so something better , or continue with our attacks
|
|
if ( !Melee_ChooseAction() )
|
|
return false;
|
|
|
|
// Return whether the action we choose is the same as this one, in which case we'll simply continue.
|
|
assert( isDefined( self.melee.func ) );
|
|
if ( self.melee.func != ::Melee_Standard_Main )
|
|
return true;
|
|
}
|
|
|
|
if ( note == "fire" )
|
|
{
|
|
if ( isdefined( self.melee.target ) )
|
|
{
|
|
oldhealth = self.melee.target.health;
|
|
self melee();
|
|
if ( isDefined( self.melee.target ) && (self.melee.target.health < oldhealth) )
|
|
Melee_Standard_ResetGiveUpTime();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// this will update our target position based on our target
|
|
Melee_Standard_UpdateAndValidateTarget()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
if ( !isDefined( self.melee.target ) )
|
|
return false;
|
|
|
|
if ( !Melee_IsValid() )
|
|
return false;
|
|
|
|
dirToTarget = vectorNormalize( self.melee.target.origin - self.origin );
|
|
self.melee.startPos = self.melee.target.origin - 40.0 * dirToTarget;
|
|
|
|
return Melee_UpdateAndValidateStartPos();
|
|
}
|
|
|
|
distance2dSquared( a, b ) // should be moved to code
|
|
{
|
|
diff = (a[0] - b[0], a[1] - b[1], 0 );
|
|
return lengthSquared( diff );
|
|
}
|
|
|
|
// this function makes the guy run towards his enemy, and start raising his gun if he's close enough to melee.
|
|
// it will return false if he gives up, or true if he's ready to start a melee animation.
|
|
Melee_Standard_GetInPosition()
|
|
{
|
|
if ( !Melee_Standard_UpdateAndValidateTarget() )
|
|
return false;
|
|
|
|
enemyDistanceSq = distance2dSquared( self.origin, self.melee.target.origin );
|
|
|
|
if ( enemyDistanceSq <= MELEE_RANGE_SQ )
|
|
{
|
|
// just play a melee-from-standing transition
|
|
self SetFlaggedAnimKnobAll( "readyanim", %stand_2_melee_1, %body, 1, .3, 1 );
|
|
self animscripts\shared::DoNoteTracks( "readyanim" );
|
|
return true;
|
|
}
|
|
|
|
self Melee_PlayChargeSound();
|
|
|
|
prevEnemyPos = self.melee.target.origin;
|
|
|
|
sampleTime = 0.1;
|
|
|
|
raiseGunAnimTravelDist = length( getmovedelta( %run_2_melee_charge, 0, 1 ) );
|
|
meleeAnimTravelDist = 32;
|
|
shouldRaiseGunDist = MELEE_RANGE * 0.75 + meleeAnimTravelDist + raiseGunAnimTravelDist;
|
|
shouldRaiseGunDistSq = shouldRaiseGunDist * shouldRaiseGunDist;
|
|
|
|
shouldMeleeDist = MELEE_RANGE + meleeAnimTravelDist;
|
|
shouldMeleeDistSq = shouldMeleeDist * shouldMeleeDist;
|
|
|
|
raiseGunFullDuration = getanimlength( %run_2_melee_charge ) * 1000;
|
|
raiseGunFinishDuration = raiseGunFullDuration - 100;
|
|
raiseGunPredictDuration = raiseGunFullDuration - 200;
|
|
raiseGunStartTime = 0;
|
|
|
|
predictedEnemyDistSqAfterRaiseGun = undefined;
|
|
|
|
runAnim = %run_lowready_F;
|
|
|
|
if ( isplayer( self.melee.target ) && self.melee.target == self.enemy )
|
|
self orientMode( "face enemy" );
|
|
else
|
|
self orientMode( "face point", self.melee.target.origin );
|
|
|
|
self SetFlaggedAnimKnobAll( "chargeanim", runAnim, %body, 1, .3, 1 );
|
|
raisingGun = false;
|
|
|
|
while ( 1 )
|
|
{
|
|
time = gettime();
|
|
|
|
willBeWithinRangeWhenGunIsRaised = ( isdefined( predictedEnemyDistSqAfterRaiseGun ) && predictedEnemyDistSqAfterRaiseGun <= shouldRaiseGunDistSq );
|
|
|
|
if ( !raisingGun )
|
|
{
|
|
if ( willBeWithinRangeWhenGunIsRaised )
|
|
{
|
|
Melee_StartMovement();
|
|
self SetFlaggedAnimKnobAllRestart( "chargeanim", %run_2_melee_charge, %body, 1, .2, 1 );
|
|
raiseGunStartTime = time;
|
|
raisingGun = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if we *are* raising our gun, don't stop unless we're hopelessly out of range,
|
|
// or if we hit the end of the raise gun animation and didn't melee yet
|
|
withinRangeNow = enemyDistanceSq <= shouldRaiseGunDistSq;
|
|
if ( time - raiseGunStartTime >= raiseGunFinishDuration || ( !willBeWithinRangeWhenGunIsRaised && !withinRangeNow ) )
|
|
{
|
|
Melee_StartMovement();
|
|
self SetFlaggedAnimKnobAll( "chargeanim", runAnim, %body, 1, .3, 1 );
|
|
raisingGun = false;
|
|
}
|
|
}
|
|
self animscripts\shared::DoNoteTracksForTime( sampleTime, "chargeanim" );
|
|
|
|
// now that we moved a bit, see if our target moved before we check for valid melee
|
|
// it's possible something happened in the meantime that makes meleeing impossible.
|
|
if ( !Melee_Standard_UpdateAndValidateTarget() )
|
|
{
|
|
Melee_StopMovement();
|
|
return false;
|
|
}
|
|
|
|
enemyDistanceSq = distance2dSquared( self.origin, self.melee.target.origin );
|
|
enemyVel = vector_multiply( self.melee.target.origin - prevEnemyPos, 1 / ( gettime() - time ) );// units / msec
|
|
prevEnemyPos = self.melee.target.origin;
|
|
|
|
// figure out where the player will be when we hit them if we (a) start meleeing now, or (b) start raising our gun now
|
|
predictedEnemyPosAfterRaiseGun = self.melee.target.origin + vector_multiply( enemyVel, raiseGunPredictDuration );
|
|
predictedEnemyDistSqAfterRaiseGun = distance2dSquared( self.origin, predictedEnemyPosAfterRaiseGun );
|
|
|
|
// if we're done raising our gun, and starting a melee now will hit the guy, our preparation is finished
|
|
// when fighting non-players, don't wait for the gun raise to finish, or we'll walk through them
|
|
if ( raisingGun && (enemyDistanceSq <= shouldMeleeDistSq) && (gettime() - raiseGunStartTime >= raiseGunFinishDuration || !isPlayer( self.melee.target )) )
|
|
break;
|
|
|
|
// don't keep charging if we've been doing this for too long.
|
|
if ( !raisingGun && (gettime() >= self.melee.giveUpTime) )
|
|
{
|
|
Melee_StopMovement();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Melee_StopMovement();
|
|
return true;
|
|
}
|
|
|
|
Melee_PlayChargeSound()
|
|
{
|
|
if ( !isdefined( self.a.nextMeleeChargeSound ) )
|
|
self.a.nextMeleeChargeSound = 0;
|
|
|
|
if ( ( isdefined( self.enemy ) && isplayer( self.enemy ) ) || randomint( 3 ) == 0 )
|
|
{
|
|
if ( gettime() > self.a.nextMeleeChargeSound )
|
|
{
|
|
self animscripts\face::SayGenericDialogue( "meleecharge" );
|
|
self.a.nextMeleeChargeSound = gettime() + 8000;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================================
|
|
// AI vs AI synced melee
|
|
// ===========================================================
|
|
|
|
|
|
Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Flip( angleDiff )
|
|
{
|
|
flipAngleThreshold = 90;
|
|
|
|
// Have extra tolerance when already in progress, since some animations twist the origin quite a bit ( for example standard melee )
|
|
if ( self.melee.inProgress )
|
|
flipAngleThreshold += 50;
|
|
|
|
// facing each other
|
|
if ( abs( angleDiff ) < flipAngleThreshold )
|
|
return false;
|
|
|
|
target = self.melee.target;
|
|
Melee_Decide_Winner();
|
|
if ( self.melee.winner )
|
|
{
|
|
self.melee.animName = %melee_F_awin_attack;
|
|
target.melee.animName = %melee_F_awin_defend;
|
|
target.melee.surviveAnimName = %melee_F_awin_defend_survive;
|
|
}
|
|
else
|
|
{
|
|
self.melee.animName = %melee_F_dwin_attack;
|
|
target.melee.animName = %melee_F_dwin_defend;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Wrestle( angleDiff )
|
|
{
|
|
wrestleAngleThreshold = 100;
|
|
|
|
// Have extra tolerance when already in progress, since some animations twist the origin quite a bit ( for example standard melee )
|
|
if ( self.melee.inProgress )
|
|
wrestleAngleThreshold += 50;
|
|
|
|
// facing each other
|
|
if ( abs( angleDiff ) < wrestleAngleThreshold )
|
|
return false;
|
|
|
|
target = self.melee.target;
|
|
|
|
// Attacker must be able to win
|
|
if ( isDefined( target.magic_bullet_shield ) )
|
|
return false;
|
|
|
|
/#
|
|
// DEBUGGING CASES FOR TEST MAP
|
|
if ( isDefined( target.meleeAlwaysWin ) )
|
|
{
|
|
assert( !isDefined( self.magic_bullet_shield ) );
|
|
return false;
|
|
}
|
|
#/
|
|
|
|
self.melee.winner = true;
|
|
self.melee.animName = %bog_melee_R_attack;
|
|
target.melee.animName = %bog_melee_R_defend;
|
|
target.melee.surviveAnimName = %bog_melee_R_backdeath2;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Behind( angleDiff )
|
|
{
|
|
// from behind right
|
|
if ( (-90 > angleDiff) || (angleDiff > 0) )
|
|
return false;
|
|
|
|
target = self.melee.target;
|
|
|
|
// Attacker must be able to win
|
|
if ( isDefined( target.magic_bullet_shield ) )
|
|
return false;
|
|
|
|
/#
|
|
// DEBUGGING CASES FOR TEST MAP
|
|
if ( isDefined( target.meleeAlwaysWin ) )
|
|
{
|
|
assert( !isDefined( self.magic_bullet_shield ) );
|
|
return false;
|
|
}
|
|
#/
|
|
|
|
self.melee.winner = true;
|
|
self.melee.animName = %melee_sync_attack;
|
|
target.melee.animName = %melee_sync_defend;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_BuildExposedList()
|
|
{
|
|
// If this AI is forced to play a specific melee, do so!
|
|
if ( isDefined( self.meleeForcedExposedFlip ) )
|
|
{
|
|
assert( !isDefined( self.meleeForcedExposedWrestle ) ); //can't force both
|
|
exposedMelees[0] = ::Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Flip;
|
|
}
|
|
else if ( isDefined( self.meleeForcedExposedWrestle ) )
|
|
{
|
|
exposedMelees[0] = ::Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Wrestle;
|
|
}
|
|
else
|
|
{
|
|
// Randomize whether flip or wrestle gets tested first. Behind always tested last.
|
|
flipIndex = randomInt( 2 );
|
|
wrestleIndex = 1 - flipIndex;
|
|
behindIndex = 2;
|
|
|
|
exposedMelees[flipIndex] = ::Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Flip;
|
|
exposedMelees[wrestleIndex] = ::Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Wrestle;
|
|
exposedMelees[behindIndex] = ::Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_Behind;
|
|
}
|
|
|
|
return exposedMelees;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_Exposed_ChooseAnimationAndPosition()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
|
|
// Choose which sequence to play based on angles
|
|
target = self.melee.target;
|
|
angleToEnemy = vectortoangles( target.origin - self.origin );
|
|
angleDiff = AngleClamp180( target.angles[ 1 ] - angleToEnemy[ 1 ] );
|
|
|
|
exposedMelees = Melee_AIvsAI_Exposed_ChooseAnimationAndPosition_BuildExposedList();
|
|
for( i = 0; i < exposedMelees.size; i++ )
|
|
{
|
|
// Test each melee move in order
|
|
if ( [[ exposedMelees[i] ]]( angleDiff ) )
|
|
{
|
|
assert( isDefined ( self.melee.animName ) );
|
|
assert( isDefined ( target.melee.animName ) );
|
|
|
|
// Calculate the position based on the chosen animation. The angles are set so that the attacker faces the enemy before linking
|
|
self.melee.startAngles = ( 0, angleToEnemy[1], 0 );
|
|
self.melee.startPos = getStartOrigin( target.origin, target.angles, self.melee.animName );
|
|
|
|
// Succeed if it's on a proper floor/height, we can move in position and we we have a LOS to the target
|
|
if ( Melee_UpdateAndValidateStartPos() )
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// No moves possible
|
|
return false;
|
|
}
|
|
|
|
Melee_Decide_Winner()
|
|
{
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
|
|
target = self.melee.target;
|
|
|
|
/#
|
|
// DEBUGGING CASES FOR TEST MAP
|
|
if( isDefined( self.meleeAlwaysWin ) )
|
|
{
|
|
assert( !isDefined( target.magic_bullet_shield ) );
|
|
self.melee.winner = true;
|
|
return;
|
|
}
|
|
else if ( isDefined( target.meleeAlwaysWin ) )
|
|
{
|
|
assert( !isDefined( self.magic_bullet_shield ) );
|
|
self.melee.winner = false;
|
|
return;
|
|
}
|
|
#/
|
|
|
|
// Figure out who wins
|
|
if ( isDefined( self.magic_bullet_shield ) )
|
|
{
|
|
assert( !isDefined( target.magic_bullet_shield ) );
|
|
self.melee.winner = true;
|
|
}
|
|
else if ( isDefined( target.magic_bullet_shield ) )
|
|
{
|
|
self.melee.winner = false;
|
|
}
|
|
else
|
|
{
|
|
self.melee.winner = cointoss();
|
|
}
|
|
}
|
|
|
|
Melee_AIvsAI_SpecialCover_ChooseAnimationAndPosition()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
assert( isDefined( self.melee.target.covernode ) );
|
|
|
|
target = self.melee.target;
|
|
|
|
Melee_Decide_Winner();
|
|
|
|
if ( target.covernode.type == "Cover Left" )
|
|
{
|
|
if ( self.melee.winner )
|
|
{
|
|
self.melee.animName = %cornerSdL_melee_winA_attacker;
|
|
target.melee.animName = %cornerSdL_melee_winA_defender;
|
|
target.melee.surviveAnimName = %cornerSdL_melee_winA_defender_survive;
|
|
}
|
|
else
|
|
{
|
|
self.melee.animName = %cornerSdL_melee_winD_attacker;
|
|
self.melee.surviveAnimName = %cornerSdL_melee_winD_attacker_survive;
|
|
target.melee.animName = %cornerSdL_melee_winD_defender;
|
|
}
|
|
}
|
|
else // Right
|
|
{
|
|
assert( target.covernode.type == "Cover Right" );
|
|
if ( self.melee.winner )
|
|
{
|
|
self.melee.animName = %cornerSdR_melee_winA_attacker;
|
|
target.melee.animName = %cornerSdR_melee_winA_defender;
|
|
}
|
|
else
|
|
{
|
|
self.melee.animName = %cornerSdR_melee_winD_attacker;
|
|
target.melee.animName = %cornerSdR_melee_winD_defender;
|
|
}
|
|
}
|
|
|
|
// The start position is based on the cover node of the target
|
|
self.melee.startPos = getStartOrigin( target.covernode.origin, target.covernode.angles, self.melee.animName );
|
|
self.melee.startAngles = ( target.covernode.angles[0], AngleClamp180( target.covernode.angles[1] + 180 ), target.covernode.angles[2] );
|
|
|
|
target.melee.faceYaw = getNodeForwardYaw( target.covernode );
|
|
|
|
// Make sure we can move to the selected point ( no re-try for now )
|
|
self.melee.startToTargetCornerAngles = target.covernode.angles;
|
|
if ( !Melee_UpdateAndValidateStartPos() )
|
|
{
|
|
self.melee.startToTargetCornerAngles = undefined;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_SpecialCover_CanExecute()
|
|
{
|
|
assert( isDefined ( self ) );
|
|
assert( isDefined ( self.melee.target ) );
|
|
|
|
cover = self.melee.target.covernode;
|
|
if ( !isDefined( cover ) )
|
|
return false;
|
|
|
|
// Make sure the enemy is hiding or leaning out and not currently exposing
|
|
if ( (distanceSquared( cover.origin, self.melee.target.origin ) > 16) && isdefined( self.melee.target.a.coverMode ) && ( (self.melee.target.a.coverMode != "hide") && (self.melee.target.a.coverMode != "lean") ) )
|
|
return false;
|
|
|
|
// Must be within a some arc in front of the cover
|
|
coverToSelfAngles = vectortoangles( self.origin - cover.origin );
|
|
angleDiff = AngleClamp180( cover.angles[ 1 ] - coverToSelfAngles[ 1 ] );
|
|
|
|
// Only do it for left/right covers for now
|
|
if ( cover.type == "Cover Left" )
|
|
{
|
|
if ( (angleDiff >= -50) && (angleDiff <= 0) )
|
|
return true;
|
|
}
|
|
else if ( cover.type == "Cover Right" )
|
|
{
|
|
if ( (angleDiff >= 0) && (angleDiff <= 50) )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_ChooseAction()
|
|
{
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
|
|
target = self.melee.target;
|
|
|
|
// We can only do AI vs AI between human AIs
|
|
if ( !isAI( target ) || (target.type != "human") )
|
|
return false;
|
|
|
|
// Can't do AIvsAI in stairs
|
|
assert( isDefined( self.stairsState ) );
|
|
assert( isDefined( target.stairsState ) );
|
|
if ( (self.stairsState != "none") || (target.stairsState != "none") )
|
|
return false;
|
|
|
|
// At least one of the two needs not to have bullet shield to be in melee to begin with
|
|
assert( !isDefined( self.magic_bullet_shield ) || !isdefined( self.melee.target.magic_bullet_shield ) );
|
|
|
|
if ( isdefined( self.specialMeleeChooseAction ) )
|
|
{
|
|
if ( ![[ self.specialMeleeChooseAction ]]() )
|
|
return false;
|
|
self.melee.precisePositioning = true;
|
|
}
|
|
else if ( isdefined( target.specialMeleeChooseAction ) )
|
|
{
|
|
return false;
|
|
}
|
|
// If we can execute a special cover sequence, do so, otherwise revert to standard
|
|
else if ( Melee_AIvsAI_SpecialCover_CanExecute() && Melee_AIvsAI_SpecialCover_ChooseAnimationAndPosition() )
|
|
{
|
|
self.melee.precisePositioning = true;
|
|
}
|
|
else
|
|
{
|
|
if ( !Melee_AIvsAI_Exposed_ChooseAnimationAndPosition() )
|
|
return false;
|
|
self.melee.precisePositioning = false;
|
|
}
|
|
|
|
// Save the current facing yaw if none of the behaviors requested something specific.
|
|
if ( !isDefined ( target.melee.faceYaw ) )
|
|
target.melee.faceYaw = target.angles[1];
|
|
|
|
// And the offset from the target to the start pos so that we can do validity checks
|
|
self.melee.startPosOffset = ( self.melee.startPos - target.origin );
|
|
|
|
// If we get here, we can get to our position and an action has been chosen
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_ScheduleNoteTrackLink( target )
|
|
{
|
|
// Set us up to get sync'd when we get the note track ( not immediately as regular melees )
|
|
self.melee.syncNoteTrackEnt = target;
|
|
target.melee.syncNoteTrackEnt = undefined;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_TargetLink( target )
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( target ) );
|
|
|
|
// If the target is no longer meleeing, don't attach (should only be valid if surviving)
|
|
if ( !isDefined( target.melee ) )
|
|
{
|
|
assert( isDefined( self.melee.survive ) );
|
|
return;
|
|
}
|
|
|
|
self Melee_PlayChargeSound();
|
|
|
|
// Only attach to our target if he's still alive
|
|
if ( !isAlive( target ) )
|
|
return;
|
|
|
|
// Sync up - this var needs to stay outside the melee struct because code uses it!
|
|
self.syncedMeleeTarget = target;
|
|
target.syncedMeleeTarget = self;
|
|
|
|
self.melee.linked = true;
|
|
target.melee.linked = true;
|
|
self linkToBlendToTag( target, "tag_sync", true, true );
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_Main()
|
|
{
|
|
// charge to correct position
|
|
if ( !Melee_AIvsAI_GetInPosition() )
|
|
{
|
|
// if we couldn't get in place to melee, don't try to charge for a little while and abort
|
|
self.nextMeleeChargeTime = gettime() + FAILED_CHARGE_NEXT_MELEE_TIME;
|
|
self.nextMeleeChargeTarget = self.melee.target;
|
|
return;
|
|
}
|
|
|
|
target = self.melee.target;
|
|
|
|
// make sure both are still alive - get in position should have aborted otherwise
|
|
assert( isAlive( self ) && isAlive( target ) );
|
|
|
|
// setup linking/syncing
|
|
|
|
// catch any leftover sync issues
|
|
assert( !isDefined( self.syncedMeleeTarget ) );
|
|
assert( !isDefined( target.syncedMeleeTarget ) );
|
|
|
|
assert( isDefined( self.melee.animName ) );
|
|
assert( animHasNotetrack( self.melee.animName, NOTETRACK_SYNC ) );
|
|
self Melee_AIvsAI_ScheduleNoteTrackLink( target );
|
|
|
|
// Setup who gets to live
|
|
if ( self.melee.winner )
|
|
{
|
|
self.melee.death = undefined;
|
|
target.melee.death = true;
|
|
}
|
|
else
|
|
{
|
|
target.melee.death = undefined;
|
|
self.melee.death = true;
|
|
}
|
|
|
|
// link up the two in case someone ends the script early
|
|
self.melee.partner = target;
|
|
target.melee.partner = self;
|
|
|
|
if ( self usingSideArm() )
|
|
{
|
|
self forceUseWeapon( self.primaryWeapon, "primary" );
|
|
self.lastWeapon = self.primaryWeapon;
|
|
}
|
|
if ( target usingSideArm() )
|
|
{
|
|
target forceUseWeapon( target.primaryWeapon, "primary" );
|
|
target.lastWeapon = target.primaryWeapon;
|
|
}
|
|
|
|
//save weapons
|
|
self.melee.weapon = self.weapon;
|
|
self.melee.weaponSlot = self getCurrentWeaponSlotName();
|
|
target.melee.weapon = target.weapon;
|
|
target.melee.weaponSlot = target getCurrentWeaponSlotName();
|
|
|
|
// mark melee as in progress for the initiater
|
|
self.melee.inProgress = true;
|
|
|
|
// Run animation on our target
|
|
target animcustom( ::Melee_AIvsAI_Execute, ::Melee_EndScript );
|
|
target thread Melee_AIvsAI_AnimCustomInterruptionMonitor( self );
|
|
|
|
// release the target now that it started, we're no longer allowed to mess with it
|
|
self.melee.target = undefined;
|
|
|
|
// We're already in a custom, call directly
|
|
self Melee_AIvsAI_Execute();
|
|
}
|
|
|
|
Melee_AIvsAI_AnimCustomInterruptionMonitor( attacker )
|
|
{
|
|
assert( isDefined( attacker ) );
|
|
|
|
self endon( "end_melee" );
|
|
self endon( "melee_aivsai_execute" );
|
|
|
|
// Wait for a couple of frames. If the execution hasn't started then, fail.
|
|
wait 0.1;
|
|
|
|
if ( isDefined( attacker ) )
|
|
attacker notify( "end_melee" );
|
|
|
|
self notify( "end_melee" );
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_GetInPosition_UpdateAndValidateTarget( initialTargetOrigin, giveUpTime )
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( initialTargetOrigin ) );
|
|
|
|
// Took too long
|
|
if ( isDefined( giveUpTime ) && (giveUpTime <= getTime()) )
|
|
return false;
|
|
|
|
// Check if we can still melee while charging
|
|
if ( !Melee_IsValid() )
|
|
return false;
|
|
|
|
target = self.melee.target;
|
|
|
|
// If target moves too much , fail
|
|
positionDelta = distanceSquared( target.origin, initialTargetOrigin );
|
|
|
|
// Less tolerant to movement when the target should be in cover
|
|
assert( isDefined( self.melee.precisePositioning ) );
|
|
if ( self.melee.precisePositioning )
|
|
positionThreshold = sqr16;
|
|
else
|
|
positionThreshold = sqr36;
|
|
|
|
if ( positionDelta > positionThreshold )
|
|
return false;
|
|
|
|
// Make sure the target hasn't moved in a way that would make us unable to do the melee
|
|
// Update our starting position
|
|
// Make sure target is not out of reach
|
|
self.melee.startPos = target.origin + self.melee.startPosOffset;
|
|
if ( !Melee_UpdateAndValidateStartPos() )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_GetInPosition_IsSuccessful( initialTargetOrigin )
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.startPos ) );
|
|
assert( isDefined( self.melee.target ) );
|
|
assert( isDefined( initialTargetOrigin ) );
|
|
|
|
// at the start pos
|
|
dist2dToStartPos = distanceSquared( (self.origin[0], self.origin[1], 0), (self.melee.startPos[0], self.melee.startPos[1], 0) );
|
|
if ( (dist2dToStartPos < sqr8) && (abs( self.melee.startPos[2] - self.origin[2] ) < MELEE_RANGE) )
|
|
return true;
|
|
|
|
// in between enemy and start pos
|
|
dist2dFromStartPosToTargetSq = distanceSquared( (initialTargetOrigin[0], initialTargetOrigin[1], 0), (self.melee.startPos[0], self.melee.startPos[1], 0) );
|
|
dist2dToTargetSq = distanceSquared( (self.origin[0], self.origin[1], 0), (self.melee.target.origin[0], self.melee.target.origin[1], 0) );
|
|
if ( (dist2dFromStartPosToTargetSq > dist2dToTargetSq) && (abs( self.melee.target.origin[2] - self.origin[2] ) < MELEE_RANGE) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_GetInPosition_Finalize( initialTargetOrigin )
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.precisePositioning ) );
|
|
assert( isDefined( initialTargetOrigin ) );
|
|
|
|
// stop the animation and such
|
|
Melee_StopMovement();
|
|
|
|
if ( self.melee.precisePositioning )
|
|
{
|
|
assert( isDefined( self.melee.startPos ) );
|
|
assert( isDefined( self.melee.startAngles ) );
|
|
|
|
self forceTeleport( self.melee.startPos, self.melee.startAngles );
|
|
wait 0.05;
|
|
}
|
|
else
|
|
{
|
|
self orientMode( "face angle", self.melee.startAngles[1] );
|
|
wait 0.05;
|
|
}
|
|
|
|
// Teleport might have made the sequence invalid, make sure it's still right as we exit
|
|
return Melee_AIvsAI_GetInPosition_UpdateAndValidateTarget( initialTargetOrigin );
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_GetInPosition()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
// Check if we can still melee while charging
|
|
if ( !Melee_IsValid() )
|
|
return false;
|
|
|
|
Melee_StartMovement();
|
|
self clearanim( %body, 0.2 );
|
|
self setAnimKnobAll( animscripts\run::GetRunAnim(), %body, 1, 0.2 );
|
|
self animMode( "zonly_physics" );
|
|
self.keepClaimedNode = true;
|
|
|
|
giveUpTime = getTime() + 1500;
|
|
|
|
assert( isDefined( self.melee.target ) );
|
|
assert( isDefined( self.melee.target.origin ) );
|
|
initialTargetOrigin = self.melee.target.origin;
|
|
|
|
/#
|
|
self notify ( "MDBG_att_getInPosition", self.melee.target );
|
|
self.melee.target notify ( "MDBG_def_getInPosition", self );
|
|
#/
|
|
|
|
while ( Melee_AIvsAI_GetInPosition_UpdateAndValidateTarget( initialTargetOrigin, giveUpTime ) )
|
|
{
|
|
if ( Melee_AIvsAI_GetInPosition_IsSuccessful( initialTargetOrigin ) )
|
|
return Melee_AIvsAI_GetInPosition_Finalize( initialTargetOrigin );
|
|
|
|
// play run forward anim
|
|
self orientMode( "face point", self.melee.startPos );
|
|
wait .05;
|
|
}
|
|
|
|
Melee_StopMovement();
|
|
return false;
|
|
}
|
|
|
|
|
|
Melee_AIvsAI_Execute()
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "end_melee" );
|
|
|
|
self notify( "melee_aivsai_execute" );
|
|
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
self animMode( "zonly_physics" );
|
|
self.a.special = "none";
|
|
self.specialDeathFunc = undefined;
|
|
|
|
// Check whether something makes us drop our weapon. If we get revived we'll need to restore it
|
|
self thread Melee_DroppedWeaponMonitorThread();
|
|
|
|
// Check for our partner ending melee early, for getting saved etc
|
|
self thread Melee_PartnerEndedMeleeMonitorThread();
|
|
|
|
// If we have faceYaw specified, use them, otherwise stay oriented as we were
|
|
if ( isDefined( self.melee.faceYaw ) )
|
|
self orientMode( "face angle", self.melee.faceYaw );
|
|
else
|
|
self orientMode( "face current" );
|
|
|
|
// only have standing melees for now, set these with notetracks
|
|
self.a.pose = "stand";
|
|
self clearanim( %body, 0.2 );
|
|
|
|
// Disable some interruptions if we're going to die, we don't want to break out of melee
|
|
if ( isDefined( self.melee.death ) )
|
|
self Melee_DisableInterruptions();
|
|
|
|
// Start the base animation, and loop over the note tracks until one of them tell us to stop
|
|
self setFlaggedAnimKnobAllRestart( "meleeAnim", self.melee.animName, %body, 1, 0.2 );
|
|
endNote = self animscripts\shared::DoNoteTracks( "meleeAnim", ::Melee_HandleNoteTracks );
|
|
|
|
// If the survival animation stopped us, play it now
|
|
if ( (endNote == NOTETRACK_DEATH) && isDefined( self.melee.survive ) )
|
|
{
|
|
// If we dropped our weapon but we got saved, restore it immediately
|
|
Melee_DroppedWeaponRestore();
|
|
|
|
self setflaggedanimknoballrestart( "meleeAnim", self.melee.surviveAnimName, %body, 1, 0.2 );
|
|
endNote = self animscripts\shared::DoNoteTracks( "meleeAnim", ::Melee_HandleNoteTracks );
|
|
}
|
|
|
|
// If we're marked for death, make sure we die before exiting
|
|
if ( isDefined( self.melee ) && isDefined( self.melee.death ) )
|
|
self kill();
|
|
|
|
// note sure what this does:
|
|
self.keepClaimedNode = false;
|
|
}
|
|
|
|
|
|
Melee_DisableInterruptions()
|
|
{
|
|
//save the states so we can restore them
|
|
self.melee.wasAllowingPain = self.allowPain;
|
|
self.melee.wasFlashbangImmune = self.flashBangImmunity;
|
|
|
|
//disable what makes sense
|
|
self disable_pain();
|
|
self setFlashbangImmunity( true );
|
|
}
|
|
|
|
|
|
Melee_NeedsWeaponSwap()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
return ( isDefined( self.melee.weapon ) && (self.melee.weapon != "none") && (self.weapon != self.melee.weapon) );
|
|
}
|
|
|
|
|
|
Melee_DroppedWeaponRestore()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
// Give back the weapon we had initially, if we had one, and we dropped it
|
|
if ( self.weapon != "none" && self.lastWeapon != "none" )
|
|
return;
|
|
|
|
// If we did not have one to begin with, not much we can do
|
|
if ( !isDefined( self.melee.weapon ) || (self.melee.weapon == "none") )
|
|
return;
|
|
|
|
// Immediately swap the weapon. Can't animate when ending the script, and we don't want to when playing the revive
|
|
self forceUseWeapon( self.melee.weapon, self.melee.weaponSlot );
|
|
|
|
// if we dropped the item, destroy it
|
|
if ( isDefined( self.melee.droppedWeaponEnt ) )
|
|
{
|
|
self.melee.droppedWeaponEnt delete();
|
|
self.melee.droppedWeaponEnt = undefined;
|
|
}
|
|
}
|
|
|
|
|
|
Melee_DroppedWeaponMonitorThread()
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "end_melee" );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
self waittill( "weapon_dropped", droppedWeapon );
|
|
|
|
// the weapon drop might fail if in solid and such. droppedWeapon would be 'removed entity' in that case.
|
|
if ( isDefined( droppedWeapon ) )
|
|
{
|
|
assert( isDefined( self.melee ) );
|
|
self.melee.droppedWeaponEnt = droppedWeapon;
|
|
}
|
|
}
|
|
|
|
|
|
Melee_PartnerEndedMeleeMonitorThread_ShouldAnimSurvive()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
// Doesn't have a survival animation set
|
|
if ( !isDefined( self.melee.surviveAnimName ) )
|
|
return false;
|
|
|
|
// Too early if before they interact
|
|
if ( !isDefined( self.melee.surviveAnimAllowed ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_PartnerEndedMeleeMonitorThread()
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "end_melee" );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
self waittill( "partner_end_melee" );
|
|
|
|
if ( isDefined( self.melee.death ) )
|
|
{
|
|
// partner ended the melee, and we're supposed to die. end the script
|
|
if ( isDefined( self.melee.animatedDeath ) || isDefined( self.melee.interruptDeath ) )
|
|
{
|
|
self kill();
|
|
}
|
|
else
|
|
{
|
|
// don't die!
|
|
self.melee.death = undefined;
|
|
|
|
// partner ended before we decided we'd die, we should revive now
|
|
if ( Melee_PartnerEndedMeleeMonitorThread_ShouldAnimSurvive() )
|
|
{
|
|
assert ( animHasNotetrack( self.melee.animName, NOTETRACK_DEATH ) );
|
|
self.melee.survive = true;
|
|
}
|
|
else
|
|
{
|
|
self notify( "end_melee" );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if we're not doing the last part of the animation, end immediately
|
|
if ( !isDefined( self.melee.unsyncHappened ) )
|
|
self notify( "end_melee" );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Melee_Unlink()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
if ( !isDefined( self.melee.linked ) )
|
|
return;
|
|
|
|
// Unlink our sync'd target first, because our own unlink will clear this information
|
|
if ( isDefined( self.syncedMeleeTarget ) )
|
|
self.syncedMeleeTarget Melee_UnlinkInternal();
|
|
|
|
self Melee_UnlinkInternal();
|
|
}
|
|
|
|
|
|
Melee_UnlinkInternal()
|
|
{
|
|
assert( isDefined( self ) );
|
|
|
|
self unlink();
|
|
self.syncedMeleeTarget = undefined;
|
|
|
|
if ( !isAlive( self ) )
|
|
return;
|
|
|
|
assert( isDefined( self.melee ) );
|
|
assert( isDefined( self.melee.linked ) );
|
|
self.melee.linked = undefined;
|
|
|
|
self animMode( "zonly_physics" );
|
|
self orientMode( "face angle", self.angles[1] );
|
|
}
|
|
|
|
|
|
Melee_HandleNoteTracks_Unsync()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
self Melee_Unlink();
|
|
|
|
// let the AIs know that the unsync happened, which changes the interruption behavior
|
|
self.melee.unsyncHappened = true;
|
|
if ( isDefined( self.melee.partner ) && isDefined( self.melee.partner.melee ) )
|
|
self.melee.partner.melee.unsyncHappened = true;
|
|
}
|
|
|
|
|
|
Melee_HandleNoteTracks_ShouldDieAfterUnsync()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
if ( animHasNotetrack( self.melee.animName, NOTETRACK_DEATH ) )
|
|
{
|
|
assert( isDefined( self.melee.surviveAnimName ) );
|
|
return false;
|
|
}
|
|
|
|
return isdefined( self.melee.death );
|
|
}
|
|
|
|
|
|
Melee_HandleNoteTracks_Death( interruptAnimation )
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
assert( isdefined( self.melee.death ) );
|
|
|
|
// set whether we should die immediately if melee were to end, or finish playing the animation
|
|
if ( isDefined( interruptAnimation ) && interruptAnimation )
|
|
self.melee.interruptDeath = true;
|
|
else
|
|
self.melee.animatedDeath = true;
|
|
}
|
|
|
|
|
|
Melee_HandleNoteTracks( note )
|
|
{
|
|
if ( isSubStr( note, "ps_" ) )
|
|
{
|
|
alias = GetSubStr( note, 3 );
|
|
self playSound( alias );
|
|
return;
|
|
}
|
|
|
|
if ( note == NOTETRACK_SYNC )
|
|
{
|
|
if ( isDefined( self.melee.syncNoteTrackEnt ) )
|
|
{
|
|
self Melee_AIvsAI_TargetLink( self.melee.syncNoteTrackEnt );
|
|
self.melee.syncNoteTrackEnt = undefined;
|
|
}
|
|
}
|
|
else if ( note == NOTETRACK_UNSYNC )
|
|
{
|
|
self Melee_HandleNoteTracks_Unsync();
|
|
|
|
// After the targets unsync, the final 'death' sequence is usually played, and we want to handle the pre-corpse sequence ourself.
|
|
// We could add a seperate note track too, if this turns out not to be precise enough.
|
|
if ( Melee_HandleNoteTracks_ShouldDieAfterUnsync() )
|
|
Melee_HandleNoteTracks_Death();
|
|
}
|
|
else if ( note == NOTETRACK_INTERACT )
|
|
{
|
|
// From this point on, it's okay to get revived by the animation
|
|
self.melee.surviveAnimAllowed = true;
|
|
}
|
|
else if ( note == NOTETRACK_DEATH )
|
|
{
|
|
// Check if we got saved. If we did, play the alternate ending
|
|
if ( isDefined( self.melee.survive ) )
|
|
{
|
|
assert( !isdefined( self.melee.death ) );
|
|
assert( isDefined( self.melee.surviveAnimName ) );
|
|
|
|
// Interrupt the waiting loop so that we may start a new one with the survival animation
|
|
return note;
|
|
}
|
|
|
|
assert( isdefined( self.melee.death ) );
|
|
Melee_HandleNoteTracks_Death();
|
|
|
|
if ( isDefined( self.melee.animatedDeath ) )
|
|
return note; // abort DoNoteTracks so we do our death immediately.
|
|
}
|
|
else if ( note == NOTETRACK_ATTACHKNIFE )
|
|
{
|
|
self attach( KNIFE_ATTACK_MODEL, KNIFE_ATTACK_TAG, true );
|
|
self.melee.hasKnife = true;
|
|
}
|
|
else if ( note == NOTETRACK_DETACTKNIFE )
|
|
{
|
|
self detach( KNIFE_ATTACK_MODEL, KNIFE_ATTACK_TAG, true );
|
|
self.melee.hasKnife = undefined;
|
|
}
|
|
else if ( note == NOTETRACK_STAB )
|
|
{
|
|
assert( isDefined( self.melee.hasKnife ) );
|
|
|
|
// Play the knife effect
|
|
self playsound( KNIFE_ATTACK_SOUND );
|
|
playfxontag( level._effect[ KNIFE_ATTACK_FX_NAME ], self, KNIFE_ATTACK_FX_TAG );
|
|
|
|
// make sure the target dies after being stabbed if he's still doing the melee
|
|
if ( isDefined( self.melee.partner ) && isDefined( self.melee.partner.melee ) )
|
|
self.melee.partner Melee_HandleNoteTracks_Death( true );
|
|
}
|
|
}
|
|
|
|
|
|
Melee_DeathHandler_Regular()
|
|
{
|
|
self endon( "end_melee" );
|
|
self animscripts\shared::DropAllAIWeapons();
|
|
return false; //play regular death
|
|
}
|
|
|
|
|
|
Melee_DeathHandler_Delayed()
|
|
{
|
|
self endon( "end_melee" );
|
|
self animscripts\shared::DoNoteTracksWithTimeout( "meleeAnim", 10.0 );
|
|
self animscripts\shared::DropAllAIWeapons();
|
|
self startRagdoll();
|
|
|
|
return true; //skip regular death
|
|
}
|
|
|
|
|
|
Melee_EndScript_CheckDeath()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
if ( !isAlive( self ) && isDefined( self.melee.death ) )
|
|
{
|
|
if ( isDefined( self.melee.animatedDeath ) )
|
|
self.deathFunction = ::Melee_DeathHandler_Delayed;
|
|
else
|
|
self.deathFunction = ::Melee_DeathHandler_Regular;
|
|
}
|
|
}
|
|
|
|
|
|
Melee_EndScript_CheckPositionAndMovement()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
if ( !isAlive( self ) )
|
|
return;
|
|
|
|
// make sure we're not marked as moving anymore
|
|
if ( isDefined( self.melee.playingMovementAnim ) )
|
|
Melee_StopMovement();
|
|
|
|
// Adjust Ground Position
|
|
newOrigin = self getDropToFloorPosition();
|
|
if ( isDefined ( newOrigin ) )
|
|
self forceTeleport( newOrigin, self.angles );
|
|
else
|
|
println( "Warning: Melee animation might have ended up in solid for entity #" + self getentnum() );
|
|
}
|
|
|
|
|
|
Melee_EndScript_CheckWeapon()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
// melee ended with the knife equipped, remove it
|
|
if ( isDefined( self.melee.hasKnife ) )
|
|
self detach( KNIFE_ATTACK_MODEL, KNIFE_ATTACK_TAG, true );
|
|
|
|
// If we dropped our weapon but we didn't die, restore it
|
|
if ( isAlive( self ) )
|
|
Melee_DroppedWeaponRestore();
|
|
}
|
|
|
|
|
|
Melee_EndScript_CheckStateChanges()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
// Restore interruption-based state changes
|
|
|
|
if ( isDefined( self.melee.wasAllowingPain ) )
|
|
{
|
|
if ( self.melee.wasAllowingPain )
|
|
self enable_pain();
|
|
else
|
|
self disable_pain();
|
|
}
|
|
|
|
if ( isDefined( self.melee.wasFlashbangImmune ) )
|
|
self setFlashbangImmunity( self.melee.wasFlashbangImmune );
|
|
}
|
|
|
|
|
|
Melee_EndScript()
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( self.melee ) );
|
|
|
|
self Melee_Unlink();
|
|
self Melee_EndScript_CheckDeath();
|
|
self Melee_EndScript_CheckPositionAndMovement();
|
|
self Melee_EndScript_CheckWeapon();
|
|
self Melee_EndScript_CheckStateChanges();
|
|
|
|
// End the melee prematurely for the other sync'd ent when someone dies/script ends
|
|
if ( isDefined( self.melee.partner ) )
|
|
self.melee.partner notify( "partner_end_melee" );
|
|
|
|
self Melee_ReleaseMutex( self.melee.target );
|
|
}
|
|
|
|
|
|
Melee_AcquireMutex( target )
|
|
{
|
|
assert( isDefined( self ) );
|
|
assert( isDefined( target ) );
|
|
|
|
// Can't acquire when soemone is targeting us for a melee
|
|
if ( isDefined( self.melee ) )
|
|
return false;
|
|
|
|
// Can't acquire enemy mutex if he's already in a melee process
|
|
if ( isDefined( target.melee ) )
|
|
return false;
|
|
|
|
self.melee = spawnStruct();
|
|
target.melee = spawnStruct();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Melee_ReleaseMutex( target )
|
|
{
|
|
assert( isDefined( self ) );
|
|
self.melee = undefined;
|
|
|
|
if ( isDefined( target ) )
|
|
target.melee = undefined;
|
|
}
|