Ticket #18: advanced_attack-250412.diff

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

updated to latest svn

  • 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/templates/units/rome_mechanical_siege_ballista.xml

     
    22<Entity parent="template_unit_mechanical_siege_onager">
    33  <Attack>
    44    <Ranged>
    5       <Hack>60.0</Hack>
    6       <Crush>60.0</Crush>
     5      <Hack>0.0</Hack>
     6      <Crush>0.0</Crush>
    77      <MaxRange>80</MaxRange>
    88      <MinRange>16.0</MinRange>
    99      <ProjectileSpeed>22.0</ProjectileSpeed>
    1010      <RepeatTime>8000</RepeatTime>
     11      <Spread>20</Spread>
    1112    </Ranged>
    1213  </Attack>
    1314  <Cost>
  • binaries/data/mods/public/simulation/templates/units/celt_cavalry_javelinist_e.xml

     
    99    <Ranged>
    1010      <Pierce>35.0</Pierce>
    1111      <MaxRange>52</MaxRange>
     12      <Spread>2</Spread>
     13        <Splash>
     14            <Shape>Circular</Shape>
     15            <Range>20</Range>
     16            <FriendlyFire>false</FriendlyFire>
     17            <Hack>0.0</Hack>
     18            <Pierce>10.0</Pierce>
     19            <Crush>0.0</Crush>
     20        </Splash>
    1221    </Ranged>
    1322  </Attack>
    1423  <Health>
  • binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml

     
    66  </Armour>
    77  <Attack>
    88    <Ranged>
    9       <Hack>50.0</Hack>
     9      <Hack>40.0</Hack>
    1010      <Pierce>0.0</Pierce>
    11       <Crush>50.0</Crush>
     11      <Crush>40.0</Crush>
    1212      <MaxRange>68</MaxRange>
    1313      <MinRange>12.0</MinRange>
    1414      <ProjectileSpeed>30.0</ProjectileSpeed>
     
    2020          <Multiplier>2.0</Multiplier>
    2121        </BonusStruct>
    2222      </Bonuses>
     23      <Splash>
     24        <Shape>Circular</Shape>
     25        <Range>12</Range>
     26        <FriendlyFire>true</FriendlyFire>
     27        <Hack>12.0</Hack>
     28        <Pierce>0.0</Pierce>
     29        <Crush>12.0</Crush>
     30      </Splash>
    2331    </Ranged>
    2432  </Attack>
    2533  <Cost>
  • binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml

     
    77  <Attack>
    88    <Ranged>
    99      <Hack>0.0</Hack>
    10       <Pierce>50.0</Pierce>
    11       <Crush>50.0</Crush>
     10      <Pierce>20.0</Pierce>
     11      <Crush>20.0</Crush>
    1212      <MaxRange>60</MaxRange>
    1313      <MinRange>8.0</MinRange>
     14      <Spread>6.0</Spread>
    1415      <ProjectileSpeed>60.0</ProjectileSpeed>
    1516      <PrepareTime>5000</PrepareTime>
    1617      <RepeatTime>5000</RepeatTime>
    17       <Bonuses>
    18         <BonusOrganic>
    19           <Classes>Organic</Classes>
    20           <Multiplier>2.0</Multiplier>
    21         </BonusOrganic>
    22       </Bonuses>
     18      <Bonuses>
     19        <BonusOrganic>
     20        <Classes>Organic</Classes>
     21        <Multiplier>2.0</Multiplier>
     22        </BonusOrganic>
     23      </Bonuses>
     24      <Splash>
     25        <Shape>Linear</Shape>
     26        <Range>12</Range>
     27        <FriendlyFire>false</FriendlyFire>
     28        <Hack>0.0</Hack>
     29        <Pierce>30.0</Pierce>
     30        <Crush>30.0</Crush>
     31      </Splash>
    2332    </Ranged>
    2433  </Attack>
    2534  <Cost>
  • binaries/data/mods/public/simulation/components/Attack.js

     
    4848            "<PrepareTime>800</PrepareTime>" +
    4949            "<RepeatTime>1600</RepeatTime>" +
    5050            "<ProjectileSpeed>50.0</ProjectileSpeed>" +
     51            "<Spread>2.5</Spread>" +
    5152            "<Bonuses>" +
    5253                "<Bonus1>" +
    5354                    "<Classes>Cavalry</Classes>" +
    5455                    "<Multiplier>2</Multiplier>" +
    5556                "</Bonus1>" +
    5657            "</Bonuses>" +
     58            "<Splash>" +
     59                "<Shape>Circular</Shape>" +
     60                "<Range>20</Range>" +
     61                "<FriendlyFire>false</FriendlyFire>" +
     62                "<Hack>0.0</Hack>" +
     63                "<Pierce>10.0</Pierce>" +
     64                "<Crush>0.0</Crush>" +
     65            "</Splash>" +
    5766        "</Ranged>" +
    5867        "<Charge>" +
    5968            "<Hack>10.0</Hack>" +
     
    94103                "<element name='ProjectileSpeed' a:help='Speed of projectiles (in metres per second). If unspecified, then it is a melee attack instead'>" +
    95104                    "<ref name='nonNegativeDecimal'/>" +
    96105                "</element>" +
     106                "<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>" +
    97107                bonusesSchema +
     108                "<optional>" +
     109                    "<element name='Splash'>" +
     110                        "<interleave>" +
     111                            "<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
     112                            "<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
     113                            "<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
     114                            "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
     115                            "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
     116                            "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
     117                            bonusesSchema +
     118                        "</interleave>" +
     119                    "</element>" +
     120                "</optional>" +
    98121            "</interleave>" +
    99122        "</element>" +
    100123    "</optional>" +
     
    149172    // Work out the attack values with technology effects
    150173    var self = this;
    151174   
     175    var template = this.template[type];
     176    var splash = "";
     177    if (!template)
     178    {
     179        template = this.template[type.split(".")[0]].Splash;
     180        splash = "/Splash";
     181    }
     182   
    152183    var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager);
    153184    var applyTechs = function(damageType)
    154185    {
    155186        // All causes caching problems so disable it for now.
    156         //var allComponent = cmpTechMan.ApplyModifications("Attack/" + type + "/All", (+self.template[type][damageType] || 0), self.entity) - self.template[type][damageType];
    157         return cmpTechMan.ApplyModifications("Attack/" + type + "/" + damageType, (+self.template[type][damageType] || 0), self.entity);
     187        //var allComponent = cmpTechMan.ApplyModifications("Attack/" + type + splash + "/All", (+template[damageType] || 0), self.entity) - template[damageType];
     188        return cmpTechMan.ApplyModifications("Attack/" + type + splash + "/" + damageType, (+template[damageType] || 0), self.entity);
    158189    };
    159190   
    160191    return {
     
    178209Attack.prototype.GetAttackBonus = function(type, target)
    179210{
    180211    var attackBonus = 1;
    181     if (this.template[type].Bonuses)
     212    var template = this.template[type];
     213    if (!template)
     214        template = this.template[type.split(".")[0]].Splash;
     215   
     216    if (template.Bonuses)
    182217    {
    183218        var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    184219        if (!cmpIdentity)
    185220            return 1;
    186221       
    187222        // Multiply the bonuses for all matching classes
    188         for (var key in this.template[type].Bonuses)
     223        for (var key in template.Bonuses)
    189224        {
    190             var bonus = this.template[type].Bonuses[key];
     225            var bonus = template.Bonuses[key];
    191226           
    192227            var hasClasses = true;
    193228            if (bonus.Classes){
     
    204239    return attackBonus;
    205240};
    206241
     242// Returns a 2d random distribution scaled for a spread of scale 1.
     243// The current implementation is a 2d gaussian with sigma = 1
     244Attack.prototype.GetNormalizedDistribution = function(){
     245   
     246    // Use the Box-Muller transform to get a gaussian distribution
     247    var a = Math.random();
     248    var b = Math.random();
     249   
     250    var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b);
     251    var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b);
     252   
     253    return [c, d];
     254};
     255
    207256/**
    208257 * Attack the target entity. This should only be called after a successful range check,
    209258 * and should only be called after GetTimers().repeat msec has passed since the last
     
    214263    // If this is a ranged attack, then launch a projectile
    215264    if (type == "Ranged")
    216265    {
    217         // To implement (in)accuracy, for arrows and javelins, we want to do the following:
    218         //  * Compute an accuracy rating, based on the entity's characteristics and the distance to the target
    219         //  * Pick a random point 'close' to the target (based on the accuracy) which is the real target point
    220         //  * Pick a real target unit, based on their footprint's proximity to the real target point
    221         //  * If there is none, then harmlessly shoot to the real target point instead
    222         //  * If the real target unit moves after being targeted, the projectile will follow it and hit it anyway
    223         //
    224         // In the future this should be extended:
    225         //  * If the target unit moves too far, the projectile should 'detach' and not hit it, so that
    226         //    players can dodge projectiles. (Or it should pick a new target after detaching, so it can still
    227         //    hit somebody.)
     266        // In the future this could be extended:
    228267        //  * Obstacles like trees could reduce the probability of the target being hit
    229268        //  * Obstacles like walls should block projectiles entirely
    230         //  * There should be more control over the probabilities of hitting enemy units vs friendly units vs missing,
    231         //    for gameplay balance tweaks
    232         //  * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just
    233         //    hurt anybody near their landing point
    234269
    235270        // Get some data about the entity
    236271        var horizSpeed = +this.template[type].ProjectileSpeed;
    237272        var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now
    238         var accuracy = 6; // TODO: get from entity template
    239 
    240         //horizSpeed /= 8; gravity /= 8; // slow it down for testing
    241 
    242         // Find the distance to the target
     273        var spread = 1.5; //
     274        if (this.template.Ranged.Spread !== undefined) // explicit test here because the value might be 0
     275            spread = this.template.Ranged.Spread;
     276       
     277        //horizSpeed /= 2; gravity /= 2; // slow it down for testing
     278       
    243279        var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
    244280        if (!cmpPosition || !cmpPosition.IsInWorld())
    245281            return;
     
    248284        if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
    249285            return;
    250286        var targetPosition = cmpTargetPosition.GetPosition();
    251         var horizDistance = Math.sqrt(Math.pow(targetPosition.x - selfPosition.x, 2) + Math.pow(targetPosition.z - selfPosition.z, 2));
     287       
     288        var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z}
     289        var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
     290       
     291        var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength}
     292        // the component of the targets velocity radially away from the archer
     293        var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition);
     294       
     295        var horizDistance = this.VectorDistance(targetPosition, selfPosition);
     296       
     297        // This is an approximation of the time ot the target, it assumes that the target has a constant radial
     298        // velocity, but since units move in straight lines this is not true.  The exact value would be more
     299        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
     300        // about 5% of the units radius out in the worst case)
     301        var timeToTarget = horizDistance / (horizSpeed - radialSpeed);
     302       
     303        // Predict where the unit is when the missile lands.
     304        var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget,
     305                                 "z": targetPosition.z + targetVelocity.z * timeToTarget};
     306       
     307        // Compute the real target point (based on spread and target speed)
     308        var randNorm = this.GetNormalizedDistribution();
     309        var offsetX = randNorm[0] * spread * (1 + this.VectorLength(targetVelocity) / 20);
     310        var offsetZ = randNorm[1] * spread * (1 + this.VectorLength(targetVelocity) / 20);
    252311
    253         // Compute the real target point (based on accuracy)
    254         var angle = Math.random() * 2*Math.PI;
    255         var r = 1 - Math.sqrt(Math.random()); // triangular distribution [0,1] (cluster around the center)
    256         var offset = r * accuracy; // TODO: should be affected by range
    257         var offsetX = offset * Math.sin(angle);
    258         var offsetZ = offset * Math.cos(angle);
    259 
    260         var realTargetPosition = { "x": targetPosition.x + offsetX, "y": targetPosition.y, "z": targetPosition.z + offsetZ };
    261 
    262         // TODO: what we should really do here is select the unit whose footprint is closest to the realTargetPosition
    263         // (and harmlessly hit the ground if there's none), but as a simplification let's just randomly decide whether to
    264         // hit the original target or not.
    265         var realTargetUnit = undefined;
    266         if (Math.random() < 0.5) // TODO: this is yucky and hardcoded
    267         {
    268             // Hit the original target
    269             realTargetUnit = target;
    270             realTargetPosition = targetPosition;
    271         }
    272         else
    273         {
    274             // Hit the ground
    275             // TODO: ought to make sure Y is on the ground
    276         }
    277 
    278         // Hurt the target after the appropriate time
    279         if (realTargetUnit)
    280         {
    281             var realHorizDistance = Math.sqrt(Math.pow(realTargetPosition.x - selfPosition.x, 2) + Math.pow(realTargetPosition.z - selfPosition.z, 2));
    282             var timeToTarget = realHorizDistance / horizSpeed;
    283             var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    284             cmpTimer.SetTimeout(this.entity, IID_Attack, "CauseDamage", timeToTarget*1000, {"type": type, "target": target});
    285         }
    286 
     312        var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ };
     313       
     314        // Calculate when the missile will hit the target position
     315        var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition);
     316        var timeToTarget = realHorizDistance / horizSpeed;
     317       
     318        var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance};
     319       
    287320        // Launch the graphical projectile
    288321        var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
    289         if (realTargetUnit)
    290             cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity);
    291         else
    292             cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
     322        var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
     323       
     324        var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     325        cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id});
    293326    }
    294327    else
    295328    {
     
    317350    }
    318351};
    319352
     353Attack.prototype.InterpolatedLocation = function(ent, lateness)
     354{
     355    var targetPositionCmp = Engine.QueryInterface(ent, IID_Position);
     356    if (!targetPositionCmp) // TODO: handle dead target properly
     357        return undefined;
     358    var curPos = targetPositionCmp.GetPosition();
     359    var prevPos = targetPositionCmp.GetPreviousPosition();
     360    lateness /= 1000;
     361    return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength,
     362            "z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength};
     363};
     364
     365Attack.prototype.VectorDistance = function(p1, p2)
     366{
     367    return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z));
     368};
     369
     370Attack.prototype.VectorDot = function(p1, p2)
     371{
     372    return (p1.x * p2.x + p1.z * p2.z);
     373};
     374
     375Attack.prototype.VectorCross = function(p1, p2)
     376{
     377    return (p1.x * p2.z - p1.z * p2.x);
     378};
     379
     380Attack.prototype.VectorLength = function(p)
     381{
     382    return Math.sqrt(p.x*p.x + p.z*p.z);
     383};
     384
     385// Tests whether it point is inside of ent's footprint
     386Attack.prototype.testCollision = function(ent, point, lateness)
     387{
     388    var targetPosition = this.InterpolatedLocation(ent, lateness);
     389    var targetShape = Engine.QueryInterface(ent, IID_Footprint).GetShape();
     390   
     391    if (!targetShape || !targetPosition)
     392        return false;
     393   
     394    if (targetShape.type === 'circle')
     395    {
     396        return (this.VectorDistance(point, targetPosition) < targetShape.radius);
     397    }
     398    else
     399    {
     400        var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
     401       
     402        var dx = point.x - targetPosition.x;
     403        var dz = point.z - targetPosition.z;
     404       
     405        var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz;
     406        var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz;
     407       
     408        return (-targetShape.width/2 <= dxr && dxr < targetShape.width/2 && -targetShape.depth/2 <= dzr && dzr < targetShape.depth/2);
     409    }
     410};
     411
     412Attack.prototype.MissileHit = function(data, lateness)
     413{
     414   
     415    var targetPosition = this.InterpolatedLocation(data.target, lateness);
     416    if (!targetPosition)
     417        return;
     418   
     419    if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target
     420    {
     421        var friendlyFire = this.template.Ranged.Splash.FriendlyFire;
     422        var splashRadius = this.template.Ranged.Splash.Range;
     423        var splashShape = this.template.Ranged.Splash.Shape;
     424       
     425        var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire);
     426        ents.push(data.target); // Add the original unit to the list of splash damage targets
     427       
     428        for (var i = 0; i < ents.length; i++)
     429        {
     430            var entityPosition = this.InterpolatedLocation(ents[i], lateness);
     431            var radius = this.VectorDistance(data.position, entityPosition);
     432           
     433            if (radius < splashRadius)
     434            {
     435                var multiplier = 1;
     436                if (splashShape == "Circular") // quadratic falloff
     437                {
     438                    multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius));
     439                }
     440                else if (splashShape == "Linear")
     441                {
     442                    // position of entity relative to where the missile hit
     443                    var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z};
     444                   
     445                    var splashWidth = splashRadius / 5;
     446                    var parallelDist = this.VectorDot(relPos, data.direction);
     447                    var perpDist = Math.abs(this.VectorCross(relPos, data.direction));
     448                   
     449                    // Check that the unit is within the distance splashWidth of the line starting at the missile's
     450                    // landing point which extends in the direction of the missile for length splashRadius.
     451                    if (parallelDist > -splashWidth && perpDist < splashWidth)
     452                    {
     453                        // Use a quadratic falloff in both directions
     454                        multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius)
     455                                     * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth);
     456                    }
     457                    else
     458                    {
     459                        multiplier = 0;
     460                    }
     461                }
     462                var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier};
     463                this.CauseDamage(newData);
     464            }
     465        }
     466    }
     467   
     468    if (this.testCollision(data.target, data.position, lateness))
     469    {
     470        // Hit the primary target
     471        this.CauseDamage(data);
     472       
     473        // Remove the projectile
     474        var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
     475        cmpProjectileManager.RemoveProjectile(data.projectileId);
     476    }
     477    else
     478    {
     479        // If we didn't hit the main target look for nearby units
     480        var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2);
     481       
     482        for (var i = 0; i < ents.length; i++)
     483        {
     484            if (this.testCollision(ents[i], data.position, lateness))
     485            {
     486                var newData = {"type": data.type, "target": ents[i]};
     487                this.CauseDamage(newData);
     488               
     489                // Remove the projectile
     490                var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
     491                cmpProjectileManager.RemoveProjectile(data.projectileId);
     492            }
     493        }
     494    }
     495};
     496
     497Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire)
     498{
     499    var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     500    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     501    var owner = cmpOwnership.GetOwner();
     502    var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
     503    var numPlayers = cmpPlayerManager.GetNumPlayers();
     504    var players = [];
     505   
     506    for (var i = 1; i < numPlayers; ++i)
     507    {   
     508        // Only target enemies unless friendly fire is on
     509        if (cmpPlayer.IsEnemy(i) || friendlyFire)
     510            players.push(i);
     511    }
     512   
     513    var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     514    return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver);
     515}
     516
    320517/**
    321518 * Inflict damage on the target
    322519 */
     
    324521{
    325522    var strengths = this.GetAttackStrengths(data.type);
    326523   
    327     var attackBonus = this.GetAttackBonus(data.type, data.target);
     524    var damageMultiplier = this.GetAttackBonus(data.type, data.target);
     525    if (data.damageMultiplier !== undefined)
     526        damageMultiplier *= data.damageMultiplier;
    328527   
    329528    var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
    330529    if (!cmpDamageReceiver)
    331530        return;
    332     var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * attackBonus, strengths.pierce * attackBonus, strengths.crush * attackBonus);
     531    var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier);
    333532    // if target killed pick up loot and credit experience
    334533    if (targetState.killed == true)
    335534    {
     
    342541    PlaySound("attack_impact", this.entity);
    343542};
    344543
     544Attack.prototype.OnUpdate = function(msg)
     545{
     546    this.turnLength = msg.turnLength;
     547}
     548
    345549Engine.RegisterComponentType(IID_Attack, "Attack", Attack);