Ticket #18: advanced_attack.diff
File advanced_attack.diff, 31.4 KB (added by , 12 years ago) |
---|
-
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. -
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("LaunchProjectileAtEntity", void, ICmpProjectileManager, LaunchProjectileAtEntity, entity_id_t, entity_id_t, fixed, fixed)26 25 DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", void, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed) 27 26 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" … … 86 88 case MT_Interpolate: 87 89 { 88 90 const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg); 89 Interpolate(msgData.frameTime , msgData.offset);91 Interpolate(msgData.frameTime); 90 92 break; 91 93 } 92 94 case MT_RenderSubmit: … … 98 100 } 99 101 } 100 102 101 virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, fixed speed, fixed gravity)102 {103 LaunchProjectile(source, CFixedVector3D(), target, speed, gravity);104 }105 106 103 virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) 107 104 { 108 LaunchProjectile(source, target, INVALID_ENTITY,speed, gravity);105 LaunchProjectile(source, target, speed, gravity); 109 106 } 110 107 111 108 private: … … 114 111 CUnit* unit; 115 112 CVector3D pos; 116 113 CVector3D target; 117 entity_id_t targetEnt; // INVALID_ENTITY if the target is just a point118 114 float timeLeft; 119 115 float speedFactor; 120 116 float gravity; … … 125 121 126 122 uint32_t m_ActorSeed; 127 123 128 void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt,fixed speed, fixed gravity);124 void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity); 129 125 130 void AdvanceProjectile(Projectile& projectile, float dt , float frameOffset);126 void AdvanceProjectile(Projectile& projectile, float dt); 131 127 132 void Interpolate(float frameTime , float frameOffset);128 void Interpolate(float frameTime); 133 129 134 130 void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); 135 131 }; 136 132 137 133 REGISTER_COMPONENT_TYPE(ProjectileManager) 138 134 139 void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt,fixed speed, fixed gravity)135 void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity) 140 136 { 141 137 if (!GetSimContext().HasUnitManager()) 142 138 return; // do nothing if graphics are disabled … … 169 165 170 166 CVector3D targetVec; 171 167 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; 168 targetVec = CVector3D(targetPoint); 181 169 182 targetVec = CVector3D(cmpTargetPosition->GetPosition());183 }184 185 170 Projectile projectile; 186 171 std::set<CStr> selections; 187 172 projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections); … … 193 178 194 179 projectile.pos = sourceVec; 195 180 projectile.target = targetVec; 196 projectile.targetEnt = targetEnt;197 181 198 182 CVector3D offset = projectile.target - projectile.pos; 199 183 float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z); … … 207 191 m_Projectiles.push_back(projectile); 208 192 } 209 193 210 void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt , float frameOffset)194 void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt) 211 195 { 212 196 // Do special processing if we've already reached the target 213 197 if (projectile.timeLeft <= 0) … … 223 207 // apply a bit of drag to them 224 208 projectile.speedFactor *= powf(1.0f - 0.4f*projectile.speedFactor, dt); 225 209 } 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 210 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 211 CVector3D offset = (projectile.target - projectile.pos) * projectile.speedFactor; 246 212 247 213 // Compute the vertical velocity that's needed so we travel in a ballistic curve and … … 267 233 { 268 234 projectile.pos.Y = h; // stick precisely to the terrain 269 235 projectile.stopped = true; 236 237 // See if the missile hit anything. If it did then get rid of it. 238 CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); 239 if (!cmpObstructionManager) 240 return; 241 242 ICmpObstructionManager::ObstructionSquare obstruction; 243 NullObstructionFilter filter; 244 245 // This checks the current position with 0.5m of padding around it because that looks about right 246 if (cmpObstructionManager->FindMostImportantObstruction(filter, entity_pos_t::FromFloat(projectile.pos.X), 247 entity_pos_t::FromFloat(projectile.pos.Z), 248 entity_pos_t::FromFloat(0.5), obstruction)) 249 { 250 projectile.timeLeft = -PROJECTILE_DECAY_TIME; // Make the projectile get destroyed on the next check 251 } 270 252 } 271 253 } 272 254 } … … 296 278 projectile.unit->GetModel().SetTransform(transform); 297 279 } 298 280 299 void CCmpProjectileManager::Interpolate(float frameTime , float frameOffset)281 void CCmpProjectileManager::Interpolate(float frameTime) 300 282 { 301 283 for (size_t i = 0; i < m_Projectiles.size(); ++i) 302 284 { 303 AdvanceProjectile(m_Projectiles[i], frameTime , frameOffset);285 AdvanceProjectile(m_Projectiles[i], frameTime); 304 286 } 305 287 306 288 // Remove the ones that have reached their target … … 310 292 // Those hitting the ground stay for a while, because it looks pretty. 311 293 if (m_Projectiles[i].timeLeft <= 0.f) 312 294 { 313 if (m_Projectiles[i].t argetEnt == INVALID_ENTITY && m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)295 if (m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME) 314 296 { 315 297 // Keep the projectile until it exceeds the decay time 316 298 } -
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 const TYPICAL_RANGE = 40; 258 //horizSpeed /= 2; gravity /= 2; // slow it down for testing 259 227 260 var selfPosition = Engine.QueryInterface(this.entity, IID_Position).GetPosition(); 228 261 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)); 230 262 var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z} 263 var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); 264 265 var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength} 266 // the component of the targets velocity radially away from the archer 267 var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition); 268 269 var horizDistance = this.VectorDistance(targetPosition, selfPosition); 270 271 // This is an approximation of the time ot the target, it assumes that the target has a constant radial 272 // velocity, but since units move in straight lines this is not true. The exact value would be more 273 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was 274 // about 5% of the units radius out in the worst case) 275 var timeToTarget = horizDistance / (horizSpeed - radialSpeed); 276 277 // Predict where the unit is when the missile lands. 278 var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget, 279 "z": targetPosition.z + targetVelocity.z * timeToTarget}; 280 231 281 // 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); 282 var randNorm = this.GetNormalizedDistribution(); 283 var offsetX = randNorm[0] * spread * horizDistance / TYPICAL_RANGE; 284 var offsetZ = randNorm[1] * spread * horizDistance / TYPICAL_RANGE; 237 285 238 var realTargetPosition = { "x": targetPosition.x + offsetX, "y": targetPosition.y, "z": targetPosition.z + offsetZ }; 286 var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ }; 287 288 // Calculate when the missile will hit the target position 289 var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition); 290 var timeToTarget = realHorizDistance / horizSpeed; 291 292 var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance}; 293 294 var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 295 cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection}); 239 296 240 // TODO: what we should really do here is select the unit whose footprint is closest to the realTargetPosition241 // (and harmlessly hit the ground if there's none), but as a simplification let's just randomly decide whether to242 // hit the original target or not.243 var realTargetUnit = undefined;244 if (Math.random() < 0.5) // TODO: this is yucky and hardcoded245 {246 // Hit the original target247 realTargetUnit = target;248 realTargetPosition = targetPosition;249 }250 else251 {252 // Hit the ground253 // TODO: ought to make sure Y is on the ground254 }255 256 // Hurt the target after the appropriate time257 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 265 297 // Launch the graphical projectile 266 298 var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); 267 if (realTargetUnit) 268 cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity); 269 else 270 cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); 299 cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); 271 300 } 272 301 else 273 302 { … … 295 324 } 296 325 }; 297 326 327 Attack.prototype.InterpolatedLocation = function(ent, lateness) 328 { 329 var targetPositionCmp = Engine.QueryInterface(ent, IID_Position); 330 if (!targetPositionCmp) // TODO: handle dead target properly 331 return undefined; 332 var curPos = targetPositionCmp.GetPosition(); 333 var prevPos = targetPositionCmp.GetPreviousPosition(); 334 lateness /= 1000; 335 return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength, 336 "z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength}; 337 }; 338 339 Attack.prototype.VectorDistance = function(p1, p2) 340 { 341 return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z)); 342 }; 343 344 Attack.prototype.VectorDot = function(p1, p2) 345 { 346 return (p1.x * p2.x + p1.z * p2.z); 347 }; 348 349 Attack.prototype.VectorCross = function(p1, p2) 350 { 351 return (p1.x * p2.z - p1.z * p2.x); 352 }; 353 354 Attack.prototype.VectorLength = function(p) 355 { 356 return Math.sqrt(p.x*p.x + p.z*p.z); 357 }; 358 359 // Tests whether it point is inside of ent's footprint 360 Attack.prototype.testCollision = function(ent, point, lateness) 361 { 362 var targetPosition = this.InterpolatedLocation(ent, lateness); 363 var targetShape = Engine.QueryInterface(ent, IID_Footprint).GetShape(); 364 365 if (!targetShape || !targetPosition) 366 return false; 367 368 if (targetShape.type === 'circle') 369 { 370 return (this.VectorDistance(point, targetPosition) < targetShape.radius); 371 } 372 else 373 { 374 var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y; 375 376 var dx = point.x - targetPosition.x; 377 var dz = point.z - targetPosition.z; 378 379 var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz; 380 var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz; 381 382 return (-targetShape.width <= dxr && dxr < targetShape.width && -targetShape.depth <= dzr && dzr < targetShape.depth); 383 } 384 }; 385 386 Attack.prototype.MissileHit = function(data, lateness) 387 { 388 389 var targetPosition = this.InterpolatedLocation(data.target, lateness); 390 if (!targetPosition) 391 return; 392 393 if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target 394 { 395 var friendlyFire = this.template.Ranged.Splash.FriendlyFire; 396 var splashRadius = this.template.Ranged.Splash.Range; 397 var splashShape = this.template.Ranged.Splash.Shape; 398 399 var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire); 400 ents.push(data.target); // Add the original unit to the list of splash damage targets 401 402 for (var i = 0; i < ents.length; i++) 403 { 404 var entityPosition = this.InterpolatedLocation(ents[i], lateness); 405 var radius = this.VectorDistance(data.position, entityPosition); 406 407 if (radius < splashRadius) 408 { 409 var multiplier = 1; 410 if (splashShape == "Circular") // quadratic falloff 411 { 412 multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius)); 413 } 414 else if (splashShape == "Linear") 415 { 416 // position of entity relative to where the missile hit 417 var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z}; 418 419 var splashWidth = splashRadius / 5; 420 var parallelDist = this.VectorDot(relPos, data.direction); 421 var perpDist = Math.abs(this.VectorCross(relPos, data.direction)); 422 423 // Check that the unit is within the distance splashWidth of the line starting at the missile's 424 // landing point which extends in the direction of the missile for length splashRadius. 425 if (parallelDist > -splashWidth && perpDist < splashWidth) 426 { 427 // Use a quadratic falloff in both directions 428 multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius) 429 * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth); 430 } 431 else 432 { 433 multiplier = 0; 434 } 435 } 436 var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier}; 437 this.CauseDamage(newData); 438 } 439 } 440 } 441 442 if (this.testCollision(data.target, data.position, lateness)) 443 { 444 // Hit the primary target 445 this.CauseDamage(data); 446 } 447 else 448 { 449 // If we didn't hit the main target look for nearby units 450 var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2); 451 452 for (var i = 0; i < ents.length; i++) 453 { 454 if (this.testCollision(ents[i], data.position, lateness)) 455 { 456 var newData = {"type": data.type, "target": ents[i]}; 457 this.CauseDamage(newData); 458 } 459 } 460 } 461 }; 462 463 Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire) 464 { 465 var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); 466 var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); 467 var owner = cmpOwnership.GetOwner(); 468 var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player); 469 var numPlayers = cmpPlayerManager.GetNumPlayers(); 470 var players = []; 471 472 for (var i = 1; i < numPlayers; ++i) 473 { 474 // Only target enemies unless friendly fire is on 475 if (cmpPlayer.IsEnemy(i) || friendlyFire) 476 players.push(i); 477 } 478 479 var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 480 return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver); 481 } 482 298 483 /** 299 484 * Inflict damage on the target 300 485 */ … … 302 487 { 303 488 var strengths = this.GetAttackStrengths(data.type); 304 489 305 var attackBonus = this.GetAttackBonus(data.type, data.target); 490 var damageMultiplier = this.GetAttackBonus(data.type, data.target); 491 if (data.damageMultiplier !== undefined) 492 damageMultiplier *= data.damageMultiplier; 306 493 307 494 var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); 308 495 if (!cmpDamageReceiver) 309 496 return; 310 var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * attackBonus, strengths.pierce * attackBonus, strengths.crush * attackBonus);497 var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier); 311 498 // if target killed pick up loot and credit experience 312 499 if (targetState.killed == true) 313 500 { … … 320 507 PlaySound("attack_impact", this.entity); 321 508 }; 322 509 510 Attack.prototype.OnUpdate = function(msg) 511 { 512 this.turnLength = msg.turnLength; 513 } 514 323 515 Engine.RegisterComponentType(IID_Attack, "Attack", Attack);