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; }, @@ -535,7 +542,8 @@ */ "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; @@ -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; @@ -143,16 +143,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 cp 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()) @@ -164,7 +168,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 @@ -172,34 +176,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 @@ -13,7 +13,7 @@ Capturable.prototype.Init = function() { - this.maxCp = +this.template.CapturePoints; + this.maxCapturePoints = +this.template.CapturePoints; this.garrisonRegenRate = +this.template.GarrisonRegenRate; this.regenRate = +this.template.RegenRate; this.cp = []; @@ -31,7 +31,7 @@ Capturable.prototype.GetMaxCapturePoints = function() { - return this.maxCp; + return this.maxCapturePoints; }; Capturable.prototype.GetGarrisonRegenRate = function() @@ -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 @@ -123,7 +136,7 @@ } // Give all cp taken to the player. - let takenCp = this.maxCp - this.cp.reduce((a, b) => a + b); + let takenCp = this.maxCapturePoints - this.cp.reduce((a, b) => a + b); this.cp[playerID] += takenCp; this.CheckTimer(); @@ -138,6 +151,10 @@ */ Capturable.prototype.CanCapture = function(playerID) { + let cmpResistance = QueryMiragedInterface(this.entity, IID_Resistance); + if (cmpResistance && cmpResistance.IsInvulnerable()) + return false; + let cmpPlayerSource = QueryPlayerIDInterface(playerID); if (!cmpPlayerSource) @@ -263,7 +280,7 @@ { this.garrisonRegenRate = ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity); this.regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", +this.template.RegenRate, this.entity); - this.maxCp = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity); + this.maxCapturePoints = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity); }; /** @@ -273,15 +290,15 @@ */ Capturable.prototype.UpdateCachedValuesAndNotify = function(dontSendCpChanged = false) { - let oldMaxCp = this.maxCp; + let oldMaxCapturePoints = this.maxCapturePoints; let oldGarrisonRegenRate = this.garrisonRegenRate; let oldRegenRate = this.regenRate; this.UpdateCachedValues(); - if (oldMaxCp != this.maxCp) + if (oldMaxCapturePoints != this.maxCapturePoints) { - let scale = this.maxCp / oldMaxCp; + let scale = this.maxCapturePoints / oldMaxCapturePoints; for (let i in this.cp) this.cp[i] *= scale; if (!dontSendCpChanged) @@ -329,7 +346,7 @@ for (let i = 0; i < numPlayers; ++i) { if (i == msg.to) - this.cp[i] = this.maxCp; + this.cp[i] = this.maxCapturePoints; else this.cp[i] = 0; } 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() @@ -107,6 +139,21 @@ this.RegisterHealthChanged(old); }; +Health.prototype.CanDamage = function(attackerOwner) +{ + let cmpResistance = QueryMiragedInterface(this.entity, IID_Resistance); + if (cmpResistance && cmpResistance.IsInvulnerable()) + return false; + + let cmpIdentity = QueryMiragedInterface(this.entity, IID_Identity); + let cmpPlayerEntity = QueryOwnerInterface(this.entity); + let cmpPlayerSource = QueryPlayerIDInterface(attackerOwner); + if (!cmpIdentity || !cmpPlayerEntity || !cmpPlayerSource) + return false; + + return this.hitpoints > 0 && (cmpPlayerSource.IsEnemy(cmpPlayerEntity.GetPlayerID()) || cmpIdentity.GetClassesList().indexOf("Domestic") != -1); +}; + Health.prototype.IsRepairable = function() { return Engine.QueryInterface(this.entity, IID_Repairable) != null; 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,9 @@ this.repairable = cmpHealth.IsRepairable(); this.injured = cmpHealth.IsInjured(); this.unhealable = cmpHealth.IsUnhealable(); + + this.CanDamage = Health.prototype.CanDamage; + this.GetRelativeDamage = Health.prototype.GetRelativeDamage; }; Mirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; }; @@ -121,6 +127,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 +146,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(); } 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