Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js +++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js @@ -136,9 +136,10 @@ return false; return { - "possible": Engine.GuiInterfaceCall("CanCapture", { + "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, - "target": targetState.id + "target": targetState.id, + "types": ["Capture"] }) }; }, @@ -183,7 +184,8 @@ return { "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, - "target": targetState.id + "target": targetState.id, + "types": ["!Capture"] }) }; }, @@ -443,7 +445,7 @@ return true; }, - "getActionInfo": function(entState, targetState) + "getActionInfo": function(entState, targetState) { if (!targetState.resourceSupply) return false; @@ -1059,6 +1061,7 @@ unloadAll(); }, }, + "delete": { "getInfo": function(entState) { @@ -1097,6 +1100,7 @@ openDeleteDialog(selection); }, }, + "stop": { "getInfo": function(entState) { Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js @@ -1,5 +1,7 @@ function Attack() {} +var g_AttackTypes = ["Melee", "Ranged", "Capture"]; + Attack.prototype.bonusesSchema = "" + "" + @@ -188,9 +190,15 @@ Attack.prototype.Serialize = null; // we have no dynamic state to save -Attack.prototype.GetAttackTypes = function() +Attack.prototype.GetAttackTypes = function(wantedTypes) { - return ["Melee", "Ranged", "Capture"].filter(type => !!this.template[type]); + let types = g_AttackTypes.filter(type => !!this.template[type]); + if (!wantedTypes) + return types; + + let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); + return types.filter(type => wantedTypes.indexOf("!" + type) == -1 && + (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); }; Attack.prototype.GetPreferredClasses = function(type) @@ -211,7 +219,7 @@ return []; }; -Attack.prototype.CanAttack = function(target) +Attack.prototype.CanAttack = function(target, wantedTypes) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) @@ -222,20 +230,36 @@ if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; + let cmpIdentity = Engine.QueryInterface(target, IID_Identity); + if (!cmpIdentity) + return false; + + let targetClasses = cmpIdentity.GetClassesList(); + if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && + (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length)) + return true; + + let cmpEntityPlayer = QueryOwnerInterface(this.entity); + let cmpTargetPlayer = QueryOwnerInterface(target); + if (!cmpTargetPlayer || !cmpEntityPlayer) + return false; + + let types = this.GetAttackTypes(wantedTypes); + let entityOwner = cmpEntityPlayer.GetPlayerID(); + let targetOwner = cmpTargetPlayer.GetPlayerID(); + let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); + // Check if the relative height difference is larger than the attack range // If the relative height is bigger, it means they will never be able to // reach each other, no matter how close they come. let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); - const cmpIdentity = Engine.QueryInterface(target, IID_Identity); - if (!cmpIdentity) - return undefined; - - const targetClasses = cmpIdentity.GetClassesList(); - - for (let type of this.GetAttackTypes()) + for (let type of types) { - if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable)) + if (type != "Capture" && !cmpEntityPlayer.IsEnemy(targetOwner)) + continue; + + if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) continue; if (heightDiff > this.GetRange(type).max) @@ -301,7 +325,7 @@ { // TODO: Formation against formation needs review let types = this.GetAttackTypes(); - return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1); + return g_AttackTypes.find(attack => types.indexOf(attack) != -1); } let cmpIdentity = Engine.QueryInterface(target, IID_Identity); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -450,6 +450,7 @@ let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; + for (let type of types) { ret.attack[type] = cmpAttack.GetAttackStrengths(type); @@ -1843,37 +1844,10 @@ return result; }; -GuiInterface.prototype.CanCapture = function(player, data) -{ - let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); - if (!cmpAttack) - return false; - - let owner = QueryOwnerInterface(data.entity).GetPlayerID(); - - let cmpCapturable = QueryMiragedInterface(data.target, IID_Capturable); - if (cmpCapturable && cmpCapturable.CanCapture(owner) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1) - return cmpAttack.CanAttack(data.target); - - return false; -}; - GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); - if (!cmpAttack) - return false; - - let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); - let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); - if (!cmpEntityPlayer || !cmpTargetPlayer) - return false; - - // if the owner is an enemy, it's up to the attack component to decide - if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) - return cmpAttack.CanAttack(data.target); - - return false; + return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* @@ -2018,7 +1992,6 @@ "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, - "CanCapture": 1, "CanAttack": 1, "GetBatchTime": 1, 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 +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -10,7 +10,7 @@ let entityID = 903; -function attackComponentTest(defenderClass, test_function) +function attackComponentTest(defenderClass, isEnemy, test_function) { ResetState(); @@ -22,7 +22,8 @@ }); AddMock(playerEnt1, IID_Player, { - "GetPlayerID": () => 1 + "GetPlayerID": () => 1, + "IsEnemy": () => isEnemy }); } @@ -87,6 +88,10 @@ "HasClass": className => className == defenderClass }); + AddMock(defender, IID_Ownership, { + "GetOwner": () => 1 + }); + AddMock(defender, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 0 @@ -96,9 +101,19 @@ } // Validate template getter functions -attackComponentTest(undefined, (attacker, cmpAttack, defender) => { +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 }); @@ -122,7 +137,7 @@ }); for (let className of ["Infantry", "Cavalry"]) - attackComponentTest(className, (attacker, cmpAttack, defender) => { + attackComponentTest(className, true, (attacker, cmpAttack, defender) => { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Melee", defender), className == "Cavalry" ? 2 : 1); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Ranged", defender), 1); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Capture", defender), 1); @@ -130,13 +145,13 @@ }); // CanAttack rejects elephant attack due to RestrictedClasses -attackComponentTest("Elephant", (attacker, cmpAttack, defender) => { +attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false); }); function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = false) { - attackComponentTest(defenderClass, (attacker, cmpAttack, defender) => { + attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { @@ -147,6 +162,14 @@ }); 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) @@ -155,6 +178,40 @@ 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); + + let attack = undefined; + if (defenderClass == "Domestic") + attack = "Slaughter"; + else if (defenderClass == "Structure") + attack = "Capture"; + + for (let ac of allowCapturing) + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); + }); } testGetBestAttackAgainst("FemaleCitizen", "Melee");