2010-02-05 23:00:39 +01:00
function Attack ( ) { }
2010-04-09 21:02:39 +02:00
Attack . prototype . Schema =
2010-04-23 18:09:03 +02:00
"<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
"<a:example>" +
2010-05-15 23:07:52 +02:00
"<Melee>" +
"<Hack>10.0</Hack>" +
"<Pierce>0.0</Pierce>" +
"<Crush>5.0</Crush>" +
"<MaxRange>4.0</MaxRange>" +
"<RepeatTime>1000</RepeatTime>" +
"</Melee>" +
"<Ranged>" +
"<Hack>0.0</Hack>" +
"<Pierce>10.0</Pierce>" +
"<Crush>0.0</Crush>" +
"<MaxRange>44.0</MaxRange>" +
"<MinRange>20.0</MinRange>" +
"<PrepareTime>800</PrepareTime>" +
"<RepeatTime>1600</RepeatTime>" +
"<ProjectileSpeed>50.0</ProjectileSpeed>" +
"</Ranged>" +
"<Charge>" +
"<Hack>10.0</Hack>" +
"<Pierce>0.0</Pierce>" +
"<Crush>50.0</Crush>" +
"<MaxRange>24.0</MaxRange>" +
"<MinRange>20.0</MinRange>" +
"</Charge>" +
2010-04-23 18:09:03 +02:00
"</a:example>" +
2010-04-09 21:02:39 +02:00
"<optional>" +
2010-05-15 23:07:52 +02:00
"<element name='Melee'>" +
"<interleave>" +
"<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + // TODO: it shouldn't be stretched
"<data type='positiveInteger'/>" +
"</element>" +
"</interleave>" +
2010-04-09 21:02:39 +02:00
"</element>" +
"</optional>" +
"<optional>" +
2010-05-15 23:07:52 +02:00
"<element name='Ranged'>" +
"<interleave>" +
"<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
"<element name='MinRange' a:help='Minimum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
"<element name='PrepareTime' a:help='Time from the start of the attack command until the attack actually occurs (in milliseconds). This value relative to RepeatTime should closely match the \"event\" point in the actor's attack animation'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"<element name='ProjectileSpeed' a:help='Speed of projectiles (in metres per second). If unspecified, then it is a melee attack instead'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"</interleave>" +
2010-04-09 21:02:39 +02:00
"</element>" +
"</optional>" +
"<optional>" +
2010-05-15 23:07:52 +02:00
"<element name='Charge'>" +
"<interleave>" +
"<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='MaxRange'><ref name='nonNegativeDecimal'/></element>" + // TODO: how do these work?
"<element name='MinRange'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
2010-04-09 21:02:39 +02:00
"</element>" +
"</optional>" ;
2010-05-15 23:07:52 +02:00
/ * *
* Return the type of the best attack .
* TODO : this should probably depend on range , target , etc ,
* so we can automatically switch between ranged and melee
* /
Attack . prototype . GetBestAttack = function ( )
2010-02-05 23:00:39 +01:00
{
2010-05-15 23:07:52 +02:00
if ( this . template . Ranged )
return "Ranged" ;
else if ( this . template . Melee )
return "Melee" ;
else if ( this . template . Charge )
return "Charge" ;
else
return undefined ;
2010-02-05 23:00:39 +01:00
} ;
2010-05-15 23:07:52 +02:00
Attack . prototype . GetTimers = function ( type )
2010-02-05 23:00:39 +01:00
{
2010-05-15 23:07:52 +02:00
var prepare = + ( this . template [ type ] . PrepareTime || 0 ) ;
var repeat = + ( this . template [ type ] . RepeatTime || 1000 ) ;
2010-02-05 23:00:39 +01:00
return { "prepare" : prepare , "repeat" : repeat , "recharge" : repeat - prepare } ;
} ;
2010-05-15 23:07:52 +02:00
Attack . prototype . GetAttackStrengths = function ( type )
2010-02-05 23:00:39 +01:00
{
// Convert attack values to numbers, default 0 if unspecified
return {
2010-05-15 23:07:52 +02:00
hack : + ( this . template [ type ] . Hack || 0 ) ,
pierce : + ( this . template [ type ] . Pierce || 0 ) ,
crush : + ( this . template [ type ] . Crush || 0 )
2010-02-05 23:00:39 +01:00
} ;
} ;
2010-05-15 23:07:52 +02:00
Attack . prototype . GetRange = function ( type )
2010-02-05 23:00:39 +01:00
{
2010-05-15 23:07:52 +02:00
var max = + this . template [ type ] . MaxRange ;
var min = + ( this . template [ type ] . MinRange || 0 ) ;
2010-03-07 22:38:39 +01:00
return { "max" : max , "min" : min } ;
2010-02-05 23:00:39 +01:00
}
/ * *
2010-02-12 23:46:53 +01:00
* Attack the target entity . This should only be called after a successful range check ,
2010-02-05 23:00:39 +01:00
* and should only be called after GetTimers ( ) . repeat msec has passed since the last
* call to PerformAttack .
* /
2010-05-15 23:07:52 +02:00
Attack . prototype . PerformAttack = function ( type , target )
2010-03-07 22:38:39 +01:00
{
// If this is a ranged attack, then launch a projectile
2010-05-15 23:07:52 +02:00
if ( type == "Ranged" )
2010-03-07 22:38:39 +01:00
{
// To implement (in)accuracy, for arrows and javelins, we want to do the following:
// * Compute an accuracy rating, based on the entity's characteristics and the distance to the target
// * Pick a random point 'close' to the target (based on the accuracy) which is the real target point
// * Pick a real target unit, based on their footprint's proximity to the real target point
// * If there is none, then harmlessly shoot to the real target point instead
// * If the real target unit moves after being targeted, the projectile will follow it and hit it anyway
//
// In the future this should be extended:
// * If the target unit moves too far, the projectile should 'detach' and not hit it, so that
// players can dodge projectiles. (Or it should pick a new target after detaching, so it can still
// hit somebody.)
// * Obstacles like trees could reduce the probability of the target being hit
// * Obstacles like walls should block projectiles entirely
// * There should be more control over the probabilities of hitting enemy units vs friendly units vs missing,
// for gameplay balance tweaks
// * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just
// hurt anybody near their landing point
// Get some data about the entity
2010-05-15 23:07:52 +02:00
var horizSpeed = + this . template [ type ] . ProjectileSpeed ;
2010-03-07 22:38:39 +01:00
var gravity = 9.81 ; // this affects the shape of the curve; assume it's constant for now
var accuracy = 6 ; // TODO: get from entity template
//horizSpeed /= 8; gravity /= 8; // slow it down for testing
// Find the distance to the target
var selfPosition = Engine . QueryInterface ( this . entity , IID _Position ) . GetPosition ( ) ;
var targetPosition = Engine . QueryInterface ( target , IID _Position ) . GetPosition ( ) ;
var horizDistance = Math . sqrt ( Math . pow ( targetPosition . x - selfPosition . x , 2 ) + Math . pow ( targetPosition . z - selfPosition . z , 2 ) ) ;
// Compute the real target point (based on accuracy)
var angle = Math . random ( ) * 2 * Math . PI ;
var r = 1 - Math . sqrt ( Math . random ( ) ) ; // triangular distribution [0,1] (cluster around the center)
var offset = r * accuracy ; // TODO: should be affected by range
var offsetX = offset * Math . sin ( angle ) ;
var offsetZ = offset * Math . cos ( angle ) ;
var realTargetPosition = { "x" : targetPosition . x + offsetX , "y" : targetPosition . y , "z" : targetPosition . z + offsetZ } ;
// TODO: what we should really do here is select the unit whose footprint is closest to the realTargetPosition
// (and harmlessly hit the ground if there's none), but as a simplification let's just randomly decide whether to
// hit the original target or not.
var realTargetUnit = undefined ;
if ( Math . random ( ) < 0.5 ) // TODO: this is yucky and hardcoded
{
// Hit the original target
realTargetUnit = target ;
realTargetPosition = targetPosition ;
}
else
{
// Hit the ground
// TODO: ought to make sure Y is on the ground
}
// Hurt the target after the appropriate time
if ( realTargetUnit )
{
var realHorizDistance = Math . sqrt ( Math . pow ( realTargetPosition . x - selfPosition . x , 2 ) + Math . pow ( realTargetPosition . z - selfPosition . z , 2 ) ) ;
var timeToTarget = realHorizDistance / horizSpeed ;
var cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
2010-05-15 23:07:52 +02:00
cmpTimer . SetTimeout ( this . entity , IID _Attack , "CauseDamage" , timeToTarget * 1000 , { "type" : type , "target" : target } ) ;
2010-03-07 22:38:39 +01:00
}
// Launch the graphical projectile
var cmpProjectileManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _ProjectileManager ) ;
if ( realTargetUnit )
cmpProjectileManager . LaunchProjectileAtEntity ( this . entity , realTargetUnit , horizSpeed , gravity ) ;
else
cmpProjectileManager . LaunchProjectileAtPoint ( this . entity , realTargetPosition , horizSpeed , gravity ) ;
}
else
{
// Melee attack - hurt the target immediately
2010-05-15 23:07:52 +02:00
this . CauseDamage ( { "type" : type , "target" : target } ) ;
2010-03-07 22:38:39 +01:00
}
2010-05-15 23:07:52 +02:00
// TODO: charge attacks (need to design how they work)
2010-03-07 22:38:39 +01:00
} ;
// Inflict damage on the target
2010-05-15 23:07:52 +02:00
Attack . prototype . CauseDamage = function ( data )
2010-02-05 23:00:39 +01:00
{
2010-05-15 23:07:52 +02:00
var strengths = this . GetAttackStrengths ( data . type ) ;
2010-02-05 23:00:39 +01:00
2010-05-15 23:07:52 +02:00
var cmpDamageReceiver = Engine . QueryInterface ( data . target , IID _DamageReceiver ) ;
2010-02-05 23:00:39 +01:00
if ( ! cmpDamageReceiver )
return ;
cmpDamageReceiver . TakeDamage ( strengths . hack , strengths . pierce , strengths . crush ) ;
2010-07-21 18:09:58 +02:00
Engine . PostMessage ( data . target , MT _Attacked ,
{ "attacker" : this . entity , "target" : data . target } ) ;
2010-08-10 03:25:24 +02:00
PlaySound ( "attack_impact" , this . entity ) ;
2010-02-05 23:00:39 +01:00
} ;
Engine . RegisterComponentType ( IID _Attack , "Attack" , Attack ) ;