Ticket #18: advanced_attack.diff

File advanced_attack.diff, 31.4 KB (added by Jonathan Waller, 12 years ago)
  • source/simulation2/components/tests/test_RangeManager.h

     
    5050    virtual bool IsFloating() { return false; }
    5151    virtual CFixedVector3D GetPosition() { return CFixedVector3D(); }
    5252    virtual CFixedVector2D GetPosition2D() { return CFixedVector2D(); }
     53    virtual CFixedVector3D GetPreviousPosition() { return CFixedVector3D(); }
     54    virtual CFixedVector2D GetPreviousPosition2D() { return CFixedVector2D(); }
    5355    virtual void TurnTo(entity_angle_t UNUSED(y)) { }
    5456    virtual void SetYRotation(entity_angle_t UNUSED(y)) { }
    5557    virtual void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) { }
  • source/simulation2/components/ICmpProjectileManager.h

     
    3131class ICmpProjectileManager : public IComponent
    3232{
    3333public:
    34     /**
    35      * Launch a projectile from entity @p source to entity @p target.
    36      * @param source source entity; the projectile will determined from the "projectile" prop in its actor
    37      * @param target target entity; the projectile will automatically track the target to ensure it always hits precisely
    38      * @param speed horizontal speed in m/s
    39      * @param gravity gravitational acceleration in m/s^2 (determines the height of the ballistic curve)
    40      */
    41     virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, fixed speed, fixed gravity) = 0;
    4234
    4335    /**
    4436     * Launch a projectile from entity @p source to point @p target.
  • source/simulation2/components/ICmpProjectileManager.cpp

     
    2222#include "simulation2/system/InterfaceScripted.h"
    2323
    2424BEGIN_INTERFACE_WRAPPER(ProjectileManager)
    25 DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtEntity", void, ICmpProjectileManager, LaunchProjectileAtEntity, entity_id_t, entity_id_t, fixed, fixed)
    2625DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", void, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
    2726END_INTERFACE_WRAPPER(ProjectileManager)
  • source/simulation2/components/ICmpPosition.h

     
    108108     */
    109109    virtual CFixedVector2D GetPosition2D() = 0;
    110110
     111    /**
     112     * Returns the previous turn's x,y,z position (no interpolation).
     113     * Depends on the current terrain heightmap.
     114     * Must not be called unless IsInWorld is true.
     115     */
     116    virtual CFixedVector3D GetPreviousPosition() = 0;
     117
     118    /**
     119     * Returns the previous turn's x,z position (no interpolation).
     120     * Must not be called unless IsInWorld is true.
     121     */
     122    virtual CFixedVector2D GetPreviousPosition2D() = 0;
     123
    111124    /**
    112125     * Rotate smoothly to the given angle around the upwards axis.
    113126     * @param y clockwise radians from the +Z axis.
  • source/simulation2/components/ICmpPosition.cpp

     
    3232DEFINE_INTERFACE_METHOD_0("IsFloating", bool, ICmpPosition, IsFloating)
    3333DEFINE_INTERFACE_METHOD_0("GetPosition", CFixedVector3D, ICmpPosition, GetPosition)
    3434DEFINE_INTERFACE_METHOD_0("GetPosition2D", CFixedVector2D, ICmpPosition, GetPosition2D)
     35DEFINE_INTERFACE_METHOD_0("GetPreviousPosition", CFixedVector3D, ICmpPosition, GetPreviousPosition)
     36DEFINE_INTERFACE_METHOD_0("GetPreviousPosition2D", CFixedVector2D, ICmpPosition, GetPreviousPosition2D)
    3537DEFINE_INTERFACE_METHOD_1("TurnTo", void, ICmpPosition, TurnTo, entity_angle_t)
    3638DEFINE_INTERFACE_METHOD_1("SetYRotation", void, ICmpPosition, SetYRotation, entity_angle_t)
    3739DEFINE_INTERFACE_METHOD_2("SetXZRotation", void, ICmpPosition, SetXZRotation, entity_angle_t, entity_angle_t)
  • source/simulation2/components/CCmpProjectileManager.cpp

     
    2020#include "simulation2/system/Component.h"
    2121#include "ICmpProjectileManager.h"
    2222
     23#include "ICmpObstruction.h"
     24#include "ICmpObstructionManager.h"
    2325#include "ICmpPosition.h"
    2426#include "ICmpRangeManager.h"
    2527#include "ICmpTerrain.h"
     
    8688        case MT_Interpolate:
    8789        {
    8890            const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
    89             Interpolate(msgData.frameTime, msgData.offset);
     91            Interpolate(msgData.frameTime);
    9092            break;
    9193        }
    9294        case MT_RenderSubmit:
     
    98100        }
    99101    }
    100102
    101     virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, fixed speed, fixed gravity)
    102     {
    103         LaunchProjectile(source, CFixedVector3D(), target, speed, gravity);
    104     }
    105 
    106103    virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
    107104    {
    108         LaunchProjectile(source, target, INVALID_ENTITY, speed, gravity);
     105        LaunchProjectile(source, target, speed, gravity);
    109106    }
    110107
    111108private:
     
    114111        CUnit* unit;
    115112        CVector3D pos;
    116113        CVector3D target;
    117         entity_id_t targetEnt; // INVALID_ENTITY if the target is just a point
    118114        float timeLeft;
    119115        float speedFactor;
    120116        float gravity;
     
    125121
    126122    uint32_t m_ActorSeed;
    127123
    128     void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity);
     124    void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity);
    129125
    130     void AdvanceProjectile(Projectile& projectile, float dt, float frameOffset);
     126    void AdvanceProjectile(Projectile& projectile, float dt);
    131127
    132     void Interpolate(float frameTime, float frameOffset);
     128    void Interpolate(float frameTime);
    133129
    134130    void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling);
    135131};
    136132
    137133REGISTER_COMPONENT_TYPE(ProjectileManager)
    138134
    139 void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity)
     135void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity)
    140136{
    141137    if (!GetSimContext().HasUnitManager())
    142138        return; // do nothing if graphics are disabled
     
    169165
    170166    CVector3D targetVec;
    171167
    172     if (targetEnt == INVALID_ENTITY)
    173     {
    174         targetVec = CVector3D(targetPoint);
    175     }
    176     else
    177     {
    178         CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), targetEnt);
    179         if (!cmpTargetPosition)
    180             return;
     168    targetVec = CVector3D(targetPoint);
    181169
    182         targetVec = CVector3D(cmpTargetPosition->GetPosition());
    183     }
    184 
    185170    Projectile projectile;
    186171    std::set<CStr> selections;
    187172    projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections);
     
    193178
    194179    projectile.pos = sourceVec;
    195180    projectile.target = targetVec;
    196     projectile.targetEnt = targetEnt;
    197181
    198182    CVector3D offset = projectile.target - projectile.pos;
    199183    float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z);
     
    207191    m_Projectiles.push_back(projectile);
    208192}
    209193
    210 void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt, float frameOffset)
     194void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt)
    211195{
    212196    // Do special processing if we've already reached the target
    213197    if (projectile.timeLeft <= 0)
     
    223207        // apply a bit of drag to them
    224208        projectile.speedFactor *= powf(1.0f - 0.4f*projectile.speedFactor, dt);
    225209    }
    226     else
    227     {
    228         // Projectile hasn't reached the target yet:
    229         // Track the target entity (if there is one, and it's still alive)
    230         if (projectile.targetEnt != INVALID_ENTITY)
    231         {
    232             CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), projectile.targetEnt);
    233             if (cmpTargetPosition && cmpTargetPosition->IsInWorld())
    234             {
    235                 CMatrix3D t = cmpTargetPosition->GetInterpolatedTransform(frameOffset, false);
    236                 projectile.target = t.GetTranslation();
    237                 projectile.target.Y += 2.f; // TODO: ought to aim towards a random point in the solid body of the target
    238210
    239                 // TODO: if the unit is moving, we should probably aim a bit in front of it
    240                 // so we don't have to curve so much just before reaching it
    241             }
    242         }
    243     }
    244 
    245211    CVector3D offset = (projectile.target - projectile.pos) * projectile.speedFactor;
    246212
    247213    // Compute the vertical velocity that's needed so we travel in a ballistic curve and
     
    267233            {
    268234                projectile.pos.Y = h; // stick precisely to the terrain
    269235                projectile.stopped = true;
     236               
     237                // See if the missile hit anything.  If it did then get rid of it.
     238                CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
     239                if (!cmpObstructionManager)
     240                    return;
     241
     242                ICmpObstructionManager::ObstructionSquare obstruction;
     243                NullObstructionFilter filter;
     244               
     245                // This checks the current position with 0.5m of padding around it because that looks about right
     246                if (cmpObstructionManager->FindMostImportantObstruction(filter, entity_pos_t::FromFloat(projectile.pos.X),
     247                                                                        entity_pos_t::FromFloat(projectile.pos.Z),
     248                                                                        entity_pos_t::FromFloat(0.5), obstruction))
     249                {
     250                    projectile.timeLeft = -PROJECTILE_DECAY_TIME; // Make the projectile get destroyed on the next check
     251                }
    270252            }
    271253        }
    272254    }
     
    296278    projectile.unit->GetModel().SetTransform(transform);
    297279}
    298280
    299 void CCmpProjectileManager::Interpolate(float frameTime, float frameOffset)
     281void CCmpProjectileManager::Interpolate(float frameTime)
    300282{
    301283    for (size_t i = 0; i < m_Projectiles.size(); ++i)
    302284    {
    303         AdvanceProjectile(m_Projectiles[i], frameTime, frameOffset);
     285        AdvanceProjectile(m_Projectiles[i], frameTime);
    304286    }
    305287
    306288    // Remove the ones that have reached their target
     
    310292        // Those hitting the ground stay for a while, because it looks pretty.
    311293        if (m_Projectiles[i].timeLeft <= 0.f)
    312294        {
    313             if (m_Projectiles[i].targetEnt == INVALID_ENTITY && m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
     295            if (m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
    314296            {
    315297                // Keep the projectile until it exceeds the decay time
    316298            }
  • source/simulation2/components/CCmpPosition.cpp

     
    6666    // Dynamic state:
    6767
    6868    bool m_InWorld;
    69     entity_pos_t m_X, m_Z, m_LastX, m_LastZ; // these values contain undefined junk if !InWorld
     69    // m_LastX/Z contain the position from the start of the most recent turn
     70    // m_PrevX/Z conatain the position from the turn before that
     71    entity_pos_t m_X, m_Z, m_LastX, m_LastZ, m_PrevX, m_PrevZ; // these values contain undefined junk if !InWorld
    7072    entity_pos_t m_YOffset;
    7173    bool m_RelativeToGround; // whether m_YOffset is relative to terrain/water plane, or an absolute height
    7274
     
    201203        if (!m_InWorld)
    202204        {
    203205            m_InWorld = true;
    204             m_LastX = m_X;
    205             m_LastZ = m_Z;
     206            m_LastX = m_PrevX = m_X;
     207            m_LastZ = m_PrevZ = m_Z;
    206208        }
    207209
    208210        AdvertisePositionChanges();
     
    210212
    211213    virtual void JumpTo(entity_pos_t x, entity_pos_t z)
    212214    {
    213         m_LastX = m_X = x;
    214         m_LastZ = m_Z = z;
     215        m_LastX = m_PrevX = m_X = x;
     216        m_LastZ = m_PrevZ = m_Z = z;
    215217        m_InWorld = true;
    216218
    217219        AdvertisePositionChanges();
     
    278280        return CFixedVector2D(m_X, m_Z);
    279281    }
    280282
     283    virtual CFixedVector3D GetPreviousPosition()
     284    {
     285        if (!m_InWorld)
     286        {
     287            LOGERROR(L"CCmpPosition::GetPreviousPosition called on entity when IsInWorld is false");
     288            return CFixedVector3D();
     289        }
     290
     291        entity_pos_t baseY;
     292        if (m_RelativeToGround)
     293        {
     294            CmpPtr<ICmpTerrain> cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
     295            if (cmpTerrain)
     296                baseY = cmpTerrain->GetGroundLevel(m_PrevX, m_PrevZ);
     297
     298            if (m_Floating)
     299            {
     300                CmpPtr<ICmpWaterManager> cmpWaterMan(GetSimContext(), SYSTEM_ENTITY);
     301                if (cmpWaterMan)
     302                    baseY = std::max(baseY, cmpWaterMan->GetWaterLevel(m_PrevX, m_PrevZ));
     303            }
     304        }
     305
     306        return CFixedVector3D(m_PrevX, baseY + m_YOffset, m_PrevZ);
     307    }
     308
     309    virtual CFixedVector2D GetPreviousPosition2D()
     310    {
     311        if (!m_InWorld)
     312        {
     313            LOGERROR(L"CCmpPosition::GetPreviousPosition2D called on entity when IsInWorld is false");
     314            return CFixedVector2D();
     315        }
     316
     317        return CFixedVector2D(m_PrevX, m_PrevZ);
     318    }
     319
    281320    virtual void TurnTo(entity_angle_t y)
    282321    {
    283322        m_RotY = y;
     
    408447        }
    409448        case MT_TurnStart:
    410449        {
     450            // Store the positions from the turn before
     451            m_PrevX = m_LastX;
     452            m_PrevZ = m_LastZ;
     453           
    411454            m_LastX = m_X;
    412455            m_LastZ = m_Z;
    413456
  • source/simulation2/components/CCmpObstructionManager.cpp

     
    927927
    928928    // Then look for obstructions that cover the target point when expanded by r
    929929    // (i.e. if the target is not inside an object but closer than we can get to it)
     930   
     931    GetObstructionsInRange(filter, x-r, z-r, x+r, z+r, squares);
     932    // Building squares are more important but returned last, so check backwards
     933    for (std::vector<ObstructionSquare>::reverse_iterator it = squares.rbegin(); it != squares.rend(); ++it)
     934    {
     935        CFixedVector2D halfSize(it->hw + r, it->hh + r);
     936        if (Geometry::PointIsInSquare(CFixedVector2D(it->x, it->z) - center, it->u, it->v, halfSize))
     937        {
     938            square = *it;
     939            return true;
     940        }
     941    }
    930942
    931     // TODO: actually do that
    932     // (This might matter when you tell a unit to walk too close to the edge of a building)
    933 
    934943    return false;
    935944}
    936945
  • binaries/data/mods/public/simulation/components/Attack.js

     
    11function Attack() {}
    22
     3
    34var bonusesSchema =
    45    "<optional>" +
    56        "<element name='Bonuses'>" +
     
    4849            "<PrepareTime>800</PrepareTime>" +
    4950            "<RepeatTime>1600</RepeatTime>" +
    5051            "<ProjectileSpeed>50.0</ProjectileSpeed>" +
     52            "<Spread>2.5</Spread>" +
    5153            "<Bonuses>" +
    5254                "<Bonus1>" +
    5355                    "<Classes>Cavalry</Classes>" +
    5456                    "<Multiplier>2</Multiplier>" +
    5557                "</Bonus1>" +
    5658            "</Bonuses>" +
     59            "<Splash>" +
     60                "<Shape>Circular</Shape>" +
     61                "<Range>20</Range>" +
     62                "<FriendlyFire>false</FriendlyFire>" +
     63                "<Hack>0.0</Hack>" +
     64                "<Pierce>10.0</Pierce>" +
     65                "<Crush>0.0</Crush>" +
     66            "</Splash>" +
    5767        "</Ranged>" +
    5868        "<Charge>" +
    5969            "<Hack>10.0</Hack>" +
     
    94104                "<element name='ProjectileSpeed' a:help='Speed of projectiles (in metres per second). If unspecified, then it is a melee attack instead'>" +
    95105                    "<ref name='nonNegativeDecimal'/>" +
    96106                "</element>" +
     107                "<optional><element name='Spread' a:help='Radius over which missiles will tend to land. Roughly 2/3 will land inside this radius (in metres)'><ref name='nonNegativeDecimal'/></element></optional>" +
    97108                bonusesSchema +
     109                "<optional>" +
     110                    "<element name='Splash'>" +
     111                        "<interleave>" +
     112                            "<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
     113                            "<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
     114                            "<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
     115                            "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
     116                            "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
     117                            "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
     118                            bonusesSchema +
     119                        "</interleave>" +
     120                    "</element>" +
     121                "</optional>" +
    98122            "</interleave>" +
    99123        "</element>" +
    100124    "</optional>" +
     
    143167
    144168Attack.prototype.GetAttackStrengths = function(type)
    145169{
     170    var template = this.template[type];
     171    if (!template)
     172        template = this.template[type.split(".")[0]].Splash;
    146173    // Convert attack values to numbers, default 0 if unspecified
    147174    return {
    148         hack: +(this.template[type].Hack || 0),
    149         pierce: +(this.template[type].Pierce || 0),
    150         crush: +(this.template[type].Crush || 0)
     175        hack: +(template.Hack || 0),
     176        pierce: +(template.Pierce || 0),
     177        crush: +(template.Crush || 0)
    151178    };
    152179};
    153180
     
    162189Attack.prototype.GetAttackBonus = function(type, target)
    163190{
    164191    var attackBonus = 1;
    165     if (this.template[type].Bonuses)
     192    var template = this.template[type];
     193    if (!template)
     194        template = this.template[type.split(".")[0]].Splash;
     195   
     196    if (template.Bonuses)
    166197    {
    167198        var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    168199        if (!cmpIdentity)
    169200            return 1;
    170201       
    171202        // Multiply the bonuses for all matching classes
    172         for (var key in this.template[type].Bonuses)
     203        for (var key in template.Bonuses)
    173204        {
    174             var bonus = this.template[type].Bonuses[key];
     205            var bonus = template.Bonuses[key];
    175206           
    176207            var hasClasses = true;
    177208            if (bonus.Classes){
     
    188219    return attackBonus;
    189220};
    190221
     222// Returns a 2d random distribution scaled for a spread of scale 1.
     223// The current implementation is a 2d gaussian with sigma = 1
     224Attack.prototype.GetNormalizedDistribution = function(){
     225   
     226    // Use the Box-Muller transform to get a gaussian distribution
     227    var a = Math.random();
     228    var b = Math.random();
     229   
     230    var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b);
     231    var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b);
     232   
     233    return [c, d];
     234};
     235
    191236/**
    192237 * Attack the target entity. This should only be called after a successful range check,
    193238 * and should only be called after GetTimers().repeat msec has passed since the last
     
    198243    // If this is a ranged attack, then launch a projectile
    199244    if (type == "Ranged")
    200245    {
    201         // To implement (in)accuracy, for arrows and javelins, we want to do the following:
    202         //  * Compute an accuracy rating, based on the entity's characteristics and the distance to the target
    203         //  * Pick a random point 'close' to the target (based on the accuracy) which is the real target point
    204         //  * Pick a real target unit, based on their footprint's proximity to the real target point
    205         //  * If there is none, then harmlessly shoot to the real target point instead
    206         //  * If the real target unit moves after being targeted, the projectile will follow it and hit it anyway
    207         //
    208         // In the future this should be extended:
    209         //  * If the target unit moves too far, the projectile should 'detach' and not hit it, so that
    210         //    players can dodge projectiles. (Or it should pick a new target after detaching, so it can still
    211         //    hit somebody.)
     246        // In the future this could be extended:
    212247        //  * Obstacles like trees could reduce the probability of the target being hit
    213248        //  * Obstacles like walls should block projectiles entirely
    214         //  * There should be more control over the probabilities of hitting enemy units vs friendly units vs missing,
    215         //    for gameplay balance tweaks
    216         //  * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just
    217         //    hurt anybody near their landing point
    218249
    219250        // Get some data about the entity
    220251        var horizSpeed = +this.template[type].ProjectileSpeed;
    221252        var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now
    222         var accuracy = 6; // TODO: get from entity template
    223 
    224         //horizSpeed /= 8; gravity /= 8; // slow it down for testing
    225 
    226         // Find the distance to the target
     253        var spread = 1.5; //
     254        if (this.template.Ranged.Spread !== undefined) // explicit test here because the value might be 0
     255            spread = this.template.Ranged.Spread;
     256       
     257        const TYPICAL_RANGE = 40;
     258        //horizSpeed /= 2; gravity /= 2; // slow it down for testing
     259       
    227260        var selfPosition = Engine.QueryInterface(this.entity, IID_Position).GetPosition();
    228261        var targetPosition = Engine.QueryInterface(target, IID_Position).GetPosition();
    229         var horizDistance = Math.sqrt(Math.pow(targetPosition.x - selfPosition.x, 2) + Math.pow(targetPosition.z - selfPosition.z, 2));
    230 
     262        var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z}
     263        var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
     264       
     265        var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength}
     266        // the component of the targets velocity radially away from the archer
     267        var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition);
     268       
     269        var horizDistance = this.VectorDistance(targetPosition, selfPosition);
     270       
     271        // This is an approximation of the time ot the target, it assumes that the target has a constant radial
     272        // velocity, but since units move in straight lines this is not true.  The exact value would be more
     273        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
     274        // about 5% of the units radius out in the worst case)
     275        var timeToTarget = horizDistance / (horizSpeed - radialSpeed);
     276       
     277        // Predict where the unit is when the missile lands.
     278        var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget,
     279                                 "z": targetPosition.z + targetVelocity.z * timeToTarget};
     280       
    231281        // Compute the real target point (based on accuracy)
    232         var angle = Math.random() * 2*Math.PI;
    233         var r = 1 - Math.sqrt(Math.random()); // triangular distribution [0,1] (cluster around the center)
    234         var offset = r * accuracy; // TODO: should be affected by range
    235         var offsetX = offset * Math.sin(angle);
    236         var offsetZ = offset * Math.cos(angle);
     282        var randNorm = this.GetNormalizedDistribution();
     283        var offsetX = randNorm[0] * spread * horizDistance / TYPICAL_RANGE;
     284        var offsetZ = randNorm[1] * spread * horizDistance / TYPICAL_RANGE;
    237285
    238         var realTargetPosition = { "x": targetPosition.x + offsetX, "y": targetPosition.y, "z": targetPosition.z + offsetZ };
     286        var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ };
     287       
     288        // Calculate when the missile will hit the target position
     289        var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition);
     290        var timeToTarget = realHorizDistance / horizSpeed;
     291       
     292        var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance};
     293       
     294        var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     295        cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection});
    239296
    240         // TODO: what we should really do here is select the unit whose footprint is closest to the realTargetPosition
    241         // (and harmlessly hit the ground if there's none), but as a simplification let's just randomly decide whether to
    242         // hit the original target or not.
    243         var realTargetUnit = undefined;
    244         if (Math.random() < 0.5) // TODO: this is yucky and hardcoded
    245         {
    246             // Hit the original target
    247             realTargetUnit = target;
    248             realTargetPosition = targetPosition;
    249         }
    250         else
    251         {
    252             // Hit the ground
    253             // TODO: ought to make sure Y is on the ground
    254         }
    255 
    256         // Hurt the target after the appropriate time
    257         if (realTargetUnit)
    258         {
    259             var realHorizDistance = Math.sqrt(Math.pow(realTargetPosition.x - selfPosition.x, 2) + Math.pow(realTargetPosition.z - selfPosition.z, 2));
    260             var timeToTarget = realHorizDistance / horizSpeed;
    261             var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    262             cmpTimer.SetTimeout(this.entity, IID_Attack, "CauseDamage", timeToTarget*1000, {"type": type, "target": target});
    263         }
    264 
    265297        // Launch the graphical projectile
    266298        var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
    267         if (realTargetUnit)
    268             cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity);
    269         else
    270             cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
     299        cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
    271300    }
    272301    else
    273302    {
     
    295324    }
    296325};
    297326
     327Attack.prototype.InterpolatedLocation = function(ent, lateness)
     328{
     329    var targetPositionCmp = Engine.QueryInterface(ent, IID_Position);
     330    if (!targetPositionCmp) // TODO: handle dead target properly
     331        return undefined;
     332    var curPos = targetPositionCmp.GetPosition();
     333    var prevPos = targetPositionCmp.GetPreviousPosition();
     334    lateness /= 1000;
     335    return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength,
     336            "z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength};
     337};
     338
     339Attack.prototype.VectorDistance = function(p1, p2)
     340{
     341    return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z));
     342};
     343
     344Attack.prototype.VectorDot = function(p1, p2)
     345{
     346    return (p1.x * p2.x + p1.z * p2.z);
     347};
     348
     349Attack.prototype.VectorCross = function(p1, p2)
     350{
     351    return (p1.x * p2.z - p1.z * p2.x);
     352};
     353
     354Attack.prototype.VectorLength = function(p)
     355{
     356    return Math.sqrt(p.x*p.x + p.z*p.z);
     357};
     358
     359// Tests whether it point is inside of ent's footprint
     360Attack.prototype.testCollision = function(ent, point, lateness)
     361{
     362    var targetPosition = this.InterpolatedLocation(ent, lateness);
     363    var targetShape = Engine.QueryInterface(ent, IID_Footprint).GetShape();
     364   
     365    if (!targetShape || !targetPosition)
     366        return false;
     367   
     368    if (targetShape.type === 'circle')
     369    {
     370        return (this.VectorDistance(point, targetPosition) < targetShape.radius);
     371    }
     372    else
     373    {
     374        var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
     375       
     376        var dx = point.x - targetPosition.x;
     377        var dz = point.z - targetPosition.z;
     378       
     379        var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz;
     380        var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz;
     381       
     382        return (-targetShape.width <= dxr && dxr < targetShape.width && -targetShape.depth <= dzr && dzr < targetShape.depth);
     383    }
     384};
     385
     386Attack.prototype.MissileHit = function(data, lateness)
     387{
     388   
     389    var targetPosition = this.InterpolatedLocation(data.target, lateness);
     390    if (!targetPosition)
     391        return;
     392   
     393    if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target
     394    {
     395        var friendlyFire = this.template.Ranged.Splash.FriendlyFire;
     396        var splashRadius = this.template.Ranged.Splash.Range;
     397        var splashShape = this.template.Ranged.Splash.Shape;
     398       
     399        var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire);
     400        ents.push(data.target); // Add the original unit to the list of splash damage targets
     401       
     402        for (var i = 0; i < ents.length; i++)
     403        {
     404            var entityPosition = this.InterpolatedLocation(ents[i], lateness);
     405            var radius = this.VectorDistance(data.position, entityPosition);
     406           
     407            if (radius < splashRadius)
     408            {
     409                var multiplier = 1;
     410                if (splashShape == "Circular") // quadratic falloff
     411                {
     412                    multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius));
     413                }
     414                else if (splashShape == "Linear")
     415                {
     416                    // position of entity relative to where the missile hit
     417                    var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z};
     418                   
     419                    var splashWidth = splashRadius / 5;
     420                    var parallelDist = this.VectorDot(relPos, data.direction);
     421                    var perpDist = Math.abs(this.VectorCross(relPos, data.direction));
     422                   
     423                    // Check that the unit is within the distance splashWidth of the line starting at the missile's
     424                    // landing point which extends in the direction of the missile for length splashRadius.
     425                    if (parallelDist > -splashWidth && perpDist < splashWidth)
     426                    {
     427                        // Use a quadratic falloff in both directions
     428                        multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius)
     429                                     * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth);
     430                    }
     431                    else
     432                    {
     433                        multiplier = 0;
     434                    }
     435                }
     436                var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier};
     437                this.CauseDamage(newData);
     438            }
     439        }
     440    }
     441   
     442    if (this.testCollision(data.target, data.position, lateness))
     443    {
     444        // Hit the primary target
     445        this.CauseDamage(data);
     446    }
     447    else
     448    {
     449        // If we didn't hit the main target look for nearby units
     450        var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2);
     451       
     452        for (var i = 0; i < ents.length; i++)
     453        {
     454            if (this.testCollision(ents[i], data.position, lateness))
     455            {
     456                var newData = {"type": data.type, "target": ents[i]};
     457                this.CauseDamage(newData);
     458            }
     459        }
     460    }
     461};
     462
     463Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire)
     464{
     465    var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     466    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     467    var owner = cmpOwnership.GetOwner();
     468    var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
     469    var numPlayers = cmpPlayerManager.GetNumPlayers();
     470    var players = [];
     471   
     472    for (var i = 1; i < numPlayers; ++i)
     473    {   
     474        // Only target enemies unless friendly fire is on
     475        if (cmpPlayer.IsEnemy(i) || friendlyFire)
     476            players.push(i);
     477    }
     478   
     479    var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     480    return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver);
     481}
     482
    298483/**
    299484 * Inflict damage on the target
    300485 */
     
    302487{
    303488    var strengths = this.GetAttackStrengths(data.type);
    304489   
    305     var attackBonus = this.GetAttackBonus(data.type, data.target);
     490    var damageMultiplier = this.GetAttackBonus(data.type, data.target);
     491    if (data.damageMultiplier !== undefined)
     492        damageMultiplier *= data.damageMultiplier;
    306493   
    307494    var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
    308495    if (!cmpDamageReceiver)
    309496        return;
    310     var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * attackBonus, strengths.pierce * attackBonus, strengths.crush * attackBonus);
     497    var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier);
    311498    // if target killed pick up loot and credit experience
    312499    if (targetState.killed == true)
    313500    {
     
    320507    PlaySound("attack_impact", this.entity);
    321508};
    322509
     510Attack.prototype.OnUpdate = function(msg)
     511{
     512    this.turnLength = msg.turnLength;
     513}
     514
    323515Engine.RegisterComponentType(IID_Attack, "Attack", Attack);