#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 );

		melee_anim = self get_melee_anim( "attack" );

		self setflaggedanimknoballrestart( "meleeanim", melee_anim, %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
		melee_anim = self get_melee_anim( "stand_to_melee" );
		self SetFlaggedAnimKnobAll( "readyanim", melee_anim, %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( anim.animsets.melee["charge"], 0, 1 ) );
	meleeAnimTravelDist = 32;
	shouldRaiseGunDist = MELEE_RANGE * 0.75 + meleeAnimTravelDist + raiseGunAnimTravelDist;
	shouldRaiseGunDistSq = shouldRaiseGunDist * shouldRaiseGunDist;

	shouldMeleeDist = MELEE_RANGE + meleeAnimTravelDist;
	shouldMeleeDistSq = shouldMeleeDist * shouldMeleeDist;

	charge_anim = self get_melee_anim( "charge" );
	raiseGunFullDuration = getanimlength( charge_anim ) * 1000;
	raiseGunFinishDuration = raiseGunFullDuration - 100;
	raiseGunPredictDuration = raiseGunFullDuration - 200;
	raiseGunStartTime = 0;

	predictedEnemyDistSqAfterRaiseGun = undefined;

	runAnim = get_melee_anim( "run" );

	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", charge_anim, %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;
}

get_melee_anim( anim_type )
{
	if ( isdefined( self.customMeleeAnims ) )
	{
		return self.customMeleeAnims[ anim_type ];
	}

	return anim.animsets.melee[ anim_type ];
}
