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
@@ -116,7 +116,8 @@
this.CauseSplashDamage({
"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,
@@ -151,7 +152,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 +176,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 +189,26 @@
Damage.prototype.CauseSplashDamage = 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 - data.minRange;
// 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 +219,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 +281,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.CauseSplashDamage({
"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,270 @@
+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()
+{
+ let min = ApplyValueModificationsToEntity("ProximityDamage/MinRange", +this.template.MinRange, this.entity);
+ let max = ApplyValueModificationsToEntity("ProximityDamage/MaxRange", +this.template.MaxRange, this.entity);
+ return { "max": max, "min": min };
+};
+
+/**
+ * Work out the timer value with technology effects.
+ * @return {object} - The prepare/repeat timers of the proximity damage.
+ */
+ProximityDamage.prototype.GetTimers = function()
+{
+ let prepare = ApplyValueModificationsToEntity("ProximityDamage/PrepareTime", +this.template.PrepareTime, this.entity);
+ let repeat = ApplyValueModificationsToEntity("ProximityDamage/RepeatTime", +this.template.RepeatTime, this.entity);
+ return { "prepare": prepare, "repeat": repeat };
+};
+
+/**
+ * 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 of DamageTypes.GetTypes())
+ 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.CauseSplashDamage({
+ "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 == "true")
+ {
+ 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.starting": boolean, "msg.error": boolean }} - Whether a move is started and whether an error is thrown.
+ */
+ProximityDamage.prototype.OnMotionChanged = 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,7 +154,8 @@
let data = {
"attacker": attacker,
"origin": origin,
- "radius": 10,
+ "minRange": 0,
+ "maxRange": 10,
"shape": "Linear",
"strengths": { "hack" : 100, "pierce" : 0, "crush": 0 },
"direction": new Vector3D(1, 747, 0),
@@ -165,7 +166,7 @@
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,7 +322,8 @@
cmpDamage.CauseSplashDamage({
"attacker": 50,
"origin": new Vector2D(3, 4),
- "radius": radius,
+ "minRange": 0,
+ "maxRange": radius,
"shape": "Circular",
"strengths": { "hack" : 100, "pierce" : 0, "crush": 0 },
"playersToDamage": [2],
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,105 @@
+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": 1 });
+
+let damagingEnt = 42;
+let player = 1;
+let playersToDamage = [1, 2, 3, 7];
+let pos = new Vector2D(3, 4.2);
+
+AddMock(damagingEnt, IID_Position, {
+ "GetPosition2D": () => pos,
+ "IsInWorld": () => true
+});
+
+AddMock(damagingEnt, IID_Ownership, {
+ "GetOwner": () => player
+});
+
+ApplyValueModificationsToEntity = function(value, stat, ent)
+{
+ if (value == "ProximityDamage/Damage/Pierce" && ent == damagingEnt)
+ return stat + 200;
+ return stat;
+};
+
+let cmpProximityDamage = ConstructComponent(damagingEnt, "ProximityDamage", {
+ "Damage": {
+ "Hack": 0.0,
+ "Pierce": 10.0,
+ "Crush": 50.0,
+ },
+ "MinRange": 10,
+ "MaxRange": 10,
+ "Shape": "Circular",
+ "FriendlyFire": true,
+ "OnlyWhenMoving": false
+});
+
+let modifiedDamage = {
+ "Hack": 0.0,
+ "Pierce": 210.0,
+ "Crush": 50.0
+};
+
+let result = {
+ "attacker": damagingEnt,
+ "origin": pos,
+ "radius": cmpProximityDamage.Range,
+ "shape": cmpProximityDamage.Shape,
+ "strengths": modifiedDamage,
+ "bonus": null,
+ "playersToDamage": playersToDamage,
+ "type": "Proximity",
+ "attackerOwner": player
+};
+
+TS_ASSERT(cmpProximityDamage.GetBonusTemplate() == null);
+
+AddMock(damagingEnt, IID_ProximityDamage, {
+ "CauseProximityDamage": data => TS_ASSERT_UNEVAL_EQUALS(data, result),
+ "GetPlayersToDamage": (owner, friendlyFire) => playersToDamage
+});
+
+TS_ASSERT_UNEVAL_EQUALS(cmpProximityDamage.GetProximityDamageStrengths(), modifiedDamage);
+cmpProximityDamage.CauseProximityDamage();
+
+// Test proximity damage with bonus
+
+cmpProximityDamage = ConstructComponent(6, "ProximityDamage", {
+ "Shape": "Circular",
+ "Range": 10,
+ "FriendlyFire": true,
+ "OnlyWhenMoving": false,
+ "Damage": {
+ "Hack": 0.0,
+ "Pierce": 10.0,
+ "Crush": 50.0,
+ },
+ "RestrictedClasses": {
+ "_string": "Javelin"
+ },
+ "Bonuses": {
+ "Bonus1": {
+ "Civ": "iber"
+ },
+ "Bonus2": {
+ "Classes": "Javelin"
+ },
+ "Bonus3": {
+ "Civ": "athen",
+ "Multiplier": 2
+ }
+ }
+});
+TS_ASSERT(cmpProximityDamage.GetBonusTemplate() != 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