Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg
+++ binaries/data/config/default.cfg
@@ -327,21 +327,23 @@
stop = "H" ; Stop the current action
backtowork = "Y" ; The unit will go back to work
unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected
-unloadturrets = "U" ; Unload turreted units.
-leaveturret = "U" ; Leave turret point.
+unloadturrets = "U" ; Unload turreted units.
+leaveturret = "U" ; Leave turret point.
move = "" ; Modifier to move to a point instead of another action (e.g. gather)
-attack = Ctrl ; Modifier to attack instead of another action (e.g. capture)
+attacknocapture = Ctrl ; Modifier to attack instead of another action (e.g. capture)
attackmove = Ctrl ; Modifier to attackmove when clicking on a point
attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point
+attackprojectile = Alt ; Modifier to attack using a projectile (generally long ranged attacks)
+attacknoprojectile= "I" ; Modifier to attack without using a projectile (generally short ranged attacks)
garrison = Ctrl ; Modifier to garrison when clicking on building
-occupyturret = Ctrl ; Modifier to occupy a turret when clicking on a turret holder.
+occupyturret = Ctrl ; Modifier to occupy a turret when clicking on a turret holder.
autorallypoint = Ctrl ; Modifier to set the rally point on the building itself
guard = "G" ; Modifier to escort/guard when clicking on unit/building
patrol = "P" ; Modifier to patrol a unit
repair = "J" ; Modifier to repair when clicking on building/mechanical unit
queue = Shift ; Modifier to queue unit orders instead of replacing
pushorderfront = "" ; Modifier to push unit orders to the front instead of replacing.
-orderone = Alt ; Modifier to order only one entity in selection.
+orderone = "O" ; Modifier to order only one entity in selection.
batchtrain = Shift ; Modifier to train units in batches
massbarter = Shift ; Modifier to barter bunch of resources
masstribute = Shift ; Modifier to tribute bunch of resources
@@ -371,7 +373,7 @@
menu.toggle = "F10" ; Toggle in-game menu
diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page
barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page
-objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page
+objectives.toggle = "Ctrl+N" ; Toggle in-game objectives page
tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel
[hotkey.session.savedgames]
Index: binaries/data/mods/public/art/actors/units/persians/infantry_spearman_c2.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/persians/infantry_spearman_c2.xml
+++ binaries/data/mods/public/art/actors/units/persians/infantry_spearman_c2.xml
@@ -77,6 +77,11 @@
+
+
+
+
+
player_trans_spec_helmet.xml
Index: binaries/data/mods/public/art/textures/cursors/action-attack-projectile.txt
===================================================================
--- /dev/null
+++ binaries/data/mods/public/art/textures/cursors/action-attack-projectile.txt
@@ -0,0 +1 @@
+1 1
Index: binaries/data/mods/public/art/textures/cursors/action-attack.txt
===================================================================
--- binaries/data/mods/public/art/textures/cursors/action-attack.txt
+++ binaries/data/mods/public/art/textures/cursors/action-attack.txt
@@ -1 +0,0 @@
-1 1
Index: binaries/data/mods/public/art/textures/cursors/action-capture.txt
===================================================================
--- binaries/data/mods/public/art/textures/cursors/action-capture.txt
+++ binaries/data/mods/public/art/textures/cursors/action-capture.txt
@@ -1 +0,0 @@
-1 1
Index: binaries/data/mods/public/globalscripts/AttackEffects.js
===================================================================
--- binaries/data/mods/public/globalscripts/AttackEffects.js
+++ binaries/data/mods/public/globalscripts/AttackEffects.js
@@ -24,8 +24,10 @@
this.effectReceivers.push({
"type": data.code,
+ "cmp": data.cmp,
"IID": data.IID,
- "method": data.method
+ "method": data.method,
+ "getRelativeEffectMethod": data.getRelativeEffectMethod
});
}
@@ -35,8 +37,10 @@
effectsDataObj[b.type]
);
this.effectReceivers.sort(effSort);
+ this.effectCodes = this.effectReceivers.map(receiver => receiver.type);
deepfreeze(this.effectReceivers);
+ deepfreeze(this.effectCodes);
}
/**
@@ -46,4 +50,20 @@
{
return this.effectReceivers;
}
+
+ /**
+ * @return {string[]} - List of the possible effect codes.
+ */
+ Codes()
+ {
+ return this.effectCodes;
+ }
+
+ /**
+ * @return {Object} - Get the data from the given effect data.
+ */
+ GetReceiverFromCode(type)
+ {
+ return this.effectReceivers.find(receiver => receiver.type == type);
+ }
}
Index: binaries/data/mods/public/globalscripts/tests/test_AttackEffects.js
===================================================================
--- binaries/data/mods/public/globalscripts/tests/test_AttackEffects.js
+++ binaries/data/mods/public/globalscripts/tests/test_AttackEffects.js
@@ -1,31 +1,50 @@
-let effects = {
+const effects = {
"eff_A": {
"code": "a",
"name": "A",
"order": "2",
+ "cmp": "A",
"IID": "IID_A",
- "method": "doA"
+ "method": "doA",
+ "getRelativeEffectMethod": "getA"
},
"eff_B": {
"code": "b",
"name": "B",
"order": "1",
+ "cmp": "B",
"IID": "IID_B",
- "method": "doB"
+ "method": "doB",
+ "getRelativeEffectMethod": "getB"
}
};
Engine.ListDirectoryFiles = () => Object.keys(effects);
Engine.ReadJSONFile = (file) => effects[file];
-let attackEffects = new AttackEffects();
+const attackEffects = new AttackEffects();
TS_ASSERT_UNEVAL_EQUALS(attackEffects.Receivers(), [{
"type": "b",
+ "cmp": "B",
"IID": "IID_B",
- "method": "doB"
+ "method": "doB",
+ "getRelativeEffectMethod": "getB"
}, {
"type": "a",
+ "cmp": "A",
"IID": "IID_A",
- "method": "doA"
+ "method": "doA",
+ "getRelativeEffectMethod": "getA"
}]);
+
+TS_ASSERT_UNEVAL_EQUALS(attackEffects.Codes(), ["b", "a"]);
+
+TS_ASSERT_UNEVAL_EQUALS(attackEffects.GetReceiverFromCode("b"), {
+ "type": "b",
+ "cmp": "B",
+ "IID": "IID_B",
+ "method": "doB",
+ "getRelativeEffectMethod": "getB"
+});
+TS_ASSERT_UNEVAL_EQUALS(attackEffects.GetReceiverFromCode("c"), undefined);
Index: binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- binaries/data/mods/public/gui/common/tooltips.js
+++ binaries/data/mods/public/gui/common/tooltips.js
@@ -439,10 +439,6 @@
let tooltips = [];
for (let attackType in template.attack)
{
- // Slaughter is used to kill animals, so do not show it.
- if (attackType == "Slaughter")
- continue;
-
let attackTypeTemplate = template.attack[attackType];
let attackLabel = sprintf(headerFont(translate("%(attackType)s")), {
"attackType": translateWithContext(attackTypeTemplate.attackName.context || "Name of an attack, usually the weapon.", attackTypeTemplate.attackName.name)
Index: binaries/data/mods/public/gui/hotkeys/spec/ingame.json
===================================================================
--- binaries/data/mods/public/gui/hotkeys/spec/ingame.json
+++ binaries/data/mods/public/gui/hotkeys/spec/ingame.json
@@ -43,7 +43,7 @@
"name": "Force move",
"desc": "Modifier to move to a point instead of another action (e.g. gather)."
},
- "session.attack": {
+ "session.attacknocapture": {
"name": "Force attack",
"desc": "Modifier to attack instead of another action (e.g. capture)."
},
@@ -55,6 +55,14 @@
"name": "Attack Move (unit only)",
"desc": "Modifier to attackmove targeting only units when clicking on a point."
},
+ "session.attackprojectile": {
+ "name": "Force projectile",
+ "desc": "Modifier to attack using a projectile (generally long ranged attacks)."
+ },
+ "session.attacknoprojectile": {
+ "name": "Forbid projectile",
+ "desc": "Modifier to attack without using a projectile (generally short ranged attacks)."
+ },
"session.garrison": {
"name": "Garrison",
"desc": "Modifier to garrison when clicking on building."
Index: binaries/data/mods/public/gui/manual/intro.txt
===================================================================
--- binaries/data/mods/public/gui/manual/intro.txt
+++ binaries/data/mods/public/gui/manual/intro.txt
@@ -113,9 +113,11 @@
hotkey.selection.singleselection – Modifier to select units individually, opposed to per formation.
Right Click with a structure(s) selected – Set a rally point for units created/ungarrisoned from that structure
hotkey.session.garrison + Right Click with unit(s) selected – Garrison (If the cursor is over an own or allied structure)
- hotkey.session.attack + Right Click with unit(s) selected – Attack (instead of capture or gather)
+ hotkey.session.attacknocapture + Right Click with unit(s) selected – Attack (instead of capture or gather)
hotkey.session.attackmove + Right Click with unit(s) selected – Attack move (by default all enemy units and structures along the way are targeted)
hotkey.session.attackmoveUnit + Right Click with unit(s) selected – Attack move, only units along the way are targeted
+ hotkey.session.attackprojectile + attack command – Modifier to attack using a projectile (generally long ranged attacks)
+ hotkey.session.attacknoprojectile + attack command – Modifier to attack without using a projectile (generally short ranged attacks)
hotkey.session.snaptoedges + Mouse Move near structures – Align the new structure with an existing nearby structure
hotkey.session.flare + Right Click – Send a flare to your allies
Index: binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- binaries/data/mods/public/gui/session/unit_actions.js
+++ binaries/data/mods/public/gui/session/unit_actions.js
@@ -104,12 +104,24 @@
else
targetClasses = { "attack": ["Unit", "Structure"] };
+ const noCaptureHotkeyPressed = Engine.HotkeyIsPressed("session.attacknocapture");
+ const projectileHotkeyPressed = Engine.HotkeyIsPressed("session.attackprojectile");
+ const noProjectileHotkeyPressed = Engine.HotkeyIsPressed("session.attacknoprojectile");
+
Engine.PostNetworkCommand({
"type": "attack-walk",
"entities": selection,
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
+ "ignoreAttackEffects": {
+ "ApplyStatus": !noCaptureHotkeyPressed,
+ "Capture": noCaptureHotkeyPressed,
+ "Damage": !noCaptureHotkeyPressed
+
+ },
+ "projectile": projectileHotkeyPressed && !noProjectileHotkeyPressed ? "required" :
+ !projectileHotkeyPressed && noProjectileHotkeyPressed ? "disallowed" : null,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
@@ -147,64 +159,28 @@
"specificness": 30,
},
- "capture":
- {
- "execute": function(target, action, selection, queued, pushFront)
- {
- Engine.PostNetworkCommand({
- "type": "attack",
- "entities": selection,
- "target": action.target,
- "allowCapture": true,
- "queued": queued,
- "pushFront": pushFront,
- "formation": g_AutoFormation.getNull()
- });
-
- Engine.GuiInterfaceCall("PlaySound", {
- "name": "order_attack",
- "entity": action.firstAbleEntity
- });
-
- return true;
- },
- "getActionInfo": function(entState, targetState)
- {
- if (!entState.attack || !targetState || !targetState.capturePoints)
- return false;
-
- return {
- "possible": Engine.GuiInterfaceCall("CanAttack", {
- "entity": entState.id,
- "target": targetState.id,
- "types": ["Capture"]
- })
- };
- },
- "actionCheck": function(target, selection)
- {
- let actionInfo = getActionInfo("capture", target, selection);
- return actionInfo.possible && {
- "type": "capture",
- "cursor": "action-capture",
- "target": target,
- "firstAbleEntity": actionInfo.entity
- };
- },
- "specificness": 9,
- },
-
"attack":
{
"execute": function(target, action, selection, queued, pushFront)
{
+ const noCaptureHotkeyPressed = Engine.HotkeyIsPressed("session.attacknocapture");
+ const projectileHotkeyPressed = Engine.HotkeyIsPressed("session.attackprojectile");
+ const noProjectileHotkeyPressed = Engine.HotkeyIsPressed("session.attacknoprojectile");
+
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
+ "ignoreAttackEffects": {
+ "ApplyStatus": !noCaptureHotkeyPressed,
+ "Capture": noCaptureHotkeyPressed,
+ "Damage": !noCaptureHotkeyPressed
+
+ },
+ "projectile": projectileHotkeyPressed && !noProjectileHotkeyPressed ? "required" :
+ !projectileHotkeyPressed && noProjectileHotkeyPressed ? "disallowed" : null,
"queued": queued,
"pushFront": pushFront,
- "allowCapture": false,
"formation": g_AutoFormation.getNull()
});
@@ -217,33 +193,37 @@
},
"getActionInfo": function(entState, targetState)
{
- if (!entState.attack || !targetState || !targetState.hitpoints)
+ if (!entState.attack || !targetState)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
- "target": targetState.id,
- "types": ["!Capture"]
+ "target": targetState.id
})
};
},
- "hotkeyActionCheck": function(target, selection)
- {
- return Engine.HotkeyIsPressed("session.attack") &&
- this.actionCheck(target, selection);
- },
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack", target, selection);
+ if (!actionInfo.possible)
+ return false;
+
return actionInfo.possible && {
"type": "attack",
- "cursor": "action-attack",
+ "cursor": getAttackCursor(actionInfo.entity, target),
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
- "specificness": 10,
+ "hotkeyActionCheck": function(target, selection)
+ {
+ return (Engine.HotkeyIsPressed("session.attacknocapture") ||
+ Engine.HotkeyIsPressed("session.attackprojectile") ||
+ Engine.HotkeyIsPressed("session.attacknoprojectile")) &&
+ this.actionCheck(target, selection);
+ },
+ "specificness": 9,
},
"call-to-arms": {
@@ -254,14 +234,26 @@
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
+
+ const noCaptureHotkeyPressed = Engine.HotkeyIsPressed("session.attacknocapture");
+ const projectileHotkeyPressed = Engine.HotkeyIsPressed("session.attackprojectile");
+ const noProjectileHotkeyPressed = Engine.HotkeyIsPressed("session.attacknoprojectile");
+
Engine.PostNetworkCommand({
"type": "call-to-arms",
"entities": selection,
"target": target,
"targetClasses": targetClasses,
+ "ignoreAttackEffects": {
+ "ApplyStatus": !noCaptureHotkeyPressed,
+ "Capture": noCaptureHotkeyPressed,
+ "Damage": !noCaptureHotkeyPressed
+
+ },
+ "projectile": projectileHotkeyPressed && !noProjectileHotkeyPressed ? "required" :
+ !projectileHotkeyPressed && noProjectileHotkeyPressed ? "disallowed" : null,
"queued": queued,
"pushFront": pushFront,
- "allowCapture": true,
"formation": g_AutoFormation.getNull()
});
return true;
@@ -275,7 +267,10 @@
const actionInfo = getActionInfo("call-to-arms", target, selection);
return actionInfo.possible && {
"type": "call-to-arms",
- "cursor": "action-attack",
+ "cursor": typeof target == "number" && Engine.GuiInterfaceCall("CanAttack", {
+ "entity": actionInfo.entity,
+ "target": target
+ }) ? getAttackCursor(actionInfo.entity, target) : "action-attack-move",
"target": target,
"firstAbleEntity": actionInfo.entity
};
@@ -297,6 +292,10 @@
{
"execute": function(target, action, selection, queued, pushFront)
{
+ const noCaptureHotkeyPressed = Engine.HotkeyIsPressed("session.attacknocapture");
+ const projectileHotkeyPressed = Engine.HotkeyIsPressed("session.attackprojectile");
+ const noProjectileHotkeyPressed = Engine.HotkeyIsPressed("session.attacknoprojectile");
+
Engine.PostNetworkCommand({
"type": "patrol",
"entities": selection,
@@ -304,8 +303,15 @@
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
+ "ignoreAttackEffects": {
+ "ApplyStatus": !noCaptureHotkeyPressed,
+ "Capture": noCaptureHotkeyPressed,
+ "Damage": !noCaptureHotkeyPressed
+
+ },
+ "projectile": projectileHotkeyPressed && !noProjectileHotkeyPressed ? "required" :
+ !projectileHotkeyPressed && noProjectileHotkeyPressed ? "disallowed" : null,
"queued": queued,
- "allowCapture": false,
"formation": g_AutoFormation.getDefault()
});
@@ -1080,6 +1086,18 @@
data.command = "attack-walk";
data.targetClasses = targetClasses;
+
+ const noCaptureHotkeyPressed = Engine.HotkeyIsPressed("session.attacknocapture");
+ const projectileHotkeyPressed = Engine.HotkeyIsPressed("session.attackprojectile");
+ const noProjectileHotkeyPressed = Engine.HotkeyIsPressed("session.attacknoprojectile");
+ data.ignoreAttackEffects = {
+ "ApplyStatus": !noCaptureHotkeyPressed,
+ "Capture": noCaptureHotkeyPressed,
+ "Damage": !noCaptureHotkeyPressed
+
+ };
+ data.projectile = projectileHotkeyPressed && !noProjectileHotkeyPressed ? "required" :
+ !projectileHotkeyPressed && noProjectileHotkeyPressed ? "disallowed" : null;
cursor = "action-attack-move";
}
@@ -1198,7 +1216,28 @@
{
data.target = targetState.id;
data.command = "attack";
- cursor = "action-attack";
+
+ const noCaptureHotkeyPressed = Engine.HotkeyIsPressed("session.attacknocapture");
+ const projectileHotkeyPressed = Engine.HotkeyIsPressed("session.attackprojectile");
+ const noProjectileHotkeyPressed = Engine.HotkeyIsPressed("session.attacknoprojectile");
+ data.ignoreAttackEffects = {
+ "ApplyStatus": !noCaptureHotkeyPressed,
+ "Capture": noCaptureHotkeyPressed,
+ "Damage": !noCaptureHotkeyPressed
+
+ };
+ data.projectile = projectileHotkeyPressed && !noProjectileHotkeyPressed ? "required" :
+ !projectileHotkeyPressed && noProjectileHotkeyPressed ? "disallowed" : null;
+
+ // We don't check for canAttack here since we cannot know what entities will use the rallypoint.
+ if (!noCaptureHotkeyPressed)
+ cursor = "action-attack-capture";
+
+ else if (!noProjectileHotkeyPressed && projectileHotkeyPressed)
+ cursor = "action-attack-projectile";
+ else
+ // TODO: In case there is no projectile preference we might want another cursor. Needs a new cursor and some code here.
+ cursor = "action-attack-noprojectile";
}
return {
@@ -1913,6 +1952,26 @@
g_MiniMapPanel.flare(target, playerID);
}
+function getAttackCursor(ent, target)
+{
+ if (!Engine.HotkeyIsPressed("session.attacknocapture") && Engine.GuiInterfaceCall("CanAttack", {
+ "entity": ent,
+ "target": target,
+ "ignoreAttackEffects": { "Damage": true, "ApplyStatus": true }
+ }))
+ return "action-attack-capture";
+
+ if (!Engine.HotkeyIsPressed("session.attacknoprojectile") && Engine.HotkeyIsPressed("session.attackprojectile") && Engine.GuiInterfaceCall("CanAttack", {
+ "entity": ent,
+ "target": target,
+ "projectile": "required"
+ }))
+ return "action-attack-projectile";
+
+ // TODO: In case there is no projectile preference we might want another cursor. Needs a new cursor and some code here.
+ return "action-attack-noprojectile";
+}
+
function getCommandInfo(command, entStates)
{
return entStates && g_EntityCommands[command] &&
Index: binaries/data/mods/public/maps/random/danubius_triggers.js
===================================================================
--- binaries/data/mods/public/maps/random/danubius_triggers.js
+++ binaries/data/mods/public/maps/random/danubius_triggers.js
@@ -436,8 +436,10 @@
"type": "attack",
"entities": attackers,
"target": closestTarget,
- "queued": true,
- "allowCapture": false
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "queued": true
});
let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount);
@@ -454,8 +456,10 @@
"targetClasses": {
"attack": targetClass
},
- "queued": true,
- "allowCapture": false
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "queued": true
});
}
};
Index: binaries/data/mods/public/maps/random/jebel_barkal_triggers.js
===================================================================
--- binaries/data/mods/public/maps/random/jebel_barkal_triggers.js
+++ binaries/data/mods/public/maps/random/jebel_barkal_triggers.js
@@ -476,8 +476,10 @@
"targetClasses": {
"attack": jebelBarkal_cityPatrolGroup_balancing.targetClasses()
},
- "queued": true,
- "allowCapture": false
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "queued": true
});
}
}
@@ -578,8 +580,10 @@
"targetClasses": {
"attack": spawnPointBalancing.targetClasses()
},
- "queued": true,
- "allowCapture": false
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "queued": true
});
}
}
Index: binaries/data/mods/public/maps/random/polar_sea_triggers.js
===================================================================
--- binaries/data/mods/public/maps/random/polar_sea_triggers.js
+++ binaries/data/mods/public/maps/random/polar_sea_triggers.js
@@ -98,6 +98,9 @@
"type": "attack",
"entities": attackers[spawnPoint],
"target": target,
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
"queued": true
});
}
Index: binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js
===================================================================
--- binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js
+++ binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js
@@ -251,7 +251,9 @@
"x": targetPos.x,
"z": targetPos.y,
"targetClasses": undefined,
- "allowCapture": false,
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
"queued": true
});
Index: binaries/data/mods/public/maps/skirmishes/gallic_fields_3p.js
===================================================================
--- binaries/data/mods/public/maps/skirmishes/gallic_fields_3p.js
+++ binaries/data/mods/public/maps/skirmishes/gallic_fields_3p.js
@@ -23,7 +23,7 @@
cmd.type = "attack-walk";
cmd.entities = intruders[origin];
cmd.targetClasses = { "attack": ["Unit", "Structure"] };
- cmd.allowCapture = false;
+ cmd.ignoreAttackEffects = { "Capture": true };
cmd.queued = true;
ProcessCommand(0, cmd);
}
Index: binaries/data/mods/public/maps/tutorials/introductory_tutorial.js
===================================================================
--- binaries/data/mods/public/maps/tutorials/introductory_tutorial.js
+++ binaries/data/mods/public/maps/tutorials/introductory_tutorial.js
@@ -412,7 +412,9 @@
"x": position.x,
"z": position.y,
"targetClasses": { "attack": ["Unit"] },
- "allowCapture": false,
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
"queued": false
});
};
Index: binaries/data/mods/public/simulation/ai/common-api/attackeffects.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/ai/common-api/attackeffects.js
@@ -0,0 +1 @@
+AttackEffects = new AttackEffects();
Index: binaries/data/mods/public/simulation/ai/common-api/entity.js
===================================================================
--- binaries/data/mods/public/simulation/ai/common-api/entity.js
+++ binaries/data/mods/public/simulation/ai/common-api/entity.js
@@ -237,23 +237,30 @@
};
},
- "attackStrengths": function(type) {
- let attackDamageTypes = this.get("Attack/" + type + "/Damage");
- if (!attackDamageTypes)
- return undefined;
+ "attackStrengths": function(type, againstClassList = [], civ = undefined) {
+ // TODO: StatusEffect.
+ let strengths = {};
+
+ const multiplier = this.getMultiplierAgainst(type, againstClassList, civ);
+ const attackStrengthsDamage = this.get("Attack/" + type + "/Damage");
+ if (attackStrengthsDamage)
+ {
+ strengths.Damage = {};
+ for (let damageType in attackStrengthsDamage)
+ strengths.Damage[damageType] = +attackStrengthsDamage[damageType] * multiplier;
+ }
- let damage = {};
- for (let damageType in attackDamageTypes)
- damage[damageType] = +attackDamageTypes[damageType];
+ strengths.Capture = +this.get("Attack/" + type + "/Capture") * multiplier || 0;
- return damage;
+ return strengths;
},
- "captureStrength": function() {
- if (!this.get("Attack/Capture"))
- return undefined;
+ "captureStrength": function(againstClassList = [], civ = undefined) {
+ let strength = 0;
+ for (let type in this.get("Attack"))
+ strength = Math.max(strength, +(this.get("Attack/"+ type + "/Capture") || 0) * this.getMultiplierAgainst(type, againstClassList, civ));
- return +this.get("Attack/Capture/Capture") || 0;
+ return strength;
},
"attackTimes": function(type) {
@@ -311,25 +318,25 @@
return target.hasClasses(mcounter);
},
- // returns, if it exists, the multiplier from each attack against a given class
- "getMultiplierAgainst": function(type, againstClass) {
- if (!this.get("Attack/" + type +""))
- return undefined;
+ // Returns the multiplier from each attack against a given class and civ.
+ "getMultiplierAgainst": function(type, againstClassList, civ) {
+ let multiplier = 1;
+ if (!this.get("Attack/" + type))
+ return multiplier;
- let bonuses = this.get("Attack/" + type + "/Bonuses");
+ const bonuses = this.get("Attack/" + type + "/Bonuses");
if (bonuses)
- {
- for (let b in bonuses)
+ for (const bonus in bonuses)
{
- let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
- if (!bonusClasses)
+ if (civ && bonus.Civ && civ != bonus.Civ)
continue;
- for (let bcl of bonusClasses.split(" "))
- if (bcl == againstClass)
- return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
+
+ const bonusClasses = this.get("Attack/" + type + "/Bonuses/" + bonus + "/Classes");
+ if (MatchesClassList(againstClassList, this.get("Attack/" + type + "/Bonuses/" + bonus + "/Classes")))
+ multiplier *= +this.get("Attack/" + type + "/Bonuses/" + bonus + "/Multiplier");
}
- }
- return 1;
+
+ return multiplier;
},
"buildableEntities": function(civ) {
@@ -542,19 +549,28 @@
"isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },
/**
- * returns true if the tempalte can capture the given target entity
+ * returns true if the template can capture the given target entity
* if no target is given, returns true if the template has the Capture attack
*/
"canCapture": function(target)
{
- if (!this.get("Attack/Capture"))
+ const attack = this.get("Attack")
+ if (!attack)
+ return false;
+ const captureTypes = Object.keys(attack).filter(type => !this.get("Attack/" + type + "/Capture"))
+ if (!captureTypes.length)
return false;
if (!target)
return true;
if (!target.get("Capturable"))
return false;
- let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
- return !restrictedClasses || !target.hasClasses(restrictedClasses);
+ for (const type of captureTypes)
+ {
+ const restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
+ if (!restrictedClasses || !target.hasClasses(restrictedClasses))
+ return true;
+ }
+ return false;
},
"isCapturable": function() { return this.get("Capturable") !== undefined; },
@@ -761,8 +777,6 @@
for (let type in attack)
{
- if (type == "Slaughter")
- continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
return true;
@@ -772,25 +786,34 @@
/**
* Derived from Attack.js' similary named function.
+ * TODO: more general wantedTypes, see attack.js.
* @return {boolean} - Whether an entity can attack a given target.
*/
- "canAttackTarget": function(target, allowCapture)
+ "canAttackTarget": function(target, mustBeInRange = false, ignoreAttackEffects = {}, wantedTypes = [], projectile = undefined)
{
- let attackTypes = this.get("Attack");
- if (!attackTypes)
+ if (target.isInvulnerable())
return false;
- let canCapture = allowCapture && this.canCapture(target);
- let health = target.get("Health");
- if (!health)
- return canCapture;
+ let types = Object.keys(this.get("Attack"));
+ if (wantedTypes.length)
+ types = types.filter(type => wantedTypes.includes(type));
- for (let type in attackTypes)
+ // TODO: Care about the range.
+ for (const type of types)
{
- if (type == "Capture" ? !canCapture : target.isInvulnerable())
+ const attackStrengths = this.attackStrengths(type);
+ if (Object.keys(attackStrengths).every(attackEffect =>
+ !attackStrengths[attackEffect] ||
+ // TODO: Storing and getting the component name from the json shouldn't be done.
+ !target.get(AttackEffects.GetReceiverFromCode(attackEffect).cmp) ||
+ ignoreAttackEffects[attackEffect]))
continue;
- let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
+ const templateProjectile = this.get("Attack/" + type + "/Projectile");
+ if ((projectile == "required" && !templateProjectile) || (projectile == "disallowed" && templateProjectile))
+ continue;
+
+ const restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !target.hasClasses(restrictedClasses))
return true;
}
@@ -808,8 +831,19 @@
return this;
},
- "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) {
- Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
+ "attackMove": function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = null, queued = false, pushFront = false) {
+ Engine.PostCommand(PlayerID, {
+ "type": "attack-walk",
+ "entities": [this.id()],
+ "x": x,
+ "z": z,
+ "targetClasses": targetClasses,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "queued": queued,
+ "pushFront": pushFront
+ });
return this;
},
@@ -850,8 +884,17 @@
return this;
},
- "attack": function(unitId, allowCapture = true, queued = false, pushFront = false) {
- Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
+ "attack": function(unitId, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = null, queued = false, pushFront = false) {
+ Engine.PostCommand(PlayerID, {
+ "type": "attack",
+ "entities": [this.id()],
+ "target": unitId,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "queued": queued,
+ "pushFront": pushFront
+ });
return this;
},
@@ -925,7 +968,7 @@
},
"tradeRoute": function(target, source) {
- Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false });
+ Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": null, "queued": false, "pushFront": false });
return this;
},
Index: binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
===================================================================
--- binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
+++ binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
@@ -170,7 +170,7 @@
return this;
};
-m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
+m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = null, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "attack-walk",
@@ -178,7 +178,9 @@
"x": x,
"z": z,
"targetClasses": targetClasses,
- "allowCapture": allowCapture,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
"queued": queued,
"pushFront": pushFront
});
@@ -229,12 +231,15 @@
return this;
};
-m.EntityCollection.prototype.attack = function(unitId, queued = false, pushFront = false)
+m.EntityCollection.prototype.attack = function(unitId, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = null, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "attack",
"entities": this.toIdArray(),
"target": unitId,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
"queued": queued,
"pushFront": pushFront
});
Index: binaries/data/mods/public/simulation/ai/common-api/filters.js
===================================================================
--- binaries/data/mods/public/simulation/ai/common-api/filters.js
+++ binaries/data/mods/public/simulation/ai/common-api/filters.js
@@ -77,8 +77,8 @@
"dynamicProperties": []
}),
- "byCanAttackTarget": target => ({
- "func": ent => ent.canAttackTarget(target),
+ "byCanAttackTarget": (target, mustBeInRange = false, ignoreAttackEffects = {}, wantedTypes = [], projectile = undefined) => ({
+ "func": ent => ent.canAttackTarget(target, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile),
"dynamicProperties": []
}),
Index: binaries/data/mods/public/simulation/ai/petra/attackManager.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/attackManager.js
+++ binaries/data/mods/public/simulation/ai/petra/attackManager.js
@@ -180,7 +180,7 @@
let access = PETRA.getLandAccess(gameState, ent);
for (let struct of gameState.getEnemyStructures().values())
{
- if (!ent.canAttackTarget(struct, PETRA.allowCapture(gameState, ent, struct)))
+ if (!ent.canAttackTarget(struct, false, PETRA.ignoreAttackEffects(gameState, ent, struct)))
continue;
let structPos = struct.position();
@@ -225,7 +225,7 @@
attackingUnits.add(ent.id());
if (dist > range)
ent.move(x, z);
- ent.attack(struct.id(), false, dist > range);
+ ent.attack(struct.id(), { "Capture": true }, [], undefined, dist > range);
break;
}
}
Index: binaries/data/mods/public/simulation/ai/petra/attackPlan.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/attackPlan.js
+++ binaries/data/mods/public/simulation/ai/petra/attackPlan.js
@@ -1338,16 +1338,17 @@
if (PETRA.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out
continue;
- let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
- if (!ent.canAttackTarget(attacker, allowCapture))
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ent, attacker);
+ if (!ent.canAttackTarget(attacker, false, ignoreAttackEffects))
continue;
- ent.attack(attacker.id(), allowCapture);
+ ent.attack(attacker.id(), ignoreAttackEffects);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ourUnit, attacker);
// And if this attacker is a non-ranged siege unit and our unit also, attack it
- if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, PETRA.allowCapture(gameState, ourUnit, attacker)))
+ if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, false, ignoreAttackEffects))
{
- ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker));
+ ourUnit.attack(attacker.id(), ignoreAttackEffects);
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
@@ -1364,10 +1365,10 @@
let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
for (let ent of collec.values())
{
- let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
- if (!ent.canAttackTarget(attacker, allowCapture))
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ent, attacker);
+ if (!ent.canAttackTarget(attacker, false, ignoreAttackEffects))
continue;
- ent.attack(attacker.id(), allowCapture);
+ ent.attack(attacker.id(), ignoreAttackEffects);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
@@ -1377,8 +1378,8 @@
let collec = this.unitCollection.filterNearest(ourUnit.position(), 2);
for (let ent of collec.values())
{
- let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
- if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, allowCapture))
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ent, attacker);
+ if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, false, ignoreAttackEffects))
continue;
let orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
@@ -1389,7 +1390,7 @@
if (target && !target.hasClasses(["Structure", "Support"]))
continue;
}
- ent.attack(attacker.id(), allowCapture);
+ ent.attack(attacker.id(), ignoreAttackEffects);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate
@@ -1406,10 +1407,10 @@
continue;
}
}
- let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker);
- if (ourUnit.canAttackTarget(attacker, allowCapture))
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ourUnit, attacker);
+ if (ourUnit.canAttackTarget(attacker, false, ignoreAttackEffects))
{
- ourUnit.attack(attacker.id(), allowCapture);
+ ourUnit.attack(attacker.id(), ignoreAttackEffects);
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
@@ -1562,7 +1563,7 @@
if (siegeUnit)
{
let mStruct = enemyStructures.filter(enemy => {
- if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
+ if (!enemy.position() || !ent.canAttackTarget(enemy, false, PETRA.ignoreAttackEffects(gameState, ent, enemy)))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
@@ -1592,11 +1593,11 @@
return valb - vala;
});
if (mStruct[0].hasClass("Gate"))
- ent.attack(mStruct[0].id(), PETRA.allowCapture(gameState, ent, mStruct[0]));
+ ent.attack(mStruct[0].id(), PETRA.ignoreAttackEffects(gameState, ent, mStruct[0]));
else
{
let rand = randIntExclusive(0, mStruct.length * 0.2);
- ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
+ ent.attack(mStruct[rand].id(), PETRA.ignoreAttackEffects(gameState, ent, mStruct[rand]));
}
}
else
@@ -1614,7 +1615,7 @@
{
const nearby = !ent.hasClasses(["FastMoving", "Ranged"]);
let mUnit = enemyUnits.filter(enemy => {
- if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
+ if (!enemy.position() || !ent.canAttackTarget(enemy, false, PETRA.ignoreAttackEffects(gameState, ent, enemy)))
return false;
if (enemy.hasClass("Animal"))
return false;
@@ -1654,12 +1655,12 @@
return valb - vala;
});
let rand = randIntExclusive(0, mUnit.length * 0.1);
- ent.attack(mUnit[rand].id(), PETRA.allowCapture(gameState, ent, mUnit[rand]));
+ ent.attack(mUnit[rand].id(), PETRA.ignoreAttackEffects(gameState, ent, mUnit[rand]));
}
// This may prove dangerous as we may be blocked by something we
// cannot attack. See similar behaviour at #5741.
- else if (this.isBlocked && ent.canAttackTarget(this.target, false))
- ent.attack(this.target.id(), false);
+ else if (this.isBlocked && ent.canAttackTarget(this.target, false, { "Capture": true }))
+ ent.attack(this.target.id(), { "Capture": true });
else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
{
let targetClasses = targetClassesUnit;
@@ -1679,7 +1680,7 @@
let mStruct = enemyStructures.filter(enemy => {
if (this.isBlocked && enemy.id() != this.target.id())
return false;
- if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
+ if (!enemy.position() || !ent.canAttackTarget(enemy, false, PETRA.ignoreAttackEffects(gameState, ent, enemy)))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
@@ -1703,11 +1704,11 @@
return valb - vala;
});
if (mStruct[0].hasClass("Gate"))
- ent.attack(mStruct[0].id(), false);
+ ent.attack(mStruct[0].id(), { "Capture": true });
else
{
let rand = randIntExclusive(0, mStruct.length * 0.2);
- ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
+ ent.attack(mStruct[rand].id(), PETRA.ignoreAttackEffects(gameState, ent, mStruct[rand]));
}
}
else if (needsUpdate) // really nothing let's try to help our nearest unit
@@ -1727,12 +1728,12 @@
if (dist > distmin)
return;
distmin = dist;
- if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target)))
+ if (!ent.canAttackTarget(target, false, PETRA.ignoreAttackEffects(gameState, ent, target)))
return;
attacker = target;
});
if (attacker)
- ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker));
+ ent.attack(attacker.id(), PETRA.ignoreAttackEffects(gameState, ent, attacker));
}
}
}
@@ -1784,10 +1785,10 @@
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
- let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
- if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture))
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ent, attacker);
+ if (!ent.isIdle() || !ent.canAttackTarget(attacker, false, ignoreAttackEffects))
continue;
- ent.attack(attacker.id(), allowCapture);
+ ent.attack(attacker.id(), ignoreAttackEffects);
}
break;
}
Index: binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
+++ binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
@@ -312,7 +312,7 @@
if (!eEnt || !eEnt.position()) // probably can't happen.
continue;
- if (!ent.canAttackTarget(eEnt, PETRA.allowCapture(gameState, ent, eEnt)))
+ if (!ent.canAttackTarget(eEnt, false, PETRA.ignoreAttackEffects(gameState, ent, eEnt)))
continue;
if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length &&
@@ -358,7 +358,7 @@
{
this.assignedTo[entID] = idFoe;
this.assignedAgainst[idFoe].push(entID);
- ent.attack(idFoe, PETRA.allowCapture(gameState, ent, foeEnt), queued);
+ ent.attack(idFoe, PETRA.ignoreAttackEffects(gameState, ent, foeEnt), [], undefined, queued);
}
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition);
@@ -571,11 +571,15 @@
let orderData = ent.unitAIOrderData();
if (!orderData.length && !ent.getMetadata(PlayerID, "transport"))
this.assignUnit(gameState, entId);
- else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture")
+ else if (orderData.length && orderData[0].target && orderData[0].attackType)
{
- let target = gameState.getEntityById(orderData[0].target);
- if (target && !PETRA.allowCapture(gameState, ent, target))
- ent.attack(orderData[0].target, false);
+ const target = gameState.getEntityById(orderData[0].target);
+ if (!target)
+ continue;
+
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ent, target);
+ if (!ent.canAttackTarget(target, false, ignoreAttackEffects, [orderData[0].attackType]))
+ ent.attack(orderData[0].target, ignoreAttackEffects);
}
}
Index: binaries/data/mods/public/simulation/ai/petra/defenseManager.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/defenseManager.js
+++ binaries/data/mods/public/simulation/ai/petra/defenseManager.js
@@ -457,7 +457,7 @@
// Do not assign defender if it cannot attack at least part of the attacking army.
if (!armiesNeeding[a].army.foeEntities.some(eEnt => {
let eEntID = gameState.getEntityById(eEnt);
- return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID));
+ return ent.canAttackTarget(eEntID, false, PETRA.ignoreAttackEffects(gameState, ent, eEntID));
}))
continue;
@@ -702,7 +702,7 @@
if (allAttacked[entId])
continue;
let ent = gameState.getEntityById(entId);
- if (!ent || !ent.position() || !ent.canAttackTarget(attacker, PETRA.allowCapture(gameState, ent, attacker)))
+ if (!ent || !ent.position() || !ent.canAttackTarget(attacker, false, PETRA.ignoreAttackEffects(gameState, ent, attacker)))
continue;
// Check that the unit is still attacking the structure (since the last played turn).
let state = ent.unitAIState();
@@ -721,13 +721,13 @@
if (minEnt)
{
capturableTarget.ents.delete(minEnt.id());
- minEnt.attack(attacker.id(), PETRA.allowCapture(gameState, minEnt, attacker));
+ minEnt.attack(attacker.id(), PETRA.ignoreAttackEffects(gameState, minEnt, attacker));
}
}
}
- let allowCapture = PETRA.allowCapture(gameState, target, attacker);
- if (target.canAttackTarget(attacker, allowCapture))
- target.attack(attacker.id(), allowCapture);
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, target, attacker);
+ if (target.canAttackTarget(attacker, false, ignoreAttackEffects))
+ target.attack(attacker.id(), ignoreAttackEffects);
}
}
};
Index: binaries/data/mods/public/simulation/ai/petra/entityExtend.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/entityExtend.js
+++ binaries/data/mods/public/simulation/ai/petra/entityExtend.js
@@ -12,7 +12,7 @@
};
/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */
-PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass)
+PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClassList = [], civ = undefined)
{
let strength = 0;
let attackTypes = ent.attackTypes();
@@ -22,21 +22,22 @@
for (let type of attackTypes)
{
+ // TODO: Nuke this check in a reasonable fashion.
if (type == "Slaughter")
continue;
- let attackStrength = ent.attackStrengths(type);
- for (let str in attackStrength)
+ const attackStrengths = ent.attackStrengths(type, againstClassList, civ);
+ for (const damageType in attackStrengths.Damage)
{
- let val = parseFloat(attackStrength[str]);
- if (againstClass)
- val *= ent.getMultiplierAgainst(type, againstClass);
- if (DamageTypeImportance[str])
- strength += DamageTypeImportance[str] * val / damageTypes.length;
+ let val = parseFloat(attackStrengths.Damage[damageType]);
+ if (DamageTypeImportance[damageType])
+ strength += DamageTypeImportance[damageType] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js).");
}
+ // TODO: Capture, Statuseffect.
+
let attackRange = ent.attackRange(type);
if (attackRange)
strength += attackRange.max * 0.0125;
@@ -156,16 +157,20 @@
PETRA.getSeaAccess(gameState, ent);
};
-/** Decide if we should try to capture (returns true) or destroy (return false) */
-PETRA.allowCapture = function(gameState, ent, target)
+/**
+ * Decide which attackEffects we should ignore when choosing the attack type.
+ */
+PETRA.ignoreAttackEffects = function(gameState, ent, target)
{
if (!target.isCapturable() || !ent.canCapture(target))
- return false;
- if (target.isInvulnerable())
- return true;
+ return { "Capture": true };
+
// always try to recapture capture points from an allied, except if it's decaying
if (gameState.isPlayerAlly(target.owner()))
- return !target.decaying();
+ {
+ const ignoreCapture = target.decaying();
+ return { "Damage": !ignoreCapture, "Capture": ignoreCapture, "ApplyStatus": !ignoreCapture };
+ }
let antiCapture = target.defaultRegenRate();
if (target.isGarrisonHolder() && target.garrisoned())
@@ -174,44 +179,29 @@
antiCapture -= target.territoryDecayRate();
let capture;
- let capturableTargets = gameState.ai.HQ.capturableTargets;
+ const capturableTargets = gameState.ai.HQ.capturableTargets;
if (!capturableTargets.has(target.id()))
{
- capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
+ capture = ent.captureStrength(target.classes(), target.civ());
capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) });
}
else
{
- let capturable = capturableTargets.get(target.id());
+ const capturable = capturableTargets.get(target.id());
if (!capturable.ents.has(ent.id()))
{
- capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
+ capturable.strength += ent.captureStrength(target.classes(), target.civ());
capturable.ents.add(ent.id());
}
capture = capturable.strength;
}
- capture *= 1 / (0.1 + 0.9*target.healthLevel());
- let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b);
- if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned())
- return capture > antiCapture + sumCapturePoints/50;
- return capture > antiCapture + sumCapturePoints/80;
-};
+ capture *= 1 / (0.1 + 0.9 * target.healthLevel());
+ const sumCapturePoints = target.capturePoints().reduce((a, b) => a + b);
+ const ignoreCapture = target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned() ?
+ capture < antiCapture + sumCapturePoints / 50 :
+ capture < antiCapture + sumCapturePoints / 80;
-PETRA.getAttackBonus = function(ent, target, type)
-{
- let attackBonus = 1;
- if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses"))
- return attackBonus;
- let bonuses = ent.get("Attack/" + type + "/Bonuses");
- for (let key in bonuses)
- {
- let bonus = bonuses[key];
- if (bonus.Civ && bonus.Civ !== target.civ())
- continue;
- if (!bonus.Classes || target.hasClasses(bonus.Classes))
- attackBonus *= bonus.Multiplier;
- }
- return attackBonus;
+ return { "Damage": !ignoreCapture, "Capture": ignoreCapture, "ApplyStatus": !ignoreCapture };
};
/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */
Index: binaries/data/mods/public/simulation/ai/petra/headquarters.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/headquarters.js
+++ binaries/data/mods/public/simulation/ai/petra/headquarters.js
@@ -522,8 +522,8 @@
}
else if (param[0] == "siegeStrength")
{
- aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
- bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
+ aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, ["Structure"]) * param[1];
+ bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, ["Structure"]) * param[1];
}
else if (param[0] == "speed")
{
@@ -2080,13 +2080,13 @@
continue;
if (!this.capturableTargets.has(targetId))
this.capturableTargets.set(targetId, {
- "strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"),
+ "strength": ent.captureStrength(target.classes(), target.civ()),
"ents": new Set([ent.id()])
});
else
{
let capturableTarget = this.capturableTargets.get(target.id());
- capturableTarget.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
+ capturableTarget.strength += ent.captureStrength(target.classes(), target.civ());
capturableTarget.ents.add(ent.id());
}
}
@@ -2094,17 +2094,15 @@
for (let [targetId, capturableTarget] of this.capturableTargets)
{
let target = gameState.getEntityById(targetId);
- let allowCapture;
for (let entId of capturableTarget.ents)
{
let ent = gameState.getEntityById(entId);
- if (allowCapture === undefined)
- allowCapture = PETRA.allowCapture(gameState, ent, target);
+ const ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ent, target);
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].attackType)
continue;
- if ((orderData[0].attackType == "Capture") !== allowCapture)
- ent.attack(targetId, allowCapture);
+ if (!ent.canAttackTarget(target, false, ignoreAttackEffects, [orderData[0].attackType]))
+ ent.attack(targetId, ignoreAttackEffects);
}
}
Index: binaries/data/mods/public/simulation/ai/petra/victoryManager.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/victoryManager.js
+++ binaries/data/mods/public/simulation/ai/petra/victoryManager.js
@@ -659,7 +659,7 @@
if (!attack)
continue;
for (let ent of attack.unitCollection.values())
- capture += ent.captureStrength() * PETRA.getAttackBonus(ent, relic, "Capture");
+ capture += ent.captureStrength(relic.classes(), relic.civ());
}
// No need to make a new attack if already enough units
if (capture > sumCapturePoints / 50)
@@ -689,7 +689,7 @@
let expedition = [];
for (let ent of units.values())
{
- capture += ent.captureStrength() * PETRA.getAttackBonus(ent, relic, "Capture");
+ capture += ent.captureStrength(relic.classes(), relic.civ());
expedition.push(ent);
if (capture > sumCapturePoints / 25)
break;
Index: binaries/data/mods/public/simulation/ai/petra/worker.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/worker.js
+++ binaries/data/mods/public/simulation/ai/petra/worker.js
@@ -179,11 +179,11 @@
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture")
{
- // If we are here, an enemy structure must have targeted one of our workers
- // and UnitAI sent it fight back with allowCapture=true
+ // If we are here, an enemy structure must have targeted one of our workers and
+ // UnitAI sent it to fight back. Make sure the correct attack values are ignored.
let target = gameState.getEntityById(orderData.target);
if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner()))
- ent.attack(orderData.target, PETRA.allowCapture(gameState, ent, target));
+ ent.attack(orderData.target, PETRA.ignoreAttackEffects(gameState, ent, target));
}
}
return;
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
@@ -1,7 +1,5 @@
function Attack() {}
-var g_AttackTypes = ["Melee", "Ranged", "Capture"];
-
Attack.prototype.preferredClassesSchema =
"" +
"" +
@@ -99,7 +97,7 @@
"" +
"" +
"" +
- "" +
+ "" +
"" +
"" +
"" +
@@ -193,11 +191,11 @@
Attack.prototype.GetAttackTypes = function(wantedTypes)
{
- let types = g_AttackTypes.filter(type => !!this.template[type]);
+ const types = Object.keys(this.template);
if (!wantedTypes)
return types;
- let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0);
+ const wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0);
return types.filter(type => wantedTypes.indexOf("!" + type) == -1 &&
(!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1));
};
@@ -220,66 +218,118 @@
return [];
};
-Attack.prototype.CanAttack = function(target, wantedTypes)
+/**
+ * Figure out whether we can attack a given target, with some attack type.
+ * @param {number} target - The entityID of the target.
+ * @param {boolean} mustBeInRange - Consider only attack types for which we are currently in range.
+ * @param {Object} ignoreAttackEffects - Object of the form { "Damage": true, "Capture": false } defining which attackEffects to ignore.
+ * @param {string[]} wantedTypes - List of (negated) attacktypes to allow.
+ * @param {string} projectile - Only allow types with(out) projectiles. Use "required" to allow only types with a projectile, use "disallowed" to allow only types without a projectile.
+ * @return {boolean} - Whether we can attack the target.
+ */
+Attack.prototype.CanAttack = function(target, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile)
+{
+ return this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile).length > 0;
+};
+
+/**
+ * Find all attack types we can use to attack the target.
+ * @param {number} target - The entityID of the target.
+ * @param {boolean} mustBeInRange - Consider only attack types for which we are currently in range.
+ * @param {Object} ignoreAttackEffects - Object of the form { "Damage": true, "Capture": false } defining which attackEffects to ignore.
+ * @param {string[]} preAttackTypes - List of (negated) attacktypes to allow.
+ * @param {string} projectile - Only allow types with(out) projectiles. Use "required" to allow only types with a projectile, use "disallowed" to allow only types without a projectile.
+ * @return {string[]} - The list of allowed attack types.
+ */
+Attack.prototype.GetAllowedAttackTypes = function(target, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile)
{
const cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
- return true;
+ {
+ let types = [];
+ for (let member of cmpFormation.GetMembers())
+ types = types.concat(cmpMemberAttack.GetAllowedAttackTypes(member, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile));
+ return types;
+ }
+
+ const allowedAttackEffects = g_AttackEffects.Codes().filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]);
+ if (!allowedAttackEffects.length)
+ return [];
+
+ const types = this.GetAttackTypes(wantedTypes);
+ if (!types.length)
+ return [];
const cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
const cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
- return false;
+ return [];
const cmpResistance = QueryMiragedInterface(target, IID_Resistance);
if (!cmpResistance)
- return false;
-
- const cmpIdentity = QueryMiragedInterface(target, IID_Identity);
- if (!cmpIdentity)
- return false;
-
- const cmpHealth = QueryMiragedInterface(target, IID_Health);
- const targetClasses = cmpIdentity.GetClassesList();
- if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() &&
- (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length || wantedTypes.indexOf("Slaughter") != -1))
- return true;
+ return [];
const cmpEntityPlayer = QueryOwnerInterface(this.entity);
- const cmpTargetPlayer = QueryOwnerInterface(target);
- if (!cmpTargetPlayer || !cmpEntityPlayer)
- return false;
+ const cmpTargetIdentity = QueryMiragedInterface(target, IID_Identity);
+ if (!cmpEntityPlayer || !cmpTargetIdentity)
+ return [];
- const types = this.GetAttackTypes(wantedTypes);
+ const targetClasses = cmpTargetIdentity.GetClassesList();
const entityOwner = cmpEntityPlayer.GetPlayerID();
- const targetOwner = cmpTargetPlayer.GetPlayerID();
- const 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.
- const heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
-
- for (const type of types)
- {
- if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
- continue;
+ const cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ const heightDiff = cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset();
- if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
- continue;
+ return types.filter(type => {
+ if (mustBeInRange)
+ {
+ const range = this.GetRange(type);
+ // Parabolic range compuation is the same as in and UnitAI's' MoveToTargetAttackRange and CheckTargetAttackRange.
+ // h is positive when I'm higher than the target.
+ const h = heightDiff + range.elevationBonus;
+
+ // TODO: merge D3249
+ // In case the target is too high compared to us, we are out of range.
+ if (h <= -range.max / 2)
+ return false;
+
+ if (!cmpObstructionManager.IsInTargetRange(
+ this.entity,
+ target,
+ range.min,
+ Math.sqrt(Math.square(range.max) + 2 * range.max * h),
+ false))
+ return false;
+ }
- if (heightDiff > this.GetRange(type).max)
- continue;
+ const attackEffects = this.GetAttackEffectsData(type, false);
+ const attackEffectsSplash = this.GetAttackEffectsData(type, true);
- const restrictedClasses = this.GetRestrictedClasses(type);
- if (!restrictedClasses.length)
- return true;
+ const bonusMultiplier = attackEffects && attackEffects.Bonuses ?
+ AttackHelper.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses) :
+ 1;
+ const bonusMultiplierSplash = attackEffectsSplash && attackEffectsSplash.Bonuses ?
+ AttackHelper.GetAttackBonus(this.entity, target, type + "/Splash", attackEffectsSplash.Bonuses) :
+ 1;
+
+ // We can't use the type if we can't cause any effect.
+ if (allowedAttackEffects.every(effectType => {
+ const receiver = g_AttackEffects.GetReceiverFromCode(effectType);
+ const cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]);
+ if (!cmpReceiver)
+ return true;
+
+ return ((!attackEffects[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, bonusMultiplier, entityOwner, allowedAttackEffects)) &&
+ (!attackEffectsSplash[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffectsSplash, effectType, bonusMultiplierSplash, entityOwner, allowedAttackEffects)));
+ }))
+ return false;
- if (!MatchesClassList(targetClasses, restrictedClasses))
- return true;
- }
+ if ((projectile == "required" && !this.template[type].Projectile) || (projectile == "disallowed" && this.template[type].Projectile))
+ return false;
- return false;
+ const restrictedClasses = this.GetRestrictedClasses(type);
+ return !restrictedClasses.length || !MatchesClassList(targetClasses, restrictedClasses);
+ });
};
/**
@@ -330,62 +380,173 @@
{
let template = this.template[type];
if (splash)
+ {
+ if (!template.Splash)
+ return {};
template = template.Splash;
+ }
return AttackHelper.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity);
};
/**
- * Find the best attack against a target.
+ * Find the best attack against a target. Using a DPS/range algorithm.
* @param {number} target - The entity-ID of the target.
- * @param {boolean} allowCapture - Whether capturing is allowed.
+ * @param {boolean} mustBeInRange - Consider only attack types for which we are currently in range. This will always be honoured.
+ * @param {Object} ignoreAttackEffects - Object of the form { "Damage": true, "Capture": false } defining which attackEffects to ignore.
+ * @param {string[]} preAttackTypes - List of (negated) attacktypes to prefer.
+ * @param {string} projectile - Prefer types with(out) projectiles. Use "required" to prefer types with a projectile, use "disallowed" to prefer types without a projectile.
* @return {string} - The preferred attack type.
*/
-Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
+Attack.prototype.GetBestAttackAgainst = function(target, mustBeInRange, ignoreAttackEffects, prefTypes, projectile)
{
- let cmpFormation = Engine.QueryInterface(target, IID_Formation);
- if (cmpFormation)
+ // Work out, based on the preferences, which types are potentially possible.
+ let types = this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects, prefTypes, projectile);
+ if (!types.length && projectile)
+ types = this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects, prefTypes);
+ if (!types.length && prefTypes && prefTypes.length)
+ types = this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects);
+ if (!types.length && ignoreAttackEffects && Object.keys(ignoreAttackEffects).filter(effect => ignoreAttackEffects[effect]).length)
+ types = this.GetAllowedAttackTypes(target, mustBeInRange);
+
+ if (!types.length)
+ return undefined;
+
+ //if (types.length == 1)
+ // return types[0];
+
+ // Work out if there is any preference among the possible types with respect to
+ // prefTypes and projectile. We already know the given types satisfy the
+ // relevant mustBeInRange condition and the best possible ignoreAttackEffects.
+ // So we don't recheck on them to improve performance.
+ if (prefTypes && prefTypes.length)
{
- // TODO: Formation against formation needs review
- let types = this.GetAttackTypes();
- return g_AttackTypes.find(attack => types.indexOf(attack) != -1);
+ const types2 = this.GetAllowedAttackTypes(target, false, {}, prefTypes.concat(types))
+ if (types2.length)
+ types = types2;
}
- let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
- if (!cmpIdentity)
- return undefined;
+ //if (types.length == 1)
+ // return types[0];
- // Always slaughter domestic animals instead of using a normal attack
- if (this.template.Slaughter && cmpIdentity.HasClass("Domestic"))
- return "Slaughter";
-
- let types = this.GetAttackTypes().filter(type => this.CanAttack(target, [type]));
-
- // Check whether the target is capturable and prefer that when it is allowed.
- let captureIndex = types.indexOf("Capture");
- if (captureIndex != -1)
- {
- if (allowCapture)
- return "Capture";
- types.splice(captureIndex, 1);
+ if (projectile)
+ {
+ const types2 = this.GetAllowedAttackTypes(target, false, {}, types, projectile)
+ if (types2.length)
+ types = types2;
}
- let targetClasses = cmpIdentity.GetClassesList();
- let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType));
+ //if (types.length == 1)
+ // return types[0];
- return types.sort((a, b) =>
- (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
- (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
-};
+ const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership)
+ return undefined;
+
+ const distance = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).DistanceToTarget(this.entity, target);
+ if (distance < 0)
+ return undefined;
+
+ const owner = cmpOwnership.GetOwner();
+ const fullRange = this.GetFullAttackRange();
+ const allAttackEffects = g_AttackEffects.Codes();
+ const consideredAttackEffects = allAttackEffects.filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]);
+
+ // Choose the best attack on a DPS/Range.
+ let bestType;
+ let bestDPSRange = -Infinity;
+ let bestAllEffectsDPSRange = -Infinity;
+
+ const cmpFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpFormation)
+ {
+ const members = cmpFormation.GetMembers();
+ for (const type of types)
+ {
+ let DPSRange = 0;
+ for (const member of members)
+ DPSRange += this.GetDPSRange(type, member, consideredAttackEffects, owner, distance, fullRange);
+
+ if (DPSRange > bestDPSRange)
+ {
+ bestType = type;
+ bestDPSRange = DPSRange;
+ bestAllEffectsDPSRange = 0;
+ for (const member of members)
+ bestAllEffectsDPSRange += this.GetDPSRange(type, member, allAttackEffects, owner, distance, fullRange)
+ }
+ else if (DPSRange == bestDPSRange)
+ {
+ let allEffectsDPSRange = 0;
+ for (const member of members)
+ allEffectsDPSRange += this.GetDPSRange(type, member, allAttackEffects, owner, distance, fullRange);
+ if (allEffectsDPSRange > bestAllEffectsDPSRange)
+ {
+ bestType = type;
+ bestAllEffectsDPSRange = allEffectsDPSRange;
+ }
+ }
+ }
+ return bestType;
+ }
-Attack.prototype.CompareEntitiesByPreference = function(a, b)
+ for (const type of types)
+ {
+ const DPSRange = this.GetDPSRange(type, target, consideredAttackEffects, owner, distance, fullRange);
+ if (DPSRange > bestDPSRange)
+ {
+ bestType = type;
+ bestDPSRange = DPSRange;
+ bestAllEffectsDPSRange = this.GetDPSRange(type, target, allAttackEffects, owner, distance, fullRange);
+ }
+ else if (DPSRange == bestDPSRange)
+ {
+ const allEffectsDPSRange = this.GetDPSRange(type, target, allAttackEffects, owner, distance, fullRange);
+ if (allEffectsDPSRange > bestAllEffectsDPSRange)
+ {
+ bestType = type;
+ bestAllEffectsDPSRange = allEffectsDPSRange;
+ }
+ }
+ }
+ return bestType;
+};
+/**
+ * Compute a DPS range.
+ * @param {string} type - The attack type.
+ * @param {number} target - Id of the target entity.
+ * @param {string[]} - Array of the AttackEffects we should consider.
+ * @param {number} attackerOwner - Owner of this entity
+ * @param {number} distance - Distance between this.entity and target.
+ * @param {Object} fullRange - Range object as returned by this.GetFullAttackRange.
+ * @return {number} - The DPS range value.
+ */
+Attack.prototype.GetDPSRange = function(type, target, consideredAttackEffects, owner, distance, fullRange)
{
- let aPreference = this.GetPreference(a);
- let bPreference = this.GetPreference(b);
+ let DPSRange = 0;
+ const attackEffects = this.GetAttackEffectsData(type, false);
+ const multiplier = AttackHelper.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses || {});
+ for (const effectType of consideredAttackEffects)
+ {
+ if (!attackEffects[effectType])
+ continue;
+ const receiver = g_AttackEffects.GetReceiverFromCode(effectType);
+ const cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]);
+ if (!cmpReceiver)
+ continue;
- if (aPreference === null && bPreference === null) return 0;
- if (aPreference === null) return 1;
- if (bPreference === null) return -1;
- return aPreference - bPreference;
+ DPSRange += cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, multiplier, owner, consideredAttackEffects);
+ }
+ DPSRange /= this.GetRepeatTime(type);
+
+ // Apply an exponential dropoff when out of range.
+ // TODO elevation?
+ const range = this.GetRange(type);
+ if (distance < range.min)
+ DPSRange *= Math.pow(0.2, (range.min - distance) / fullRange.min);
+ else if (distance > range.max)
+ DPSRange *= Math.pow(0.2, (distance - range.max) / (fullRange.max || 1));
+
+ return DPSRange;
};
Attack.prototype.GetAttackName = function(type)
@@ -398,12 +559,7 @@
Attack.prototype.GetRepeatTime = function(type)
{
- let repeatTime = 1000;
-
- if (this.template[type] && this.template[type].RepeatTime)
- repeatTime = +this.template[type].RepeatTime;
-
- return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeatTime, this.entity);
+ return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", +this.template[type].RepeatTime, this.entity);
};
Attack.prototype.GetTimers = function(type)
@@ -456,7 +612,10 @@
if (this.target)
this.StopAttacking();
- if (!this.CanAttack(target, [type]))
+ // We should be in range, but requiring that here yields an infinite loop:
+ // unitAI might keep trying to attack this entity from the idle state.
+ // TODO: figure our why we are not in range in this case.
+ if (!this.CanAttack(target, false, {}, [type]))
return false;
const cmpResistance = QueryMiragedInterface(target, IID_Resistance);
@@ -534,7 +693,8 @@
*/
Attack.prototype.Attack = function(type, lateness)
{
- if (!this.CanAttack(this.target, [type]))
+ // We will check the range after rather than before the attack to facilitate chasing.
+ if (!this.CanAttack(this.target, false, {}, [type]))
{
this.StopAttacking("TargetInvalidated");
return;
@@ -553,7 +713,6 @@
if (!this.target)
return;
- // We check the range after the attack to facilitate chasing.
if (!this.IsTargetInRange(this.target, type))
{
this.StopAttacking("OutOfRange");
@@ -574,9 +733,9 @@
};
/**
- * 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
- * call to PerformAttack.
+ * Attack the target entity. This should only be called after successful range and
+ * possibility check, and should only be called after GetTimers().repeat msec has
+ * passed since the last call to PerformAttack.
*/
Attack.prototype.PerformAttack = function(type, target)
{
@@ -704,6 +863,7 @@
data.position = targetPosition;
data.direction = Vector3D.sub(targetPosition, selfPosition);
}
+
if (delay)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
Index: binaries/data/mods/public/simulation/components/BuildingAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/BuildingAI.js
+++ binaries/data/mods/public/simulation/components/BuildingAI.js
@@ -1,6 +1,5 @@
// Number of rounds of firing per 2 seconds.
const roundCount = 10;
-const attackType = "Ranged";
function BuildingAI() {}
@@ -123,7 +122,7 @@
if (!enemies.length)
return;
- var range = cmpAttack.GetRange(attackType);
+ const range = cmpAttack.GetFullAttackRange();
// This takes entity sizes into accounts, so no need to compensate for structure size.
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
this.entity, range.min, range.max, range.elevationBonus,
@@ -151,7 +150,7 @@
if (!cmpPlayer || !cmpPlayer.IsEnemy(0))
return;
- var range = cmpAttack.GetRange(attackType);
+ const range = cmpAttack.GetFullAttackRange();
// This query is only interested in Gaia entities that can attack.
// This takes entity sizes into accounts, so no need to compensate for structure size.
@@ -185,7 +184,7 @@
// Add new targets.
for (let entity of msg.added)
- if (cmpAttack.CanAttack(entity))
+ if (cmpAttack.CanAttack(entity, false))
this.targetUnits.push(entity);
// Remove targets outside of vision-range.
@@ -210,7 +209,10 @@
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- var attackTimers = cmpAttack.GetTimers(attackType);
+ // Take the timer of the first attackType. This is a hack
+ // TODO: We should properly implement the timer and design what we
+ // actually want. See #4000.
+ var attackTimers = cmpAttack.GetTimers(cmpAttack.GetAttackTypes()[0]);
this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows",
attackTimers.prepare, attackTimers.repeat / roundCount, null);
@@ -327,33 +329,17 @@
for (let target of this.targetUnits)
addTarget(target);
- // The obstruction manager performs approximate range checks.
- // so we need to verify them here.
- // TODO: perhaps an optional 'precise' mode to range queries would be more performant.
- let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
- let range = cmpAttack.GetRange(attackType);
-
- let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!thisCmpPosition.IsInWorld())
- return;
- let s = thisCmpPosition.GetPosition();
-
let firedArrows = 0;
while (firedArrows < arrowsToFire && targets.length())
{
let selectedTarget = targets.randomItem();
-
- let targetCmpPosition = Engine.QueryInterface(selectedTarget, IID_Position);
- if (targetCmpPosition && targetCmpPosition.IsInWorld() && this.CheckTargetVisible(selectedTarget))
+ if (this.CheckTargetVisible(selectedTarget))
{
- // Parabolic range compuation is the same as in UnitAI's MoveToTargetAttackRange.
- // h is positive when I'm higher than the target.
- let h = s.y - targetCmpPosition.GetPosition().y + range.elevationBonus;
- if (h > -range.max / 2 && cmpObstructionManager.IsInTargetRange(
- this.entity,
- selectedTarget,
- range.min,
- Math.sqrt(Math.square(range.max) + 2 * range.max * h), false))
+ // The obstruction manager performs approximate range checks.
+ // so we need to verify them here. Hence the second argument.
+ // TODO: perhaps an optional 'precise' mode to range queries would be more performant.
+ const attackType = cmpAttack.GetBestAttackAgainst(selectedTarget, true);
+ if (attackType)
{
cmpAttack.PerformAttack(attackType, selectedTarget);
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
Index: binaries/data/mods/public/simulation/components/Capturable.js
===================================================================
--- binaries/data/mods/public/simulation/components/Capturable.js
+++ binaries/data/mods/public/simulation/components/Capturable.js
@@ -39,6 +39,20 @@
return this.garrisonRegenRate;
};
+Capturable.prototype.GetRelativeCapture = function(effectData, effectType, bonusMultiplier, attackerOwner)
+{
+ return this.CanCapture(attackerOwner) ?
+ AttackHelper.GetTotalAttackEffects(
+ this.entity,
+ effectData,
+ effectType,
+ bonusMultiplier,
+ QueryMiragedInterface(this.entity, IID_Resistance),
+ true
+ ) / this.maxCapturePoints :
+ 0;
+};
+
/**
* Set the new capture points, used for cloning entities.
* The caller should assure that the sum of capture points
@@ -139,9 +153,13 @@
Capturable.prototype.CanCapture = function(playerID)
{
let cmpPlayerSource = QueryPlayerIDInterface(playerID);
-
if (!cmpPlayerSource)
warn(playerID + " has no player component defined on its id.");
+
+ let cmpResistance = QueryMiragedInterface(this.entity, IID_Resistance);
+ if (cmpResistance && cmpResistance.IsInvulnerable())
+ return false;
+
let capturePoints = this.GetCapturePoints();
let sourceEnemyCapturePoints = 0;
for (let i in this.GetCapturePoints())
@@ -370,8 +388,9 @@
};
function CapturableMirage() {}
-CapturableMirage.prototype.Init = function(cmpCapturable)
+CapturableMirage.prototype.Init = function(cmpCapturable, miragedEnt)
{
+ this.entity = miragedEnt;
this.capturePoints = clone(cmpCapturable.GetCapturePoints());
this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
};
@@ -379,13 +398,14 @@
CapturableMirage.prototype.GetCapturePoints = function() { return this.capturePoints; };
CapturableMirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; };
CapturableMirage.prototype.CanCapture = Capturable.prototype.CanCapture;
+CapturableMirage.prototype.GetRelativeCapture = Capturable.prototype.GetRelativeCapture;
Engine.RegisterGlobal("CapturableMirage", CapturableMirage);
-Capturable.prototype.Mirage = function()
+Capturable.prototype.Mirage = function(miragedEnt)
{
let mirage = new CapturableMirage();
- mirage.Init(this);
+ mirage.Init(this, miragedEnt);
return mirage;
};
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -479,14 +479,6 @@
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
- if (type != "Ranged")
- {
- // Not a ranged attack, set some defaults.
- ret.attack[type].elevationBonus = 0;
- ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
- continue;
- }
-
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
@@ -1970,7 +1962,7 @@
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
- return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
+ return cmpAttack && cmpAttack.CanAttack(data.target, data.mustBeInRange || false, data.ignoreAttackEffects || {}, data.wantedTypes || [], data.projectile || undefined);
};
/*
Index: binaries/data/mods/public/simulation/components/Health.js
===================================================================
--- binaries/data/mods/public/simulation/components/Health.js
+++ binaries/data/mods/public/simulation/components/Health.js
@@ -79,6 +79,39 @@
};
/**
+ * @param {Object} effectData - The effects calculate the effect for.
+ * @param {string} effectType - The type of effect to apply (e.g. Damage, Capture or ApplyStatus).
+ * @param {number} bonusMultiplier - The factor to multiply the total effect with.
+ * @param {number} attackerOwner - The player id of the attacker.
+ * @return {number} - The fraction of the damage when this attack is done with maxHitpoints.
+ */
+Health.prototype.GetRelativeDamage = function(effectData, effectType, bonusMultiplier, attackerOwner)
+{
+
+ const cmpResistance = QueryMiragedInterface(this.entity, IID_Resistance);
+ if (cmpResistance && cmpResistance.IsInvulnerable())
+ return 0;
+
+ const cmpIdentity = QueryMiragedInterface(this.entity, IID_Identity);
+ const cmpPlayerEntity = QueryOwnerInterface(this.entity);
+ const cmpPlayerSource = QueryPlayerIDInterface(attackerOwner);
+ if (!cmpIdentity || !cmpPlayerEntity || !cmpPlayerSource)
+ return 0;
+
+ if (this.hitpoints <= 0 || (!cmpPlayerSource.IsEnemy(cmpPlayerEntity.GetPlayerID()) && !cmpIdentity.GetClassesList().includes("Domestic")))
+ return 0;
+
+ return AttackHelper.GetTotalAttackEffects(
+ this.entity,
+ effectData,
+ effectType,
+ bonusMultiplier,
+ QueryMiragedInterface(this.entity, IID_Resistance),
+ true
+ ) / this.maxHitpoints;
+};
+
+/**
* @return {boolean} Whether the units are injured. Dead units are not considered injured.
*/
Health.prototype.IsInjured = function()
@@ -500,26 +533,28 @@
};
function HealthMirage() {}
-HealthMirage.prototype.Init = function(cmpHealth)
+HealthMirage.prototype.Init = function(cmpHealth, miragedEnt)
{
+ this.entity = miragedEnt;
this.maxHitpoints = cmpHealth.GetMaxHitpoints();
this.hitpoints = cmpHealth.GetHitpoints();
this.repairable = cmpHealth.IsRepairable();
this.injured = cmpHealth.IsInjured();
this.unhealable = cmpHealth.IsUnhealable();
};
-HealthMirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; };
HealthMirage.prototype.GetHitpoints = function() { return this.hitpoints; };
+HealthMirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; };
+HealthMirage.prototype.GetRelativeDamage = Health.prototype.GetRelativeDamage;
HealthMirage.prototype.IsRepairable = function() { return this.repairable; };
HealthMirage.prototype.IsInjured = function() { return this.injured; };
HealthMirage.prototype.IsUnhealable = function() { return this.unhealable; };
Engine.RegisterGlobal("HealthMirage", HealthMirage);
-Health.prototype.Mirage = function()
+Health.prototype.Mirage = function(miragedEnt)
{
let mirage = new HealthMirage();
- mirage.Init(this);
+ mirage.Init(this, miragedEnt);
return mirage;
};
Index: binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js
===================================================================
--- binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js
+++ binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js
@@ -21,6 +21,42 @@
};
/**
+ * Quantify how much the effect does, relative to the maximum amount.
+ * For now just check if there is some effect.
+ */
+StatusEffectsReceiver.prototype.GetRelativeStatusEffect = function(effectData, effectType, bonusMultiplier, attackerOwner, consideredAttackEffects)
+{
+ const statusEffects = AttackHelper.GetTotalAttackEffects(
+ this.entity,
+ effectData,
+ effectType,
+ bonusMultiplier,
+ QueryMiragedInterface(this.entity, IID_Resistance),
+ true
+ );
+
+ let total = 0;
+ for (const effectTypeOfStatus of consideredAttackEffects)
+ {
+ const receiver = g_AttackEffects.GetReceiverFromCode(effectTypeOfStatus);
+ const cmpReceiver = QueryMiragedInterface(this.entity, global[receiver.IID]);
+ if (!cmpReceiver)
+ continue;
+
+ for (const statusName in statusEffects)
+ {
+ const status = statusEffects[statusName];
+ if (!status[effectTypeOfStatus])
+ continue;
+
+ total += cmpReceiver[receiver.getRelativeEffectMethod](status, effectTypeOfStatus, bonusMultiplier, attackerOwner, consideredAttackEffects) * Math.ceil(+status.Duration / +(status.Interval || this.DefaultInterval) + 1);
+ }
+ }
+
+ return total;
+};
+
+/**
* Called by Attacking effects. Adds status effects for each entry in the effectData.
*
* @param {Object} effectData - An object containing the status effects to give to the entity.
@@ -167,4 +203,20 @@
this.RemoveStatus(statusCode);
};
+function StatusEffectsReceiverMirage() {}
+StatusEffectsReceiverMirage.prototype.Init = function(cmpStatusEffectsReceiver)
+{
+};
+
+StatusEffectsReceiverMirage.prototype.GetRelativeStatusEffect = StatusEffectsReceiver.prototype.GetRelativeStatusEffect;
+
+Engine.RegisterGlobal("StatusEffectsReceiverMirage", StatusEffectsReceiverMirage);
+
+StatusEffectsReceiver.prototype.Mirage = function()
+{
+ const mirage = new StatusEffectsReceiverMirage();
+ mirage.Init(this);
+ return mirage;
+};
+
Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver);
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js
+++ binaries/data/mods/public/simulation/components/UnitAI.js
@@ -397,7 +397,25 @@
},
"Order.Attack": function(msg) {
- let type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
+ // We should not attack formations, but their members. However if we try to attack a formation store the formation.
+ let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
+ if (cmpFormation)
+ {
+ this.order.data.formationTarget = this.order.data.target;
+ this.order.data.target = cmpFormation.GetClosestMember(this.entity, (t) => this.CanAttack(
+ t,
+ (this.GetStance().respondStandGround && !this.order.data.force) || !this.AbleToMove(),
+ this.order.data.ignoreAttackEffects,
+ this.order.data.prefAttackTypes,
+ this.order.data.projectile));
+ }
+
+ let type = this.GetBestAttackAgainst(
+ this.order.data.target,
+ (this.GetStance().respondStandGround && !this.order.data.force) || !this.AbleToMove(),
+ this.order.data.ignoreAttackEffects,
+ this.order.data.prefAttackTypes,
+ this.order.data.projectile);
if (!type)
return this.FinishOrder();
@@ -497,7 +515,7 @@
if (this.MustKillGatherTarget(msg.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
- if (!this.GetBestAttackAgainst(msg.data.target, false))
+ if (!this.GetBestAttackAgainst(msg.data.target, false, { "Capture": true }))
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
@@ -523,7 +541,14 @@
return ACCEPT_ORDER;
}
- this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "allowCapture": false });
+ this.PushOrderFront("Attack", {
+ "target": this.order.data.target,
+ "force": !!this.order.data.force,
+ "hunting": true,
+ "ignoreAttackEffects": { "Capture": true },
+ "prefAttackTypes": [],
+ "projectile": undefined
+ });
return ACCEPT_ORDER;
}
@@ -755,7 +780,6 @@
"Order.Attack": function(msg) {
let target = msg.data.target;
- let allowCapture = msg.data.allowCapture;
let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
@@ -769,7 +793,7 @@
}
return this.FinishOrder();
}
- this.CallMemberFunction("Attack", [target, allowCapture, false]);
+ this.CallMemberFunction("Attack", [target, msg.data.ignoreAttackEffects, msg.data.prefAttackTypes, msg.data.projectile,, false]);
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (cmpAttack && cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
@@ -820,7 +844,16 @@
}
return ACCEPT_ORDER;
}
- this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "allowCapture": false, "min": 0, "max": 10 });
+ this.PushOrderFront("Attack", {
+ "target": msg.data.target,
+ "force": !!msg.data.force,
+ "hunting": true,
+ "ignoreAttackEffects": { "Capture": true },
+ "prefAttackTypes": [],
+ "projectile": undefined,
+ "min": 0,
+ "max": 10
+ });
return ACCEPT_ORDER;
}
@@ -1093,7 +1126,9 @@
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
- this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
+ this.patrolStartPosOrder.ignoreAttackEffects = this.order.data.ignoreAttackEffects;
+ this.patrolStartPosOrder.prefAttackTypes = this.order.data.prefAttackTypes;
+ this.patrolStartPosOrder.projectile = this.order.data.projectile;
}
this.SetAnimationVariant("combat");
@@ -1270,7 +1305,7 @@
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- this.CallMemberFunction("Attack", [target, this.order.data.allowCapture, false]);
+ this.CallMemberFunction("Attack", [target, this.order.data.ignoreAttackEffects, this.order.data.prefAttackTypes, this.order.data.projectile, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
@@ -1282,10 +1317,9 @@
// Wait for individual members to finish
"enter": function(msg) {
let target = this.order.data.target;
- let allowCapture = this.order.data.allowCapture;
if (!this.CheckFormationTargetAttackRange(target))
{
- if (this.CanAttack(target) && this.CheckTargetVisible(target))
+ if (this.CanAttack(target, (this.GetStance().respondStandGround && !this.order.data.force) || !this.AbleToMove()) && this.CheckTargetVisible(target))
{
this.SetNextState("COMBAT.APPROACHING");
return true;
@@ -1304,10 +1338,9 @@
"Timer": function(msg) {
let target = this.order.data.target;
- let allowCapture = this.order.data.allowCapture;
if (!this.CheckFormationTargetAttackRange(target))
{
- if (this.CanAttack(target) && this.CheckTargetVisible(target))
+ if (this.CanAttack(target, (this.GetStance().respondStandGround && !this.order.data.force) || !this.AbleToMove()) && this.CheckTargetVisible(target))
{
this.SetNextState("COMBAT.APPROACHING");
return;
@@ -1529,7 +1562,7 @@
}
// if we already are targeting another unit still alive, finish with it first
if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack"))
- if (this.order.data.target != msg.data.attacker && this.CanAttack(msg.data.attacker))
+ if (this.order.data.target != msg.data.attacker && this.CanAttack(msg.data.attacker, this.GetStance().respondStandGround || !this.AbleToMove()))
return;
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
@@ -1552,7 +1585,7 @@
}
if (this.CheckTargetVisible(msg.data.attacker))
- this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true });
+ this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "ignoreAttackEffects": {}, "prefAttackTypes": [], "projectile": undefined });
else
{
var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
@@ -1804,7 +1837,9 @@
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
- this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
+ this.patrolStartPosOrder.ignoreAttackEffects = this.order.data.ignoreAttackEffects;
+ this.patrolStartPosOrder.prefAttackTypes = this.order.data.prefAttackTypes;
+ this.patrolStartPosOrder.projectile = this.order.data.projectile;
}
this.SetAnimationVariant("combat");
@@ -2026,10 +2061,9 @@
},
"Attacked": function(msg) {
- // If we're already in combat mode, ignore anyone else who's attacking us
- // unless it's a melee attack since they may be blocking our way to the target
- if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
- this.RespondToTargetedEntities([msg.data.attacker]);
+ // Switch target to the attacker if that is better target.
+ if (this.GetStance().targetAttackersAlways || !this.order.data.force)
+ this.AttackEntitiesByPreference([this.order.data.target, msg.data.attacker]);
},
"leave": function() {
@@ -2092,13 +2126,11 @@
// under the assumption that this is desirable if the target
// was somewhat far away - we'll likely end up closer to where
// the player hoped we would.
+ // Don't set any prefered attack types, let the attack component handle itself.
let lastPos = this.order.data.lastPos;
this.PushOrder("WalkAndFight", {
"x": lastPos.x, "z": lastPos.z,
- "force": false,
- // Force to true - otherwise structures might be attacked instead of captured,
- // which is generally not expected (attacking units usually has allowCapture false).
- "allowCapture": true
+ "force": false
});
return;
}
@@ -2123,13 +2155,6 @@
"ATTACKING": {
"enter": function() {
let target = this.order.data.target;
- let cmpFormation = Engine.QueryInterface(target, IID_Formation);
- if (cmpFormation)
- {
- this.order.data.formationTarget = target;
- target = cmpFormation.GetClosestMember(this.entity);
- this.order.data.target = target;
- }
this.shouldCheer = false;
@@ -2210,9 +2235,10 @@
},
"Attacked": function(msg) {
- if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) &&
- this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture")
- this.RespondToTargetedEntities([msg.data.attacker]);
+ // Switch target to the attacker if that is better target.
+ if ((this.GetStance().targetAttackersAlways || !this.order.data.force) &&
+ this.order.data.target != msg.data.attacker && this.CanAttack(msg.data.attacker, (this.GetStance().respondStandGround && !this.order.data.force) || !this.AbleToMove()))
+ this.AttackEntitiesByPreference([this.order.data.target, msg.data.attacker]);
},
},
@@ -2234,10 +2260,8 @@
// If the target is a formation, pick closest member.
if (cmpFormation)
{
- let filter = (t) => this.CanAttack(t);
this.order.data.formationTarget = this.order.data.target;
- let target = cmpFormation.GetClosestMember(this.entity, filter);
- this.order.data.target = target;
+ this.order.data.target = cmpFormation.GetClosestMember(this.entity, (target) => this.CanAttack(target, (this.GetStance().respondStandGround && !this.order.data.force) || !this.AbleToMove()));;
this.SetNextState("COMBAT.ATTACKING");
return true;
}
@@ -3576,6 +3600,12 @@
return (state == "WALKING");
};
+UnitAI.prototype.isAttackingTarget = function(target)
+{
+ const state = this.GetCurrentState().split(".").pop();
+ return state == "COMBAT" && this.order?.data?.target && this.order.data.target == target;
+};
+
/**
* Return true if the current order is WalkAndFight or Patrol.
*/
@@ -4684,9 +4714,7 @@
};
/**
- * Move unit so we hope the target is in the attack range
- * for melee attacks, this goes straight to the default range checks
- * for ranged attacks, the parabolic range is used
+ * Move unit so we hope the target is in the attack range. Use parabolic ranges.
*/
UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
{
@@ -4704,10 +4732,7 @@
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
- target = cmpFormation.GetClosestMember(this.entity);
-
- if (type != "Ranged")
- return this.MoveToTargetRange(target, IID_Attack, type);
+ target = cmpFormation.GetClosestMember(this.entity, (targ) => this.CanAttack(targ, false, {}, [type]));
if (!this.CheckTargetVisible(target))
return false;
@@ -4719,22 +4744,19 @@
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
- let s = thisCmpPosition.GetPosition();
let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
- // Parabolic range compuation is the same as in BuildingAI's FireArrows.
- let t = targetCmpPosition.GetPosition();
- // h is positive when I'm higher than the target
- let h = s.y - t.y + range.elevationBonus;
-
- let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
- // No negative roots please
- if (h <= -range.max / 2)
- // return false? Or hope you come close enough?
- parabolicMaxRange = 0;
+ // Parabolic range compuation is the same as in Attack's GetAllowedAttackTypes and IsInTargetRange.
+ // TODO: merge D3249
+ // h is positive when I'm higher than the target.
+ let h = thisCmpPosition.GetHeightOffset() - targetCmpPosition.GetHeightOffset() + range.elevationBonus;
+
+ // Target is too high for our range. We can't attack.
+ // Return false? Or hope you come close enough?
+ let parabolicMaxRange = h <= -range.max / 2 ? 0 : Math.sqrt(Math.square(range.max) + 2 * range.max * h);
// The parabole changes while walking so be cautious:
let guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange;
@@ -4761,7 +4783,7 @@
{
let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpTargetFormation)
- target = cmpTargetFormation.GetClosestMember(this.entity);
+ target = cmpTargetFormation.GetClosestMember(this.entity, (targ) => this.CanAttack(targ));
if (!this.CheckTargetVisible(target))
return false;
@@ -4814,10 +4836,9 @@
};
/**
- * Check if the target is inside the attack range
- * For melee attacks, this goes straigt to the regular range calculation
- * For ranged attacks, the parabolic formula is used to accout for bigger ranges
- * when the target is lower, and smaller ranges when the target is higher
+ * Check if the target is inside the attack range. We account for any
+ * height difference between the entity and the target. When the target is
+ * lower the range is bigger, and when the target is higher the range is smaller.
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
@@ -4832,7 +4853,7 @@
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
- target = cmpFormation.GetClosestMember(this.entity);
+ target = cmpFormation.GetClosestMember(this.entity, (targ) => this.CanAttack(targ, false, {}, [type]));
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.IsTargetInRange(target, type);
@@ -4854,7 +4875,7 @@
{
let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpTargetFormation)
- target = cmpTargetFormation.GetClosestMember(this.entity);
+ target = cmpTargetFormation.GetClosestMember(this.entity, (targ) => this.CanAttack(targ));
let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpFormationAttack)
@@ -4986,12 +5007,12 @@
return distance < range;
};
-UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
+UnitAI.prototype.GetBestAttackAgainst = function(target, mustBeInRange, ignoreAttackEffects, prefAttackTypes, projectile)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
- return cmpAttack.GetBestAttackAgainst(target, allowCapture);
+ return cmpAttack.GetBestAttackAgainst(target, mustBeInRange, ignoreAttackEffects, prefAttackTypes, projectile);
};
/**
@@ -5001,11 +5022,22 @@
*/
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
- var target = ents.find(target => this.CanAttack(target));
+ const target = ents.find(targ => this.CanAttack(targ, this.GetStance().respondStandGround || !this.AbleToMove()));
if (!target)
return false;
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
+ // Don't push a new order if we already are attacking the target.
+ // TODO: Do allow to change the parameters of the order.
+ if (this.isAttackingTarget(target))
+ return true;
+
+ this.PushOrderFront("Attack", {
+ "target": target,
+ "force": false,
+ "ignoreAttackEffects": {},
+ "prefAttackTypes": [],
+ "projectile": undefined
+ });
return true;
};
@@ -5016,15 +5048,26 @@
*/
UnitAI.prototype.AttackEntityInZone = function(ents)
{
- var target = ents.find(target =>
- this.CanAttack(target) &&
- this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true)) &&
+ const target = ents.find(target =>
+ this.CanAttack(target, this.GetStance().respondStandGround || !this.AbleToMove()) &&
+ this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, this.GetStance().respondStandGround || !this.AbleToMove())) &&
(this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
);
if (!target)
return false;
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
+ // Don't push a new order if we already are attacking the target.
+ // TODO: Do allow to change the parameters of the order.
+ if (this.isAttackIngTarget(target))
+ return true;
+
+ this.PushOrderFront("Attack", {
+ "target": target,
+ "force": false,
+ "ignoreAttackEffects": {},
+ "prefAttackTypes": [],
+ "projectile": undefined
+ });
return true;
};
@@ -5455,12 +5498,20 @@
* to a player order, and so is forced.
* If targetClasses is given, only entities matching the targetClasses can be attacked.
*/
-UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
+UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false, pushFront = false)
{
- this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront);
+ this.AddOrder("WalkAndFight", {
+ "x": x,
+ "z": z,
+ "targetClasses": targetClasses,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "force": true
+ }, queued, pushFront);
};
-UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
+UnitAI.prototype.Patrol = function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false, pushFront = false)
{
if (!this.CanPatrol())
{
@@ -5468,7 +5519,15 @@
return;
}
- this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront);
+ this.AddOrder("Patrol", {
+ "x": x,
+ "z": z,
+ "targetClasses": targetClasses,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "force": true
+ }, queued, pushFront);
};
/**
@@ -5498,8 +5557,10 @@
/**
* Adds attack order to the queue, forced by the player.
*/
-UnitAI.prototype.Attack = function(target, allowCapture = true, queued = false, pushFront = false)
+UnitAI.prototype.Attack = function(target, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false, pushFront = false)
{
+ // Use ignoreAttackEffects, prefAttackTypes and projectile only for choosing the attackType here.
+ // We should allow to attack with other types if we can't have the prefered one.
if (!this.CanAttack(target))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
@@ -5514,7 +5575,9 @@
let order = {
"target": target,
"force": true,
- "allowCapture": allowCapture,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile
};
this.RememberTargetPosition(order);
@@ -5522,7 +5585,9 @@
if (this.order && this.order.type == "Attack" &&
this.order.data &&
this.order.data.target === order.target &&
- this.order.data.allowCapture === order.allowCapture)
+ JSON.stringify(this.order.data.ignoreAttackEffects) === JSON.stringify(order.ignoreAttackEffects) &&
+ this.order.data.prefAttackTypes.toString() === order.prefAttackTypes.toString() &&
+ this.order.data.projectile === order.projectile)
{
this.order.data.lastPos = order.lastPos;
this.order.data.force = order.force;
@@ -6014,7 +6079,7 @@
}
let attackfilter = e => {
- if (this?.order?.data?.targetClasses)
+ if (this.order?.data?.targetClasses)
{
let cmpIdentity = Engine.QueryInterface(e, IID_Identity);
let targetClasses = this.order.data.targetClasses;
@@ -6041,12 +6106,18 @@
let pref;
for (let v of entities)
{
- if (this.CanAttack(v) && attackfilter(v))
+ if (this.CanAttack(v, this.GetStance().respondStandGround || !this.AbleToMove()) && attackfilter(v))
{
pref = cmpAttack.GetPreference(v);
if (pref === 0)
{
- this.PushOrderFront("Attack", { "target": v, "force": false, "allowCapture": this?.order?.data?.allowCapture });
+ this.PushOrderFront("Attack", {
+ "target": v,
+ "force": false,
+ "ignoreAttackEffects": this.order?.data?.ignoreAttackEffects || {},
+ "prefAttackTypes": this.order?.data?.prefAttackTypes || [],
+ "projectile": this.order?.data?.projectile
+ });
return true;
}
targets.push(v);
@@ -6060,7 +6131,13 @@
{
if (prefs[targ] !== bestPref)
continue;
- this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this?.order?.data?.allowCapture });
+ this.PushOrderFront("Attack", {
+ "target": targ,
+ "force": false,
+ "ignoreAttackEffects": this.order?.data?.ignoreAttackEffects || {},
+ "prefAttackTypes": this.order?.data?.prefAttackTypes || [],
+ "projectile": this.order?.data?.projectile,
+ });
return true;
}
@@ -6227,7 +6304,7 @@
return component.GetRange(type, target);
};
-UnitAI.prototype.CanAttack = function(target)
+UnitAI.prototype.CanAttack = function(target, mustBeInRange = false, ignoreAttackEffects = {}, wantedAttackTypes = [], projectile = undefined)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
@@ -6235,7 +6312,7 @@
return true;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- return cmpAttack && cmpAttack.CanAttack(target);
+ return cmpAttack && cmpAttack.CanAttack(target, mustBeInRange, ignoreAttackEffects, wantedAttackTypes, projectile);
};
UnitAI.prototype.CanGarrison = function(target)
@@ -6409,8 +6486,10 @@
if (!cmpAttack)
return false;
+ const mustBeInRange = this.GetStance().respondStandGround || !this.AbleToMove();
+
let attackfilter = function(e) {
- if (!cmpAttack.CanAttack(e))
+ if (!cmpAttack.CanAttack(e, mustBeInRange))
return false;
let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
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
@@ -1,24 +1,37 @@
AttackEffects = class AttackEffects
{
- constructor() {}
- Receivers()
- {
- return [{
+ constructor() {
+ this.effectReceivers = [{
"type": "Damage",
"IID": "IID_Health",
- "method": "TakeDamage"
+ "method": "TakeDamage",
+ "getRelativeEffectMethod": "GetRelativeDamage"
},
{
"type": "Capture",
"IID": "IID_Capturable",
- "method": "Capture"
+ "method": "Capture",
+ "getRelativeEffectMethod": "GetRelativeCapture"
},
{
"type": "ApplyStatus",
"IID": "IID_StatusEffectsReceiver",
- "method": "ApplyStatus"
+ "method": "ApplyStatus",
+ "getRelativeEffectMethod": "GetRelativeStatusEffect"
}];
}
+ Receivers()
+ {
+ return this.effectReceivers;
+ }
+ Codes()
+ {
+ return ["Damage", "Capture", "ApplyStatus"];
+ }
+ GetReceiverFromCode(type)
+ {
+ return this.effectReceivers.find(receiver => receiver.type == type);
+ }
};
Engine.LoadHelperScript("Attack.js");
@@ -30,6 +43,7 @@
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
+Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("Attack.js");
@@ -43,15 +57,20 @@
"GetPlayerByID": () => playerEnt1
});
+ AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
+ "IsInTargetRange": () => true,
+ "DistanceToTarget": (ent, target) => 10
+ });
+
AddMock(playerEnt1, IID_Player, {
- "GetPlayerID": () => 1,
- "IsEnemy": () => isEnemy
+ "GetPlayerID": () => 1
});
let attacker = entityID;
AddMock(attacker, IID_Position, {
"IsInWorld": () => true,
+ "GetTurretParent": () => INVALID_ENTITY,
"GetHeightOffset": () => 5,
"GetPosition2D": () => new Vector2D(1, 2)
});
@@ -61,7 +80,7 @@
});
let cmpAttack = ConstructComponent(attacker, "Attack", {
- "Melee": {
+ "Spear": {
"Damage": {
"Hack": 11,
"Pierce": 5,
@@ -69,6 +88,8 @@
},
"MinRange": 3,
"MaxRange": 5,
+ "PrepareTime": 0,
+ "RepeatTime": 1000,
"PreferredClasses": {
"_string": "FemaleCitizen"
},
@@ -83,7 +104,7 @@
}
}
},
- "Ranged": {
+ "Bow": {
"Damage": {
"Hack": 0,
"Pierce": 10,
@@ -125,8 +146,22 @@
"Capture": {
"Capture": 8,
"MaxRange": 10,
+ "PrepareTime": 0,
+ "RepeatTime": 1000
+ },
+ "Slaughter": {
+ "Damage": {
+ "Hack": 100,
+ "Pierce": 0,
+ "Crush": 0
+ },
+ "MaxRange": 5,
+ "PrepareTime": 0,
+ "RepeatTime": 1000,
+ "RestrictedClasses": {
+ "_string": "!Domestic"
+ }
},
- "Slaughter": {},
"StatusEffect": {
"ApplyStatus": {
"StatusInternalName": {
@@ -149,7 +184,12 @@
}
},
"MinRange": "10",
- "MaxRange": "80"
+ "MaxRange": "80",
+ "PrepareTime": 0,
+ "RepeatTime": 1000,
+ "RestrictedClasses": {
+ "_string": "Elephant"
+ }
}
});
@@ -171,7 +211,20 @@
});
AddMock(defender, IID_Health, {
- "GetHitpoints": () => 100
+ "GetHitpoints": () => 100,
+ "GetRelativeDamage": (attackEffects, effectType) => {
+ if (!isEnemy && defenderClass != "Domestic")
+ return 0;
+ let strength = 0;
+ if (attackEffects[effectType])
+ for (const damageType in attackEffects[effectType])
+ strength += attackEffects[effectType][damageType];
+ return strength / 100;
+ }
+ });
+
+ AddMock(defender, IID_StatusEffectsReceiver, {
+ "GetRelativeStatusEffect": () => 0.0000000001
});
AddMock(defender, IID_Resistance, {
@@ -182,24 +235,23 @@
// 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(), ["Spear", "Bow", "Capture", "Slaughter", "StatusEffect"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Spear", "Bow", "Capture", "Slaughter", "StatusEffect"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Spear", "Bow", "Capture"]), ["Spear", "Bow", "Capture"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Spear", "Bow"]), ["Spear", "Bow"]);
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.GetAttackTypes(["Spear", "!Spear"]), []);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Spear"]), ["Bow", "Capture", "Slaughter", "StatusEffect"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Spear", "!Bow"]), ["Capture", "Slaughter", "StatusEffect"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Bow"]), ["Capture"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Spear", "!Bow"]), ["Spear", "Capture"]);
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]);
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Spear"), ["FemaleCitizen"]);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Spear"), ["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"), {
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Bow"), {
"Damage": {
"Hack": 0,
"Pierce": 10,
@@ -207,7 +259,7 @@
}
});
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), {
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Bow", true), {
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
@@ -242,13 +294,13 @@
}
});
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), {
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Bow"), {
"prepare": 300,
"repeat": 500
});
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Ranged"), 500);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Bow"), 500);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), {
"prepare": 0,
@@ -257,7 +309,14 @@
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Capture"), 1000);
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashData("Ranged"), {
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("StatusEffect"), {
+ "prepare": 0,
+ "repeat": 1000
+ });
+
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("StatusEffect"), 1000);
+
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashData("Bow"), {
"attackData": {
"Damage": {
"Hack": 0,
@@ -280,14 +339,14 @@
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("Spear").Bonuses.BonusCav.Multiplier, 2);
TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null);
let getAttackBonus = (s, t, e, splash) => AttackHelper.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, "Spear", defender), className == "Cavalry" ? 2 : 1);
+ TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Bow", defender), 1);
+ TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Bow", defender, true), className == "Cavalry" ? 3 : 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1);
});
@@ -297,7 +356,7 @@
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false);
});
-function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false)
+function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = false)
{
attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => {
@@ -306,25 +365,47 @@
"CanCapture": playerID => {
TS_ASSERT_EQUALS(playerID, 1);
return true;
- }
+ },
+ "GetRelativeCapture": (attackEffects, effectType) => attackEffects[effectType] / 10
});
- 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);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, []), true);
+
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Bow"]), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["!Spear"]), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Capture"]), isBuilding);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }), isBuilding);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }, ["!Capture"]), false);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }, ["Bow"]), false);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }, ["Capture"]), isBuilding);
+
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Slaughter"]), defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["!Slaughter"]), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Spear", "Capture"]), defenderClass != "Archer");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Bow", "Capture"]), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["!Bow", "!Spear", "!StatusEffect"]), isBuilding || defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Spear", "!Spear"]), false);
+
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender), bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false), bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}), bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, []), bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Slaughter"]), bestAttack);
+
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["!Slaughter"]), bestAttack == "Slaughter" ? "Bow" : bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Bow"]), "Bow");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["!Spear"]), bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Capture"]), bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Capture": true }), bestAttack == "Capture" ? "Bow" : bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Capture": true }, ["Bow"]), "Bow");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Capture": true }, ["!Capture"]), bestAttack == "Capture" ? "Bow" : bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Capture": true }, ["Capture"]), bestAttack == "Capture" ? "Bow" : bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Spear", "Capture"]), isBuilding ? "Capture" : defenderClass == "Archer" ? "Bow" : "Spear");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Bow", "Capture"]), isBuilding ? "Capture" : "Bow");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["!Bow", "!Spear", "!StatusEffect"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Spear", "!Spear"]), bestAttack);
});
attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => {
@@ -334,33 +415,62 @@
"CanCapture": playerID => {
TS_ASSERT_EQUALS(playerID, 1);
return true;
- }
+ },
+ "GetRelativeCapture": (attackEffects, effectType) => attackEffects[effectType] / 10
});
- 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);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, []), true);
+
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["!StatusEffect"]), isBuilding || defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["!StatusEffect"]), isBuilding || defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Bow", "!StatusEffect"]), defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["!Spear", "!StatusEffect"]), isBuilding || defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Capture", "!StatusEffect"]), isBuilding);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Spear", "Capture", "!StatusEffect"]), isBuilding || defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Bow", "Capture", "!StatusEffect"]), isBuilding || defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Slaughter", "!StatusEffect"]), defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["!Bow", "!Spear", "!StatusEffect"]), isBuilding || defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, {}, ["Spear", "!Spear"]), false);
+
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Capture": true }, ["!StatusEffect"]), defenderClass == "Domestic");
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }), isBuilding);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }, ["!Capture"]), false);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }, ["Bow"]), false);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, false, { "Damage": true, "ApplyStatus": true }, ["Capture"]), isBuilding);
+
+
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, []), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["!StatusEffect"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Slaughter"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["!Slaughter"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Bow" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Bow"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Bow" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Spear"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Spear" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["!Spear"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+
+ // When we have a domestic animal, we will end up doing the first in the array, since we ignore Damage.
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Damage": true, "ApplyStatus": true }), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Damage": true, "ApplyStatus": true }, ["Bow"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Damage": true, "ApplyStatus": true }, ["!Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Damage": true, "ApplyStatus": true }, ["Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Spear", "Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Spear" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Bow", "Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Bow" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["!Bow", "!Spear"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, {}, ["Spear", "!Spear"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect");
});
}
-testGetBestAttackAgainst("FemaleCitizen", "Melee", undefined);
-testGetBestAttackAgainst("Archer", "Ranged", undefined);
-testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter");
-testGetBestAttackAgainst("Structure", "Capture", "Capture", true);
-testGetBestAttackAgainst("Structure", "Ranged", undefined, false);
+testGetBestAttackAgainst("FemaleCitizen", "Bow");
+testGetBestAttackAgainst("Archer", "Bow");
+testGetBestAttackAgainst("Domestic", "Slaughter");
+testGetBestAttackAgainst("Structure", "Capture", true);
+testGetBestAttackAgainst("Structure", "Bow", false);
function testAttackPreference()
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
@@ -16,6 +16,7 @@
Engine.LoadHelperScript("Position.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/DelayedDamage.js");
+Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
@@ -33,6 +34,9 @@
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
cmpTimer.OnUpdate({ "turnLength": 1 });
+ AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
+ "IsInTargetRange": () => true
+ });
let attacker = 11;
let atkPlayerEntity = 1;
let attackerOwner = 6;
@@ -78,7 +82,8 @@
};
AddMock(atkPlayerEntity, IID_Player, {
- "GetEnemies": () => [targetOwner]
+ "GetEnemies": () => [targetOwner],
+ "GetPlayerID": () => atkPlayerEntity
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
@@ -86,6 +91,10 @@
"GetAllPlayers": () => [0, 1, 2, 3, 4]
});
+ AddMock(target, IID_Identity, {
+ "GetClassesList": () => []
+ });
+
AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
"RemoveProjectile": () => {},
"LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
@@ -97,6 +106,7 @@
"GetPosition2D": () => Vector2D.From(targetPos),
"GetHeightAt": () => 0,
"IsInWorld": () => true,
+ "GetHeightOffset": () => 0
});
AddMock(target, IID_Health, {
@@ -104,6 +114,7 @@
damageTaken = true;
return { "healthChange": -amount };
},
+ "GetRelativeDamage": () => 1
});
AddMock(SYSTEM_ENTITY, IID_DelayedDamage, {
@@ -138,6 +149,8 @@
"GetPosition": () => new Vector3D(2, 0, 3),
"GetRotation": () => new Vector3D(1, 2, 3),
"IsInWorld": () => true,
+ "GetTurretParent": () => INVALID_ENTITY,
+ "GetHeightOffset": () => 0
});
function TestDamage()
Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
+++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
@@ -132,6 +132,7 @@
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
+ "GetLosVisibility": function() { return "visible"; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
@@ -161,6 +162,7 @@
AddMock(unit, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
+ "GetHeightOffset": function() { return 0; },
"GetPosition": function() { return new Vector3D(); },
"GetPosition2D": function() { return new Vector2D(); },
"GetRotation": function() { return { "y": 0 }; },
@@ -176,6 +178,7 @@
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
+ "FaceTowardsPoint": () => {},
"GetPassabilityClassName": () => "default"
});
@@ -186,11 +189,10 @@
AddMock(unit, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
- "GetBestAttackAgainst": function(t) { return "melee"; },
+ "GetBestAttackAgainst": function(t) { return "Melee"; },
"GetPreference": function(t) { return 0; },
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
"CanAttack": function(v) { return true; },
- "CompareEntitiesByPreference": function(a, b) { return 0; },
"IsTargetInRange": () => true,
"StartAttacking": () => true
});
@@ -205,6 +207,14 @@
AddMock(enemy, IID_Health, {
"GetHitpoints": function() { return 10; },
});
+
+ AddMock(enemy, IID_Position, {
+ "GetHeightOffset": function() { return 0; },
+ "GetPosition": function() { return new Vector3D(); },
+ "GetPosition2D": function() { return new Vector2D(this.x, this.z); },
+ "IsInWorld": function() { return true; }
+ });
+
AddMock(enemy, IID_UnitAI, {
"IsAnimal": () => "false",
"IsDangerousAnimal": () => "false"
@@ -234,6 +244,7 @@
AddMock(controller, IID_Position, {
"JumpTo": function(x, z) { this.x = x; this.z = z; },
"GetTurretParent": function() { return INVALID_ENTITY; },
+ "GetHeightOffset": function() { return 0; },
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
"GetRotation": function() { return { "y": 0 }; },
@@ -304,6 +315,7 @@
"ResetActiveQuery": function(id) { return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
+ "GetLosVisibility": function() { return "visible"; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
@@ -343,8 +355,10 @@
AddMock(unit + i, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
+ "GetHeightOffset": function() { return 0; },
"GetPosition": function() { return new Vector3D(); },
"GetPosition2D": function() { return new Vector2D(); },
+ "TurnTo": function() {},
"GetRotation": function() { return { "y": 0 }; },
"IsInWorld": function() { return true; },
});
@@ -358,6 +372,7 @@
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
+ "FaceTowardsPoint": () => {},
"GetPassabilityClassName": () => "default"
});
@@ -368,10 +383,9 @@
AddMock(unit + i, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
- "GetBestAttackAgainst": function(t) { return "melee"; },
+ "GetBestAttackAgainst": function(t) { return "Melee"; },
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
"CanAttack": function(v) { return true; },
- "CompareEntitiesByPreference": function(a, b) { return 0; },
"IsTargetInRange": () => true,
"StartAttacking": () => true,
"StopAttacking": () => {}
@@ -389,6 +403,13 @@
"GetHitpoints": function() { return 40; },
});
+ AddMock(enemy, IID_Position, {
+ "GetHeightOffset": function() { return 0; },
+ "GetPosition": function() { return new Vector3D(); },
+ "GetPosition2D": function() { return new Vector2D(); },
+ "IsInWorld": function() { return true; }
+ });
+
let controllerFormation = ConstructComponent(controller, "Formation", {
"FormationName": "Line Closed",
"FormationShape": "square",
@@ -435,7 +456,7 @@
controllerFormation.SetMembers(units);
- controllerAI.Attack(enemy, []);
+ controllerAI.Attack(enemy);
for (let ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
Index: binaries/data/mods/public/simulation/data/attack_effects/applystatus.json
===================================================================
--- binaries/data/mods/public/simulation/data/attack_effects/applystatus.json
+++ binaries/data/mods/public/simulation/data/attack_effects/applystatus.json
@@ -1,8 +1,10 @@
{
"code": "ApplyStatus",
"description": "Various (timed) effects.",
+ "cmp": "StatusEffectsReceiver",
"IID": "IID_StatusEffectsReceiver",
"method": "ApplyStatus",
+ "getRelativeEffectMethod": "GetRelativeStatusEffect",
"name": "Apply Status",
"order": 3
}
Index: binaries/data/mods/public/simulation/data/attack_effects/capture.json
===================================================================
--- binaries/data/mods/public/simulation/data/attack_effects/capture.json
+++ binaries/data/mods/public/simulation/data/attack_effects/capture.json
@@ -1,8 +1,10 @@
{
"code": "Capture",
"description": "Reduces capture points of a target.",
+ "cmp": "Capturable",
"IID": "IID_Capturable",
"method": "Capture",
+ "getRelativeEffectMethod": "GetRelativeCapture",
"name": "Capture",
"order": 2
}
Index: binaries/data/mods/public/simulation/data/attack_effects/damage.json
===================================================================
--- binaries/data/mods/public/simulation/data/attack_effects/damage.json
+++ binaries/data/mods/public/simulation/data/attack_effects/damage.json
@@ -1,8 +1,10 @@
{
"code": "Damage",
"description": "Reduces the health of a target.",
+ "cmp": "Health",
"IID": "IID_Health",
"method": "TakeDamage",
+ "getRelativeEffectMethod": "GetRelativeDamage",
"name": "Damage",
"order": 1
}
Index: binaries/data/mods/public/simulation/helpers/Attack.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Attack.js
+++ binaries/data/mods/public/simulation/helpers/Attack.js
@@ -74,10 +74,10 @@
"" +
"" +
"" +
- "" +
+ "" +
"" +
- "" +
- "" +
+ "" +
+ "" +
"" +
"" +
"" +
@@ -161,23 +161,23 @@
*
* @return {number} - The total value of the effect.
*/
-AttackHelper.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance)
+AttackHelper.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance, staticCall)
{
let total = 0;
if (!cmpResistance)
- cmpResistance = Engine.QueryInterface(target, IID_Resistance);
+ cmpResistance = QueryMiragedInterface(target, IID_Resistance);
let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {};
- if (effectType == "Damage")
+ if (effectType == "Damage" && effectData.Damage)
for (let type in effectData.Damage)
total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0);
- else if (effectType == "Capture")
+ else if (effectType == "Capture" && effectData.Capture)
{
total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0);
// If Health is lower we are more susceptible to capture attacks.
- let cmpHealth = Engine.QueryInterface(target, IID_Health);
+ let cmpHealth = QueryMiragedInterface(target, IID_Health);
if (cmpHealth)
total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints();
}
@@ -196,7 +196,8 @@
continue;
}
- if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance))
+ // When using this function from the GUI we shouldn't call randBool since that will OOS.
+ if (!staticCall && randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance))
continue;
result[statusEffect] = effectData[effectType][statusEffect];
@@ -318,7 +319,7 @@
if (!cmpReceiver)
continue;
- Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, data.attackData, receiver.type, bonusMultiplier, cmpResistance), data.attacker, data.attackerOwner));
+ Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, data.attackData, receiver.type, bonusMultiplier, cmpResistance, false), data.attacker, data.attackerOwner));
}
if (!Object.keys(targetState).length)
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js
+++ binaries/data/mods/public/simulation/helpers/Commands.js
@@ -183,41 +183,59 @@
"attack-walk": function(player, cmd, data)
{
- let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
-
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
- cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront);
+ cmpUnitAI.WalkAndFight(
+ cmd.x,
+ cmd.z,
+ cmd.targetClasses,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ cmd.queued,
+ cmd.pushFront);
});
},
"attack-walk-custom": function(player, cmd, data)
{
- let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
- for (let ent in data.entities)
+ for (const ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
- cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront);
+ cmpUnitAI.WalkAndFight(
+ cmd.targetPositions[ent].x,
+ cmd.targetPositions[ent].y,
+ cmd.targetClasses,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ cmd.queued,
+ cmd.pushFront);
});
},
"attack": function(player, cmd, data)
{
- let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
-
- if (g_DebugCommands && !allowCapture &&
- !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
- warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
-
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
- cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued, cmd.pushFront);
+ cmpUnitAI.Attack(
+ cmd.target,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ cmd.queued,
+ cmd.pushFront);
});
},
"patrol": function(player, cmd, data)
{
- let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
-
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
- cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued)
+ cmpUnitAI.Patrol(
+ cmd.x,
+ cmd.z,
+ cmd.targetClasses,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ cmd.queued)
);
},
@@ -289,13 +307,29 @@
const target = cmd.target;
if (cmd.pushFront)
{
- cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, false, cmd.pushFront);
+ cmpUnitAI.WalkAndFight(
+ target.x,
+ target.z,
+ cmd.targetClasses,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ false,
+ cmd.pushFront);
cmpUnitAI.DropAtNearestDropSite(false, cmd.pushFront);
}
else
{
cmpUnitAI.DropAtNearestDropSite(cmd.queued, false)
- cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, true, false);
+ cmpUnitAI.WalkAndFight(
+ target.x,
+ target.z,
+ cmd.targetClasses,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ true,
+ false);
}
});
},
Index: binaries/data/mods/public/simulation/helpers/tests/test_Attack.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/tests/test_Attack.js
+++ binaries/data/mods/public/simulation/helpers/tests/test_Attack.js
@@ -22,6 +22,7 @@
};
Engine.LoadHelperScript("Attack.js");
+Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Index: binaries/data/mods/public/simulation/templates/units/pers/champion_infantry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/pers/champion_infantry.xml
+++ binaries/data/mods/public/simulation/templates/units/pers/champion_infantry.xml
@@ -1,5 +1,26 @@
+
+
+ Bow
+
+ 0
+ 6.0
+ 0
+
+ 72.0
+ 0.0
+ 600
+ 1000
+
+ 75.0
+ 3.0
+ false
+ 9.81
+
+
+
+
pers
persian