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 @@ -232,6 +232,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,180 @@ +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 = null; // We have no dynamic state to save + +/** + * Work out the damage values with technology effects + * @return {Array} modifications to the damage types + */ +ProximityDamage.prototype.GetProximityDamageStrengths = function() +{ + let applyMods = damageType => + ApplyValueModificationsToEntity("ProximityDamage/" + damageType, +(this.template[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,100 @@ +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/Pierce" && ent == damagingEnt) + return stat + 200; + return stat; +}; + +let cmpProximityDamage = ConstructComponent(damagingEnt, "ProximityDamage", { + "Shape": "Circular", + "Range": 10, + "FriendlyFire": true, + "OnlyWhenMoving": false, + "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, + "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);