Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg
+++ binaries/data/config/default.cfg
@@ -294,9 +294,11 @@
backtowork = "Y" ; The unit will go back to work
unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected
move = unused ; 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)
-attackmove = Ctrl ; Modifier to attackmove when clicking on a point
-attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
+attack = Ctrl ; Modifier to primary attack instead of another attack action (e.g. capture)
+attackmove = Ctrl ; Modifier to primary attackmove when clicking on a point
+attackmoveUnit = "Ctrl+Q" ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys
+meleeattack = "Ctrl+Alt" ; Modifier to melee attack instead of another attack action (e.g. ranged attack, capture)
+rangedattack = "Ctrl+Shift" ; Modifier to ranged attack instead of another attack action (e.g. melee attack, capture)
garrison = Ctrl ; Modifier to garrison when clicking on building
autorallypoint = Ctrl ; Modifier to set the rally point on the building itself
guard = "G" ; Modifier to escort/guard when clicking on unit/building
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-ranged.txt
===================================================================
--- /dev/null
+++ binaries/data/mods/public/art/textures/cursors/action-attack-ranged.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
@@ -2,16 +2,22 @@
const g_EffectTypes = ["Damage", "Capture", "ApplyStatus"];
const g_EffectReceiver = {
"Damage": {
+ "cmp": "Health",
"IID": "IID_Health",
- "method": "TakeDamage"
+ "method": "TakeDamage",
+ "getRelativeEffectMethod": "GetRelativeDamage"
},
"Capture": {
+ "cmp": "Capturable",
"IID": "IID_Capturable",
"method": "Capture",
+ "getRelativeEffectMethod": "GetRelativeCapture",
"sound": "capture"
},
"ApplyStatus": {
+ "cmp": "StatusEffectsReceiver",
"IID": "IID_StatusEffectsReceiver",
- "method": "ApplyStatus"
+ "method": "ApplyStatus",
+ "getRelativeEffectMethod": "GetRelativeStatusEffect"
}
};
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
@@ -428,10 +428,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": attackTypeTemplate.attackName.context ?
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
@@ -110,9 +110,11 @@
hotkey.selection.woundedonly + Left Drag over units on map – Only select wounded units
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.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.attack + Right Click with unit(s) selected - Attack to destroy (instead of another attack action, e.g. capture)
+ hotkey.session.attackmove + Right Click with unit(s) selected - Attack move to destroy (by default all enemy units and structures along the way are targeted)
+ hotkey.session.attackmoveUnit + Right Click with unit(s) selected - Attack move to destroy, only units along the way are targeted
+ hotkey.session.meleeattack + Right Click with unit(s) selected - Melee attack (instead of another attack action, e.g. capture, ranged)
+ hotkey.session.rangedattack + Right Click with unit(s) selected - Ranged attack (instead of another attack action, e.g. capture, melee)
hotkey.session.snaptoedges + Mouse Move near structures – Align the new structure with an existing nearby structure
[font="sans-bold-14"]Overlays[font="sans-14"]
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
@@ -84,7 +84,7 @@
return { "type": "move" };
},
- "specificness": 12,
+ "specificness": 13,
},
"attack-move":
@@ -134,7 +134,7 @@
"specificness": 30,
},
- "capture":
+ "attack-capture":
{
"execute": function(target, action, selection, queued)
{
@@ -142,7 +142,10 @@
"type": "attack",
"entities": selection,
"target": action.target,
- "allowCapture": true,
+ "ignoreAttackEffects": {
+ "Damage": true,
+ "ApplyStatus": true
+ },
"queued": queued
});
@@ -162,25 +165,28 @@
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
- "types": ["Capture"]
+ "ignoreAttackEffects": {
+ "Damage": true,
+ "ApplyStatus": true
+ }
})
};
},
"actionCheck": function(target, selection)
{
- if (!getActionInfo("capture", target, selection).possible)
+ if (!getActionInfo("attack-capture", target, selection).possible)
return false;
return {
- "type": "capture",
- "cursor": "action-capture",
+ "type": "attack-capture",
+ "cursor": "action-attack-capture",
"target": target
};
},
- "specificness": 9,
+ "specificness": 7,
},
- "attack":
+ "attack-nocapture":
{
"execute": function(target, action, selection, queued)
{
@@ -188,8 +194,10 @@
"type": "attack",
"entities": selection,
"target": action.target,
- "queued": queued,
- "allowCapture": false
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
@@ -208,36 +216,172 @@
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
- "types": ["!Capture"]
+ "ignoreAttackEffects": {
+ "Capture": true
+ }
})
};
},
"hotkeyActionCheck": function(target, selection)
{
+ // The melee and ranged hotkeys should go before this while without the
+ // hotkeys nocapture should precede. This is a slight hack.
+ let meleeHotkeyAction = g_UnitActions["attack-melee"].hotkeyActionCheck(target, selection);
+ if (meleeHotkeyAction)
+ return meleeHotkeyAction;
+ let rangedHotkeyAction = g_UnitActions["attack-ranged"].hotkeyActionCheck(target, selection);
+ if (rangedHotkeyAction)
+ return rangedHotkeyAction;
+
if (!Engine.HotkeyIsPressed("session.attack") ||
- !getActionInfo("attack", target, selection).possible)
+ !getActionInfo("attack-nocapture", target, selection).possible)
return false;
return {
+ "type": "attack-nocapture",
+ // TODO: new cursor?
+ "cursor": "action-attack-melee",
+ "target": target
+ };
+ },
+ "actionCheck": function(target, selection)
+ {
+ if (!getActionInfo("attack-nocapture", target, selection).possible)
+ return false;
+
+ return {
+ "type": "attack-nocapture",
+ "cursor": "action-attack-melee",
+ "target": target
+ };
+ },
+ "specificness": 8,
+ },
+ "attack-melee":
+ {
+ "execute": function(target, action, selection, queued)
+ {
+ Engine.PostNetworkCommand({
"type": "attack",
- "cursor": "action-attack",
+ "entities": selection,
+ "target": action.target,
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "projectile": "disallowed",
+ "queued": queued
+ });
+
+ Engine.GuiInterfaceCall("PlaySound", {
+ "name": "order_attack",
+ "entity": selection[0]
+ });
+
+ return true;
+ },
+ "getActionInfo": function(entState, targetState)
+ {
+ if (!entState.attack || !targetState.hitpoints)
+ return false;
+
+ return {
+ "possible": Engine.GuiInterfaceCall("CanAttack", {
+ "entity": entState.id,
+ "target": targetState.id,
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "projectile": "disallowed"
+ })
+ };
+ },
+ "hotkeyActionCheck": function(target, selection)
+ {
+ if (!Engine.HotkeyIsPressed("session.meleeattack") || !getActionInfo("attack-melee", target, selection).possible)
+ return false;
+
+ return {
+ "type": "attack-melee",
+ "cursor": "action-attack-melee",
"target": target
};
},
"actionCheck": function(target, selection)
{
- if (!getActionInfo("attack", target, selection).possible)
+ if (!getActionInfo("attack-melee", target, selection).possible)
return false;
return {
+ "type": "attack-melee",
+ "cursor": "action-attack-melee",
+ "target": target
+ };
+ },
+ "specificness": 9,
+ },
+
+ "attack-ranged":
+ {
+ "execute": function(target, action, selection, queued)
+ {
+ Engine.PostNetworkCommand({
"type": "attack",
- "cursor": "action-attack",
+ "entities": selection,
+ "target": action.target,
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "projectile": "required",
+ "queued": queued
+ });
+
+ Engine.GuiInterfaceCall("PlaySound", {
+ "name": "order_attack",
+ "entity": selection[0]
+ });
+
+ return true;
+ },
+ "getActionInfo": function(entState, targetState)
+ {
+ if (!entState.attack || !targetState.hitpoints)
+ return false;
+
+ return {
+ "possible": Engine.GuiInterfaceCall("CanAttack", {
+ "entity": entState.id,
+ "target": targetState.id,
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "projectile": "required"
+ })
+ };
+ },
+ "hotkeyActionCheck": function(target, selection)
+ {
+ if (!Engine.HotkeyIsPressed("session.rangedattack") || !getActionInfo("attack-ranged", target, selection).possible)
+ return false;
+
+ return {
+ "type": "attack-ranged",
+ "cursor": "action-attack-ranged",
+ "target": target
+ };
+ },
+ "actionCheck": function(target, selection)
+ {
+ if (!getActionInfo("attack-ranged", target, selection).possible)
+ return false;
+
+ return {
+ "type": "attack-ranged",
+ "cursor": "action-attack-ranged",
"target": target
};
},
"specificness": 10,
},
-
"patrol":
{
"execute": function(target, action, selection, queued)
@@ -249,8 +393,10 @@
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
- "queued": queued,
- "allowCapture": false
+ "ignoreAttackEffects": {
+ "Capture": true
+ },
+ "queued": queued
});
DrawTargetMarker(target);
@@ -337,7 +483,7 @@
"target": target
};
},
- "specificness": 7,
+ "specificness": 6,
},
// "Fake" action to check if an entity can be ordered to "construct"
@@ -1015,8 +1161,9 @@
else if (playerCheck(entState, targetState, ["Enemy"]))
{
data.target = targetState.id;
- data.command = "attack";
- cursor = "action-attack";
+ data.command = "attack-nocapture";
+ // TODO other attackTypes should be allowed too, maybe this needs a rewrite of the rallypoint handling
+ cursor = "action-attack-melee";
}
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
@@ -1064,7 +1211,7 @@
"position": actionInfo.position
};
},
- "specificness": 6,
+ "specificness": 5,
},
"unset-rallypoint":
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
@@ -87,6 +87,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/tutorials/introductory_tutorial.js
===================================================================
--- binaries/data/mods/public/maps/tutorials/introductory_tutorial.js
+++ binaries/data/mods/public/maps/tutorials/introductory_tutorial.js
@@ -399,7 +399,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/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 = {};
+
+ let multiplier = this.getMultiplierAgainst(type, againstClassList, civ);
+ let 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) {
@@ -312,23 +319,23 @@
return false;
},
- // 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, if it exists, 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 1;
- if (this.get("Attack/" + type + "/Bonuses"))
- {
- for (let b in this.get("Attack/" + type + "/Bonuses"))
+ let bonuses = this.get("Attack/" + type + "/Bonuses");
+ if (bonuses)
+ for (let 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");
+
+ let 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;
},
@@ -530,18 +537,19 @@
"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== 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"))
+ let attack = this.get("Attack");
+ if (!attack || Object.keys(attack).every(type => !this.get("Attack/" + type + "/Capture")))
return false;
if (!target)
return true;
if (!target.get("Capturable"))
return false;
- let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
+ let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses);
},
@@ -742,8 +750,6 @@
for (let type in this.get("Attack"))
{
- if (type == "Slaughter")
- continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
return true;
@@ -753,28 +759,36 @@
/**
* 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 of types)
+ {
+ // TODO: care about the range
+ let attackStrengths = this.attackStrengths(type);
+ if (Object.keys(attackStrengths).every(attackEffect =>
+ !attackStrengths[attackEffect] ||
+ !target.get(g_EffectReceiver[attackEffect].cmp) ||
+ ignoreAttackEffects[attackEffect]))
+ continue;
- for (let type in attackTypes)
- {
- if (type == "Capture" ? !canCapture : target.isInvulnerable())
+ let templateProjectile = this.get("Attack/" + type + "/Projectile");
+ if ((projectile == "required" && !templateProjectile) || (projectile == "disallowed" && templateProjectile))
continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses))
return true;
- };
+ }
return false;
},
@@ -789,8 +803,18 @@
return this;
},
- "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false) {
- Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued });
+ "attackMove": function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false) {
+ Engine.PostCommand(PlayerID, {
+ "type": "attack-walk",
+ "entities": [this.id()],
+ "x": x,
+ "z": z,
+ "targetClasses": targetClasses,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "queued": queued
+ });
return this;
},
@@ -826,8 +850,16 @@
return this;
},
- "attack": function(unitId, allowCapture = true, queued = false) {
- Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued });
+ "attack": function(unitId, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false) {
+ Engine.PostCommand(PlayerID, {
+ "type": "attack",
+ "entities": [this.id()],
+ "target": unitId,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "queued": queued
+ });
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
@@ -155,10 +155,19 @@
return this;
};
-m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCapture = true, queued = false)
+m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false)
{
- Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z,
- "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued });
+ Engine.PostCommand(PlayerID, {
+ "type": "attack-walk",
+ "entities": this.toIdArray(),
+ "x": x,
+ "z": z,
+ "targetClasses": targetClasses,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "queued": queued
+ });
return this;
};
@@ -181,9 +190,17 @@
return this;
};
-m.EntityCollection.prototype.attack = function(unitId, queued = false)
+m.EntityCollection.prototype.attack = function(unitId, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false)
{
- Engine.PostCommand(PlayerID, { "type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": queued });
+ Engine.PostCommand(PlayerID, {
+ "type": "attack",
+ "entities": this.toIdArray(),
+ "target": unitId,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "queued": queued
+ });
return this;
};
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
@@ -1323,16 +1323,18 @@
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))
+ let 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);
}
+
// 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)))
+ let ignoreAttackEffects = PETRA.ignoreAttackEffects(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);
}
}
@@ -1349,10 +1351,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))
+ let 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);
}
}
@@ -1362,8 +1364,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))
+ let 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)
@@ -1374,7 +1376,7 @@
if (target && !target.hasClass("Structure") && !target.hasClass("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
@@ -1391,10 +1393,10 @@
continue;
}
}
- let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker);
- if (ourUnit.canAttackTarget(attacker, allowCapture))
+ let 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);
}
}
@@ -1547,7 +1549,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;
@@ -1577,11 +1579,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
@@ -1599,7 +1601,7 @@
{
let nearby = !ent.hasClass("FastMoving") && !ent.hasClass("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;
@@ -1639,12 +1641,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;
@@ -1664,7 +1666,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;
@@ -1688,11 +1690,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
@@ -1712,12 +1714,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));
}
}
}
@@ -1769,10 +1771,10 @@
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
- let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
- if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture))
+ let 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);
@@ -574,8 +574,9 @@
else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture")
{
let target = gameState.getEntityById(orderData[0].target);
- if (target && !PETRA.allowCapture(gameState, ent, target))
- ent.attack(orderData[0].target, false);
+ let ignoreAttackEffects = PETRA.ignoreAttackEffects(gameState, ent, target);
+ if (target && ignoreAttackEffects.Capture)
+ 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
@@ -468,8 +468,8 @@
// 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;
let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition);
@@ -588,7 +588,7 @@
}
continue;
}
-
+
// TODO integrate other ships later, need to be sure it is accessible.
if (target.hasClass("Ship"))
continue;
@@ -713,7 +713,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();
@@ -732,13 +732,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);
+ let 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
@@ -11,8 +11,8 @@
return ent.hasClass("FastMoving");
};
-/** 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)
+/** returns some sort of DPS * health factor. If you specify a class and or civ, it'll use the modifiers against that too. */
+PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClassList = [], civ = undefined)
{
let strength = 0;
let attackTypes = ent.attackTypes();
@@ -25,18 +25,18 @@
if (type == "Slaughter")
continue;
- let attackStrength = ent.attackStrengths(type);
- for (let str in attackStrength)
+ let attackStrength = ent.attackStrengths(type, againstClassList, civ);
+ for (let damageType in attackStrength.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(attackStrength[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 +156,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();
+ {
+ let ignoreCapture = target.decaying();
+ return { "Damage": !ignoreCapture, "Capture": ignoreCapture, "ApplyStatus": !ignoreCapture };
+ }
let antiCapture = target.defaultRegenRate();
if (target.isGarrisonHolder() && target.garrisoned())
@@ -177,7 +181,7 @@
let 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
@@ -185,34 +189,18 @@
let 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());
+ 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;
-};
+ let 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 && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls)))
- continue;
- 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
@@ -777,8 +777,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")
{
@@ -2502,13 +2502,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());
}
}
@@ -2516,17 +2516,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);
+ let 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(target.classes(), target.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(target.classes(), target.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
@@ -177,13 +177,13 @@
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
- if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture")
+ if (orderData && orderData.target && orderData.attackType)
{
- // 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 =
"" +
"" +
@@ -94,151 +92,101 @@
"0.0" +
"" +
"4.0" +
+ "1000" +
+ "!Domestic" +
"" +
"" +
- "" +
- "" +
- "" +
- "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ Attacking.BuildAttackEffectsSchema() +
+ "" +
"" +
- "" +
- "" +
- "" +
+ "" +
"" +
- "" +
- "" +
- Attacking.BuildAttackEffectsSchema() +
- "" +
- "" +
- "" +
- "" +
- "" + // TODO: it shouldn't be stretched
- "" +
- "" +
- Attack.prototype.preferredClassesSchema +
- Attack.prototype.restrictedClassesSchema +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
"" +
- "" +
- "" +
- "" +
+ "" +
"" +
- "" +
- "" +
- Attacking.BuildAttackEffectsSchema() +
- "" +
- "" +
- ""+
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- Attacking.BuildAttackEffectsSchema() +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
- "" +
- "" +
- "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- Attack.prototype.preferredClassesSchema +
- Attack.prototype.restrictedClassesSchema +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
- "" +
- "" +
- "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ Attacking.BuildAttackEffectsSchema() +
+ "" +
+ "" +
"" +
- "" +
- "" +
- Attacking.BuildAttackEffectsSchema() +
- "" +
- "" + // TODO: it shouldn't be stretched
- "" +
- "" +
- Attack.prototype.preferredClassesSchema +
- Attack.prototype.restrictedClassesSchema +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
"" +
- "" +
- "" +
- "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
- "" +
- "" +
- Attacking.BuildAttackEffectsSchema() +
- "" + // TODO: how do these work?
- Attack.prototype.preferredClassesSchema +
- Attack.prototype.restrictedClassesSchema +
- "" +
- "" +
- "";
+ Attack.prototype.preferredClassesSchema +
+ Attack.prototype.restrictedClassesSchema +
+ "" +
+ "" +
+ "" +
+ "";
Attack.prototype.Init = function()
{
@@ -248,7 +196,7 @@
Attack.prototype.GetAttackTypes = function(wantedTypes)
{
- let types = g_AttackTypes.filter(type => !!this.template[type]);
+ let types = Object.keys(this.template);
if (!wantedTypes)
return types;
@@ -275,62 +223,113 @@
return [];
};
-Attack.prototype.CanAttack = function(target, wantedTypes)
+/**
+ * Figure out whether we can attack a given target, with some attack type.
+ * @param {number} target - The entity-ID 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 allowed.
+ * @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 entity-ID 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 allowed.
+ * @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)
{
let 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;
+ }
- let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
- let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
- if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
- return false;
+ let allowedAttackEffects = g_EffectTypes.filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]);
+ if (!allowedAttackEffects.length)
+ return [];
- let cmpIdentity = QueryMiragedInterface(target, IID_Identity);
- if (!cmpIdentity)
- return false;
+ let types = this.GetAttackTypes(wantedTypes);
+ if (!types.length)
+ return [];
- let cmpHealth = QueryMiragedInterface(target, IID_Health);
- let targetClasses = cmpIdentity.GetClassesList();
- if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() &&
- (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length))
- return true;
+ let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpThisPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return [];
let cmpEntityPlayer = QueryOwnerInterface(this.entity);
- let cmpTargetPlayer = QueryOwnerInterface(target);
- if (!cmpTargetPlayer || !cmpEntityPlayer)
- return false;
+ let cmpTargetIdentity = QueryMiragedInterface(target, IID_Identity);
+ if (!cmpEntityPlayer || !cmpTargetIdentity)
+ return [];
- let types = this.GetAttackTypes(wantedTypes);
+ let targetClasses = cmpTargetIdentity.GetClassesList();
let entityOwner = cmpEntityPlayer.GetPlayerID();
- let targetOwner = cmpTargetPlayer.GetPlayerID();
- let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
- // Check if the relative height difference is larger than the attack range
- // If the relative height is bigger, it means they will never be able to
- // reach each other, no matter how close they come.
- let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
- for (let type of types)
- {
- if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
- continue;
+ let heightDiff = cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset();
+ return types.filter(type => {
+ if (mustBeInRange)
+ {
+ let range = this.GetRange(type);
+ // Parabolic range compuation is the same as in BuildingAI's FireArrows and UnitAI's' MoveToTargetAttackRange and CheckTargetAttackRange.
+ // h is positive when I'm higher than the target.
+ let h = heightDiff + range.elevationBonus;
+
+ // 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 (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
- continue;
+ let attackEffects = this.GetAttackEffectsData(type, false);
+ let attackEffectsSplash = this.GetAttackEffectsData(type, true);
- if (heightDiff > this.GetRange(type).max)
- continue;
+ let bonusMultiplier = attackEffects && attackEffects.Bonuses ?
+ Attacking.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses) :
+ 1;
+ let bonusMultiplierSplash = attackEffectsSplash && attackEffectsSplash.Bonuses ?
+ Attacking.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 => {
+ let receiver = g_EffectReceiver[effectType];
+ let cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]);
+ if (!cmpReceiver)
+ return true;
+
+ return ((!attackEffects[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, bonusMultiplier, entityOwner)) &&
+ (!attackEffectsSplash[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffectsSplash, effectType, bonusMultiplierSplash, entityOwner)));
+ }))
+ return false;
- let restrictedClasses = this.GetRestrictedClasses(type);
- if (!restrictedClasses.length)
- return true;
+ if ((projectile == "required" && !this.template[type].Projectile) || (projectile == "disallowed" && this.template[type].Projectile))
+ return false;
- if (!MatchesClassList(targetClasses, restrictedClasses))
- return true;
- }
-
- return false;
+ let restrictedClasses = this.GetRestrictedClasses(type);
+ return !restrictedClasses.length || !MatchesClassList(targetClasses, restrictedClasses);
+ });
};
/**
@@ -379,51 +378,127 @@
{
let template = this.template[type];
if (splash)
+ {
+ if (!template.Splash)
+ return {};
template = template.Splash;
+ }
+
return Attacking.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)
{
+ // 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);
+
+ // We can't attack at all...
+ if (!types.length)
+ return undefined;
+
+ // Boring choosing...
+ if (types.length == 1)
+ return types[0];
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership)
+ return undefined;
+
+ let distance = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).DistanceToTarget(this.entity, target);
+ // In case the unit or target is not in world, we can't attack them.
+ if (distance < 0)
+ return undefined;
+
+ let owner = cmpOwnership.GetOwner();
+ let fullRange = this.GetFullAttackRange();
+ let consideredAttackEffects = g_EffectTypes.filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]);
+
+ // Choose the best attack on a DPS/Range.
+ let bestType;
+ let bestDPSRange = -Infinity;
+
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
{
- // TODO: Formation against formation needs review
- let types = this.GetAttackTypes();
- return g_AttackTypes.find(attack => types.indexOf(attack) != -1);
+ let members = cmpFormation.GetMembers();
+ for (let type of types)
+ {
+ let DPSRange = 0;
+ for (let member of members)
+ DPSRange += this.GetDPSRange(type, member, consideredAttackEffects, owner, distance, fullRange);
+
+ if (DPSRange > bestDPSRange)
+ {
+ bestType = type;
+ bestDPSRange = DPSRange;
+ }
+ }
+ return bestType;
}
- let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
- if (!cmpIdentity)
- return undefined;
-
- // 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)
+ for (let type of types)
+ {
+ let DPSRange = this.GetDPSRange(type, target, consideredAttackEffects, owner, distance, fullRange);
+ if (DPSRange > bestDPSRange)
+ {
+ bestType = type;
+ bestDPSRange = DPSRange;
+ }
+ }
+ 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 DPSRange = 0;
+ let attackEffects = this.GetAttackEffectsData(type, false);
+ let multiplier = Attacking.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses || {});
+ for (let effectType of consideredAttackEffects)
{
- if (allowCapture)
- return "Capture";
- types.splice(captureIndex, 1);
+ if (!attackEffects[effectType])
+ continue;
+ let receiver = g_EffectReceiver[effectType];
+ let cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]);
+ if (!cmpReceiver)
+ continue;
+
+ DPSRange += cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, multiplier, owner);
}
+ DPSRange /= this.GetRepeatTime(type);
- let targetClasses = cmpIdentity.GetClassesList();
- let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType));
+ // Apply an exponential dropoff when out of range.
+ // TODO elevation?
+ let range = this.GetRange(type);
+ if (distance < range.min)
+ DPSRange *= Math.pow(0.2, (range.min - distance) / (fullRange.min || 1));
+ else if (distance > range.max)
+ DPSRange *= Math.pow(0.2, (distance - range.max) / (fullRange.max || 1));
- return types.sort((a, b) =>
- (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
- (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
+ return DPSRange;
};
Attack.prototype.CompareEntitiesByPreference = function(a, b)
@@ -447,12 +522,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)
@@ -500,12 +570,38 @@
*/
Attack.prototype.PerformAttack = function(type, target)
{
- let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
+ // Safety check, TODO find out if it is required.
+ if (!this.CanAttack(target, true, {}, [type]))
+ return;
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpPosition || !cmpPosition.IsInWorld())
+ return;
+ let selfPosition = cmpPosition.GetPosition();
+
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return;
+ let targetPosition = cmpTargetPosition.GetPosition();
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership)
+ return;
+ let attackerOwner = cmpOwnership.GetOwner();
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+
+ let data = {
+ "type": type,
+ "attackData": this.GetAttackEffectsData(type),
+ "attacker": this.entity,
+ "target": target,
+ "attackerOwner": attackerOwner,
+ "splash": this.GetSplashData(type)
+ };
- // If this is a ranged attack, then launch a projectile
- if (type == "Ranged")
+ // When we have a projectile, launch it.
+ if (this.template[type].Projectile)
{
- let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let turnLength = cmpTimer.GetLatestTurnLength()/1000;
// In the future this could be extended:
// * Obstacles like trees could reduce the probability of the target being hit
@@ -515,15 +611,6 @@
let gravity = +this.template[type].Projectile.Gravity;
// horizSpeed /= 2; gravity /= 2; // slow it down for testing
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!cmpPosition || !cmpPosition.IsInWorld())
- return;
- let selfPosition = cmpPosition.GetPosition();
- let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
- if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
- return;
- let targetPosition = cmpTargetPosition.GetPosition();
-
let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength);
@@ -531,7 +618,7 @@
let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition;
// Add inaccuracy based on spread.
- let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) *
+ let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/" + type + "/Spread", +this.template[type].Projectile.Spread, this.entity) *
predictedPosition.horizDistanceTo(selfPosition) / 100;
let randNorm = randomNormal2D();
@@ -539,12 +626,13 @@
let offsetZ = randNorm[1] * distanceModifiedSpread;
let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ);
+ data.position = realTargetPosition;
// Recalculate when the missile will hit the target position.
let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition);
timeToTarget = realHorizDistance / horizSpeed;
- let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance);
+ data.direction = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance);
// Launch the graphical projectile.
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
@@ -574,31 +662,22 @@
launchPoint = visualActorLaunchPoint;
}
- let id = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime);
+ data.projectileId = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime);
- let attackImpactSound = "";
let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
- if (cmpSound)
- attackImpactSound = cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase());
+ data.attackImpactSound = cmpSound ? cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()) : "";
- let data = {
- "type": type,
- "attackData": this.GetAttackEffectsData(type),
- "target": target,
- "attacker": this.entity,
- "attackerOwner": attackerOwner,
- "position": realTargetPosition,
- "direction": missileDirection,
- "projectileId": id,
- "attackImpactSound": attackImpactSound,
- "splash": this.GetSplashData(type),
- "friendlyFire": this.template[type].Projectile.FriendlyFire == "true",
- };
+ data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true";
- cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data);
+ cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", timeToTarget * 1000 + (+this.template[type].Delay || 0), data);
}
+ // Close attack, hurt the target immediately after this.template.Delay
else
- Attacking.HandleAttackEffects(target, type, this.GetAttackEffectsData(type), this.entity, attackerOwner);
+ {
+ data.position = targetPosition;
+ data.direction = Vector3D.sub(targetPosition, selfPosition);
+ cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "CauseAttackEffects", +(this.template[type].Delay || 0), data);
+ }
};
Attack.prototype.OnValueModification = function(msg)
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,5 +1,6 @@
// Number of rounds of firing per 2 seconds.
const roundCount = 10;
+// TODO stop hardcoding the attackType, see #4000
const attackType = "Ranged";
function BuildingAI() {}
@@ -198,7 +199,7 @@
// Add new targets.
for (let entity of msg.added)
- if (cmpAttack.CanAttack(entity))
+ if (cmpAttack.CanAttack(entity, false, {}, [attackType]))
this.targetUnits.push(entity);
// Remove targets outside of vision-range.
@@ -360,9 +361,9 @@
let targetCmpPosition = Engine.QueryInterface(selectedTarget, IID_Position);
if (targetCmpPosition && targetCmpPosition.IsInWorld() && this.CheckTargetVisible(selectedTarget))
{
- // Parabolic range compuation is the same as in UnitAI's MoveToTargetAttackRange.
+ // Parabolic range compuation is the same as in UnitAI's MoveToTargetAttackRange and CheckTargetAttackRange and Attack's.
// h is positive when I'm higher than the target.
- let h = s.y - targetCmpPosition.GetPosition().y + range.elevationBonus;
+ let h = thisCmpPosition.GetHeightOffset().y - targetCmpPosition.GetHeightOffset().y + range.elevationBonus;
if (h > -range.max / 2 && cmpObstructionManager.IsInTargetRange(
this.entity,
selectedTarget,
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,19 @@
return this.garrisonRegenRate;
};
+Capturable.prototype.GetRelativeCapture = function(effectData, effectType, bonusMultiplier, attackerOwner)
+{
+ return this.CanCapture(attackerOwner) ?
+ Attacking.GetTotalAttackEffects(
+ this.entity,
+ effectData,
+ effectType,
+ bonusMultiplier,
+ QueryMiragedInterface(this.entity, IID_Resistance)
+ ) / this.maxCapturePoints :
+ 0;
+};
+
/**
* Set the new capture points, used for cloning entities.
* The caller should assure that the sum of capture points
@@ -139,9 +152,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())
Index: binaries/data/mods/public/simulation/components/DelayedDamage.js
===================================================================
--- binaries/data/mods/public/simulation/components/DelayedDamage.js
+++ binaries/data/mods/public/simulation/components/DelayedDamage.js
@@ -22,7 +22,6 @@
* @param {number} data.target - The entity id of the target.
* @param {number} data.attacker - The entity id of the attacker.
* @param {number} data.attackerOwner - The player id of the owner of the attacker.
- * @param {Vector2D} data.origin - The origin of the projectile hit.
* @param {Vector3D} data.position - The expected position of the target.
* @param {number} data.projectileId - The id of the projectile.
* @param {Vector3D} data.direction - The unit vector defining the direction.
@@ -89,4 +88,51 @@
}
};
+/**
+ * Handles damage caused by non projectile attack.
+ * @param {Object} data - The data sent by the caller.
+ * @param {string} data.type - The type of damage.
+ * @param {Object} data.attackData - Data of the form { 'effectType': { ...opaque effect data... }, 'Bonuses': {...} }.
+ * @param {number} data.target - The entity id of the target.
+ * @param {number} data.attacker - The entity id of the attacker.
+ * @param {number} data.attackerOwner - The player id of the owner of the attacker.
+ * @param {Vector3D} data.position - The expected position of the target.
+ * @param {Vector3D} data.direction - The unit vector defining the direction.
+ * @param {boolean} data.friendlyFire - A flag indicating whether allied entities can also be damaged.
+ * ***When splash damage***
+ * @param {boolean} data.splash.friendlyFire - A flag indicating if allied entities are also damaged.
+ * @param {number} data.splash.radius - The radius of the splash damage.
+ * @param {string} data.splash.shape - The shape of the splash range.
+ * @param {Object} data.splash.attackData - same as attackData, for splash.
+ */
+DelayedDamage.prototype.CauseAttackEffects = function(data, lateness)
+{
+ if (!data.position)
+ return;
+
+ // Do this first in case the direct hit kills the target
+ if (data.splash)
+ {
+ Attacking.CauseDamageOverArea({
+ "type": data.type,
+ "attackData": data.splash.attackData,
+ "attacker": data.attacker,
+ "attackerOwner": data.attackerOwner,
+ "origin": Vector2D.from3D(data.position),
+ "radius": data.splash.radius,
+ "shape": data.splash.shape,
+ "direction": data.direction,
+ "friendlyFire": data.splash.friendlyFire
+ });
+ }
+
+ let target = data.target;
+ // Since we can't damage mirages, replace a miraged target by the real target.
+ let cmpMirage = Engine.QueryInterface(data.target, IID_Mirage);
+ if (cmpMirage)
+ target = cmpMirage.GetParent();
+
+ Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner);
+};
+
Engine.RegisterSystemComponentType(IID_DelayedDamage, "DelayedDamage", DelayedDamage);
Index: binaries/data/mods/public/simulation/components/Fogging.js
===================================================================
--- binaries/data/mods/public/simulation/components/Fogging.js
+++ binaries/data/mods/public/simulation/components/Fogging.js
@@ -134,6 +134,14 @@
if (cmpCapturable)
cmpMirage.CopyCapturable(cmpCapturable);
+ var cmpStatusEffectsReceiver = Engine.QueryInterface(this.entity, IID_StatusEffectsReceiver);
+ if (cmpStatusEffectsReceiver)
+ cmpMirage.CopyStatusEffectsReceiver(cmpStatusEffectsReceiver);
+
+ var cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
+ if (cmpResistance)
+ cmpMirage.CopyResistance(cmpResistance);
+
var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply);
if (cmpResourceSupply)
cmpMirage.CopyResourceSupply(cmpResourceSupply);
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
@@ -440,18 +440,6 @@
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
- let timers = cmpAttack.GetTimers(type);
- 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())
@@ -461,6 +449,10 @@
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
+
+ let timers = cmpAttack.GetTimers(type);
+ ret.attack[type].prepareTime = timers.prepare;
+ ret.attack[type].repeatTime = timers.repeat;
}
}
@@ -1884,7 +1876,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,38 @@
};
/**
+ * @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)
+{
+
+ let cmpResistance = QueryMiragedInterface(this.entity, IID_Resistance);
+ if (cmpResistance && cmpResistance.IsInvulnerable())
+ return 0;
+
+ let cmpIdentity = QueryMiragedInterface(this.entity, IID_Identity);
+ let cmpPlayerEntity = QueryOwnerInterface(this.entity);
+ let cmpPlayerSource = QueryPlayerIDInterface(attackerOwner);
+ if (!cmpIdentity || !cmpPlayerEntity || !cmpPlayerSource)
+ return 0;
+
+ if (this.hitpoints <= 0 || (!cmpPlayerSource.IsEnemy(cmpPlayerEntity.GetPlayerID()) && !cmpIdentity.GetClassesList().includes("Domestic")))
+ return 0;
+
+ return Attacking.GetTotalAttackEffects(
+ this.entity,
+ effectData,
+ effectType,
+ bonusMultiplier,
+ QueryMiragedInterface(this.entity, IID_Resistance)
+ ) / this.maxHitpoints;
+};
+
+/**
* @return {boolean} Whether the units are injured. Dead units are not considered injured.
*/
Health.prototype.IsInjured = function()
Index: binaries/data/mods/public/simulation/components/Mirage.js
===================================================================
--- binaries/data/mods/public/simulation/components/Mirage.js
+++ binaries/data/mods/public/simulation/components/Mirage.js
@@ -26,6 +26,9 @@
this.unhealable = null;
this.injured = null;
+ this.resistanceStrengths = {};
+ this.invulnerable = null;
+
this.capturePoints = [];
this.maxCapturePoints = 0;
@@ -113,6 +116,8 @@
this.repairable = cmpHealth.IsRepairable();
this.injured = cmpHealth.IsInjured();
this.unhealable = cmpHealth.IsUnhealable();
+
+ this.GetRelativeDamage = Health.prototype.GetRelativeDamage;
};
Mirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; };
@@ -121,6 +126,18 @@
Mirage.prototype.IsInjured = function() { return this.injured; };
Mirage.prototype.IsUnhealable = function() { return this.unhealable; };
+// Resistance data
+
+Mirage.prototype.CopyResistance = function(cmpResistance)
+{
+ this.miragedIids.add(IID_Resistance);
+ this.resistanceStrengths = cmpResistance.GetEffectiveResistance();
+ this.invulnerable = cmpResistance.IsInvulnerable();
+};
+
+Mirage.prototype.IsInvulnerable = function() { return this.invulnerable; };
+Mirage.prototype.GetEffectiveResistance = function() { return this.resistanceStrengths; };
+
// Capture data
Mirage.prototype.CopyCapturable = function(cmpCapturable)
@@ -128,12 +145,22 @@
this.miragedIids.add(IID_Capturable);
this.capturePoints = clone(cmpCapturable.GetCapturePoints());
this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
+
+ this.CanCapture = Capturable.prototype.CanCapture;
+ this.GetRelativeCapture = Capturable.prototype.GetRelativeCapture;
};
Mirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; };
Mirage.prototype.GetCapturePoints = function() { return this.capturePoints; };
-Mirage.prototype.CanCapture = Capturable.prototype.CanCapture;
+// StatusEffects data
+
+Mirage.prototype.CopyStatusEffectsReceiver = function(cmpStatusEffectsReceiver)
+{
+ this.miragedIids.add(IID_StatusEffectsReceiver);
+
+ this.GetRelativeStatusEffect = StatusEffectsReceiver.prototype.GetRelativeStatusEffect;
+};
// ResourceSupply data
Index: binaries/data/mods/public/simulation/components/Resistance.js
===================================================================
--- binaries/data/mods/public/simulation/components/Resistance.js
+++ binaries/data/mods/public/simulation/components/Resistance.js
@@ -87,20 +87,13 @@
};
/**
- * Calculate the effective resistance of an entity to a particular effect.
+ * Calculate the effective resistance of an entity.
* ToDo: Support resistance against status effects.
- * @param {string} effectType - The type of attack effect the resistance has to be calculated for (e.g. "Damage", "Capture").
* @return {Object} - An object of the type { "Damage": { "Crush": number, "Hack": number }, "Capture": number }.
*/
-Resistance.prototype.GetEffectiveResistanceAgainst = function(effectType)
+Resistance.prototype.GetEffectiveResistance = function()
{
- let ret = {};
-
- let template = this.GetResistanceOfForm(Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity");
- if (template[effectType])
- ret[effectType] = template[effectType];
-
- return ret;
+ return this.GetResistanceOfForm(Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity");
};
/**
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,24 @@
};
/**
+ * 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)
+{
+ // return canTakeEffect ?
+ // Attacking.GetTotalAttackEffects(
+ // this.entity,
+ // effectData,
+ // effectType,
+ // bonusMultiplier,
+ // QueryMiragedInterface(this.entity, IID_Resistance)
+ // ) / this.max :
+ // 0;
+ return effectData[effectType] ? 1 : 0;
+};
+
+/**
* 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.
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
@@ -383,7 +383,24 @@
},
"Order.Attack": function(msg) {
- let type = this.GetBestAttackAgainst(this.order.data.target, this.order.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)
{
// Oops, we can't attack at all
@@ -514,7 +531,7 @@
if (this.MustKillGatherTarget(this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
- if (!this.GetBestAttackAgainst(this.order.data.target, false))
+ if (!this.GetBestAttackAgainst(this.order.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
@@ -545,7 +562,14 @@
return;
}
- this.PushOrderFront("Attack", { "target": this.order.data.target, "force": !!this.order.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;
}
@@ -729,14 +753,13 @@
"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();
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;
@@ -744,7 +767,7 @@
this.FinishOrder();
return;
}
- 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");
@@ -801,7 +824,16 @@
}
return;
}
- 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;
}
@@ -971,7 +1003,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");
@@ -1143,7 +1177,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
@@ -1155,10 +1189,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;
@@ -1177,10 +1210,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;
@@ -1419,7 +1451,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);
@@ -1442,7 +1474,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);
@@ -1633,7 +1665,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");
@@ -1935,16 +1969,8 @@
"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;
- if (!this.CanAttack(target))
+ if (!this.CanAttack(target, false, {}, [this.order.data.attackType]))
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return true;
@@ -2018,7 +2044,7 @@
let target = this.order.data.target;
let attackType = this.order.data.attackType;
- if (!this.CanAttack(target))
+ if (!this.CanAttack(target, false, {}, [attackType]))
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
@@ -2076,8 +2102,9 @@
// until the next Timer event
"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")
+ if ((this.GetStance().targetAttackersAlways || !this.order.data.force) &&
+ this.order.data.target != msg.data.attacker &&
+ this.GetBestAttackAgainst(msg.data.attacker, true))
this.RespondToTargetedEntities([msg.data.attacker]);
},
},
@@ -2100,10 +2127,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, (t) => this.CanAttack(t, this.GetStance().respondStandGround || !this.AbleToMove()));
this.SetNextState("COMBAT.ATTACKING");
return true;
}
@@ -3261,8 +3286,8 @@
}
else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive")
{
- if (this.CanAttack(msg.data.attacker))
- this.Attack(msg.data.attacker, false);
+ if (this.CanAttack(msg.data.attacker, this.GetStance().respondStandGround || !this.AbleToMove()))
+ this.Attack(msg.data.attacker, { "Capture": true });
}
else if (this.template.NaturalBehaviour == "domestic")
{
@@ -4602,10 +4627,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, (t) => this.CanAttack(t, false, {}, [type]));
if (!this.CheckTargetVisible(target))
return false;
@@ -4617,22 +4639,18 @@
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 BuildingAI's FireArrows, Attack's GetAllowedAttackTypes and this' CheckTargetAttackRange.
+ // 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;
@@ -4659,7 +4677,7 @@
{
let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpTargetFormation)
- target = cmpTargetFormation.GetClosestMember(this.entity);
+ target = cmpTargetFormation.GetClosestMember(this.entity, (t) => this.CanAttack(t));
if (!this.CheckTargetVisible(target))
return false;
@@ -4726,10 +4744,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)
{
@@ -4744,10 +4761,7 @@
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
- target = cmpFormation.GetClosestMember(this.entity);
-
- if (type != "Ranged")
- return this.CheckTargetRange(target, IID_Attack, type);
+ target = cmpFormation.GetClosestMember(this.entity, (t) => this.CanAttack(t, false, {}, [type]));
let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
@@ -4758,21 +4772,23 @@
return false;
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!thisCmpPosition.IsInWorld())
+ if (!thisCmpPosition || !thisCmpPosition.IsInWorld())
return false;
- let s = thisCmpPosition.GetPosition();
-
- let t = targetCmpPosition.GetPosition();
-
- let h = s.y - t.y + range.elevationBonus;
- let maxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
+ // Parabolic range compuation is the same as in BuildingAI's FireArrows, Attack's GetAllowedAttackTypes and this' MoveToTargetAttackRange.
+ // h is positive when I'm higher than the target.
+ let h = thisCmpPosition.GetHeightOffset() - targetCmpPosition.GetHeightOffset() + range.elevationBonus;
- if (maxRange < 0)
+ // In case the target is too much higher, we are out of range.
+ if (h <= -range.max / 2)
return false;
- let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
- return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, maxRange, false);
+ return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetRange(
+ this.entity,
+ target,
+ range.min,
+ Math.sqrt(Math.square(range.max) + 2 * range.max * h),
+ false);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
@@ -4791,7 +4807,7 @@
{
let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpTargetFormation)
- target = cmpTargetFormation.GetClosestMember(this.entity);
+ target = cmpTargetFormation.GetClosestMember(this.entity, (t) => this.CanAttack(t));
let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpFormationAttack)
@@ -4933,12 +4949,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);
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
- return cmpAttack.GetBestAttackAgainst(target, allowCapture);
+ return cmpAttack.GetBestAttackAgainst(target, mustBeInRange, ignoreAttackEffects, prefAttackTypes, projectile);
};
/**
@@ -4948,11 +4964,11 @@
*/
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
- var target = ents.find(target => this.CanAttack(target));
+ let target = ents.find(t => this.CanAttack(t, this.GetStance().respondStandGround || !this.AbleToMove()));
if (!target)
return false;
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
+ this.PushOrderFront("Attack", { "target": target, "force": false, "ignoreAttackEffects": {}, "prefAttackTypes": [], "projectile": undefined });
return true;
};
@@ -4963,15 +4979,15 @@
*/
UnitAI.prototype.AttackEntityInZone = function(ents)
{
- var target = ents.find(target =>
- this.CanAttack(target)
- && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
- && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
+ let target = ents.find(t =>
+ this.CanAttack(t, this.GetStance().respondStandGround || !this.AbleToMove()) &&
+ this.CheckTargetDistanceFromHeldPosition(t, IID_Attack, this.GetBestAttackAgainst(t, this.GetStance().respondStandGround || !this.AbleToMove())) &&
+ (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(t))
);
if (!target)
return false;
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
+ this.PushOrderFront("Attack", { "target": target, "force": false, "ignoreAttackEffects": {}, "prefAttackTypes": [], "projectile": undefined });
return true;
};
@@ -5373,12 +5389,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)
+UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false)
{
- this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
+ this.AddOrder("WalkAndFight", {
+ "x": x,
+ "z": z,
+ "targetClasses": targetClasses,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "force": true
+ }, queued);
};
-UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false)
+UnitAI.prototype.Patrol = function(x, z, targetClasses, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false)
{
if (!this.CanPatrol())
{
@@ -5386,7 +5410,15 @@
return;
}
- this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
+ this.AddOrder("Patrol", {
+ "x": x,
+ "z": z,
+ "targetClasses": targetClasses,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile,
+ "force": true
+ }, queued);
};
/**
@@ -5416,7 +5448,7 @@
/**
* Adds attack order to the queue, forced by the player.
*/
-UnitAI.prototype.Attack = function(target, allowCapture = true, queued = false)
+UnitAI.prototype.Attack = function(target, ignoreAttackEffects = {}, prefAttackTypes = [], projectile = undefined, queued = false)
{
if (!this.CanAttack(target))
{
@@ -5432,7 +5464,9 @@
let order = {
"target": target,
"force": true,
- "allowCapture": allowCapture,
+ "ignoreAttackEffects": ignoreAttackEffects,
+ "prefAttackTypes": prefAttackTypes,
+ "projectile": projectile
};
this.RememberTargetPosition(order);
@@ -5895,8 +5929,6 @@
var targets = cmpUnitAI.GetTargetsFromUnit();
for (var targ of targets)
{
- if (!cmpUnitAI.CanAttack(targ))
- continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
@@ -5911,7 +5943,13 @@
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
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;
}
}
@@ -5921,8 +5959,6 @@
var targets = this.GetTargetsFromUnit();
for (var targ of targets)
{
- if (!this.CanAttack(targ))
- continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
@@ -5937,7 +5973,13 @@
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
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;
}
@@ -5970,8 +6012,8 @@
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery);
- let targets = entities.filter(function(v) { return cmpAttack.CanAttack(v) && attackfilter(v); })
- .sort(function(a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); });
+ let targets = entities.filter(ent => cmpAttack.CanAttack(ent, this.GetStance().respondStandGround || !this.AbleToMove()) && attackfilter(ent))
+ .sort((a, b) => cmpAttack.CompareEntitiesByPreference(a, b));
return targets;
};
@@ -6131,7 +6173,7 @@
return component.GetRange(type);
}
-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)
@@ -6139,7 +6181,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)
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
@@ -6,6 +6,7 @@
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
+Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("Attack.js");
@@ -19,15 +20,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)
});
@@ -37,7 +43,7 @@
});
let cmpAttack = ConstructComponent(attacker, "Attack", {
- "Melee": {
+ "Spear": {
"Damage": {
"Hack": 11,
"Pierce": 5,
@@ -45,6 +51,8 @@
},
"MinRange": 3,
"MaxRange": 5,
+ "PrepareTime": 0,
+ "RepeatTime": 1000,
"PreferredClasses": {
"_string": "FemaleCitizen"
},
@@ -59,7 +67,7 @@
}
}
},
- "Ranged": {
+ "Bow": {
"Damage": {
"Hack": 0,
"Pierce": 10,
@@ -101,8 +109,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": {
@@ -125,7 +147,12 @@
}
},
"MinRange": "10",
- "MaxRange": "80"
+ "MaxRange": "80",
+ "PrepareTime": 0,
+ "RepeatTime": 1000,
+ "RestrictedClasses": {
+ "_string": "Elephant"
+ }
}
});
@@ -147,7 +174,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 (let damageType in attackEffects[effectType])
+ strength += attackEffects[effectType][damageType];
+ return strength / 100;
+ }
+ });
+
+ AddMock(defender, IID_StatusEffectsReceiver, {
+ "GetRelativeStatusEffect": () => 0.0000000001
});
test_function(attacker, cmpAttack, defender);
@@ -155,24 +195,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,
@@ -180,7 +219,7 @@
}
});
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), {
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Bow", true), {
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
@@ -215,13 +254,12 @@
}
});
- 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,
@@ -230,7 +268,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,
@@ -253,14 +298,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) => Attacking.GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null);
- TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1);
- TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1);
- TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1);
+ TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "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);
});
@@ -270,34 +315,57 @@
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) => {
-
if (isBuilding)
AddMock(defender, IID_Capturable, {
"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);
+ 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);
- for (let ac of allowCapturing)
- TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack);
});
attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => {
@@ -307,30 +375,59 @@
"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" ? "Spear" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Damage": true, "ApplyStatus": true }, ["Bow"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Spear" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Damage": true, "ApplyStatus": true }, ["!Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Spear" : "StatusEffect");
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, false, { "Damage": true, "ApplyStatus": true }, ["Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Spear" : "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);
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
@@ -3,6 +3,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");
@@ -19,6 +20,10 @@
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;
@@ -64,7 +69,8 @@
};
AddMock(atkPlayerEntity, IID_Player, {
- "GetEnemies": () => [targetOwner]
+ "GetEnemies": () => [targetOwner],
+ "GetPlayerID": () => atkPlayerEntity
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
@@ -72,6 +78,10 @@
"GetAllPlayers": () => [0, 1, 2, 3, 4]
});
+ AddMock(target, IID_Identity, {
+ "GetClassesList": () => []
+ });
+
AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
"RemoveProjectile": () => {},
"LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
@@ -82,6 +92,7 @@
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.From(targetPos),
"IsInWorld": () => true,
+ "GetHeightOffset": () => 0
});
AddMock(target, IID_Health, {
@@ -89,6 +100,7 @@
damageTaken = true;
return { "healthChange": -amount };
},
+ "GetRelativeDamage": () => 1
});
AddMock(SYSTEM_ENTITY, IID_DelayedDamage, {
@@ -123,6 +135,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
@@ -129,6 +129,7 @@
ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; },
DisableActiveQuery: function(id) { },
GetEntityFlagMask: function(identifier) { },
+ GetLosVisibility: function() { return "visible"; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
@@ -163,6 +164,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,7 +189,7 @@
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; },
@@ -203,6 +206,12 @@
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: function() { return false; }
});
@@ -231,6 +240,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 }; },
@@ -300,6 +310,7 @@
ResetActiveQuery: function(id) { return [enemy]; },
DisableActiveQuery: function(id) { },
GetEntityFlagMask: function(identifier) { },
+ GetLosVisibility: function() { return "visible"; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
@@ -338,8 +349,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; },
});
@@ -351,6 +364,7 @@
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
+ "FaceTowardsPoint": () => {},
"GetPassabilityClassName": () => "default"
});
@@ -361,7 +375,7 @@
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; },
@@ -379,6 +393,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",
@@ -424,7 +445,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/helpers/Attacking.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Attacking.js
+++ binaries/data/mods/public/simulation/helpers/Attacking.js
@@ -74,10 +74,10 @@
"" +
"" +
"" +
- "" +
+ "" +
"" +
- "" +
- "" +
+ "" +
+ "" +
"" +
"" +
"" +
@@ -167,17 +167,17 @@
if (!cmpResistance)
cmpResistance = Engine.QueryInterface(target, IID_Resistance);
- let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {};
+ let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistance() : {};
- 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,6 +196,7 @@
continue;
}
+ // TODO we need to do something about this, this can OOS.
if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance))
continue;
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
@@ -163,41 +163,56 @@
"attack-walk": function(player, cmd, data)
{
- let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
-
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued);
+ cmpUnitAI.WalkAndFight(
+ cmd.x,
+ cmd.z,
+ cmd.targetClasses,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.rangeFactor !== undefined ? cmd.rangeFactor : 1,
+ cmd.queued);
});
},
"attack-walk-custom": function(player, cmd, data)
{
- let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player).forEach(cmpUnitAI => {
- cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued);
+ cmpUnitAI.WalkAndFight(
+ cmd.targetPositions[ent].x,
+ cmd.targetPositions[ent].y,
+ cmd.targetClasses,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ cmd.queued);
});
},
"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).forEach(cmpUnitAI => {
- cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued);
+ cmpUnitAI.Attack(
+ cmd.target,
+ cmd.ignoreAttackEffects || {},
+ cmd.prefAttackTypes || [],
+ cmd.projectile || undefined,
+ cmd.queued);
});
},
"patrol": function(player, cmd, data)
{
- let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
-
GetFormationUnitAIs(data.entities, player).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)
);
},
Index: binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js
+++ binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js
@@ -1,4 +1,5 @@
Engine.LoadHelperScript("Attacking.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/template_unit_cavalry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
@@ -16,6 +16,8 @@
0.0
2
+ 1000
+ !Domestic
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
@@ -16,6 +16,8 @@
0.0
2
+ 1000
+ !Domestic
Index: binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
@@ -20,6 +20,8 @@
0.0
2
+ 1000
+ !Domestic
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
Immortal