Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 23758) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 23759) @@ -1,948 +1,976 @@ var API3 = function(m) { // defines a template. m.Template = m.Class({ "_init": function(sharedAI, templateName, template) { this._templateName = templateName; this._template = template; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; this._tpCache = new Map(); }, // helper function to return a template value, optionally adjusting for tech. // TODO: there's no support for "_string" values here. "get": function(string) { let value = this._template; if (this._entityModif && this._entityModif.has(string)) return this._entityModif.get(string); else if (this._templateModif) { let owner = this._entity ? this._entity.owner : PlayerID; if (this._templateModif[owner] && this._templateModif[owner].has(string)) return this._templateModif[owner].get(string); } if (!this._tpCache.has(string)) { let args = string.split("/"); for (let arg of args) { if (value[arg]) value = value[arg]; else { value = undefined; break; } } this._tpCache.set(string, value); } return this._tpCache.get(string); }, "templateName": function() { return this._templateName; }, "genericName": function() { return this.get("Identity/GenericName"); }, "civ": function() { return this.get("Identity/Civ"); }, "classes": function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, "hasClass": function(name) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; return classes && classes.indexOf(name) != -1; }, "hasClasses": function(array) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; if (!classes) return false; for (let cls of array) if (classes.indexOf(cls) == -1) return false; return true; }, "requiredTech": function() { return this.get("Identity/RequiredTechnology"); }, "available": function(gameState) { let techRequired = this.requiredTech(); if (!techRequired) return true; return gameState.isResearched(techRequired); }, // specifically "phase": function() { let techRequired = this.requiredTech(); if (!techRequired) return 0; if (techRequired == "phase_village") return 1; if (techRequired == "phase_town") return 2; if (techRequired == "phase_city") return 3; if (techRequired.startsWith("phase_")) return 4; return 0; }, "cost": function(productionQueue) { if (!this.get("Cost")) return undefined; let ret = {}; for (let type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, "costSum": function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return undefined; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1); }, /** * Returns { "max": max, "min": min } or undefined if no obstruction. * max: radius of the outer circle surrounding this entity's obstruction shape * min: radius of the inner circle */ "obstructionRadius": function() { if (!this.get("Obstruction")) return undefined; if (this.get("Obstruction/Static")) { let w = +this.get("Obstruction/Static/@width"); let h = +this.get("Obstruction/Static/@depth"); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } if (this.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); return { "max": r, "min": r }; } let right = this.get("Obstruction/Obstructions/Right"); let left = this.get("Obstruction/Obstructions/Left"); if (left && right) { let w = +right["@x"] + right["@width"]/2 - left["@x"] + left["@width"]/2; let h = Math.max(+right["@z"] + right["@depth"]/2, +left["@z"] + left["@depth"]/2) - Math.min(+right["@z"] - right["@depth"]/2, +left["@z"] - left["@depth"]/2); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } return { "max": 0, "min": 0 }; // Units have currently no obstructions }, /** * Returns the radius of a circle surrounding this entity's footprint. */ "footprintRadius": function() { if (!this.get("Footprint")) return undefined; if (this.get("Footprint/Square")) { let w = +this.get("Footprint/Square/@width"); let h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w*w + h*h) / 2; } if (this.get("Footprint/Circle")) return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, "maxHitpoints": function() { return +(this.get("Health/Max") || 0); }, "isHealable": function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, "isRepairable": function() { return this.get("Repairable") !== undefined; }, "getPopulationBonus": function() { return +this.get("Cost/PopulationBonus"); }, "armourStrengths": function() { let armourDamageTypes = this.get("Armour"); if (!armourDamageTypes) return undefined; let armour = {}; for (let damageType in armourDamageTypes) if (damageType != "Foundation") armour[damageType] = +armourDamageTypes[damageType]; return armour; }, "attackTypes": function() { if (!this.get("Attack")) return undefined; let ret = []; for (let type in this.get("Attack")) ret.push(type); return ret; }, "attackRange": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { "max": +this.get("Attack/" + type +"/MaxRange"), "min": +(this.get("Attack/" + type +"/MinRange") || 0) }; }, "attackStrengths": function(type) { let attackDamageTypes = this.get("Attack/" + type + "/Damage"); if (!attackDamageTypes) return undefined; let damage = {}; for (let damageType in attackDamageTypes) damage[damageType] = +attackDamageTypes[damageType]; return damage; }, "captureStrength": function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Capture") || 0; }, "attackTimes": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { "prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0), "repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses- , multiplier], … ]. "getCounteredClasses": function() { if (!this.get("Attack")) return undefined; let Classes = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]); } } return Classes; }, // returns true if the entity counters those classes. // TODO: refine using the multiplier "countersClasses": function(classes) { if (!this.get("Attack")) return false; let mcounter = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) mcounter.concat(bonusClasses.split(" ")); } } for (let i in classes) if (mcounter.indexOf(classes[i]) != -1) return true; return false; }, // returns, if it exists, the multiplier from each attack against a given class "getMultiplierAgainst": function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; if (this.get("Attack/" + type + "/Bonuses")) { for (let b in this.get("Attack/" + type + "/Bonuses")) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (!bonusClasses) continue; for (let bcl of bonusClasses.split(" ")) if (bcl == againstClass) return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier"); } } return 1; }, "buildableEntities": function(civ) { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "trainableEntities": function(civ) { let templates = this.get("ProductionQueue/Entities/_string"); if (!templates) return undefined; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "researchableTechs": function(gameState, civ) { let templates = this.get("ProductionQueue/Technologies/_string"); if (!templates) return undefined; let techs = templates.split(/\s+/); for (let i = 0; i < techs.length; ++i) { let tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; let civTech = tech.replace("{civ}", civ); techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } return techs; }, "resourceSupplyType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); return { "generic": type, "specific": subtype }; }, // will return either "food", "wood", "stone", "metal" and not treasure. "getResourceType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }, "resourceSupplyMax": function() { return +this.get("ResourceSupply/Amount"); }, "maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }, "resourceGatherRates": function() { if (!this.get("ResourceGatherer")) return undefined; let ret = {}; let baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (let r in this.get("ResourceGatherer/Rates")) ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed; return ret; }, "resourceDropsiteTypes": function() { if (!this.get("ResourceDropsite")) return undefined; let types = this.get("ResourceDropsite/Types"); return types ? types.split(/\s+/) : []; }, "garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); }, "garrisonMax": function() { return this.get("GarrisonHolder/Max"); }, "garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); }, "getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); }, "getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, "getGarrisonArrowClasses": function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, "buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); }, "promotion": function() { return this.get("Promotion/Entity"); }, "isPackable": function() { return this.get("Pack") != undefined; }, /** * Returns whether this is an animal that is too difficult to hunt. */ "isHuntable": function() { if(!this.get("ResourceSupply/KillBeforeGather")) return false; // do not hunt retaliating animals (animals without UnitAI are dead animals) let behaviour = this.get("UnitAI/NaturalBehaviour"); return !behaviour || behaviour != "violent" && behaviour != "aggressive" && behaviour != "defensive"; }, "walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); }, "trainingCategory": function() { return this.get("TrainingRestrictions/Category"); }, "buildTime": function(productionQueue) { let time = +this.get("Cost/BuildTime"); if (productionQueue) time *= productionQueue.techCostMultiplier("time"); return time; }, "buildCategory": function() { return this.get("BuildRestrictions/Category"); }, "buildDistance": function() { let distance = this.get("BuildRestrictions/Distance"); if (!distance) return undefined; let ret = {}; for (let key in distance) ret[key] = this.get("BuildRestrictions/Distance/" + key); return ret; }, "buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); }, "buildTerritories": function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory")) return undefined; return this.get("BuildRestrictions/Territory").split(/\s+/); }, "hasBuildTerritory": function(territory) { let territories = this.buildTerritories(); return territories && territories.indexOf(territory) != -1; }, "hasTerritoryInfluence": function() { return this.get("TerritoryInfluence") !== undefined; }, "hasDefensiveFire": function() { if (!this.get("Attack/Ranged")) return false; return this.getDefaultArrow() || this.getArrowMultiplier(); }, "territoryInfluenceRadius": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); return -1; }, "territoryInfluenceWeight": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); return -1; }, "territoryDecayRate": function() { return +(this.get("TerritoryDecay/DecayRate") || 0); }, "defaultRegenRate": function() { return +(this.get("Capturable/RegenRate") || 0); }, "garrisonRegenRate": function() { return +(this.get("Capturable/GarrisonRegenRate") || 0); }, "visionRange": function() { return +this.get("Vision/Range"); }, "gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); }, "isBuilder": function() { return this.get("Builder") !== undefined; }, "isGatherer": function() { return this.get("ResourceGatherer") !== undefined; }, "canGather": function(type) { let gatherRates = this.get("ResourceGatherer/Rates"); if (!gatherRates) return false; for (let r in gatherRates) if (r.split('.')[0] === type) return true; return false; }, "isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; }, /** * returns true if the tempalte 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")) return false; if (!target) return true; if (!target.get("Capturable")) return false; let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string"); return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return "Garrisonable" in this._template; }, }); // defines an entity, with a super Template. // also redefines several of the template functions where the only change is applying aura and tech modifications. m.Entity = m.Class({ "_super": m.Template, "_init": function(sharedAI, entity) { this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template)); this._entity = entity; this._ai = sharedAI; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; // save a reference to the entity tech/aura modifications if (!sharedAI._entitiesModifications.has(entity.id)) sharedAI._entitiesModifications.set(entity.id, new Map()); this._entityModif = sharedAI._entitiesModifications.get(entity.id); }, "toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, "id": function() { return this._entity.id; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data should not be shared with any other AI scripts.) */ "getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); }, /** * Sets extra data to be associated with this entity. */ "setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); }, "deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; }, "deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); }, "position": function() { return this._entity.position; }, "angle": function() { return this._entity.angle; }, "isIdle": function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, "getStance": function() { return this._entity.stance !== undefined ? this._entity.stance : undefined; }, "unitAIState": function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; }, "unitAIOrderData": function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; }, "hitpoints": function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; }, "isHurt": function() { return this.hitpoints() < this.maxHitpoints(); }, "healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); }, "needsHeal": function() { return this.isHurt() && this.isHealable(); }, "needsRepair": function() { return this.isHurt() && this.isRepairable(); }, "decaying": function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; }, "capturePoints": function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; }, "isInvulnerable": function() { return this._entity.invulnerability || false; }, "isSharedDropsite": function() { return this._entity.sharedDropsite === true; }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ "trainingQueue": function() { let queue = this._entity.trainingQueue; return queue; }, "trainingQueueTime": function() { let queue = this._entity.trainingQueue; if (!queue) return undefined; let time = 0; for (let item of queue) time += item.timeRemaining; return time/1000; }, "foundationProgress": function() { if (this._entity.foundationProgress === undefined) return undefined; return this._entity.foundationProgress; }, "getBuilders": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return []; return this._entity.foundationBuilders; }, "getBuildersNb": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return 0; return this._entity.foundationBuilders.length; }, "owner": function() { return this._entity.owner; }, "isOwn": function(player) { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === player; }, "resourceSupplyAmount": function() { if (this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, "resourceSupplyNumGatherers": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this._entity.resourceSupplyNumGatherers; return undefined; }, "isFull": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, "resourceCarrying": function() { if (this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, "currentGatherRate": function() { // returns the gather rate for the current target if applicable. if (!this.get("ResourceGatherer")) return undefined; if (this.unitAIOrderData().length && (this.unitAIState().split(".")[1] == "GATHER" || this.unitAIState().split(".")[1] == "RETURNRESOURCE")) { let res; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[0].target); else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[1].target); if (!res) return 0; let type = res.resourceSupplyType(); if (!type) return 0; if (type.generic == "treasure") return 1000; let tstring = type.generic + "." + type.specific; let rate = +this.get("ResourceGatherer/BaseSpeed"); rate *= +this.get("ResourceGatherer/Rates/" +tstring); if (rate) return rate; return 0; } return undefined; }, "garrisoned": function() { return this._entity.garrisoned; }, "canGarrisonInside": function() { return this._entity.garrisoned.length < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { if (!this.get("Attack")) return false; for (let type in this.get("Attack")) { if (type == "Slaughter") continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses)) return true; } return false; }, + /** + * Derived from Attack.js' similary named function. + * @return {boolean} - Whether an entity can attack a given target. + */ + "canAttackTarget": function(target, allowCapture) + { + let attackTypes = this.get("Attack"); + if (!attackTypes) + return false; + + let canCapture = allowCapture && this.canCapture(target); + let armourStrengths = target.get("Armour"); + if (!armourStrengths) + return canCapture; + + 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; + }; + + return false; + }, + "move": function(x, z, queued = false) { Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, "moveToRange": function(x, z, min, max, queued = false) { Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); 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 }); return this; }, // violent, aggressive, defensive, passive, standground "setStance": function(stance, queued = false) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance, "queued": queued }); return this; }, "stopMoving": function() { Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false }); }, "unload": function(id) { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] }); return this; }, // Unloads all owned units, don't unload allies "unloadAll": function() { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] }); return this; }, "garrison": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "attack": function(unitId, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued }); return this; }, // moveApart from a point in the opposite direction with a distance dist "moveApart": function(point, dist) { if (this.position() !== undefined) { let direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; let norm = m.VectorDistance(point, this.position()); if (norm === 0) direction = [1, 0]; else { direction[0] /= norm; direction[1] /= norm; } Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false }); } return this; }, // Flees from a unit in the opposite direction. "flee": function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0], this.position()[1] - unitToFleeFrom.position()[1]]; let dist = m.VectorDistance(unitToFleeFrom.position(), this.position()); FleeDirection[0] = 40 * FleeDirection[0]/dist; FleeDirection[1] = 40 * FleeDirection[1]/dist; Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false }); } return this; }, "gather": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "repair": function(target, autocontinue = false, queued = false) { Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued }); return this; }, "returnResources": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "destroy": function() { Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] }); return this; }, "barter": function(buyType, sellType, amount) { Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount }); return this; }, "tradeRoute": function(target, source) { Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false }); return this; }, "setRallyPoint": function(target, command) { let data = { "command": command, "target": target.id() }; Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data }); return this; }, "unsetRallyPoint": function() { Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] }); return this; }, "train": function(civ, type, count, metadata, promotedTypes) { let trainable = this.trainableEntities(civ); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) == -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } Engine.PostCommand(PlayerID, { "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata, "promoted": promotedTypes }); return this; }, "construct": function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting Engine.PostCommand(PlayerID, { "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false, "metadata": metadata // can be undefined }); return this; }, "research": function(template) { Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template }); return this; }, "stopProduction": function(id) { Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id }); return this; }, "stopAllProduction": function(percentToStopAt) { let queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (let item of queue) if (item.progress < percentToStopAt) Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id }); return this; }, "guard": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "removeGuard": function() { Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] }); return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js (revision 23758) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js (revision 23759) @@ -1,158 +1,163 @@ var API3 = function(m) { m.Filters = { "byType": type => ({ "func": ent => ent.templateName() == type, "dynamicProperties": [] }), "byClass": cls => ({ "func": ent => ent.hasClass(cls), "dynamicProperties": [] }), "byClassesAnd": clsList => ({ "func": ent => clsList.every(cls => ent.hasClass(cls)), "dynamicProperties": [] }), "byClassesOr": clsList => ({ "func": ent => clsList.some(cls => ent.hasClass(cls)), "dynamicProperties": [] }), "byMetadata": (player, key, value) => ({ "func": ent => ent.getMetadata(player, key) == value, "dynamicProperties": ['metadata.' + key] }), "byHasMetadata": (player, key) => ({ "func": ent => ent.getMetadata(player, key) !== undefined, "dynamicProperties": ['metadata.' + key] }), "and": (filter1, filter2) => ({ "func": ent => filter1.func(ent) && filter2.func(ent), "dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties) }), "or": (filter1, filter2) => ({ "func": ent => filter1.func(ent) || filter2.func(ent), "dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties) }), "not": (filter) => ({ "func": ent => !filter.func(ent), "dynamicProperties": filter.dynamicProperties }), "byOwner": owner => ({ "func": ent => ent.owner() == owner, "dynamicProperties": ['owner'] }), "byNotOwner": owner => ({ "func": ent => ent.owner() != owner, "dynamicProperties": ['owner'] }), "byOwners": owners => ({ "func": ent => owners.some(owner => owner == ent.owner()), "dynamicProperties": ['owner'] }), "byCanGarrison": () => ({ "func": ent => ent.garrisonMax() > 0, "dynamicProperties": [] }), "byTrainingQueue": () => ({ "func": ent => ent.trainingQueue(), "dynamicProperties": ['trainingQueue'] }), "byResearchAvailable": (gameState, civ) => ({ "func": ent => ent.researchableTechs(gameState, civ) !== undefined, "dynamicProperties": [] }), "byCanAttackClass": aClass => ({ "func": ent => ent.canAttackClass(aClass), "dynamicProperties": [] }), + "byCanAttackTarget": target => ({ + "func": ent => ent.canAttackTarget(target), + "dynamicProperties": [] + }), + "isGarrisoned": () => ({ "func": ent => ent.position() === undefined, "dynamicProperties": [] }), "isIdle": () => ({ "func": ent => ent.isIdle(), "dynamicProperties": ['idle'] }), "isFoundation": () => ({ "func": ent => ent.foundationProgress() !== undefined, "dynamicProperties": [] }), "isBuilt": () => ({ "func": ent => ent.foundationProgress() === undefined, "dynamicProperties": [] }), "hasDefensiveFire": () => ({ "func": ent => ent.hasDefensiveFire(), "dynamicProperties": [] }), "isDropsite": resourceType => ({ "func": ent => ent.resourceDropsiteTypes() && (resourceType === undefined || ent.resourceDropsiteTypes().indexOf(resourceType) != -1), "dynamicProperties": [] }), "byResource": resourceType => ({ "func": ent => { if (!ent.resourceSupplyMax()) return false; let type = ent.resourceSupplyType(); if (!type) return false; // Skip targets that are too hard to hunt if (!ent.isHuntable() || ent.hasClass("SeaCreature")) return false; // Don't go for floating treasures since we won't be able to reach them and it kills the pathfinder. if (ent.templateName() == "gaia/treasure/shipwreck_debris" || ent.templateName() == "gaia/treasure/shipwreck") return false; if (type.generic == "treasure") return resourceType == type.specific; return resourceType == type.generic; }, "dynamicProperties": [] }), "isHuntable": () => ({ // Skip targets that are too hard to hunt and don't go for the fish! TODO: better accessibility checks "func": ent => ent.hasClass("Animal") && ent.resourceSupplyMax() && ent.isHuntable() && !ent.hasClass("SeaCreature"), "dynamicProperties": [] }), "isFishable": () => ({ // temporarily do not fish moving fish (i.e. whales) "func": ent => !ent.get("UnitMotion") && ent.hasClass("SeaCreature") && ent.resourceSupplyMax(), "dynamicProperties": [] }) }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 23758) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 23759) @@ -1,800 +1,803 @@ /** * Attack Manager */ PETRA.AttackManager = function(Config) { this.Config = Config; this.totalNumber = 0; this.attackNumber = 0; this.rushNumber = 0; this.raidNumber = 0; this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start this.debugTime = 0; this.maxRushes = 0; this.rushSize = []; this.currentEnemyPlayer = undefined; // enemy player we are currently targeting this.defeated = {}; }; /** More initialisation for stuff that needs the gameState */ PETRA.AttackManager.prototype.init = function(gameState) { this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); this.outOfPlan.registerUpdates(); }; PETRA.AttackManager.prototype.setRushes = function(allowed) { if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) { this.maxRushes = 3; this.rushSize = [ 16, 20, 24 ]; } else if (this.Config.personality.aggressive > this.Config.personalityCut.medium && allowed > 1) { this.maxRushes = 2; this.rushSize = [ 18, 22 ]; } else if (this.Config.personality.aggressive > this.Config.personalityCut.weak && allowed > 0) { this.maxRushes = 1; this.rushSize = [ 20 ]; } }; PETRA.AttackManager.prototype.checkEvents = function(gameState, events) { for (let evt of events.PlayerDefeated) this.defeated[evt.playerId] = true; let answer = "decline"; let other; let targetPlayer; for (let evt of events.AttackRequest) { if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) continue; targetPlayer = evt.player; let available = 0; for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing") { if (attack.targetPlayer === targetPlayer) available += attack.unitCollection.length; else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) other = attack.targetPlayer; continue; } attack.targetPlayer = targetPlayer; if (attack.unitCollection.length > 2) available += attack.unitCollection.length; } } if (available > 12) // launch the attack immediately { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing" || attack.targetPlayer !== targetPlayer || attack.unitCollection.length < 3) continue; attack.forceStart(); attack.requested = true; } } answer = "join"; } else if (other !== undefined) answer = "other"; break; // take only the first attack request into account } if (targetPlayer !== undefined) PETRA.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); for (let evt of events.EntityRenamed) // take care of packing units in bombing attacks { for (let [targetId, unitIds] of this.bombingAttacks) { if (targetId == evt.entity) { this.bombingAttacks.set(evt.newentity, unitIds); this.bombingAttacks.delete(evt.entity); } else if (unitIds.has(evt.entity)) { unitIds.add(evt.newentity); unitIds.delete(evt.entity); } } } }; /** * Check for any structure in range from within our territory, and bomb it */ PETRA.AttackManager.prototype.assignBombers = function(gameState) { // First some cleaning of current bombing attacks for (let [targetId, unitIds] of this.bombingAttacks) { let target = gameState.getEntityById(targetId); if (!target || !gameState.isPlayerEnemy(target.owner())) this.bombingAttacks.delete(targetId); else { for (let entId of unitIds.values()) { let ent = gameState.getEntityById(entId); if (ent && ent.owner() == PlayerID) { let plan = ent.getMetadata(PlayerID, "plan"); let orders = ent.unitAIOrderData(); let lastOrder = orders && orders.length ? orders[orders.length-1] : null; if (lastOrder && lastOrder.target && lastOrder.target == targetId && plan != -2 && plan != -3) continue; } unitIds.delete(entId); } if (!unitIds.size) this.bombingAttacks.delete(targetId); } } let bombers = gameState.updatingCollection("bombers", API3.Filters.byClassesOr(["BoltShooter", "StoneThrower"]), gameState.getOwnUnits()); for (let ent of bombers.values()) { if (!ent.position() || !ent.isIdle() || !ent.attackRange("Ranged")) continue; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) continue; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) { let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) continue; } let alreadyBombing = false; for (let unitIds of this.bombingAttacks.values()) { if (!unitIds.has(ent.id())) continue; alreadyBombing = true; break; } if (alreadyBombing) break; let range = ent.attackRange("Ranged").max; let entPos = ent.position(); let access = PETRA.getLandAccess(gameState, ent); for (let struct of gameState.getEnemyStructures().values()) { + if (!ent.canAttackTarget(struct, PETRA.allowCapture(gameState, ent, struct))) + continue; + let structPos = struct.position(); let x; let z; if (struct.hasClass("Field")) { if (!struct.resourceSupplyNumGatherers() || !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos))) continue; } let dist = API3.VectorDistance(entPos, structPos); if (dist > range) { let safety = struct.footprintRadius() + 30; x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist; z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist; let owner = gameState.ai.HQ.territoryMap.getOwner([x, z]); if (owner != 0 && gameState.isPlayerEnemy(owner)) continue; x = structPos[0] + (entPos[0] - structPos[0]) * range / dist; z = structPos[1] + (entPos[1] - structPos[1]) * range / dist; if (gameState.ai.HQ.territoryMap.getOwner([x, z]) != PlayerID || gameState.ai.accessibility.getAccessValue([x, z]) != access) continue; } let attackingUnits; for (let [targetId, unitIds] of this.bombingAttacks) { if (targetId != struct.id()) continue; attackingUnits = unitIds; break; } if (attackingUnits && attackingUnits.size > 4) continue; // already enough units against that target if (!attackingUnits) { attackingUnits = new Set(); this.bombingAttacks.set(struct.id(), attackingUnits); } attackingUnits.add(ent.id()); if (dist > range) ent.move(x, z); ent.attack(struct.id(), false, dist > range); break; } } }; /** * Some functions are run every turn * Others once in a while */ PETRA.AttackManager.prototype.update = function(gameState, queues, events) { if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) { this.debugTime = gameState.ai.elapsedTime; API3.warn(" upcoming attacks ================="); for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" started attacks =================="); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" =================================="); } this.checkEvents(gameState, events); let unexecutedAttacks = { "Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0 }; for (let attackType in this.upcomingAttacks) { for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) { let attack = this.upcomingAttacks[attackType][i]; attack.checkEvents(gameState, events); if (attack.isStarted()) API3.warn("Petra problem in attackManager: attack in preparation has already started ???"); let updateStep = attack.updatePreparation(gameState); // now we're gonna check if the preparation time is over if (updateStep == 1 || attack.isPaused()) { // just chillin' if (attack.state == "unexecuted") ++unexecutedAttacks[attackType]; } else if (updateStep == 0) { if (this.Config.debug > 1) API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--, 1); } else if (updateStep == 2) { if (attack.StartAttack(gameState)) { if (this.Config.debug > 1) API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); if (this.Config.chat) PETRA.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType()); this.startedAttacks[attackType].push(attack); } else attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--, 1); } } } for (let attackType in this.startedAttacks) { for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { let attack = this.startedAttacks[attackType][i]; attack.checkEvents(gameState, events); // okay so then we'll update the attack. if (attack.isPaused()) continue; let remaining = attack.update(gameState, events); if (!remaining) { if (this.Config.debug > 1) API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--, 1); } } } // creating plans after updating because an aborted plan might be reused in that case. let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length; if (this.rushNumber < this.maxRushes && barracksNb >= 1) { if (unexecutedAttacks.Rush === 0) { // we have a barracks and we want to rush, rush. let data = { "targetSize": this.rushSize[this.rushNumber] }; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Military Manager: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Rush.push(attackPlan); } this.rushNumber++; } } else if (unexecutedAttacks.Attack == 0 && unexecutedAttacks.HugeAttack == 0 && this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100)) && (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12)) { if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack { let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, type); if (attackPlan.failed) this.attackPlansEncounteredWater = true; // hack else { if (this.Config.debug > 1) API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks[type].push(attackPlan); } this.attackNumber++; } } if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length) { let target; for (let targetId of gameState.ai.HQ.defenseManager.targetList) { target = gameState.getEntityById(targetId); if (!target) continue; if (gameState.isPlayerEnemy(target.owner())) break; target = undefined; } if (target) // prepare a raid against this target this.raidTargetEntity(gameState, target); } // Check if we have some unused ranged siege unit which could do something useful while waiting if (this.Config.difficulty > 1 && gameState.ai.playedTurn % 5 == 0) this.assignBombers(gameState); }; PETRA.AttackManager.prototype.getPlan = function(planName) { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) if (attack.getName() == planName) return attack; } for (let attackType in this.startedAttacks) { for (let attack of this.startedAttacks[attackType]) if (attack.getName() == planName) return attack; } return undefined; }; PETRA.AttackManager.prototype.pausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(true); }; PETRA.AttackManager.prototype.unpausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(false); }; PETRA.AttackManager.prototype.pauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(true); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(true); }; PETRA.AttackManager.prototype.unpauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(false); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(false); }; PETRA.AttackManager.prototype.getAttackInPreparation = function(type) { return this.upcomingAttacks[type].length ? this.upcomingAttacks[type][0] : undefined; }; /** * Determine which player should be attacked: when called when starting the attack, * attack.targetPlayer is undefined and in that case, we keep track of the chosen target * for future attacks. */ PETRA.AttackManager.prototype.getEnemyPlayer = function(gameState, attack) { let enemyPlayer; // First check if there is a preferred enemy based on our victory conditions. // If both wonder and relic, choose randomly between them TODO should combine decisions if (gameState.getVictoryConditions().has("wonder")) enemyPlayer = this.getWonderEnemyPlayer(gameState, attack); if (gameState.getVictoryConditions().has("capture_the_relic")) if (!enemyPlayer || randBool()) enemyPlayer = this.getRelicEnemyPlayer(gameState, attack) || enemyPlayer; if (enemyPlayer) return enemyPlayer; let veto = {}; for (let i in this.defeated) veto[i] = true; // No rush if enemy too well defended (i.e. iberians) if (attack.type == "Rush") { for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || veto[i]) continue; if (this.defeated[i]) continue; let enemyDefense = 0; for (let ent of gameState.getEnemyStructures(i).values()) if (ent.hasClass("Tower") || ent.hasClass("Fortress")) enemyDefense++; if (enemyDefense > 6) veto[i] = true; } } // then if not a huge attack, continue attacking our previous target as long as it has some entities, // otherwise target the most accessible one if (attack.type != "HugeAttack") { if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && !this.defeated[this.currentEnemyPlayer] && gameState.isPlayerEnemy(this.currentEnemyPlayer) && gameState.getEntities(this.currentEnemyPlayer).hasEntities()) return this.currentEnemyPlayer; let distmin; let ccmin; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let ourcc of ccEnts.values()) { if (ourcc.owner() != PlayerID) continue; let ourPos = ourcc.position(); let access = PETRA.getLandAccess(gameState, ourcc); for (let enemycc of ccEnts.values()) { if (veto[enemycc.owner()]) continue; if (!gameState.isPlayerEnemy(enemycc.owner())) continue; if (access != PETRA.getLandAccess(gameState, enemycc)) continue; let dist = API3.SquareVectorDistance(ourPos, enemycc.position()); if (distmin && dist > distmin) continue; ccmin = enemycc; distmin = dist; } } if (ccmin) { enemyPlayer = ccmin.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; } } // then let's target our strongest enemy (basically counting enemies units) // with priority to enemies with civ center let max = 0; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (veto[i]) continue; if (!gameState.isPlayerEnemy(i)) continue; let enemyCount = 0; let enemyCivCentre = false; for (let ent of gameState.getEntities(i).values()) { enemyCount++; if (ent.hasClass("CivCentre")) enemyCivCentre = true; } if (enemyCivCentre) enemyCount += 500; if (!enemyCount || enemyCount < max) continue; max = enemyCount; enemyPlayer = i; } if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; }; /** * Target the player with the most advanced wonder. * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. */ PETRA.AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack) { let enemyPlayer; let enemyWonder; let moreAdvanced; for (let wonder of gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")).values()) { if (wonder.owner() == 0) continue; let progress = wonder.foundationProgress(); if (progress === undefined) { enemyWonder = wonder; break; } if (enemyWonder && moreAdvanced > progress) continue; enemyWonder = wonder; moreAdvanced = progress; } if (enemyWonder) { enemyPlayer = enemyWonder.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; } return enemyPlayer; }; /** * Target the player with the most relics (including gaia). */ PETRA.AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack) { let enemyPlayer; let allRelics = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")); let maxRelicsOwned = 0; for (let i = 0; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || this.defeated[i] || i == 0 && !gameState.ai.HQ.victoryManager.tryCaptureGaiaRelic) continue; let relicsCount = allRelics.filter(relic => relic.owner() == i).length; if (relicsCount <= maxRelicsOwned) continue; maxRelicsOwned = relicsCount; enemyPlayer = i; } if (enemyPlayer !== undefined) { if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; if (enemyPlayer == 0) gameState.ai.HQ.victoryManager.resetCaptureGaiaRelic(gameState); } return enemyPlayer; }; /** f.e. if we have changed diplomacy with another player. */ PETRA.AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player) { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) if (attack.targetPlayer === player) attack.targetPlayer = undefined; for (let attackType in this.startedAttacks) for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { let attack = this.startedAttacks[attackType][i]; if (attack.targetPlayer === player) { attack.Abort(gameState); this.startedAttacks[attackType].splice(i--, 1); } } }; PETRA.AttackManager.prototype.raidTargetEntity = function(gameState, ent) { let data = { "target": ent }; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); if (attackPlan.failed) return null; if (this.Config.debug > 1) API3.warn("Military Manager: Raiding plan " + this.totalNumber); this.raidNumber++; this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Raid.push(attackPlan); return attackPlan; }; /** * Return the number of units from any of our attacking armies around this position */ PETRA.AttackManager.prototype.numAttackingUnitsAround = function(pos, dist) { let num = 0; for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) { if (!attack.position) // this attack may be inside a transport continue; if (API3.SquareVectorDistance(pos, attack.position) < dist*dist) num += attack.unitCollection.length; } return num; }; /** * Switch defense armies into an attack one against the given target * data.range: transform all defense armies inside range of the target into a new attack * data.armyID: transform only the defense army ID into a new attack * data.uniqueTarget: the attack will stop when the target is destroyed or captured */ PETRA.AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data) { if (!target || !target.position()) return false; if (!data.range && !data.armyID) { API3.warn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data)); return false; } let attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined; let pos = target.position(); let attackType = "Attack"; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData); if (attackPlan.failed) return false; this.totalNumber++; attackPlan.init(gameState); this.startedAttacks[attackType].push(attackPlan); let targetAccess = PETRA.getLandAccess(gameState, target); for (let army of gameState.ai.HQ.defenseManager.armies) { if (data.range) { army.recalculatePosition(gameState); if (API3.SquareVectorDistance(pos, army.foePosition) > data.range * data.range) continue; } else if (army.ID != +data.armyID) continue; while (army.foeEntities.length > 0) army.removeFoe(gameState, army.foeEntities[0]); while (army.ownEntities.length > 0) { let unitId = army.ownEntities[0]; army.removeOwn(gameState, unitId); let unit = gameState.getEntityById(unitId); let accessOk = unit.getMetadata(PlayerID, "transport") !== undefined || unit.position() && PETRA.getLandAccess(gameState, unit) == targetAccess; if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit)) { unit.setMetadata(PlayerID, "plan", attackPlan.name); unit.setMetadata(PlayerID, "role", "attack"); attackPlan.unitCollection.updateEnt(unit); } } } if (!attackPlan.unitCollection.hasEntities()) { attackPlan.Abort(gameState); return false; } for (let unit of attackPlan.unitCollection.values()) unit.setMetadata(PlayerID, "role", "attack"); attackPlan.targetPlayer = target.owner(); attackPlan.targetPos = pos; attackPlan.target = target; attackPlan.state = "arrived"; return true; }; PETRA.AttackManager.prototype.Serialize = function() { let properties = { "totalNumber": this.totalNumber, "attackNumber": this.attackNumber, "rushNumber": this.rushNumber, "raidNumber": this.raidNumber, "debugTime": this.debugTime, "maxRushes": this.maxRushes, "rushSize": this.rushSize, "currentEnemyPlayer": this.currentEnemyPlayer, "defeated": this.defeated }; let upcomingAttacks = {}; for (let key in this.upcomingAttacks) { upcomingAttacks[key] = []; for (let attack of this.upcomingAttacks[key]) upcomingAttacks[key].push(attack.Serialize()); } let startedAttacks = {}; for (let key in this.startedAttacks) { startedAttacks[key] = []; for (let attack of this.startedAttacks[key]) startedAttacks[key].push(attack.Serialize()); } return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; }; PETRA.AttackManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.upcomingAttacks = {}; for (let key in data.upcomingAttacks) { this.upcomingAttacks[key] = []; for (let dataAttack of data.upcomingAttacks[key]) { let attack = new PETRA.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.upcomingAttacks[key].push(attack); } } this.startedAttacks = {}; for (let key in data.startedAttacks) { this.startedAttacks[key] = []; for (let dataAttack of data.startedAttacks[key]) { let attack = new PETRA.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.startedAttacks[key].push(attack); } } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 23758) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 23759) @@ -1,2159 +1,2178 @@ /** * This is an attack plan: * It deals with everything in an attack, from picking a target to picking a path to it * To making sure units are built, and pushing elements to the queue manager otherwise * It also handles the actual attack, though much work is needed on that. */ PETRA.AttackPlan = function(gameState, Config, uniqueID, type, data) { this.Config = Config; this.name = uniqueID; this.type = type || "Attack"; this.state = "unexecuted"; this.forced = false; // true when this attacked has been forced to help an ally if (data && data.target) { this.target = data.target; this.targetPos = this.target.position(); this.targetPlayer = this.target.owner(); } else { this.target = undefined; this.targetPos = undefined; this.targetPlayer = undefined; } this.uniqueTargetId = data && data.uniqueTargetId || undefined; // get a starting rallyPoint ... will be improved later let rallyPoint; let rallyAccess; let allAccesses = {}; for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; let access = PETRA.getLandAccess(gameState, base.anchor); if (!rallyPoint) { rallyPoint = base.anchor.position(); rallyAccess = access; } if (!allAccesses[access]) allAccesses[access] = base.anchor.position(); } if (!rallyPoint) // no base ? take the position of any of our entities { for (let ent of gameState.getOwnEntities().values()) { if (!ent.position()) continue; let access = PETRA.getLandAccess(gameState, ent); rallyPoint = ent.position(); rallyAccess = access; allAccesses[access] = rallyPoint; break; } if (!rallyPoint) { this.failed = true; return false; } } this.rallyPoint = rallyPoint; this.overseas = 0; if (gameState.ai.HQ.navalMap) { for (let structure of gameState.getEnemyStructures().values()) { if (this.target && structure.id() != this.target.id()) continue; if (!structure.position()) continue; let access = PETRA.getLandAccess(gameState, structure); if (access in allAccesses) { this.overseas = 0; this.rallyPoint = allAccesses[access]; break; } else if (!this.overseas) { let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); if (!sea) { if (this.target) { API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target " + this.target.templateName() + " indices " + rallyAccess + " " + access); this.failed = true; return false; } continue; } this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1); } } } this.paused = false; this.maxCompletingTime = 0; // priority of the queues we'll create. let priority = 70; // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". // if not, this is a "bonus". The higher the priority, the faster this unit will get built. // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built // only once every other category is at least 50% of its target size. // note: siege build order is currently added by the military manager if a fortress is there. this.unitStat = {}; // neededShips is the minimal number of ships which should be available for transport if (type == "Rush") { priority = 250; this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"], "interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [["strength", 1]] }; if (data && data.targetSize) this.unitStat.Infantry.targetSize = data.targetSize; this.neededShips = 1; } else if (type == "Raid") { priority = 150; this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength", 1] ] }; this.neededShips = 1; } else if (type == "HugeAttack") { priority = 90; // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Ranged", "CitizenSoldier"], "interests": [["strength", 3]] }; this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Melee", "CitizenSoldier"], "interests": [["strength", 3]] }; this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Ranged", "Champion"], "interests": [["strength", 3]] }; this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Melee", "Champion"], "interests": [["strength", 3]] }; this.unitStat.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], "interests": [["strength", 2]] }; this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], "interests": [["strength", 2]] }; this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], "interests": [["strength", 3]] }; this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Melee", "Champion"], "interests": [["strength", 2]] }; this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], "interests": [["strength", 2]] }; this.neededShips = 5; } else { priority = 70; this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Ranged"], "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Melee"], "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [["strength", 1]] }; this.neededShips = 3; } // Put some randomness on the attack size let variation = randFloat(0.8, 1.2); // and lower priority and smaller sizes for easier difficulty levels if (this.Config.difficulty < 2) { priority *= 0.6; variation *= 0.5; } else if (this.Config.difficulty < 3) { priority *= 0.8; variation *= 0.8; } for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(variation * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize); } // change the sizes according to max population this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips); for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize); } // TODO: there should probably be one queue per type of training building gameState.ai.queueManager.addQueue("plan_" + this.name, priority); gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority); // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] this.buildOrders = []; this.canBuildUnits = gameState.ai.HQ.canBuildUnits; this.siegeState = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders // some variables used during the attack this.position5TurnsAgo = [0, 0]; this.lastPosition = [0, 0]; this.position = [0, 0]; this.isBlocked = false; // true when this attack faces walls return true; }; PETRA.AttackPlan.prototype.init = function(gameState) { this.queue = gameState.ai.queues["plan_" + this.name]; this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"]; this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name)); this.unitCollection.registerUpdates(); this.unit = {}; // defining the entity collections. Will look for units I own, that are part of this plan. // Also defining the buildOrders. for (let cat in this.unitStat) { let Unit = this.unitStat[cat]; this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[cat].registerUpdates(); if (this.canBuildUnits) this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); } }; PETRA.AttackPlan.prototype.getName = function() { return this.name; }; PETRA.AttackPlan.prototype.getType = function() { return this.type; }; PETRA.AttackPlan.prototype.isStarted = function() { return this.state !== "unexecuted" && this.state !== "completing"; }; PETRA.AttackPlan.prototype.isPaused = function() { return this.paused; }; PETRA.AttackPlan.prototype.setPaused = function(boolValue) { this.paused = boolValue; }; /** * Returns true if the attack can be executed at the current time * Basically it checks we have enough units. */ PETRA.AttackPlan.prototype.canStart = function() { if (!this.canBuildUnits) return true; for (let unitCat in this.unitStat) if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) return false; return true; }; PETRA.AttackPlan.prototype.mustStart = function() { if (this.isPaused()) return false; if (!this.canBuildUnits) return this.unitCollection.hasEntities(); let MaxReachedEverywhere = true; let MinReachedEverywhere = true; for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit.targetSize) MaxReachedEverywhere = false; if (this.unit[unitCat].length < Unit.minSize) { MinReachedEverywhere = false; break; } } if (MaxReachedEverywhere) return true; if (MinReachedEverywhere) return this.type == "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 50; return false; }; PETRA.AttackPlan.prototype.forceStart = function() { for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; Unit.targetSize = 0; Unit.minSize = 0; } this.forced = true; }; PETRA.AttackPlan.prototype.emptyQueues = function() { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); }; PETRA.AttackPlan.prototype.removeQueues = function(gameState) { gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); }; /** Adds a build order. If resetQueue is true, this will reset the queue. */ PETRA.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { // no minsize as we don't want the plan to fail at the last minute though. this.unitStat[name] = unitStats; let Unit = this.unitStat[name]; this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[name].registerUpdates(); this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); if (resetQueue) this.emptyQueues(); } }; PETRA.AttackPlan.prototype.addSiegeUnits = function(gameState) { if (this.siegeState == 2 || this.state !== "unexecuted") return false; let civ = gameState.getPlayerCiv(); let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]]; let hasTrainer = [false, false, false]; for (let ent of gameState.getOwnTrainingFacilities().values()) { let trainables = ent.trainableEntities(civ); if (!trainables) continue; for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.available(gameState)) continue; for (let i = 0; i < classes.length; ++i) if (classes[i].every(c => template.hasClass(c))) hasTrainer[i] = true; } } if (hasTrainer.every(e => !e)) return false; let i = this.name % classes.length; for (let k = 0; k < classes.length; ++k) { if (hasTrainer[i]) break; i = ++i % classes.length; } this.siegeState = 2; let targetSize; if (this.Config.difficulty < 3) targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); else targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1; targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0); if (!targetSize) return true; // no minsize as we don't want the plan to fail at the last minute though. let stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2), "classes": classes[i], "interests": [ ["siegeStrength", 3] ] }; this.addBuildOrder(gameState, "Siege", stat, true); return true; }; /** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */ PETRA.AttackPlan.prototype.updatePreparation = function(gameState) { // the completing step is used to return resources and regroup the units // so we check that we have no more forced order before starting the attack if (this.state == "completing") { // if our target was destroyed, go back to "unexecuted" state if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) { this.state = "unexecuted"; this.target = undefined; } else { // check that all units have finished with their transport if needed if (this.waitingForTransport()) return 1; // bloqued units which cannot finish their order should not stop the attack if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) return 1; return 2; } } if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0) this.debugAttack(); // if we need a transport, wait for some transport ships if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length) return 1; if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture) this.assignUnits(gameState); if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) this.reassignCavUnit(gameState); // reassign some cav (if any) to fasten raid preparations // Fasten the end game. if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits()) { let totEnemies = 0; let hasEnemies = false; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i]) continue; hasEnemies = true; totEnemies += gameState.getEnemyUnits(i).length; } if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies) this.forceStart(); } // special case: if we've reached max pop, and we can start the plan, start it. if (gameState.getPopulationMax() - gameState.getPopulation() < 5) { let lengthMin = 16; if (gameState.getPopulationMax() < 300) lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300); if (this.canStart() || this.unitCollection.length > lengthMin) { this.emptyQueues(); } else // Abort the plan so that its units will be reassigned to other plans. { if (this.Config.debug > 1) { let am = gameState.ai.HQ.attackManager; API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + " rush " + am.upcomingAttacks.Rush.length + " attack " + am.upcomingAttacks.Attack.length + " huge " + am.upcomingAttacks.HugeAttack.length); API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + " rush " + am.startedAttacks.Rush.length + " attack " + am.startedAttacks.Attack.length + " huge " + am.startedAttacks.HugeAttack.length); } return 0; } } else if (this.mustStart()) { if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) { // keep on while the units finish being trained, then we'll start this.emptyQueues(); return 1; } } else { if (this.canBuildUnits) { // We still have time left to recruit units and do stuffs. if (this.siegeState == 0 || this.siegeState == 1 && gameState.ai.playedTurn % 5 == 0) this.addSiegeUnits(gameState); this.trainMoreUnits(gameState); // may happen if we have no more training facilities and build orders are canceled if (!this.buildOrders.length) return 0; // will abort the plan } return 1; } // if we're here, it means we must start this.state = "completing"; // Raids have their predefined target if (!this.target && !this.chooseTarget(gameState)) return 0; if (!this.overseas) this.getPathToTarget(gameState); if (this.type == "Raid") this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; else { if (this.type == "Rush" || this.forced) this.maxCompletingTime = gameState.ai.elapsedTime + 40; else this.maxCompletingTime = gameState.ai.elapsedTime + 60; // warn our allies so that they can help if possible if (!this.requested) Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer }); } // Remove those units which were in a temporary bombing attack for (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) { for (let entId of unitIds.values()) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) continue; unitIds.delete(entId); ent.stopMoving(); } } let rallyPoint = this.rallyPoint; let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); for (let ent of this.unitCollection.values()) { // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) { ent.setMetadata(PlayerID, "plan", -1); continue; } ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "subrole", "completing"); let queued = false; if (ent.resourceCarrying() && ent.resourceCarrying().length) queued = PETRA.returnResources(gameState, ent); let index = PETRA.getLandAccess(gameState, ent); if (index == rallyIndex) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint); } // reset all queued units this.removeQueues(gameState); return 1; }; PETRA.AttackPlan.prototype.trainMoreUnits = function(gameState) { // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. for (let order of this.buildOrders) { let special = "Plan_" + this.name + "_" + order[4]; let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); aQueued += this.queue.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special); order[0] = order[2].length + aQueued; } this.buildOrders.sort((a, b) => { let va = a[0]/a[3].targetSize - a[3].priority; if (a[0] >= a[3].targetSize) va += 1000; let vb = b[0]/b[3].targetSize - b[3].priority; if (b[0] >= b[3].targetSize) vb += 1000; return va - vb; }); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) { API3.warn("===================================="); API3.warn("======== build order for plan " + this.name); for (let order of this.buildOrders) { let specialData = "Plan_"+this.name+"_"+order[4]; let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); } API3.warn("===================================="); } let firstOrder = this.buildOrders[0]; if (firstOrder[0] < firstOrder[3].targetSize) { // find the actual queue we want let queue = this.queue; if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 && firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Hero") != -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Champion") != -1) queue = this.queueChamp; if (queue.length() <= 5) { let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests); // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, // effectively removing the unit from the plan. if (template === undefined) { if (this.Config.debug > 1) API3.warn("attack no template found " + firstOrder[1]); delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat. this.buildOrders.splice(0, 1); } else { if (this.Config.debug > 2) API3.warn("attack template " + template + " added for plan " + this.name); let max = firstOrder[3].batchSize; let specialData = "Plan_" + this.name + "_" + firstOrder[4]; let data = { "plan": this.name, "special": specialData, "base": 0 }; data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; let trainingPlan = new PETRA.TrainingPlan(gameState, template, data, max, max); if (trainingPlan.template) queue.addPlan(trainingPlan); else if (this.Config.debug > 1) API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + " build3 " + uneval(firstOrder[3].interests)); } } } }; PETRA.AttackPlan.prototype.assignUnits = function(gameState) { let plan = this.name; let added = false; // If we can not build units, assign all available except those affected to allied defense to the current attack if (!this.canBuildUnits) { for (let ent of gameState.getOwnUnits().values()) { if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } if (this.type == "Raid") { // Raid are fast cavalry attack: assign all cav except some for hunting let num = 0; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Cavalry") || !this.isAvailableUnit(gameState, ent)) continue; if (num++ < 2) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } // Assign all units without specific role for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) { if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent)) continue; if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Add units previously in a plan, but which left it because needed for defense or attack finished for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values()) { if (!this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Finally add also some workers, // If Rush, assign all kind of workers, keeping only a minimum number of defenders // Otherwise, assign only some idle workers if too much of them let num = 0; let numbase = {}; let keep = this.type != "Rush" ? 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; keep = Math.round(this.Config.popScaling * keep); for (let ent of gameState.getOwnEntitiesByRole("worker", true).values()) { if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) continue; let baseID = ent.getMetadata(PlayerID, "base"); if (baseID) numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; else { API3.warn("Petra problem ent without base "); PETRA.dumpEntity(ent); continue; } if (num++ < keep || numbase[baseID] < 5) continue; if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle") continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; }; PETRA.AttackPlan.prototype.isAvailableUnit = function(gameState, ent) { if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 || ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return false; if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8)) return false; return true; }; /** Reassign one (at each turn) Cav unit to fasten raid preparation. */ PETRA.AttackPlan.prototype.reassignCavUnit = function(gameState) { for (let ent of this.unitCollection.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) continue; let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); ent.setMetadata(PlayerID, "plan", raid.name); this.unitCollection.updateEnt(ent); raid.unitCollection.updateEnt(ent); return; } }; PETRA.AttackPlan.prototype.chooseTarget = function(gameState) { if (this.targetPlayer === undefined) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; } this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) { if (this.uniqueTargetId) return false; // may-be all our previous enemey target (if not recomputed here) have been destroyed ? this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) return false; } this.targetPos = this.target.position(); // redefine a new rally point for this target if we have a base on the same land // find a new one on the pseudo-nearest base (dist weighted by the size of the island) let targetIndex = PETRA.getLandAccess(gameState, this.target); let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); if (targetIndex != rallyIndex) { let distminSame = Math.min(); let rallySame; let distminDiff = Math.min(); let rallyDiff; for (let base of gameState.ai.HQ.baseManagers) { let anchor = base.anchor; if (!anchor || !anchor.position()) continue; let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos); if (base.accessIndex == targetIndex) { if (dist >= distminSame) continue; distminSame = dist; rallySame = anchor.position(); } else { dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]); if (dist >= distminDiff) continue; distminDiff = dist; rallyDiff = anchor.position(); } } if (rallySame) { this.rallyPoint = rallySame; this.overseas = 0; } else if (rallyDiff) { rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff); this.rallyPoint = rallyDiff; let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); if (sea) { this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); } else { API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target" + " with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName()); return false; } } } else if (this.overseas) this.overseas = 0; return true; }; /** * sameLand true means that we look for a target for which we do not need to take a transport */ PETRA.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand) { this.isBlocked = false; // Temporary variables needed by isValidTarget this.gameState = gameState; this.sameLand = sameLand && sameLand > 1 ? sameLand : false; let targets; if (this.uniqueTargetId) { targets = new API3.EntityCollection(gameState.sharedScript); let ent = gameState.getEntityById(this.uniqueTargetId); if (ent) targets.addEnt(ent); } else { if (this.type == "Raid") targets = this.raidTargetFinder(gameState); else if (this.type == "Rush" || this.type == "Attack") { targets = this.rushTargetFinder(gameState, this.targetPlayer); if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced)) targets = this.defaultTargetFinder(gameState, this.targetPlayer); } else targets = this.defaultTargetFinder(gameState, this.targetPlayer); } if (!targets.hasEntities()) return undefined; // picking the nearest target let target; let minDist = Math.min(); for (let ent of targets.values()) { if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && (!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id()))) continue; // Do not bother with some pointless targets if (!this.isValidTarget(ent)) continue; let dist = API3.SquareVectorDistance(ent.position(), position); // In normal attacks, disfavor fields if (this.type != "Rush" && this.type != "Raid" && ent.hasClass("Field")) dist += 100000; if (dist < minDist) { minDist = dist; target = ent; } } if (!target) return undefined; // Check that we can reach this target target = this.checkTargetObstruction(gameState, target, position); if (!target) return undefined; if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic")) gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]); // Rushes can change their enemy target if nothing found with the preferred enemy // Obstruction also can change the enemy target this.targetPlayer = target.owner(); return target; }; /** * Default target finder aims for conquest critical targets * We must apply the *same* selection (isValidTarget) as done in getNearestTarget */ PETRA.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); if (gameState.getVictoryConditions().has("wonder")) for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values()) targets.addEnt(ent); if (gameState.getVictoryConditions().has("regicide")) for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values()) targets.addEnt(ent); if (gameState.getVictoryConditions().has("capture_the_relic")) for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) targets.addEnt(ent); targets = targets.filter(this.isValidTarget, this); if (targets.hasEntities()) return targets; let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); targets = validTargets.filter(API3.Filters.byClass("CivCentre")); if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("ConquestCritical")); // If there's nothing, attack anything else that's less critical if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("Town")); if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("Village")); // No buildings, attack anything conquest critical, units included. // TODO Should add naval attacks against the last remaining ships. if (!targets.hasEntities()) targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")). filter(API3.Filters.not(API3.Filters.byClass("Ship"))); return targets; }; PETRA.AttackPlan.prototype.isValidTarget = function(ent) { if (!ent.position()) return false; if (this.sameLand && PETRA.getLandAccess(this.gameState, ent) != this.sameLand) return false; return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length; }; /** Rush target finder aims at isolated non-defended buildings */ PETRA.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); let buildings; if (playerEnemy !== undefined) buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray(); else buildings = gameState.getEnemyStructures().toEntityArray(); if (!buildings.length) return targets; this.position = this.unitCollection.getCentrePosition(); if (!this.position) this.position = this.rallyPoint; let target; let minDist = Math.min(); for (let building of buildings) { if (building.owner() == 0) continue; if (building.hasDefensiveFire()) continue; if (!this.isValidTarget(building)) continue; let pos = building.position(); let defended = false; for (let defense of buildings) { if (!defense.hasDefensiveFire()) continue; let dist = API3.SquareVectorDistance(pos, defense.position()); if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 { defended = true; break; } } if (defended) continue; let dist = API3.SquareVectorDistance(pos, this.position); if (dist > minDist) continue; minDist = dist; target = building; } if (target) targets.addEnt(target); if (!targets.hasEntities() && this.type == "Rush" && playerEnemy) targets = this.rushTargetFinder(gameState); return targets; }; /** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ PETRA.AttackPlan.prototype.raidTargetFinder = function(gameState) { let targets = new API3.EntityCollection(gameState.sharedScript); for (let targetId of gameState.ai.HQ.defenseManager.targetList) { let target = gameState.getEntityById(targetId); if (target && target.position()) targets.addEnt(target); } return targets; }; /** * Check that we can have a path to this target * otherwise we may be blocked by walls and try to react accordingly * This is done only when attacker and target are on the same land */ PETRA.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) { if (PETRA.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) return target; let targetPos = target.position(); let startPos = { "x": position[0], "y": position[1] }; let endPos = { "x": targetPos[0], "y": targetPos[1] }; let blocker; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); if (!path.length) return undefined; let pathPos = [path[0].x, path[0].y]; let dist = API3.VectorDistance(pathPos, targetPos); let radius = target.obstructionRadius().max; for (let struct of gameState.getEnemyStructures().values()) { if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field")) continue; // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate if (dist < radius + 10 && !struct.hasClass("Gates")) continue; // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m // in the target direction would bring us inside its obstruction. let structPos = struct.position(); let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; if (struct.get("Obstruction/Static")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Static/@width"); let depth = +struct.get("Obstruction/Static/@depth"); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } } else if (struct.get("Obstruction/Obstructions")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Obstructions/Door/@width"); let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); let doorHalfWidth = width / 2; width += +struct.get("Obstruction/Obstructions/Left/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth")); width += +struct.get("Obstruction/Obstructions/Right/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth")); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } // check that the path does not cross this gate (could happen if not locked) for (let i = 1; i < path.length; ++i) { let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; if (v1 * v2 < 0) { let u0 = (u1*v2 - u2*v1) / (v2-v1); if (Math.abs(u0) > doorHalfWidth) continue; blocker = struct; break; } } if (blocker) break; } else if (struct.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); if (x*x + y*y < r*r) { blocker = struct; break; } } } if (blocker && blocker.hasClass("StoneWall")) { this.isBlocked = true; return blocker; } else if (blocker) { this.isBlocked = true; return blocker; } return target; }; PETRA.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false) { let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let endAccess = PETRA.getLandAccess(gameState, this.target); if (startAccess != endAccess) return false; Engine.ProfileStart("AI Compute path"); let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); this.path = []; this.path.push(this.targetPos); for (let p in path) this.path.push([path[p].x, path[p].y]); this.path.push(this.rallyPoint); this.path.reverse(); // Change the rally point to something useful if (!fixedRallyPoint) this.setRallyPoint(gameState); Engine.ProfileStop(); return true; }; /** Set rally point at the border of our territory */ PETRA.AttackPlan.prototype.setRallyPoint = function(gameState) { for (let i = 0; i < this.path.length; ++i) { if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID) continue; if (i === 0) this.rallyPoint = this.path[0]; else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20)) { this.rallyPoint = this.path[i-2]; this.path.splice(0, i-2); } else { this.rallyPoint = this.path[i-1]; this.path.splice(0, i-1); } break; } }; /** * Executes the attack plan, after this is executed the update function will be run every turn * If we're here, it's because we have enough units. */ PETRA.AttackPlan.prototype.StartAttack = function(gameState) { if (this.Config.debug > 1) API3.warn("start attack " + this.name + " with type " + this.type); // if our target was destroyed during preparation, choose a new one if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) && !this.chooseTarget(gameState)) return false; // erase our queue. This will stop any leftover unit from being trained. this.removeQueues(gameState); for (let ent of this.unitCollection.values()) { ent.setMetadata(PlayerID, "subrole", "walking"); let stance = ent.isPackable() ? "standground" : "aggressive"; if (ent.getStance() != stance) ent.setStance(stance); } let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let targetAccess = PETRA.getLandAccess(gameState, this.target); if (rallyAccess == targetAccess) { if (!this.path) this.getPathToTarget(gameState, true); if (!this.path || !this.path[0][0] || !this.path[0][1]) return false; this.overseas = 0; this.state = "walking"; this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); } else { this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess); if (!this.overseas) return false; this.state = "transporting"; // TODO require a global transport for the collection, // and put back its state to "walking" when the transport is finished for (let ent of this.unitCollection.values()) gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos); } return true; }; /** Runs every turn after the attack is executed */ PETRA.AttackPlan.prototype.update = function(gameState, events) { if (!this.unitCollection.hasEntities()) return 0; Engine.ProfileStart("Update Attack"); this.position = this.unitCollection.getCentrePosition(); // we are transporting our units, let's wait // TODO instead of state "arrived", made a state "walking" with a new path if (this.state == "transporting") this.UpdateTransporting(gameState, events); if (this.state == "walking" && !this.UpdateWalking(gameState, events)) { Engine.ProfileStop(); return 0; } if (this.state == "arrived") { // let's proceed on with whatever happens now. this.state = ""; this.startingAttack = true; this.unitCollection.forEach(ent => { ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "attacking"); }); if (this.type == "Rush") // try to find a better target for rush { let newtarget = this.getNearestTarget(gameState, this.position); if (newtarget) { this.target = newtarget; this.targetPos = this.target.position(); } } } // basic state of attacking. if (this.state == "") { // First update the target and/or its position if needed if (!this.UpdateTarget(gameState)) { Engine.ProfileStop(); return false; } let time = gameState.ai.elapsedTime; let attackedByStructure = {}; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); let ourUnit = gameState.getEntityById(evt.target); if (!ourUnit || !attacker || !attacker.position()) continue; if (!attacker.hasClass("Unit")) { attackedByStructure[evt.target] = true; continue; } if (PETRA.isSiegeUnit(ourUnit)) { // if our siege units are attacked, we'll send some units to deal with enemies. let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { if (PETRA.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out continue; - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (!ent.canAttackTarget(attacker, allowCapture)) + continue; + ent.attack(attacker.id(), allowCapture); 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")) + if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, PETRA.allowCapture(gameState, ourUnit, attacker))) { ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) { // do not react if our melee units are attacked by ranged one and we are blocked by walls // TODO check that the attacker is from behind the wall continue; } else if (PETRA.isSiegeUnit(attacker)) { // if our unit is attacked by a siege unit, we'll send some melee units to help it. let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (!ent.canAttackTarget(attacker, allowCapture)) + continue; + ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { // Look first for nearby units to help us if possible let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); for (let ent of collec.values()) { - if (PETRA.isSiegeUnit(ent)) + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, allowCapture)) continue; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClass("Structure") && !target.hasClass("Support")) continue; } - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate let orderData = ourUnit.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClass("Structure") && !target.hasClass("Support")) { if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) continue; } } - ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker); + if (ourUnit.canAttackTarget(attacker, allowCapture)) + { + ourUnit.attack(attacker.id(), allowCapture); + ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + } } } } let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); // Count the number of times an enemy is targeted, to prevent all units to follow the same target let unitTargets = {}; for (let ent of this.unitCollection.values()) { if (ent.hasClass("Ship")) // TODO What to do with ships continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || target.hasClass("Structure")) continue; if (!(targetId in unitTargets)) { if (PETRA.isSiegeUnit(target) || target.hasClass("Hero")) unitTargets[targetId] = -8; else if (target.hasClass("Champion") || target.hasClass("Ship")) unitTargets[targetId] = -5; else unitTargets[targetId] = -3; } ++unitTargets[targetId]; } let veto = {}; for (let target in unitTargets) if (unitTargets[target] > 0) veto[target] = true; let targetClassesUnit; let targetClassesSiege; if (this.type == "Rush") targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto }; else { if (this.target.hasClass("Fortress")) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto }; else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall")) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; else targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Fortress"], "vetoEntities": veto }; } if (this.target.hasClass("Structure")) targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto }; else targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto }; // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later if (this.target.hasDefensiveFire()) { targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); } if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length) this.unitCollUpdateArray = this.unitCollection.toIdArray(); // Let's check a few units each time we update (currently 10) except when attack starts let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; for (let check = 0; check < lgth; check++) { let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); if (!ent || !ent.position()) continue; // Do not reassign units which have reacted to an attack in that same turn if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) continue; let targetId; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) targetId = orderData[0].target; // update the order if needed let needsUpdate = false; let maybeUpdate = false; let siegeUnit = PETRA.isSiegeUnit(ent); if (ent.isIdle()) needsUpdate = true; else if (siegeUnit && targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (!target.hasClass("Structure")) maybeUpdate = true; } else if (targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (target.hasClass("Ship") && !ent.hasClass("Ship")) maybeUpdate = true; else if (attackedByStructure[ent.id()] && target.hasClass("Field")) maybeUpdate = true; else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") && target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") maybeUpdate = true; } // don't update too soon if not necessary if (!needsUpdate) { if (!maybeUpdate) continue; let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) continue; } ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); let range = 60; let attackTypes = ent.attackTypes(); if (this.isBlocked) { if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = ent.attackRange("Ranged").max; else if (attackTypes && attackTypes.indexOf("Melee") !== -1) range = ent.attackRange("Melee").max; else range = 10; } else if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = 30 + ent.attackRange("Ranged").max; else if (ent.hasClass("Cavalry")) range += 30; range *= range; let entAccess = PETRA.getLandAccess(gameState, ent); // Checking for gates if we're a siege unit. if (siegeUnit) { let mStruct = enemyStructures.filter(enemy => { - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) + if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (enemy.foundationProgress() == 0) return false; if (PETRA.getLandAccess(gameState, enemy) != entAccess) return false; return true; }).toEntityArray(); if (mStruct.length) { mStruct.sort((structa, structb) => { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasDefensiveFire()) vala += 1000; else if (structa.hasClass("ConquestCritical")) vala += 200; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasDefensiveFire()) valb += 1000; else if (structb.hasClass("ConquestCritical")) valb += 200; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), PETRA.allowCapture(gameState, ent, mStruct[0])); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand])); } } else { if (!ent.hasClass("Ranged")) { let targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto }; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege); } } else { let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); let mUnit = enemyUnits.filter(enemy => { - if (!enemy.position()) + if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (enemy.hasClass("Animal")) return false; if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") return false; let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); if (dist > range) return false; if (PETRA.getLandAccess(gameState, enemy) != entAccess) return false; // if already too much units targeting this enemy, let's continue towards our main target if (veto[enemy.id()] && API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) return false; enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist)); return true; }, this).toEntityArray(); if (mUnit.length) { mUnit.sort((unitA, unitB) => { let vala = unitA.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitA.classes())) vala += 100; let valb = unitB.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitB.classes())) valb += 100; let distA = unitA.getMetadata(PlayerID, "distance"); let distB = unitB.getMetadata(PlayerID, "distance"); if (distA && distB) { vala -= distA; valb -= distB; } if (veto[unitA.id()]) vala -= 20000; if (veto[unitB.id()]) valb -= 20000; return valb - vala; }); let rand = randIntExclusive(0, mUnit.length * 0.1); ent.attack(mUnit[rand].id(), PETRA.allowCapture(gameState, ent, mUnit[rand])); } - else if (this.isBlocked) + // 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 (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) { let targetClasses = targetClassesUnit; if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything { if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; else targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; } else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else { let mStruct = enemyStructures.filter(enemy => { if (this.isBlocked && enemy.id() != this.target.id()) return false; - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) + if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (PETRA.getLandAccess(gameState, enemy) != entAccess) return false; return true; }, this).toEntityArray(); if (mStruct.length) { mStruct.sort((structa, structb) => { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasClass("ConquestCritical")) vala += 100; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasClass("ConquestCritical")) valb += 100; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), false); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand])); } } else if (needsUpdate) // really nothing let's try to help our nearest unit { let distmin = Math.min(); let attacker; this.unitCollection.forEach(unit => { if (!unit.position()) return; if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length || !unit.unitAIOrderData()[0].target) return; - if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) + let target = gameState.getEntityById(unit.unitAIOrderData()[0].target); + if (!target) return; let dist = API3.SquareVectorDistance(unit.position(), ent.position()); if (dist > distmin) return; distmin = dist; - attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); + if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target))) + return; + attacker = target; }); if (attacker) ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); } } } } this.unitCollUpdateArray.splice(0, lgth); this.startingAttack = false; // check if this enemy has resigned if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) this.target = undefined; } this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; PETRA.AttackPlan.prototype.UpdateTransporting = function(gameState, events) { let done = true; for (let ent of this.unitCollection.values()) { if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined) Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] }); else if (this.Config.debug > 1) Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] }); if (!done) continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) done = false; } if (done) { this.state = "arrived"; return; } // if we are attacked while waiting the rest of the army, retaliate for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (!attacker || !gameState.getEntityById(evt.target)) continue; for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; - if (!ent.isIdle()) + + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture)) continue; - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + ent.attack(attacker.id(), allowCapture); } break; } }; PETRA.AttackPlan.prototype.UpdateWalking = function(gameState, events) { // we're marching towards the target // Let's check if any of our unit has been attacked. // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building // or if we reached the enemy base. Different plans may react differently. let attackedNB = 0; let attackedUnitNB = 0; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0)) { attackedNB++; if (attacker.hasClass("Unit")) attackedUnitNB++; } } // Are we arrived at destination ? if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits())) { if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3) { this.state = "arrived"; return true; } } // basically haven't moved an inch: very likely stuck) if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { // check for stuck siege units let farthest = 0; let farthestEnt; for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) { let dist = API3.SquareVectorDistance(ent.position(), this.position); if (dist < farthest) continue; farthest = dist; farthestEnt = ent; } if (farthestEnt) farthestEnt.destroy(); } if (gameState.ai.playedTurn % 5 === 0) this.position5TurnsAgo = this.position; if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0) { if (!this.path[0][0] || !this.path[0][1]) API3.warn("Start: Problem with path " + uneval(this.path)); // We're stuck, presumably. Check if there are no walls just close to us. for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "StoneWall"])).values()) { if (API3.SquareVectorDistance(this.position, ent.position()) > 800) continue; let enemyClass = ent.hasClass("StoneWall") ? "StoneWall" : "Palisade"; // there are walls, so check if we can attack if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities()) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); this.state = "arrived"; return true; } // abort plan if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up."); return false; } // this.unitCollection.move(this.path[0][0], this.path[0][1]); this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]); } // check if our units are close enough from the next waypoint. if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) { this.path.shift(); if (this.path.length) this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); else { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } } return true; }; PETRA.AttackPlan.prototype.UpdateTarget = function(gameState) { // First update the target position in case it's a unit (and check if it has garrisoned) if (this.target && this.target.hasClass("Unit")) { this.targetPos = this.target.position(); if (!this.targetPos) { let holder = PETRA.getHolder(gameState, this.target); if (holder && gameState.isPlayerEnemy(holder.owner())) { this.target = holder; this.targetPos = holder.position(); } else this.target = undefined; } } // Then update the target if needed: if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer)) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; if (this.target && this.target.owner() !== this.targetPlayer) this.target = undefined; } if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned this.target = undefined; if (!this.target || !gameState.getEntityById(this.target.id())) { if (this.Config.debug > 1) API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching."); let accessIndex = this.getAttackAccess(gameState); this.target = this.getNearestTarget(gameState, this.position, accessIndex); if (!this.target) { if (this.uniqueTargetId) return false; // Check if we could help any current attack let attackManager = gameState.ai.HQ.attackManager; for (let attackType in attackManager.startedAttacks) { for (let attack of attackManager.startedAttacks[attackType]) { if (attack.name == this.name) continue; if (!attack.target || !gameState.getEntityById(attack.target.id()) || !gameState.isPlayerEnemy(attack.target.owner())) continue; if (accessIndex != PETRA.getLandAccess(gameState, attack.target)) continue; if (attack.target.owner() == 0 && attack.targetPlayer != 0) // looks like it has resigned continue; if (!gameState.isPlayerEnemy(attack.targetPlayer)) continue; this.target = attack.target; this.targetPlayer = attack.targetPlayer; this.targetPos = this.target.position(); return true; } } // If not, let's look for another enemy if (!this.target) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.position, accessIndex); if (!this.target) { if (this.Config.debug > 1) API3.warn("No new target found. Remaining units " + this.unitCollection.length); return false; } } if (this.Config.debug > 1) API3.warn("We will help one of our other attacks"); } this.targetPos = this.target.position(); } return true; }; /** reset any units */ PETRA.AttackPlan.prototype.Abort = function(gameState) { this.unitCollection.unregister(); if (this.unitCollection.hasEntities()) { // If the attack was started, look for a good rallyPoint to withdraw let rallyPoint; if (this.isStarted()) { let access = this.getAttackAccess(gameState); let dist = Math.min(); if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) { rallyPoint = this.rallyPoint; dist = API3.SquareVectorDistance(this.position, rallyPoint); } // Then check if we have a nearer base (in case this attack has captured one) for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (PETRA.getLandAccess(gameState, base.anchor) != access) continue; let newdist = API3.SquareVectorDistance(this.position, base.anchor.position()); if (newdist > dist) continue; dist = newdist; rallyPoint = base.anchor.position(); } } for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "role") == "attack") ent.stopMoving(); if (rallyPoint) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); this.removeUnit(ent); } } for (let unitCat in this.unitStat) this.unit[unitCat].unregister(); this.removeQueues(gameState); }; PETRA.AttackPlan.prototype.removeUnit = function(ent, update) { if (ent.getMetadata(PlayerID, "role") == "attack") { if (ent.hasClass("CitizenSoldier")) ent.setMetadata(PlayerID, "role", "worker"); else ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); } ent.setMetadata(PlayerID, "plan", -1); if (update) this.unitCollection.updateEnt(ent); }; PETRA.AttackPlan.prototype.checkEvents = function(gameState, events) { for (let evt of events.EntityRenamed) { if (!this.target || this.target.id() != evt.entity) continue; if (this.type == "Raid" && !this.isStarted()) this.target = undefined; else this.target = gameState.getEntityById(evt.newentity); if (this.target) this.targetPos = this.target.position(); } for (let evt of events.OwnershipChanged) // capture event if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) this.target = undefined; for (let evt of events.PlayerDefeated) { if (this.targetPlayer !== evt.playerId) continue; this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); this.target = undefined; } if (!this.overseas || this.state !== "unexecuted") return; // let's check if an enemy has built a structure at our access for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.position() || !ent.hasClass("Structure")) continue; if (!gameState.isPlayerEnemy(ent.owner())) continue; let access = PETRA.getLandAccess(gameState, ent); for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; this.overseas = 0; this.rallyPoint = base.anchor.position(); } } }; PETRA.AttackPlan.prototype.waitingForTransport = function() { for (let ent of this.unitCollection.values()) if (ent.getMetadata(PlayerID, "transport") !== undefined) return true; return false; }; PETRA.AttackPlan.prototype.hasSiegeUnits = function() { for (let ent of this.unitCollection.values()) if (PETRA.isSiegeUnit(ent)) return true; return false; }; PETRA.AttackPlan.prototype.hasForceOrder = function(data, value) { for (let ent of this.unitCollection.values()) { if (data && +ent.getMetadata(PlayerID, data) !== value) continue; let orders = ent.unitAIOrderData(); for (let order of orders) if (order.force) return true; } return false; }; /** * The center position of this attack may be in an inaccessible area. So we use the access * of the unit nearest to this center position. */ PETRA.AttackPlan.prototype.getAttackAccess = function(gameState) { for (let ent of this.unitCollection.filterNearest(this.position, 1).values()) return PETRA.getLandAccess(gameState, ent); return 0; }; PETRA.AttackPlan.prototype.debugAttack = function() { API3.warn("---------- attack " + this.name); for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); } API3.warn("------------------------------"); }; PETRA.AttackPlan.prototype.Serialize = function() { let properties = { "name": this.name, "type": this.type, "state": this.state, "forced": this.forced, "rallyPoint": this.rallyPoint, "overseas": this.overseas, "paused": this.paused, "maxCompletingTime": this.maxCompletingTime, "neededShips": this.neededShips, "unitStat": this.unitStat, "siegeState": this.siegeState, "position5TurnsAgo": this.position5TurnsAgo, "lastPosition": this.lastPosition, "position": this.position, "isBlocked": this.isBlocked, "targetPlayer": this.targetPlayer, "target": this.target !== undefined ? this.target.id() : undefined, "targetPos": this.targetPos, "uniqueTargetId": this.uniqueTargetId, "path": this.path }; return { "properties": properties }; }; PETRA.AttackPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; if (this.target) this.target = gameState.getEntityById(this.target); this.failed = undefined; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 23758) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 23759) @@ -1,646 +1,649 @@ /** * Armies used by the defense manager. * An army is a collection of own entities and enemy entities. * * Types of armies: * "default": army to counter an invading army * "capturing": army set to capture a gaia building or recover capture points to one of its own structures * It must contain only one foe (the building to capture) and never be merged */ PETRA.DefenseArmy = function(gameState, foeEntities, type) { this.ID = gameState.ai.uniqueIDs.armies++; this.type = type || "default"; this.Config = gameState.ai.Config; this.compactSize = this.Config.Defense.armyCompactSize; this.breakawaySize = this.Config.Defense.armyBreakawaySize; // average this.foePosition = [0, 0]; this.positionLastUpdate = gameState.ai.elapsedTime; // Some caching // A list of our defenders that were tasked with attacking a particular unit // This doesn't mean that they actually are since they could move on to something else on their own. this.assignedAgainst = {}; // who we assigned against, for quick removal. this.assignedTo = {}; this.foeEntities = []; this.foeStrength = 0; this.ownEntities = []; this.ownStrength = 0; // actually add units for (let id of foeEntities) this.addFoe(gameState, id, true); this.recalculatePosition(gameState, true); return true; }; /** * add an entity to the enemy army * Will return true if the entity was added and false otherwise. * won't recalculate our position but will dirty it. * force is true at army creation or when merging armies, so in this case we should add it even if far */ PETRA.DefenseArmy.prototype.addFoe = function(gameState, enemyId, force) { if (this.foeEntities.indexOf(enemyId) !== -1) return false; let ent = gameState.getEntityById(enemyId); if (!ent || !ent.position()) return false; // check distance if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) return false; this.foeEntities.push(enemyId); this.assignedAgainst[enemyId] = []; this.positionLastUpdate = 0; this.evaluateStrength(ent); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); return true; }; /** * returns true if the entity was removed and false otherwise. * TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. */ PETRA.DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity) { let idx = this.foeEntities.indexOf(enemyId); if (idx === -1) return false; this.foeEntities.splice(idx, 1); this.assignedAgainst[enemyId] = undefined; for (let to in this.assignedTo) if (this.assignedTo[to] == enemyId) this.assignedTo[to] = undefined; let ent = enemyEntity ? enemyEntity : gameState.getEntityById(enemyId); if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) { this.evaluateStrength(ent, false, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); } return true; }; /** * adds a defender but doesn't assign him yet. * force is true when merging armies, so in this case we should add it even if no position as it can be in a ship */ PETRA.DefenseArmy.prototype.addOwn = function(gameState, id, force) { if (this.ownEntities.indexOf(id) !== -1) return false; let ent = gameState.getEntityById(id); if (!ent || !ent.position() && !force) return false; this.ownEntities.push(id); this.evaluateStrength(ent, true); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); this.assignedTo[id] = 0; let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole === undefined || subrole !== "defender") ent.setMetadata(PlayerID, "formerSubrole", subrole); ent.setMetadata(PlayerID, "subrole", "defender"); return true; }; PETRA.DefenseArmy.prototype.removeOwn = function(gameState, id, Entity) { let idx = this.ownEntities.indexOf(id); if (idx === -1) return false; this.ownEntities.splice(idx, 1); if (this.assignedTo[id] !== 0) { let temp = this.assignedAgainst[this.assignedTo[id]]; if (temp) temp.splice(temp.indexOf(id), 1); } this.assignedTo[id] = undefined; let ent = Entity ? Entity : gameState.getEntityById(id); if (!ent) return true; this.evaluateStrength(ent, true, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); let formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); if (formerSubrole !== undefined) ent.setMetadata(PlayerID, "subrole", formerSubrole); else ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "formerSubrole", undefined); // Remove from tranport plan if not yet on Board if (ent.getMetadata(PlayerID, "transport") !== undefined) { let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); if (plan && plan.state == "boarding" && ent.position()) plan.removeUnit(gameState, ent); } /* // TODO be sure that all units in the transport need the cancelation if (!ent.position()) // this unit must still be in a transport plan ... try to cancel it { let planID = ent.getMetadata(PlayerID, "transport"); // no plans must mean that the unit was in a ship which was destroyed, so do nothing if (planID) { if (gameState.ai.Config.debug > 0) warn("ent from army still in transport plan: plan " + planID + " canceled"); let plan = gameState.ai.HQ.navalManager.getPlan(planID); if (plan && !plan.canceled) plan.cancelTransport(gameState); } } */ return true; }; /** * resets the army properly. * assumes we already cleared dead units. */ PETRA.DefenseArmy.prototype.clear = function(gameState) { while (this.foeEntities.length > 0) this.removeFoe(gameState, this.foeEntities[0]); // Go back to our or allied territory if needed let posOwn = [0, 0]; let nOwn = 0; let posAlly = [0, 0]; let nAlly = 0; let posOther = [0, 0]; let nOther = 0; for (let entId of this.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position()) continue; let pos = ent.position(); let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); if (territoryOwner === PlayerID) { posOwn[0] += pos[0]; posOwn[1] += pos[1]; ++nOwn; } else if (gameState.isPlayerMutualAlly(territoryOwner)) { posAlly[0] += pos[0]; posAlly[1] += pos[1]; ++nAlly; } else { posOther[0] += pos[0]; posOther[1] += pos[1]; ++nOther; } } let destination; let defensiveFound; let distmin; let radius = 0; if (nOwn > 0) destination = [posOwn[0]/nOwn, posOwn[1]/nOwn]; else if (nAlly > 0) destination = [posAlly[0]/nAlly, posAlly[1]/nAlly]; else { posOther[0] /= nOther; posOther[1] /= nOther; let armyAccess = gameState.ai.accessibility.getAccessValue(posOther); for (let struct of gameState.getAllyStructures().values()) { let pos = struct.position(); if (!pos || !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(pos))) continue; if (PETRA.getLandAccess(gameState, struct) !== armyAccess) continue; let defensiveStruct = struct.hasDefensiveFire(); if (defensiveFound && !defensiveStruct) continue; let dist = API3.SquareVectorDistance(posOther, pos); if (distmin && dist > distmin && (defensiveFound || !defensiveStruct)) continue; if (defensiveStruct) defensiveFound = true; distmin = dist; destination = pos; radius = struct.obstructionRadius().max; } } while (this.ownEntities.length > 0) { let entId = this.ownEntities[0]; this.removeOwn(gameState, entId); let ent = gameState.getEntityById(entId); if (ent) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) continue; if (ent.healthLevel() < this.Config.garrisonHealthLevel.low && gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, ent)) continue; if (destination && !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(ent.position()))) ent.moveToRange(destination[0], destination[1], radius, radius+5); else ent.stopMoving(); } } this.assignedAgainst = {}; this.assignedTo = {}; this.recalculateStrengths(gameState); this.recalculatePosition(gameState); }; PETRA.DefenseArmy.prototype.assignUnit = function(gameState, entID) { // we'll assume this defender is ours already. // we'll also override any previous assignment let ent = gameState.getEntityById(entID); if (!ent || !ent.position()) return false; // try to return its resources, and if any, the attack order will be queued let queued = PETRA.returnResources(gameState, ent); let idMin; let distMin; let idMinAll; let distMinAll; for (let id of this.foeEntities) { let eEnt = gameState.getEntityById(id); if (!eEnt || !eEnt.position()) // probably can't happen. continue; + if (!ent.canAttackTarget(eEnt, PETRA.allowCapture(gameState, ent, eEnt))) + continue; + if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length && eEnt.unitAIOrderData()[0].target && eEnt.unitAIOrderData()[0].target == entID) { // being attacked >>> target the unit idMin = id; break; } // already enough units against it if (this.assignedAgainst[id].length > 8 || this.assignedAgainst[id].length > 5 && !eEnt.hasClass("Hero") && !PETRA.isSiegeUnit(eEnt)) continue; let dist = API3.SquareVectorDistance(ent.position(), eEnt.position()); if (idMinAll === undefined || dist < distMinAll) { idMinAll = id; distMinAll = dist; } if (this.assignedAgainst[id].length > 2) continue; if (idMin === undefined || dist < distMin) { idMin = id; distMin = dist; } } let idFoe; if (idMin !== undefined) idFoe = idMin; else if (idMinAll !== undefined) idFoe = idMinAll; else return false; let ownIndex = PETRA.getLandAccess(gameState, ent); let foeEnt = gameState.getEntityById(idFoe); let foePosition = foeEnt.position(); let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); if (ownIndex == foeIndex || ent.hasClass("Ship")) { this.assignedTo[entID] = idFoe; this.assignedAgainst[idFoe].push(entID); ent.attack(idFoe, PETRA.allowCapture(gameState, ent, foeEnt), queued); } else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); return true; }; PETRA.DefenseArmy.prototype.getType = function() { return this.type; }; PETRA.DefenseArmy.prototype.getState = function() { if (!this.foeEntities.length) return 0; return 1; }; /** * merge this army with another properly. * assumes units are in only one army. * also assumes that all have been properly cleaned up (no dead units). */ PETRA.DefenseArmy.prototype.merge = function(gameState, otherArmy) { // copy over all parameters. for (let i in otherArmy.assignedAgainst) { if (this.assignedAgainst[i] === undefined) this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; else this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); } for (let i in otherArmy.assignedTo) this.assignedTo[i] = otherArmy.assignedTo[i]; for (let id of otherArmy.foeEntities) this.addFoe(gameState, id, true); // TODO: reassign those ? for (let id of otherArmy.ownEntities) this.addOwn(gameState, id, true); this.recalculatePosition(gameState, true); this.recalculateStrengths(gameState); return true; }; PETRA.DefenseArmy.prototype.needsDefenders = function(gameState) { let defenseRatio; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(this.foePosition); if (territoryOwner == PlayerID) defenseRatio = this.Config.Defense.defenseRatio.own; else if (gameState.isPlayerAlly(territoryOwner)) { defenseRatio = this.Config.Defense.defenseRatio.ally; let numExclusiveAllies = 0; for (let p = 1; p < gameState.sharedScript.playersData.length; ++p) if (p != territoryOwner && gameState.sharedScript.playersData[p].isAlly[territoryOwner]) ++numExclusiveAllies; defenseRatio /= 1 + 0.5*Math.max(0, numExclusiveAllies-1); } else defenseRatio = this.Config.Defense.defenseRatio.neutral; // some preliminary checks because we don't update for tech so entStrength removed can be > entStrength added if (this.foeStrength <= 0 || this.ownStrength <= 0) this.recalculateStrengths(gameState); if (this.foeStrength * defenseRatio <= this.ownStrength) return false; return this.foeStrength * defenseRatio - this.ownStrength; }; /** if not forced, will only recalculate if on a different turn. */ PETRA.DefenseArmy.prototype.recalculatePosition = function(gameState, force) { if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) return; let npos = 0; let pos = [0, 0]; for (let id of this.foeEntities) { let ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; npos++; let epos = ent.position(); pos[0] += epos[0]; pos[1] += epos[1]; } // if npos = 0, the army must have been destroyed and will be removed next turn. keep previous position if (npos > 0) { this.foePosition[0] = pos[0]/npos; this.foePosition[1] = pos[1]/npos; } this.positionLastUpdate = gameState.ai.elapsedTime; }; PETRA.DefenseArmy.prototype.recalculateStrengths = function(gameState) { this.ownStrength = 0; this.foeStrength = 0; for (let id of this.foeEntities) this.evaluateStrength(gameState.getEntityById(id)); for (let id of this.ownEntities) this.evaluateStrength(gameState.getEntityById(id), true); }; /** adds or remove the strength of the entity either to the enemy or to our units. */ PETRA.DefenseArmy.prototype.evaluateStrength = function(ent, isOwn, remove) { if (!ent) return; let entStrength; if (ent.hasClass("Structure")) { if (ent.owner() !== PlayerID) entStrength = ent.getDefaultArrow() ? 6*ent.getDefaultArrow() : 4; else // small strength used only when we try to recover capture points entStrength = 2; } else entStrength = PETRA.getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); // TODO adapt the getMaxStrength function for animals. // For the time being, just increase it for elephants as the returned value is too small. if (ent.hasClass("Animal") && ent.hasClass("Elephant")) entStrength *= 3; if (remove) entStrength *= -1; if (isOwn) this.ownStrength += entStrength; else this.foeStrength += entStrength; }; PETRA.DefenseArmy.prototype.checkEvents = function(gameState, events) { // Warning the metadata is already cloned in shared.js. Futhermore, changes should be done before destroyEvents // otherwise it would remove the old entity from this army list // TODO we should may-be reevaluate the strength for (let evt of events.EntityRenamed) // take care of promoted and packed units { if (this.foeEntities.indexOf(evt.entity) !== -1) { let ent = gameState.getEntityById(evt.newentity); if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed continue; let idx = this.foeEntities.indexOf(evt.entity); this.foeEntities[idx] = evt.newentity; this.assignedAgainst[evt.newentity] = this.assignedAgainst[evt.entity]; this.assignedAgainst[evt.entity] = undefined; for (let to in this.assignedTo) if (this.assignedTo[to] === evt.entity) this.assignedTo[to] = evt.newentity; } else if (this.ownEntities.indexOf(evt.entity) !== -1) { let idx = this.ownEntities.indexOf(evt.entity); this.ownEntities[idx] = evt.newentity; this.assignedTo[evt.newentity] = this.assignedTo[evt.entity]; this.assignedTo[evt.entity] = undefined; for (let against in this.assignedAgainst) { if (!this.assignedAgainst[against]) continue; if (this.assignedAgainst[against].indexOf(evt.entity) !== -1) this.assignedAgainst[against][this.assignedAgainst[against].indexOf(evt.entity)] = evt.newentity; } } } for (let evt of events.Garrison) this.removeFoe(gameState, evt.entity); for (let evt of events.OwnershipChanged) // captured { if (!gameState.isPlayerEnemy(evt.to)) this.removeFoe(gameState, evt.entity); else if (evt.from === PlayerID) this.removeOwn(gameState, evt.entity); } for (let evt of events.Destroy) { let entityObj = evt.entityObj || undefined; // we may have capture+destroy, so do not trust owner and check all possibilities this.removeOwn(gameState, evt.entity, entityObj); this.removeFoe(gameState, evt.entity, entityObj); } }; PETRA.DefenseArmy.prototype.update = function(gameState) { for (let entId of this.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent) continue; let orderData = ent.unitAIOrderData(); if (!orderData.length && !ent.getMetadata(PlayerID, "transport")) this.assignUnit(gameState, entId); else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") { let target = gameState.getEntityById(orderData[0].target); if (target && !PETRA.allowCapture(gameState, ent, target)) ent.attack(orderData[0].target, false); } } if (this.type == "capturing") { if (this.foeEntities.length && gameState.getEntityById(this.foeEntities[0])) { // Check if we still still some capturePoints to recover // and if not, remove this foe from the list (capture army have only one foe) let capture = gameState.getEntityById(this.foeEntities[0]).capturePoints(); if (capture) for (let j = 0; j < capture.length; ++j) if (gameState.isPlayerEnemy(j) && capture[j] > 0) return []; this.removeFoe(gameState, this.foeEntities[0]); } return []; } let breakaways = []; // TODO: assign unassigned defenders, cleanup of a few things. // perhaps occasional strength recomputation // occasional update or breakaways, positions… if (gameState.ai.elapsedTime - this.positionLastUpdate > 5) { this.recalculatePosition(gameState); this.positionLastUpdate = gameState.ai.elapsedTime; // Check for breakaways. for (let i = 0; i < this.foeEntities.length; ++i) { let id = this.foeEntities[i]; let ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) { breakaways.push(id); if (this.removeFoe(gameState, id)) i--; } } this.recalculatePosition(gameState); } return breakaways; }; PETRA.DefenseArmy.prototype.Serialize = function() { return { "ID": this.ID, "type": this.type, "foePosition": this.foePosition, "positionLastUpdate": this.positionLastUpdate, "assignedAgainst": this.assignedAgainst, "assignedTo": this.assignedTo, "foeEntities": this.foeEntities, "foeStrength": this.foeStrength, "ownEntities": this.ownEntities, "ownStrength": this.ownStrength }; }; PETRA.DefenseArmy.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 23758) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 23759) @@ -1,954 +1,964 @@ PETRA.DefenseManager = function(Config) { // Array of "army" Objects. this.armies = []; this.Config = Config; this.targetList = []; this.armyMergeSize = this.Config.Defense.armyMergeSize; // Stats on how many enemies are currently attacking our allies // this.attackingArmies[enemy][ally] = number of enemy armies inside allied territory // this.attackingUnits[enemy][ally] = number of enemy units not in armies inside allied territory // this.attackedAllies[ally] = number of enemies attacking the ally this.attackingArmies = {}; this.attackingUnits = {}; this.attackedAllies = {}; }; PETRA.DefenseManager.prototype.update = function(gameState, events) { Engine.ProfileStart("Defense Manager"); this.territoryMap = gameState.ai.HQ.territoryMap; this.checkEvents(gameState, events); // Check if our potential targets are still valid. for (let i = 0; i < this.targetList.length; ++i) { let target = gameState.getEntityById(this.targetList[i]); if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) this.targetList.splice(i--, 1); } // Count the number of enemies attacking our allies in the previous turn. // We'll be more cooperative if several enemies are attacking him simultaneously. this.attackedAllies = {}; let attackingArmies = clone(this.attackingArmies); for (let enemy in this.attackingUnits) { if (!this.attackingUnits[enemy]) continue; for (let ally in this.attackingUnits[enemy]) { if (this.attackingUnits[enemy][ally] < 8) continue; if (attackingArmies[enemy] === undefined) attackingArmies[enemy] = {}; if (attackingArmies[enemy][ally] === undefined) attackingArmies[enemy][ally] = 0; attackingArmies[enemy][ally] += 1; } } for (let enemy in attackingArmies) { for (let ally in attackingArmies[enemy]) { if (this.attackedAllies[ally] === undefined) this.attackedAllies[ally] = 0; this.attackedAllies[ally] += 1; } } this.checkEnemyArmies(gameState); this.checkEnemyUnits(gameState); this.assignDefenders(gameState); Engine.ProfileStop(); }; PETRA.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default") { if (type == "default") { // Try to add it to an existing army. for (let army of this.armies) if (army.getType() == type && army.addFoe(gameState, entityID)) return; // over } // Create a new army for it. let army = new PETRA.DefenseArmy(gameState, [entityID], type); this.armies.push(army); }; PETRA.DefenseManager.prototype.getArmy = function(partOfArmy) { // Find the army corresponding to this ID partOfArmy. for (let army of this.armies) if (army.ID == partOfArmy) return army; return undefined; }; PETRA.DefenseManager.prototype.isDangerous = function(gameState, entity) { if (!entity.position()) return false; let territoryOwner = this.territoryMap.getOwner(entity.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) return false; // Check if the entity is trying to build a new base near our buildings, // and if yes, add this base in our target list. if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING") { let targetId = entity.unitAIOrderData()[0].target; if (this.targetList.indexOf(targetId) != -1) return true; let target = gameState.getEntityById(targetId); if (target) { let isTargetEnemy = gameState.isPlayerEnemy(target.owner()); if (isTargetEnemy && territoryOwner == PlayerID) { if (target.hasClass("Structure")) this.targetList.push(targetId); return true; } else if (isTargetEnemy && target.hasClass("CivCentre")) { let myBuildings = gameState.getOwnStructures(); for (let building of myBuildings.values()) { if (building.foundationProgress() == 0) continue; if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000) continue; this.targetList.push(targetId); return true; } } } } if (entity.attackTypes() === undefined || entity.hasClass("Support")) return false; let dist2Min = 6000; // TODO the 30 is to take roughly into account the structure size in following checks. Can be improved. if (entity.attackTypes().indexOf("Ranged") != -1) dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30); for (let targetId of this.targetList) { let target = gameState.getEntityById(targetId); // The enemy base is either destroyed or built. if (!target || !target.position()) continue; if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min) return true; } let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) continue; let cooperation = this.GetCooperationLevel(cc.owner()); if (cooperation < 0.3 || cooperation < 0.6 && !!cc.foundationProgress()) continue; if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) return true; } for (let building of gameState.getOwnStructures().values()) { if (building.foundationProgress() == 0 || API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) continue; if (!this.territoryMap.isBlinking(building.position()) || gameState.ai.HQ.isDefendable(building)) return true; } if (gameState.isPlayerMutualAlly(territoryOwner)) { // If ally attacked by more than 2 enemies, help him not only for cc but also for structures. if (territoryOwner != PlayerID && this.attackedAllies[territoryOwner] && this.attackedAllies[territoryOwner] > 1 && this.GetCooperationLevel(territoryOwner) > 0.7) { for (let building of gameState.getAllyStructures(territoryOwner).values()) { if (building.foundationProgress() == 0 || API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) continue; if (!this.territoryMap.isBlinking(building.position())) return true; } } // Update the number of enemies attacking this ally. let enemy = entity.owner(); if (this.attackingUnits[enemy] === undefined) this.attackingUnits[enemy] = {}; if (this.attackingUnits[enemy][territoryOwner] === undefined) this.attackingUnits[enemy][territoryOwner] = 0; this.attackingUnits[enemy][territoryOwner] += 1; } return false; }; PETRA.DefenseManager.prototype.checkEnemyUnits = function(gameState) { const nbPlayers = gameState.sharedScript.playersData.length; let i = gameState.ai.playedTurn % nbPlayers; this.attackingUnits[i] = undefined; if (i == PlayerID) { if (!this.armies.length) { // Check if we can recover capture points from any of our notdecaying structures. for (let ent of gameState.getOwnStructures().values()) { if (ent.decaying()) continue; let capture = ent.capturePoints(); if (capture === undefined) continue; let lost = 0; for (let j = 0; j < capture.length; ++j) if (gameState.isPlayerEnemy(j)) lost += capture[j]; if (lost < Math.ceil(0.25 * capture[i])) continue; this.makeIntoArmy(gameState, ent.id(), "capturing"); break; } } return; } else if (!gameState.isPlayerEnemy(i)) return; for (let ent of gameState.getEnemyUnits(i).values()) { if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; // Keep animals attacking us or our allies. if (ent.hasClass("Animal")) { if (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT") continue; let orders = ent.unitAIOrderData(); if (!orders || !orders.length || !orders[0].target) continue; let target = gameState.getEntityById(orders[0].target); if (!target || !gameState.isPlayerAlly(target.owner())) continue; } // TODO what to do for ships ? if (ent.hasClass("Ship") || ent.hasClass("Trader")) continue; // Check if unit is dangerous "a priori". if (this.isDangerous(gameState, ent)) this.makeIntoArmy(gameState, ent.id()); } if (i != 0 || this.armies.length > 1 || gameState.ai.HQ.numActiveBases() == 0) return; // Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay) // and attack it only if useful (and capturable) or dangereous. for (let ent of gameState.getEnemyStructures(i).values()) { if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; if (!ent.capturePoints() && !ent.hasDefensiveFire()) continue; let owner = this.territoryMap.getOwner(ent.position()); if (owner == PlayerID) this.makeIntoArmy(gameState, ent.id(), "capturing"); } }; PETRA.DefenseManager.prototype.checkEnemyArmies = function(gameState) { for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; // This returns a list of IDs: the units that broke away from the army for being too far. let breakaways = army.update(gameState); // Assume dangerosity. for (let breaker of breakaways) this.makeIntoArmy(gameState, breaker); if (army.getState() == 0) { if (army.getType() == "default") this.switchToAttack(gameState, army); army.clear(gameState); this.armies.splice(i--, 1); } } // Check if we can't merge it with another. for (let i = 0; i < this.armies.length - 1; ++i) { let army = this.armies[i]; if (army.getType() != "default") continue; for (let j = i+1; j < this.armies.length; ++j) { let otherArmy = this.armies[j]; if (otherArmy.getType() != "default" || API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize) continue; // No need to clear here. army.merge(gameState, otherArmy); this.armies.splice(j--, 1); } } if (gameState.ai.playedTurn % 5 != 0) return; // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base). this.attackingArmies = {}; for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; army.recalculatePosition(gameState); let owner = this.territoryMap.getOwner(army.foePosition); if (!gameState.isPlayerEnemy(owner)) { if (gameState.isPlayerMutualAlly(owner)) { // Update the number of enemies attacking this ally. for (let id of army.foeEntities) { let ent = gameState.getEntityById(id); if (!ent) continue; let enemy = ent.owner(); if (this.attackingArmies[enemy] === undefined) this.attackingArmies[enemy] = {}; if (this.attackingArmies[enemy][owner] === undefined) this.attackingArmies[enemy][owner] = 0; this.attackingArmies[enemy][owner] += 1; break; } } continue; } // Enemy army back in its territory. else if (owner != 0) { army.clear(gameState); this.armies.splice(i--, 1); continue; } // Army in neutral territory. // TODO check smaller distance with all our buildings instead of only ccs with big distance. let stillDangerous = false; let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let base of bases.values()) { if (!gameState.isEntityAlly(base)) continue; let cooperation = this.GetCooperationLevel(base.owner()); if (cooperation < 0.3 && !gameState.isEntityOwn(base)) continue; if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000) continue; if(this.Config.debug > 1) API3.warn("army in neutral territory, but still near one of our CC"); stillDangerous = true; break; } if (stillDangerous) continue; // Need to also check docks because of oversea bases. for (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values()) { if (API3.SquareVectorDistance(dock.position(), army.foePosition) > 10000) continue; stillDangerous = true; break; } if (stillDangerous) continue; if (army.getType() == "default") this.switchToAttack(gameState, army); army.clear(gameState); this.armies.splice(i--, 1); } }; PETRA.DefenseManager.prototype.assignDefenders = function(gameState) { if (!this.armies.length) return; let armiesNeeding = []; // Let's add defenders. for (let army of this.armies) { let needsDef = army.needsDefenders(gameState); if (needsDef === false) continue; let armyAccess; for (let entId of army.foeEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position()) continue; armyAccess = PETRA.getLandAccess(gameState, ent); break; } if (!armyAccess) API3.warn(" Petra error: attacking army " + army.ID + " without access"); army.recalculatePosition(gameState); armiesNeeding.push({ "army": army, "access": armyAccess, "need": needsDef }); } if (!armiesNeeding.length) return; // Let's get our potential units. let potentialDefenders = []; gameState.getOwnUnits().forEach(function(ent) { if (!ent.position()) return; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return; if (ent.hasClass("Support") || ent.attackTypes() === undefined) return; if (ent.hasClass("StoneThrower")) return; if (ent.hasClass("FishingBoat") || ent.hasClass("Trader")) return; if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return; if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id())) return; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) { let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) return; } potentialDefenders.push(ent.id()); }); for (let ipass = 0; ipass < 2; ++ipass) { // First pass only assign defenders with the right access. // Second pass assign all defenders. // TODO could sort them by distance. let backup = 0; for (let i = 0; i < potentialDefenders.length; ++i) { let ent = gameState.getEntityById(potentialDefenders[i]); if (!ent || !ent.position()) continue; let aMin; let distMin; let access = ipass == 0 ? PETRA.getLandAccess(gameState, ent) : undefined; for (let a = 0; a < armiesNeeding.length; ++a) { if (access && armiesNeeding[a].access != access) continue; + + // 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)); + })) + continue; + let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition); if (aMin !== undefined && dist > distMin) continue; aMin = a; distMin = dist; } // If outside our territory (helping an ally or attacking a cc foundation) // or if in another access, keep some troops in backup. if (backup < 12 && (aMin == undefined || distMin > 40000 && this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) != PlayerID)) { ++backup; potentialDefenders[i] = undefined; continue; } else if (aMin === undefined) continue; armiesNeeding[aMin].need -= PETRA.getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]); armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]); potentialDefenders[i] = undefined; if (armiesNeeding[aMin].need <= 0) armiesNeeding.splice(aMin, 1); if (!armiesNeeding.length) return; } } // If shortage of defenders, produce infantry garrisoned in nearest civil center. let armiesPos = []; for (let a = 0; a < armiesNeeding.length; ++a) armiesPos.push(armiesNeeding[a].army.foePosition); gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); }; PETRA.DefenseManager.prototype.abortArmy = function(gameState, army) { army.clear(gameState); for (let i = 0; i < this.armies.length; ++i) { if (this.armies[i].ID != army.ID) continue; this.armies.splice(i, 1); break; } }; /** * If our defense structures are attacked, garrison soldiers inside when possible * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress. * If our hero is attacked with regicide victory condition, the victoryManager will handle it. */ PETRA.DefenseManager.prototype.checkEvents = function(gameState, events) { // Must be called every turn for all armies. for (let army of this.armies) army.checkEvents(gameState, events); // Capture events. for (let evt of events.OwnershipChanged) { if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) { let ent = gameState.getEntityById(evt.entity); // One of our cc has been captured. if (ent && ent.hasClass("CivCentre")) gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); } } let allAttacked = {}; for (let evt of events.Attacked) allAttacked[evt.target] = evt.attacker; for (let evt of events.Attacked) { let target = gameState.getEntityById(evt.target); if (!target || !target.position()) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship") && (!target.hasClass("Structure") || target.attackRange("Ranged"))) { // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier // (enemy non-defensive structure are not considered to stay in sync with garrisonManager). if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier() && (target.owner() != 0 || !target.hasClass("Unit") || target.unitAIState() && target.unitAIState().split(".")[1] == "COMBAT")) this.garrisonUnitsInside(gameState, attacker, { "attacker": target }); } if (!gameState.isEntityOwn(target)) continue; // If attacked by one of our allies (he must trying to recover capture points), do not react. if (attacker && gameState.isEntityAlly(attacker)) continue; if (attacker && attacker.position() && target.hasClass("FishingBoat")) { let unitAIState = target.unitAIState(); let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; if (target.isIdle() || unitAIStateOrder == "GATHER") { let pos = attacker.position(); let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; if (range * range > API3.SquareVectorDistance(pos, target.position())) target.moveToRange(pos[0], pos[1], range, range); } continue; } // TODO integrate other ships later, need to be sure it is accessible. if (target.hasClass("Ship")) continue; // If a building on a blinking tile is attacked, check if it can be defended. // Same thing for a building in an isolated base (not connected to a base with anchor). if (target.hasClass("Structure")) { let base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base")); if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) || !base || gameState.ai.HQ.baseManagers.every(b => !b.anchor || b.accessIndex != base.accessIndex)) { let capture = target.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio > 0.50 && captureRatio < 0.70) target.destroy(); continue; } } // If inside a started attack plan, let the plan deal with this unit. let plan = target.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = gameState.ai.HQ.attackManager.getPlan(plan); if (attack && attack.state != "unexecuted") continue; } // Signal this attacker to our defense manager, except if we are in enemy territory. // TODO treat ship attack. if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined && !attacker.hasClass("Structure") && !attacker.hasClass("Ship")) { let territoryOwner = this.territoryMap.getOwner(attacker.position()); if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) this.makeIntoArmy(gameState, attacker.id()); } if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) { let army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); if (army.getType() == "capturing") { let abort = false; // If one of the units trying to capture a structure is attacked, // abort the army so that the unit can defend itself if (army.ownEntities.indexOf(target.id()) != -1) abort = true; else if (army.foeEntities[0] == target.id() && target.owner() == PlayerID) { // else we may be trying to regain some capture point from one of our structure. abort = true; let capture = target.capturePoints(); for (let j = 0; j < capture.length; ++j) { if (!gameState.isPlayerEnemy(j) || capture[j] == 0) continue; abort = false; break; } } if (abort) this.abortArmy(gameState, army); } continue; } // Try to garrison any attacked support unit if low health. if (target.hasClass("Support") && target.healthLevel() < this.Config.garrisonHealthLevel.medium && !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) { this.garrisonAttackedUnit(gameState, target); continue; } // Try to garrison any attacked stone thrower. if (target.hasClass("StoneThrower") && !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) { this.garrisonSiegeUnit(gameState, target); continue; } if (!attacker || !attacker.position()) continue; if (target.isGarrisonHolder() && target.getArrowMultiplier()) this.garrisonUnitsInside(gameState, target, { "attacker": attacker }); if (target.hasClass("Unit") && attacker.hasClass("Unit")) { // Consider whether we should retaliate or continue our task. if (target.hasClass("Support") || target.attackTypes() === undefined) continue; let orderData = target.unitAIOrderData(); let currentTarget = orderData && orderData.length && orderData[0].target ? gameState.getEntityById(orderData[0].target) : undefined; if (currentTarget) { let unitAIState = target.unitAIState(); let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() || !currentTarget.hasClass("Structure") && !currentTarget.hasClass("Support"))) continue; if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire()) continue; if (unitAIStateOrder == "COMBAT" && !PETRA.isSiegeUnit(currentTarget) && gameState.ai.HQ.capturableTargets.has(orderData[0].target)) { // Take the nearest unit also attacking this structure to help us. let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); let minDist; let minEnt; let pos = attacker.position(); capturableTarget.ents.delete(target.id()); for (let entId of capturableTarget.ents) { if (allAttacked[entId]) continue; let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) + if (!ent || !ent.position() || !ent.canAttackTarget(attacker, PETRA.allowCapture(gameState, ent, attacker))) continue; // Check that the unit is still attacking the structure (since the last played turn). let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") continue; let entOrderData = ent.unitAIOrderData(); if (!entOrderData || !entOrderData.length || !entOrderData[0].target || entOrderData[0].target != orderData[0].target) continue; let dist = API3.SquareVectorDistance(pos, ent.position()); if (minEnt && dist > minDist) continue; minDist = dist; minEnt = ent; } if (minEnt) { capturableTarget.ents.delete(minEnt.id()); minEnt.attack(attacker.id(), PETRA.allowCapture(gameState, minEnt, attacker)); } } } - target.attack(attacker.id(), PETRA.allowCapture(gameState, target, attacker)); + let allowCapture = PETRA.allowCapture(gameState, target, attacker); + if (target.canAttackTarget(attacker, allowCapture)) + target.attack(attacker.id(), allowCapture); } } }; PETRA.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) { if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) return false; let minGarrison = data.min || target.garrisonMax(); if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) return false; if (data.attacker) { let attackTypes = target.attackTypes(); if (!attackTypes || attackTypes.indexOf("Ranged") == -1) return false; let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); let range = target.attackRange("Ranged").max; if (dist >= range*range) return false; } let access = PETRA.getLandAccess(gameState, target); let garrisonManager = gameState.ai.HQ.garrisonManager; let garrisonArrowClasses = target.getGarrisonArrowClasses(); let typeGarrison = data.type || "protection"; let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target); if (allowMelee === undefined) { // Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units. if (data.attacker) allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : !PETRA.isSiegeUnit(data.attacker); else allowMelee = true; } let units = gameState.getOwnUnits().filter(ent => { if (!ent.position()) return false; if (!MatchesClassList(ent.classes(), garrisonArrowClasses)) return false; if (typeGarrison != "decay" && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) return false; if (ent.getMetadata(PlayerID, "transport") !== undefined) return false; let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; if (!army && (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let subrole = ent.getMetadata(PlayerID, "subrole"); // When structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan. if (typeGarrison != "decay" && subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) return false; } if (PETRA.getLandAccess(gameState, ent) != access) return false; return true; }).filterNearest(target.position()); let ret = false; for (let ent of units.values()) { if (garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) break; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); if (attackPlan) attackPlan.removeUnit(ent, true); } let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; if (army) army.removeOwn(gameState, ent.id()); garrisonManager.garrison(gameState, ent, target, typeGarrison); ret = true; } return ret; }; /** Garrison a attacked siege ranged unit inside the nearest fortress. */ PETRA.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) { let distmin = Math.min(); let nearest; let unitAccess = PETRA.getLandAccess(gameState, unit); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!ent.isGarrisonHolder()) continue; if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax()) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (PETRA.getLandAccess(gameState, ent) != unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (nearest) garrisonManager.garrison(gameState, unit, nearest, "protection"); return nearest !== undefined; }; /** * Garrison a hurt unit inside a player-owned or allied structure. * If emergency is true, the unit will be garrisoned in the closest possible structure. * Otherwise, it will garrison in the closest healing structure. */ PETRA.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) { let distmin = Math.min(); let nearest; let unitAccess = PETRA.getLandAccess(gameState, unit); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!ent.isGarrisonHolder()) continue; if (!emergency && !ent.buffHeal()) continue; if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax() && (!emergency || !ent.garrisoned().length)) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (PETRA.getLandAccess(gameState, ent) != unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (!nearest) return false; if (!emergency) { garrisonManager.garrison(gameState, unit, nearest, "protection"); return true; } if (garrisonManager.numberOfGarrisonedUnits(nearest) >= nearest.garrisonMax()) // make room for this ent nearest.unload(nearest.garrisoned()[0]); garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); return true; }; /** * Be more inclined to help an ally attacked by several enemies. */ PETRA.DefenseManager.prototype.GetCooperationLevel = function(ally) { let cooperation = this.Config.personality.cooperative; if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1) cooperation += 0.2 * (this.attackedAllies[ally] - 1); return cooperation; }; /** * Switch a defense army into an attack if needed. */ PETRA.DefenseManager.prototype.switchToAttack = function(gameState, army) { if (!army) return; for (let targetId of this.targetList) { let target = gameState.getEntityById(targetId); if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) continue; let targetAccess = PETRA.getLandAccess(gameState, target); let targetPos = target.position(); for (let entId of army.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position() || PETRA.getLandAccess(gameState, ent) != targetAccess) continue; if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400) continue; gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); return; } } }; PETRA.DefenseManager.prototype.Serialize = function() { let properties = { "targetList": this.targetList, "armyMergeSize": this.armyMergeSize, "attackingUnits": this.attackingUnits, "attackingArmies": this.attackingArmies, "attackedAllies": this.attackedAllies }; let armies = []; for (let army of this.armies) armies.push(army.Serialize()); return { "properties": properties, "armies": armies }; }; PETRA.DefenseManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.armies = []; for (let dataArmy of data.armies) { let army = new PETRA.DefenseArmy(gameState, []); army.Deserialize(dataArmy); this.armies.push(army); } };