Index: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js (revision 23992) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js (nonexistent) @@ -1,30 +0,0 @@ -/** - * Calculates the attack damage multiplier against a target. - * @param {entity_id_t} source - The source entity's id. - * @param {entity_id_t} target - The target entity's id. - * @param {string} type - The type of attack. - * @param {Object} template - The bonus' template. - * @return {number} - The source entity's attack bonus against the specified target. - */ -function GetAttackBonus(source, target, type, template) -{ - let attackBonus = 1; - - let cmpIdentity = Engine.QueryInterface(target, IID_Identity); - if (!cmpIdentity) - return 1; - - // Multiply the bonuses for all matching classes - for (let key in template) - { - let bonus = template[key]; - if (bonus.Civ && bonus.Civ !== cmpIdentity.GetCiv()) - continue; - if (!bonus.Classes || MatchesClassList(cmpIdentity.GetClassesList(), bonus.Classes)) - attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source); - } - - return attackBonus; -} - -Engine.RegisterGlobal("GetAttackBonus", GetAttackBonus); Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 23992) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 23993) @@ -1,378 +1,378 @@ -Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("Attack.js"); let entityID = 903; function attackComponentTest(defenderClass, isEnemy, test_function) { ResetState(); { let playerEnt1 = 5; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => playerEnt1 }); AddMock(playerEnt1, IID_Player, { "GetPlayerID": () => 1, "IsEnemy": () => isEnemy }); } let attacker = entityID; AddMock(attacker, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 5, "GetPosition2D": () => new Vector2D(1, 2) }); AddMock(attacker, IID_Ownership, { "GetOwner": () => 1 }); let cmpAttack = ConstructComponent(attacker, "Attack", { "Melee": { "Damage": { "Hack": 11, "Pierce": 5, "Crush": 0 }, "MinRange": 3, "MaxRange": 5, "PreferredClasses": { "_string": "FemaleCitizen" }, "RestrictedClasses": { "_string": "Elephant Archer" }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 2 } } }, "Ranged": { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 }, "MinRange": 10, "MaxRange": 80, "PrepareTime": 300, "RepeatTime": 500, "Projectile": { "Speed": 10, "Spread": 2, "Gravity": 1, "FriendlyFire": "false" }, "PreferredClasses": { "_string": "Archer" }, "RestrictedClasses": { "_string": "Elephant" }, "Splash": { "Shape": "Circular", "Range": 10, "FriendlyFire": "false", "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } } }, "Capture": { "Capture": 8, "MaxRange": 10, }, "Slaughter": {}, "StatusEffect": { "ApplyStatus": { "StatusInternalName": { "StatusName": "StatusShownName", "ApplierTooltip": "ApplierTooltip", "ReceiverTooltip": "ReceiverTooltip", "Duration": 5000, "Stackability": "Stacks", "Modifiers": { "SE": { "Paths": { "_string": "Health/Max" }, "Affects": { "_string": "Unit" }, "Add": 10 } } } }, "MinRange": "10", "MaxRange": "80" } }); let defender = ++entityID; AddMock(defender, IID_Identity, { "GetClassesList": () => [defenderClass], - "HasClass": className => className == defenderClass + "HasClass": className => className == defenderClass, + "GetCiv": () => "civ" }); AddMock(defender, IID_Ownership, { "GetOwner": () => 1 }); AddMock(defender, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 0 }); AddMock(defender, IID_Health, { "GetHitpoints": () => 100 }); test_function(attacker, cmpAttack, defender); } // Validate template getter functions attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), { "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("StatusEffect"), { "ApplyStatus": { "StatusInternalName": { "StatusName": "StatusShownName", "ApplierTooltip": "ApplierTooltip", "ReceiverTooltip": "ReceiverTooltip", "Duration": 5000, "Interval": 0, "Stackability": "Stacks", "Modifiers": { "SE": { "Paths": { "_string": "Health/Max" }, "Affects": { "_string": "Unit" }, "Add": 10 } } } } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { "prepare": 300, "repeat": 500 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Ranged"), 500); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), { "prepare": 0, "repeat": 1000 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Capture"), 1000); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashData("Ranged"), { "attackData": { "Damage": { "Hack": 0, "Pierce": 15, "Crush": 35, }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }, "friendlyFire": false, "radius": 10, "shape": "Circular" }); }); for (let className of ["Infantry", "Cavalry"]) attackComponentTest(className, true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2); TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null); - let getAttackBonus = (s, t, e, splash) => GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null); + let getAttackBonus = (s, t, e, splash) => Attacking.GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1); }); // CanAttack rejects elephant attack due to RestrictedClasses attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false); }); function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false) { attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); }); attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack); }); } testGetBestAttackAgainst("FemaleCitizen", "Melee", undefined); testGetBestAttackAgainst("Archer", "Ranged", undefined); testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter"); testGetBestAttackAgainst("Structure", "Capture", "Capture", true); testGetBestAttackAgainst("Structure", "Ranged", undefined, false); function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity) { ResetState(); let cmpAttack = ConstructComponent(1, "Attack", {}); let timeToTarget = cmpAttack.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); if (timeToTarget === false) return; // Position of the target after that time. let targetPos = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); // Time that the projectile need to reach it. let time = targetPos.horizDistanceTo(selfPosition) / horizSpeed; TS_ASSERT_EQUALS(timeToTarget.toFixed(1), time.toFixed(1)); } testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 4)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 16)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2)); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 23992) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 23993) @@ -1,626 +1,626 @@ -Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/AttackDetection.js"); Engine.LoadComponentScript("interfaces/DelayedDamage.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); Engine.LoadComponentScript("DelayedDamage.js"); Engine.LoadComponentScript("Timer.js"); function Test_Generic() { ResetState(); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); cmpTimer.OnUpdate({ "turnLength": 1 }); let attacker = 11; let atkPlayerEntity = 1; let attackerOwner = 6; let cmpAttack = ConstructComponent(attacker, "Attack", { "Ranged": { "Damage": { "Crush": 5, }, "MaxRange": 50, "MinRange": 0, "Delay": 0, "Projectile": { "Speed": 75.0, "Spread": 0.5, "Gravity": 9.81, "FriendlyFire": "false", "LaunchPoint": { "@y": 3 } } } }); let damage = 5; let target = 21; let targetOwner = 7; let targetPos = new Vector3D(3, 0, 3); let type = "Melee"; let damageTaken = false; cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage }); let data = { "type": "Melee", "attackData": { "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage }, }, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "position": targetPos, "projectileId": 9, "direction": new Vector3D(1, 0, 0) }; AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [targetOwner] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => atkPlayerEntity, "GetAllPlayers": () => [0, 1, 2, 3, 4] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(target, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(target, IID_Health, { "TakeDamage": (effectData, __, ___, bonusMultiplier) => { damageTaken = true; return { "healthChange": -bonusMultiplier * effectData.Crush }; }, }); AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { "MissileHit": () => { damageTaken = true; }, }); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [], "fromStatusEffect": false }, message); }; AddMock(target, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(attacker, IID_Ownership, { "GetOwner": () => attackerOwner, }); AddMock(attacker, IID_Position, { "GetPosition": () => new Vector3D(2, 0, 3), "GetRotation": () => new Vector3D(1, 2, 3), "IsInWorld": () => true, }); function TestDamage() { cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(damageTaken); damageTaken = false; } Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); data.type = "Ranged"; type = data.type; Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); // Check for damage still being dealt if the attacker dies cmpAttack.PerformAttack("Ranged", target); Engine.DestroyEntity(attacker); TestDamage(); atkPlayerEntity = 1; AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [2, 3] }); TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); } Test_Generic(); function TestLinearSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const attacker = 50; const attackerOwner = 1; const origin = new Vector2D(0, 0); let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": attacker, "attackerOwner": attackerOwner, "origin": origin, "radius": 10, "shape": "Linear", "direction": new Vector3D(1, 747, 0), "friendlyFire": false, }; let fallOff = function(x, y) { return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); }; let hitEnts = new Set(); AddMock(attackerOwner, IID_Player, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62], }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(2.2, -0.4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(5, 2), }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(2.2, -0.4)); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0, 0)); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); data.direction = new Vector3D(0.6, 747, 0.8); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1, 2)); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } TestLinearSplashDamage(); function TestCircularSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const radius = 10; let attackerOwner = 1; let fallOff = function(r) { return 1 - r * r / (radius * radius); }; AddMock(attackerOwner, IID_Player, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62, 64], }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), }); // Target on the frontier of the shape AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0)); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(5)); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1)); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(63, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT(false); } }); let cmphealth = AddMock(64, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); let spy = new Spy(cmphealth, "TakeDamage"); Attacking.CauseDamageOverArea({ "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, "attackerOwner": attackerOwner, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", "friendlyFire": false, }); TS_ASSERT_EQUALS(spy._called, 1); } TestCircularSplashDamage(); function Test_MissileHit() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage"); let target = 60; let targetOwner = 1; let targetPos = new Vector3D(3, 10, 0); let hitEnts = new Set(); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetLatestTurnLength": () => 500 }); const radius = 10; let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } }, "target": 60, "attacker": 70, "attackerOwner": 1, "position": targetPos, "direction": new Vector3D(1, 0, 0), "projectileId": 9, "friendlyFire": "false", }; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id == 1 ? 10 : 11, "GetAllPlayers": () => [0, 1] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(60, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(60, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(70, IID_Ownership, { "GetOwner": () => 1, }); AddMock(70, IID_Position, { "GetPosition": () => new Vector3D(0, 0, 0), "GetRotation": () => new Vector3D(0, 0, 0), "IsInWorld": () => true, }); AddMock(10, IID_Player, { "GetEnemies": () => [2] }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); // The main target is not hit but another one is hit. AddMock(60, IID_Position, { "GetPosition": () => new Vector3D(900, 10, 0), "GetPreviousPosition": () => new Vector3D(900, 10, 0), "GetPosition2D": () => new Vector2D(900, 0), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(false); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); AddMock(61, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.from3D(targetPos), "IsInWorld": () => true, }); AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); // Add a splash damage. data.splash = {}; data.splash.friendlyFire = false; data.splash.radius = 10; data.splash.shape = "Circular"; data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); let dealtDamage = 0; AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); dealtDamage += mult * (effectData.Hack + effectData.Pierce + effectData.Crush); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 200 * 0.75); return { "healthChange": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); // Add some hard counters bonus. Engine.DestroyEntity(62); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } }; let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } }; AddMock(61, IID_Identity, { - "GetClassesList": () => ["Cavalry"] + "GetClassesList": () => ["Cavalry"], + "GetCiv": () => "civ" }); data.attackData.Bonuses = bonus; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200); dealtDamage = 0; hitEnts.clear(); data.splash.attackData.Bonuses = splashBonus; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = undefined; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = null; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = {}; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); // Test splash damage with friendly fire. data.splash = {}; data.splash.friendlyFire = true; data.splash.radius = 10; data.splash.shape = "Circular"; data.splash.attackData = { "Damage": { "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); dealtDamage = 0; AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); dealtDamage += mult * (effectData.Pierce + effectData.Crush); return { "healthChange": -mult * (effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); TS_ASSERT_EQUALS(mult * (effectData.Pierce + effectData.Crush), 200 * 0.75); return { "healtChange": -mult * (effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } Test_MissileHit(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 23992) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 23993) @@ -1,74 +1,73 @@ -Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("DeathDamage.js"); let deadEnt = 60; let player = 1; ApplyValueModificationsToEntity = function(value, stat, ent) { if (value == "DeathDamage/Damage/Pierce" && ent == deadEnt) return stat + 200; return stat; }; let template = { "Shape": "Circular", "Range": 10.7, "FriendlyFire": "false", "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 } }; let effects = { "Damage": { "Hack": 0.0, "Pierce": 215.0, "Crush": 35.0 } }; let cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); let playersToDamage = [2, 3, 7]; let pos = new Vector2D(3, 4.2); let result = { "type": "Death", "attackData": effects, "attacker": deadEnt, "attackerOwner": player, "origin": pos, "radius": template.Range, "shape": template.Shape, "friendlyFire": false }; Attacking.CauseDamageOverArea = data => TS_ASSERT_UNEVAL_EQUALS(data, result); Attacking.GetPlayersToDamage = () => playersToDamage; AddMock(deadEnt, IID_Position, { "GetPosition2D": () => pos, "IsInWorld": () => true }); AddMock(deadEnt, IID_Ownership, { "GetOwner": () => player }); TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects); cmpDeathDamage.CauseDeathDamage(); // Test splash damage bonus effects.Bonuses = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } }; template.Bonuses = effects.Bonuses; cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); result.attackData.Bonuses = effects.Bonuses; TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects); cmpDeathDamage.CauseDeathDamage(); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 23992) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 23993) @@ -1,368 +1,399 @@ /** * Provides attack and damage-related helpers under the Attacking umbrella (to avoid name ambiguity with the component). */ function Attacking() {} const DirectEffectsSchema = "" + "" + "" + "" + // Armour requires Foundation to not be a damage type. "Foundation" + "" + "" + "" + "" + "" + "" + "" + ""; const StatusEffectsSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + DirectEffectsSchema + "" + "" + "" + "" + "" + ModificationsSchema + "" + "" + "" + "Ignore" + "Extend" + "Replace" + "Stack" + "" + "" + "" + "" + "" + ""; /** * Builds a RelaxRNG schema of possible attack effects. * See globalscripts/AttackEffects.js for possible elements. * Attacks may also have a "Bonuses" element. * * @return {string} - RelaxNG schema string. */ Attacking.prototype.BuildAttackEffectsSchema = function() { return "" + "" + "" + DirectEffectsSchema + StatusEffectsSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; }; /** * Returns a template-like object of attack effects. */ Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity) { let ret = {}; if (template.Damage) { ret.Damage = {}; let applyMods = damageType => ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity); for (let damageType in template.Damage) ret.Damage[damageType] = applyMods(damageType); } if (template.Capture) ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity); if (template.ApplyStatus) ret.ApplyStatus = this.GetStatusEffectsData(valueModifRoot, template.ApplyStatus, entity); if (template.Bonuses) ret.Bonuses = template.Bonuses; return ret; }; Attacking.prototype.GetStatusEffectsData = function(valueModifRoot, template, entity) { let result = {}; for (let effect in template) { let statusTemplate = template[effect]; result[effect] = { "StatusName": statusTemplate.StatusName, "ApplierTooltip": statusTemplate.ApplierTooltip, "ReceiverTooltip": statusTemplate.ReceiverTooltip, "Duration": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Duration", +(statusTemplate.Duration || 0), entity), "Interval": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Interval", +(statusTemplate.Interval || 0), entity), "Stackability": statusTemplate.Stackability }; Object.assign(result[effect], this.GetAttackEffectsData(valueModifRoot + "/ApplyStatus" + effect, statusTemplate, entity)); if (statusTemplate.Modifiers) result[effect].Modifiers = this.GetStatusEffectsModifications(valueModifRoot, statusTemplate.Modifiers, entity, effect); } return result; }; Attacking.prototype.GetStatusEffectsModifications = function(valueModifRoot, template, entity, effect) { let modifiers = {}; for (let modifier in template) { let modifierTemplate = template[modifier]; modifiers[modifier] = { "Paths": modifierTemplate.Paths, "Affects": modifierTemplate.Affects }; if (modifierTemplate.Add !== undefined) modifiers[modifier].Add = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Add", +modifierTemplate.Add, entity); if (modifierTemplate.Multiply !== undefined) modifiers[modifier].Multiply = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Multiply", +modifierTemplate.Multiply, entity); if (modifierTemplate.Replace !== undefined) modifiers[modifier].Replace = modifierTemplate.Replace; } return modifiers; }; Attacking.prototype.GetTotalAttackEffects = function(effectData, effectType, cmpResistance) { let total = 0; let armourStrengths = cmpResistance ? cmpResistance.GetArmourStrengths(effectType) : {}; for (let type in effectData) total += effectData[type] * Math.pow(0.9, armourStrengths[type] || 0); return total; }; /** * Gives the position of the given entity, taking the lateness into account. * @param {number} ent - Entity id of the entity we are finding the location for. * @param {number} lateness - The time passed since the expected time to fire the function. * @return {Vector3D} The location of the entity. */ Attacking.prototype.InterpolatedLocation = function(ent, lateness) { let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly return undefined; let curPos = cmpTargetPosition.GetPosition(); let prevPos = cmpTargetPosition.GetPreviousPosition(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let turnLength = cmpTimer.GetLatestTurnLength(); return new Vector3D( (curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength, 0, (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength ); }; /** * Test if a point is inside of an entity's footprint. * @param {number} ent - Id of the entity we are checking with. * @param {Vector3D} point - The point we are checking with. * @param {number} lateness - The time passed since the expected time to fire the function. * @return {boolean} True if the point is inside of the entity's footprint. */ Attacking.prototype.TestCollision = function(ent, point, lateness) { let targetPosition = this.InterpolatedLocation(ent, lateness); if (!targetPosition) return false; let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); if (!cmpFootprint) return false; let targetShape = cmpFootprint.GetShape(); if (!targetShape) return false; if (targetShape.type == "circle") return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius; if (targetShape.type == "square") { let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y; let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle); return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2; } warn("TestCollision called with an invalid footprint shape"); return false; }; /** * Get the list of players affected by the damage. * @param {number} attackerOwner - The player id of the attacker. * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged. * @return {number[]} The ids of players need to be damaged. */ Attacking.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) { if (!friendlyFire) return QueryPlayerIDInterface(attackerOwner).GetEnemies(); return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); }; /** * Damages units around a given origin. * @param {Object} data - The data sent by the caller. * @param {string} data.type - The type of damage. * @param {Object} data.attackData - The attack data. * @param {number} data.attacker - The entity id of the attacker. * @param {number} data.attackerOwner - The player 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 {string} data.shape - The shape of the radius. * @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage. * @param {boolean} data.friendlyFire - A flag indicating if allied entities also ought to be damaged. */ Attacking.prototype.CauseDamageOverArea = function(data) { let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire)); 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) { 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) { // Get position of entity relative to splash origin. let relativePos = entityPosition.sub(data.origin); // Get the position relative to the missile direction. let direction = Vector2D.from3D(data.direction); let parallelPos = relativePos.dot(direction); let perpPos = relativePos.cross(direction); // The width of linear splash is one fifth of the normal splash radius. let width = data.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)) * (1 - perpPos * perpPos / (width * width)); else damageMultiplier = 0; } else // In case someone calls this function with an invalid shape. { warn("The " + data.shape + " splash damage shape is not implemented!"); } this.HandleAttackEffects(data.type + ".Splash", data.attackData, ent, data.attacker, data.attackerOwner, damageMultiplier); } }; Attacking.prototype.HandleAttackEffects = function(attackType, attackData, target, attacker, attackerOwner, bonusMultiplier = 1) { - bonusMultiplier *= !attackData.Bonuses ? 1 : GetAttackBonus(attacker, target, attackType, attackData.Bonuses); + bonusMultiplier *= !attackData.Bonuses ? 1 : this.GetAttackBonus(attacker, target, attackType, attackData.Bonuses); let targetState = {}; for (let effectType of g_EffectTypes) { if (!attackData[effectType]) continue; let receiver = g_EffectReceiver[effectType]; let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]); if (!cmpReceiver) continue; Object.assign(targetState, cmpReceiver[receiver.method](attackData[effectType], attacker, attackerOwner, bonusMultiplier)); } if (!Object.keys(targetState).length) return; Engine.PostMessage(target, MT_Attacked, { "type": attackType, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": -(targetState.healthChange || 0), "capture": targetState.captureChange || 0, "statusEffects": targetState.inflictedStatuses || [], "fromStatusEffect": !!attackData.StatusEffect, }); // We do not want an entity to get XP from active Status Effects. if (!!attackData.StatusEffect) return; let cmpPromotion = Engine.QueryInterface(attacker, IID_Promotion); if (cmpPromotion && targetState.xp) cmpPromotion.IncreaseXp(targetState.xp); }; /** * 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[]} players - The players of which we need to check entities. * @param {number} itf - Interface IID that returned entities must implement. Defaults to none. * @return {number[]} The id's of the entities in range of the given point. */ Attacking.prototype.EntitiesNearPoint = function(origin, radius, players, itf = 0) { // If there is insufficient data return an empty array. if (!origin || !radius || !players || !players.length) return []; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.ExecuteQueryAroundPos(origin, 0, radius, players, itf); }; +/** + * Calculates the attack damage multiplier against a target. + * @param {number} source - The source entity's id. + * @param {number} target - The target entity's id. + * @param {string} type - The type of attack. + * @param {Object} template - The bonus' template. + * @return {number} - The source entity's attack bonus against the specified target. + */ +Attacking.prototype.GetAttackBonus = function(source, target, type, template) +{ + let cmpIdentity = Engine.QueryInterface(target, IID_Identity); + if (!cmpIdentity) + return 1; + + let attackBonus = 1; + let targetClasses = cmpIdentity.GetClassesList(); + let targetCiv = cmpIdentity.GetCiv(); + + // Multiply the bonuses for all matching classes. + for (let key in template) + { + let bonus = template[key]; + if (bonus.Civ && bonus.Civ !== targetCiv) + continue; + if (!bonus.Classes || MatchesClassList(targetClasses, bonus.Classes)) + attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source); + } + + return attackBonus; +}; + var AttackingInstance = new Attacking(); Engine.RegisterGlobal("Attacking", AttackingInstance);