Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -222,6 +222,17 @@ ret.deathDamage.damage[damageType] = getEntityValue("DeathDamage/Damage/" + damageType); } + if (template.ProximityDamage) + { + ret.proximityDamage = { + "friendlyFire": template.ProximityDamage.FriendlyFire != "false", + "range": template.ProximityDamage.MaxRange, + "damage": {} + }; + for (let damageType in template.ProximityDamage.Damage) + ret.proximityDamage.damage[damageType] = getEntityValue("ProximityDamage/Damage/" + damageType); + } + if (template.Auras && auraTemplates) { ret.auras = {}; Index: binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- binaries/data/mods/public/gui/common/tooltips.js +++ binaries/data/mods/public/gui/common/tooltips.js @@ -331,6 +331,33 @@ return tooltips.join("\n"); } +function getProximityDamageTooltip(template) +{ + if (!template.proximityDamage) + return ""; + + let tooltips = []; + + let proximityDamageTooltip = sprintf(g_RangeTooltipString["non-relative"][template.proximityDamage.minRange ? "minRange" : "no-minRange"], { + "attackLabel": headerFont("Proximity Damage"), + "damageTypes": damageTypesToText(template.proximityDamage.strengths), + "rangeLabel": headerFont(translate("Range:")), + "minRange": template.proximityDamage.minRange, + "maxRange": template.proximityDamage.maxRange, + "rangeUnit": unitFont(template.proximityDamage.minRange || translatePlural("meter", "meters", template.proximityDamage.maxRange)), + "rate": getSecondsString(template.proximityDamage.rate / 1000) + }); + + if (g_AlwaysDisplayFriendlyFire || template.proximityDamage.friendlyFire) + proximityDamageTooltip += commaFont(translate(", ")) + sprintf(translate("Friendly Fire: %(enabled)s"), { + "enabled": template.proximityDamage.friendlyFire ? translate("Yes") : translate("No") + }); + + tooltips.push(proximityDamageTooltip); + + return tooltips.join("\n"); +} + function getGarrisonTooltip(template) { if (!template.garrisonHolder) Index: binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- binaries/data/mods/public/gui/session/selection_details.js +++ binaries/data/mods/public/gui/session/selection_details.js @@ -286,6 +286,7 @@ Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = [ getAttackTooltip, getSplashDamageTooltip, + getProximityDamageTooltip, getHealerTooltip, getArmorTooltip, getGatherTooltip, Index: binaries/data/mods/public/maps/random/polar_sea_triggers.js =================================================================== --- binaries/data/mods/public/maps/random/polar_sea_triggers.js +++ binaries/data/mods/public/maps/random/polar_sea_triggers.js @@ -52,7 +52,7 @@ continue; // The returned entities are sorted by RangeManager already - let targets = cmpDamage.EntitiesNearPoint(attackerPos, 200, players).filter(ent => { + let targets = cmpDamage.EntitiesNearPoint(attackerPos, 0, 200, players).filter(ent => { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses); }); Index: binaries/data/mods/public/simulation/components/Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/Damage.js +++ binaries/data/mods/public/simulation/components/Damage.js @@ -110,13 +110,13 @@ if (cmpSoundManager && data.attackImpactSound) cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position); - // Do this first in case the direct hit kills the target + // Do this first in case the direct hit kills the target. if (data.isSplash) - { this.CauseDamageOverArea({ "attacker": data.attacker, "origin": Vector2D.from3D(data.position), - "radius": data.radius, + "minRange": 0, + "maxRange": data.radius, "shape": data.shape, "strengths": data.splashStrengths, "splashBonus": data.splashBonus, @@ -125,7 +125,6 @@ "type": data.type, "attackerOwner": data.attackerOwner }); - } let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); @@ -151,7 +150,7 @@ // If we didn't hit the main target look for nearby units let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner); - let ents = this.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies()); + let ents = this.EntitiesNearPoint(Vector2D.from3D(data.position), 0, targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies()); for (let ent of ents) { if (!this.TestCollision(ent, data.position, lateness)) @@ -175,7 +174,8 @@ * @param {Object} data - The data sent by the caller. * @param {number} data.attacker - The entity id of the attacker. * @param {Vector2D} data.origin - The origin of the projectile hit. - * @param {number} data.radius - The radius of the splash damage. + * @param {number} data.minRange - The minimal radius of the splash damage. + * @param {number} data.maxRange - The maximal radius of the splash damage. * @param {string} data.shape - The shape of the radius. * @param {Object} data.strengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }. * @param {string} data.type - The type of damage. @@ -187,16 +187,26 @@ Damage.prototype.CauseDamageOverArea = function(data) { // Get nearby entities and define variables - let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage); + let nearEnts = this.EntitiesNearPoint(data.origin, data.minRange, data.maxRange, data.playersToDamage); let damageMultiplier = 1; + let radius = data.maxRange; // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. for (let ent of nearEnts) { - let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D(); - if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction - damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius); - else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles) + // Do not damage self when causing proximity damage. + if (ent == data.attacker && data.type == "Proximity") + continue; + let cmpPosition = Engine.QueryInterface(ent, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + continue; + let entityPosition = cmpPosition.GetPosition2D(); + + // Circular effect with quadratic falloff in every direction. + if (data.shape == 'Circular') + damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (radius * radius); + // Linear effect with quadratic falloff in two directions (only used for certain missiles). + else if (data.shape == 'Linear' && data.direction) { // Get position of entity relative to splash origin. let relativePos = entityPosition.sub(data.origin); @@ -207,20 +217,19 @@ let perpPos = relativePos.cross(direction); // The width of linear splash is one fifth of the normal splash radius. - let width = data.radius / 5; + let width = radius / 5; // Check that the unit is within the distance splash width of the line starting at the missile's // landing point which extends in the direction of the missile for length splash radius. if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions - damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) * + damageMultiplier = (1 - parallelPos * parallelPos / (radius * radius)) * (1 - perpPos * perpPos / (width * width)); else damageMultiplier = 0; } - else // In case someone calls this function with an invalid shape. - { + // In case someone calls this function with an invalid shape. + else warn("The " + data.shape + " splash damage shape is not implemented!"); - } if (data.splashBonus) damageMultiplier *= GetDamageBonus(data.attacker, ent, data.type, data.splashBonus); @@ -270,17 +279,18 @@ /** * Gets entities near a give point for given players. * @param {Vector2D} origin - The point to check around. - * @param {number} radius - The radius around the point to check. + * @param {number} minRange - The maximum distance the entities may have from the point to check. + * @param {number} maxRange - The minumum distance the entities may have from the point to check. * @param {number[]} players - The players of which we need to check entities. * @return {number[]} The id's of the entities in range of the given point. */ -Damage.prototype.EntitiesNearPoint = function(origin, radius, players) +Damage.prototype.EntitiesNearPoint = function(origin, minRange, maxRange, players) { // If there is insufficient data return an empty array. - if (!origin || !radius || !players) + if (!origin || !maxRange || !players) return []; - return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver); + return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, minRange, maxRange, players, IID_DamageReceiver); }; /** Index: binaries/data/mods/public/simulation/components/DeathDamage.js =================================================================== --- binaries/data/mods/public/simulation/components/DeathDamage.js +++ binaries/data/mods/public/simulation/components/DeathDamage.js @@ -82,7 +82,8 @@ cmpDamage.CauseDamageOverArea({ "attacker": this.entity, "origin": pos, - "radius": radius, + "minRange": 0, + "maxRange": radius, "shape": this.template.Shape, "strengths": this.GetDeathDamageStrengths(), "splashBonus": this.GetBonusTemplate(), Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -436,6 +436,15 @@ if (cmpArmour) ret.armour = cmpArmour.GetArmourStrengths(); + let cmpProximityDamage = Engine.QueryInterface(ent, IID_ProximityDamage); + if (cmpProximityDamage) + ret.proximityDamage = { + "strengths": cmpProximityDamage.GetProximityDamageStrengths(), + "minRange": cmpProximityDamage.GetRange().min, + "maxRange": cmpProximityDamage.GetRange().max, + "rate": cmpProximityDamage.GetTimers().repeat + }; + let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { Index: binaries/data/mods/public/simulation/components/ProximityDamage.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/ProximityDamage.js @@ -0,0 +1,273 @@ +function ProximityDamage() {} + +ProximityDamage.prototype.statusEffectsSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + +ProximityDamage.prototype.bonusesSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + +ProximityDamage.prototype.restrictedClassesSchema = + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + + ""; + +ProximityDamage.prototype.Schema = + "Whether a unit or building inflicts damage to nearby units." + + "" + + "" + + "0.0" + + "10.0" + + "50.0" + + "" + + "0" + + "10" + + "true" + + "Circular" + + "true" + + "0" + + "200" + + "1000" + + "" + + "" + + "Support" + + "2" + + "" + + "" + + "" + + "" + + DamageTypes.BuildSchema("damage strength") + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ProximityDamage.prototype.statusEffectsSchema + + ProximityDamage.prototype.restrictedClassesSchema + + ProximityDamage.prototype.bonusesSchema; + +ProximityDamage.prototype.Init = function() +{ +}; + +/** + * Find the entities not to be damaged by this proximity damage. + * @return {Array} - Classes not to be damaged by this proximity damage. + */ +ProximityDamage.prototype.GetRestrictedClasses = function() +{ + if (this.template.RestrictedClasses && + this.template.RestrictedClasses._string) + return this.template.RestrictedClasses._string.split(/\s+/); + + return []; +}; + +ProximityDamage.prototype.Serialize = function() +{ + return { "proximityDamageTimer": this.proximityDamageTimer }; +}; + +ProximityDamage.prototype.Deserialize = function(data) +{ + this.proximityDamageTimer = data.proximityDamageTimer; +}; + +/** + * Work out the range value with technology effects. + * @return {object} - The min/max range values of the proximity damage. + */ +ProximityDamage.prototype.GetRange = function() +{ + return { + "min": ApplyValueModificationsToEntity("ProximityDamage/MinRange", +(this.template.MinRange || 0), this.entity), + "max": ApplyValueModificationsToEntity("ProximityDamage/MaxRange", +this.template.MaxRange, this.entity) + }; +}; + +/** + * Work out the timer value with technology effects. + * @return {object} - The prepare/repeat timers of the proximity damage. + */ +ProximityDamage.prototype.GetTimers = function() +{ + return { + "prepare": ApplyValueModificationsToEntity("ProximityDamage/PrepareTime", +this.template.PrepareTime, this.entity), + "repeat": ApplyValueModificationsToEntity("ProximityDamage/RepeatTime", +this.template.RepeatTime, this.entity) + }; +}; + +/** + * Work out the damage values with technology effects. + * @return {Array} modifications to the damage types. + */ +ProximityDamage.prototype.GetProximityDamageStrengths = function() +{ + let applyMods = damageType => + ApplyValueModificationsToEntity("ProximityDamage/Damage/" + damageType, +(this.template.Damage[damageType] || 0), this.entity); + + let ret = {}; + for (let damageType in this.template.Damage) + ret[damageType] = applyMods(damageType); + + return ret; +}; + +/** + * Handle any bonuses with this damage type. + * @return {Array} - Bonuses to apply. + */ +ProximityDamage.prototype.GetBonusTemplate = function() +{ + return this.template.Bonuses || null; +}; + +ProximityDamage.prototype.CauseProximityDamage = function() +{ + // Return when this entity is otherworldly or garrisoned + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + + let owner; + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (cmpOwnership) + owner = cmpOwnership.GetOwner(); + if (owner == INVALID_PLAYER) + warn("Unit causing proximity damage does not have any owner."); + + // Get an array of the players which ought to be damaged. + let playersToDamage; + let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); + if (cmpDamage) + playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.FriendlyFire != "false"); + + let radii = this.GetRange(); + + // Call the Damage component to find out which entities to damage and damage them. + if (cmpDamage) + cmpDamage.CauseDamageOverArea({ + "attacker": this.entity, + "origin": cmpPosition.GetPosition2D(), + "minRange": radii.min, + "maxRange": radii.max, + "shape": this.template.Shape, + "strengths": this.GetProximityDamageStrengths(), + "bonus": this.GetBonusTemplate(), + "playersToDamage": playersToDamage, + "restrictedClasses": this.GetRestrictedClasses(), + "type": "Proximity", + "attackerOwner": owner, + "statusEffects": this.template.StatusEffects + }); +}; + +// Start the proximity damage timer when no timer exists. +ProximityDamage.prototype.CheckTimer = function() +{ + // If the unit only causes proximity damage whilst moving, disable timer when not moving. + if (this.template.OnlyWhenMoving != false) + { + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + if (!cmpUnitMotion || !cmpUnitMotion.IsMoving()) + { + // We don't need a timer, disable if one exists. + if (this.proximityDamageTimer) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.proximityDamageTimer); + delete this.proximityDamageTimer; + } + return; + } + } + // We need a timer, enable if one doesn't exist. + if (this.proximityDamageTimer) + return; + + let timers = this.GetTimers(); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.proximityDamageTimer = cmpTimer.SetInterval(this.entity, IID_ProximityDamage, "CauseProximityDamage", timers.prepare, timers.repeat, undefined); +}; + +/** + * Check timer when a unit changes motion. + * @param {{ "msg.to": string }} - The state to which the unit has changed. + */ +ProximityDamage.prototype.OnUnitAIStateChanged = function(msg) +{ + this.CheckTimer(); +}; + +/** + * Check timer when a unit changes ownership. + * @param {{ "msg.from": number, "msg.to": number }} - From which player to which player the ownership is changed. + */ +ProximityDamage.prototype.OnOwnershipChanged = function(msg) +{ + if (msg.to == INVALID_PLAYER) + { + if (this.proximityDamageTimer) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.proximityDamageTimer); + delete this.proximityDamageTimer; + } + return; + } + + this.CheckTimer(); +}; + +Engine.RegisterComponentType(IID_ProximityDamage, "ProximityDamage", ProximityDamage); Index: binaries/data/mods/public/simulation/components/interfaces/ProximityDamage.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/ProximityDamage.js @@ -0,0 +1 @@ +Engine.RegisterInterface("ProximityDamage"); Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -154,18 +154,19 @@ let data = { "attacker": attacker, "origin": origin, - "radius": 10, + "minRange": 0, + "maxRange": 10, "shape": "Linear", - "strengths": { "hack" : 100, "pierce" : 0, "crush": 0 }, + "strengths": { "hack": 100, "pierce": 0, "crush": 0 }, "direction": new Vector3D(1, 747, 0), "playersToDamage": [2], "type": "Ranged", "attackerOwner": attackerOwner }; - let fallOff = function(x,y) + let fallOff = function(x, y) { - return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); + return (1 - x * x / (data.maxRange * data.maxRange)) * (1 - 25 * y * y / (data.maxRange * data.maxRange)); }; let hitEnts = new Set(); @@ -178,14 +179,17 @@ AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(2.2, -0.4), + "IsInWorld": () => true }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), + "IsInWorld": () => true }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(5, 2), + "IsInWorld": () => true }); AddMock(60, IID_DamageReceiver, { @@ -257,23 +261,28 @@ AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), + "IsInWorld": () => true }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), + "IsInWorld": () => true }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), + "IsInWorld": () => true }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), + "IsInWorld": () => true }); // Target on the frontier of the shape AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), + "IsInWorld": () => true }); AddMock(60, IID_DamageReceiver, { @@ -313,9 +322,10 @@ cmpDamage.CauseDamageOverArea({ "attacker": 50, "origin": new Vector2D(3, 4), - "radius": radius, + "minRange": 0, + "maxRange": radius, "shape": "Circular", - "strengths": { "hack" : 100, "pierce" : 0, "crush": 0 }, + "strengths": { "hack": 100, "pierce": 0, "crush": 0 }, "playersToDamage": [2], "type": "Ranged", "attackerOwner": 1 Index: binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js +++ binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js @@ -42,7 +42,8 @@ let result = { "attacker": deadEnt, "origin": pos, - "radius": template.Range, + "minRange": 0, + "maxRange": template.Range, "shape": template.Shape, "strengths": modifiedDamage, "splashBonus": null, Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -8,6 +8,7 @@ Engine.LoadComponentScript("interfaces/CeasefireManager.js"); Engine.LoadComponentScript("interfaces/DamageReceiver.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); +Engine.LoadComponentScript("interfaces/ProximityDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Index: binaries/data/mods/public/simulation/components/tests/test_ProximityDamage.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_ProximityDamage.js @@ -0,0 +1,121 @@ +Engine.LoadHelperScript("DamageBonus.js"); +Engine.LoadHelperScript("DamageTypes.js"); +Engine.LoadHelperScript("ValueModification.js"); +Engine.LoadComponentScript("interfaces/AuraManager.js"); +Engine.LoadComponentScript("interfaces/Damage.js"); +Engine.LoadComponentScript("interfaces/ProximityDamage.js"); +Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("ProximityDamage.js"); +Engine.LoadComponentScript("Timer.js"); + +let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); +cmpTimer.OnUpdate({ "turnLength": 100 }); + +let damagingEnt = 42; +let player = 1; +let playersToDamage = [1, 2, 3, 7]; +let pos = new Vector2D(3, 4.2); + +ApplyValueModificationsToEntity = function(value, stat, ent) +{ + return stat; +}; + +let template = { + "Damage": { + "Pierce": 10.0, + "Crush": 50.0, + }, + "MinRange": 4, + "MaxRange": 9, + "PrepareTime": 0, + "RepeatTime": 100, + "Shape": "Circular", + "FriendlyFire": true, + "OnlyWhenMoving": false +}; + +let cmpProximityDamage = ConstructComponent(damagingEnt, "ProximityDamage", template); + +AddMock(damagingEnt, IID_Position, { + "GetPosition2D": () => pos, + "IsInWorld": () => true +}); + +AddMock(damagingEnt, IID_Ownership, { + "GetOwner": () => player +}); + +let result = { + "attacker": damagingEnt, + "origin": pos, + "minRange": template.MinRange, + "maxRange": template.MaxRange, + "shape": template.Shape, + "strengths": template.Damage, + "bonus": null, + "playersToDamage": playersToDamage, + "restrictedClasses": [], + "type": "Proximity", + "attackerOwner": player, + "statusEffects": template.StatusEffects +}; + +TS_ASSERT(cmpProximityDamage.GetBonusTemplate() == null); + +AddMock(SYSTEM_ENTITY, IID_Damage, { + "CauseDamageOverArea": data => TS_ASSERT_UNEVAL_EQUALS(data, result), + "GetPlayersToDamage": (owner, friendlyFire) => playersToDamage +}); + +TS_ASSERT_UNEVAL_EQUALS(cmpProximityDamage.GetProximityDamageStrengths(), template.Damage); +cmpProximityDamage.CauseProximityDamage(); + +// Test getting bonuses. + +template.Bonuses = { + "Bonus1": { + "Civ": "iber", + "Multiplier": 1.5 + }, + "Bonus2": { + "Classes": "Javelin", + "Multiplier": 4 + }, + "Bonus3": { + "Civ": "athen", + "Multiplier": 2 + } +}; +cmpProximityDamage = ConstructComponent(damagingEnt, "ProximityDamage", template); +TS_ASSERT_UNEVAL_EQUALS(cmpProximityDamage.GetBonusTemplate(), template.Bonuses); + +// Test timer. + +// Timer needed because we do not need to move to cause damage. +cmpProximityDamage.CheckTimer(); +TS_ASSERT(cmpProximityDamage.proximityDamageTimer != null); + +// Timer not needed because we need to move to cause damage. +template.OnlyWhenMoving = true; +cmpProximityDamage = ConstructComponent(damagingEnt, "ProximityDamage", template); +AddMock(damagingEnt, IID_UnitMotion, { + "IsMoving": () => false +}); +cmpProximityDamage.CheckTimer(); +TS_ASSERT(cmpProximityDamage.proximityDamageTimer == null); + +// Timer needed because we need to move to cause damage. +AddMock(damagingEnt, IID_UnitMotion, { + "IsMoving": () => true +}); +cmpProximityDamage.CheckTimer(); +TS_ASSERT(cmpProximityDamage.proximityDamageTimer != null); + +// Timer not needed because we stopped moving. +AddMock(damagingEnt, IID_UnitMotion, { + "IsMoving": () => false +}); +cmpProximityDamage.CheckTimer(); +TS_ASSERT(cmpProximityDamage.proximityDamageTimer == null); Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml @@ -21,6 +21,22 @@ 2 + + + 0.0 + 0.0 + 10.0 + + 0 + 10 + true + Circular + true + 0 + 100 + 0 + Structure Cavalry Ship Elephant + 1 15