Ticket #18: advanced_attack-160312.diff
File advanced_attack-160312.diff, 35.0 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/components/Attack.js
1 1 function Attack() {} 2 2 3 3 4 var bonusesSchema = 4 5 "<optional>" + 5 6 "<element name='Bonuses'>" + … … 48 49 "<PrepareTime>800</PrepareTime>" + 49 50 "<RepeatTime>1600</RepeatTime>" + 50 51 "<ProjectileSpeed>50.0</ProjectileSpeed>" + 52 "<Spread>2.5</Spread>" + 51 53 "<Bonuses>" + 52 54 "<Bonus1>" + 53 55 "<Classes>Cavalry</Classes>" + 54 56 "<Multiplier>2</Multiplier>" + 55 57 "</Bonus1>" + 56 58 "</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>" + 57 67 "</Ranged>" + 58 68 "<Charge>" + 59 69 "<Hack>10.0</Hack>" + … … 94 104 "<element name='ProjectileSpeed' a:help='Speed of projectiles (in metres per second). If unspecified, then it is a melee attack instead'>" + 95 105 "<ref name='nonNegativeDecimal'/>" + 96 106 "</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>" + 97 108 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>" + 98 122 "</interleave>" + 99 123 "</element>" + 100 124 "</optional>" + … … 143 167 144 168 Attack.prototype.GetAttackStrengths = function(type) 145 169 { 170 var template = this.template[type]; 171 if (!template) 172 template = this.template[type.split(".")[0]].Splash; 146 173 // Convert attack values to numbers, default 0 if unspecified 147 174 return { 148 hack: +(t his.template[type].Hack || 0),149 pierce: +(t his.template[type].Pierce || 0),150 crush: +(t his.template[type].Crush || 0)175 hack: +(template.Hack || 0), 176 pierce: +(template.Pierce || 0), 177 crush: +(template.Crush || 0) 151 178 }; 152 179 }; 153 180 … … 162 189 Attack.prototype.GetAttackBonus = function(type, target) 163 190 { 164 191 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) 166 197 { 167 198 var cmpIdentity = Engine.QueryInterface(target, IID_Identity); 168 199 if (!cmpIdentity) 169 200 return 1; 170 201 171 202 // Multiply the bonuses for all matching classes 172 for (var key in t his.template[type].Bonuses)203 for (var key in template.Bonuses) 173 204 { 174 var bonus = t his.template[type].Bonuses[key];205 var bonus = template.Bonuses[key]; 175 206 176 207 var hasClasses = true; 177 208 if (bonus.Classes){ … … 188 219 return attackBonus; 189 220 }; 190 221 222 // Returns a 2d random distribution scaled for a spread of scale 1. 223 // The current implementation is a 2d gaussian with sigma = 1 224 Attack.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 191 236 /** 192 237 * Attack the target entity. This should only be called after a successful range check, 193 238 * and should only be called after GetTimers().repeat msec has passed since the last … … 198 243 // If this is a ranged attack, then launch a projectile 199 244 if (type == "Ranged") 200 245 { 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: 212 247 // * Obstacles like trees could reduce the probability of the target being hit 213 248 // * 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 tweaks216 // * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just217 // hurt anybody near their landing point218 249 219 250 // Get some data about the entity 220 251 var horizSpeed = +this.template[type].ProjectileSpeed; 221 252 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 227 259 var selfPosition = Engine.QueryInterface(this.entity, IID_Position).GetPosition(); 228 260 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); 230 284 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 265 293 // Launch the graphical projectile 266 294 var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); 267 if (realTargetUnit)268 cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity);269 else270 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}); 271 299 } 272 300 else 273 301 { … … 295 323 } 296 324 }; 297 325 326 Attack.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 338 Attack.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 343 Attack.prototype.VectorDot = function(p1, p2) 344 { 345 return (p1.x * p2.x + p1.z * p2.z); 346 }; 347 348 Attack.prototype.VectorCross = function(p1, p2) 349 { 350 return (p1.x * p2.z - p1.z * p2.x); 351 }; 352 353 Attack.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 359 Attack.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 385 Attack.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 470 Attack.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 298 490 /** 299 491 * Inflict damage on the target 300 492 */ … … 302 494 { 303 495 var strengths = this.GetAttackStrengths(data.type); 304 496 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; 306 500 307 501 var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); 308 502 if (!cmpDamageReceiver) 309 503 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); 311 505 // if target killed pick up loot and credit experience 312 506 if (targetState.killed == true) 313 507 { … … 320 514 PlaySound("attack_impact", this.entity); 321 515 }; 322 516 517 Attack.prototype.OnUpdate = function(msg) 518 { 519 this.turnLength = msg.turnLength; 520 } 521 323 522 Engine.RegisterComponentType(IID_Attack, "Attack", Attack);