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.Range,
+ "damage": {}
+ };
+ for (let damageType of damageTypes.GetTypes())
+ ret.ProximityDamage.damage[damageType] = getEntityValue("ProximityDamage/Damage/" + damageType);
+ }
+
if (template.Auras && auraTemplates)
{
ret.auras = {};
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,65 @@
};
/**
+ * 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.radius - the 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.proximityBonus - the attack bonus template from the attacker.
+ * @param {number[]} data.playersToDamage - the array of player id's to damage.
+ */
+Damage.prototype.CauseProximityDamage = function(data)
+{
+ // Get nearby entities and define variables
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ let nearEnts;
+ if (cmpRangeManager)
+ nearEnts = cmpRangeManager.ExecuteQuery(data.attacker, 0, data.radius, data.playersToDamage, IID_DamageReceiver);
+ let cmpProximityDamage = Engine.QueryInterface(data.attacker, IID_ProximityDamage);
+ let restrictedClasses = cmpProximityDamage.GetRestrictedClasses();
+ let damageMultiplier = 1;
+
+ // Cycle through all the nearby entities and damage it 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 (restrictedClasses.length && MatchesClassList(targetClasses, 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) / (data.radius * data.radius);
+ else
+ warn("The " + data.shape + " proximity damage shape is not implemented!");
+
+ // Apply bonusses
+ if (data.proximityBonus)
+ damageMultiplier *= GetDamageBonus(ent, data.proximityBonus);
+
+ // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds....
+ 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/ProximityDamage.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/components/ProximityDamage.js
@@ -0,0 +1,194 @@
+function ProximityDamage() {}
+
+ProximityDamage.prototype.bonusesSchema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
+ProximityDamage.prototype.restrictedClassesSchema =
+ "" +
+ "" +
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "" +
+ "";
+
+ProximityDamage.prototype.Schema =
+ "Whether a unit or building inflicts damage to nearby units." +
+ "" +
+ "Circular" +
+ "10" +
+ "true" +
+ "true" +
+ "" +
+ "0.0" +
+ "10.0" +
+ "50.0" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ DamageTypes.BuildSchema("damage strength") +
+ "" +
+ 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 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 radius = ApplyValueModificationsToEntity("ProximityDamage/Range", +this.template.Range, this.entity);
+
+ // Call the Damage-component to find out which entities to damage and damage them
+ if (cmpDamage)
+ cmpDamage.CauseProximityDamage({
+ "attacker": this.entity,
+ "origin": cmpPosition.GetPosition2D(),
+ "radius": radius,
+ "shape": this.template.Shape,
+ "strengths": this.GetProximityDamageStrengths(),
+ "proximityBonus": this.GetBonusTemplate(),
+ "playersToDamage": playersToDamage,
+ "type": "Proximity",
+ "attackerOwner": owner
+ });
+};
+
+// Start the 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 cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.proximityDamageTimer = cmpTimer.SetInterval(this.entity, IID_ProximityDamage, "CauseProximityDamage", 100, 100, 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)
+ 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_ProximityDamage.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/components/tests/test_ProximityDamage.js
@@ -0,0 +1,104 @@
+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", {
+ "Shape": "Circular",
+ "Range": 10,
+ "FriendlyFire": true,
+ "OnlyWhenMoving": false,
+ "Damage": {
+ "Hack": 0.0,
+ "Pierce": 10.0,
+ "Crush": 50.0,
+ }
+});
+
+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,
+ "splashBonus": null,
+ "playersToDamage": playersToDamage,
+ "type": "Proximity",
+ "attackerOwner": player
+};
+
+TS_ASSERT(cmpProximityDamage.GetBonusTemplate() == null);
+
+AddMock(SYSTEM_ENTITY, 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);