Ticket #18: advanced_attack-160312.diff

File advanced_attack-160312.diff, 35.0 KB (added by quantumstate, 15 months 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);