diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index a2e8142235..ef34244539 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -261,7 +261,7 @@ Attack.prototype.GetPreference = function(target) return undefined; const targetClasses = cmpIdentity.GetClassesList(); - + var minPref = null; for each (var type in this.GetAttackTypes()) { @@ -300,7 +300,7 @@ Attack.prototype.GetBestAttackAgainst = function(target) const isAllowed = function (value, i, a) { return !attack.GetRestrictedClasses(value).some(isTargetClass); } const isPreferred = function (value, i, a) { return attack.GetPreferredClasses(value).some(isTargetClass); } const byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); } - + // Always slaughter domestic animals instead of using a normal attack if (isTargetClass("Domestic") && this.template.Slaughter) return "Slaughter"; @@ -334,7 +334,7 @@ Attack.prototype.GetAttackStrengths = function(type) { // Work out the attack values with technology effects var self = this; - + var template = this.template[type]; var splash = ""; if (!template) @@ -376,18 +376,18 @@ Attack.prototype.GetAttackBonus = function(type, target) var template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; - + if (template.Bonuses) { var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return 1; - + // Multiply the bonuses for all matching classes for (var key in template.Bonuses) { var bonus = template.Bonuses[key]; - + var hasClasses = true; if (bonus.Classes){ var classes = bonus.Classes.split(/\s+/); @@ -399,21 +399,21 @@ Attack.prototype.GetAttackBonus = function(type, target) attackBonus *= bonus.Multiplier; } } - + return attackBonus; }; // Returns a 2d random distribution scaled for a spread of scale 1. // The current implementation is a 2d gaussian with sigma = 1 Attack.prototype.GetNormalDistribution = function(){ - + // Use the Box-Muller transform to get a gaussian distribution var a = Math.random(); var b = Math.random(); - + var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b); var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b); - + return [c, d]; }; @@ -434,12 +434,12 @@ Attack.prototype.PerformAttack = function(type, target) // Get some data about the entity var horizSpeed = +this.template[type].ProjectileSpeed; var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now - + var spread = +this.template.Ranged.Spread; spread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", spread, this.entity); - + //horizSpeed /= 2; gravity /= 2; // slow it down for testing - + var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; @@ -448,74 +448,57 @@ Attack.prototype.PerformAttack = function(type, target) if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; var targetPosition = cmpTargetPosition.GetPosition(); - + var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z} var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); - + var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength} // the component of the targets velocity radially away from the archer var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition); - + var horizDistance = this.VectorDistance(targetPosition, selfPosition); - + // This is an approximation of the time ot the target, it assumes that the target has a constant radial // velocity, but since units move in straight lines this is not true. The exact value would be more // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was // about 5% of the units radius out in the worst case) var timeToTarget = horizDistance / (horizSpeed - radialSpeed); - + // Predict where the unit is when the missile lands. var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget, "z": targetPosition.z + targetVelocity.z * timeToTarget}; - + // Compute the real target point (based on spread and target speed) var randNorm = this.GetNormalDistribution(); var offsetX = randNorm[0] * spread * (1 + this.VectorLength(targetVelocity) / 20); var offsetZ = randNorm[1] * spread * (1 + this.VectorLength(targetVelocity) / 20); var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ }; - + // Calculate when the missile will hit the target position var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition); var timeToTarget = realHorizDistance / horizSpeed; - + var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance}; - + // Make the arrow appear to land slightly behind the target so that arrows landing next to a guys foot don't count but arrows that go through the torso do var graphicalPosition = {"x": realTargetPosition.x + 2*missileDirection.x, "y": realTargetPosition.y + 2*missileDirection.y}; // Launch the graphical projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); - + + var playerId = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner() var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id}); + cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "playerId":playerId}); } else { // Melee attack - hurt the target immediately - this.CauseDamage({"type": type, "target": target}); + Damage.CauseDamage({"strengths":this.GetAttackStrengths(type), "target":target, "attacker":this.entity, "multiplier":this.GetAttackBonus(type, target), "type":type}); } // TODO: charge attacks (need to design how they work) }; -/** - * Called when some units kills something (another unit, building, animal etc) - */ -Attack.prototype.TargetKilled = function(killerEntity, targetEntity) -{ - var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker); - if (cmpKillerPlayerStatisticsTracker) cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity); - var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker); - if (cmpTargetPlayerStatisticsTracker) cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity); - - // if unit can collect loot, lets try to collect it - var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter); - if (cmpLooter) - { - cmpLooter.Collect(targetEntity); - } -}; - Attack.prototype.InterpolatedLocation = function(ent, lateness) { var cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); @@ -538,11 +521,6 @@ Attack.prototype.VectorDot = function(p1, p2) return (p1.x * p2.x + p1.z * p2.z); }; -Attack.prototype.VectorCross = function(p1, p2) -{ - return (p1.x * p2.z - p1.z * p2.x); -}; - Attack.prototype.VectorLength = function(p) { return Math.sqrt(p.x*p.x + p.z*p.z); @@ -558,10 +536,10 @@ Attack.prototype.testCollision = function(ent, point, lateness) if (!cmpFootprint) return false; var targetShape = cmpFootprint.GetShape(); - + if (!targetShape || !targetPosition) return false; - + if (targetShape.type === 'circle') { return (this.VectorDistance(point, targetPosition) < targetShape.radius); @@ -569,13 +547,13 @@ Attack.prototype.testCollision = function(ent, point, lateness) else { var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y; - + var dx = point.x - targetPosition.x; var dz = point.z - targetPosition.z; - + var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz; var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz; - + return (-targetShape.width/2 <= dxr && dxr < targetShape.width/2 && -targetShape.depth/2 <= dzr && dzr < targetShape.depth/2); } }; @@ -591,55 +569,25 @@ Attack.prototype.MissileHit = function(data, lateness) var friendlyFire = this.template.Ranged.Splash.FriendlyFire; var splashRadius = this.template.Ranged.Splash.Range; var splashShape = this.template.Ranged.Splash.Shape; - - var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire); - ents.push(data.target); // Add the original unit to the list of splash damage targets - - for (var i = 0; i < ents.length; i++) + var playersToDamage; + // If friendlyFire isn't enabled, get all player enemies to pass to "Damage.CauseSplashDamage". + if (friendlyFire == false) { - var entityPosition = this.InterpolatedLocation(ents[i], lateness); - var radius = this.VectorDistance(data.position, entityPosition); - - if (radius < splashRadius) - { - var multiplier = 1; - if (splashShape == "Circular") // quadratic falloff - { - multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius)); - } - else if (splashShape == "Linear") - { - // position of entity relative to where the missile hit - var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z}; - - var splashWidth = splashRadius / 5; - var parallelDist = this.VectorDot(relPos, data.direction); - var perpDist = Math.abs(this.VectorCross(relPos, data.direction)); - - // Check that the unit is within the distance splashWidth of the line starting at the missile's - // landing point which extends in the direction of the missile for length splashRadius. - if (parallelDist > -splashWidth && perpDist < splashWidth) - { - // Use a quadratic falloff in both directions - multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius) - * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth); - } - else - { - multiplier = 0; - } - } - var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier}; - this.CauseDamage(newData); - } + var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player) + playersToDamage = cmpPlayer.GetEnemies(); } + // Damage the units. + Damage.CauseSplashDamage({"attacker":this.entity, "origin":data.position, "radius":splashRadius, "shape":splashShape, "strengths":this.GetAttackStrengths(data.type), "direction":data.direction, "playersToDamage":playersToDamage, "type":data.type}); } - + if (this.testCollision(data.target, data.position, lateness)) { + data.attacker = this.entity + data.multiplier = this.GetAttackBonus(data.type, data.target) + data.strengths = this.GetAttackStrengths(data.type) // Hit the primary target - this.CauseDamage(data); - + Damage.CauseDamage(data); + // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); @@ -647,15 +595,15 @@ Attack.prototype.MissileHit = function(data, lateness) else { // If we didn't hit the main target look for nearby units - var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2); - + var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player) + var ents = Damage.EntitiesNearPoint(data.position, this.VectorDistance(data.position, targetPosition) * 2, cmpPlayer.GetEnemies()); + for (var i = 0; i < ents.length; i++) { if (this.testCollision(ents[i], data.position, lateness)) { - var newData = {"type": data.type, "target": ents[i]}; - this.CauseDamage(newData); - + var newData = {"strengths":this.GetAttackStrengths(data.type), "target":ents[i], "attacker":this.entity, "multiplier":this.GetAttackBonus(data.type, ents[i]), "type":data.type}; + Damage.CauseDamage(newData); // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); @@ -664,53 +612,6 @@ Attack.prototype.MissileHit = function(data, lateness) } }; -Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire) -{ - var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); - var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - var owner = cmpOwnership.GetOwner(); - var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player); - var numPlayers = cmpPlayerManager.GetNumPlayers(); - var players = []; - - for (var i = 1; i < numPlayers; ++i) - { - // Only target enemies unless friendly fire is on - if (cmpPlayer.IsEnemy(i) || friendlyFire) - players.push(i); - } - - var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver); -} - -/** - * Inflict damage on the target - */ -Attack.prototype.CauseDamage = function(data) -{ - var strengths = this.GetAttackStrengths(data.type); - - var damageMultiplier = this.GetAttackBonus(data.type, data.target); - if (data.damageMultiplier !== undefined) - damageMultiplier *= data.damageMultiplier; - - var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); - if (!cmpDamageReceiver) - return; - var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier, this.entity); - // if target killed pick up loot and credit experience - if (targetState.killed == true) - { - this.TargetKilled(this.entity, data.target); - } - - Engine.PostMessage(data.target, MT_Attacked, - { "attacker": this.entity, "target": data.target, "type": data.type, "damage": -targetState.change }); - - PlaySound("attack_impact", this.entity); -}; - Attack.prototype.OnUpdate = function(msg) { this.turnLength = msg.turnLength; diff --git a/binaries/data/mods/public/simulation/components/Player.js b/binaries/data/mods/public/simulation/components/Player.js index bee490cf16..195cfdb266 100644 --- a/binaries/data/mods/public/simulation/components/Player.js +++ b/binaries/data/mods/public/simulation/components/Player.js @@ -450,6 +450,18 @@ Player.prototype.SetEnemy = function(id) this.SetDiplomacyIndex(id, -1); }; +/** + * Get all enemies of a given player. + */ +Player.prototype.GetEnemies = function() +{ + var enemies = []; + for (var i = 0; i < this.diplomacy.length; i++) + if (this.diplomacy[i] < 0) + enemies.push(i); + return enemies; +}; + /** * Check if given player is our enemy */ diff --git a/binaries/data/mods/public/simulation/components/PlayerManager.js b/binaries/data/mods/public/simulation/components/PlayerManager.js index a87ea9cd90..4c1ee32e89 100644 --- a/binaries/data/mods/public/simulation/components/PlayerManager.js +++ b/binaries/data/mods/public/simulation/components/PlayerManager.js @@ -46,4 +46,8 @@ PlayerManager.prototype.RemoveAllPlayers = function() this.playerEntities = []; }; +PlayerManager.prototype.GetAllPlayerEntities = function() +{ + return this.playerEntities; +}; Engine.RegisterComponentType(IID_PlayerManager, "PlayerManager", PlayerManager); diff --git a/binaries/data/mods/public/simulation/helpers/Damage.js b/binaries/data/mods/public/simulation/helpers/Damage.js new file mode 100644 index 0000000000..68fc50b708 --- /dev/null +++ b/binaries/data/mods/public/simulation/helpers/Damage.js @@ -0,0 +1,161 @@ +// Create global Damage object. +var Damage = {}; + +/** + * Damages units around a given origin. + * data.attacker = + * data.origin = {'x':, 'z':} + * data.radius = + * data.shape = + * data.strengths = {'hack':, 'pierce':, 'crush':} + * data.type = + * ***Optional Variables*** + * data.direction = + * data.playersToDamage = + */ +Damage.CauseSplashDamage = function(data) +{ + // Get nearby entities and define variables + var nearEnts = Damage.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage); + var damageMultiplier = 1; + // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. + for each (var entity in nearEnts) + { + var entityPosition = Engine.QueryInterface(entity, IID_Position).GetPosition(); + if(data.shape == 'Circular') // circular effect with quadratic falloff in every direction + { + var squaredDistanceFromOrigin = Damage.VectorDistanceSquared(data.origin, entityPosition); + damageMultiplier == 1 - squaredDistanceFromOrigin / (data.radius * data.radius); + } + else if(data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles) + { + // Get position of entity relative to splash origin. + var relativePos = {"x":entityPosition.x - data.origin.x, "z":entityPosition.z - data.origin.z}; + + // The width of linear splash is one fifth of the normal splash radius. + var width = data.radius/5; + + // Effectivly rotate the axis to align with the missile direction. + var parallelDist = Damage.VectorDot(relativePos, data.direction); // z axis + var perpDist = Math.abs(Damage.VectorCross(relativePos, data.direction)); // y axis + + // Check that the unit is within the distance at which it will get damaged. + if (parallelDist > -width && perpDist < width) // If in radius, quadratic falloff in both directions + damageMultiplier = (data.radius * data.radius - parallelDist * parallelDist) / (data.radius * data.radius) + * (width * width - perpDist * perpDist) / (width * width); + else + damageMultiplier = 0; + } + else // In case someone calls this function with an invalid shape. + { + warn("The " + data.shape + " splash damage shape is not implemented!"); + } + // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds.... + Damage.CauseDamage({"strengths":data.strengths, "target":entity, "attacker":data.attacker, "multiplier":damageMultiplier, "type":data.type + ".Splash"}) + } +}; + +/** + * Causes damage on a given unit + * data.strengths = {'hack':, 'pierce':, 'crush':} + * data.target = + * data.attacker = + * data.multiplier = + * data.type = + */ +Damage.CauseDamage = function(data) +{ + // Check the target can be damaged otherwise don't do anything. + var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); + if (!cmpDamageReceiver) + return; + + // Damage the target + var targetState = cmpDamageReceiver.TakeDamage(data.strengths.hack * data.multiplier, data.strengths.pierce * data.multiplier, data.strengths.crush * data.multiplier, data.attacker); + + // If the target was killed run some cleanup + if (targetState.killed) + Damage.TargetKilled(data.attacker, data.target); + + // Post the network command (make it work in multiplayer) + Engine.PostMessage(data.target, MT_Attacked, {"attacker":data.attacker, "target":data.target, "type":data.type, "damage":-targetState.change}); + + // Play attacking sounds + PlaySound("attack_impact", data.attacker); +}; + +/** + * Gets entities near a give point for given players. + * origin = {'x':, 'z':} + * radius = + * players = + * If players is not included, entities from all players are used. + */ +Damage.EntitiesNearPoint = function(origin, radius, players) +{ + // If there is insufficient data return an empty array. + if (!origin || !radius) + return []; + // Create the dummy entity used for range calculations if it doesn't exist. + if (!Damage.dummyTargetEntity) + Damage.dummyTargetEntity = Engine.AddEntity('special/dummy'); + // Move the dummy entity to the origin of the query. + var cmpDummyPosition = Engine.QueryInterface(Damage.dummyTargetEntity, IID_Position); + if (!cmpDummyPosition) + return []; + cmpDummyPosition.JumpTo(origin.x, origin.z); + + // If the players parameter is not specified use all players. + if (!players) + { + var playerEntities = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayerEntities(); + players = []; + for (var entity in playerEntities) + players.append(Engine.QueryInterface(entity, IID_Player).GetPlayerID()); + } + + // Call RangeManager with dummy entity and return the result. + var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var rangeQuery = rangeManager.ExecuteQuery(Damage.dummyTargetEntity, 0, radius, players, IID_DamageReceiver); + return rangeQuery; +}; + +/** + * Called when some units kills something (another unit, building, animal etc) + * killerEntity = + * targetEntity = + */ +Damage.TargetKilled = function(killerEntity, targetEntity) +{ + // Add to killer statistics. + var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker); + if (cmpKillerPlayerStatisticsTracker) + cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity); + // Add to loser statistics. + var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker); + if (cmpTargetPlayerStatisticsTracker) + cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity); + + // If killer can collect loot, let's try to collect it. + var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter); + if (cmpLooter) + cmpLooter.Collect(targetEntity); +}; + +// Gets the straight line distance between p1 and p2 +Damage.VectorDistanceSquared = function(p1, p2) +{ + return (p1.x - p2.x) * (p1.x - p2.x) + (p1.z - p2.z) * (p1.z - p2.z); +}; + +// Gets the dot product of two vectors. +Damage.VectorDot = function(p1, p2) +{ + return p1.x * p2.x + p1.z * p2.z; +}; + +// Gets the 2D interpreted version of the cross product of two vectors. +Damage.VectorCross = function(p1, p2) +{ + return p1.x * p2.z - p1.z * p2.x; +}; diff --git a/binaries/data/mods/public/simulation/templates/special/dummy.xml b/binaries/data/mods/public/simulation/templates/special/dummy.xml new file mode 100644 index 0000000000..4b5120cc1a --- /dev/null +++ b/binaries/data/mods/public/simulation/templates/special/dummy.xml @@ -0,0 +1,8 @@ + + + 0 + upright + false + 6.0 + +