Add a type argument for CanAttack method of Attack component. Patch by bb. Fixes #4220.

Differential Revision: https://code.wildfiregames.com/D122
This was SVN commit r19528.
This commit is contained in:
fatherbushido 2017-05-07 19:32:59 +00:00
parent ca89fa35b9
commit 1c64742edc
4 changed files with 109 additions and 51 deletions

View file

@ -136,9 +136,10 @@ var unitActions =
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 @@ var unitActions =
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id
"target": targetState.id,
"types": ["!Capture"]
})
};
},
@ -443,7 +445,7 @@ var unitActions =
return true;
},
"getActionInfo": function(entState, targetState)
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceSupply)
return false;
@ -1059,6 +1061,7 @@ var g_EntityCommands =
unloadAll();
},
},
"delete": {
"getInfo": function(entState)
{
@ -1097,6 +1100,7 @@ var g_EntityCommands =
openDeleteDialog(selection);
},
},
"stop": {
"getInfo": function(entState)
{

View file

@ -1,5 +1,7 @@
function Attack() {}
var g_AttackTypes = ["Melee", "Ranged", "Capture"];
Attack.prototype.bonusesSchema =
"<optional>" +
"<element name='Bonuses'>" +
@ -188,9 +190,15 @@ Attack.prototype.Init = function()
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 @@ Attack.prototype.GetRestrictedClasses = function(type)
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 @@ Attack.prototype.CanAttack = function(target)
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 @@ Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
// 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);

View file

@ -450,6 +450,7 @@ GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = cmpAttack.GetAttackStrengths(type);
@ -1843,37 +1844,10 @@ GuiInterface.prototype.GetTradingDetails = function(player, data)
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 @@ let exposedFunctions = {
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanCapture": 1,
"CanAttack": 1,
"GetBatchTime": 1,

View file

@ -10,7 +10,7 @@ Engine.LoadComponentScript("Attack.js");
let entityID = 903;
function attackComponentTest(defenderClass, test_function)
function attackComponentTest(defenderClass, isEnemy, test_function)
{
ResetState();
@ -22,7 +22,8 @@ function attackComponentTest(defenderClass, test_function)
});
AddMock(playerEnt1, IID_Player, {
"GetPlayerID": () => 1
"GetPlayerID": () => 1,
"IsEnemy": () => isEnemy
});
}
@ -87,6 +88,10 @@ function attackComponentTest(defenderClass, test_function)
"HasClass": className => className == defenderClass
});
AddMock(defender, IID_Ownership, {
"GetOwner": () => 1
});
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 0
@ -96,9 +101,19 @@ function attackComponentTest(defenderClass, test_function)
}
// 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 @@ attackComponentTest(undefined, (attacker, cmpAttack, defender) => {
});
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 @@ for (let className of ["Infantry", "Cavalry"])
});
// 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 @@ function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = false)
});
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 @@ function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = 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);
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");