Ticket #18: advanced_attack-250412.diff
File advanced_attack-250412.diff, 39.3 KB (added by , 12 years ago) |
---|
-
source/simulation2/docs/SimulationDocs.h
395 395 The @c Init and @c Deinit functions are optional. Unlike C++, there are no @c Serialize/Deserialize functions - 396 396 each JS component instance is automatically serialized and restored. 397 397 (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.) 398 because they're too hard to serialize. This will serialize Strings, numbers, bools, null, undefined, arrays of serializable 399 values whose property names are purely numeric, objects whose properties are serializable values. Cyclic structures are allowed.) 399 400 400 401 Instead of @c ClassInit and @c HandleMessage, you simply add functions of the form <code>On<var>MessageType</var></code>. 401 402 (If you want the equivalent of SubscribeGloballyToMessageType, then use <code>OnGlobal<var>MessageType</var></code> instead.) -
source/simulation2/components/tests/test_RangeManager.h
50 50 virtual bool IsFloating() { return false; } 51 51 virtual CFixedVector3D GetPosition() { return CFixedVector3D(); } 52 52 virtual CFixedVector2D GetPosition2D() { return CFixedVector2D(); } 53 virtual CFixedVector3D GetPreviousPosition() { return CFixedVector3D(); } 54 virtual CFixedVector2D GetPreviousPosition2D() { return CFixedVector2D(); } 53 55 virtual void TurnTo(entity_angle_t UNUSED(y)) { } 54 56 virtual void SetYRotation(entity_angle_t UNUSED(y)) { } 55 57 virtual void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) { } -
source/simulation2/components/ICmpProjectileManager.h
31 31 class ICmpProjectileManager : public IComponent 32 32 { 33 33 public: 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 actor37 * @param target target entity; the projectile will automatically track the target to ensure it always hits precisely38 * @param speed horizontal speed in m/s39 * @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;42 34 43 35 /** 44 36 * Launch a projectile from entity @p source to point @p target. … … 46 38 * @param target target point 47 39 * @param speed horizontal speed in m/s 48 40 * @param gravity gravitational acceleration in m/s^2 (determines the height of the ballistic curve) 41 * @return id of the created projectile 49 42 */ 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; 51 50 52 51 DECLARE_INTERFACE_TYPE(ProjectileManager) 53 52 }; -
source/simulation2/components/ICmpProjectileManager.cpp
22 22 #include "simulation2/system/InterfaceScripted.h" 23 23 24 24 BEGIN_INTERFACE_WRAPPER(ProjectileManager) 25 DEFINE_INTERFACE_METHOD_4("LaunchProjectileAt Entity", 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)25 DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", uint32_t, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed) 26 DEFINE_INTERFACE_METHOD_1("RemoveProjectile", void, ICmpProjectileManager, RemoveProjectile, uint32_t) 27 27 END_INTERFACE_WRAPPER(ProjectileManager) -
source/simulation2/components/ICmpPosition.h
108 108 */ 109 109 virtual CFixedVector2D GetPosition2D() = 0; 110 110 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 111 124 /** 112 125 * Rotate smoothly to the given angle around the upwards axis. 113 126 * @param y clockwise radians from the +Z axis. -
source/simulation2/components/ICmpPosition.cpp
32 32 DEFINE_INTERFACE_METHOD_0("IsFloating", bool, ICmpPosition, IsFloating) 33 33 DEFINE_INTERFACE_METHOD_0("GetPosition", CFixedVector3D, ICmpPosition, GetPosition) 34 34 DEFINE_INTERFACE_METHOD_0("GetPosition2D", CFixedVector2D, ICmpPosition, GetPosition2D) 35 DEFINE_INTERFACE_METHOD_0("GetPreviousPosition", CFixedVector3D, ICmpPosition, GetPreviousPosition) 36 DEFINE_INTERFACE_METHOD_0("GetPreviousPosition2D", CFixedVector2D, ICmpPosition, GetPreviousPosition2D) 35 37 DEFINE_INTERFACE_METHOD_1("TurnTo", void, ICmpPosition, TurnTo, entity_angle_t) 36 38 DEFINE_INTERFACE_METHOD_1("SetYRotation", void, ICmpPosition, SetYRotation, entity_angle_t) 37 39 DEFINE_INTERFACE_METHOD_2("SetXZRotation", void, ICmpPosition, SetXZRotation, entity_angle_t, entity_angle_t) -
source/simulation2/components/CCmpProjectileManager.cpp
20 20 #include "simulation2/system/Component.h" 21 21 #include "ICmpProjectileManager.h" 22 22 23 #include "ICmpObstruction.h" 24 #include "ICmpObstructionManager.h" 23 25 #include "ICmpPosition.h" 24 26 #include "ICmpRangeManager.h" 25 27 #include "ICmpTerrain.h" … … 58 60 virtual void Init(const CParamNode& UNUSED(paramNode)) 59 61 { 60 62 m_ActorSeed = 0; 63 m_Id = 1; 61 64 } 62 65 63 66 virtual void Deinit() … … 86 89 case MT_Interpolate: 87 90 { 88 91 const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg); 89 Interpolate(msgData.frameTime , msgData.offset);92 Interpolate(msgData.frameTime); 90 93 break; 91 94 } 92 95 case MT_RenderSubmit: … … 98 101 } 99 102 } 100 103 101 virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_ttarget, fixed speed, fixed gravity)104 virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) 102 105 { 103 LaunchProjectile(source, CFixedVector3D(), target, speed, gravity);106 return LaunchProjectile(source, target, speed, gravity); 104 107 } 108 109 virtual void RemoveProjectile(uint32_t); 105 110 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 111 111 private: 112 112 struct Projectile 113 113 { 114 114 CUnit* unit; 115 115 CVector3D pos; 116 116 CVector3D target; 117 entity_id_t targetEnt; // INVALID_ENTITY if the target is just a point118 117 float timeLeft; 119 118 float speedFactor; 120 119 float gravity; 121 120 bool stopped; 121 uint32_t id; 122 122 }; 123 123 124 124 std::vector<Projectile> m_Projectiles; 125 125 126 126 uint32_t m_ActorSeed; 127 128 uint32_t m_Id; 127 129 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); 129 131 130 void AdvanceProjectile(Projectile& projectile, float dt , float frameOffset);132 void AdvanceProjectile(Projectile& projectile, float dt); 131 133 132 void Interpolate(float frameTime , float frameOffset);134 void Interpolate(float frameTime); 133 135 134 136 void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); 135 137 }; 136 138 137 139 REGISTER_COMPONENT_TYPE(ProjectileManager) 138 140 139 void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity)141 uint32_t CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity) 140 142 { 141 143 if (!GetSimContext().HasUnitManager()) 142 return ; // do nothing if graphics are disabled144 return 0; // do nothing if graphics are disabled 143 145 144 146 CmpPtr<ICmpVisual> cmpSourceVisual(GetSimContext(), source); 145 147 if (!cmpSourceVisual) 146 return ;148 return 0; 147 149 148 150 std::wstring name = cmpSourceVisual->GetProjectileActor(); 149 151 if (name.empty()) … … 151 153 // If the actor was actually loaded, complain that it doesn't have a projectile 152 154 if (!cmpSourceVisual->GetActorShortName().empty()) 153 155 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; 155 157 } 156 158 157 159 CVector3D sourceVec(cmpSourceVisual->GetProjectileLaunchPoint()); … … 161 163 162 164 CmpPtr<ICmpPosition> sourcePos(GetSimContext(), source); 163 165 if (!sourcePos) 164 return ;166 return 0; 165 167 166 168 sourceVec = sourcePos->GetPosition(); 167 169 sourceVec.Y += 3.f; … … 169 171 170 172 CVector3D targetVec; 171 173 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); 181 175 182 targetVec = CVector3D(cmpTargetPosition->GetPosition());183 }184 185 176 Projectile projectile; 186 177 std::set<CStr> selections; 187 178 projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections); 179 projectile.id = m_Id++; 188 180 if (!projectile.unit) 189 181 { 190 182 // The error will have already been logged 191 return ;183 return 0; 192 184 } 193 185 194 186 projectile.pos = sourceVec; 195 187 projectile.target = targetVec; 196 projectile.targetEnt = targetEnt;197 188 198 189 CVector3D offset = projectile.target - projectile.pos; 199 190 float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z); … … 205 196 projectile.gravity = gravity.ToFloat(); 206 197 207 198 m_Projectiles.push_back(projectile); 199 200 return projectile.id; 208 201 } 209 202 210 void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt , float frameOffset)203 void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt) 211 204 { 212 205 // Do special processing if we've already reached the target 213 206 if (projectile.timeLeft <= 0) … … 223 216 // apply a bit of drag to them 224 217 projectile.speedFactor *= powf(1.0f - 0.4f*projectile.speedFactor, dt); 225 218 } 226 else227 {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 target238 219 239 // TODO: if the unit is moving, we should probably aim a bit in front of it240 // so we don't have to curve so much just before reaching it241 }242 }243 }244 245 220 CVector3D offset = (projectile.target - projectile.pos) * projectile.speedFactor; 246 221 247 222 // Compute the vertical velocity that's needed so we travel in a ballistic curve and … … 296 271 projectile.unit->GetModel().SetTransform(transform); 297 272 } 298 273 299 void CCmpProjectileManager::Interpolate(float frameTime , float frameOffset)274 void CCmpProjectileManager::Interpolate(float frameTime) 300 275 { 301 276 for (size_t i = 0; i < m_Projectiles.size(); ++i) 302 277 { 303 AdvanceProjectile(m_Projectiles[i], frameTime , frameOffset);278 AdvanceProjectile(m_Projectiles[i], frameTime); 304 279 } 305 280 306 281 // Remove the ones that have reached their target … … 310 285 // Those hitting the ground stay for a while, because it looks pretty. 311 286 if (m_Projectiles[i].timeLeft <= 0.f) 312 287 { 313 if (m_Projectiles[i].t argetEnt == INVALID_ENTITY && m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)288 if (m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME) 314 289 { 315 290 // Keep the projectile until it exceeds the decay time 316 291 } … … 328 303 } 329 304 } 330 305 306 void 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 331 322 void CCmpProjectileManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) 332 323 { 333 324 CmpPtr<ICmpRangeManager> cmpRangeManager(GetSimContext(), SYSTEM_ENTITY); -
source/simulation2/components/CCmpPosition.cpp
66 66 // Dynamic state: 67 67 68 68 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 70 72 entity_pos_t m_YOffset; 71 73 bool m_RelativeToGround; // whether m_YOffset is relative to terrain/water plane, or an absolute height 72 74 … … 201 203 if (!m_InWorld) 202 204 { 203 205 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; 206 208 } 207 209 208 210 AdvertisePositionChanges(); … … 210 212 211 213 virtual void JumpTo(entity_pos_t x, entity_pos_t z) 212 214 { 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; 215 217 m_InWorld = true; 216 218 217 219 AdvertisePositionChanges(); … … 278 280 return CFixedVector2D(m_X, m_Z); 279 281 } 280 282 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 281 320 virtual void TurnTo(entity_angle_t y) 282 321 { 283 322 m_RotY = y; … … 408 447 } 409 448 case MT_TurnStart: 410 449 { 450 // Store the positions from the turn before 451 m_PrevX = m_LastX; 452 m_PrevZ = m_LastZ; 453 411 454 m_LastX = m_X; 412 455 m_LastZ = m_Z; 413 456 -
source/simulation2/components/CCmpObstructionManager.cpp
927 927 928 928 // Then look for obstructions that cover the target point when expanded by r 929 929 // (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 } 930 942 931 // TODO: actually do that932 // (This might matter when you tell a unit to walk too close to the edge of a building)933 934 943 return false; 935 944 } 936 945 -
binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_ballista.xml
2 2 <Entity parent="template_unit_mechanical_siege_onager"> 3 3 <Attack> 4 4 <Ranged> 5 <Hack> 60.0</Hack>6 <Crush> 60.0</Crush>5 <Hack>0.0</Hack> 6 <Crush>0.0</Crush> 7 7 <MaxRange>80</MaxRange> 8 8 <MinRange>16.0</MinRange> 9 9 <ProjectileSpeed>22.0</ProjectileSpeed> 10 10 <RepeatTime>8000</RepeatTime> 11 <Spread>20</Spread> 11 12 </Ranged> 12 13 </Attack> 13 14 <Cost> -
binaries/data/mods/public/simulation/templates/units/celt_cavalry_javelinist_e.xml
9 9 <Ranged> 10 10 <Pierce>35.0</Pierce> 11 11 <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> 12 21 </Ranged> 13 22 </Attack> 14 23 <Health> -
binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml
6 6 </Armour> 7 7 <Attack> 8 8 <Ranged> 9 <Hack> 50.0</Hack>9 <Hack>40.0</Hack> 10 10 <Pierce>0.0</Pierce> 11 <Crush> 50.0</Crush>11 <Crush>40.0</Crush> 12 12 <MaxRange>68</MaxRange> 13 13 <MinRange>12.0</MinRange> 14 14 <ProjectileSpeed>30.0</ProjectileSpeed> … … 20 20 <Multiplier>2.0</Multiplier> 21 21 </BonusStruct> 22 22 </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> 23 31 </Ranged> 24 32 </Attack> 25 33 <Cost> -
binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml
7 7 <Attack> 8 8 <Ranged> 9 9 <Hack>0.0</Hack> 10 <Pierce> 50.0</Pierce>11 <Crush> 50.0</Crush>10 <Pierce>20.0</Pierce> 11 <Crush>20.0</Crush> 12 12 <MaxRange>60</MaxRange> 13 13 <MinRange>8.0</MinRange> 14 <Spread>6.0</Spread> 14 15 <ProjectileSpeed>60.0</ProjectileSpeed> 15 16 <PrepareTime>5000</PrepareTime> 16 17 <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> 23 32 </Ranged> 24 33 </Attack> 25 34 <Cost> -
binaries/data/mods/public/simulation/components/Attack.js
48 48 "<PrepareTime>800</PrepareTime>" + 49 49 "<RepeatTime>1600</RepeatTime>" + 50 50 "<ProjectileSpeed>50.0</ProjectileSpeed>" + 51 "<Spread>2.5</Spread>" + 51 52 "<Bonuses>" + 52 53 "<Bonus1>" + 53 54 "<Classes>Cavalry</Classes>" + 54 55 "<Multiplier>2</Multiplier>" + 55 56 "</Bonus1>" + 56 57 "</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>" + 57 66 "</Ranged>" + 58 67 "<Charge>" + 59 68 "<Hack>10.0</Hack>" + … … 94 103 "<element name='ProjectileSpeed' a:help='Speed of projectiles (in metres per second). If unspecified, then it is a melee attack instead'>" + 95 104 "<ref name='nonNegativeDecimal'/>" + 96 105 "</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>" + 97 107 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>" + 98 121 "</interleave>" + 99 122 "</element>" + 100 123 "</optional>" + … … 149 172 // Work out the attack values with technology effects 150 173 var self = this; 151 174 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 152 183 var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager); 153 184 var applyTechs = function(damageType) 154 185 { 155 186 // 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); 158 189 }; 159 190 160 191 return { … … 178 209 Attack.prototype.GetAttackBonus = function(type, target) 179 210 { 180 211 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) 182 217 { 183 218 var cmpIdentity = Engine.QueryInterface(target, IID_Identity); 184 219 if (!cmpIdentity) 185 220 return 1; 186 221 187 222 // Multiply the bonuses for all matching classes 188 for (var key in t his.template[type].Bonuses)223 for (var key in template.Bonuses) 189 224 { 190 var bonus = t his.template[type].Bonuses[key];225 var bonus = template.Bonuses[key]; 191 226 192 227 var hasClasses = true; 193 228 if (bonus.Classes){ … … 204 239 return attackBonus; 205 240 }; 206 241 242 // Returns a 2d random distribution scaled for a spread of scale 1. 243 // The current implementation is a 2d gaussian with sigma = 1 244 Attack.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 207 256 /** 208 257 * Attack the target entity. This should only be called after a successful range check, 209 258 * and should only be called after GetTimers().repeat msec has passed since the last … … 214 263 // If this is a ranged attack, then launch a projectile 215 264 if (type == "Ranged") 216 265 { 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: 228 267 // * Obstacles like trees could reduce the probability of the target being hit 229 268 // * 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 tweaks232 // * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just233 // hurt anybody near their landing point234 269 235 270 // Get some data about the entity 236 271 var horizSpeed = +this.template[type].ProjectileSpeed; 237 272 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 243 279 var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); 244 280 if (!cmpPosition || !cmpPosition.IsInWorld()) 245 281 return; … … 248 284 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) 249 285 return; 250 286 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); 252 311 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 287 320 // Launch the graphical projectile 288 321 var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); 289 if (realTargetUnit)290 cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity);291 else292 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}); 293 326 } 294 327 else 295 328 { … … 317 350 } 318 351 }; 319 352 353 Attack.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 365 Attack.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 370 Attack.prototype.VectorDot = function(p1, p2) 371 { 372 return (p1.x * p2.x + p1.z * p2.z); 373 }; 374 375 Attack.prototype.VectorCross = function(p1, p2) 376 { 377 return (p1.x * p2.z - p1.z * p2.x); 378 }; 379 380 Attack.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 386 Attack.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 412 Attack.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 497 Attack.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 320 517 /** 321 518 * Inflict damage on the target 322 519 */ … … 324 521 { 325 522 var strengths = this.GetAttackStrengths(data.type); 326 523 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; 328 527 329 528 var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); 330 529 if (!cmpDamageReceiver) 331 530 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); 333 532 // if target killed pick up loot and credit experience 334 533 if (targetState.killed == true) 335 534 { … … 342 541 PlaySound("attack_impact", this.entity); 343 542 }; 344 543 544 Attack.prototype.OnUpdate = function(msg) 545 { 546 this.turnLength = msg.turnLength; 547 } 548 345 549 Engine.RegisterComponentType(IID_Attack, "Attack", Attack);