Ticket #3610: DamageAfterDeathOfAtacker_v5.patch

File DamageAfterDeathOfAtacker_v5.patch, 29.4 KB (added by Leander Hemelhof, 8 years ago)

Adds a test for the new damage component

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

     
    110110            {"nick": "kingadami", "name": "Adam Winsor"},
    111111            {"nick": "kingbasil", "name": "Giannis Fafalios"},
    112112            {"nick": "lafferjm", "name": "Justin Lafferty"},
     113            {"nick": "LeanderH", "name": "Leander Hemelhof"},
    113114            {"nick": "leper", "name": "Georg Kilzer"},
    114115            {"nick": "LittleDev"},
    115116            {"nick": "livingaftermidnight", "name": "Will Dull"},
  • binaries/data/mods/public/simulation/components/Attack.js

     
    481481 */
    482482Attack.prototype.PerformAttack = function(type, target)
    483483{
     484    let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
     485    let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
    484486    // If this is a ranged attack, then launch a projectile
    485487    if (type == "Ranged")
    486488    {
     
    548550        let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
    549551        let id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
    550552
    551         let playerId = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
    552553        cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    553         cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget * 1000, {
    554             "type": type,
    555             "target": target,
    556             "position": realTargetPosition,
    557             "direction": missileDirection,
    558             "projectileId": id,
    559             "playerId":playerId
    560         });
     554        let data = {
     555                "type": type,
     556                "attacker": this.entity,
     557                "target": target,
     558                "strengths": this.GetAttackStrengths(type),
     559                "position": realTargetPosition,
     560                "direction": missileDirection,
     561                "projectileId": id,
     562                "multiplier": this.GetAttackBonus(type, target),
     563                "isSplash": false,
     564                "attackerOwner": attackerOwner
     565            }
     566        if (this.template.Ranged.Splash)
     567        {
     568            data.friendlyFire = this.template.Ranged.Splash.FriendlyFire;
     569            data.range = this.template.Ranged.Splash.Range;
     570            data.shape = this.template.Ranged.Splash.Shape;
     571            data.isSplash = true;
     572        }
     573        cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000, data);
    561574    }
    562575    else if (type == "Capture")
    563576    {
     
    582595                "attacker": this.entity,
    583596                "target": target,
    584597                "type": type,
    585                 "damage": strength
     598                "damage": strength,
     599                "attackerOwner": attackerOwner
    586600            });
    587601    }
    588602    else
    589603    {
    590604        // Melee attack - hurt the target immediately
    591         Damage.CauseDamage({
     605        cmpDamage.CauseDamage({
    592606            "strengths": this.GetAttackStrengths(type),
    593607            "target": target,
    594608            "attacker": this.entity,
    595609            "multiplier": this.GetAttackBonus(type, target),
    596             "type":type
     610            "type":type,
     611            "attackerOwner": attackerOwner
    597612        });
    598613    }
    599614    // TODO: charge attacks (need to design how they work)
    600615};
    601616
    602 Attack.prototype.InterpolatedLocation = function(ent, lateness)
    603 {
    604     let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    605     let turnLength = cmpTimer.GetLatestTurnLength()/1000;
    606     let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
    607     if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
    608         return undefined;
    609     let curPos = cmpTargetPosition.GetPosition();
    610     let prevPos = cmpTargetPosition.GetPreviousPosition();
    611     lateness /= 1000;
    612     return new Vector3D((curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
    613             0,
    614             (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength);
    615 };
    616 
    617 // Tests whether it point is inside of ent's footprint
    618 Attack.prototype.testCollision = function(ent, point, lateness)
    619 {
    620     let targetPosition = this.InterpolatedLocation(ent, lateness);
    621     if (!targetPosition)
    622         return false;
    623     let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
    624     if (!cmpFootprint)
    625         return false;
    626     let targetShape = cmpFootprint.GetShape();
    627 
    628     if (!targetShape || !targetPosition)
    629         return false;
    630 
    631     if (targetShape.type === 'circle')
    632     {
    633         // Use VectorDistanceSquared and square targetShape.radius to avoid square roots.
    634         return (targetPosition.horizDistanceTo(point) < (targetShape.radius * targetShape.radius));
    635     }
    636     else
    637     {
    638         let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
    639 
    640         let d = Vector3D.sub(point, targetPosition);
    641         d = Vector2D.from3D(d).rotate(-angle);
    642 
    643         return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
    644     }
    645 };
    646 
    647 Attack.prototype.MissileHit = function(data, lateness)
    648 {
    649     let targetPosition = this.InterpolatedLocation(data.target, lateness);
    650     if (!targetPosition)
    651         return;
    652 
    653     // Do this first in case the direct hit kills the target
    654     if (this.template.Ranged.Splash)
    655     {
    656         let playersToDamage;
    657 
    658         if (this.template.Ranged.Splash.FriendlyFire == "false")
    659         {
    660             let cmpPlayer = QueryPlayerIDInterface(data.playerId);
    661             playersToDamage = cmpPlayer.GetEnemies();
    662         }
    663 
    664         Damage.CauseSplashDamage({
    665             "attacker": this.entity,
    666             "origin": Vector2D.from3D(data.position),
    667             "radius": this.template.Ranged.Splash.Range,
    668             "shape": this.template.Ranged.Splash.Shape,
    669             "strengths": this.GetAttackStrengths(data.type),
    670             "direction": data.direction,
    671             "playersToDamage": playersToDamage,
    672             "type": data.type
    673         });
    674     }
    675 
    676     if (this.testCollision(data.target, data.position, lateness))
    677     {
    678         data.attacker = this.entity;
    679         data.multiplier = this.GetAttackBonus(data.type, data.target);
    680         data.strengths = this.GetAttackStrengths(data.type);
    681         // Hit the primary target
    682         Damage.CauseDamage(data);
    683 
    684         // Remove the projectile
    685         let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
    686         cmpProjectileManager.RemoveProjectile(data.projectileId);
    687     }
    688     else
    689     {
    690         // If we didn't hit the main target look for nearby units
    691         let cmpPlayer = QueryPlayerIDInterface(data.playerId);
    692         let ents = Damage.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
    693 
    694         for (let i = 0; i < ents.length; ++i)
    695         {
    696             if (!this.testCollision(ents[i], data.position, lateness))
    697                 continue;
    698 
    699             Damage.CauseDamage({
    700                 "strengths": this.GetAttackStrengths(data.type),
    701                 "target": ents[i],
    702                 "attacker": this.entity,
    703                 "multiplier": this.GetAttackBonus(data.type, ents[i]),
    704                 "type": data.type
    705             });
    706 
    707             // Remove the projectile
    708             let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
    709             cmpProjectileManager.RemoveProjectile(data.projectileId);
    710         }
    711     }
    712 };
    713 
    714617Attack.prototype.OnValueModification = function(msg)
    715618{
    716619    if (msg.component != "Attack")
  • binaries/data/mods/public/simulation/components/AttackDetection.js

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

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

    Property changes on: binaries/data/mods/public/simulation/components/interfaces/Damage.js
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
     1Engine.LoadHelperScript("Player.js");
     2Engine.LoadHelperScript("Sound.js");
     3Engine.LoadHelperScript("ValueModification.js");
     4Engine.LoadComponentScript("interfaces/Damage.js");
     5Engine.LoadComponentScript("interfaces/Timer.js");
     6Engine.LoadComponentScript("interfaces/Health.js");
     7Engine.LoadComponentScript("interfaces/Attack.js");
     8Engine.LoadComponentScript("interfaces/DamageReceiver.js");
     9Engine.LoadComponentScript("interfaces/Promotion.js");
     10Engine.LoadComponentScript("interfaces/Loot.js");
     11Engine.LoadComponentScript("interfaces/Sound.js");
     12Engine.LoadComponentScript("interfaces/AttackDetection.js")
     13Engine.LoadComponentScript("interfaces/Player.js");
     14Engine.LoadComponentScript("interfaces/TechnologyManager.js");
     15Engine.LoadComponentScript("interfaces/AuraManager.js");
     16Engine.LoadComponentScript("Damage.js");
     17Engine.LoadComponentScript("Attack.js");
     18Engine.LoadComponentScript("Timer.js");
     19Engine.LoadComponentScript("PlayerManager.js")
     20
     21let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
     22let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
     23cmpTimer.OnUpdate({ turnLength: 1 });
     24let attacker = 11;
     25let atkPlayerEntity = 1;
     26let attackerOwner = 6;
     27let cmpAttack = ConstructComponent(attacker, "Attack", {Ranged: {ProjectileSpeed: 500, Spread: 0.5, MaxRange: 50, MinRange: 0}});
     28let baseHp = 50;
     29let damage = 5;
     30let target = 21;
     31let targetOwner = 7;
     32
     33var tmp = 0;
     34
     35cmpAttack.GetAttackStrengths = (type) => { return {"hack": 0, "pierce": 0, "crush": damage} };
     36cmpAttack.GetAttackBonus = (type, target) => 1.0;
     37
     38AddMock(atkPlayerEntity, IID_Player, {
     39    GetEnemies: () => [targetOwner],
     40});
     41
     42AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
     43    GetPlayerByID: (id) => atkPlayerEntity,
     44    GetAllPlayerEntities: () => [attackerOwner, targetOwner],
     45});
     46
     47AddMock(SYSTEM_ENTITY, IID_RangeManager, {
     48    ExecuteQueryAroundPos: () => [target],
     49    GetElevationAdaptedRange: (pos, rot, max, bonus, a) => max,
     50});
     51
     52AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
     53    RemoveProjectile: () => {},
     54    LaunchProjectileAtPoint: (ent, pos, speed, gravity) => {},
     55});
     56
     57AddMock(attacker, IID_Position, {
     58    GetPosition: () => new Vector3D(2, 0, 3),
     59    GetRotation: () => new Vector3D(1, 2, 3),
     60    IsInWorld: () => true,
     61});
     62 
     63AddMock(attacker, IID_Ownership, {
     64    GetOwner: () => attackerOwner,
     65});
     66
     67AddMock(target, IID_Position, {
     68    GetPosition: () => new Vector3D(3, 0, 3),
     69    GetPreviousPosition: () => new Vector3D(3, 0, 3),
     70    GetPosition2D: () => new Vector2D(3, 3),
     71    GetRotation: () => new Vector3D(1, 2, 3),
     72    IsInWorld: () => true,
     73});
     74
     75AddMock(target, IID_Health, {
     76    GetHitpoints: () => baseHp,
     77    Reduce: (a) => { tmp = a; return {"killed": false, "change": -a}; },
     78});
     79
     80AddMock(target, IID_DamageReceiver, {
     81    TakeDamage: (hack, pierce, crush) => Engine.QueryInterface(target, IID_Health).Reduce(crush),
     82});
     83
     84AddMock(target, IID_AttackDetection, {
     85    OnGlobalAttacked: (msg) => TS_ASSERT_EQUALS(attackerOwner, msg.attackerOwner),
     86});
     87
     88AddMock(target, IID_Footprint, {
     89    GetShape: () => { return { "type": "circle", "radius": 20 }; },
     90});
     91
     92function TestDamage()
     93{
     94    cmpTimer.OnUpdate({ turnLength: 1 });
     95    TS_ASSERT_EQUALS(damage, tmp);
     96    tmp = 0;
     97}
     98
     99cmpAttack.PerformAttack("Melee", target);
     100TestDamage();
     101
     102cmpAttack.PerformAttack("Ranged", target);
     103TestDamage();
     104
     105cmpAttack.template["Ranged"].Splash = {
     106    FriendlyFire: false,
     107    Range: 10,
     108    Shape: "Circular",
     109};
     110
     111cmpAttack.PerformAttack("Ranged", target);
     112TestDamage();
     113
     114cmpAttack.PerformAttack("Ranged", target);
     115Engine.DestroyEntity(attacker);
     116TestDamage();
  • binaries/data/mods/public/simulation/helpers/Damage.js

    Property changes on: binaries/data/mods/public/simulation/components/tests/test_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