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/simulation/components/Damage.js
===================================================================
--- binaries/data/mods/public/simulation/components/Damage.js
+++ binaries/data/mods/public/simulation/components/Damage.js
@@ -238,6 +238,70 @@
};
/**
+ * Damages units around a given entity.
+ * @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 attacker.
+ * @param {number} data.minRange - The minimal radius of the proximity damage.
+ * @param {number} data.maxRange - The maximal radius of the proximity 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.
+ * @param {number} data.attackerOwner - The player id of the attacker.
+ * @param {Object} data.bonus - The attack bonus template from the attacker.
+ * @param {number[]} data.playersToDamage - The array of player id's to damage.
+ * @param {string[]} data.restrictedClasses - An array of classes not to damage.
+ * @param {Object} data.statusEffects - Status effects e.g. poisoning, burning etc.
+ */
+Damage.prototype.CauseProximityDamage = function(data)
+{
+ // Get nearby entities.
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ let nearEnts = cmpRangeManager.ExecuteQuery(data.attacker, data.minRange, data.maxRange, data.playersToDamage, IID_DamageReceiver);
+ let damageMultiplier = 1;
+
+ // Cycle through all the nearby entities and damage them appropriately based on its distance from the origin.
+ for (let ent of nearEnts)
+ {
+ // Do not damage restricted classes.
+ let cmpIdentity = QueryMiragedInterface(ent, IID_Identity);
+ let targetClasses = cmpIdentity.GetClassesList();
+ if (data.restrictedClasses.length && MatchesClassList(targetClasses, data.restrictedClasses))
+ continue;
+
+ // Calculate distance effects.
+ let cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ if (!cmpPosition || !cmpPosition.IsInWorld())
+ continue;
+ let entityPosition = cmpPosition.GetPosition2D();
+ if (data.shape == 'Circular')
+ damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / Math.square(data.maxRange - data.minRange);
+ else
+ warn("The " + data.shape + " proximity damage shape is not implemented!");
+
+ if (data.bonus)
+ damageMultiplier *= GetDamageBonus(ent, data.bonus);
+
+ if (data.statusEffects)
+ {
+ let cmpStatusReceiver = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
+ if (cmpStatusReceiver)
+ cmpStatusReceiver.InflictEffects(data.statusEffects);
+ }
+
+ // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds etc.
+ this.CauseDamage({
+ "strengths": data.strengths,
+ "target": ent,
+ "attacker": data.attacker,
+ "multiplier": damageMultiplier,
+ "type": data.type,
+ "attackerOwner": data.attackerOwner
+ });
+ }
+};
+
+/**
* Causes damage on a given unit.
* @param {Object} data - The data passed by the caller.
* @param {Object} data.strengths - Data in the form of { 'hack': number, 'pierce': number, 'crush': number }.
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.CauseProximityDamage({
+ "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_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