Ticket #18: advanced_attack-220212.diff

File advanced_attack-220212.diff, 33.8 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.
     
    4638     * @param target target point
    4739     * @param speed horizontal speed in m/s
    4840     * @param gravity gravitational acceleration in m/s^2 (determines the height of the ballistic curve)
     41     * @return id of the created projectile
    4942     */
    50     virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) = 0;
     43    virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) = 0;
     44   
     45    /**
     46     * Removes a projectile, used when the projectile has hit a target
     47     * @param id of the projectile to remove
     48     */
     49    virtual void RemoveProjectile(uint32_t id) = 0;
    5150
    5251    DECLARE_INTERFACE_TYPE(ProjectileManager)
    5352};
  • 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)
    26 DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", void, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
     25DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", uint32_t, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
     26DEFINE_INTERFACE_METHOD_1("RemoveProjectile", void, ICmpProjectileManager, RemoveProjectile, uint32_t)
    2727END_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"
     
    5860    virtual void Init(const CParamNode& UNUSED(paramNode))
    5961    {
    6062        m_ActorSeed = 0;
     63        m_Id = 1;
    6164    }
    6265
    6366    virtual void Deinit()
     
    8689        case MT_Interpolate:
    8790        {
    8891            const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
    89             Interpolate(msgData.frameTime, msgData.offset);
     92            Interpolate(msgData.frameTime);
    9093            break;
    9194        }
    9295        case MT_RenderSubmit:
     
    98101        }
    99102    }
    100103
    101     virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, fixed speed, fixed gravity)
     104    virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
    102105    {
    103         LaunchProjectile(source, CFixedVector3D(), target, speed, gravity);
     106        return LaunchProjectile(source, target, speed, gravity);
    104107    }
     108   
     109    virtual void RemoveProjectile(uint32_t);
    105110
    106     virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
    107     {
    108         LaunchProjectile(source, target, INVALID_ENTITY, speed, gravity);
    109     }
    110 
    111111private:
    112112    struct Projectile
    113113    {
    114114        CUnit* unit;
    115115        CVector3D pos;
    116116        CVector3D target;
    117         entity_id_t targetEnt; // INVALID_ENTITY if the target is just a point
    118117        float timeLeft;
    119118        float speedFactor;
    120119        float gravity;
    121120        bool stopped;
     121        uint32_t id;
    122122    };
    123123
    124124    std::vector<Projectile> m_Projectiles;
    125125
    126126    uint32_t m_ActorSeed;
     127   
     128    uint32_t m_Id;
    127129
    128     void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity);
     130    uint32_t LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity);
    129131
    130     void AdvanceProjectile(Projectile& projectile, float dt, float frameOffset);
     132    void AdvanceProjectile(Projectile& projectile, float dt);
    131133
    132     void Interpolate(float frameTime, float frameOffset);
     134    void Interpolate(float frameTime);
    133135
    134136    void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling);
    135137};
    136138
    137139REGISTER_COMPONENT_TYPE(ProjectileManager)
    138140
    139 void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity)
     141uint32_t CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity)
    140142{
    141143    if (!GetSimContext().HasUnitManager())
    142         return; // do nothing if graphics are disabled
     144        return 0; // do nothing if graphics are disabled
    143145
    144146    CmpPtr<ICmpVisual> cmpSourceVisual(GetSimContext(), source);
    145147    if (!cmpSourceVisual)
    146         return;
     148        return 0;
    147149
    148150    std::wstring name = cmpSourceVisual->GetProjectileActor();
    149151    if (name.empty())
     
    151153        // If the actor was actually loaded, complain that it doesn't have a projectile
    152154        if (!cmpSourceVisual->GetActorShortName().empty())
    153155            LOGERROR(L"Unit with actor '%ls' launched a projectile but has no actor on 'projectile' attachpoint", cmpSourceVisual->GetActorShortName().c_str());
    154         return;
     156        return 0;
    155157    }
    156158
    157159    CVector3D sourceVec(cmpSourceVisual->GetProjectileLaunchPoint());
     
    161163
    162164        CmpPtr<ICmpPosition> sourcePos(GetSimContext(), source);
    163165        if (!sourcePos)
    164             return;
     166            return 0;
    165167
    166168        sourceVec = sourcePos->GetPosition();
    167169        sourceVec.Y += 3.f;
     
    169171
    170172    CVector3D targetVec;
    171173
    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;
     174    targetVec = CVector3D(targetPoint);
    181175
    182         targetVec = CVector3D(cmpTargetPosition->GetPosition());
    183     }
    184 
    185176    Projectile projectile;
    186177    std::set<CStr> selections;
    187178    projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections);
     179    projectile.id = m_Id++;
    188180    if (!projectile.unit)
    189181    {
    190182        // The error will have already been logged
    191         return;
     183        return 0;
    192184    }
    193185
    194186    projectile.pos = sourceVec;
    195187    projectile.target = targetVec;
    196     projectile.targetEnt = targetEnt;
    197188
    198189    CVector3D offset = projectile.target - projectile.pos;
    199190    float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z);
     
    205196    projectile.gravity = gravity.ToFloat();
    206197
    207198    m_Projectiles.push_back(projectile);
     199   
     200    return projectile.id;
    208201}
    209202
    210 void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt, float frameOffset)
     203void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt)
    211204{
    212205    // Do special processing if we've already reached the target
    213206    if (projectile.timeLeft <= 0)
     
    223216        // apply a bit of drag to them
    224217        projectile.speedFactor *= powf(1.0f - 0.4f*projectile.speedFactor, dt);
    225218    }
    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
    238219
    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 
    245220    CVector3D offset = (projectile.target - projectile.pos) * projectile.speedFactor;
    246221
    247222    // Compute the vertical velocity that's needed so we travel in a ballistic curve and
     
    296271    projectile.unit->GetModel().SetTransform(transform);
    297272}
    298273
    299 void CCmpProjectileManager::Interpolate(float frameTime, float frameOffset)
     274void CCmpProjectileManager::Interpolate(float frameTime)
    300275{
    301276    for (size_t i = 0; i < m_Projectiles.size(); ++i)
    302277    {
    303         AdvanceProjectile(m_Projectiles[i], frameTime, frameOffset);
     278        AdvanceProjectile(m_Projectiles[i], frameTime);
    304279    }
    305280
    306281    // Remove the ones that have reached their target
     
    310285        // Those hitting the ground stay for a while, because it looks pretty.
    311286        if (m_Projectiles[i].timeLeft <= 0.f)
    312287        {
    313             if (m_Projectiles[i].targetEnt == INVALID_ENTITY && m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
     288            if (m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
    314289            {
    315290                // Keep the projectile until it exceeds the decay time
    316291            }
     
    328303    }
    329304}
    330305
     306void CCmpProjectileManager::RemoveProjectile(uint32_t id)
     307{
     308    // Scan through the projectile list looking for one with the correct id to remove
     309    for (size_t i = 0; i < m_Projectiles.size(); i++)
     310    {
     311        if (m_Projectiles[i].id == id)
     312        {
     313            // Delete in-place by swapping with the last in the list
     314            std::swap(m_Projectiles[i], m_Projectiles.back());
     315            GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
     316            m_Projectiles.pop_back();
     317            return;
     318        }
     319    }
     320}
     321
    331322void CCmpProjectileManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
    332323{
    333324    CmpPtr<ICmpRangeManager> cmpRangeManager(GetSimContext(), SYSTEM_ENTITY);
  • 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        //horizSpeed /= 2; gravity /= 2; // slow it down for testing
     258       
    227259        var selfPosition = Engine.QueryInterface(this.entity, IID_Position).GetPosition();
    228260        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));
     261        var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z}
     262        var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
     263       
     264        var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength}
     265        // the component of the targets velocity radially away from the archer
     266        var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition);
     267       
     268        var horizDistance = this.VectorDistance(targetPosition, selfPosition);
     269       
     270        // This is an approximation of the time ot the target, it assumes that the target has a constant radial
     271        // velocity, but since units move in straight lines this is not true.  The exact value would be more
     272        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
     273        // about 5% of the units radius out in the worst case)
     274        var timeToTarget = horizDistance / (horizSpeed - radialSpeed);
     275       
     276        // Predict where the unit is when the missile lands.
     277        var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget,
     278                                 "z": targetPosition.z + targetVelocity.z * timeToTarget};
     279       
     280        // Compute the real target point (based on spread and target speed)
     281        var randNorm = this.GetNormalizedDistribution();
     282        var offsetX = randNorm[0] * spread * (1 + this.VectorLength(targetVelocity) / 20);
     283        var offsetZ = randNorm[1] * spread * (1 + this.VectorLength(targetVelocity) / 20);
    230284
    231         // 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);
    237 
    238         var realTargetPosition = { "x": targetPosition.x + offsetX, "y": targetPosition.y, "z": targetPosition.z + offsetZ };
    239 
    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 
     285        var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ };
     286       
     287        // Calculate when the missile will hit the target position
     288        var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition);
     289        var timeToTarget = realHorizDistance / horizSpeed;
     290       
     291        var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance};
     292       
    265293        // Launch the graphical projectile
    266294        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);
     295        var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
     296       
     297        var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     298        cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id});
    271299    }
    272300    else
    273301    {
     
    295323    }
    296324};
    297325
     326Attack.prototype.InterpolatedLocation = function(ent, lateness)
     327{
     328    var targetPositionCmp = Engine.QueryInterface(ent, IID_Position);
     329    if (!targetPositionCmp) // TODO: handle dead target properly
     330        return undefined;
     331    var curPos = targetPositionCmp.GetPosition();
     332    var prevPos = targetPositionCmp.GetPreviousPosition();
     333    lateness /= 1000;
     334    return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength,
     335            "z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength};
     336};
     337
     338Attack.prototype.VectorDistance = function(p1, p2)
     339{
     340    return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z));
     341};
     342
     343Attack.prototype.VectorDot = function(p1, p2)
     344{
     345    return (p1.x * p2.x + p1.z * p2.z);
     346};
     347
     348Attack.prototype.VectorCross = function(p1, p2)
     349{
     350    return (p1.x * p2.z - p1.z * p2.x);
     351};
     352
     353Attack.prototype.VectorLength = function(p)
     354{
     355    return Math.sqrt(p.x*p.x + p.z*p.z);
     356};
     357
     358// Tests whether it point is inside of ent's footprint
     359Attack.prototype.testCollision = function(ent, point, lateness)
     360{
     361    var targetPosition = this.InterpolatedLocation(ent, lateness);
     362    var targetShape = Engine.QueryInterface(ent, IID_Footprint).GetShape();
     363   
     364    if (!targetShape || !targetPosition)
     365        return false;
     366   
     367    if (targetShape.type === 'circle')
     368    {
     369        return (this.VectorDistance(point, targetPosition) < targetShape.radius);
     370    }
     371    else
     372    {
     373        var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
     374       
     375        var dx = point.x - targetPosition.x;
     376        var dz = point.z - targetPosition.z;
     377       
     378        var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz;
     379        var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz;
     380       
     381        return (-targetShape.width <= dxr && dxr < targetShape.width && -targetShape.depth <= dzr && dzr < targetShape.depth);
     382    }
     383};
     384
     385Attack.prototype.MissileHit = function(data, lateness)
     386{
     387   
     388    var targetPosition = this.InterpolatedLocation(data.target, lateness);
     389    if (!targetPosition)
     390        return;
     391   
     392    if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target
     393    {
     394        var friendlyFire = this.template.Ranged.Splash.FriendlyFire;
     395        var splashRadius = this.template.Ranged.Splash.Range;
     396        var splashShape = this.template.Ranged.Splash.Shape;
     397       
     398        var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire);
     399        ents.push(data.target); // Add the original unit to the list of splash damage targets
     400       
     401        for (var i = 0; i < ents.length; i++)
     402        {
     403            var entityPosition = this.InterpolatedLocation(ents[i], lateness);
     404            var radius = this.VectorDistance(data.position, entityPosition);
     405           
     406            if (radius < splashRadius)
     407            {
     408                var multiplier = 1;
     409                if (splashShape == "Circular") // quadratic falloff
     410                {
     411                    multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius));
     412                }
     413                else if (splashShape == "Linear")
     414                {
     415                    // position of entity relative to where the missile hit
     416                    var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z};
     417                   
     418                    var splashWidth = splashRadius / 5;
     419                    var parallelDist = this.VectorDot(relPos, data.direction);
     420                    var perpDist = Math.abs(this.VectorCross(relPos, data.direction));
     421                   
     422                    // Check that the unit is within the distance splashWidth of the line starting at the missile's
     423                    // landing point which extends in the direction of the missile for length splashRadius.
     424                    if (parallelDist > -splashWidth && perpDist < splashWidth)
     425                    {
     426                        // Use a quadratic falloff in both directions
     427                        multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius)
     428                                     * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth);
     429                    }
     430                    else
     431                    {
     432                        multiplier = 0;
     433                    }
     434                }
     435                var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier};
     436                this.CauseDamage(newData);
     437            }
     438        }
     439    }
     440   
     441    if (this.testCollision(data.target, data.position, lateness))
     442    {
     443        // Hit the primary target
     444        this.CauseDamage(data);
     445       
     446        // Remove the projectile
     447        var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
     448        cmpProjectileManager.RemoveProjectile(data.projectileId);
     449    }
     450    else
     451    {
     452        // If we didn't hit the main target look for nearby units
     453        var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2);
     454       
     455        for (var i = 0; i < ents.length; i++)
     456        {
     457            if (this.testCollision(ents[i], data.position, lateness))
     458            {
     459                var newData = {"type": data.type, "target": ents[i]};
     460                this.CauseDamage(newData);
     461               
     462                // Remove the projectile
     463                var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
     464                cmpProjectileManager.RemoveProjectile(data.projectileId);
     465            }
     466        }
     467    }
     468};
     469
     470Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire)
     471{
     472    var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     473    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     474    var owner = cmpOwnership.GetOwner();
     475    var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
     476    var numPlayers = cmpPlayerManager.GetNumPlayers();
     477    var players = [];
     478   
     479    for (var i = 1; i < numPlayers; ++i)
     480    {   
     481        // Only target enemies unless friendly fire is on
     482        if (cmpPlayer.IsEnemy(i) || friendlyFire)
     483            players.push(i);
     484    }
     485   
     486    var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     487    return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver);
     488}
     489
    298490/**
    299491 * Inflict damage on the target
    300492 */
     
    302494{
    303495    var strengths = this.GetAttackStrengths(data.type);
    304496   
    305     var attackBonus = this.GetAttackBonus(data.type, data.target);
     497    var damageMultiplier = this.GetAttackBonus(data.type, data.target);
     498    if (data.damageMultiplier !== undefined)
     499        damageMultiplier *= data.damageMultiplier;
    306500   
    307501    var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
    308502    if (!cmpDamageReceiver)
    309503        return;
    310     var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * attackBonus, strengths.pierce * attackBonus, strengths.crush * attackBonus);
     504    var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier);
    311505    // if target killed pick up loot and credit experience
    312506    if (targetState.killed == true)
    313507    {
     
    320514    PlaySound("attack_impact", this.entity);
    321515};
    322516
     517Attack.prototype.OnUpdate = function(msg)
     518{
     519    this.turnLength = msg.turnLength;
     520}
     521
    323522Engine.RegisterComponentType(IID_Attack, "Attack", Attack);