Ticket #18: advanced_attack-220212.diff
File advanced_attack-220212.diff, 33.8 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. … … 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 <= dxr && dxr < targetShape.width && -targetShape.depth <= dzr && dzr < targetShape.depth); 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);