Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -40,6 +40,16 @@ "" + ""; +Attack.prototype.continuousDamageSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + @@ -73,6 +83,10 @@ "800" + "1600" + "1000" + + "" + + "1000" + + "5" + + "" + "" + "" + "Cavalry" + @@ -114,6 +128,7 @@ "" + // TODO: it shouldn't be stretched "" + "" + + Attack.prototype.continuousDamageSchema + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + @@ -145,6 +160,10 @@ "" + "" + "" + + Attack.prototype.continuousDamageSchema + + Attack.prototype.bonusesSchema + + Attack.prototype.preferredClassesSchema + + Attack.prototype.restrictedClassesSchema + "" + "" + "" + @@ -477,6 +496,18 @@ return template.Bonuses || null; }; +Attack.prototype.GetContinuousDamage = function(type) +{ + let interval = this.template[type].Continuous ? +this.template[type].Continuous.Interval : 0; + let numberOfIntervals = this.template[type].Continuous ? +this.template[type].Continuous.NumberOfIntervals : 0; + interval = ApplyValueModificationsToEntity( + "ContinuousDamage/" + type + "/Interval", interval, this.entity); + numberOfIntervals = ApplyValueModificationsToEntity( + "ContinuousDamage/" + type + "/NumberOfIntervals", numberOfIntervals, this.entity); + + return { "interval": interval, "numberOfIntervals": numberOfIntervals}; +}; + /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last @@ -485,7 +516,8 @@ Attack.prototype.PerformAttack = function(type, target) { let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); - let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); + let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); + let continuousDamage = this.GetContinuousDamage(type); // If this is a ranged attack, then launch a projectile if (type == "Ranged") @@ -577,7 +609,9 @@ "bonus": this.GetBonusTemplate(type), "isSplash": false, "attackerOwner": attackerOwner, - "attackImpactSound": attackImpactSound + "attackImpactSound": attackImpactSound, + "interval": continuousDamage.interval, + "numberOfIntervals": continuousDamage.numberOfIntervals }; if (this.template[type].Splash) { @@ -624,7 +658,9 @@ "attacker": this.entity, "multiplier": GetDamageBonus(target, this.GetBonusTemplate(type)), "type": type, - "attackerOwner": attackerOwner + "attackerOwner": attackerOwner, + "interval": continuousDamage.interval, + "numberOfIntervals": continuousDamage.numberOfIntervals }); } }; 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 @@ -122,7 +122,9 @@ "direction": data.direction, "playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire), "type": data.type, - "attackerOwner": data.attackerOwner + "attackerOwner": data.attackerOwner, + "interval": data.interval, + "numberOfIntervals": data.numberOfIntervals }); } @@ -157,7 +159,9 @@ "attacker": data.attacker, "multiplier": GetDamageBonus(ent, data.bonus), "type": data.type, - "attackerOwner": data.attackerOwner + "attackerOwner": data.attackerOwner, + "interval": data.interval, + "numberOfIntervals": data.numberOfIntervals }); cmpProjectileManager.RemoveProjectile(data.projectileId); break; @@ -226,7 +230,9 @@ "attacker": data.attacker, "multiplier": damageMultiplier, "type": data.type + ".Splash", - "attackerOwner": data.attackerOwner + "attackerOwner": data.attackerOwner, + "interval": data.interval, + "numberOfIntervals": data.numberOfIntervals }); } }; @@ -240,6 +246,8 @@ * @param {number} data.multiplier - the damage multiplier. * @param {string} data.type - the type of damage. * @param {number} data.attackerOwner - the player id of the attacker. + * @param {number} data.interval - time between damages caused during continuous damage. + * @param {number} data.numberOfIntervals - remaining intervals during continuous damage. */ Damage.prototype.CauseDamage = function(data) { @@ -255,6 +263,12 @@ if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0) cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints()); + if (data.numberOfIntervals && --data.numberOfIntervals > 0) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "CauseDamage", +data.interval, data); + } + if (targetState.killed) this.TargetKilled(data.attacker, data.target, data.attackerOwner); Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -76,6 +76,10 @@ "Spread": 2, "Gravity": 1 }, + "Continuous": { + "Interval": 500, + "NumberOfIntervals": 4 + }, "PreferredClasses": { "_string": "Archer" }, @@ -175,6 +179,11 @@ "friendlyFire": false, "shape": "Circular" }); + + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetContinuousDamage("Ranged"), { + "interval": 500, + "numberOfIntervals": 4 + }); }); for (let className of ["Infantry", "Cavalry"]) 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 @@ -548,3 +548,47 @@ } Test_MissileHit(); + +function Test_ContinuousDamage() +{ + ResetState(); + + let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); + let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + let target = 60; + let damageTaken = false; + let data = { + "attacker": 50, + "target": target, + "type": "Melee", + "strengths": { "hack": 0, "pierce": 0, "crush": 1 }, + "multiplier": 1.0, + "interval": 10000, + "numberOfIntervals": 3 + } + + AddMock(target, IID_DamageReceiver, { + "TakeDamage": (strengths, multiplier) => { damageTaken = true; return { "killed": false, "change": -multiplier * strengths.crush }; } + }); + + // damage scheduled: 0 sec, 10 sec, 20 sec + cmpDamage.CauseDamage(data); + TS_ASSERT(damageTaken); // 0 sec + + damageTaken = false; + cmpTimer.OnUpdate({ turnLength: 9 }); + TS_ASSERT(!damageTaken); // 9 sec + + cmpTimer.OnUpdate({ turnLength: 1 }); + TS_ASSERT(damageTaken); // 10 sec + + damageTaken = false; + cmpTimer.OnUpdate({ turnLength: 10 }); + TS_ASSERT(damageTaken); // 20 sec + + damageTaken = false; + cmpTimer.OnUpdate({ turnLength: 10 }); + TS_ASSERT(!damageTaken); // 30 sec +} + +Test_ContinuousDamage(); Index: binaries/data/mods/public/simulation/templates/units/athen_cavalry_javelinist_a.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/athen_cavalry_javelinist_a.xml +++ binaries/data/mods/public/simulation/templates/units/athen_cavalry_javelinist_a.xml @@ -1,5 +1,22 @@ + + + 0 + 18.0 + 0 + 28.0 + 0.0 + 62.5 + 750 + 1250 + 3.0 + + 1000 + 3 + + + Advanced