Ticket #3610: DamageAfterDeathOfAtacker_v2.patch

File DamageAfterDeathOfAtacker_v2.patch, 24.7 KB (added by Leander Hemelhof, 8 years ago)

Second version taking fatherbushido's regards into account

  • data/mods/public/gui/credits/texts/programming.json

     
    108108            {"nick": "kingadami", "name": "Adam Winsor"},
    109109            {"nick": "kingbasil", "name": "Giannis Fafalios"},
    110110            {"nick": "lafferjm", "name": "Justin Lafferty"},
     111            {"nick": "LeanderH", "name": "Leander Hemelhof"},
    111112            {"nick": "leper", "name": "Georg Kilzer"},
    112113            {"nick": "LittleDev"},
    113114            {"nick": "livingaftermidnight", "name": "Will Dull"},
  • data/mods/public/simulation/components/Attack.js

     
    487487 */
    488488Attack.prototype.PerformAttack = function(type, target)
    489489{
     490    let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
    490491    // If this is a ranged attack, then launch a projectile
    491492    if (type == "Ranged")
    492493    {
     
    556557
    557558        let playerId = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
    558559        cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    559         cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget * 1000, {
    560             "type": type,
    561             "target": target,
    562             "position": realTargetPosition,
    563             "direction": missileDirection,
    564             "projectileId": id,
    565             "playerId":playerId
    566         });
     560        let data = {
     561                "type": type,
     562                "attacker": this.entity,
     563                "target": target,
     564                "strengths": this.GetAttackStrengths(type),
     565                "position": realTargetPosition,
     566                "direction": missileDirection,
     567                "projectileId": id,
     568                "playerId": playerId,
     569                "multiplier": this.GetAttackBonus(type, target),
     570                "isSplash": false
     571            }
     572        if (this.template.Ranged.Splash)
     573        {
     574            data.friendlyFire = this.template.Ranged.Splash.FriendlyFire;
     575            data.range = this.template.Ranged.Splash.Range;
     576            data.shape = this.template.Ranged.Splash.Shape;
     577            data.isSplash = true;
     578        }
     579        cmpDamage.entityOwners.set(this.entity, Engine.QueryInterface(this.entity, IID_Ownership).GetOwner());
     580        cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000, data);
    567581    }
    568582    else if (type == "Capture")
    569583    {
     
    594608    else
    595609    {
    596610        // Melee attack - hurt the target immediately
    597         Damage.CauseDamage({
     611        cmpDamage.CauseDamage({
    598612            "strengths": this.GetAttackStrengths(type),
    599613            "target": target,
    600614            "attacker": this.entity,
     
    605619    // TODO: charge attacks (need to design how they work)
    606620};
    607621
    608 Attack.prototype.InterpolatedLocation = function(ent, lateness)
    609 {
    610     let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    611     let turnLength = cmpTimer.GetLatestTurnLength()/1000;
    612     let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
    613     if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
    614         return undefined;
    615     let curPos = cmpTargetPosition.GetPosition();
    616     let prevPos = cmpTargetPosition.GetPreviousPosition();
    617     lateness /= 1000;
    618     return new Vector3D((curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
    619             0,
    620             (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength);
    621 };
    622 
    623 // Tests whether it point is inside of ent's footprint
    624 Attack.prototype.testCollision = function(ent, point, lateness)
    625 {
    626     let targetPosition = this.InterpolatedLocation(ent, lateness);
    627     if (!targetPosition)
    628         return false;
    629     let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
    630     if (!cmpFootprint)
    631         return false;
    632     let targetShape = cmpFootprint.GetShape();
    633 
    634     if (!targetShape || !targetPosition)
    635         return false;
    636 
    637     if (targetShape.type === 'circle')
    638     {
    639         // Use VectorDistanceSquared and square targetShape.radius to avoid square roots.
    640         return (targetPosition.horizDistanceTo(point) < (targetShape.radius * targetShape.radius));
    641     }
    642     else
    643     {
    644         let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
    645 
    646         let d = Vector3D.sub(point, targetPosition);
    647         d = Vector2D.from3D(d).rotate(-angle);
    648 
    649         return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
    650     }
    651 };
    652 
    653 Attack.prototype.MissileHit = function(data, lateness)
    654 {
    655     let targetPosition = this.InterpolatedLocation(data.target, lateness);
    656     if (!targetPosition)
    657         return;
    658 
    659     // Do this first in case the direct hit kills the target
    660     if (this.template.Ranged.Splash)
    661     {
    662         let friendlyFire = this.template.Ranged.Splash.FriendlyFire;
    663         let splashRadius = this.template.Ranged.Splash.Range;
    664         let splashShape = this.template.Ranged.Splash.Shape;
    665         let playersToDamage;
    666 
    667         if (friendlyFire == "false")
    668         {
    669             let cmpPlayer = QueryPlayerIDInterface(data.playerId);
    670             playersToDamage = cmpPlayer.GetEnemies();
    671         }
    672 
    673         Damage.CauseSplashDamage({
    674             "attacker": this.entity,
    675             "origin": Vector2D.from3D(data.position),
    676             "radius": splashRadius,
    677             "shape": splashShape,
    678             "strengths": this.GetAttackStrengths(data.type),
    679             "direction": data.direction,
    680             "playersToDamage": playersToDamage,
    681             "type": data.type
    682         });
    683     }
    684 
    685     if (this.testCollision(data.target, data.position, lateness))
    686     {
    687         data.attacker = this.entity;
    688         data.multiplier = this.GetAttackBonus(data.type, data.target);
    689         data.strengths = this.GetAttackStrengths(data.type);
    690         // Hit the primary target
    691         Damage.CauseDamage(data);
    692 
    693         // Remove the projectile
    694         let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
    695         cmpProjectileManager.RemoveProjectile(data.projectileId);
    696     }
    697     else
    698     {
    699         // If we didn't hit the main target look for nearby units
    700         let cmpPlayer = QueryPlayerIDInterface(data.playerId);
    701         let ents = Damage.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
    702 
    703         for (let i = 0; i < ents.length; ++i)
    704         {
    705             if (!this.testCollision(ents[i], data.position, lateness))
    706                 continue;
    707 
    708             Damage.CauseDamage({
    709                 "strengths": this.GetAttackStrengths(data.type),
    710                 "target": ents[i],
    711                 "attacker": this.entity,
    712                 "multiplier": this.GetAttackBonus(data.type, ents[i]),
    713                 "type": data.type
    714             });
    715 
    716             // Remove the projectile
    717             let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
    718             cmpProjectileManager.RemoveProjectile(data.projectileId);
    719         }
    720     }
    721 };
    722 
    723622Attack.prototype.OnValueModification = function(msg)
    724623{
    725624    if (msg.component != "Attack")
  • data/mods/public/simulation/components/AttackDetection.js

     
    6363    if (cmpTargetOwnership.GetOwner() != cmpPlayer.GetPlayerID())
    6464        return;
    6565    var cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
     66    let attackerOwner;
     67    if (cmpAttackerOwnership)
     68        attackerOwner = cmpAttackerOwnership.GetOwner();
     69    else // The attacker is dead, so we use the data stored in the Damage component
     70        attackerOwner = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage).GetDeadEntityOwner(attacker);
    6671    // Don't register attacks dealt by myself
    67     if (cmpAttackerOwnership.GetOwner() == cmpPlayer.GetPlayerID())
     72    if (attackerOwner == cmpPlayer.GetPlayerID())
    6873        return;
    6974       
    7075    // Since livestock can be attacked/gathered by other players
     
    122127        "type": "attack",
    123128        "target": target,
    124129        "players": [cmpPlayer.GetPlayerID()],
    125         "attacker": cmpAttackerOwnership.GetOwner(),
     130        "attacker": attackerOwner,
    126131        "targetIsDomesticAnimal": targetIsDomesticAnimal
    127132    });
    128133    PlaySound("attacked", target);
  • data/mods/public/simulation/components/Damage.js

     
     1function Damage() {}
     2
     3Damage.prototype.Schema =
     4    "<a:component type='system'/><empty/>";
     5
     6Damage.prototype.Init = function()
     7{
     8};
     9
     10Damage.prototype.entityOwners = new Map();
     11
     12Damage.prototype.InterpolatedLocation = function(ent, lateness)
     13{
     14    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     15    let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     16    let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
     17    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
     18        return undefined;
     19    let curPos = cmpTargetPosition.GetPosition();
     20    let prevPos = cmpTargetPosition.GetPreviousPosition();
     21    lateness /= 1000;
     22    return new Vector3D((curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
     23            0,
     24            (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength);
     25};
     26/**
     27 * Test if a point is inside of an entities footprint
     28 * ent = <entity id>
     29 * point = <Vector2D>
     30 * lateness = <int>
     31 */
     32Damage.prototype.TestCollision = function(ent, point, lateness)
     33{
     34    let targetPosition = this.InterpolatedLocation(ent, lateness);
     35    if (!targetPosition)
     36        return false;
     37    let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
     38    if (!cmpFootprint)
     39        return false;
     40    let targetShape = cmpFootprint.GetShape();
     41
     42    if (!targetShape || !targetPosition)
     43        return false;
     44
     45    if (targetShape.type === 'circle')
     46    {
     47        // Use VectorDistanceSquared and square targetShape.radius to avoid square roots.
     48        return (targetPosition.horizDistanceTo(point) < (targetShape.radius * targetShape.radius));
     49    }
     50    else
     51    {
     52        let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
     53
     54        let d = Vector3D.sub(point, targetPosition);
     55        d = Vector2D.from3D(d).rotate(-angle);
     56
     57        return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
     58    }
     59};
     60
     61Damage.prototype.MissileHit = function(data, lateness)
     62{
     63    let targetPosition = this.InterpolatedLocation(data.target, lateness);
     64    if (!targetPosition)
     65        return;
     66   
     67    // Do this first in case the direct hit kills the target
     68    if (data.isSplash)
     69    {
     70        let playersToDamage;
     71
     72        if (!data.friendlyFire)
     73        {
     74            let cmpPlayer = QueryPlayerIDInterface(data.playerId);
     75            playersToDamage = cmpPlayer.GetEnemies();
     76        }
     77
     78        this.CauseSplashDamage({
     79            "attacker": data.attacker,
     80            "origin": Vector2D.from3D(data.position),
     81            "radius": data.radius,
     82            "shape": data.shape,
     83            "strengths": data.strengths,
     84            "direction": data.direction,
     85            "playersToDamage": playersToDamage,
     86            "type": data.type
     87        });
     88    }
     89
     90    if (this.TestCollision(data.target, data.position, lateness))
     91    {
     92        // Hit the primary target
     93        this.CauseDamage(data);
     94
     95        // Remove the projectile
     96        let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
     97        cmpProjectileManager.RemoveProjectile(data.projectileId);
     98    }
     99    else
     100    {
     101        // If we didn't hit the main target look for nearby units
     102        let cmpPlayer = QueryPlayerIDInterface(data.playerId);
     103        let ents = this.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
     104
     105        for (let i = 0; i < ents.length; ++i)
     106        {
     107            if (!this.TestCollision(ents[i], data.position, lateness))
     108                continue;
     109
     110            this.CauseDamage({
     111                "strengths": data.strengths,
     112                "target": ents[i],
     113                "attacker": data.attacker,
     114                "multiplier": data.multiplier,
     115                "type": data.type
     116            });
     117
     118            // Remove the projectile
     119            let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
     120            cmpProjectileManager.RemoveProjectile(data.projectileId);
     121        }
     122    }
     123    let cmpHealth = Engine.QueryInterface(data.attacker, IID_Health);
     124    if (cmpHealth && cmpHealth.GetHitpoints() != 0) // Remove killer data if it's still alive.
     125    {
     126        this.entityOwners.delete(data.attacker);
     127    }
     128};
     129
     130/**
     131 * Damages units around a given origin.
     132 * data.attacker = <entity id>
     133 * data.origin = <Vector2D>
     134 * data.radius = <int>
     135 * data.shape = <string>
     136 * data.strengths = {'hack':<float>, 'pierce':<float>, 'crush':<float>}
     137 * data.type = <string>
     138 * ***Optional Variables***
     139 * data.direction = <unit vector>
     140 * data.playersToDamage = <array of player ids>
     141 */
     142Damage.prototype.CauseSplashDamage = function(data)
     143{
     144    // Get nearby entities and define variables
     145    var nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage);
     146    var damageMultiplier = 1;
     147    // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
     148    for each (var entity in nearEnts)
     149    {
     150        var entityPosition = Engine.QueryInterface(entity, IID_Position).GetPosition2D();
     151        if(data.shape == 'Circular') // circular effect with quadratic falloff in every direction
     152        {
     153            var squaredDistanceFromOrigin = data.origin.distanceToSquared(entityPosition);
     154            damageMultiplier = 1 - squaredDistanceFromOrigin / (data.radius * data.radius);
     155        }
     156        else if(data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
     157        {
     158            // Get position of entity relative to splash origin.
     159            var relativePos = entityPosition.sub(data.origin);
     160
     161            // The width of linear splash is one fifth of the normal splash radius.
     162            var width = data.radius/5;
     163
     164            // Effectivly rotate the axis to align with the missile direction.
     165            var parallelDist = relativePos.dot(data.direction); // z axis
     166            var perpDist = Math.abs(relativePos.cross(data.direction)); // y axis
     167
     168            // Check that the unit is within the distance at which it will get damaged.
     169            if (parallelDist > -width && perpDist < width) // If in radius, quadratic falloff in both directions
     170                damageMultiplier = (data.radius * data.radius - parallelDist * parallelDist) / (data.radius * data.radius)
     171                                 * (width * width - perpDist * perpDist) / (width * width);
     172            else
     173                damageMultiplier = 0;
     174        }
     175        else // In case someone calls this function with an invalid shape.
     176        {
     177            warn("The " + data.shape + " splash damage shape is not implemented!");
     178        }
     179        // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds....
     180        this.CauseDamage({"strengths":data.strengths, "target":entity, "attacker":data.attacker, "multiplier":damageMultiplier, "type":data.type + ".Splash"});
     181    }
     182};
     183
     184/**
     185 * Causes damage on a given unit
     186 * data.strengths = {'hack':<float>, 'pierce':<float>, 'crush':<float>}
     187 * data.target = <entity id>
     188 * data.attacker = <entity id>
     189 * data.multiplier = <float between 1 and 0>
     190 * data.type = <string>
     191 */
     192Damage.prototype.CauseDamage = function(data)
     193{
     194    // Check the target can be damaged otherwise don't do anything.
     195    var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
     196    var cmpHealth = Engine.QueryInterface(data.target, IID_Health);
     197    if (!cmpDamageReceiver || !cmpHealth)
     198        return;
     199
     200    // Damage the target
     201    var targetState = cmpDamageReceiver.TakeDamage(data.strengths.hack * data.multiplier, data.strengths.pierce * data.multiplier, data.strengths.crush * data.multiplier);
     202
     203    var cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion);
     204    var cmpLoot = Engine.QueryInterface(data.target, IID_Loot);
     205    if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0)
     206        cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints());
     207
     208    // If the target was killed run some cleanup
     209    if (targetState.killed)
     210        this.TargetKilled(data.attacker, data.target);
     211
     212    // Post the network command (make it work in multiplayer)
     213    Engine.PostMessage(data.target, MT_Attacked, {"attacker":data.attacker, "target":data.target, "type":data.type, "damage":-targetState.change});
     214
     215    // Play attacking sounds
     216    PlaySound("attack_impact", data.attacker);
     217};
     218
     219/**
     220 * Gets entities near a give point for given players.
     221 * origin = <Vector2D>
     222 * radius = <int>
     223 * players = <array>
     224 * If players is not included, entities from all players are used.
     225 */
     226Damage.prototype.EntitiesNearPoint = function(origin, radius, players)
     227{
     228    // If there is insufficient data return an empty array.
     229    if (!origin || !radius)
     230        return [];
     231
     232    // If the players parameter is not specified use all players.
     233    if (!players)
     234    {
     235        var playerEntities = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayerEntities();
     236        players = [];
     237        for each (var entity in playerEntities)
     238            players.push(Engine.QueryInterface(entity, IID_Player).GetPlayerID());
     239    }
     240
     241    // Call RangeManager with dummy entity and return the result.
     242    var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     243    var rangeQuery = rangeManager.ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver);
     244    return rangeQuery;
     245};
     246
     247/**
     248 * Called when some units kills something (another unit, building, animal etc)
     249 * killerEntity = <entity id>
     250 * targetEntity = <entity id>
     251 */
     252Damage.prototype.TargetKilled = function(killerEntity, targetEntity)
     253{
     254    // Add to killer statistics.
     255    var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker);
     256    if (cmpKillerPlayerStatisticsTracker)
     257        cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity);
     258    // Add to loser statistics.
     259    var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker);
     260    if (cmpTargetPlayerStatisticsTracker)
     261        cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity);
     262
     263    // If killer can collect loot, let's try to collect it.
     264    var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter);
     265    if (cmpLooter)
     266        cmpLooter.Collect(targetEntity);
     267};
     268
     269
     270Damage.prototype.GetDeadEntityOwner = function(entityId)
     271{
     272    var cmpOwner = this.entityOwners.get(entityId);
     273    this.entityOwners.delete(entityId);
     274    return cmpOwner;
     275};
     276   
     277Engine.RegisterSystemComponentType(IID_Damage, "Damage", Damage);
  • data/mods/public/simulation/components/interfaces/Damage.js

    Property changes on: data/mods/public/simulation/components/Damage.js
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
     1Engine.RegisterInterface("Damage");
  • data/mods/public/simulation/helpers/Damage.js

    Property changes on: data/mods/public/simulation/components/interfaces/Damage.js
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
    1 // Create global Damage object.
    2 var Damage = {};
    3 
    4 /**
    5  * Damages units around a given origin.
    6  * data.attacker = <entity id>
    7  * data.origin = <Vector2D>
    8  * data.radius = <int>
    9  * data.shape = <string>
    10  * data.strengths = {'hack':<float>, 'pierce':<float>, 'crush':<float>}
    11  * data.type = <string>
    12  * ***Optional Variables***
    13  * data.direction = <unit vector>
    14  * data.playersToDamage = <array of player ids>
    15  */
    16 Damage.CauseSplashDamage = function(data)
    17 {
    18     // Get nearby entities and define variables
    19     var nearEnts = Damage.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage);
    20     var damageMultiplier = 1;
    21     // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
    22     for each (var entity in nearEnts)
    23     {
    24         var entityPosition = Engine.QueryInterface(entity, IID_Position).GetPosition2D();
    25         if(data.shape == 'Circular') // circular effect with quadratic falloff in every direction
    26         {
    27             var squaredDistanceFromOrigin = data.origin.distanceToSquared(entityPosition);
    28             damageMultiplier = 1 - squaredDistanceFromOrigin / (data.radius * data.radius);
    29         }
    30         else if(data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
    31         {
    32             // Get position of entity relative to splash origin.
    33             var relativePos = entityPosition.sub(data.origin);
    34 
    35             // The width of linear splash is one fifth of the normal splash radius.
    36             var width = data.radius/5;
    37 
    38             // Effectivly rotate the axis to align with the missile direction.
    39             var parallelDist = relativePos.dot(data.direction); // z axis
    40             var perpDist = Math.abs(relativePos.cross(data.direction)); // y axis
    41 
    42             // Check that the unit is within the distance at which it will get damaged.
    43             if (parallelDist > -width && perpDist < width) // If in radius, quadratic falloff in both directions
    44                 damageMultiplier = (data.radius * data.radius - parallelDist * parallelDist) / (data.radius * data.radius)
    45                                  * (width * width - perpDist * perpDist) / (width * width);
    46             else
    47                 damageMultiplier = 0;
    48         }
    49         else // In case someone calls this function with an invalid shape.
    50         {
    51             warn("The " + data.shape + " splash damage shape is not implemented!");
    52         }
    53         // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds....
    54         Damage.CauseDamage({"strengths":data.strengths, "target":entity, "attacker":data.attacker, "multiplier":damageMultiplier, "type":data.type + ".Splash"});
    55     }
    56 };
    57 
    58 /**
    59  * Causes damage on a given unit
    60  * data.strengths = {'hack':<float>, 'pierce':<float>, 'crush':<float>}
    61  * data.target = <entity id>
    62  * data.attacker = <entity id>
    63  * data.multiplier = <float between 1 and 0>
    64  * data.type = <string>
    65  */
    66 Damage.CauseDamage = function(data)
    67 {
    68     // Check the target can be damaged otherwise don't do anything.
    69     var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
    70     var cmpHealth = Engine.QueryInterface(data.target, IID_Health);
    71     if (!cmpDamageReceiver || !cmpHealth)
    72         return;
    73 
    74     // Damage the target
    75     var targetState = cmpDamageReceiver.TakeDamage(data.strengths.hack * data.multiplier, data.strengths.pierce * data.multiplier, data.strengths.crush * data.multiplier);
    76 
    77     var cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion);
    78     var cmpLoot = Engine.QueryInterface(data.target, IID_Loot);
    79     if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0)
    80         cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints());
    81 
    82     // If the target was killed run some cleanup
    83     if (targetState.killed)
    84         Damage.TargetKilled(data.attacker, data.target);
    85 
    86     // Post the network command (make it work in multiplayer)
    87     Engine.PostMessage(data.target, MT_Attacked, {"attacker":data.attacker, "target":data.target, "type":data.type, "damage":-targetState.change});
    88 
    89     // Play attacking sounds
    90     PlaySound("attack_impact", data.attacker);
    91 };
    92 
    93 /**
    94  * Gets entities near a give point for given players.
    95  * origin = <Vector2D>
    96  * radius = <int>
    97  * players = <array>
    98  * If players is not included, entities from all players are used.
    99  */
    100 Damage.EntitiesNearPoint = function(origin, radius, players)
    101 {
    102     // If there is insufficient data return an empty array.
    103     if (!origin || !radius)
    104         return [];
    105 
    106     // If the players parameter is not specified use all players.
    107     if (!players)
    108     {
    109         var playerEntities = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayerEntities();
    110         players = [];
    111         for each (var entity in playerEntities)
    112             players.push(Engine.QueryInterface(entity, IID_Player).GetPlayerID());
    113     }
    114 
    115     // Call RangeManager with dummy entity and return the result.
    116     var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    117     var rangeQuery = rangeManager.ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver);
    118     return rangeQuery;
    119 };
    120 
    121 /**
    122  * Called when some units kills something (another unit, building, animal etc)
    123  * killerEntity = <entity id>
    124  * targetEntity = <entity id>
    125  */
    126 Damage.TargetKilled = function(killerEntity, targetEntity)
    127 {
    128     // Add to killer statistics.
    129     var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker);
    130     if (cmpKillerPlayerStatisticsTracker)
    131         cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity);
    132     // Add to loser statistics.
    133     var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker);
    134     if (cmpTargetPlayerStatisticsTracker)
    135         cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity);
    136 
    137     // If killer can collect loot, let's try to collect it.
    138     var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter);
    139     if (cmpLooter)
    140         cmpLooter.Collect(targetEntity);
    141 };
    142 
    143 Engine.RegisterGlobal("Damage", Damage);
    144