Ykkrosh
dd809f83e8
Simplify the format a bit. Use less <interleave> in the RNG so that error reports become understandable. Fixes #491. This was SVN commit r7478.
182 lines
7.2 KiB
JavaScript
182 lines
7.2 KiB
JavaScript
function Attack() {}
|
|
|
|
Attack.prototype.Schema =
|
|
"<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
|
|
"<a:example>" +
|
|
"<Hack>10.0</Hack>" +
|
|
"<Pierce>0.0</Pierce>" +
|
|
"<Crush>5.0</Crush>" +
|
|
"<MaxRange>10.0</MaxRange>" +
|
|
"<MinRange>0.0</MinRange>" +
|
|
"<PrepareTime>800</PrepareTime>" +
|
|
"<RepeatTime>1600</RepeatTime>" +
|
|
"<ProjectileSpeed>50.0</ProjectileSpeed>" +
|
|
"</a:example>" +
|
|
"<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>" +
|
|
"<optional>" +
|
|
"<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>" +
|
|
"</optional>" +
|
|
"<optional>" +
|
|
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" +
|
|
"<data type='positiveInteger'/>" +
|
|
"</element>" +
|
|
"</optional>" +
|
|
"<optional>" +
|
|
"<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>" +
|
|
"</optional>";
|
|
|
|
Attack.prototype.Init = function()
|
|
{
|
|
};
|
|
|
|
/*
|
|
* TODO: to handle secondary attacks in the future, what we might do is
|
|
* add a 'mode' parameter to most of these functions, to indicate which
|
|
* attack mode we're trying to use, and some other function that allows
|
|
* UnitAI to pick the best attack mode (based on range, damage, etc)
|
|
*/
|
|
|
|
Attack.prototype.GetTimers = function()
|
|
{
|
|
var prepare = +(this.template.PrepareTime || 0);
|
|
var repeat = +(this.template.RepeatTime || 1000);
|
|
return { "prepare": prepare, "repeat": repeat, "recharge": repeat - prepare };
|
|
};
|
|
|
|
Attack.prototype.GetAttackStrengths = function()
|
|
{
|
|
// Convert attack values to numbers, default 0 if unspecified
|
|
return {
|
|
hack: +(this.template.Hack || 0),
|
|
pierce: +(this.template.Pierce || 0),
|
|
crush: +(this.template.Crush || 0)
|
|
};
|
|
};
|
|
|
|
Attack.prototype.GetRange = function()
|
|
{
|
|
var max = +this.template.MaxRange;
|
|
var min = +this.template.MinRange;
|
|
return { "max": max, "min": min };
|
|
}
|
|
|
|
/**
|
|
* Attack the target entity. This should only be called after a successful range check,
|
|
* and should only be called after GetTimers().repeat msec has passed since the last
|
|
* call to PerformAttack.
|
|
*/
|
|
Attack.prototype.PerformAttack = function(target)
|
|
{
|
|
// If this is a ranged attack, then launch a projectile
|
|
if (this.template.ProjectileSpeed)
|
|
{
|
|
// 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
|
|
var horizSpeed = +this.template.ProjectileSpeed;
|
|
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);
|
|
cmpTimer.SetTimeout(this.entity, IID_Attack, "CauseDamage", timeToTarget*1000, target);
|
|
}
|
|
|
|
// 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
|
|
this.CauseDamage(target);
|
|
}
|
|
};
|
|
|
|
|
|
// Inflict damage on the target
|
|
Attack.prototype.CauseDamage = function(target)
|
|
{
|
|
var strengths = this.GetAttackStrengths();
|
|
|
|
var cmpDamageReceiver = Engine.QueryInterface(target, IID_DamageReceiver);
|
|
if (!cmpDamageReceiver)
|
|
return;
|
|
cmpDamageReceiver.TakeDamage(strengths.hack, strengths.pierce, strengths.crush);
|
|
};
|
|
|
|
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
|