Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -285,16 +285,19 @@ 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) +move = unused ; Modifier to move to a point instead of another attack action (e.g. gather) +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 garrison = Ctrl ; Modifier to garrison when clicking on building autorallypoint = Ctrl ; Modifier to set the rally point on the building itself +meleeattack = Alt ; Modifier to melee attack instead of another attack action (e.g. ranged attack, capture) +rangedattack = Space ; Modifier to ranged attack instead of another attack action (e.g. melee attack, capture) guard = "G" ; Modifier to escort/guard when clicking on unit/building patrol = "P" ; Modifier to patrol a unit repair = "J" ; Modifier to repair when clicking on building/mechanical unit queue = Shift ; Modifier to queue unit orders instead of replacing -orderone = Alt ; Modifier to order only one entity in selection. +orderone = "O" ; Modifier to order only one entity in selection. batchtrain = Shift ; Modifier to train units in batches massbarter = Shift ; Modifier to barter bunch of resources masstribute = Shift ; Modifier to tribute bunch of resources 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 @@ -3,14 +3,20 @@ const g_EffectReceiver = { "Damage": { "IID": "IID_Health", - "method": "TakeDamage" + "method": "TakeDamage", + "canTakeEffectMethod": "CanAttack", + "getRelativeEffectMethod": "GetRelativeDamage" }, "Capture": { "IID": "IID_Capturable", - "method": "Capture" + "method": "Capture", + "canTakeEffectMethod": "CanCapture", + "getRelativeEffectMethod": "GetRelativeCapture" }, "ApplyStatus": { "IID": "IID_StatusEffectsReceiver", - "method": "ApplyStatus" + "method": "ApplyStatus", + "canTakeEffectMethod": "CanTakeEffect", + "getRelativeEffectMethod": "GetRelativeEffect" } }; Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -196,6 +196,7 @@ }; ret.attack[type] = { + "form": template.Attack[type].Form, "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus"), 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 @@ -13,6 +13,13 @@ return g_ResourceData.GetCodes().concat(["population", "populationBonus", "time"]); } +var g_AttackForms = { + "Melee": translate("Melee Attack:"), + "Ranged": translate("Ranged Attack:"), + "Capture": translate("Capture Attack:"), + "Slaughter": translate("Slaughter Attack:") +}; + var g_DamageTypesMetadata = new DamageTypesMetadata(); var g_StatusEffectsMetadata = new StatusEffectsMetadata(); @@ -312,13 +319,8 @@ let tooltips = []; for (let attackType in template.attack) { - // Slaughter is used to kill animals, so do not show it. - if (attackType == "Slaughter") - continue; + let attackLabel = g_AttackForms[template.attack[attackType].form]; - let attackLabel = sprintf(headerFont(translate("%(attackType)s Attack")), { - "attackType": attackType - }); let attackTypeTemplate = template.attack[attackType]; let projectiles; @@ -337,7 +339,7 @@ } statusEffectsDetails = statusEffectsDetails.join(""); - tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s%(statusEffects)s"), { + tooltips.push(sprintf(translate("%(attackLabel)s %(effects)s, %(range)s, %(rate)s%(statusEffects)s"), { "attackLabel": attackLabel, "effects": attackEffectsDetails(attackTypeTemplate), "range": rangeDetails(attackTypeTemplate), 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 @@ -100,7 +100,7 @@ J + Right Click on structure – Repair P + Right Click – Patrol Shift + Right Click – Queue the move/build/gather/etc. order - Alt + Right Click – Order one unit from the current selection to move/build/gather/etc. and unselect it. Used to quickly dispatch units with specific tasks + O + Right Click – Order one unit from the current selection to move/build/gather/etc and unselect it. Used to quickly dispatch units with specific tasks. Shift + Left Click when training units – Add units in batches (the batch size is 5 by default and can be changed in the options) Shift + Left Click or Left Drag over unit on map – Add unit to selection Ctrl + Left Click or Left Drag over unit on map – Remove unit from selection @@ -112,10 +112,12 @@ Right Click with a structure(s) selected – Set a rally point for units created/ungarrisoned from that structure Ctrl + Right Click with unit(s) selected: • If the cursor is over an own or allied structure – Garrison - • If the cursor is over an enemy unit/structure – Attack (instead of capture or gather) + • If the cursor is over a non-allied unit or building: Primary Attack (instead of capture, gather) • Otherwise – Attack move (by default all enemy units and structures along the way are targeted) Ctrl + Q + Right Click with unit(s) selected – Attack move, only units along the way are targeted Ctrl + Mouse Move near structures – Align the new structure with an existing nearby structure + Alt + Right Click on non-allied unit or building – Melee attack + Space + Right Click on non-allied unit or building – Ranged attack [font="sans-bold-14"]Overlays[font="sans-14"] Alt + G – Hide/show the GUI 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,7 @@ "type": "attack", "entities": selection, "target": action.target, - "allowCapture": true, + "prefAttackForms": ["Capture"], "queued": queued }); @@ -155,7 +155,7 @@ }, "getActionInfo": function(entState, targetState) { - if (!entState.attack || !targetState.hitpoints) + if (!entState.attack) return false; return { @@ -168,19 +168,19 @@ }, "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 +188,8 @@ "type": "attack", "entities": selection, "target": action.target, - "queued": queued, - "allowCapture": false + "prefAttackForms": ["!Capture"], + "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { @@ -215,23 +215,137 @@ "hotkeyActionCheck": function(target, selection) { if (!Engine.HotkeyIsPressed("session.attack") || - !getActionInfo("attack", target, selection).possible) + !getActionInfo("attack-nocapture", target, selection).possible) return false; return { - "type": "attack", - "cursor": "action-attack", + "type": "attack-nocapture", + // TODO: new cursor? + "cursor": "action-attack-melee", "target": target }; }, "actionCheck": function(target, selection) { - if (!getActionInfo("attack", target, selection).possible) + 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", + "entities": selection, + "target": action.target, + "prefAttackForms": ["Melee"], + "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, + "types": ["Melee"] + }) + }; + }, + "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-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, + "prefAttackForms": ["Ranged"], + "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, + "types": ["Ranged"] + }) + }; + }, + "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 }; }, @@ -249,8 +363,8 @@ "z": target.z, "target": action.target, "targetClasses": { "attack": g_PatrolTargets }, - "queued": queued, - "allowCapture": false + "prefAttackForms": ["!Capture"], + "queued": queued }); DrawTargetMarker(target); @@ -337,7 +451,7 @@ "target": target }; }, - "specificness": 7, + "specificness": 6, }, "repair": @@ -721,6 +835,7 @@ if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) tooltip = coloredText(tooltip, "orange"); + //TODO check for attackTypes if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) return false; @@ -1001,8 +1116,9 @@ else if (playerCheck(entState, targetState, ["Enemy"])) { data.target = targetState.id; - data.command = "attack"; - cursor = "action-attack"; + // TODO other attackTypes should be allowed too, maybe this needs a rewrite of the rallypoint handling + data.command = "attack-nocapture"; + cursor = "action-attack-melee"; } // Don't allow the rally point to be set on any of the currently selected entities (used for unset) @@ -1050,7 +1166,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,8 @@ "type": "attack", "entities": attackers, "target": closestTarget, - "queued": true, - "allowCapture": false + "prefAttackForms": ["!Capture"], + "queued": true }); let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount); @@ -454,8 +454,8 @@ "targetClasses": { "attack": targetClass }, - "queued": true, - "allowCapture": false + "prefAttackForms": ["!Capture"], + "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,8 @@ "targetClasses": { "attack": jebelBarkal_cityPatrolGroup_balancing.targetClasses() }, - "queued": true, - "allowCapture": false + "prefAttackForms": ["!Capture"], + "queued": true }); } } @@ -578,8 +578,8 @@ "targetClasses": { "attack": spawnPointBalancing.targetClasses() }, - "queued": true, - "allowCapture": false + "prefAttackForms": ["!Capture"], + "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,7 @@ "type": "attack", "entities": attackers[spawnPoint], "target": target, + "prefAttackForms": ["!Capture"], "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 @@ -247,7 +247,7 @@ "x": targetPos.x, "z": targetPos.y, "targetClasses": undefined, - "allowCapture": false, + "prefAttackForms": ["!Capture"], "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 @@ -380,7 +380,7 @@ entities.find(e => { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); return cmpIdentity && cmpIdentity.HasClass("DefenseTower") && Engine.QueryInterface(e, IID_Position); - }) || + }) || entities.find(e => { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); return cmpIdentity && cmpIdentity.HasClass("CivilCentre") && Engine.QueryInterface(e, IID_Position); @@ -399,7 +399,7 @@ "x": position.x, "z": position.y, "targetClasses": { "attack": ["Unit"] }, - "allowCapture": false, + "prefAttackForms": ["!Capture"], "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 @@ -230,23 +230,44 @@ }; }, + // TODO: maybe DPS "attackStrengths": function(type) { let attackDamageTypes = this.get("Attack/" + type + "/Damage"); if (!attackDamageTypes) return undefined; - let damage = {}; + let strength = {}; for (let damageType in attackDamageTypes) - damage[damageType] = +attackDamageTypes[damageType]; + strength[damageType] = +attackDamageTypes[damageType]; - return damage; + strength.Capture = +this.get("Attack/" + type + "/Capture") || 0; + + return strength; }, - "captureStrength": function() { - if (!this.get("Attack/Capture")) - return undefined; + "captureStrength": function(target) { + let strength = 0; + for (let type in this.get("Attack")) + strength = Math.max(strength, +(this.get("Attack/"+ type + "/Capture")|| 0) * this.getAttackBonus(this, target, type)); - return +this.get("Attack/Capture/Capture") || 0; + return strength; + }, + + "getAttackBonus": function(target, type) { + let attackBonus = 1; + if (!this.get("Attack/" + type) || !this.get("Attack/" + type + "/Bonuses")) + return attackBonus; + let bonuses = this.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; }, "attackTimes": function(type) { @@ -523,12 +544,12 @@ "isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; }, /** - * returns true if the tempalte can capture the given target entity + * returns true if the template can capture the given target entity * if no target is given, returns true if the template has the Capture attack */ "canCapture": function(target) { - if (!this.get("Attack/Capture")) + if (!this.get("Attack") || Object.keys(this.get("Attack")).every(type => !this.get("Attack/" + type + "/Capture"))) return false; if (!target) return true; @@ -748,22 +769,16 @@ * Derived from Attack.js' similary named function. * @return {boolean} - Whether an entity can attack a given target. */ - "canAttackTarget": function(target, allowCapture) + "canAttackTarget": function(target) { let attackTypes = this.get("Attack"); if (!attackTypes) return false; - let canCapture = allowCapture && this.canCapture(target); - let armourStrengths = target.get("Armour"); - if (!armourStrengths) - return canCapture; + // TODO there was some stuff from allowCapture here, which did not esily translate for (let type in attackTypes) { - if (type == "Capture" ? !canCapture : target.isInvulnerable()) - continue; - let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses)) return true; @@ -782,8 +797,8 @@ 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, prefAttackForms = ["Capture"], prefAttackTypes = undefined, queued = false) { + Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackForms": prefAttackForms, "prefAttackTypes": prefAttackTypes, "queued": queued }); return this; }, @@ -819,8 +834,8 @@ 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, prefAttackForms = ["Capture"], prefAttackTypes = undefined, queued = false) { + Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "prefAttackForms": prefAttackForms, "prefAttackTypes": prefAttackTypes, "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,10 @@ return this; }; -m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCapture = true, queued = false) +m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, prefAttackForms = ["Capture"], prefAttackTypes = undefined, queued = false) { Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, - "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued }); + "targetClasses": targetClasses, "prefAttackForms": prefAttackForms, "prefAttackTypes": prefAttackTypes, "queued": queued }); return this; }; @@ -181,9 +181,9 @@ return this; }; -m.EntityCollection.prototype.attack = function(unitId) +m.EntityCollection.prototype.attack = function(unitId, prefAttackForms = ["Capture"], prefAttackTypes = undefined, queued = false) { - Engine.PostCommand(PlayerID, { "type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false }); + Engine.PostCommand(PlayerID, { "type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false, "prefAttackForms": prefAttackForms, "prefAttackTypes": prefAttackTypes, "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)) 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"], 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,15 @@ 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)) + if (!ent.canAttackTarget(attacker)) continue; - ent.attack(attacker.id(), allowCapture); + ent.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, ent, attacker)); 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))) + if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker)) { - ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker)); + ourUnit.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } @@ -1349,10 +1348,9 @@ 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)) + if (!ent.canAttackTarget(attacker)) continue; - ent.attack(attacker.id(), allowCapture); + ent.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } @@ -1362,8 +1360,7 @@ 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)) + if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker)) continue; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) @@ -1374,7 +1371,7 @@ if (target && !target.hasClass("Structure") && !target.hasClass("Support")) continue; } - ent.attack(attacker.id(), allowCapture); + ent.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, ent, attacker)); 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 +1388,9 @@ continue; } } - let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker); - if (ourUnit.canAttackTarget(attacker, allowCapture)) + if (ourUnit.canAttackTarget(attacker)) { - ourUnit.attack(attacker.id(), allowCapture); + ourUnit.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } @@ -1547,7 +1543,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)) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; @@ -1577,11 +1573,11 @@ return valb - vala; }); if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), PETRA.allowCapture(gameState, ent, mStruct[0])); + ent.attack(mStruct[0].id(), PETRA.getPrefAttackForms(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.getPrefAttackForms(gameState, ent, mStruct[rand])); } } else @@ -1599,7 +1595,7 @@ { let nearby = !ent.hasClass("Cavalry") && !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)) return false; if (enemy.hasClass("Animal")) return false; @@ -1639,12 +1635,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.getPrefAttackForms(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)) + ent.attack(this.target.id(), ["!Capture"]); else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) { let targetClasses = targetClassesUnit; @@ -1664,7 +1660,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)) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; @@ -1688,11 +1684,11 @@ return valb - vala; }); if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), false); + ent.attack(mStruct[0].id(), ["!Capture"]); 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.getPrefAttackForms(gameState, ent, mStruct[rand])); } } else if (needsUpdate) // really nothing let's try to help our nearest unit @@ -1712,12 +1708,12 @@ if (dist > distmin) return; distmin = dist; - if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target))) + if (!ent.canAttackTarget(target)) return; attacker = target; }); if (attacker) - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + ent.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, ent, attacker)); } } } @@ -1769,10 +1765,9 @@ if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; - let allowCapture = PETRA.allowCapture(gameState, ent, attacker); - if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture)) + if (!ent.isIdle() || !ent.canAttackTarget(attacker)) continue; - ent.attack(attacker.id(), allowCapture); + ent.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, ent, attacker)); } 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)) 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.getPrefAttackForms(gameState, ent, foeEnt), undefined, queued); } else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); @@ -574,8 +574,8 @@ 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); + if (target) + ent.attack(orderData[0].target, PETRA.getPrefAttackForms(gameState, ent, target)); } } 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,7 +468,7 @@ // Do not assign defender if it cannot attack at least part of the attacking army. if (!armiesNeeding[a].army.foeEntities.some(eEnt => { let eEntID = gameState.getEntityById(eEnt); - return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID)); + return ent.canAttackTarget(eEntID); })) 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)) continue; // Check that the unit is still attacking the structure (since the last played turn). let state = ent.unitAIState(); @@ -732,13 +732,12 @@ if (minEnt) { capturableTarget.ents.delete(minEnt.id()); - minEnt.attack(attacker.id(), PETRA.allowCapture(gameState, minEnt, attacker)); + minEnt.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, minEnt, attacker)); } } } - let allowCapture = PETRA.allowCapture(gameState, target, attacker); - if (target.canAttackTarget(attacker, allowCapture)) - target.attack(attacker.id(), allowCapture); + if (target.canAttackTarget(attacker)) + target.attack(attacker.id(), PETRA.getPrefAttackForms(gameState, target, attacker)); } } }; 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 @@ -132,16 +132,19 @@ PETRA.getSeaAccess(gameState, ent); }; -/** Decide if we should try to capture (returns true) or destroy (return false) */ -PETRA.allowCapture = function(gameState, ent, target) +// TODO: probably use some DPS to choose some specific type. +/** + * Decide if we should try to capture, melee or ranged attack. +*/ +PETRA.getPrefAttackForms = function(gameState, ent, target) { if (!target.isCapturable() || !ent.canCapture(target)) - return false; + return ["!Capture"]; if (target.isInvulnerable()) - return true; + return ["Capture"]; // always try to recapture cp from an allied, except if it's decaying if (gameState.isPlayerAlly(target.owner())) - return !target.decaying(); + return target.decaying() ? ["!Capture"] : ["Capture"]; let antiCapture = target.defaultRegenRate(); if (target.isGarrisonHolder() && target.garrisoned()) @@ -153,7 +156,7 @@ let capturableTargets = gameState.ai.HQ.capturableTargets; if (!capturableTargets.has(target.id())) { - capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); + capture = ent.captureStrength(target); capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); } else @@ -161,7 +164,7 @@ 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); capturable.ents.add(ent.id()); } capture = capturable.strength; @@ -169,26 +172,8 @@ 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; -}; - -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 capture > antiCapture + sumCapturePoints/50 ? ["Capture"] : ["!Capture"]; + return capture > antiCapture + sumCapturePoints/80 ? ["Capture"] : ["!Capture"]; }; /** 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 @@ -2501,13 +2501,13 @@ continue; if (!this.capturableTargets.has(targetId)) this.capturableTargets.set(targetId, { - "strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"), + "strength": ent.captureStrength(target), "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); capturableTarget.ents.add(ent.id()); } } @@ -2515,17 +2515,16 @@ for (let [targetId, capturableTarget] of this.capturableTargets) { let target = gameState.getEntityById(targetId); - let allowCapture; + let prefAttackForms; for (let entId of capturableTarget.ents) { let ent = gameState.getEntityById(entId); - if (allowCapture === undefined) - allowCapture = PETRA.allowCapture(gameState, ent, target); + if (prefAttackForms === undefined) + prefAttackForms = PETRA.getPrefAttackForms(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); + ent.attack(targetId, prefAttackForms); } } Index: binaries/data/mods/public/simulation/ai/petra/victoryManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/victoryManager.js +++ binaries/data/mods/public/simulation/ai/petra/victoryManager.js @@ -659,7 +659,7 @@ if (!attack) continue; for (let ent of attack.unitCollection.values()) - capture += ent.captureStrength() * PETRA.getAttackBonus(ent, relic, "Capture"); + capture += ent.captureStrength(relic); } // No need to make a new attack if already enough units if (capture > sumCapturePoints / 50) @@ -689,7 +689,7 @@ let expedition = []; for (let ent of units.values()) { - capture += ent.captureStrength() * PETRA.getAttackBonus(ent, relic, "Capture"); + capture += ent.captureStrength(relic); 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 @@ -180,10 +180,10 @@ if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") { // If we are here, an enemy structure must have targeted one of our workers - // and UnitAI sent it fight back with allowCapture=true + // and UnitAI sent it fight back with prefAttackForms === undefined 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.getPrefAttackForms(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,117 +92,92 @@ "4.0" + "" + "" + - "" + - "" + - "" + - Attacking.BuildAttackEffectsSchema() + - "" + - "" + - "" + - "" + - "" + // TODO: it shouldn't be stretched - "" + - "" + - Attack.prototype.preferredClassesSchema + - Attack.prototype.restrictedClassesSchema + - "" + - "" + - "" + - "" + - "" + - "" + - Attacking.BuildAttackEffectsSchema() + - "" + - "" + - ""+ - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - Attacking.BuildAttackEffectsSchema() + - "" + + Attacking.BuildAttackEffectsSchema() + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + - "" + - "" + - "" + - "" + - "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + Attacking.BuildAttackEffectsSchema() + + "" + "" + - "" + - "" + - "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - Attack.prototype.preferredClassesSchema + - Attack.prototype.restrictedClassesSchema + - "" + - "" + - "" + - "" + - "" + - "" + - 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() { @@ -214,7 +187,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; @@ -223,6 +196,17 @@ (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); }; +Attack.prototype.GetAttackTypesFromForms = function(forms) +{ + let types = Object.keys(this.template); + if (!forms) + return types; + + let wantedForms = forms.filter(form => form.indexOf("!") != 0); + return types.filter(type => forms.indexOf("!" + this.template[type].Form) == -1 && + (!wantedForms || !wantedForms.length || wantedForms.indexOf(this.template[type].Form) != -1)); +}; + Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && @@ -243,50 +227,66 @@ Attack.prototype.CanAttack = function(target, wantedTypes) { + return this.GetAllowedAttackTypes(target, wantedTypes).length > 0 +}; + +Attack.prototype.GetAllowedAttackTypes = function(target, wantedTypes) +{ + let types = this.GetAttackTypes(wantedTypes); let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) - return true; + { + let wantedTypes = []; + for (let member of cmpFormation.GetMembers()) + wantedTypes = wantedTypes.concat(this.GetAllowedAttackTypes(wantedTypes)); + return wantedTypes; + } let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) - return false; + return []; - let cmpIdentity = QueryMiragedInterface(target, IID_Identity); - if (!cmpIdentity) - return false; + let isTurret = cmpThisPosition.GetTurretParent() != INVALID_ENTITY - 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 cmpTargetIdentity = QueryMiragedInterface(target, IID_Identity); + if (!cmpTargetIdentity) + return []; + let targetClasses = cmpTargetIdentity.GetClassesList(); let cmpEntityPlayer = QueryOwnerInterface(this.entity); - let cmpTargetPlayer = QueryOwnerInterface(target); - if (!cmpTargetPlayer || !cmpEntityPlayer) - return false; + if (!cmpEntityPlayer) + return []; - let types = this.GetAttackTypes(wantedTypes); let entityOwner = cmpEntityPlayer.GetPlayerID(); - let targetOwner = cmpTargetPlayer.GetPlayerID(); - let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); + + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); // 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()); - for (let type of types) - { - if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) - continue; - - if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) - continue; + return types.filter(type => { + let attackEffects = this.GetAttackEffectsData(type, false); + let attackEffectsSplash = this.GetAttackEffectsData(type, true); + // We can't use the type if we can't cause any effect. + if (g_EffectTypes.every(effectType => { + if (!attackEffects[effectType] && !attackEffectsSplash[effectType]) + return true; + + let receiver = g_EffectReceiver[effectType]; + let cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]); + return !cmpReceiver || !cmpReceiver[receiver.canTakeEffectMethod](entityOwner); + })) + return false; + + // If we are visisble garrisoned always do attacks with projectiles. + if (isTurret && this.GetRange(type).max < cmpObstructionManager.DistanceToTarget(this.entity, target)) + return false; if (heightDiff > this.GetRange(type).max) - continue; + return false; let restrictedClasses = this.GetRestrictedClasses(type); if (!restrictedClasses.length) @@ -294,9 +294,7 @@ if (!MatchesClassList(targetClasses, restrictedClasses)) return true; - } - - return false; + }); }; /** @@ -344,52 +342,111 @@ Attack.prototype.GetAttackEffectsData = function(type, splash) { let template = this.template[type]; - if (splash) + if (splash && template.Splash) template = template.Splash; + return Attacking.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity); }; /** * Find the best attack against a target. * @param {number} target - The entity-ID of the target. - * @param {boolean} allowCapture - Whether capturing is allowed. + * @param {Array} prefAttackForms - List of (negated) attack forms which are prefered above others. + * @param {Array} prefAttackTypes - List of (negated) attack types which are prefered above others. * @return {string} - The preferred attack type. */ -Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) +Attack.prototype.GetBestAttackAgainst = function(target, prefAttackForms = [], prefAttackTypes = []) { + let prefFormTypes = this.GetAttackTypesFromForms(prefAttackForms); 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 types = this.GetAttackTypes(prefAttackTypes.concat(prefFormTypes)); + if (types.length) + return types[0]; + types = this.GetAttackTypes(prefFormTypes); + if (types.length) + return types[0]; + types = this.GetAttackTypes(prefAttackTypes); + if (types.length) + return types[0]; + types = this.GetAttackTypes() + return types.length ? types[0] : 0; + } - let cmpIdentity = Engine.QueryInterface(target, IID_Identity); - if (!cmpIdentity) + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return undefined; + + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership) + if (!cmpOwnership) return undefined; + let attackerOwner = cmpOwnership.GetOwner(); - // 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) + // Try find an allowed attack preferred by prefAttackForms and prefAttackTypes. + // If none exists, try all prefered by prefAttackForms or prefAttackTypes respectively. + // If still none exists, try all all types in the template + let types = this.GetAllowedAttackTypes(target, prefAttackTypes.concat(prefFormTypes)); + if (!types.length) { - if (allowCapture) - return "Capture"; - types.splice(captureIndex, 1); + types = this.GetAllowedAttackTypes(target, prefFormTypes); + if (!types.length) + { + types = this.GetAllowedAttackTypes(target, prefAttackTypes); + if (!types.length) + { + types = this.GetAllowedAttackTypes(target); + if (!types.length) + return undefined; + } + } } - let targetClasses = cmpIdentity.GetClassesList(); - let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType)); + // For performance: don't worry calculating the best when there is only one + if (types.length == 1) + return types[0]; + + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let distance = cmpObstructionManager.DistanceToTarget(this.entity, target); + let fullRange = this.GetFullAttackRange(); + // Choose the best attack on a DPS/Range + let bestType; + let bestDPSRange = -Infinity; + for (let type of types) + { + let DPSRange = 0; + let attackEffects = this.GetAttackEffectsData(type, false); + let multiplier = GetAttackBonus(this.entity, target, type, attackEffects.Bonuses || {}); + for (let effectType of g_EffectTypes) + { + if (!attackEffects[effectType]) + continue; + + let receiver = g_EffectReceiver[effectType]; + let cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]); + if (!cmpReceiver) + continue; - return types.sort((a, b) => - (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) - - (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); + DPSRange += cmpReceiver[receiver.getRelativeEffectMethod](attackEffects[effectType], effectType, attackerOwner) * multiplier; + } + DPSRange /= this.GetRepeatTime(type); + + // 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)); + + if (DPSRange > bestDPSRange) + { + bestType = type; + bestDPSRange = DPSRange; + } + } + return bestType }; Attack.prototype.CompareEntitiesByPreference = function(a, b) @@ -403,22 +460,22 @@ return aPreference - bPreference; }; +// TODO what if !this.template[type] 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); + if (this.template[type]) + return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", +this.template[type].RepeatTime, this.entity); + return 0; }; Attack.prototype.GetTimers = function(type) { - return { - "prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity), - "repeat": this.GetRepeatTime(type) - }; + if (this.template[type]) + return { + "prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity), + "repeat": this.GetRepeatTime(type) + }; + return {}; }; Attack.prototype.GetSplashData = function(type) @@ -451,6 +508,17 @@ return { "max": max, "min": min, "elevationBonus": elevationBonus }; }; +Attack.prototype.GetForm = function(type) +{ + return this.template[type] ? this.template[type].Form : ""; +}; + +Attack.prototype.HasProjectile = function(type) +{ + return !!(this.template[type] && this.template[type].Projectile); +}; + + /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last @@ -458,12 +526,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, [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); - // If this is a ranged attack, then launch a projectile - if (type == "Ranged") + let data = { + "type": type, + "attackData": this.GetAttackEffectsData(type), + "attacker": this.entity, + "target": target, + "attackerOwner": attackerOwner, + "splash": this.GetSplashData(type) + }; + // 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 @@ -473,15 +567,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); @@ -489,7 +574,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(); @@ -497,12 +582,14 @@ 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); @@ -532,31 +619,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(type, this.GetAttackEffectsData(type), target, this.entity, attackerOwner); + { + data.position = targetPosition; + data.direction = new Vector3D.sub(targetPosition, selfPosition); + cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "CauseAttackEffects", +(this.template[type].Delay || 0), data); + } }; /** 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 @@ -40,6 +40,16 @@ return this.garrisonRegenRate; }; +Capturable.prototype.GetRelativeCapture = function(effectData, effectType, attackerOwner) +{ + let cmpHealth = QueryMiragedInterface(this.entity, IID_Health); + let hitpoints = cmpHealth && cmpHealth.GetHitpoints(); + if (attackerOwner == INVALID_PLAYER || !this.CanCapture(attackerOwner) || hitpoints) + return 0; + + return Attacking.GetTotalAttackEffects({ "Capture": effectData }, effectType) * maxHitpoints / (0.1 * maxHitpoints + 0.9 * hitpoints) / this.maxCp; +}; + /** * Set the new capture points, used for cloning entities * The caller should assure that the sum of capture points @@ -53,12 +63,12 @@ Capturable.prototype.Capture = function(effectData, attacker, attackerOwner, bonusMultiplier) { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); - - if (attackerOwner == INVALID_PLAYER || !this.CanCapture(attackerOwner) || - !cmpHealth || cmpHealth.GetHitpoints() == 0) + let hitpoints = cmpHealth && cmpHealth.GetHitpoints(); + if (attackerOwner == INVALID_PLAYER || !this.CanCapture(attackerOwner) || !hitpoints) return {}; - bonusMultiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints()); + let maxHitpoints = cmpHealth.GetMaxHitpoints(); + bonusMultiplier *= maxHitpoints / (0.1 * maxHitpoints + 0.9 * hitpoints); let total = Attacking.GetTotalAttackEffects({ "Capture": effectData }, "Capture") * bonusMultiplier; 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. @@ -91,4 +90,45 @@ } }; +/** + * Handles damage caused by immediate 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 + }); + } + + Attacking.HandleAttackEffects(data.type, data.attackData, data.target, 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 @@ -132,6 +132,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 (cmpCapturable) + 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 @@ -411,6 +411,7 @@ ret.attack[type] = {}; Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); + ret.attack[type].form = cmpAttack.GetForm(type); ret.attack[type].splash = cmpAttack.GetSplashData(type); if (ret.attack[type].splash) @@ -419,21 +420,7 @@ let range = cmpAttack.GetRange(type); 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()) // For units, take the range in front of it, no spread, so angle = 0, // else, take the average elevation around it: angle = 2 * pi. @@ -441,6 +428,12 @@ 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; + + ret.attack[type].projectile = cmpAttack.HasProjectile(type); } } 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,18 @@ }; /** + * Get damage according to the entity's resistance. + * @return {number} - + */ +Health.prototype.GetRelativeDamage = function(effectData, effectType, attackerOwner) +{ + let cmpResistance = QueryMiragedInterface(this.entity, IID_Resistance); + if (cmpResistance && cmpResistance.IsInvulnerable()) + return 0; + + return Attacking.GetTotalAttackEffects(effectData, effectType, cmpResistance) / this.maxHitpoints; +}; +/** * @return {boolean} Whether the units are injured. Dead units are not considered injured. */ Health.prototype.IsInjured = function() @@ -107,6 +119,18 @@ this.RegisterHealthChanged(old); }; +// TODO: Invulnerabe targets should maybe be excluded. +Health.prototype.CanAttack = function(playerID) +{ + let cmpIdentity = QueryMiragedInterface(this.entity, IID_Identity); + let cmpPlayerEntity = QueryOwnerInterface(this.entity); + let cmpPlayerSource = QueryPlayerIDInterface(playerID); + 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.armourStrengths = {}; + this.invulnerable = null; + this.capturePoints = []; this.maxCapturePoints = 0; @@ -115,6 +118,20 @@ Mirage.prototype.IsInjured = function() { return this.injured; }; Mirage.prototype.IsUnhealable = function() { return this.unhealable; }; +Mirage.prototype.CanAttack = Health.prototype.CanAttack; +Mirage.prototype.GetRelativeDamage = Health.prototype.GetRelativeDamage; + +// Armour data + +Mirage.prototype.CopyResistance = function(cmpResistance) +{ + this.miragedIids.add(IID_Resistance); + this.armourStrengths = cmpResistance.GetArmourStrengths("Damage"); + this.invulnerable = cmpResistance.IsInvulnerable(); +}; +Mirage.prototype.IsInvulnerable = function() { return this.invulnerable; } +Mirage.prototype.GetArmourStrengths = function(effectType) { return this.armourStrengths; }; + // Capture data Mirage.prototype.CopyCapturable = function(cmpCapturable) @@ -128,6 +145,17 @@ Mirage.prototype.GetCapturePoints = function() { return this.capturePoints; }; Mirage.prototype.CanCapture = Capturable.prototype.CanCapture; +Mirage.prototype.GetRelativeCapture = Capturable.prototype.GetRelativeCapture; + +// StatusEffects data + +Mirage.prototype.CopyStatusEffectsReceiver = function(cmpStatusEffectsReceiver) +{ + this.miragedIids.add(IID_StatusEffectsReceiver); +}; + +Mirage.prototype.CanTakeEffect = Capturable.prototype.CanTakeEffect; +Mirage.prototype.GetRelativeEffects = Capturable.prototype.GetRelativeEffects; // ResourceSupply data 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,23 @@ }; /** + * Quantify how much the effect does, relative to the maximum amount. + * For now return 0. + */ +StatusEffectsReceiver.prototype.GetRelativeEffect = function(effectData, effectType, attackerOwner) +{ + return 0; +}; + +/** + * Return whether an effect can be applied. Since effects can always be applied, return true. + */ +StatusEffectsReceiver.prototype.CanTakeEffect = function(playerID) +{ + return true; +}; + +/** * 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 @@ -405,6 +405,14 @@ }, "Order.Attack": function(msg) { + // 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)); + } + // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { @@ -413,7 +421,7 @@ } // Work out how to attack the given target - var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture); + var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.prefAttackForms, this.order.data.prefAttackTypes); if (!type) { // Oops, we can't attack at all @@ -533,7 +541,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, ["!Capture"])) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed @@ -564,7 +572,7 @@ 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, "prefAttackForms": ["!Capture"], "prefAttackTypes": [] }); return; } @@ -753,7 +761,6 @@ "Order.Attack": function(msg) { let target = msg.data.target; - let allowCapture = msg.data.allowCapture; let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) target = cmpTargetUnitAI.GetFormationController(); @@ -769,7 +776,7 @@ this.FinishOrder(); return; } - this.CallMemberFunction("Attack", [target, allowCapture, false]); + this.CallMemberFunction("Attack", [target, msg.data.prefAttackForms, msg.data.prefAttackTypes, false]); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (cmpAttack && cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); @@ -827,7 +834,7 @@ } 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, "prefAttackForms": ["!Capture"], "prefAttackTypes": [], "min": 0, "max": 10 }); return; } @@ -1009,7 +1016,8 @@ { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; - this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; + this.patrolStartPosOrder.prefAttackForms = this.order.data.prefAttackForms; + this.patrolStartPosOrder.prefAttackTypes = this.order.data.prefAttackTypes; } if (!this.MoveTo(this.order.data)) @@ -1154,7 +1162,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.prefAttackForms, this.order.data.prefAttackTypes, false]); if (cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else @@ -1166,14 +1174,13 @@ // Wait for individual members to finish "enter": function(msg) { let target = this.order.data.target; - let allowCapture = this.order.data.allowCapture; // Check if we are already in range, otherwise walk there if (!this.CheckFormationTargetAttackRange(target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { this.FinishOrder(); - this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture }); + this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackForms": this.order.data.prefAttackForms, "prefAttackTypes": this.order.data.prefAttackTypes }); return true; } this.FinishOrder(); @@ -1190,14 +1197,13 @@ "Timer": function(msg) { let target = this.order.data.target; - let allowCapture = this.order.data.allowCapture; // Check if we are already in range, otherwise walk there if (!this.CheckFormationTargetAttackRange(target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { this.FinishOrder(); - this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture }); + this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackForms": this.order.data.prefAttackForms, "prefAttackTypes": this.order.data.prefAttackTypes }); return; } this.FinishOrder(); @@ -1437,7 +1443,7 @@ // target the unit 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, "prefAttackForms": ["Capture"], "prefAttackTypes": [] }); else { var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position); @@ -1615,7 +1621,8 @@ { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; - this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; + this.patrolStartPosOrder.prefAttackForms = this.order.data.prefAttackForms; + this.patrolStartPosOrder.prefAttackTypes = this.order.data.prefAttackTypes; } this.StartTimer(0, 1000); @@ -1891,14 +1898,6 @@ "ATTACKING": { "enter": function() { let target = this.order.data.target; - let cmpFormation = Engine.QueryInterface(target, IID_Formation); - // if the target is a formation, save the attacking formation, and pick a member - if (cmpFormation) - { - this.order.data.formationTarget = target; - target = cmpFormation.GetClosestMember(this.entity); - this.order.data.target = target; - } if (!this.CanAttack(target)) { @@ -2027,8 +2026,9 @@ "Attacked": function(msg) { // If we are capturing and are attacked by something that we would not capture, attack that entity instead + // TODO find out why we want this and stop hardcoding the "Capture" type if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) - && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture") + && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, ["Capture"]) != "Capture") this.RespondToTargetedEntities([msg.data.attacker]); }, }, @@ -2043,9 +2043,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); + let target = cmpFormation.GetClosestMember(this.entity, (t) => this.CanAttack(t)); this.order.data.target = target; this.SetNextState("COMBAT.ATTACKING"); return true; @@ -4559,10 +4558,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)); if (!this.CheckTargetVisible(target)) return false; @@ -4615,7 +4611,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; @@ -4700,7 +4696,7 @@ let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) - target = cmpFormation.GetClosestMember(this.entity); + target = cmpFormation.GetClosestMember(this.entity, (t) => this.CanAttack(t)); if (type != "Ranged") return this.CheckTargetRange(target, IID_Attack, type); @@ -4747,7 +4743,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) @@ -4894,12 +4890,12 @@ return distance < range; }; -UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture) +UnitAI.prototype.GetBestAttackAgainst = function(target, prefAttackForms, prefAttackTypes) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; - return cmpAttack.GetBestAttackAgainst(target, allowCapture); + return cmpAttack.GetBestAttackAgainst(target, prefAttackForms, prefAttackTypes); }; /** @@ -4913,7 +4909,7 @@ if (!target) return false; - this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true }); + this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackForms": ["!Capture"], "prefAttackTypes": [] }); return true; }; @@ -4926,13 +4922,13 @@ { var target = ents.find(target => this.CanAttack(target) - && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true)) + && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, ["Capture"])) && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)) ); if (!target) return false; - this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true }); + this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackForms": ["!Capture"], "prefAttackTypes": [] }); return true; }; @@ -5328,12 +5324,12 @@ * 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, prefAttackForms = ["Capture"], prefAttackTypes = [], 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, "prefAttackForms": prefAttackForms, "prefAttackTypes": prefAttackTypes, "force": true }, queued); }; -UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false) +UnitAI.prototype.Patrol = function(x, z, targetClasses, prefAttackForms = ["Capture"], prefAttackTypes = [], queued = false) { if (!this.CanPatrol()) { @@ -5341,7 +5337,7 @@ return; } - this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued); + this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "prefAttackForms": prefAttackForms, "prefAttackTypes": prefAttackTypes, "force": true }, queued); }; /** @@ -5372,7 +5368,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, prefAttackForms = ["Capture"], prefAttackTypes = [], queued = false) { if (!this.CanAttack(target)) { @@ -5388,7 +5384,8 @@ let order = { "target": target, "force": true, - "allowCapture": allowCapture, + "prefAttackForms": prefAttackForms, + "prefAttackTypes": prefAttackTypes }; this.RememberTargetPosition(order); @@ -5849,7 +5846,7 @@ 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, "prefAttackForms": this.order.data.prefAttackForms, "prefAttackTypes": this.order.data.prefAttackTypes }); return true; } } @@ -5875,7 +5872,7 @@ 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, "prefAttackForms": this.order.data.prefAttackForms, "prefAttackTypes": this.order.data.prefAttackTypes }); return true; } 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 @@ -8,6 +8,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"); @@ -25,8 +26,11 @@ }); AddMock(playerEnt1, IID_Player, { - "GetPlayerID": () => 1, - "IsEnemy": () => isEnemy + "GetPlayerID": () => 1 + }); + + AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + "DistanceToTarget": (ent, target) => 10 }); } @@ -34,6 +38,7 @@ AddMock(attacker, IID_Position, { "IsInWorld": () => true, + "GetTurretParent": () => INVALID_ENTITY, "GetHeightOffset": () => 5, "GetPosition2D": () => new Vector2D(1, 2) }); @@ -43,7 +48,8 @@ }); let cmpAttack = ConstructComponent(attacker, "Attack", { - "Melee": { + "Spear": { + "Form": "Melee", "Damage": { "Hack": 11, "Pierce": 5, @@ -51,6 +57,8 @@ }, "MinRange": 3, "MaxRange": 5, + "PrepareTime": 0, + "RepeatTime": 1000, "PreferredClasses": { "_string": "FemaleCitizen" }, @@ -65,7 +73,8 @@ } } }, - "Ranged": { + "Arrow": { + "Form": "Ranged", "Damage": { "Hack": 0, "Pierce": 10, @@ -105,11 +114,28 @@ } }, "Capture": { + "Form": "Capture", "Capture": 8, "MaxRange": 10, + "PrepareTime": 0, + "RepeatTime": 1000 + }, + "Slaughter": { + "Form": "Slaughter", + "Damage": { + "Hack": 100, + "Pierce": 0, + "Crush": 0 + }, + "MaxRange": 5, + "PrepareTime": 0, + "RepeatTime": 1000, + "RestrictedClasses": { + "_string": "!Domestic" + }, }, - "Slaughter": {}, "StatusEffect": { + "Form": "Ranged", "ApplyStatus": { "StatusInternalName": { "StatusName": "StatusShownName", @@ -131,7 +157,12 @@ } }, "MinRange": "10", - "MaxRange": "80" + "MaxRange": "80", + "PrepareTime": 0, + "RepeatTime": 1000, + "RestrictedClasses": { + "_string": "Elephant" + }, } }); @@ -152,7 +183,20 @@ }); AddMock(defender, IID_Health, { - "GetHitpoints": () => 100 + "GetHitpoints": () => 100, + "GetRelativeDamage": (effectData, effectType) => { + let strength = 0 + for (let type in effectData) + strength += effectData[type]; + return strength / 100 + }, + + "CanAttack": () => isEnemy || defenderClass == "Domestic" + }); + + AddMock(defender, IID_StatusEffectsReceiver, { + "CanTakeEffect": () => true, + "GetRelativeEffect": () => 0 }); test_function(attacker, cmpAttack, defender); @@ -161,23 +205,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", "Arrow", "Capture", "Slaughter", "StatusEffect"]); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Spear", "Arrow", "Capture", "Slaughter", "StatusEffect"]); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Spear", "Arrow", "Capture"]), ["Spear", "Arrow", "Capture"]); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Spear", "Arrow"]), ["Spear", "Arrow"]); 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"]), ["Arrow", "Capture", "Slaughter", "StatusEffect"]); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Spear", "!Arrow"]), ["Capture", "Slaughter", "StatusEffect"]); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Arrow"]), ["Capture"]); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Spear", "!Arrow"]), ["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("Arrow"), { "Damage": { "Hack": 0, "Pierce": 10, @@ -185,7 +229,7 @@ } }); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), { + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Arrow", true), { "Damage": { "Hack": 0.0, "Pierce": 15.0, @@ -223,13 +267,13 @@ } }); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Arrow"), { "prepare": 300, "repeat": 500 }); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Ranged"), 500); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Arrow"), 500); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), { "prepare": 0, @@ -238,7 +282,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("Arrow"), { "attackData": { "Damage": { "Hack": 0, @@ -261,14 +312,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) => 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, "Arrow", defender), 1); + TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Arrow", defender, true), className == "Cavalry" ? 3 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1); }); @@ -278,7 +329,7 @@ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false); }); -function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false) +function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = false) { attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => { @@ -287,25 +338,34 @@ "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; - } + }, + "GetRelativeCapture": (effectData, effectType) => effectData / 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, ["Arrow"]), true); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Spear"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer"); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); - - let allowCapturing = [true]; - if (!isBuilding) - allowCapturing.push(false); - - for (let ac of allowCapturing) - TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Slaughter"]), defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Slaughter"]), true); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Spear", "Capture"]), defenderClass != "Archer"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Arrow", "Capture"]), true); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Arrow", "!Spear", "!StatusEffect"]), isBuilding || defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Spear", "!Spear"]), false); + + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender), bestAttack); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, []), bestAttack); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Slaughter"]), bestAttack); + + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["!Slaughter"]), bestAttack == "Slaughter" ? "Arrow" : bestAttack); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Ranged"]), "Arrow"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["!Melee"]), bestAttack); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Capture"]), bestAttack); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Melee", "Capture"]), isBuilding ? "Capture" : defenderClass == "Archer" ? "Arrow" : "Spear"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Ranged", "Capture"]), isBuilding ? "Capture" : "Arrow"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["!Ranged", "!Melee"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : bestAttack); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Melee", "!Melee"]), bestAttack); }); attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => { @@ -315,33 +375,45 @@ "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; - } + }, + "GetRelativeCapture": (effectData, effectType) => effectData / 10 }); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic"); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic"); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic"); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); - TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); - - let allowCapturing = [true]; - if (!isBuilding) - allowCapturing.push(false); - - for (let ac of allowCapturing) - TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!StatusEffect"]), isBuilding || defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!StatusEffect"]), isBuilding || defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Arrow", "!StatusEffect"]), defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Spear", "!StatusEffect"]), isBuilding || defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture", "!StatusEffect"]), isBuilding); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Spear", "Capture", "!StatusEffect"]), isBuilding || defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Arrow", "Capture", "!StatusEffect"]), isBuilding || defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Slaughter", "!StatusEffect"]), defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Arrow", "!Spear", "!StatusEffect"]), isBuilding || defenderClass == "Domestic"); + TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Spear", "!Spear"]), false); + + + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, []), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["!StatusEffect"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Slaughter"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["!Slaughter"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Arrow" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Ranged"]), defenderClass == "Domestic" ? "Arrow" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Melee"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Spear" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["!Melee"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Melee", "Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Spear" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Ranged", "Capture"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Arrow" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["!Ranged", "!Melee"]), isBuilding ? "Capture" : defenderClass == "Domestic" ? "Slaughter" : "StatusEffect"); + TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ["Melee", "!Melee"]), 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", "Arrow"); +testGetBestAttackAgainst("Archer", "Arrow"); +testGetBestAttackAgainst("Domestic", "Slaughter"); +testGetBestAttackAgainst("Structure", "Capture", true); +testGetBestAttackAgainst("Structure", "Arrow", false); function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity) { 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 @@ -6,12 +6,12 @@ Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/AttackDetection.js"); Engine.LoadComponentScript("interfaces/DelayedDamage.js"); +Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); -Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); @@ -69,7 +69,8 @@ }; AddMock(atkPlayerEntity, IID_Player, { - "GetEnemies": () => [targetOwner] + "GetEnemies": () => [targetOwner], + "GetPlayerID": () => atkPlayerEntity }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { @@ -77,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) => {}, @@ -87,6 +92,7 @@ "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, + "GetHeightOffset": () => 0 }); AddMock(target, IID_Health, { @@ -94,6 +100,7 @@ damageTaken = true; return { "killed": false, "HPchange": -bonusMultiplier * effectData.Crush }; }, + "CanAttack": () => true }); AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { @@ -128,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 @@ -185,7 +185,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; }, @@ -344,7 +344,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; }, 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,35 @@ "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.prefAttackForms || [], cmd.prefAttackTypes || [], 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.prefAttackForms || [], cmd.prefAttackTypes || [], cmd.queued); }); }, "attack": function(player, cmd, data) { - let allowCapture = cmd.allowCapture || cmd.allowCapture == null; - - if (g_DebugCommands && !allowCapture && + // TODO: there shouldn't be an error when we capture + if (g_DebugCommands && !(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.prefAttackForms || [], cmd.prefAttackTypes || [], 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.prefAttackForms || [], cmd.prefAttackTypes || [], cmd.queued) ); }, Index: binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml @@ -7,6 +7,7 @@ +
Melee
15.0 10.0 Index: binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml =================================================================== --- binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml +++ binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml @@ -12,6 +12,7 @@ +
Ranged
0.0 25.0 Index: binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml +++ binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml @@ -17,6 +17,7 @@ +
Ranged
0.0 12.0 Index: binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml +++ binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml @@ -2,6 +2,7 @@ +
Ranged
0 0 Index: binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml +++ binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 8.0 Index: binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml +++ binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml @@ -7,6 +7,7 @@ +
Ranged
0.0 16.0 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 @@ -7,18 +7,22 @@ +
Capture
2 4 1000 Field Palisade SiegeWall StoneWall
+
Slaughter
100.0 0.0 0.0 2 + 1000 + !Domestic
Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee.xml @@ -6,6 +6,7 @@ +
Melee
13 0 Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml @@ -2,6 +2,7 @@ +
Ranged
0 9.0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion.xml @@ -2,6 +2,7 @@ +
Capture
5 4 1000 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml @@ -2,6 +2,7 @@ +
Ranged
0 14.0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 36.0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml @@ -6,6 +6,7 @@ +
Melee
12.0 10.0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml @@ -6,6 +6,7 @@ +
Melee
13.0 0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_elephant_melee.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_elephant_melee.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_elephant_melee.xml @@ -2,6 +2,7 @@ +
Melee
20 0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml @@ -2,6 +2,7 @@ +
Ranged
0 6.5 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml @@ -2,6 +2,7 @@ +
Ranged
0 26.0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_maceman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_maceman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_maceman.xml @@ -6,6 +6,7 @@ +
Melee
0 0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml @@ -7,6 +7,7 @@ +
Melee
2.0 6.0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml @@ -6,6 +6,7 @@ +
Melee
6.0 5.0 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml @@ -6,6 +6,7 @@ +
Melee
11.0 0 Index: binaries/data/mods/public/simulation/templates/template_unit_dog.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_dog.xml +++ binaries/data/mods/public/simulation/templates/template_unit_dog.xml @@ -7,6 +7,7 @@ +
Melee
7 2 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive.xml @@ -2,6 +2,7 @@ +
Melee
1 1 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_bull.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_bull.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_bull.xml @@ -2,6 +2,7 @@ +
Melee
1 5 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml @@ -7,6 +7,7 @@ +
Melee
25.0 10.0 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_violent.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_violent.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_violent.xml @@ -2,6 +2,7 @@ +
Melee
1 1 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive.xml @@ -2,6 +2,7 @@ +
Melee
1 1 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_defensive_fox.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_defensive_fox.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_defensive_fox.xml @@ -2,6 +2,7 @@ +
Melee
2.5 5.0 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_violent.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_violent.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_violent.xml @@ -2,6 +2,7 @@ +
Melee
1 1 Index: binaries/data/mods/public/simulation/templates/template_unit_hero.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero.xml @@ -7,6 +7,7 @@ +
Capture
15 4 1000 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml @@ -2,6 +2,7 @@ +
Ranged
0 35 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 60.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_spearman.xml @@ -6,6 +6,7 @@ +
Melee
24.0 20.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_swordsman.xml @@ -6,6 +6,7 @@ +
Melee
26.0 0.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml @@ -7,6 +7,7 @@ +
Melee
20.0 0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_archer.xml @@ -2,6 +2,7 @@ +
Ranged
0 8.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_javelinist.xml @@ -2,6 +2,7 @@ +
Ranged
0 50.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml @@ -6,6 +6,7 @@ +
Melee
4.0 20.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml @@ -6,6 +6,7 @@ +
Melee
12.0 10.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml @@ -6,6 +6,7 @@ +
Melee
22.0 0 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 @@ -7,18 +7,22 @@ +
Capture
2 4 1000 Field Palisade SiegeWall StoneWall
+
Slaughter
50.0 0.0 0.0 2 + 1000 + !Domestic
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee.xml @@ -2,6 +2,7 @@ +
Melee
1 0 Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml @@ -7,6 +7,7 @@ +
Ranged
0 1.5 Index: binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 35.0 Index: binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml +++ binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml @@ -2,6 +2,7 @@ +
Melee
10.0 10.0 Index: binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml +++ binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml @@ -7,6 +7,7 @@ +
Melee
10.0 0.0 Index: binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 10.0 Index: binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 35.0 Index: binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml +++ binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 150.0 Index: binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml +++ binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml @@ -2,6 +2,7 @@ +
Melee
0.0 0.0 Index: binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml +++ binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 10.0 Index: binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml +++ binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 12.0 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 @@ -2,6 +2,7 @@ +
Melee
2.0 0 @@ -12,12 +13,15 @@ 1000
+
Slaughter
25.0 0.0 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 @@ + + +
Ranged
+ + 0 + 6.0 + 0 + + 72.0 + 0.0 + 600 + 1000 + + 75.0 + 3.0 + false + 9.81 + + +
+
pers Immortal Index: binaries/data/mods/public/simulation/templates/units/plane.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/plane.xml +++ binaries/data/mods/public/simulation/templates/units/plane.xml @@ -2,6 +2,7 @@ +
Ranged
0.0 100.0 Index: binaries/data/mods/public/simulation/templates/units/theb_siege_fireraiser.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/theb_siege_fireraiser.xml +++ binaries/data/mods/public/simulation/templates/units/theb_siege_fireraiser.xml @@ -2,6 +2,7 @@ +
Ranged
50.0 0.0 Index: source/simulation2/components/ICmpAttack.h =================================================================== --- source/simulation2/components/ICmpAttack.h +++ source/simulation2/components/ICmpAttack.h @@ -24,6 +24,7 @@ { public: virtual float GetRepeatTime(const std::string& type) const = 0; + virtual std::vector GetAttackTypes() const = 0; DECLARE_INTERFACE_TYPE(Attack) }; Index: source/simulation2/components/ICmpAttack.cpp =================================================================== --- source/simulation2/components/ICmpAttack.cpp +++ source/simulation2/components/ICmpAttack.cpp @@ -31,10 +31,15 @@ public: DEFAULT_SCRIPT_WRAPPER(AttackScripted) - virtual float GetRepeatTime(const std::string& type) const - { - return m_Script.Call("GetRepeatTime", type); - } + virtual float GetRepeatTime(const std::string& type) const + { + return m_Script.Call("GetRepeatTime", type); + } + + virtual std::vector GetAttackTypes() const + { + return m_Script.Call>("GetAttackTypes"); + } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(AttackScripted) Index: source/tools/atlas/GameInterface/ActorViewer.h =================================================================== --- source/tools/atlas/GameInterface/ActorViewer.h +++ source/tools/atlas/GameInterface/ActorViewer.h @@ -52,6 +52,7 @@ private: ActorViewerImpl& m; + std::vector GetAttackTypes() const; float GetRepeatTimeByAttackType(const std::string& type) const; }; Index: source/tools/atlas/GameInterface/ActorViewer.cpp =================================================================== --- source/tools/atlas/GameInterface/ActorViewer.cpp +++ source/tools/atlas/GameInterface/ActorViewer.cpp @@ -58,6 +58,8 @@ #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/Render.h" +#include + struct ActorViewerImpl : public Scene { NONCOPYABLE(ActorViewerImpl); @@ -403,14 +405,10 @@ m.CurrentSpeed = speed; } - else if (anim == "attack_melee") - repeattime = GetRepeatTimeByAttackType("Melee"); - else if (anim == "attack_ranged") - repeattime = GetRepeatTimeByAttackType("Ranged"); - else if (anim == "attack_slaughter") - repeattime = GetRepeatTimeByAttackType("Slaughter"); - else if (anim == "attack_capture") - repeattime = GetRepeatTimeByAttackType("Capture"); + else + for (std::string& type : GetAttackTypes()) + if (anim == "attack_" + boost::algorithm::to_lower_copy(type)) + repeattime = GetRepeatTimeByAttackType(type); CmpPtr cmpVisual(m.Simulation2, m.Entity); if (cmpVisual) @@ -482,6 +480,15 @@ g_ProfileViewer.ShowTable(""); } +std::vector ActorViewer::GetAttackTypes() const +{ + CmpPtr cmpAttack(m.Simulation2, m.Entity); + if (cmpAttack) + return cmpAttack->GetAttackTypes(); + + return {}; +} + float ActorViewer::GetRepeatTimeByAttackType(const std::string& type) const { CmpPtr cmpAttack(m.Simulation2, m.Entity);