Ticket #18: advanced_attack-160312.diff

File advanced_attack-160312.diff, 35.0 KB (added by Jonathan Waller, 12 years ago)

New version with fixed square footprint bug

  • source/simulation2/docs/SimulationDocs.h

     
    395395The @c Init and @c Deinit functions are optional. Unlike C++, there are no @c Serialize/Deserialize functions -
    396396each JS component instance is automatically serialized and restored.
    397397(This automatic serialization restricts what you can store as properties in the object - e.g. you cannot store function closures,
    398 because they're too hard to serialize. The details should be documented on some other page eventually.)
     398because they're too hard to serialize. This will serialize Strings, numbers, bools, null, undefined, arrays of serializable
     399values whose property names are purely numeric, objects whose properties are serializable values.  Cyclic structures are allowed.)
    399400
    400401Instead of @c ClassInit and @c HandleMessage, you simply add functions of the form <code>On<var>MessageType</var></code>.
    401402(If you want the equivalent of SubscribeGloballyToMessageType, then use <code>OnGlobal<var>MessageType</var></code> instead.)
  • 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/2 <= dxr && dxr < targetShape.width/2 && -targetShape.depth/2 <= dzr && dzr < targetShape.depth/2);
     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);