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 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 25584) @@ -1,1030 +1,1021 @@ 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, adjusting for tech. "get": function(string) { 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 value = this._template; let args = string.split("/"); for (let arg of args) { value = value[arg]; if (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"); }, "matchLimit": function() { if (!this.get("TrainingRestrictions")) return undefined; return this.get("TrainingRestrictions/MatchLimit"); }, "classes": function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, "hasClass": function(name) { if (!this._classes) this._classes = this.classes(); return this._classes && this._classes.indexOf(name) != -1; }, "hasClasses": function(array) { if (!this._classes) this._classes = this.classes(); - if (!this._classes) - return false; - - for (let cls of array) - if (this._classes.indexOf(cls) == -1) - return false; - return true; + return this._classes && MatchesClassList(this._classes, array); }, "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 {}; 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 0; 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() { if (!this.get("Population")) return 0; return +this.get("Population/Bonus"); }, "resistanceStrengths": function() { let resistanceTypes = this.get("Resistance"); if (!resistanceTypes || !resistanceTypes.Entity) return undefined; let resistance = {}; if (resistanceTypes.Entity.Capture) resistance.Capture = +this.get("Resistance/Entity/Capture"); if (resistanceTypes.Entity.Damage) { resistance.Damage = {}; for (let damageType in resistanceTypes.Entity.Damage) resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType); } // ToDo: Resistance to StatusEffects. return resistance; }, "attackTypes": function() { let attack = this.get("Attack"); if (!attack) return undefined; let ret = []; for (let type in 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() { let attack = this.get("Attack"); if (!attack) return undefined; let Classes = []; for (let type in 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. + // returns true if the entity counters the target entity. // TODO: refine using the multiplier - "countersClasses": function(classes) { + "counters": function(target) { let attack = this.get("Attack"); if (!attack) return false; let mcounter = []; for (let type in 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; + return target.hasClasses(mcounter); }, // returns, if it exists, the multiplier from each attack against a given class "getMultiplierAgainst": function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; let bonuses = this.get("Attack/" + type + "/Bonuses"); if (bonuses) { for (let b in 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 }; }, "getResourceType": function() { if (!this.get("ResourceSupply")) return undefined; return this.get("ResourceSupply/Type").split('.')[0]; }, "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }, "resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); }, "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+/) : []; }, "isResourceDropsite": function(resourceType) { const types = this.resourceDropsiteTypes(); return types && (!resourceType || types.indexOf(resourceType) !== -1); }, "isTreasure": function() { return this.get("Treasure") !== undefined; }, "treasureResources": function() { if (!this.get("Treasure")) return undefined; let ret = {}; for (let r in this.get("Treasure/Resources")) ret[r] = +this.get("Treasure/Resources/" + r); return ret; }, "garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); }, "garrisonMax": function() { return this.get("GarrisonHolder/Max"); }, "garrisonSize": function() { return this.get("Garrisonable/Size"); }, "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; }, "isHuntable": function() { // Do not hunt retaliating animals (dead animals can be used). // Assume entities which can attack, will attack. return this.get("ResourceSupply/KillBeforeGather") && (!this.get("Health") || !this.get("Attack")); }, "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")) return undefined; let territory = this.get("BuildRestrictions/Territory"); return !territory ? undefined : 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; }, "isTurretHolder": function() { return this.get("TurretHolder") !== 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); + return !restrictedClasses || !target.hasClasses(restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return "Garrisonable" in this._template; }, "canOccupyTurret": function() { return "Turretable" in this._template; }, "isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; }, }); // 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() { return this._entity.idle; }, "getStance": function() { return this._entity.stance; }, "unitAIState": function() { return this._entity.unitAIState; }, "unitAIOrderData": function() { return this._entity.unitAIOrderData; }, "hitpoints": function() { return this._entity.hitpoints; }, "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; }, "capturePoints": function() {return this._entity.capturePoints; }, "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() { return this._entity.trainingQueue; }, "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() { 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() { return this._entity.resourceSupplyAmount; }, "resourceSupplyNumGatherers": function() { return this._entity.resourceSupplyNumGatherers; }, "isFull": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, "resourceCarrying": function() { 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") { 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; 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; }, "garrisonHolderID": function() { return this._entity.garrisonHolderID; }, "garrisoned": function() { return this._entity.garrisoned; }, "garrisonedSlots": function() { let count = 0; if (this._entity.garrisoned) for (let ent of this._entity.garrisoned) count += +this._ai._entities.get(ent).garrisonSize(); return count; }, "canGarrisonInside": function() { return this.garrisonedSlots() < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { let attack = this.get("Attack"); if (!attack) return false; for (let type in 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 health = target.get("Health"); if (!health) 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)) + if (!restrictedClasses || !target.hasClasses(restrictedClasses)) return true; } return false; }, "move": function(x, z, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront }); return this; }, "moveToRange": function(x, z, min, max, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront }); return this; }, "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront }); return this; }, // violent, aggressive, defensive, passive, standground "setStance": function(stance, queued = false, pushFront = false) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance, "queued": queued, "pushFront": pushFront }); return this; }, "stopMoving": function() { Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": 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, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "occupy-turret": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "attack": function(unitId, allowCapture = true, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront }); return this; }, "collectTreasure": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "collect-treasure", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); 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, "pushFront": 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, "pushFront": false }); } return this; }, "gather": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "repair": function(target, autocontinue = false, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront }); return this; }, "returnResources": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); 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, "pushFront": 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, "pushFront": 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, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); 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 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js (revision 25584) @@ -1,169 +1,164 @@ 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)), + "byClasses": clsList => ({ + "func": ent => ent.hasClasses(clsList), "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.isResourceDropsite(resourceType), "dynamicProperties": [] }), "isTreasure": () => ({ "func": ent => { if (!ent.isTreasure()) return false; // Don't go for floating treasures since we might not be able // to reach them and that kills the pathfinder. let template = ent.templateName(); return template != "gaia/treasure/shipwreck_debris" && template != "gaia/treasure/shipwreck"; }, "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; 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/common-api/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 25584) @@ -1,943 +1,941 @@ var API3 = function(m) { /** * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ m.GameState = function() { this.ai = null; // must be updated by the AIs. }; m.GameState.prototype.init = function(SharedScript, state, player) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.timeElapsed = SharedScript.timeElapsed; this.circularMap = SharedScript.circularMap; this.templates = SharedScript._templates; this.entities = SharedScript.entities; this.player = player; this.playerData = SharedScript.playersData[this.player]; this.victoryConditions = SharedScript.victoryConditions; this.alliedVictory = SharedScript.alliedVictory; this.ceasefireActive = SharedScript.ceasefireActive; this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; // get the list of possible phases for this civ: // we assume all of them are researchable from the civil center this.phases = []; let cctemplate = this.getTemplate(this.applyCiv("structures/{civ}/civil_centre")); if (!cctemplate) return; let civ = this.getPlayerCiv(); let techs = cctemplate.researchableTechs(this, civ); let phaseData = {}; let phaseMap = {}; for (let techName of techs) { if (!techName.startsWith("phase")) continue; let techData = this.getTemplate(techName); if (techData._definesPair) { // Randomly pick a non-disabled choice from the phase-pair. techName = pickRandom([techData._template.top, techData._template.bottom].filter(tech => !this.playerData.disabledTechnologies[tech])) || techData._template.top; let supersedes = techData._template.supersedes; techData = clone(this.getTemplate(techName)); if (supersedes) techData._template.supersedes = supersedes; } phaseData[techName] = GetTechnologyBasicDataHelper(techData._template, civ); if (phaseData[techName].replaces) phaseMap[phaseData[techName].replaces[0]] = techName; } this.phases = UnravelPhases(phaseData).map(phaseName => ({ "name": phaseMap[phaseName] || phaseName, "requirements": phaseMap[phaseName] ? phaseData[phaseMap[phaseName]].reqs : [] })); }; m.GameState.prototype.update = function(SharedScript) { this.timeElapsed = SharedScript.timeElapsed; this.playerData = SharedScript.playersData[this.player]; this.ceasefireActive = SharedScript.ceasefireActive; this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; }; m.GameState.prototype.updatingCollection = function(id, filter, parentCollection) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID return this.updatingGlobalCollection(gid, filter, parentCollection); }; m.GameState.prototype.destroyCollection = function(id) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID this.destroyGlobalCollection(gid); }; m.GameState.prototype.updatingGlobalCollection = function(gid, filter, parentCollection) { if (this.EntCollecNames.has(gid)) return this.EntCollecNames.get(gid); let collection = parentCollection ? parentCollection.filter(filter) : this.entities.filter(filter); collection.registerUpdates(); this.EntCollecNames.set(gid, collection); return collection; }; m.GameState.prototype.destroyGlobalCollection = function(gid) { if (!this.EntCollecNames.has(gid)) return; this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(gid)); this.EntCollecNames.delete(gid); }; /** * Reset the entities collections which depend on diplomacy */ m.GameState.prototype.resetOnDiplomacyChanged = function() { for (let name of this.EntCollecNames.keys()) if (name.startsWith("player-" + this.player + "-diplo")) this.destroyGlobalCollection(name); }; m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; m.GameState.prototype.getBarterPrices = function() { return this.playerData.barterPrices; }; m.GameState.prototype.getVictoryConditions = function() { return this.victoryConditions; }; m.GameState.prototype.getAlliedVictory = function() { return this.alliedVictory; }; m.GameState.prototype.isCeasefireActive = function() { return this.ceasefireActive; }; m.GameState.prototype.getTemplate = function(type) { if (TechnologyTemplates.Has(type)) return new m.Technology(type); if (this.templates[type] === undefined) this.sharedScript.GetTemplate(type); return this.templates[type] ? new m.Template(this.sharedScript, type, this.templates[type]) : null; }; /** Return the template of the structure built from this foundation */ m.GameState.prototype.getBuiltTemplate = function(foundationName) { if (!foundationName.startsWith("foundation|")) { warn("Foundation " + foundationName + " not recognised as a foundation."); return null; } return this.getTemplate(foundationName.substr(11)); }; m.GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; m.GameState.prototype.getPlayerCiv = function(player) { return player !== undefined ? this.sharedScript.playersData[player].civ : this.playerData.civ; }; m.GameState.prototype.currentPhase = function() { for (let i = this.phases.length; i > 0; --i) if (this.isResearched(this.phases[i-1].name)) return i; return 0; }; m.GameState.prototype.getNumberOfPhases = function() { return this.phases.length; }; m.GameState.prototype.getPhaseName = function(i) { return this.phases[i-1] ? this.phases[i-1].name : undefined; }; m.GameState.prototype.getPhaseEntityRequirements = function(i) { let entityReqs = []; for (let requirement of this.phases[i-1].requirements) { if (!requirement.entities) continue; for (let entity of requirement.entities) if (entity.check == "count") entityReqs.push({ "class": entity.class, "count": entity.number }); } return entityReqs; }; m.GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs.has(template); }; /** true if started or queued */ m.GameState.prototype.isResearching = function(template) { return this.playerData.researchStarted.has(template) || this.playerData.researchQueued.has(template); }; /** this is an "in-absolute" check that doesn't check if we have a building to research from. */ m.GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) { if (this.playerData.disabledTechnologies[techTemplateName]) return false; let template = this.getTemplate(techTemplateName); if (!template) return false; // researching or already researched: NOO. if (this.playerData.researchQueued.has(techTemplateName) || this.playerData.researchStarted.has(techTemplateName) || this.playerData.researchedTechs.has(techTemplateName)) return false; if (noRequirementCheck) return true; // if this is a pair, we must check that the pair tech is not being researched if (template.pair()) { let other = template.pairedWith(); if (this.playerData.researchQueued.has(other) || this.playerData.researchStarted.has(other) || this.playerData.researchedTechs.has(other)) return false; } return this.checkTechRequirements(template.requirements(this.playerData.civ)); }; /** * Private function for checking a set of requirements is met. * Basically copies TechnologyManager, but compares against * variables only available within the AI */ m.GameState.prototype.checkTechRequirements = function(reqs) { if (!reqs) return false; if (!reqs.length) return true; function doesEntitySpecPass(entity) { switch (entity.check) { case "count": if (!this.playerData.classCounts[entity.class] || this.playerData.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.playerData.typeCountsByClass[entity.class] || Object.keys(this.playerData.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; } return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(tech => this.playerData.researchedTechs.has(tech)); case "entities": return req[type].every(doesEntitySpecPass, this); } return false; }); }); }; m.GameState.prototype.getPassabilityMap = function() { return this.sharedScript.passabilityMap; }; m.GameState.prototype.getPassabilityClassMask = function(name) { if (!this.sharedScript.passabilityClasses[name]) error("Tried to use invalid passability class name '" + name + "'"); return this.sharedScript.passabilityClasses[name]; }; m.GameState.prototype.getResources = function() { return new m.Resources(this.playerData.resourceCounts); }; m.GameState.prototype.getPopulation = function() { return this.playerData.popCount; }; m.GameState.prototype.getPopulationLimit = function() { return this.playerData.popLimit; }; m.GameState.prototype.getPopulationMax = function() { return this.playerData.popMax; }; m.GameState.prototype.getPlayerID = function() { return this.player; }; m.GameState.prototype.hasAllies = function() { for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasEnemies = function() { for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i] && +i !== 0 && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasNeutrals = function() { for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i] && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.isPlayerNeutral = function(id) { return this.playerData.isNeutral[id]; }; m.GameState.prototype.isPlayerAlly = function(id) { return this.playerData.isAlly[id]; }; m.GameState.prototype.isPlayerMutualAlly = function(id) { return this.playerData.isMutualAlly[id]; }; m.GameState.prototype.isPlayerEnemy = function(id) { return this.playerData.isEnemy[id]; }; /** Return the number of players currently enemies, not including gaia */ m.GameState.prototype.getNumPlayerEnemies = function() { let num = 0; for (let i = 1; i < this.playerData.isEnemy.length; ++i) if (this.playerData.isEnemy[i] && this.sharedScript.playersData[i].state != "defeated") ++num; return num; }; m.GameState.prototype.getEnemies = function() { let ret = []; for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i]) ret.push(+i); return ret; }; m.GameState.prototype.getNeutrals = function() { let ret = []; for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i]) ret.push(+i); return ret; }; m.GameState.prototype.getAllies = function() { let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i]) ret.push(+i); return ret; }; m.GameState.prototype.getExclusiveAllies = function() { // Player is not included let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player) ret.push(+i); return ret; }; m.GameState.prototype.getMutualAllies = function() { let ret = []; for (let i in this.playerData.isMutualAlly) if (this.playerData.isMutualAlly[i] && this.sharedScript.playersData[i].isMutualAlly[this.player]) ret.push(+i); return ret; }; m.GameState.prototype.isEntityAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()]; }; m.GameState.prototype.isEntityExclusiveAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; }; m.GameState.prototype.isEntityEnemy = function(ent) { if (!ent) return false; return this.playerData.isEnemy[ent.owner()]; }; m.GameState.prototype.isEntityOwn = function(ent) { if (!ent) return false; return ent.owner() === this.player; }; m.GameState.prototype.getEntityById = function(id) { return this.entities._entities.get(+id); }; m.GameState.prototype.getEntities = function(id) { if (id === undefined) return this.entities; return this.updatingGlobalCollection("player-" + id + "-entities", m.Filters.byOwner(id)); }; m.GameState.prototype.getStructures = function() { return this.updatingGlobalCollection("structures", m.Filters.byClass("Structure"), this.entities); }; m.GameState.prototype.getOwnEntities = function() { return this.updatingGlobalCollection("player-" + this.player + "-entities", m.Filters.byOwner(this.player)); }; m.GameState.prototype.getOwnStructures = function() { return this.updatingGlobalCollection("player-" + this.player + "-structures", m.Filters.byClass("Structure"), this.getOwnEntities()); }; m.GameState.prototype.getOwnUnits = function() { return this.updatingGlobalCollection("player-" + this.player + "-units", m.Filters.byClass("Unit"), this.getOwnEntities()); }; m.GameState.prototype.getAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getAllies())); }; m.GameState.prototype.getExclusiveAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getExclusiveAllies())); }; m.GameState.prototype.getAllyStructures = function(allyID) { if (allyID == undefined) return this.updatingCollection("diplo-ally-structures", m.Filters.byOwners(this.getAllies()), this.getStructures()); return this.updatingGlobalCollection("player-" + allyID + "-structures", m.Filters.byOwner(allyID), this.getStructures()); }; m.GameState.prototype.getNeutralStructures = function() { return this.getStructures().filter(m.Filters.byOwners(this.getNeutrals())); }; m.GameState.prototype.getEnemyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getEnemies())); }; m.GameState.prototype.getEnemyStructures = function(enemyID) { if (enemyID === undefined) return this.updatingCollection("diplo-enemy-structures", m.Filters.byOwners(this.getEnemies()), this.getStructures()); return this.updatingGlobalCollection("player-" + enemyID + "-structures", m.Filters.byOwner(enemyID), this.getStructures()); }; m.GameState.prototype.getEnemyUnits = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Unit")); return this.updatingGlobalCollection("player-" + enemyID + "-units", m.Filters.byClass("Unit"), this.getEntities(enemyID)); }; /** if maintain is true, this will be stored. Otherwise it's one-shot. */ m.GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain) { if (maintain) return this.updatingCollection(key + "-" + value, m.Filters.byMetadata(this.player, key, value), this.getOwnEntities()); return this.getOwnEntities().filter(m.Filters.byMetadata(this.player, key, value)); }; m.GameState.prototype.getOwnEntitiesByRole = function(role, maintain) { return this.getOwnEntitiesByMetadata("role", role, maintain); }; m.GameState.prototype.getOwnEntitiesByType = function(type, maintain) { let filter = m.Filters.byType(type); if (maintain) return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnEntitiesByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("class-" + cls, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnFoundationsByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("foundations-class-" + cls, filter, this.getOwnFoundations()); return this.getOwnFoundations().filter(filter); }; m.GameState.prototype.getOwnTrainingFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities()); }; m.GameState.prototype.getOwnResearchFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-research-facilities", m.Filters.byResearchAvailable(this, this.playerData.civ), this.getOwnEntities()); }; m.GameState.prototype.countEntitiesByType = function(type, maintain) { return this.getOwnEntitiesByType(type, maintain).length; }; m.GameState.prototype.countEntitiesAndQueuedByType = function(type, maintain) { let template = this.getTemplate(type); if (!template) return 0; let count = this.countEntitiesByType(type, maintain); // Count building foundations if (template.hasClass("Structure") === true) count += this.countFoundationsByType(type, true); else if (template.resourceSupplyType() !== undefined) // animal resources count += this.countEntitiesByType("resource|" + type, true); else { // Count entities in building production queues // TODO: maybe this fails for corrals. this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.unitTemplate == type) count += item.count; }); } return count; }; m.GameState.prototype.countFoundationsByType = function(type, maintain) { let foundationType = "foundation|" + type; if (maintain) return this.updatingCollection("foundation-type-" + type, m.Filters.byType(foundationType), this.getOwnFoundations()).length; let count = 0; this.getOwnStructures().forEach(function(ent) { if (ent.templateName() == foundationType) ++count; }); return count; }; m.GameState.prototype.countOwnEntitiesByRole = function(role) { return this.getOwnEntitiesByRole(role, "true").length; }; m.GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { let count = this.countOwnEntitiesByRole(role); // Count entities in building production queues this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata.role && item.metadata.role == role) count += item.count; }); return count; }; m.GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { // Count entities in building production queues let count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); return count; }; m.GameState.prototype.getOwnFoundations = function() { return this.updatingGlobalCollection("player-" + this.player + "-foundations", m.Filters.isFoundation(), this.getOwnStructures()); }; m.GameState.prototype.getOwnDropsites = function(resource) { if (resource) return this.updatingCollection("ownDropsite-" + resource, m.Filters.isDropsite(resource), this.getOwnEntities()); return this.updatingCollection("ownDropsite-all", m.Filters.isDropsite(), this.getOwnEntities()); }; m.GameState.prototype.getAnyDropsites = function(resource) { if (resource) return this.updatingGlobalCollection("anyDropsite-" + resource, m.Filters.isDropsite(resource), this.getEntities()); return this.updatingGlobalCollection("anyDropsite-all", m.Filters.isDropsite(), this.getEntities()); }; m.GameState.prototype.getResourceSupplies = function(resource) { return this.updatingGlobalCollection("resource-" + resource, m.Filters.byResource(resource), this.getEntities()); }; m.GameState.prototype.getHuntableSupplies = function() { return this.updatingGlobalCollection("resource-hunt", m.Filters.isHuntable(), this.getEntities()); }; m.GameState.prototype.getFishableSupplies = function() { return this.updatingGlobalCollection("resource-fish", m.Filters.isFishable(), this.getEntities()); }; /** This returns only units from buildings. */ m.GameState.prototype.findTrainableUnits = function(classes, anticlasses) { let allTrainable = []; let civ = this.playerData.civ; this.getOwnTrainingFacilities().forEach(function(ent) { let trainable = ent.trainableEntities(civ); if (!trainable) return; for (let unit of trainable) if (allTrainable.indexOf(unit) === -1) allTrainable.push(unit); }); let ret = []; let limits = this.getEntityLimits(); let current = this.getEntityCounts(); let matchCounts = this.getEntityMatchCounts(); for (let trainable of allTrainable) { if (this.isTemplateDisabled(trainable)) continue; let template = this.getTemplate(trainable); if (!template || !template.available(this)) continue; let limit = template.matchLimit(); if (matchCounts && limit && matchCounts[trainable] >= limit) continue; - if (classes.some(c => !template.hasClass(c))) - continue; - if (anticlasses.some(c => template.hasClass(c))) + if (!template.hasClasses(classes) || template.hasClasses(anticlasses)) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; ret.push([trainable, template]); } return ret; }; /** * Return all techs which can currently be researched * Does not factor cost. * If there are pairs, both techs are returned. */ m.GameState.prototype.findAvailableTech = function() { let allResearchable = []; let civ = this.playerData.civ; for (let ent of this.getOwnEntities().values()) { let searchable = ent.researchableTechs(this, civ); if (!searchable) continue; for (let tech of searchable) if (!this.playerData.disabledTechnologies[tech] && allResearchable.indexOf(tech) === -1) allResearchable.push(tech); } let ret = []; for (let tech of allResearchable) { let template = this.getTemplate(tech); if (template.pairDef()) { let techs = template.getPairedTechs(); if (this.canResearch(techs[0]._templateName)) ret.push([techs[0]._templateName, techs[0]]); if (this.canResearch(techs[1]._templateName)) ret.push([techs[1]._templateName, techs[1]]); } else if (this.canResearch(tech)) { // Phases are treated separately if (this.phases.every(phase => template._templateName != phase.name)) ret.push([tech, template]); } } return ret; }; /** * Return true if we have a building able to train that template */ m.GameState.prototype.hasTrainer = function(template) { let civ = this.playerData.civ; for (let ent of this.getOwnTrainingFacilities().values()) { let trainable = ent.trainableEntities(civ); if (trainable && trainable.indexOf(template) !== -1) return true; } return false; }; /** * Find buildings able to train that template. */ m.GameState.prototype.findTrainers = function(template) { let civ = this.playerData.civ; return this.getOwnTrainingFacilities().filter(function(ent) { let trainable = ent.trainableEntities(civ); return trainable && trainable.indexOf(template) !== -1; }); }; /** * Get any unit that is capable of constructing the given building type. */ m.GameState.prototype.findBuilder = function(template) { let civ = this.getPlayerCiv(); for (let ent of this.getOwnUnits().values()) { let buildable = ent.buildableEntities(civ); if (buildable && buildable.indexOf(template) !== -1) return ent; } return undefined; }; /** Return true if one of our buildings is capable of researching the given tech */ m.GameState.prototype.hasResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return false; let template = this.getTemplate(templateName); if (template.autoResearch) return true; let civ = this.playerData.civ; for (let ent of this.getOwnResearchFacilities().values()) { let techs = ent.researchableTechs(this, civ); for (let tech of techs) { let temp = this.getTemplate(tech); if (temp.pairDef()) { let pairedTechs = temp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } } return false; }; /** Find buildings that are capable of researching the given tech */ m.GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return undefined; let self = this; let civ = this.playerData.civ; return this.getOwnResearchFacilities().filter(function(ent) { let techs = ent.researchableTechs(self, civ); for (let tech of techs) { let thisTemp = self.getTemplate(tech); if (thisTemp.pairDef()) { let pairedTechs = thisTemp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } return false; }); }; m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; m.GameState.prototype.getEntityMatchCounts = function() { return this.playerData.matchEntityCounts; }; m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; m.GameState.prototype.isTemplateAvailable = function(templateName) { if (this.templates[templateName] === undefined) this.sharedScript.GetTemplate(templateName); return this.templates[templateName] && !this.isTemplateDisabled(templateName); }; m.GameState.prototype.isTemplateDisabled = function(templateName) { if (!this.playerData.disabledTemplates[templateName]) return false; return this.playerData.disabledTemplates[templateName]; }; /** Checks whether the maximum number of buildings have been constructed for a certain catergory */ m.GameState.prototype.isEntityLimitReached = function(category) { if (this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined) return false; return this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]; }; m.GameState.prototype.getTraderTemplatesGains = function() { let shipMechantTemplateName = this.applyCiv("units/{civ}/ship_merchant"); let supportTraderTemplateName = this.applyCiv("units/{civ}/support_trader"); let shipMerchantTemplate = !this.isTemplateDisabled(shipMechantTemplateName) && this.getTemplate(shipMechantTemplateName); let supportTraderTemplate = !this.isTemplateDisabled(supportTraderTemplateName) && this.getTemplate(supportTraderTemplateName); let norm = TradeGainNormalization(this.sharedScript.mapSize); let ret = {}; if (supportTraderTemplate) ret.landGainMultiplier = norm * supportTraderTemplate.gainMultiplier(); if (shipMerchantTemplate) ret.navalGainMultiplier = norm * shipMerchantTemplate.gainMultiplier(); return ret; }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 25584) @@ -1,155 +1,138 @@ LoadModificationTemplates(); var API3 = function(m) { /** Wrapper around a technology template */ m.Technology = function(templateName) { this._templateName = templateName; let template = TechnologyTemplates.Get(templateName); // check if this is one of two paired technologies. this._isPair = template.pair !== undefined; if (this._isPair) { let pairTech = TechnologyTemplates.Get(template.pair); this._pairedWith = pairTech.top == templateName ? pairTech.bottom : pairTech.top; } // check if it only defines a pair: this._definesPair = template.top !== undefined; this._template = template; }; /** returns generic, or specific if civ provided. */ m.Technology.prototype.name = function(civ) { if (civ === undefined) return this._template.genericName; if (this._template.specificName === undefined || this._template.specificName[civ] === undefined) return undefined; return this._template.specificName[civ]; }; m.Technology.prototype.pairDef = function() { return this._definesPair; }; /** in case this defines a pair only, returns the two paired technologies. */ m.Technology.prototype.getPairedTechs = function() { if (!this._definesPair) return undefined; return [ new m.Technology(this._template.top), new m.Technology(this._template.bottom) ]; }; m.Technology.prototype.pair = function() { if (!this._isPair) return undefined; return this._template.pair; }; m.Technology.prototype.pairedWith = function() { if (!this._isPair) return undefined; return this._pairedWith; }; m.Technology.prototype.cost = function(productionQueue) { if (!this._template.cost) return undefined; let cost = {}; for (let type in this._template.cost) { cost[type] = +this._template.cost[type]; if (productionQueue) cost[type] *= productionQueue.techCostMultiplier(type); } return cost; }; m.Technology.prototype.costSum = function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return 0; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }; m.Technology.prototype.researchTime = function() { return this._template.researchTime || 0; }; m.Technology.prototype.requirements = function(civ) { return DeriveTechnologyRequirements(this._template, civ); }; m.Technology.prototype.autoResearch = function() { if (!this._template.autoResearch) return undefined; return this._template.autoResearch; }; m.Technology.prototype.supersedes = function() { if (!this._template.supersedes) return undefined; return this._template.supersedes; }; m.Technology.prototype.modifications = function() { if (!this._template.modifications) return undefined; return this._template.modifications; }; m.Technology.prototype.affects = function() { if (!this._template.affects) return undefined; return this._template.affects; }; m.Technology.prototype.isAffected = function(classes) { - if (!this._template.affects) - return false; - - for (let affect of this._template.affects) - { - let reqClasses = affect.split(" "); - let fitting = true; - for (let reqClass of reqClasses) - { - if (classes.indexOf(reqClass) !== -1) - continue; - fitting = false; - break; - } - if (fitting === true) - return true; - } - return false; + return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect)); }; 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 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 25584) @@ -1,803 +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()); + const bombers = gameState.updatingCollection("bombers", API3.Filters.byClasses(["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("WallTower") || ent.hasClass("Fortress")) + if (ent.hasClasses(["Tower", "WallTower", "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 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 25584) @@ -1,2173 +1,2172 @@ /** * 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.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving", "CitizenSoldier"], + this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+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.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving", "CitizenSoldier"], + this.unitStat.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+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"], + 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"], + 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"], + 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"], + this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Melee+Champion"], "interests": [["strength", 3]] }; - this.unitStat.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving", "Ranged", "CitizenSoldier"], + this.unitStat.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Ranged+CitizenSoldier"], "interests": [["strength", 2]] }; - this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving", "Melee", "CitizenSoldier"], + this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Melee+CitizenSoldier"], "interests": [["strength", 2]] }; - this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving", "Ranged", "Champion"], + this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Ranged+Champion"], "interests": [["strength", 3]] }; - this.unitStat.ChampMeleeFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving", "Melee", "Champion"], + this.unitStat.ChampMeleeFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+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"], + 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"], + 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.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["FastMoving", "CitizenSoldier"], + this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["FastMoving+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] = this.unitCollection.filter(API3.Filters.byClasses(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] = this.unitCollection.filter(API3.Filters.byClasses(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"]]; 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))) + if (template.hasClasses(classes[i])) 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.reassignFastUnit(gameState); // reassign some fast units (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) + if (MatchesClassList(firstOrder[3].classes, ["Siege", "Elephant+Melee+Champion"])) 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") { // Raids are quick attacks: assign all FastMoving soldiers except some for hunting. let num = 0; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("FastMoving") || !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) + if (ent.hasClasses(["!Unit", "Ship", "Support"]) || + !this.isAvailableUnit(gameState, ent) || + 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) FastMoving unit to fasten raid preparation. */ PETRA.AttackPlan.prototype.reassignFastUnit = function(gameState) { for (let ent of this.unitCollection.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) continue; - if (!ent.hasClass("FastMoving") || !ent.hasClass("CitizenSoldier")) + if (!ent.hasClasses(["FastMoving", "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("Gate")) 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) { 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 || !ourUnit.position() || !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; 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") && 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()) { 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()) { 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")) + if (target && !target.hasClasses(["Structure", "Support"])) continue; } 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 && !target.hasClasses(["Structure", "Support"])) { if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) continue; } } 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")) + else if (target.hasClasses(["Champion", "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", "Wall", "Tower", "Fortress"], "vetoEntities": veto }; else { if (this.target.hasClass("Fortress")) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto }; - else if (this.target.hasClass("Palisade") || this.target.hasClass("Wall")) + else if (this.target.hasClasses(["Palisade", "Wall"])) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; else targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "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", "Forge"); targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge"); } 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("FastMoving") && !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("FastMoving")) 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() || !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("Gate") && ent.canAttackClass("Wall")) vala += 10000; else if (structa.hasDefensiveFire()) vala += 1000; else if (structa.hasClass("ConquestCritical")) vala += 200; let valb = structb.costSum(); if (structb.hasClass("Gate") && ent.canAttackClass("Wall")) valb += 10000; else if (structb.hasDefensiveFire()) valb += 1000; else if (structb.hasClass("ConquestCritical")) valb += 200; return valb - vala; }); if (mStruct[0].hasClass("Gate")) 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("FastMoving") && !ent.hasClass("Ranged"); + const nearby = !ent.hasClasses(["FastMoving", "Ranged"]); let mUnit = enemyUnits.filter(enemy => { 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())) + if (ent.counters(unitA)) vala += 100; let valb = unitB.hasClass("Support") ? 50 : 0; - if (ent.countersClasses(unitB.classes())) + if (ent.counters(unitB)) 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])); } // 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")) + if (!ent.hasClasses(["Ranged", "Ship"])) targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; else targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; } - else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) + else if (!ent.hasClasses(["Ranged", "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() || !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("Gate") && ent.canAttackClass("Wall")) vala += 10000; else if (structa.hasClass("ConquestCritical")) vala += 100; let valb = structb.costSum(); if (structb.hasClass("Gate") && ent.canAttackClass("Wall")) valb += 10000; else if (structb.hasClass("ConquestCritical")) valb += 100; return valb - vala; }); if (mStruct[0].hasClass("Gate")) 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; 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; 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; let allowCapture = PETRA.allowCapture(gameState, ent, attacker); if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture)) continue; 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", "Wall"])).values()) { if (API3.SquareVectorDistance(this.position, ent.position()) > 800) continue; let enemyClass = ent.hasClass("Wall") ? "Wall" : "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/baseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 25584) @@ -1,1106 +1,1104 @@ /** * Base Manager * Handles lower level economic stuffs. * Some tasks: * -tasking workers: gathering/hunting/building/repairing?/scouting/plans. * -giving feedback/estimates on GR * -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans. * -getting good spots for dropsites * -managing dropsite use in the base * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) */ PETRA.BaseManager = function(gameState, Config) { this.Config = Config; this.ID = gameState.ai.uniqueIDs.bases++; // anchor building: seen as the main building of the base. Needs to have territorial influence this.anchor = undefined; this.anchorId = undefined; this.accessIndex = undefined; // Maximum distance (from any dropsite) to look for resources // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max this.maxDistResourceSquare = 360*360; this.constructing = false; // Defenders to train in this cc when its construction is finished this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0; // vector for iterating, to check one use the HQ map. this.territoryIndices = []; this.timeNextIdleCheck = 0; }; PETRA.BaseManager.prototype.init = function(gameState, state) { if (state == "unconstructed") this.constructing = true; else if (state != "captured") this.neededDefenders = 0; this.workerObject = new PETRA.Worker(this); // entitycollections this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker")); this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.mobileDropsites = this.units.filter(API3.Filters.isDropsite()); this.units.registerUpdates(); this.workers.registerUpdates(); this.buildings.registerUpdates(); this.mobileDropsites.registerUpdates(); // array of entity IDs, with each being this.dropsites = {}; this.dropsiteSupplies = {}; this.gatherers = {}; for (let res of Resources.GetCodes()) { this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; } }; PETRA.BaseManager.prototype.reset = function(gameState, state) { if (state == "unconstructed") this.constructing = true; else this.constructing = false; if (state != "captured" || this.Config.difficulty < 3) this.neededDefenders = 0; else this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); }; PETRA.BaseManager.prototype.assignEntity = function(gameState, ent) { ent.setMetadata(PlayerID, "base", this.ID); this.units.updateEnt(ent); this.workers.updateEnt(ent); this.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit")) this.assignResourceToDropsite(gameState, ent); }; PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) { if (!anchorEntity.hasClass("CivCentre")) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor."); else { this.anchor = anchorEntity; this.anchorId = anchorEntity.id(); this.anchor.setMetadata(PlayerID, "baseAnchor", true); gameState.ai.HQ.resetBaseCache(); } anchorEntity.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(anchorEntity); this.accessIndex = PETRA.getLandAccess(gameState, anchorEntity); return true; }; /* we lost our anchor. Let's reassign our units and buildings */ PETRA.BaseManager.prototype.anchorLost = function(gameState, ent) { this.anchor = undefined; this.anchorId = undefined; this.neededDefenders = 0; gameState.ai.HQ.resetBaseCache(); }; /** Set a building of an anchorless base */ PETRA.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent) { if (!this.buildings.hasEntities()) { if (!PETRA.getBuiltEntity(gameState, ent).resourceDropsiteTypes()) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin."); this.accessIndex = PETRA.getLandAccess(gameState, ent); } else if (this.accessIndex != PETRA.getLandAccess(gameState, ent)) API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + " has been assigned " + ent.templateName() + " with access" + PETRA.getLandAccess(gameState, ent)); ent.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(ent); return true; }; /** * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area. * Moving resources (animals) and buildable resources (fields) are treated elsewhere. */ PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite) { if (this.dropsites[dropsite.id()]) { if (this.Config.debug > 0) warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); return; } let accessIndex = this.accessIndex; let dropsitePos = dropsite.position(); let dropsiteId = dropsite.id(); this.dropsites[dropsiteId] = true; if (this.ID == gameState.ai.HQ.baseManagers[0].ID) accessIndex = PETRA.getLandAccess(gameState, dropsite); let maxDistResourceSquare = this.maxDistResourceSquare; for (let type of dropsite.resourceDropsiteTypes()) { let resources = gameState.getResourceSupplies(type); if (!resources.length) continue; let nearby = this.dropsiteSupplies[type].nearby; let medium = this.dropsiteSupplies[type].medium; let faraway = this.dropsiteSupplies[type].faraway; resources.forEach(function(supply) { if (!supply.position()) return; - if (supply.hasClass("Animal")) // moving resources are treated differently - return; - if (supply.hasClass("Field")) // fields are treated separately + // Moving resources and fields are treated differently. + if (supply.hasClasses(["Animal", "Field"])) return; // quick accessibility check if (PETRA.getLandAccess(gameState, supply) != accessIndex) return; let dist = API3.SquareVectorDistance(supply.position(), dropsitePos); if (dist < maxDistResourceSquare) { if (dist < maxDistResourceSquare/16) // distmax/4 nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else if (dist < maxDistResourceSquare/4) // distmax/2 medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); } }); nearby.sort((r1, r2) => r1.dist - r2.dist); medium.sort((r1, r2) => r1.dist - r2.dist); faraway.sort((r1, r2) => r1.dist - r2.dist); /* let debug = false; if (debug) { faraway.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); }); medium.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); }); nearby.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); }); } */ } // Allows all allies to use this dropsite except if base anchor to be sure to keep // a minimum of resources for this base Engine.PostCommand(PlayerID, { "type": "set-dropsite-sharing", "entities": [dropsiteId], "shared": dropsiteId != this.anchorId }); }; // completely remove the dropsite resources from our list. PETRA.BaseManager.prototype.removeDropsite = function(gameState, ent) { if (!ent.id()) return; let removeSupply = function(entId, supply){ for (let i = 0; i < supply.length; ++i) { // exhausted resource, remove it from this list if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) supply.splice(i--, 1); // resource assigned to the removed dropsite, remove it else if (supply[i].dropsite == entId) supply.splice(i--, 1); } }; for (let type in this.dropsiteSupplies) { removeSupply(ent.id(), this.dropsiteSupplies[type].nearby); removeSupply(ent.id(), this.dropsiteSupplies[type].medium); removeSupply(ent.id(), this.dropsiteSupplies[type].faraway); } this.dropsites[ent.id()] = undefined; }; /** * @return {Object} - The position of the best place to build a new dropsite for the specified resource, * its quality and its template name. */ PETRA.BaseManager.prototype.findBestDropsiteAndLocation = function(gameState, resource) { let bestResult = { "quality": 0, "pos": [0, 0] }; for (const templateName of gameState.ai.HQ.buildManager.findStructuresByFilter(gameState, API3.Filters.isDropsite(resource))) { const dp = this.findBestDropsiteLocation(gameState, resource, templateName); if (dp.quality < bestResult.quality) continue; bestResult = dp; bestResult.templateName = templateName; } return bestResult; }; /** * Returns the position of the best place to build a new dropsite for the specified resource and dropsite template. */ PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource, templateName) { const template = gameState.getTemplate(gameState.applyCiv(templateName)); // CCs and Docks are handled elsewhere. - if (template.hasClass("CivCentre") || template.hasClass("Dock")) + if (template.hasClasses(["CivCentre", "Dock"])) return { "quality": 0, "pos": [0, 0] }; let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. let obstructions = PETRA.createObstructionMap(gameState, this.accessIndex, template); const dpEnts = gameState.getOwnStructures().filter(API3.Filters.isDropsite(resource)).toEntityArray(); // Foundations don't have the dropsite properties yet, so treat them separately. for (const foundation of gameState.getOwnFoundations().toEntityArray()) if (PETRA.getBuiltEntity(gameState, foundation).isResourceDropsite(resource)) dpEnts.push(foundation); let bestIdx; let bestVal = 0; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let cellSize = territoryMap.cellSize; const droppableResources = template.resourceDropsiteTypes(); for (let j of this.territoryIndices) { let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) // no room around continue; // We add 3 times the needed resource and once others that can be dropped here. let total = 2 * gameState.sharedScript.resourceMaps[resource].map[j]; for (const res in gameState.sharedScript.resourceMaps) if (droppableResources.indexOf(res) != -1) total += gameState.sharedScript.resourceMaps[res].map[j]; total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 if (total <= bestVal) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let dp of dpEnts) { let dpPos = dp.position(); if (!dpPos) continue; let dist = API3.SquareVectorDistance(dpPos, pos); if (dist < 3600) { total = 0; break; } else if (dist < 6400) total *= (Math.sqrt(dist)-60)/20; } if (total <= bestVal) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = total; bestIdx = i; } if (this.Config.debug > 2) warn(" for dropsite best is " + bestVal); if (bestVal <= 0) return { "quality": bestVal, "pos": [0, 0] }; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return { "quality": bestVal, "pos": [x, z] }; }; PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false) { let count = 0; let check = {}; for (let supply of this.dropsiteSupplies[type].nearby) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; count += supply.ent.resourceSupplyAmount(); } if (nearbyOnly) return count; for (let supply of this.dropsiteSupplies[type].medium) { if (check[supply.id]) continue; check[supply.id] = true; count += 0.6*supply.ent.resourceSupplyAmount(); } return count; }; /** check our resource levels and react accordingly */ PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues) { for (let type of Resources.GetCodes()) { if (type == "food") { if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms. { let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations let numQueue = queues.field.countQueuedUnits(); // TODO if not yet farms, add a check on time used/lost and build farmstead if needed if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them { if (count < 600) { queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID })); gameState.ai.HQ.needFarm = true; } } else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) { let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length; let goal = this.Config.Economy.provisionFields; if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) goal = Math.max(goal-1, 1); if (numFound + numQueue < goal) queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID })); } else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID })); continue; } if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) { let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted if (count < 900) { queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID })); gameState.ai.HQ.needCorral = true; } } continue; } // Non food stuff if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() || gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities()) { this.gatherers[type].nextCheck = gameState.ai.playedTurn; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; continue; } if (gameState.ai.playedTurn < this.gatherers[type].nextCheck) continue; for (let ent of this.gatherersByType(gameState, type).values()) { if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") ++this.gatherers[type].used; else if (ent.unitAIState() == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") ++this.gatherers[type].lost; } // TODO add also a test on remaining resources. let total = this.gatherers[type].used + this.gatherers[type].lost; if (total > 150 || total > 60 && type != "wood") { let ratio = this.gatherers[type].lost / total; if (ratio > 0.15) { const newDP = this.findBestDropsiteAndLocation(gameState, type); if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos)); else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits()) { // No good dropsite, try to build a new base if no base already planned, // and if not possible, be less strict on dropsite quality. if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && newDP.quality > Math.min(25, 50*0.15/ratio) && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos)); } } this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; } else if (total == 0) this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; } }; /** Adds the estimated gather rates from this base to the currentRates */ PETRA.BaseManager.prototype.addGatherRates = function(gameState, currentRates) { for (let res in currentRates) { // I calculate the exact gathering rate for each unit. // I must then lower that to account for travel time. // Given that the faster you gather, the more travel time matters, // I use some logarithms. // TODO: this should take into account for unit speed and/or distance to target this.gatherersByType(gameState, res).forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); if (res == "food") { this.workersBySubrole(gameState, "hunter").forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); this.workersBySubrole(gameState, "fisher").forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); } } }; PETRA.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) { if (!roleless) roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values(); for (let ent of roleless) { - if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat")) + if (ent.hasClasses(["Worker", "CitizenSoldier", "FishingBoat"])) ent.setMetadata(PlayerID, "role", "worker"); } }; /** * If the numbers of workers on the resources is unbalanced then set some of workers to idle so * they can be reassigned by reassignIdleWorkers. * TODO: actually this probably should be in the HQ. */ PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) { this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; // change resource only towards one which is more needed, and if changing will not change this order let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); let sumWanted = 0; let sumCurrent = 0; for (let need of mostNeeded) { sumWanted += need.wanted; sumCurrent += need.current; } let scale = 1; if (sumWanted > 0) scale = sumCurrent / sumWanted; for (let i = mostNeeded.length-1; i > 0; --i) { let lessNeed = mostNeeded[i]; for (let j = 0; j < i; ++j) { let moreNeed = mostNeeded[j]; let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; // Ensure that the most wanted resource is not exhausted if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type)) { if (lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) continue; // And if so, move the gatherer to the less wanted one. nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb); if (nb == 0) return; } // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1 // but we require a bit more to avoid too frequent changes if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 || lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) { nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb); if (nb == 0) return; } } } }; /** * Switch some gatherers (limited to number) from resource "from" to resource "to" * and return remaining number of possible switches. * Prefer FemaleCitizen for food and CitizenSoldier for other resources. */ PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, number) { let num = number; let only; let gatherers = this.gatherersByType(gameState, from); if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities()) only = "CitizenSoldier"; else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities()) only = "FemaleCitizen"; for (let ent of gatherers.values()) { if (num == 0) return num; if (!ent.canGather(to)) continue; if (only && !ent.hasClass(only)) continue; --num; ent.stopMoving(); ent.setMetadata(PlayerID, "gather-type", to); gameState.ai.HQ.AddTCResGatherer(to); } return num; }; PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) { // Search for idle workers, and tell them to gather resources based on demand if (!idleWorkers) { let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle"); idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); } for (let ent of idleWorkers) { // Check that the worker isn't garrisoned if (!ent.position()) continue; if (ent.hasClass("Worker")) { // Just emergency repairing here. It is better managed in assignToFoundations if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) ent.repair(this.anchor); else if (ent.isGatherer()) { let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); for (let needed of mostNeeded) { if (!ent.canGather(needed.type)) continue; let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type)) continue; ent.setMetadata(PlayerID, "subrole", "gatherer"); ent.setMetadata(PlayerID, "gather-type", needed.type); gameState.ai.HQ.AddTCResGatherer(needed.type); break; } } } else if (PETRA.isFastMoving(ent) && ent.canGather("food") && ent.canAttackClass("Animal")) ent.setMetadata(PlayerID, "subrole", "hunter"); else if (ent.hasClass("FishingBoat")) ent.setMetadata(PlayerID, "subrole", "fisher"); } }; PETRA.BaseManager.prototype.workersBySubrole = function(gameState, subrole) { return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers); }; PETRA.BaseManager.prototype.gatherersByType = function(gameState, type) { return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); }; /** * returns an entity collection of workers. * They are idled immediatly and their subrole set to idle. */ PETRA.BaseManager.prototype.pickBuilders = function(gameState, workers, number) { let availableWorkers = this.workers.filter(ent => { if (!ent.position() || !ent.isBuilder()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); availableWorkers.sort((a, b) => { let vala = 0; let valb = 0; if (a.getMetadata(PlayerID, "subrole") == "builder") vala = 100; if (b.getMetadata(PlayerID, "subrole") == "builder") valb = 100; if (a.getMetadata(PlayerID, "subrole") == "idle") vala = -50; if (b.getMetadata(PlayerID, "subrole") == "idle") valb = -50; if (a.getMetadata(PlayerID, "plan") === undefined) vala = -20; if (b.getMetadata(PlayerID, "plan") === undefined) valb = -20; return vala - valb; }); let needed = Math.min(number, availableWorkers.length - 3); for (let i = 0; i < needed; ++i) { availableWorkers[i].stopMoving(); availableWorkers[i].setMetadata(PlayerID, "subrole", "idle"); workers.addEnt(availableWorkers[i]); } return; }; /** * If we have some foundations, and we don't have enough builder-workers, * try reassigning some other workers who are nearby * AI tries to use builders sensibly, not completely stopping its econ. */ PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) { let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field")))); let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); // Check if nothing to build if (!foundations.length && !damagedBuildings.length) return; let workers = this.workers.filter(ent => ent.isBuilder()); let builderWorkers = this.workersBySubrole(gameState, "builder"); let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle()); // if we're constructing and we have the foundations to our base anchor, only try building that. if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities()) { foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)); let tID = foundations.toEntityArray()[0].id(); workers.forEach(ent => { let target = ent.getMetadata(PlayerID, "target-foundation"); if (target && target != tID) { ent.stopMoving(); ent.setMetadata(PlayerID, "target-foundation", tID); } }); } if (workers.length < 3) { let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2); if (fromOtherBase) { let baseID = this.ID; fromOtherBase.forEach(worker => { worker.setMetadata(PlayerID, "base", baseID); worker.setMetadata(PlayerID, "subrole", "builder"); workers.updateEnt(worker); builderWorkers.updateEnt(worker); idleBuilderWorkers.updateEnt(worker); }); } } let builderTot = builderWorkers.length - idleBuilderWorkers.length; // Make the limit on number of builders depends on the available resources let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); let builderRatio = 1; for (let res of Resources.GetCodes()) { if (availableResources[res] < 200) { builderRatio = 0.2; break; } else if (availableResources[res] < 1000) builderRatio = Math.min(builderRatio, availableResources[res] / 1000); } for (let target of foundations.values()) { if (target.hasClass("Field")) continue; // we do not build fields if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - if (!target.hasClass("CivCentre") && !target.hasClass("Wall") && + if (!target.hasClasses(["CivCentre", "Wall"]) && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; // if our territory has shrinked since this foundation was positioned, do not build it if (PETRA.isNotWorthBuilding(gameState, target)) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); if (maxTotalBuilders < 2 && workers.length > 1) maxTotalBuilders = 2; if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 && gameState.getPopulationLimit() < gameState.getPopulationMax()) maxTotalBuilders += 2; let targetNB = 2; - if (target.hasClass("Fortress") || target.hasClass("Wonder") || + if (target.hasClasses(["Fortress", "Wonder"]) || target.getMetadata(PlayerID, "phaseUp") == true) targetNB = 7; - else if (target.hasClass("Barracks") || target.hasClass("Range") || target.hasClass("Stable") || - target.hasClass("Tower") || target.hasClass("Market")) + else if (target.hasClasses(["Barracks", "Range", "Stable", "Tower", "Market"])) targetNB = 4; - else if (target.hasClass("House") || target.hasClass("DropsiteWood")) + else if (target.hasClasses(["House", "DropsiteWood"])) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { targetNB = 15; maxTotalBuilders = Math.max(maxTotalBuilders, 15); } // if no base yet, everybody should build if (gameState.ai.HQ.numActiveBases() == 0) { targetNB = workers.length; maxTotalBuilders = targetNB; } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") == "builder") return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); let time = target.buildTime(); nonBuilderWorkers.sort((workerA, workerB) => { let coeffA = API3.SquareVectorDistance(target.position(), workerA.position()); if (workerA.getMetadata(PlayerID, "gather-type") == "food") coeffA *= 3; let coeffB = API3.SquareVectorDistance(target.position(), workerB.position()); if (workerB.getMetadata(PlayerID, "gather-type") == "food") coeffB *= 3; return coeffA - coeffB; }); let current = 0; let nonBuilderTot = nonBuilderWorkers.length; while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) { ++assigned; ++builderTot; let ent = nonBuilderWorkers[current++]; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); } } for (let target of damagedBuildings.values()) { // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building // that's being destroyed. if (gameState.ai.HQ.isNearInvadingArmy(target.position())) { if (target.healthLevel() > 0.5 || - !target.hasClass("CivCentre") && !target.hasClass("Wall") && + !target.hasClasses(["CivCentre", "Wall"]) && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; } else if (noRepair && !target.hasClass("CivCentre")) continue; if (target.decaying()) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); let targetNB = 1; - if (target.hasClass("Fortress") || target.hasClass("Wonder")) + if (target.hasClasses(["Fortress", "Wonder"])) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio)); targetNB = 5; if (target.healthLevel() < 0.3) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio)); targetNB = 7; } } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") == "builder") return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }); let num = Math.min(nonBuilderWorkers.length, targetNB-assigned); let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); nearestNonBuilders.forEach(function(ent) { ++assigned; ++builderTot; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); }); } }; /** Return false when the base is not active (no workers on it) */ PETRA.BaseManager.prototype.update = function(gameState, queues, events) { if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units { // if some active base, reassigns the workers/buildings // otherwise look for anything useful to do, i.e. treasures to gather if (gameState.ai.HQ.numActiveBases() > 0) { for (let ent of this.units.values()) { let bestBase = PETRA.getBestBase(gameState, ent); if (bestBase.ID != this.ID) bestBase.assignEntity(gameState, ent); } for (let ent of this.buildings.values()) { let bestBase = PETRA.getBestBase(gameState, ent); if (!bestBase) { if (ent.hasClass("Dock")) API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName()); continue; } if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); bestBase.assignEntity(gameState, ent); } } else if (gameState.ai.HQ.canBuildUnits) { this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); } return false; } if (!this.anchor) // This anchor has been destroyed, but the base may still be usable { if (!this.buildings.hasEntities()) { // Reassign all remaining entities to its nearest base for (let ent of this.units.values()) { let base = PETRA.getBestBase(gameState, ent, false, this.ID); base.assignEntity(gameState, ent); } return false; } // If we have a base with anchor on the same land, reassign everything to it let reassignedBase; for (let ent of this.buildings.values()) { if (!ent.position()) continue; let base = PETRA.getBestBase(gameState, ent); if (base.anchor) reassignedBase = base; break; } if (reassignedBase) { for (let ent of this.units.values()) reassignedBase.assignEntity(gameState, ent); for (let ent of this.buildings.values()) { if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); reassignedBase.assignEntity(gameState, ent); } return false; } this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); return true; } Engine.ProfileStart("Base update - base " + this.ID); this.checkResourceLevels(gameState, queues); this.assignToFoundations(gameState); if (this.constructing) { let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); if(owner != 0 && !gameState.isPlayerAlly(owner)) { // we're in enemy territory. If we're too close from the enemy, destroy us. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (cc.owner() != owner) continue; if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) continue; this.anchor.destroy(); gameState.ai.HQ.resetBaseCache(); break; } } } else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) --this.neededDefenders; if (gameState.ai.elapsedTime > this.timeNextIdleCheck && (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); // check if workers can find something useful to do for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); Engine.ProfileStop(); return true; }; PETRA.BaseManager.prototype.Serialize = function() { return { "ID": this.ID, "anchorId": this.anchorId, "accessIndex": this.accessIndex, "maxDistResourceSquare": this.maxDistResourceSquare, "constructing": this.constructing, "gatherers": this.gatherers, "neededDefenders": this.neededDefenders, "territoryIndices": this.territoryIndices, "timeNextIdleCheck": this.timeNextIdleCheck }; }; PETRA.BaseManager.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js (revision 25584) @@ -1,191 +1,191 @@ /** * One task of this manager is to cache the list of structures we have builders for, * to avoid having to loop on all entities each time. * It also takes care of the structures we can't currently build and should not try to build endlessly. */ PETRA.BuildManager = function() { // List of buildings we have builders for, with number of possible builders. this.builderCounters = new Map(); // List of buildings we can't currently build (because no room, no builder or whatever), // with time we should wait before trying again to build it. this.unbuildables = new Map(); }; /** Initialization at start of game */ PETRA.BuildManager.prototype.init = function(gameState) { let civ = gameState.getPlayerCiv(); for (let ent of gameState.getOwnUnits().values()) this.incrementBuilderCounters(civ, ent, 1); }; PETRA.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) { for (let buildable of ent.buildableEntities(civ)) { if (this.builderCounters.has(buildable)) { let count = this.builderCounters.get(buildable) + increment; if (count < 0) { API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " with count < 0"); continue; } this.builderCounters.set(buildable, count); } else if (increment > 0) this.builderCounters.set(buildable, increment); else API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " not yet set"); } }; /** Update the builders counters */ PETRA.BuildManager.prototype.checkEvents = function(gameState, events) { this.elapsedTime = gameState.ai.elapsedTime; let civ = gameState.getPlayerCiv(); for (let evt of events.Create) { if (events.Destroy.some(e => e.entity == evt.entity)) continue; let ent = gameState.getEntityById(evt.entity); if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) this.incrementBuilderCounters(civ, ent, 1); } for (let evt of events.Destroy) { if (events.Create.some(e => e.entity == evt.entity) || !evt.entityObj) continue; let ent = evt.entityObj; if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) this.incrementBuilderCounters(civ, ent, -1); } for (let evt of events.OwnershipChanged) // capture events { let increment; if (evt.from == PlayerID) increment = -1; else if (evt.to == PlayerID) increment = 1; else continue; let ent = gameState.getEntityById(evt.entity); if (ent && ent.hasClass("Unit")) this.incrementBuilderCounters(civ, ent, increment); } for (let evt of events.ValueModification) { if (evt.component != "Builder" || !evt.valueNames.some(val => val.startsWith("Builder/Entities/"))) continue; // Unfortunately there really is not an easy way to determine the changes // at this stage, so we simply have to dump the cache. this.builderCounters = new Map(); let civ = gameState.getPlayerCiv(); for (let ent of gameState.getOwnUnits().values()) this.incrementBuilderCounters(civ, ent, 1); } }; /** * Get the buildable structures passing a filter. */ PETRA.BuildManager.prototype.findStructuresByFilter = function(gameState, filter) { const result = []; for (let [templateName, count] of this.builderCounters) { if (!count || gameState.isTemplateDisabled(templateName)) continue; let template = gameState.getTemplate(templateName); if (!template || !template.available(gameState)) continue; if (filter.func(template)) result.push(templateName); } return result; }; /** * Get the first buildable structure with a given class * TODO when several available, choose the best one */ PETRA.BuildManager.prototype.findStructureWithClass = function(gameState, classes) { - return this.findStructuresByFilter(gameState, API3.Filters.byClassesOr(classes))[0]; + return this.findStructuresByFilter(gameState, API3.Filters.byClasses(classes))[0]; }; PETRA.BuildManager.prototype.hasBuilder = function(template) { let numBuilders = this.builderCounters.get(template); return numBuilders && numBuilders > 0; }; PETRA.BuildManager.prototype.isUnbuildable = function(gameState, template) { return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime; }; PETRA.BuildManager.prototype.setBuildable = function(template) { if (this.unbuildables.has(template)) this.unbuildables.delete(template); }; /** Time is the duration in second that we will wait before checking again if it is buildable */ PETRA.BuildManager.prototype.setUnbuildable = function(gameState, template, time = 90, reason = "room") { if (!this.unbuildables.has(template)) this.unbuildables.set(template, { "reason": reason, "time": gameState.ai.elapsedTime + time }); else { let unbuildable = this.unbuildables.get(template); if (unbuildable.time < gameState.ai.elapsedTime + time) { unbuildable.reason = reason; unbuildable.time = gameState.ai.elapsedTime + time; } } }; /** Return the number of unbuildables due to missing room */ PETRA.BuildManager.prototype.numberMissingRoom = function(gameState) { let num = 0; for (let unbuildable of this.unbuildables.values()) if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) ++num; return num; }; /** Reset the unbuildables due to missing room */ PETRA.BuildManager.prototype.resetMissingRoom = function(gameState) { for (let [key, unbuildable] of this.unbuildables) if (unbuildable.reason == "room") this.unbuildables.delete(key); }; PETRA.BuildManager.prototype.Serialize = function() { return { "builderCounters": this.builderCounters, "unbuildables": this.unbuildables }; }; PETRA.BuildManager.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 25584) @@ -1,649 +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")) + if (ent.hasClasses(["Animal+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 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 25584) @@ -1,964 +1,962 @@ 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")) + if (ent.hasClasses(["Ship", "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")) + if (ent.hasClasses(["StoneThrower", "Support", "FishingBoat"])) 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 + 5); } 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")) + !attacker.hasClasses(["Structure", "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"))) + !currentTarget.hasClasses(["Structure", "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() || !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)); } } } 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.numberOfGarrisonedSlots(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)) + if (!ent.hasClasses(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.numberOfGarrisonedSlots(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())) + if (!unit.hasClasses(ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedSlots(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())) + if (!unit.hasClasses(ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedSlots(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.numberOfGarrisonedSlots(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); } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 25584) @@ -1,436 +1,435 @@ /** returns true if this unit should be considered as a siege unit */ PETRA.isSiegeUnit = function(ent) { - return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee"); + return ent.hasClasses(["Siege", "Elephant+Melee"]); }; /** returns true if this unit should be considered as "fast". */ PETRA.isFastMoving = function(ent) { // TODO: use clever logic based on walkspeed comparisons. return ent.hasClass("FastMoving"); }; /** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass) { let strength = 0; let attackTypes = ent.attackTypes(); let damageTypes = Object.keys(DamageTypeImportance); if (!attackTypes) return strength; for (let type of attackTypes) { if (type == "Slaughter") continue; let attackStrength = ent.attackStrengths(type); for (let str in attackStrength) { let val = parseFloat(attackStrength[str]); if (againstClass) val *= ent.getMultiplierAgainst(type, againstClass); if (DamageTypeImportance[str]) strength += DamageTypeImportance[str] * val / damageTypes.length; else if (debugLevel > 0) API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js)."); } let attackRange = ent.attackRange(type); if (attackRange) strength += attackRange.max * 0.0125; let attackTimes = ent.attackTimes(type); for (let str in attackTimes) { let val = parseFloat(attackTimes[str]); switch (str) { case "repeat": strength += val / 100000; break; case "prepare": strength -= val / 100000; break; default: API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); } } } let resistanceStrength = ent.resistanceStrengths(); if (resistanceStrength.Damage) for (let str in resistanceStrength.Damage) { let val = +resistanceStrength.Damage[str]; if (DamageTypeImportance[str]) strength += DamageTypeImportance[str] * val / damageTypes.length; else if (debugLevel > 0) API3.warn("Petra: " + str + " unknown resistanceStrength in getMaxStrength (please add " + str + " to config.js)."); } // ToDo: Add support for StatusEffects and Capture. return strength * ent.maxHitpoints() / 100.0; }; /** Get access and cache it (except for units as it can change) in metadata if not already done */ PETRA.getLandAccess = function(gameState, ent) { if (ent.hasClass("Unit")) { let pos = ent.position(); if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (holder) return PETRA.getLandAccess(gameState, holder); API3.warn("Petra error: entity without position, but not garrisoned"); PETRA.dumpEntity(ent); return undefined; } return gameState.ai.accessibility.getAccessValue(pos); } let access = ent.getMetadata(PlayerID, "access"); if (!access) { access = gameState.ai.accessibility.getAccessValue(ent.position()); // Docks are sometimes not as expected if (access < 2 && ent.buildPlacementType() == "shore") { let halfDepth = 0; if (ent.get("Footprint/Square")) halfDepth = +ent.get("Footprint/Square/@depth") / 2; else if (ent.get("Footprint/Circle")) halfDepth = +ent.get("Footprint/Circle/@radius"); let entPos = ent.position(); let cosa = Math.cos(ent.angle()); let sina = Math.sin(ent.angle()); for (let d = 3; d < halfDepth; d += 3) { let pos = [ entPos[0] - d * sina, entPos[1] - d * cosa]; access = gameState.ai.accessibility.getAccessValue(pos); if (access > 1) break; } } ent.setMetadata(PlayerID, "access", access); } return access; }; /** Sea access always cached as it never changes */ PETRA.getSeaAccess = function(gameState, ent) { let sea = ent.getMetadata(PlayerID, "sea"); if (!sea) { sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); // Docks are sometimes not as expected if (sea < 2 && ent.buildPlacementType() == "shore") { let entPos = ent.position(); let cosa = Math.cos(ent.angle()); let sina = Math.sin(ent.angle()); for (let d = 3; d < 15; d += 3) { let pos = [ entPos[0] + d * sina, entPos[1] + d * cosa]; sea = gameState.ai.accessibility.getAccessValue(pos, true); if (sea > 1) break; } } ent.setMetadata(PlayerID, "sea", sea); } return sea; }; PETRA.setSeaAccess = function(gameState, ent) { PETRA.getSeaAccess(gameState, ent); }; /** Decide if we should try to capture (returns true) or destroy (return false) */ PETRA.allowCapture = function(gameState, ent, target) { if (!target.isCapturable() || !ent.canCapture(target)) return false; if (target.isInvulnerable()) return true; // always try to recapture capture points from an allied, except if it's decaying if (gameState.isPlayerAlly(target.owner())) return !target.decaying(); let antiCapture = target.defaultRegenRate(); if (target.isGarrisonHolder() && target.garrisoned()) antiCapture += target.garrisonRegenRate() * target.garrisoned().length; if (target.decaying()) antiCapture -= target.territoryDecayRate(); let capture; let capturableTargets = gameState.ai.HQ.capturableTargets; if (!capturableTargets.has(target.id())) { capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); } else { let capturable = capturableTargets.get(target.id()); if (!capturable.ents.has(ent.id())) { capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturable.ents.add(ent.id()); } capture = capturable.strength; } capture *= 1 / (0.1 + 0.9*target.healthLevel()); let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) return capture > antiCapture + sumCapturePoints/50; return capture > antiCapture + sumCapturePoints/80; }; PETRA.getAttackBonus = function(ent, target, type) { let attackBonus = 1; if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) return attackBonus; let bonuses = ent.get("Attack/" + type + "/Bonuses"); for (let key in bonuses) { let bonus = bonuses[key]; if (bonus.Civ && bonus.Civ !== target.civ()) continue; - if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls))) - continue; - attackBonus *= bonus.Multiplier; + if (!bonus.Classes || target.hasClasses(bonus.Classes)) + attackBonus *= bonus.Multiplier; } return attackBonus; }; /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ PETRA.returnResources = function(gameState, ent) { if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) return false; let resource = ent.resourceCarrying()[0].type; let closestDropsite; let distmin = Math.min(); let access = PETRA.getLandAccess(gameState, ent); let dropsiteCollection = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); for (let dropsite of dropsiteCollection.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (PETRA.getLandAccess(gameState, dropsite) != access) continue; let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist > distmin) continue; distmin = dist; closestDropsite = dropsite; } if (!closestDropsite) return false; ent.returnResources(closestDropsite); return true; }; /** is supply full taking into account gatherers affected during this turn */ PETRA.IsSupplyFull = function(gameState, ent) { return ent.isFull() === true || ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers(); }; /** * Get the best base (in terms of distance and accessIndex) for an entity. * It should be on the same accessIndex for structures. * If nothing found, return the base[0] for units and undefined for structures. * If exclude is given, we exclude the base with ID = exclude. */ PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false) { let pos = ent.position(); let accessIndex; if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (!holder || !holder.position()) { API3.warn("Petra error: entity without position, but not garrisoned"); PETRA.dumpEntity(ent); return gameState.ai.HQ.baseManagers[0]; } pos = holder.position(); accessIndex = PETRA.getLandAccess(gameState, holder); } else accessIndex = PETRA.getLandAccess(gameState, ent); let distmin = Math.min(); let dist; let bestbase; for (let base of gameState.ai.HQ.baseManagers) { if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude) continue; if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined)) continue; if (ent.hasClass("Structure") && base.accessIndex != accessIndex) continue; if (base.anchor && base.anchor.position()) dist = API3.SquareVectorDistance(base.anchor.position(), pos); else { let found = false; for (let structure of base.buildings.values()) { if (!structure.position()) continue; dist = API3.SquareVectorDistance(structure.position(), pos); found = true; break; } if (!found) continue; } if (base.accessIndex != accessIndex) dist += 50000000; if (!base.anchor) dist += 50000000; if (dist > distmin) continue; distmin = dist; bestbase = base; } if (!bestbase && !ent.hasClass("Structure")) bestbase = gameState.ai.HQ.baseManagers[0]; return bestbase; }; PETRA.getHolder = function(gameState, ent) { for (let holder of gameState.getEntities().values()) { if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) return holder; } return undefined; }; /** return the template of the built foundation if a foundation, otherwise return the entity itself */ PETRA.getBuiltEntity = function(gameState, ent) { if (ent.foundationProgress() !== undefined) return gameState.getBuiltTemplate(ent.templateName()); return ent; }; /** * return true if it is not worth finishing this building (it would surely decay) * TODO implement the other conditions */ PETRA.isNotWorthBuilding = function(gameState, ent) { if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) { let buildTerritories = ent.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) return true; } return false; }; /** * Check if the straight line between the two positions crosses an enemy territory */ PETRA.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70) { let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1; let stepx = (pos2[0] - pos1[0]) / n; let stepy = (pos2[1] - pos1[1]) / n; for (let i = 1; i < n; ++i) { let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy]; let owner = gameState.ai.HQ.territoryMap.getOwner(pos); if (owner && gameState.isPlayerEnemy(owner)) return true; } return false; }; PETRA.gatherTreasure = function(gameState, ent, water = false) { if (!gameState.ai.HQ.treasures.hasEntities()) return false; if (!ent || !ent.position()) return false; if (!ent.isTreasureCollector()) return false; let treasureFound; let distmin = Math.min(); let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent); for (let treasure of gameState.ai.HQ.treasures.values()) { // let some time for the previous gatherer to reach the treasure before trying again let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) continue; if (!water && access != PETRA.getLandAccess(gameState, treasure)) continue; if (water && access != PETRA.getSeaAccess(gameState, treasure)) continue; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) continue; let dist = API3.SquareVectorDistance(ent.position(), treasure.position()); if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit continue; if (dist > distmin) continue; distmin = dist; treasureFound = treasure; } if (!treasureFound) return false; treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); ent.collectTreasure(treasureFound); ent.setMetadata(PlayerID, "treasure", treasureFound.id()); return true; }; PETRA.dumpEntity = function(ent) { if (!ent) return; API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + " state " + ent.unitAIState()); API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + " subrole " + ent.getMetadata(PlayerID, "subrole")); API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + " foundationProgress " + ent.foundationProgress()); API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport")); API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") + " gather-type " + ent.getMetadata(PlayerID, "gather-type") + " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 25584) @@ -1,376 +1,376 @@ /** * Manage the garrisonHolders * When a unit is ordered to garrison, it must be done through this.garrison() function so that * an object in this.holders is created. This object contains an array with the entities * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) */ PETRA.GarrisonManager = function(Config) { this.Config = Config; this.holders = new Map(); this.decayingStructures = new Map(); }; PETRA.GarrisonManager.prototype.update = function(gameState, events) { // First check for possible upgrade of a structure for (let evt of events.EntityRenamed) { for (let id of this.holders.keys()) { if (id != evt.entity) continue; let data = this.holders.get(id); let newHolder = gameState.getEntityById(evt.newentity); if (newHolder && newHolder.isGarrisonHolder()) { this.holders.delete(id); this.holders.set(evt.newentity, data); } else { for (let entId of data.list) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) continue; this.leaveGarrison(ent); ent.stopMoving(); } this.holders.delete(id); } } for (let id of this.decayingStructures.keys()) { if (id !== evt.entity) continue; this.decayingStructures.delete(id); if (this.decayingStructures.has(evt.newentity)) continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) continue; let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); this.decayingStructures.set(evt.newentity, gmin); } } for (let [id, data] of this.holders.entries()) { let list = data.list; let holder = gameState.getEntityById(id); if (!holder || !gameState.isPlayerAlly(holder.owner())) { // this holder was certainly destroyed or captured. Let's remove it for (let entId of list) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) continue; this.leaveGarrison(ent); ent.stopMoving(); } this.holders.delete(id); continue; } // Update the list of garrisoned units for (let j = 0; j < list.length; ++j) { for (let evt of events.EntityRenamed) if (evt.entity === list[j]) list[j] = evt.newentity; let ent = gameState.getEntityById(list[j]); if (!ent) // unit must have been killed while garrisoning list.splice(j--, 1); else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned { this.leaveGarrison(ent); list.splice(j--, 1); } else { if (ent.unitAIOrderData().some(order => order.target && order.target == id)) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { // The garrison order must have failed this.leaveGarrison(ent); list.splice(j--, 1); } else { if (gameState.ai.Config.debug > 0) { API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() + ") is expected to garrison in " + id + " (" + holder.genericName() + "), but has no such garrison order " + uneval(ent.unitAIOrderData())); PETRA.dumpEntity(ent); } list.splice(j--, 1); } } } if (!holder.position()) // could happen with siege unit inside a ship continue; if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) { let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; let around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; for (let ent of gameState.getEnemyEntities().values()) { if (ent.hasClass("Structure")) { if (!ent.attackRange("Ranged")) continue; } else if (ent.hasClass("Unit")) { if (ent.owner() == 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT")) continue; } else continue; if (!ent.position()) continue; let dist = API3.SquareVectorDistance(ent.position(), holder.position()); if (dist > range*range) continue; if (ent.hasClass("Structure")) around.defenseStructure = true; else if (PETRA.isSiegeUnit(ent)) { if (ent.attackTypes().indexOf("Melee") !== -1) around.meleeSiege = true; else around.rangeSiege = true; } else { around.unit = true; break; } } // Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units data.allowMelee = around.defenseStructure || around.unit; for (let entId of holder.garrisoned()) { let ent = gameState.getEntityById(entId); if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) holder.unload(entId); } for (let j = 0; j < list.length; ++j) { let ent = gameState.getEntityById(list[j]); if (this.keepGarrisoned(ent, holder, around)) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { this.leaveGarrison(ent); ent.stopMoving(); } list.splice(j--, 1); } if (this.numberOfGarrisonedSlots(holder) === 0) this.holders.delete(id); else holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); } } // Warning new garrison orders (as in the following lines) should be done after having updated the holders // (or TODO we should add a test that the garrison order is from a previous turn when updating) for (let [id, gmin] of this.decayingStructures.entries()) { let ent = gameState.getEntityById(id); if (!ent || ent.owner() !== PlayerID) this.decayingStructures.delete(id); else if (this.numberOfGarrisonedSlots(ent) < gmin) gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": "decay" }); } }; /** TODO should add the units garrisoned inside garrisoned units */ PETRA.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) { if (!this.holders.has(holder.id())) return holder.garrisoned().length; return holder.garrisoned().length + this.holders.get(holder.id()).list.length; }; /** TODO should add the units garrisoned inside garrisoned units */ PETRA.GarrisonManager.prototype.numberOfGarrisonedSlots = function(holder) { if (!this.holders.has(holder.id())) return holder.garrisonedSlots(); return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length; }; PETRA.GarrisonManager.prototype.allowMelee = function(holder) { if (!this.holders.has(holder.id())) return undefined; return this.holders.get(holder.id()).allowMelee; }; /** This is just a pre-garrison state, while the entity walk to the garrison holder */ PETRA.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) { if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison()) return; this.registerHolder(gameState, holder); this.holders.get(holder.id()).list.push(ent.id()); if (gameState.ai.Config.debug > 2) { warn("garrison unit " + ent.genericName() + " in " + holder.genericName() + " with type " + type); warn(" we try to garrison a unit with plan " + ent.getMetadata(PlayerID, "plan") + " and role " + ent.getMetadata(PlayerID, "role") + " and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport")); } if (ent.getMetadata(PlayerID, "plan") !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); ent.setMetadata(PlayerID, "subrole", "garrisoning"); ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); ent.setMetadata(PlayerID, "garrisonType", type); ent.garrison(holder); }; /** This is the end of the pre-garrison state, either because the entity is really garrisoned or because it has changed its order (i.e. because the garrisonHolder was destroyed) This function is for internal use inside garrisonManager. From outside, you should also update the holder and then using cancelGarrison should be the preferred solution */ PETRA.GarrisonManager.prototype.leaveGarrison = function(ent) { ent.setMetadata(PlayerID, "subrole", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "garrisonHolder", undefined); }; /** Cancel a pre-garrison state */ PETRA.GarrisonManager.prototype.cancelGarrison = function(ent) { ent.stopMoving(); this.leaveGarrison(ent); let holderId = ent.getMetadata(PlayerID, "garrisonHolder"); if (!holderId || !this.holders.has(holderId)) return; let list = this.holders.get(holderId).list; let index = list.indexOf(ent.id()); if (index !== -1) list.splice(index, 1); }; PETRA.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around) { switch (ent.getMetadata(PlayerID, "garrisonType")) { case 'force': // force the ungarrisoning return false; case 'trade': // trader garrisoned in ship return true; case 'protection': // hurt unit for healing or infantry for defense if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) return true; let capture = ent.capturePoints(); if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8) return true; - if (MatchesClassList(ent.classes(), holder.getGarrisonArrowClasses())) + if (ent.hasClasses(holder.getGarrisonArrowClasses())) { if (around.unit || around.defenseStructure) return true; if (around.meleeSiege || around.rangeSiege) return ent.attackTypes().indexOf("Melee") === -1 || ent.healthLevel() < this.Config.garrisonHealthLevel.low; return false; } if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1) return false; if (around.unit) return ent.hasClass("Support") || PETRA.isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above if (PETRA.isSiegeUnit(ent)) return around.meleeSiege; return holder.buffHeal() && ent.needsHeal(); case 'decay': return this.decayingStructures.has(holder.id()); case 'emergency': // f.e. hero in regicide mode if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) return true; if (around.unit || around.defenseStructure || around.meleeSiege || around.rangeSiege && ent.healthLevel() < this.Config.garrisonHealthLevel.high) return true; return holder.buffHeal() && ent.needsHeal(); default: if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager return true; API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + " for " + ent.genericName() + " id " + ent.id() + " inside " + holder.genericName() + " id " + holder.id()); ent.setMetadata(PlayerID, "garrisonType", "protection"); return true; } }; /** Add this holder in the list managed by the garrisonManager */ PETRA.GarrisonManager.prototype.registerHolder = function(gameState, holder) { if (this.holders.has(holder.id())) // already registered return; this.holders.set(holder.id(), { "list": [], "allowMelee": true }); holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); }; /** * Garrison units in decaying structures to stop their decay * do it only for structures useful for defense, except if we are expanding (justCaptured=true) * in which case we also do it for structures useful for unit trainings (TODO only Barracks are done) */ PETRA.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) { if (this.decayingStructures.has(entId)) return true; let ent = gameState.getEntityById(entId); if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire()) return false; if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) return false; let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); this.decayingStructures.set(entId, gmin); return true; }; PETRA.GarrisonManager.prototype.removeDecayingStructure = function(entId) { if (!this.decayingStructures.has(entId)) return; this.decayingStructures.delete(entId); }; PETRA.GarrisonManager.prototype.Serialize = function() { return { "holders": this.holders, "decayingStructures": this.decayingStructures }; }; PETRA.GarrisonManager.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 25584) @@ -1,2893 +1,2893 @@ /** * Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: * -defining RESS needs * -BO decisions. * > training workers * > building stuff (though we'll send that to bases) * -picking strategy (specific manager?) * -diplomacy -> diplomacyManager * -planning attacks -> attackManager * -picking new CC locations. */ PETRA.HQ = function(Config) { this.Config = Config; this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i // Cache various quantities. this.turnCache = {}; this.lastFailedGather = {}; this.firstBaseConfig = false; this.currentBase = 0; // Only one base (from baseManager) is run every turn. // Workers configuration. this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime. this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase). this.towerLapseTime = this.Config.Military.towerLapseTime; this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase). this.fortressLapseTime = this.Config.Military.fortressLapseTime; this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); this.baseManagers = []; this.attackManager = new PETRA.AttackManager(this.Config); this.buildManager = new PETRA.BuildManager(); this.defenseManager = new PETRA.DefenseManager(this.Config); this.tradeManager = new PETRA.TradeManager(this.Config); this.navalManager = new PETRA.NavalManager(this.Config); this.researchManager = new PETRA.ResearchManager(this.Config); this.diplomacyManager = new PETRA.DiplomacyManager(this.Config); this.garrisonManager = new PETRA.GarrisonManager(this.Config); this.victoryManager = new PETRA.VictoryManager(this.Config); this.capturableTargets = new Map(); this.capturableTargetsTime = 0; }; /** More initialisation for stuff that needs the gameState */ PETRA.HQ.prototype.init = function(gameState, queues) { this.territoryMap = PETRA.createTerritoryMap(gameState); // initialize base map. Each pixel is a base ID, or 0 if not or not accessible this.basesMap = new API3.Map(gameState.sharedScript, "territory"); // create borderMap: flag cells on the border of the map // then this map will be completed with our frontier in updateTerritories this.borderMap = PETRA.createBorderMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; this.treasures = gameState.getEntities().filter(ent => ent.isTreasure()); this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); }; /** * initialization needed after deserialization (only called when deserialization) */ PETRA.HQ.prototype.postinit = function(gameState) { // Rebuild the base maps from the territory indices of each base this.basesMap = new API3.Map(gameState.sharedScript, "territory"); for (let base of this.baseManagers) for (let j of base.territoryIndices) this.basesMap.map[j] = base.ID; for (let ent of gameState.getOwnEntities().values()) { if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) continue; // Entities which have been built or have changed ownership after the last AI turn have no base. // they will be dealt with in the next checkEvents let baseID = ent.getMetadata(PlayerID, "base"); if (baseID === undefined) continue; let base = this.getBaseByID(baseID); base.assignResourceToDropsite(gameState, ent); } this.updateTerritories(gameState); }; /** * Create a new base in the baseManager: * If an existing one without anchor already exist, use it. * Otherwise create a new one. * TODO when buildings, criteria should depend on distance * allowedType: undefined => new base with an anchor * "unconstructed" => new base with a foundation anchor * "captured" => captured base with an anchor * "anchorless" => anchorless base, currently with dock */ PETRA.HQ.prototype.createBase = function(gameState, ent, type) { let access = PETRA.getLandAccess(gameState, ent); let newbase; for (let base of this.baseManagers) { if (base.accessIndex != access) continue; if (type != "anchorless" && base.anchor) continue; if (type != "anchorless") { // TODO we keep the fisrt one, we should rather use the nearest if buildings // and possibly also cut on distance newbase = base; break; } else { // TODO here also test on distance instead of first if (newbase && !base.anchor) continue; newbase = base; if (newbase.anchor) break; } } if (this.Config.debug > 0) { API3.warn(" ----------------------------------------------------------"); API3.warn(" HQ createBase entrance avec access " + access + " and type " + type); API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); } if (!newbase) { newbase = new PETRA.BaseManager(gameState, this.Config); newbase.init(gameState, type); this.baseManagers.push(newbase); } else newbase.reset(type); if (type != "anchorless") newbase.setAnchor(gameState, ent); else newbase.setAnchorlessEntity(gameState, ent); return newbase; }; /** * returns the sea index linking regions 1 and region 2 (supposed to be different land region) * otherwise return undefined * for the moment, only the case land-sea-land is supported */ PETRA.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2) { let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water") return path[1]; if (this.Config.debug > 1) { API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); } return undefined; }; /** TODO check if the new anchorless bases should be added to addBase */ PETRA.HQ.prototype.checkEvents = function(gameState, events) { let addBase = false; this.buildManager.checkEvents(gameState, events); if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); for (let evt of events.DiplomacyChanged) { if (evt.player != PlayerID && evt.otherPlayer != PlayerID) continue; // Reset the entities collections which depend on diplomacy gameState.resetOnDiplomacyChanged(); break; } for (let evt of events.Destroy) { // Let's check we haven't lost an important building here. if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && evt.metadata[PlayerID].base) { let ent = evt.entityObj; if (ent.owner() != PlayerID) continue; // A new base foundation was created and destroyed on the same (AI) turn if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) continue; let base = this.getBaseByID(evt.metadata[PlayerID].base); if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) base.removeDropsite(gameState, ent); if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) base.anchorLost(gameState, ent); } } for (let evt of events.EntityRenamed) { let ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) continue; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (!base.anchorId || base.anchorId != evt.entity) continue; base.anchorId = evt.newentity; base.anchor = ent; } for (let evt of events.Create) { // Let's check if we have a valuable foundation needing builders quickly // (normal foundations are taken care in baseManager.assignToFoundations) let ent = gameState.getEntityById(evt.entity); if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) continue; if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc { // Okay so let's try to create a new base around this. let newbase = this.createBase(gameState, ent, "unconstructed"); // Let's get a few units from other bases there to build this. let builders = this.bulkPickWorkers(gameState, newbase, 10); if (builders !== false) { builders.forEach(worker => { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock { let newbase = this.createBase(gameState, ent, "anchorless"); // Let's get a few units from other bases there to build this. let builders = this.bulkPickWorkers(gameState, newbase, 4); if (builders != false) { builders.forEach(worker => { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } for (let evt of events.ConstructionFinished) { if (evt.newentity == evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID) continue; if (ent.hasClass("Market") && this.maxFields) this.maxFields = false; if (ent.getMetadata(PlayerID, "base") === undefined) continue; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes()) base.assignResourceToDropsite(gameState, ent); if (ent.getMetadata(PlayerID, "baseAnchor") === true) { if (base.constructing) base.constructing = false; addBase = true; } } for (let evt of events.OwnershipChanged) // capture events { if (evt.from == PlayerID) { let ent = gameState.getEntityById(evt.entity); if (!ent || ent.getMetadata(PlayerID, "base") === undefined) continue; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) base.removeDropsite(gameState, ent); if (ent.getMetadata(PlayerID, "baseAnchor") === true) base.anchorLost(gameState, ent); } if (evt.to != PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; if (ent.hasClass("Unit")) { PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent); ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.hasClass("Trader")) { ent.setMetadata(PlayerID, "role", "trader"); ent.setMetadata(PlayerID, "route", undefined); } if (ent.hasClass("Worker")) { ent.setMetadata(PlayerID, "role", "worker"); ent.setMetadata(PlayerID, "subrole", "idle"); } if (ent.hasClass("Ship")) PETRA.setSeaAccess(gameState, ent); - if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) + if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) ent.setMetadata(PlayerID, "plan", -1); continue; } if (ent.hasClass("CivCentre")) // build a new base around it { let newbase; if (ent.foundationProgress() !== undefined) newbase = this.createBase(gameState, ent, "unconstructed"); else { newbase = this.createBase(gameState, ent, "captured"); addBase = true; } newbase.assignEntity(gameState, ent); } else { let base; // If dropsite on new island, create a base around it if (!ent.decaying() && ent.resourceDropsiteTypes()) base = this.createBase(gameState, ent, "anchorless"); else base = PETRA.getBestBase(gameState, ent) || this.baseManagers[0]; base.assignEntity(gameState, ent); if (ent.decaying()) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } } } // deal with the different rally points of training units: the rally point is set when the training starts // for the time being, only autogarrison is used for (let evt of events.TrainingStarted) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) continue; let metadata = ent._entity.trainingQueue[0].metadata; if (metadata && metadata.garrisonType) ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison else ent.unsetRallyPoint(); } for (let evt of events.TrainingFinished) { for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent.position()) { // we are autogarrisoned, check that the holder is registered in the garrisonManager let holder = gameState.getEntityById(ent.garrisonHolderID()); if (holder) this.garrisonManager.registerHolder(gameState, holder); } else if (ent.getMetadata(PlayerID, "garrisonType")) { // we were supposed to be autogarrisoned, but this has failed (may-be full) ent.setMetadata(PlayerID, "garrisonType", undefined); } // Check if this unit is no more needed in its attack plan // (happen when the training ends after the attack is started or aborted) let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = this.attackManager.getPlan(plan); if (!attack || attack.state != "unexecuted") ent.setMetadata(PlayerID, "plan", -1); } // Assign it immediately to something useful to do if (ent.getMetadata(PlayerID, "role") == "worker") { let base; if (ent.getMetadata(PlayerID, "base") === undefined) { base = PETRA.getBestBase(gameState, ent); base.assignEntity(gameState, ent); } else base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.reassignIdleWorkers(gameState, [ent]); base.workerObject.update(gameState, ent); } else if (ent.resourceSupplyType() && ent.position()) { let type = ent.resourceSupplyType(); if (!type.generic) continue; let dropsites = gameState.getOwnDropsites(type.generic); let pos = ent.position(); let access = PETRA.getLandAccess(gameState, ent); let distmin = Math.min(); let goal; for (let dropsite of dropsites.values()) { if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access) continue; let dist = API3.SquareVectorDistance(pos, dropsite.position()); if (dist > distmin) continue; distmin = dist; goal = dropsite.position(); } if (goal) ent.moveToRange(goal[0], goal[1]); } } } for (let evt of events.TerritoryDecayChanged) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) continue; if (evt.to) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } else if (ent.isGarrisonHolder()) this.garrisonManager.removeDecayingStructure(evt.entity); } if (addBase) { if (!this.firstBaseConfig) { // This is our first base, let us configure our starting resources this.configFirstBase(gameState); } else { // Let us hope this new base will fix our possible resource shortage this.saveResources = undefined; this.saveSpace = undefined; this.maxFields = false; } } // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) if (this.Config.difficulty < 2) return; for (let entId of this.decayingStructures) { let ent = gameState.getEntityById(entId); if (ent && ent.decaying() && ent.isOwn(PlayerID)) { let capture = ent.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio < 0.50) continue; let decayToGaia = true; for (let i = 1; i < capture.length; ++i) { if (gameState.isPlayerAlly(i) || !capture[i]) continue; decayToGaia = false; break; } if (decayToGaia) continue; let ratioMax = 0.7 + randFloat(0, 0.1); for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.85 + randFloat(0, 0.1); break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; /** Ensure that all requirements are met when phasing up*/ PETRA.HQ.prototype.checkPhaseRequirements = function(gameState, queues) { if (gameState.getNumberOfPhases() == this.currentPhase) return; let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); let plan; let queue; for (let entityReq of requirements) { // Village requirements are met elsewhere by constructing more houses if (entityReq.class == "Village" || entityReq.class == "NotField") continue; if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) continue; switch (entityReq.class) { case "Town": if (!queues.economicBuilding.hasQueuedUnits() && !queues.militaryBuilding.hasQueuedUnits()) { if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/market")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/temple")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/temple", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Forge", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/forge")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge", { "phaseUp": true }); queue = "militaryBuilding"; break; } } break; default: // All classes not dealt with inside vanilla game. // We put them for the time being on the economic queue, except if wonder queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; if (!queues[queue].hasQueuedUnits()) { let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); if (structure && this.canBuild(gameState, structure)) plan = new PETRA.ConstructionPlan(gameState, structure, { "phaseUp": true }); } } if (plan) { if (queue == "wonder") { gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true }); plan.queueToReset = "majorTech"; } else { gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true }); plan.queueToReset = queue; } queues[queue].addPlan(plan); return; } } }; /** Called by any "phase" research plan once it's started */ PETRA.HQ.prototype.OnPhaseUp = function(gameState, phase) { }; /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ PETRA.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // default template let requirementsDef = [ ["costsResource", 1, "food"] ]; let classesDef = ["Support", "Worker"]; let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); // counting the workers that aren't part of a plan let numberOfWorkers = 0; // all workers let numberOfSupports = 0; // only support workers (i.e. non fighting) gameState.getOwnUnits().forEach(ent => { if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) { ++numberOfWorkers; if (ent.hasClass("Support")) ++numberOfSupports; } }); let numberInTraining = 0; gameState.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) { numberInTraining += item.count; if (item.metadata && item.metadata.role && item.metadata.role == "worker" && item.metadata.plan === undefined) { numberOfWorkers += item.count; if (item.metadata.support) numberOfSupports += item.count; } } }); // Anticipate the optimal batch size when this queue will start // and adapt the batch size of the first and second queued workers to the present population // to ease a possible recovery if our population was drastically reduced by an attack // (need to go up to second queued as it is accounted in queueManager) let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); if (queues.villager.plans[0]) { queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); if (queues.villager.plans[1]) queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); } if (queues.citizenSoldier.plans[0]) { queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); if (queues.citizenSoldier.plans[1]) queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); } let numberOfQueuedSupports = queues.villager.countQueuedUnits(); let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; let numberTotal = numberOfWorkers + numberQueued; if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) return; if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) return; if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) return; // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers // and gives a ratio alfa at startup. let supportRatio = this.supportRatio; let alpha = 0.85; if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field"))) supportRatio = Math.min(this.supportRatio, 0.1); if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length) alpha = 0.7; if (gameState.isCeasefireActive()) alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); let template; if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ]; else requirements = [ ["strength", 1] ]; let classes = ["CitizenSoldier", "Infantry"]; // We want at least 33% ranged and 33% melee classes.push(pickRandom(["Ranged", "Melee", "Infantry"])); template = this.findBestTrainableUnit(gameState, classes, requirements); } // If the template variable is empty, the default unit (Support unit) will be used // base "0" means automatic choice of base if (!template && templateDef) queues.villager.addPlan(new PETRA.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); else if (template) queues.citizenSoldier.addPlan(new PETRA.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); }; /** picks the best template based on parameters and classes */ PETRA.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) { let units; if (classes.indexOf("Hero") != -1) units = gameState.findTrainableUnits(classes, []); else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it units = gameState.findTrainableUnits(classes, ["SiegeTower"]); else // We do not want hero when not explicitely specified units = gameState.findTrainableUnits(classes, ["Hero"]); if (!units.length) return undefined; let parameters = requirements.slice(); let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources for (let type in remainingResources) { if (availableResources[type] > 800) continue; if (remainingResources[type] > 800) continue; let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; let toAdd = true; for (let param of parameters) { if (param[0] != "costsResource" || param[2] != type) continue; param[1] = Math.min(param[1], costsResource); toAdd = false; break; } if (toAdd) parameters.push(["costsResource", costsResource, type]); } units.sort((a, b) => { let aCost = 1 + a[1].costSum(); let bCost = 1 + b[1].costSum(); let aValue = 0.1; let bValue = 0.1; for (let param of parameters) { if (param[0] == "strength") { aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; } else if (param[0] == "siegeStrength") { aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; } else if (param[0] == "speed") { aValue += a[1].walkSpeed() * param[1]; bValue += b[1].walkSpeed() * param[1]; } else if (param[0] == "costsResource") { // requires a third parameter which is the resource if (a[1].cost()[param[2]]) aValue *= param[1]; if (b[1].cost()[param[2]]) bValue *= param[1]; } else if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aValue *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bValue *= param[1]; } else API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); } return -aValue/aCost + bValue/bCost; }); return units[0][0]; }; /** * returns an entity collection of workers through BaseManager.pickBuilders * TODO: when same accessIndex, sort by distance */ PETRA.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) { let accessIndex = baseRef.accessIndex; if (!accessIndex) return false; // sorting bases by whether they are on the same accessindex or not. let baseBest = this.baseManagers.slice().sort((a, b) => { if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) return -1; else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) return 1; return 0; }); let needed = number; let workers = new API3.EntityCollection(gameState.sharedScript); for (let base of baseBest) { if (base.ID == baseRef.ID) continue; base.pickBuilders(gameState, workers, needed); if (workers.length >= number) break; needed = number - workers.length; } if (!workers.length) return false; return workers; }; PETRA.HQ.prototype.getTotalResourceLevel = function(gameState) { let total = {}; for (let res of Resources.GetCodes()) total[res] = 0; for (let base of this.baseManagers) for (let res in total) total[res] += base.getResourceLevel(gameState, res); return total; }; /** * Returns the current gather rate * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. */ PETRA.HQ.prototype.GetCurrentGatherRates = function(gameState) { if (!this.turnCache.currentRates) { let currentRates = {}; for (let res of Resources.GetCodes()) currentRates[res] = 0.5 * this.GetTCResGatherer(res); for (let base of this.baseManagers) base.addGatherRates(gameState, currentRates); for (let res of Resources.GetCodes()) currentRates[res] = Math.max(currentRates[res], 0); this.turnCache.currentRates = currentRates; } return this.turnCache.currentRates; }; /** * Returns the wanted gather rate. */ PETRA.HQ.prototype.GetWantedGatherRates = function(gameState) { if (!this.turnCache.wantedRates) this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); return this.turnCache.wantedRates; }; /** * Pick the resource which most needs another worker * How this works: * We get the rates we would want to have to be able to deal with our plans * We get our current rates * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ PETRA.HQ.prototype.pickMostNeededResources = function(gameState, allowedResources = []) { let wantedRates = this.GetWantedGatherRates(gameState); let currentRates = this.GetCurrentGatherRates(gameState); if (!allowedResources.length) allowedResources = Resources.GetCodes(); let needed = []; for (let res of allowedResources) needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] }); needed.sort((a, b) => { if (a.current < a.wanted && b.current < b.wanted) { if (a.current && b.current) return b.wanted / b.current - a.wanted / a.current; if (a.current) return 1; if (b.current) return -1; return b.wanted - a.wanted; } if (a.current < a.wanted || a.wanted && !b.wanted) return -1; if (b.current < b.wanted || b.wanted && !a.wanted) return 1; return a.current - a.wanted - b.current + b.wanted; }); return needed; }; /** * Returns the best position to build a new Civil Center * Whose primary function would be to reach new resources of type "resource". */ PETRA.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) { // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then look for a good spot. Engine.ProfileStart("findEconomicCCLocation"); // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Unit"]))); + const dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClasses(["CivCentre", "Unit"]))); let ccList = []; for (let cc of ccEnts.values()) ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); let dpList = []; for (let dp of dpEnts.values()) dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) }); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let scale = 250 * 250; let proxyAccess; let nbShips = this.navalManager.transportShips.length; if (proximity) // this is our first base { // if our first base, ensure room around radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); // scale is the typical scale at which we want to find a location for our first base // look for bigger scale if we start from a ship (access < 2) or from a small island let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) scale = 400 * 400; } let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; let nearbyRejected = Math.square(120); // Reject if too near from any cc let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc let maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc let cut = 60; if (fromStrategic || proximity) // be less restrictive cut = 30; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // With enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // We require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; if (proxyAccess && nbShips == 0 && proxyAccess != index) continue; let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // Checking distance to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // We will be more tolerant for cc around our oversea docks let oversea = false; if (proximity) // This is our first cc, let's do it near our units norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale; else { let minDist = Math.min(); let accessible = false; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < nearbyRejected) { norm = 0; break; } if (!cc.ally) continue; if (dist < nearbyAllyRejected) { norm = 0; break; } if (dist < nearbyAllyDisfavored) norm *= 0.5; if (dist < minDist) minDist = dist; accessible = accessible || index == PETRA.getLandAccess(gameState, cc.ent); } if (norm == 0) continue; if (accessible && minDist > maxAccessRejected) continue; if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc { if (!accessible) { if (minDist > maxNoAccessDisfavored) norm *= 0.5; else norm *= 0.8; } else norm *= 0.5; } // Not near any of our dropsite, except for oversea docks oversea = !accessible && dpList.some(dp => PETRA.getLandAccess(gameState, dp.ent) == index); if (!oversea) { for (let dp of dpList) { let dist = API3.SquareVectorDistance(dp.pos, pos); if (dist < 3600) { norm = 0; break; } else if (dist < 6400) norm *= 0.5; } } if (norm == 0) continue; } if (this.borderMap.map[j] & PETRA.fullBorder_Mask) // disfavor the borders of the map norm *= 0.5; let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j]; for (let res in gameState.sharedScript.resourceMaps) if (res != "food") val += gameState.sharedScript.ccResourceMaps[res].map[j]; val *= norm; // If oversea, be just above threshold to be accepted if nothing else if (oversea) val = Math.max(val, cut + 0.1); if (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); if (bestVal === undefined) return false; if (this.Config.debug > 1) API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); // not good enough. if (bestVal < cut) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new Civil Center * Whose primary function would be to assure territorial continuity with our allies */ PETRA.HQ.prototype.findStrategicCCLocation = function(gameState, template) { // This builds a map. The procedure is fairly simple. // We minimize the Sum((dist - 300)^2) where the sum is on the three nearest allied CC // with the constraints that all CC have dist > 200 and at least one have dist < 400 // This needs at least 2 CC. Otherwise, go back to economic CC. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let ccList = []; let numAllyCC = 0; for (let cc of ccEnts.values()) { let ally = gameState.isPlayerAlly(cc.owner()); ccList.push({ "pos": cc.position(), "ally": ally }); if (ally) ++numAllyCC; } if (numAllyCC < 2) return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); Engine.ProfileStart("findStrategicCCLocation"); // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let currentVal, delta; let distcc0, distcc1, distcc2; let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; // checking distances to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; let minDist = Math.min(); distcc0 = undefined; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { minDist = 0; break; } if (!cc.ally) continue; if (dist < 62000) // Reject if quite near from ally cc { minDist = 0; break; } if (dist < minDist) minDist = dist; if (!distcc0 || dist < distcc0) { distcc2 = distcc1; distcc1 = distcc0; distcc0 = dist; } else if (!distcc1 || dist < distcc1) { distcc2 = distcc1; distcc1 = dist; } else if (!distcc2 || dist < distcc2) distcc2 = dist; } if (minDist < 1 || minDist > 170000 && !this.navalMap) continue; delta = Math.sqrt(distcc0) - favoredDistance; currentVal = delta*delta; delta = Math.sqrt(distcc1) - favoredDistance; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - favoredDistance; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] & PETRA.fullBorder_Mask) currentVal += 10000; if (bestVal !== undefined && currentVal > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = currentVal; bestIdx = i; } if (this.Config.debug > 1) API3.warn("We've found a strategic base with bestVal = " + bestVal); Engine.ProfileStop(); if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new market: if the allies already have a market, build it as far as possible * from it, although not in our border to be able to defend it easily. If no allied market, our second market will * follow the same logic. * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance * for performance reasons. */ PETRA.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (!markets.length) markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures()).toEntityArray(); if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; // No need for more than one market when we cannot trade. if (!Resources.GetTradableCodes().length) return false; // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let bestDistSq; let bestGainMult; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - let isNavalMarket = template.hasClass("Naval") && template.hasClass("Trade"); + const isNavalMarket = template.hasClasses(["Naval+Trade"]); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let j = 0; j < this.territoryMap.length; ++j) { // do not try on the narrow border of our territory if (this.borderMap.map[j] & PETRA.narrowFrontier_Mask) continue; if (this.basesMap.map[j] == 0) // only in our territory continue; // with enough room around to build the market let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other markets let maxVal = 0; let maxDistSq; let maxGainMult; let gainMultiplier; for (let market of markets) { - if (isNavalMarket && template.hasClass("Naval") && template.hasClass("Trade")) + if (isNavalMarket && template.hasClasses(["Naval+Trade"])) { if (PETRA.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) continue; gainMultiplier = traderTemplatesGains.navalGainMultiplier; } else if (PETRA.getLandAccess(gameState, market) == index && !PETRA.isLineInsideEnemyTerritory(gameState, market.position(), pos)) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let distSq = API3.SquareVectorDistance(market.position(), pos); if (gainMultiplier * distSq > maxVal) { maxVal = gainMultiplier * distSq; maxDistSq = distSq; maxGainMult = gainMultiplier; } } if (maxVal == 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; bestDistSq = maxDistSq; bestGainMult = maxGainMult; bestIdx = i; bestJdx = j; } if (this.Config.debug > 1) API3.warn("We found a market position with bestVal = " + bestVal); if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); if (this.Config.debug > 1) API3.warn("this would give a trading gain of " + expectedGain); // Do not keep it if gain is too small, except if this is our first Market. let idx; if (expectedGain < this.tradeManager.minimalGain) { if (template.hasClass("Market") && !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) idx = -1; // Needed by queueplanBuilding manager to keep that Market. else return false; } else idx = this.basesMap.map[bestJdx]; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, idx, expectedGain]; }; /** * Returns the best position to build defensive buildings (fortress and towers) * Whose primary function is to defend our borders */ PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template) { // We take the point in our territory which is the nearest to any enemy cc // but requiring a minimal distance with our other defensive structures // and not in range of any enemy defensive structure to avoid building under fire. - let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); + const ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Fortress", "Tower"])).toEntityArray(); let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); + filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals { enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); + filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); + filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) return undefined; } enemyStructures = enemyStructures.toEntityArray(); let wonderMode = gameState.getVictoryConditions().has("wonder"); let wonderDistmin; let wonders; if (wonderMode) { wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); wonderMode = wonders.length != 0; if (wonderMode) wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); } // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let isTower = template.hasClass("Tower"); let isFortress = template.hasClass("Fortress"); let radius; if (isFortress) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); for (let j = 0; j < this.territoryMap.length; ++j) { if (!wonderMode) { // do not try if well inside or outside territory if (!(this.borderMap.map[j] & PETRA.fullFrontier_Mask)) continue; if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower) continue; } if (this.basesMap.map[j] == 0) // inaccessible cell continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other structures let minDist = Math.min(); let dista = 0; if (wonderMode) { dista = API3.SquareVectorDistance(wonders[0].position(), pos); if (dista < wonderDistmin) continue; dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder } for (let str of enemyStructures) { if (str.foundationProgress() !== undefined) continue; let strPos = str.position(); if (!strPos) continue; let dist = API3.SquareVectorDistance(strPos, pos); if (dist < 6400) // TODO check on true attack range instead of this 80×80 { minDist = -1; break; } if (str.hasClass("CivCentre") && dist + dista < minDist) minDist = dist + dista; } if (minDist < 0) continue; let cutDist = 900; // 30×30 TODO maybe increase it for (let str of ownStructures) { let strPos = str.position(); if (!strPos) continue; if (API3.SquareVectorDistance(strPos, pos) < cutDist) { minDist = -1; break; } } if (minDist < 0 || minDist == Math.min()) continue; if (bestVal !== undefined && minDist > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = minDist; bestIdx = i; bestJdx = j; } if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.basesMap.map[bestJdx]]; }; PETRA.HQ.prototype.buildTemple = function(gameState, queues) { // at least one market (which have the same queue) should be build before any temple if (queues.economicBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) return; // Try to build a temple earlier if in regicide to recruit healer guards if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide")) return; let templateName = "structures/{civ}/temple"; if (this.canBuild(gameState, "structures/{civ}/temple_vesta")) templateName = "structures/{civ}/temple_vesta"; else if (!this.canBuild(gameState, templateName)) return; queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, templateName)); }; PETRA.HQ.prototype.buildMarket = function(gameState, queues) { if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}/market")) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("Market")) { if (!queues.economicBuilding.paused) { // Put available resources in this market let queueManager = gameState.ai.queueManager; let cost = queues.economicBuilding.plans[0].getCost(); queueManager.setAccounts(gameState, cost, "economicBuilding"); if (!queueManager.canAfford("economicBuilding", cost)) { for (let q in queueManager.queues) { if (q == "economicBuilding") continue; queueManager.transferAccounts(cost, q, "economicBuilding"); if (queueManager.canAfford("economicBuilding", cost)) break; } } } return; } gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market"); plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; /** Build a farmstead */ PETRA.HQ.prototype.buildFarmstead = function(gameState, queues) { // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) return; // Wait to have at least one dropsite and house before the farmstead if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) return; if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) return; if (!this.canBuild(gameState, "structures/{civ}/farmstead")) return; queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/farmstead")); }; /** * Try to build a wonder when required * force = true when called from the victoryManager in case of Wonder victory condition. */ PETRA.HQ.prototype.buildWonder = function(gameState, queues, force = false) { if (queues.wonder && queues.wonder.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}/wonder")) return; if (!force) { let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/wonder")); // Check that we have enough resources to start thinking to build a wonder let cost = template.cost(); let resources = gameState.getResources(); let highLevel = 0; let lowLevel = 0; for (let res in cost) { if (resources[res] && resources[res] > 0.7 * cost[res]) ++highLevel; else if (!resources[res] || resources[res] < 0.3 * cost[res]) ++lowLevel; } if (highLevel == 0 || lowLevel > 1) return; } queues.wonder.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/wonder")); }; /** Build a corral, and train animals there */ PETRA.HQ.prototype.manageCorral = function(gameState, queues) { if (queues.corral.hasQueuedUnits()) return; let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")) && nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral) { if (this.canBuild(gameState, "structures/{civ}/corral")) { queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral")); return; } if (!nCorral) return; } // And train some animals let civ = gameState.getPlayerCiv(); for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) { if (corral.foundationProgress() !== undefined) continue; let trainables = corral.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.isHuntable()) continue; let count = gameState.countEntitiesByType(trainable, true); for (let item of corral.trainingQueue()) count += item.count; if (count > nCorral) continue; queues.corral.addPlan(new PETRA.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); return; } } }; /** * build more houses if needed. * kinda ugly, lots of special cases to both build enough houses but not tooo many… */ PETRA.HQ.prototype.buildMoreHouses = function(gameState, queues) { let houseTemplateString = "structures/{civ}/apartment"; if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) || !this.canBuild(gameState, houseTemplateString)) { houseTemplateString = "structures/{civ}/house"; if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString))) return; } if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return; let numPlanned = queues.house.length(); if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) { let plan = new PETRA.ConstructionPlan(gameState, houseTemplateString); // change the starting condition according to the situation. plan.goRequirement = "houseNeeded"; queues.house.addPlan(plan); } if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) { let houseTemplateName = gameState.applyCiv(houseTemplateString); let houseTemplate = gameState.getTemplate(houseTemplateName); let needed = 0; for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) { if (!houseTemplate.hasClass(entityReq.class)) continue; let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.buildManager.setBuildable(houseTemplateName); this.requireHouses = true; } needed = Math.max(needed, entityReq.count - count); } let houseQueue = queues.house.plans; for (let i = 0; i < numPlanned; ++i) if (houseQueue[i].isGo(gameState)) --needed; else if (needed > 0) { houseQueue[i].goRequirement = undefined; --needed; } } if (this.requireHouses) { let houseTemplate = gameState.getTemplate(gameState.applyCiv(houseTemplateString)); if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) this.requireHouses = undefined; } // When population limit too tight // - if no room to build, try to improve with technology // - otherwise increase temporarily the priority of houses let house = gameState.applyCiv(houseTemplateString); let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; let popBonus = gameState.getTemplate(house).getPopulationBonus(); let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); let priority; if (freeSlots < 5) { if (this.buildManager.isUnbuildable(gameState, house)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to improve with technology"); this.researchManager.researchPopulationBonus(gameState, queues); } else priority = 2 * this.Config.priorities.house; } else priority = this.Config.priorities.house; if (priority && priority != gameState.ai.queueManager.getPriority("house")) gameState.ai.queueManager.changePriority("house", priority); }; /** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.hasQueuedUnits()) return; // First build one cc if all have been destroyed if (this.numPotentialBases() == 0) { this.buildFirstBase(gameState); return; } // Then expand if we have not enough room available for buildings if (this.buildManager.numberMissingRoom(gameState) > 1) { if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build "); this.buildNewBase(gameState, queues); return; } // If we've already planned to phase up, wait a bit before trying to expand if (this.phasing) return; // Finally expand if we have lots of units (threshold depending on the aggressivity value) let activeBases = this.numActiveBases(); let numUnits = gameState.getOwnUnits().length; let numvar = 10 * (1 - this.Config.personality.aggressive); if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) { if (this.Config.debug > 2) API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); this.buildNewBase(gameState, queues); } }; PETRA.HQ.prototype.buildNewBase = function(gameState, queues, resource) { if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) return false; let template; // We require at least one of this civ civCentre as they may allow specific units or techs let hasOwnCC = false; for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) { if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}/civil_centre")) continue; hasOwnCC = true; break; } if (hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) template = "structures/{civ}/military_colony"; else if (this.canBuild(gameState, "structures/{civ}/civil_centre")) template = "structures/{civ}/civil_centre"; else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) template = "structures/{civ}/military_colony"; else return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; /** Deals with building fortresses and towers along our border with enemies. */ PETRA.HQ.prototype.buildDefenses = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) return; if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) { // Try to build fortresses. if (this.canBuild(gameState, "structures/{civ}/fortress")) { let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) && numFortresses < this.numActiveBases() + 1 + this.extraFortresses && numFortresses < Math.floor(gameState.getPopulation() / 25) && gameState.getOwnFoundationsByClass("Fortress").length < 2) { this.fortressStartTime = gameState.ai.elapsedTime; if (!numFortresses) gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/fortress"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); return; } } } if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}/sentry_tower")) { // Count all towers + wall towers. let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length; let towerLapseTime = this.saveResource ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime; if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) { this.fortStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/sentry_tower")); } return; } if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}/defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) && numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && numTowers < Math.floor(gameState.getPopulation() / 8) && gameState.getOwnFoundationsByClass("Tower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; if (numTowers > 2 * this.numActiveBases() + 3) gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding)); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/defense_tower"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); } }; PETRA.HQ.prototype.buildForge = function(gameState, queues) { if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge || queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length) return; // Build a Market before the Forge. if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities()) return; if (this.canBuild(gameState, "structures/{civ}/forge")) queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge")); }; /** * Deals with constructing military buildings (e.g. barracks, stable). * They are mostly defined by Config.js. This is unreliable since changes could be done easily. */ PETRA.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) return; let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; if (this.saveResources && numBarracks != 0) return; let barracksTemplate = this.canBuild(gameState, "structures/{civ}/barracks") ? "structures/{civ}/barracks" : undefined; let rangeTemplate = this.canBuild(gameState, "structures/{civ}/range") ? "structures/{civ}/range" : undefined; let numRanges = gameState.getOwnEntitiesByClass("Range", true).length; let stableTemplate = this.canBuild(gameState, "structures/{civ}/stable") ? "structures/{civ}/stable" : undefined; let numStables = gameState.getOwnEntitiesByClass("Stable", true).length; if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) { // First barracks/range and stable. if (numBarracks + numRanges == 0) { let template = barracksTemplate || rangeTemplate; if (template) { gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); let plan = new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }); plan.queueToReset = "militaryBuilding"; queues.militaryBuilding.addPlan(plan); return; } } if (numStables == 0 && stableTemplate) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Second barracks/range and stable. if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); if (template) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Third barracks/range and stable, if needed. if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) { let template = barracksTemplate || stableTemplate || rangeTemplate; if (template) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } } if (this.saveResources) return; if (this.currentPhase < 3) return; if (this.canBuild(gameState, "structures/{civ}/elephant_stable") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities()) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/elephant_stable", { "militaryBase": true })); return; } if (this.canBuild(gameState, "structures/{civ}/arsenal") && !gameState.getOwnEntitiesByClass("Arsenal", true).hasEntities()) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/arsenal", { "militaryBase": true })); return; } if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) return; // Build advanced military buildings let nAdvanced = 0; for (let advanced of this.bAdvanced) nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) { for (let advanced of this.bAdvanced) { if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) continue; let template = gameState.getTemplate(advanced); if (!template) continue; let civ = gameState.getPlayerCiv(); if (template.hasDefensiveFire() || template.trainableEntities(civ)) queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced, { "militaryBase": true })); else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced)); return; } } }; /** * Find base nearest to ennemies for military buildings. */ PETRA.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase; let enemyFound = false; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) continue; let access = PETRA.getLandAccess(gameState, cce); let isEnemy = gameState.isPlayerEnemy(cce.owner()); for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; if (PETRA.getLandAccess(gameState, cc) != access) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); if (!enemyFound && isEnemy) enemyFound = true; else if (dist > distMin) continue; bestBase = cc.getMetadata(PlayerID, "base"); distMin = dist; } } return bestBase; }; /** * train with highest priority ranged infantry in the nearest civil center from a given set of positions * and garrison them there for defense */ PETRA.HQ.prototype.trainEmergencyUnits = function(gameState, positions) { if (gameState.ai.queues.emergency.hasQueuedUnits()) return false; let civ = gameState.getPlayerCiv(); // find nearest base anchor let distcut = 20000; let nearestAnchor; let distmin; for (let pos of positions) { let access = gameState.ai.accessibility.getAccessValue(pos); // check nearest base anchor for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (PETRA.getLandAccess(gameState, base.anchor) != access) continue; if (!base.anchor.trainableEntities(civ)) // base still in construction continue; let queue = base.anchor._entity.trainingQueue; if (queue) { let time = 0; for (let item of queue) if (item.progress > 0 || item.metadata && item.metadata.garrisonType) time += item.timeRemaining; if (time/1000 > 5) continue; } let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (nearestAnchor && dist > distmin) continue; distmin = dist; nearestAnchor = base.anchor; } } if (!nearestAnchor || distmin > distcut) return false; // We will choose randomly ranged and melee units, except when garrisonHolder is full // in which case we prefer melee units let numGarrisoned = this.garrisonManager.numberOfGarrisonedSlots(nearestAnchor); if (nearestAnchor._entity.trainingQueue) { for (let item of nearestAnchor._entity.trainingQueue) { if (item.metadata && item.metadata.garrisonType) numGarrisoned += item.count; else if (!item.progress && (!item.metadata || !item.metadata.trainer)) nearestAnchor.stopProduction(item.id); } } let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); let rangedWanted = randBool() && autogarrison; let total = gameState.getResources(); let templateFound; let trainables = nearestAnchor.trainableEntities(civ); let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); - if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier")) + if (!template || !template.hasClasses(["Infantry+CitizenSoldier"])) continue; - if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses)) + if (autogarrison && !template.hasClasses(garrisonArrowClasses)) continue; if (!total.canAfford(new API3.Resources(template.cost()))) continue; templateFound = [trainable, template]; if (template.hasClass("Ranged") == rangedWanted) break; } if (!templateFound) return false; // Check first if we can afford it without touching the other accounts // and if not, take some of other accounted resources // TODO sort the queues to be substracted let queueManager = gameState.ai.queueManager; let cost = new API3.Resources(templateFound[1].cost()); queueManager.setAccounts(gameState, cost, "emergency"); if (!queueManager.canAfford("emergency", cost)) { for (let q in queueManager.queues) { if (q == "emergency") continue; queueManager.transferAccounts(cost, q, "emergency"); if (queueManager.canAfford("emergency", cost)) break; } } let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; if (autogarrison) metadata.garrisonType = "protection"; gameState.ai.queues.emergency.addPlan(new PETRA.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); return true; }; PETRA.HQ.prototype.canBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.buildManager.isUnbuildable(gameState, type)) return false; if (gameState.isTemplateDisabled(type)) { this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); return false; } let template = gameState.getTemplate(type); if (!template) { this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); return false; } if (!template.available(gameState)) { this.buildManager.setUnbuildable(gameState, type, 30, "tech"); return false; } if (!this.buildManager.hasBuilder(type)) { this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); return false; } if (this.numActiveBases() < 1) { // if no base, check that we can build outside our territory let buildTerritories = template.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) { this.buildManager.setUnbuildable(gameState, type, 180, "room"); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) { this.buildManager.setUnbuildable(gameState, type, 90, "limit"); return false; } return true; }; PETRA.HQ.prototype.updateTerritories = function(gameState) { const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; let alliedVictory = gameState.getAlliedVictory(); let passabilityMap = gameState.getPassabilityMap(); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let insideSmall = Math.round(45 / cellSize); let insideLarge = Math.round(80 / cellSize); // should be about the range of towers let expansion = 0; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.borderMap.map[j] & PETRA.outside_Mask) continue; if (this.borderMap.map[j] & PETRA.fullFrontier_Mask) this.borderMap.map[j] &= ~PETRA.fullFrontier_Mask; // reset the frontier if (this.territoryMap.getOwnerIndex(j) != PlayerID) { // If this tile was already accounted, remove it if (this.basesMap.map[j] == 0) continue; let base = this.getBaseByID(this.basesMap.map[j]); if (base) { let index = base.territoryIndices.indexOf(j); if (index != -1) base.territoryIndices.splice(index, 1); else API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); } else API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]); this.basesMap.map[j] = 0; } else { // Update the frontier let ix = j%width; let iz = Math.floor(j/width); let onFrontier = false; for (let a of around) { let jx = ix + Math.round(insideSmall*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(insideSmall*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) { this.borderMap.map[j] |= PETRA.narrowFrontier_Mask; break; } jx = ix + Math.round(insideLarge*a[0]); if (jx < 0 || jx >= width) continue; jz = iz + Math.round(insideLarge*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) onFrontier = true; } if (onFrontier && !(this.borderMap.map[j] & PETRA.narrowFrontier_Mask)) this.borderMap.map[j] |= PETRA.largeFrontier_Mask; // If this tile was not already accounted, add it. if (this.basesMap.map[j] != 0) continue; let landPassable = false; let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); let access; for (let k of ind) { if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) continue; landPassable = true; access = gameState.ai.accessibility.landPassMap[k]; break; } if (!landPassable) continue; let distmin = Math.min(); let baseID; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (!baseID) continue; this.getBaseByID(baseID).territoryIndices.push(j); this.basesMap.map[j] = baseID; expansion++; } } if (!expansion) return; // We've increased our territory, so we may have some new room to build this.buildManager.resetMissingRoom(gameState); // And if sufficient expansion, check if building a new market would improve our present trade routes let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; if (expansion * cellArea > 960) this.tradeManager.routeProspection = true; }; /** Reassign territories when a base is going to be deleted */ PETRA.HQ.prototype.reassignTerritories = function(deletedBase) { let cellSize = this.territoryMap.cellSize; let width = this.territoryMap.width; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.basesMap.map[j] != deletedBase.ID) continue; if (this.territoryMap.getOwnerIndex(j) != PlayerID) { API3.warn("Petra reassignTerritories: should never happen"); this.basesMap.map[j] = 0; continue; } let distmin = Math.min(); let baseID; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != deletedBase.accessIndex) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (baseID) { this.getBaseByID(baseID).territoryIndices.push(j); this.basesMap.map[j] = baseID; } else this.basesMap.map[j] = 0; } }; /** * returns the base corresponding to baseID */ PETRA.HQ.prototype.getBaseByID = function(baseID) { for (let base of this.baseManagers) if (base.ID == baseID) return base; return undefined; }; /** * returns the number of bases with a cc * ActiveBases includes only those with a built cc * PotentialBases includes also those with a cc in construction */ PETRA.HQ.prototype.numActiveBases = function() { if (!this.turnCache.base) this.updateBaseCache(); return this.turnCache.base.active; }; PETRA.HQ.prototype.numPotentialBases = function() { if (!this.turnCache.base) this.updateBaseCache(); return this.turnCache.base.potential; }; PETRA.HQ.prototype.updateBaseCache = function() { this.turnCache.base = { "active": 0, "potential": 0 }; for (let base of this.baseManagers) { if (!base.anchor) continue; ++this.turnCache.base.potential; if (base.anchor.foundationProgress() === undefined) ++this.turnCache.base.active; } }; PETRA.HQ.prototype.resetBaseCache = function() { this.turnCache.base = undefined; }; /** * Count gatherers returning resources in the number of gatherers of resourceSupplies * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). */ PETRA.HQ.prototype.assignGatherers = function() { for (let base of this.baseManagers) { for (let worker of base.workers.values()) { if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) continue; let orders = worker.unitAIOrderData(); if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) continue; this.AddTCGatherer(orders[1].target); } } }; PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) { return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); }; /** Check that the chosen position is not too near from an invading army */ PETRA.HQ.prototype.isNearInvadingArmy = function(pos) { for (let army of this.defenseManager.armies) if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) return true; return false; }; PETRA.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) { if (!this.turnCache.firingStructures) this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); for (let ent of this.turnCache.firingStructures.values()) { let range = radius + ent.attackRange("Ranged").max; if (API3.SquareVectorDistance(ent.position(), pos) < range*range) return true; } return false; }; /** Compute the capture strength of all units attacking a capturable target */ PETRA.HQ.prototype.updateCaptureStrength = function(gameState) { this.capturableTargets.clear(); for (let ent of gameState.getOwnUnits().values()) { if (!ent.canCapture()) continue; let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") 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.isCapturable() || !ent.canCapture(target)) continue; if (!this.capturableTargets.has(targetId)) this.capturableTargets.set(targetId, { "strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"), "ents": new Set([ent.id()]) }); else { let capturableTarget = this.capturableTargets.get(target.id()); capturableTarget.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturableTarget.ents.add(ent.id()); } } for (let [targetId, capturableTarget] of this.capturableTargets) { let target = gameState.getEntityById(targetId); let allowCapture; for (let entId of capturableTarget.ents) { let ent = gameState.getEntityById(entId); if (allowCapture === undefined) allowCapture = PETRA.allowCapture(gameState, ent, target); let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].attackType) continue; if ((orderData[0].attackType == "Capture") !== allowCapture) ent.attack(targetId, allowCapture); } } this.capturableTargetsTime = gameState.ai.elapsedTime; }; /** Some functions that register that we assigned a gatherer to a resource this turn */ /** add a gatherer to the turn cache for this supply. */ PETRA.HQ.prototype.AddTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) ++this.turnCache.resourceGatherer[supplyID]; else { if (!this.turnCache.resourceGatherer) this.turnCache.resourceGatherer = {}; this.turnCache.resourceGatherer[supplyID] = 1; } }; /** remove a gatherer to the turn cache for this supply. */ PETRA.HQ.prototype.RemoveTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) --this.turnCache.resourceGatherer[supplyID]; else { if (!this.turnCache.resourceGatherer) this.turnCache.resourceGatherer = {}; this.turnCache.resourceGatherer[supplyID] = -1; } }; PETRA.HQ.prototype.GetTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) return this.turnCache.resourceGatherer[supplyID]; return 0; }; /** The next two are to register that we assigned a gatherer to a resource this turn. */ PETRA.HQ.prototype.AddTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) ++this.turnCache["resourceGatherer-" + resource]; else this.turnCache["resourceGatherer-" + resource] = 1; if (this.turnCache.currentRates) this.turnCache.currentRates[resource] += 0.5; }; PETRA.HQ.prototype.GetTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) return this.turnCache["resourceGatherer-" + resource]; return 0; }; /** * flag a resource as exhausted */ PETRA.HQ.prototype.isResourceExhausted = function(resource) { if (this.turnCache["exhausted-" + resource] == undefined) this.turnCache["exhausted-" + resource] = this.baseManagers.every(base => !base.dropsiteSupplies[resource].nearby.length && !base.dropsiteSupplies[resource].medium.length && !base.dropsiteSupplies[resource].faraway.length); return this.turnCache["exhausted-" + resource]; }; /** * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) */ PETRA.HQ.prototype.isDefendable = function(ent) { if (!this.turnCache.numAround) this.turnCache.numAround = {}; if (this.turnCache.numAround[ent.id()] === undefined) this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); return +this.turnCache.numAround[ent.id()] > 8; }; /** * Get the number of population already accounted for */ PETRA.HQ.prototype.getAccountedPopulation = function(gameState) { if (this.turnCache.accountedPopulation == undefined) { let pop = gameState.getPopulation(); for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.unitTemplate) continue; let unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population"); if (unitPop) pop += item.count * unitPop; } } this.turnCache.accountedPopulation = pop; } return this.turnCache.accountedPopulation; }; /** * Get the number of workers already accounted for */ PETRA.HQ.prototype.getAccountedWorkers = function(gameState) { if (this.turnCache.accountedWorkers == undefined) { let workers = gameState.getOwnEntitiesByRole("worker", true).length; for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.metadata || !item.metadata.role || item.metadata.role != "worker") continue; workers += item.count; } } this.turnCache.accountedWorkers = workers; } return this.turnCache.accountedWorkers; }; /** * Some functions are run every turn * Others once in a while */ PETRA.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.turnCache = {}; this.territoryMap = PETRA.createTerritoryMap(gameState); this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(API3.Filters.isBuilt()).hasEntities(); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) { if (this.Config.debug > 0) API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + " phasing " + this.phasing); this.currentPhase = gameState.currentPhase(); // In principle, this.phasing should be already reset to 0 when starting the research // but this does not work in case of an autoResearch tech if (this.phasing) this.phasing = 0; } /* if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; PETRA.dumpEntity(ent); }); } */ this.checkEvents(gameState, events); this.navalManager.checkEvents(gameState, queues, events); if (this.phasing) this.checkPhaseRequirements(gameState, queues); else this.researchManager.checkPhase(gameState, queues); if (this.numActiveBases() > 0) { if (gameState.ai.playedTurn % 4 == 0) this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 4 == 1) this.buildMoreHouses(gameState, queues); if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) this.buildFarmstead(gameState, queues); if (this.needCorral && gameState.ai.playedTurn % 4 == 3) this.manageCorral(gameState, queues); if (gameState.ai.playedTurn % 5 == 1) this.researchManager.update(gameState, queues); } if (this.numPotentialBases() < 1 || this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) this.checkBaseExpansion(gameState, queues); if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) { if (!this.canBarter) this.buildMarket(gameState, queues); if (!this.saveResources) { this.buildForge(gameState, queues); this.buildTemple(gameState, queues); } if (gameState.ai.playedTurn % 30 == 0 && gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) this.buildWonder(gameState, queues, false); } this.tradeManager.update(gameState, events, queues); this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); if (gameState.ai.playedTurn % 3 == 0) { this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > 0) this.buildDefenses(gameState, queues); } this.assignGatherers(); let nbBases = this.baseManagers.length; let activeBase; // We will loop only on 1 active base per turn do { this.currentBase %= this.baseManagers.length; activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); --nbBases; // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); } while (!activeBase && nbBases != 0); this.navalManager.update(gameState, queues, events); if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); this.victoryManager.update(gameState, events, queues); // We update the capture strength at the end as it can change attack orders if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3) this.updateCaptureStrength(gameState); Engine.ProfileStop(); }; PETRA.HQ.prototype.Serialize = function() { let properties = { "phasing": this.phasing, "currentBase": this.currentBase, "lastFailedGather": this.lastFailedGather, "firstBaseConfig": this.firstBaseConfig, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "fortressStartTime": this.fortressStartTime, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "needCorral": this.needCorral, "needFarm": this.needFarm, "needFish": this.needFish, "maxFields": this.maxFields, "canExpand": this.canExpand, "canBuildUnits": this.canBuildUnits, "navalMap": this.navalMap, "landRegions": this.landRegions, "navalRegions": this.navalRegions, "decayingStructures": this.decayingStructures, "capturableTargets": this.capturableTargets, "capturableTargetsTime": this.capturableTargetsTime }; let baseManagers = []; for (let base of this.baseManagers) baseManagers.push(base.Serialize()); if (this.Config.debug == -100) { API3.warn(" HQ serialization ---------------------"); API3.warn(" properties " + uneval(properties)); API3.warn(" baseManagers " + uneval(baseManagers)); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); } return { "properties": properties, "baseManagers": baseManagers, "attackManager": this.attackManager.Serialize(), "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), "tradeManager": this.tradeManager.Serialize(), "navalManager": this.navalManager.Serialize(), "researchManager": this.researchManager.Serialize(), "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "victoryManager": this.victoryManager.Serialize(), }; }; PETRA.HQ.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.baseManagers = []; for (let base of data.baseManagers) { // the first call to deserialize set the ID base needed by entitycollections let newbase = new PETRA.BaseManager(gameState, this.Config); newbase.Deserialize(gameState, base); newbase.init(gameState); newbase.Deserialize(gameState, base); this.baseManagers.push(newbase); } this.navalManager = new PETRA.NavalManager(this.Config); this.navalManager.init(gameState, true); this.navalManager.Deserialize(gameState, data.navalManager); this.attackManager = new PETRA.AttackManager(this.Config); this.attackManager.Deserialize(gameState, data.attackManager); this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); this.buildManager = new PETRA.BuildManager(); this.buildManager.Deserialize(data.buildManager); this.defenseManager = new PETRA.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); this.tradeManager = new PETRA.TradeManager(this.Config); this.tradeManager.init(gameState); this.tradeManager.Deserialize(gameState, data.tradeManager); this.researchManager = new PETRA.ResearchManager(this.Config); this.researchManager.Deserialize(data.researchManager); this.diplomacyManager = new PETRA.DiplomacyManager(this.Config); this.diplomacyManager.Deserialize(data.diplomacyManager); this.garrisonManager = new PETRA.GarrisonManager(this.Config); this.garrisonManager.Deserialize(data.garrisonManager); this.victoryManager = new PETRA.VictoryManager(this.Config); this.victoryManager.Deserialize(data.victoryManager); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 25584) @@ -1,888 +1,888 @@ /** * Naval Manager * Will deal with anything ships. * -Basically trade over water (with fleets and goals commissioned by the economy manager) * -Defense over water (commissioned by the defense manager) * -Transport of units over water (a few units). * -Scouting, ultimately. * Also deals with handling docks, making sure we have access and stuffs like that. */ PETRA.NavalManager = function(Config) { this.Config = Config; // ship subCollections. Also exist for land zones, idem, not caring. this.seaShips = []; this.seaTransportShips = []; this.seaWarShips = []; this.seaFishShips = []; // wanted NB per zone. this.wantedTransportShips = []; this.wantedWarShips = []; this.wantedFishShips = []; // needed NB per zone. this.neededTransportShips = []; this.neededWarShips = []; this.transportPlans = []; // shore-line regions where we can load and unload units this.landingZones = {}; }; /** More initialisation for stuff that needs the gameState */ PETRA.NavalManager.prototype.init = function(gameState, deserializing) { // docks - this.docks = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Dock", "Shipyard"])); + this.docks = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Dock", "Shipyard"])); this.docks.registerUpdates(); this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader")))); // note: those two can overlap (some transport ships are warships too and vice-versa). this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat")))); this.warShips = this.ships.filter(API3.Filters.byClass("Warship")); this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat")); this.ships.registerUpdates(); this.transportShips.registerUpdates(); this.warShips.registerUpdates(); this.fishShips.registerUpdates(); let availableFishes = {}; for (let fish of gameState.getFishableSupplies().values()) { let sea = this.getFishSea(gameState, fish); if (sea && availableFishes[sea]) availableFishes[sea] += fish.resourceSupplyAmount(); else if (sea) availableFishes[sea] = fish.resourceSupplyAmount(); } for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { if (!gameState.ai.HQ.navalRegions[i]) { // push dummies this.seaShips.push(undefined); this.seaTransportShips.push(undefined); this.seaWarShips.push(undefined); this.seaFishShips.push(undefined); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } else { let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaShips.push(collec); collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaTransportShips.push(collec); collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaWarShips.push(collec); collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaFishShips.push(collec); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); if (availableFishes[i] && availableFishes[i] > 1000) this.wantedFishShips.push(this.Config.Economy.targetNumFishers); else this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } } if (deserializing) return; // determination of the possible landing zones let width = gameState.getPassabilityMap().width; let length = width * gameState.getPassabilityMap().height; for (let i = 0; i < length; ++i) { let land = gameState.ai.accessibility.landPassMap[i]; if (land < 2) continue; let naval = gameState.ai.accessibility.navalPassMap[i]; if (naval < 2) continue; if (!this.landingZones[land]) this.landingZones[land] = {}; if (!this.landingZones[land][naval]) this.landingZones[land][naval] = new Set(); this.landingZones[land][naval].add(i); } // and keep only thoses with enough room around when possible for (let land in this.landingZones) { for (let sea in this.landingZones[land]) { let landing = this.landingZones[land][sea]; let nbaround = {}; let nbcut = 0; for (let i of landing) { let nb = 0; if (landing.has(i-1)) nb++; if (landing.has(i+1)) nb++; if (landing.has(i+width)) nb++; if (landing.has(i-width)) nb++; nbaround[i] = nb; nbcut = Math.max(nb, nbcut); } nbcut = Math.min(2, nbcut); for (let i of landing) { if (nbaround[i] < nbcut) landing.delete(i); } } } // Assign our initial docks and ships for (let ship of this.ships.values()) PETRA.setSeaAccess(gameState, ship); for (let dock of this.docks.values()) PETRA.setSeaAccess(gameState, dock); }; PETRA.NavalManager.prototype.updateFishingBoats = function(sea, num) { if (this.wantedFishShips[sea]) this.wantedFishShips[sea] = num; }; PETRA.NavalManager.prototype.resetFishingBoats = function(gameState, sea) { if (sea !== undefined) this.wantedFishShips[sea] = 0; else this.wantedFishShips.fill(0); }; /** Get the sea, cache it if not yet done and check if in opensea */ PETRA.NavalManager.prototype.getFishSea = function(gameState, fish) { let sea = fish.getMetadata(PlayerID, "sea"); if (sea) return sea; const ntry = 4; const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); let width = gameState.ai.accessibility.width; let k = pos[0] + pos[1]*width; sea = gameState.ai.accessibility.navalPassMap[k]; fish.setMetadata(PlayerID, "sea", sea); let radius = 120 / gameState.ai.accessibility.cellSize / ntry; if (around.every(a => { for (let t = 0; t < ntry; ++t) { let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); if (i < 0 || i >= width || j < 0 || j >= width) continue; if (gameState.ai.accessibility.landPassMap[i + j*width] === 1) { let navalPass = gameState.ai.accessibility.navalPassMap[i + j*width]; if (navalPass == sea) return true; else if (navalPass == 1) // we could be outside the map continue; } return false; } return true; })) fish.setMetadata(PlayerID, "opensea", true); return sea; }; /** check if we can safely fish at the fish position */ PETRA.NavalManager.prototype.canFishSafely = function(gameState, fish) { if (fish.getMetadata(PlayerID, "opensea")) return true; const ntry = 2; const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; let territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let radius = 120 / territoryMap.cellSize / ntry; let pos = territoryMap.gamePosToMapPos(fish.position()); return around.every(a => { for (let t = 0; t < ntry; ++t) { let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); if (i < 0 || i >= width || j < 0 || j >= width) continue; let owner = territoryMap.getOwnerIndex(i + j*width); if (owner != 0 && gameState.isPlayerEnemy(owner)) return false; } return true; }); }; /** get the list of seas (or lands) around this region not connected by a dock */ PETRA.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) { let seas = gameState.ai.accessibility.regionLinks[region].slice(); this.docks.forEach(dock => { if (!dock.hasClass("Dock") || PETRA.getLandAccess(gameState, dock) != region) return; let i = seas.indexOf(PETRA.getSeaAccess(gameState, dock)); if (i != -1) seas.splice(i--, 1); }); return seas; }; PETRA.NavalManager.prototype.checkEvents = function(gameState, queues, events) { for (let evt of events.Create) { if (!evt.entity) continue; let ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) + if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && ent.hasClasses(["Dock", "Shipyard"])) PETRA.setSeaAccess(gameState, ent); } for (let evt of events.TrainingFinished) { if (!evt.entities) continue; for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) continue; PETRA.setSeaAccess(gameState, ent); } } for (let evt of events.Destroy) { if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID]) continue; if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter) continue; let plan = this.getPlan(evt.metadata[PlayerID].transporter); if (!plan) continue; let shipId = evt.entityObj.id(); if (this.Config.debug > 1) API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state); if (plan.state == "boarding") { // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan plan.units.forEach(ent => { if (ent.getMetadata(PlayerID, "onBoard") == "onBoard" && ent.position() || ent.getMetadata(PlayerID, "onBoard") == shipId) ent.setMetadata(PlayerID, "onBoard", undefined); }); plan.needTransportShips = !plan.transportShips.hasEntities(); } else if (plan.state == "sailing") { let endIndex = plan.endIndex; for (let ent of plan.units.values()) { if (!ent.position()) // unit from another ship of this plan ... do nothing continue; let access = PETRA.getLandAccess(gameState, ent); let endPos = ent.getMetadata(PlayerID, "endPos"); ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); // nothing else to do if access = endIndex as already at destination // otherwise, we should require another transport // TODO if attacking and no more ships available, remove the units from the attack // to avoid delaying it too much if (access != endIndex) this.requireTransport(gameState, ent, access, endIndex, endPos); } } } for (let evt of events.OwnershipChanged) // capture events { if (evt.to !== PlayerID) continue; let ent = gameState.getEntityById(evt.entity); - if (ent && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) + if (ent && ent.hasClasses(["Dock", "Shipyard"])) PETRA.setSeaAccess(gameState, ent); } }; PETRA.NavalManager.prototype.getPlan = function(ID) { for (let plan of this.transportPlans) if (plan.ID === ID) return plan; return undefined; }; PETRA.NavalManager.prototype.addPlan = function(plan) { this.transportPlans.push(plan); }; /** * complete already existing plan or create a new one for this requirement * (many units can then call this separately and end up in the same plan) * TODO check garrison classes */ PETRA.NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos) { if (!ent.canGarrison()) return false; if (ent.getMetadata(PlayerID, "transport") !== undefined) { if (this.Config.debug > 0) API3.warn("Petra naval manager error: unit " + ent.id() + " has already required a transport"); return false; } let plans = []; for (let plan of this.transportPlans) { if (plan.startIndex != startIndex || plan.endIndex != endIndex || plan.state != "boarding") continue; // Limit the number of siege units per transport to avoid problems when ungarrisoning if (PETRA.isSiegeUnit(ent) && plan.units.filter(unit => PETRA.isSiegeUnit(unit)).length > 3) continue; plans.push(plan); } if (plans.length) { plans.sort(plan => plan.units.length); plans[0].addUnit(ent, endPos); return true; } let plan = new PETRA.TransportPlan(gameState, [ent], startIndex, endIndex, endPos); if (plan.failed) { if (this.Config.debug > 1) API3.warn(">>>> transport plan aborted <<<<"); return false; } plan.init(gameState); this.transportPlans.push(plan); return true; }; /** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */ PETRA.NavalManager.prototype.splitTransport = function(gameState, plan) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan started <<<<"); let newplan = new PETRA.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); if (newplan.failed) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan aborted <<<<"); return false; } newplan.init(gameState); for (let ent of plan.needSplit) { if (ent.getMetadata(PlayerID, "onBoard")) // Should never happen. continue; newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos")); plan.units.updateEnt(ent); } if (newplan.units.length) this.transportPlans.push(newplan); return newplan.units.length != 0; }; /** * create a transport from a garrisoned ship to a land location * needed at start game when starting with a garrisoned ship */ PETRA.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) { let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); if (fromAccess !== 1) return; if (toAccess < 2) return; for (let ship of this.ships.values()) { if (!ship.isGarrisonHolder() || !ship.garrisoned().length) continue; if (ship.getMetadata(PlayerID, "transporter") !== undefined) continue; let units = []; for (let entId of ship.garrisoned()) units.push(gameState.getEntityById(entId)); // TODO check that the garrisoned units have not another purpose let plan = new PETRA.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); if (plan.failed) continue; plan.init(gameState); this.transportPlans.push(plan); } }; // set minimal number of needed ships when a new event (new base or new attack plan) PETRA.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) { if (!sea) return; if (this.wantedTransportShips[sea] < number) this.wantedTransportShips[sea] = number; }; // bumps up the number of ships we want if we need more. PETRA.NavalManager.prototype.checkLevels = function(gameState, queues) { if (queues.ships.hasQueuedUnits()) return; for (let sea = 0; sea < this.neededTransportShips.length; sea++) this.neededTransportShips[sea] = 0; for (let plan of this.transportPlans) { if (!plan.needTransportShips || plan.units.length < 2) continue; let sea = plan.sea; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 || this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) continue; ++this.neededTransportShips[sea]; if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2) { ++this.wantedTransportShips[sea]; return; } } for (let sea = 0; sea < this.neededTransportShips.length; sea++) if (this.neededTransportShips[sea] > 2) ++this.wantedTransportShips[sea]; }; PETRA.NavalManager.prototype.maintainFleet = function(gameState, queues) { if (queues.ships.hasQueuedUnits()) return; if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities()) return; // check if we have enough transport ships per region. for (let sea = 0; sea < this.seaShips.length; ++sea) { if (this.seaShips[sea] === undefined) continue; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) continue; if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) { let template = this.getBestShip(gameState, sea, "transport"); if (template) { queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); continue; } } if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) { let template = this.getBestShip(gameState, sea, "fishing"); if (template) { queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); continue; } } } }; /** assigns free ships to plans that need some */ PETRA.NavalManager.prototype.assignShipsToPlans = function(gameState) { for (let plan of this.transportPlans) if (plan.needTransportShips) plan.assignShip(gameState); }; /** Return true if this ship is likeky (un)garrisoning units */ PETRA.NavalManager.prototype.isShipBoarding = function(ship) { if (!ship.position()) return false; let plan = this.getPlan(ship.getMetadata(PlayerID, "transporter")); if (!plan || !plan.boardingPos[ship.id()]) return false; return API3.SquareVectorDistance(plan.boardingPos[ship.id()], ship.position()) < plan.boardingRange; }; /** let blocking ships move apart from active ships (waiting for a better pathfinder) * TODO Ships entity collections are currently in two parts as the trader ships are dealt with * in the tradeManager. That should be modified to avoid dupplicating all the code here. */ PETRA.NavalManager.prototype.moveApart = function(gameState) { let blockedShips = []; let blockedIds = []; for (let ship of this.ships.values()) { let shipPosition = ship.position(); if (!shipPosition) continue; if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship)) continue; let unitAIState = ship.unitAIState(); if (ship.getMetadata(PlayerID, "transporter") !== undefined || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { let previousPosition = ship.getMetadata(PlayerID, "previousPosition"); if (!previousPosition || previousPosition[0] != shipPosition[0] || previousPosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousPosition", shipPosition); ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); continue; } // New transport ships receive boarding commands only on the following turn. if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) continue; ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); blockedShips.push(ship); blockedIds.push(ship.id()); } else if (ship.isIdle()) { let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || previousIdlePosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); ship.setMetadata(PlayerID, "stationnary", undefined); continue; } if (ship.getMetadata(PlayerID, "stationnary")) continue; ship.setMetadata(PlayerID, "stationnary", true); // Check if there are some treasure around if (PETRA.gatherTreasure(gameState, ship, true)) continue; // Do not stay idle near a dock to not disturb other ships let sea = ship.getMetadata(PlayerID, "sea"); for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) { if (PETRA.getSeaAccess(gameState, dock) != sea) continue; if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) continue; ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); } } } for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) { let shipPosition = ship.position(); if (!shipPosition) continue; let role = ship.getMetadata(PlayerID, "role"); if (!role || role != "trader") // already accounted before continue; let unitAIState = ship.unitAIState(); if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET") { let previousPosition = ship.getMetadata(PlayerID, "previousPosition"); if (!previousPosition || previousPosition[0] != shipPosition[0] || previousPosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousPosition", shipPosition); ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); continue; } // New transport ships receives boarding commands only on the following turn. if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) continue; ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); blockedShips.push(ship); blockedIds.push(ship.id()); } else if (ship.isIdle()) { let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || previousIdlePosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); ship.setMetadata(PlayerID, "stationnary", undefined); continue; } if (ship.getMetadata(PlayerID, "stationnary")) continue; ship.setMetadata(PlayerID, "stationnary", true); // Check if there are some treasure around if (PETRA.gatherTreasure(gameState, ship, true)) continue; // Do not stay idle near a dock to not disturb other ships let sea = ship.getMetadata(PlayerID, "sea"); for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) { if (PETRA.getSeaAccess(gameState, dock) != sea) continue; if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) continue; ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); } } } for (let ship of blockedShips) { let shipPosition = ship.position(); let sea = ship.getMetadata(PlayerID, "sea"); for (let blockingShip of this.seaShips[sea].values()) { if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) continue; let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); let unitAIState = blockingShip.unitAIState(); if (blockingShip.getMetadata(PlayerID, "transporter") === undefined && unitAIState != "INDIVIDUAL.GATHER.APPROACHING" && unitAIState != "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { if (distSquare < 1600) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); } else if (distSquare < 900) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); } for (let blockingShip of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) { if (blockingShip.getMetadata(PlayerID, "sea") != sea) continue; if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) continue; let role = blockingShip.getMetadata(PlayerID, "role"); if (!role || role != "trader") // already accounted before continue; let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); let unitAIState = blockingShip.unitAIState(); if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET") { if (distSquare < 1600) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); } else if (distSquare < 900) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); } } }; PETRA.NavalManager.prototype.buildNavalStructures = function(gameState, queues) { if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) return; if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) { if (queues.dock.countQueuedUnitsWithClass("Dock") === 0 && !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Dock"), API3.Filters.isFoundation())).hasEntities() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/dock")) { let dockStarted = false; for (let base of gameState.ai.HQ.baseManagers) { if (dockStarted) break; if (!base.anchor || base.constructing) continue; let remaining = this.getUnconnectedSeas(gameState, base.accessIndex); for (let sea of remaining) { if (!gameState.ai.HQ.navalRegions[sea]) continue; let wantedLand = {}; wantedLand[base.accessIndex] = true; queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/dock", { "land": wantedLand, "sea": sea })); dockStarted = true; break; } } } } if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || queues.militaryBuilding.hasQueuedUnits()) return; if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() || this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities()) return; // Use in priority resources to build a Market. if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market")) return; let template; if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/super_dock")) template = "structures/{civ}/super_dock"; else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/shipyard")) template = "structures/{civ}/shipyard"; else return; let wantedLand = {}; for (let base of gameState.ai.HQ.baseManagers) if (base.anchor) wantedLand[base.accessIndex] = true; let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); }; /** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ PETRA.NavalManager.prototype.getBestShip = function(gameState, sea, goal) { let civ = gameState.getPlayerCiv(); let trainableShips = []; gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { let trainables = ent.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) trainableShips.push(trainable); } }); let best = 0; let bestShip; let limits = gameState.getEntityLimits(); let current = gameState.getEntityCounts(); for (let trainable of trainableShips) { let template = gameState.getTemplate(trainable); if (!template.available(gameState)) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; let arrows = +(template.getDefaultArrow() || 0); if (goal === "attack") // choose the maximum default arrows { if (best > arrows) continue; best = arrows; } else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport { let capacity = +(template.garrisonMax() || 0); if (capacity < 2) continue; capacity += 10*arrows; if (MatchesClassList(template.garrisonableClasses(), "Siege")) capacity += 50; if (best > capacity) continue; best = capacity; } else if (goal === "fishing") if (!template.hasClass("FishingBoat")) continue; bestShip = trainable; } return bestShip; }; PETRA.NavalManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Naval Manager update"); // close previous transport plans if finished for (let i = 0; i < this.transportPlans.length; ++i) { let remaining = this.transportPlans[i].update(gameState); if (remaining) continue; if (this.Config.debug > 1) API3.warn("no more units on transport plan " + this.transportPlans[i].ID); this.transportPlans[i].releaseAll(); this.transportPlans.splice(i--, 1); } // assign free ships to plans which need them this.assignShipsToPlans(gameState); // and require for more ships/structures if needed if (gameState.ai.playedTurn % 3 === 0) { this.checkLevels(gameState, queues); this.maintainFleet(gameState, queues); this.buildNavalStructures(gameState, queues); } // let inactive ships move apart from active ones (waiting for a better pathfinder) this.moveApart(gameState); Engine.ProfileStop(); }; PETRA.NavalManager.prototype.Serialize = function() { let properties = { "wantedTransportShips": this.wantedTransportShips, "wantedWarShips": this.wantedWarShips, "wantedFishShips": this.wantedFishShips, "neededTransportShips": this.neededTransportShips, "neededWarShips": this.neededWarShips, "landingZones": this.landingZones }; let transports = {}; for (let plan in this.transportPlans) transports[plan] = this.transportPlans[plan].Serialize(); return { "properties": properties, "transports": transports }; }; PETRA.NavalManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.transportPlans = []; for (let i in data.transports) { let dataPlan = data.transports[i]; let plan = new PETRA.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); plan.Deserialize(dataPlan); plan.init(gameState); this.transportPlans.push(plan); } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 25584) @@ -1,946 +1,945 @@ /** * Defines a construction plan, ie a building. * We'll try to fing a good position if non has been provided */ PETRA.ConstructionPlan = function(gameState, type, metadata, position) { if (!PETRA.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; PETRA.ConstructionPlan.prototype = Object.create(PETRA.QueuePlan.prototype); PETRA.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn return false; if (!this.isGo(gameState)) return false; if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) return false; return gameState.ai.HQ.buildManager.hasBuilder(this.type); }; PETRA.ConstructionPlan.prototype.start = function(gameState) { Engine.ProfileStart("Building construction start"); // We don't care which builder we assign, since they won't actually do // the building themselves - all we care about is that there is at least // one unit that can start the foundation (should always be the case here). let builder = gameState.findBuilder(this.type); if (!builder) { API3.warn("petra error: builder not found when starting construction."); Engine.ProfileStop(); return; } let pos = this.findGoodPosition(gameState); if (!pos) { gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); Engine.ProfileStop(); return; } if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") || gameState.getOwnEntitiesByClass("Market", true).hasEntities())) { // Check if this Market is still worth building (others may have been built making it useless). let tradeManager = gameState.ai.HQ.tradeManager; tradeManager.checkRoutes(gameState); if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { Engine.ProfileStop(); return; } } gameState.ai.HQ.turnCache.buildingBuilt = true; if (this.metadata === undefined) this.metadata = { "base": pos.base }; else if (this.metadata.base === undefined) this.metadata.base = pos.base; if (pos.access) this.metadata.access = pos.access; // needed for Docks whose position is on water else this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); if (this.template.buildPlacementType() == "shore") { // adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); else // try with the lowest, move towards us unless we're same { for (let step = 0; step <= 1; step += 0.2) builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, pos.angle, this.metadata); } this.onStart(gameState); Engine.ProfileStop(); if (this.metadata && this.metadata.proximity) gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); }; PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState) { let template = this.template; if (template.buildPlacementType() == "shore") return this.findDockPosition(gameState); let HQ = gameState.ai.HQ; if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; const newpos = base.findBestDropsiteLocation(gameState, type, template._templateName); if (newpos && newpos.quality > 0) { let pos = newpos.pos; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; } } if (!this.position) { if (template.hasClass("CivCentre")) { let pos; if (this.metadata && this.metadata.resource) { let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; // No possible location, try to build instead a dock in a not-enemy island let templateName = gameState.applyCiv("structures/{civ}/dock"); if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) { template = gameState.getTemplate(templateName); if (template && gameState.getResources().canAfford(new API3.Resources(template.cost()))) this.buildOverseaDock(gameState, template); } return false; } - else if (template.hasClass("Tower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) + else if (template.hasClasses(["Tower", "Fortress", "ArmyCamp"])) { let pos = HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; // if this fortress is our first one, just try the standard placement if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) return false; } else if (template.hasClass("Market")) // Docks are done before. { let pos = HQ.findMarketLocation(gameState, template); if (pos && pos[2] > 0) { if (!this.metadata) this.metadata = {}; this.metadata.expectedGain = pos[3]; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; } else if (!pos) return false; } } // Compute each tile's closeness to friendly structures: let placement = new API3.Map(gameState.sharedScript, "territory"); let cellSize = placement.cellSize; // size of each tile let alreadyHasHouses = false; if (this.position) // If a position was specified then place the building as close to it as possible { let x = Math.floor(this.position[0] / cellSize); let z = Math.floor(this.position[1] / cellSize); placement.addInfluence(x, z, 255); } else // No position was specified so try and find a sensible place to build { // give a small > 0 level as the result of addInfluence is constrained to be > 0 // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) if (HQ.basesMap.map[j] == base) placement.set(j, 45); } else { for (let j = 0; j < placement.map.length; ++j) if (HQ.basesMap.map[j] != 0) placement.set(j, 45); } if (!HQ.requireHouses || !template.hasClass("House")) { gameState.getOwnStructures().forEach(function(ent) { let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); let struct = PETRA.getBuiltEntity(gameState, ent); if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) { - if (template.hasClass("Field") || template.hasClass("Corral")) + if (template.hasClasses(["Field", "Corral"])) placement.addInfluence(x, z, 80 / cellSize, 50); else // If this is not a field add a negative influence because we want to leave this area for fields placement.addInfluence(x, z, 80 / cellSize, -20); } else if (template.hasClass("House")) { if (ent.hasClass("House")) { placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses alreadyHasHouses = true; } - else if (!ent.hasClass("Wall") || ent.hasClass("Gate")) + else if (ent.hasClasses(["Gate", "!Wall"])) placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs } - else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && !ent.hasClass("Corral") && - (!ent.hasClass("Wall") || ent.hasClass("Gate")))) + else if (template.hasClass("Farmstead") && !ent.hasClasses(["Field", "Corral"]) && + ent.hasClasses(["Gate", "!Wall"])) placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) placement.addInfluence(x, z, 120 / cellSize, -50); else if (template.hasClass("Military")) placement.addInfluence(x, z, 40 / cellSize, -40); else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) placement.addInfluence(x, z, 60 / cellSize, 40); }); } if (template.hasClass("Farmstead")) { for (let j = 0; j < placement.map.length; ++j) { let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; if (HQ.borderMap.map[j] & PETRA.fullBorder_Mask) value /= 2; // we need space around farmstead, so disfavor map border placement.set(j, value); } } } // Requires to be inside our territory, and inside our base territory if required // and if our first market, put it on border if possible to maximize distance with next Market. let favorBorder = template.hasClass("Market"); let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); let favoredBase = this.metadata && (this.metadata.favoredBase || (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (HQ.basesMap.map[j] != base) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask) placement.set(j, placement.map[j] + 50); else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask)) placement.set(j, placement.map[j] + 10); let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (HQ.basesMap.map[j] == 0) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask) placement.set(j, placement.map[j] + 50); else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask)) placement.set(j, placement.map[j] + 10); let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; else if (favoredBase && HQ.basesMap.map[j] == favoredBase) placement.set(j, placement.map[j] + 100); } } } // Find the best non-obstructed: // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, // this allows room for units to walk between buildings. // note: not for houses and dropsites who ought to be closer to either each other or a resource. // also not for fields who can be stacked quite a bit let obstructions = PETRA.createObstructionMap(gameState, 0, template); // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let radius = 0; - if (template.hasClass("Fortress") || template.hasClass("Arsenal") || + if (template.hasClasses(["Fortress", "Arsenal"]) || this.type == gameState.applyCiv("structures/{civ}/elephant_stable")) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && - !template.hasClass("Field") && !template.hasClass("Market")) + else if (template.resourceDropsiteTypes() === undefined && !template.hasClasses(["House", "Field", "Market"])) radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let bestTile; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); if (!bestTile.val) bestTile = undefined; } if (!bestTile) bestTile = placement.findBestTile(radius, obstructions); if (!bestTile.val) return false; let bestIdx = bestTile.idx; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; let territorypos = placement.gamePosToMapPos([x, z]); let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory * => we try to minimize distance from our current point * metadata.oversea is defined for dock in oversea islands * => we try to maximize distance to our current docks (for trade) * otherwise standard dock on an island where we already have a cc * => we try not to be too far from our territory * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. */ PETRA.ConstructionPlan.prototype.findDockPosition = function(gameState) { let template = this.template; let territoryMap = gameState.ai.HQ.territoryMap; let obstructions = PETRA.createObstructionMap(gameState, 0, template); // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let bestIdx; let bestJdx; let bestAngle; let bestLand; let bestWater; let bestVal = -1; let navalPassMap = gameState.ai.accessibility.navalPassMap; let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let nbShips = gameState.ai.HQ.navalManager.transportShips.length; let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; if (nbShips == 0 && proxyAccess && proxyAccess > 1) { wantedLand = {}; wantedLand[proxyAccess] = true; } let dropsiteTypes = template.resourceDropsiteTypes(); let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let halfSize = 0; // used for dock angle let halfDepth = 0; // used by checkPlacement let halfWidth = 0; // used by checkPlacement if (template.get("Footprint/Square")) { halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; halfDepth = +template.get("Footprint/Square/@depth") / 2; halfWidth = +template.get("Footprint/Square/@width") / 2; } else if (template.get("Footprint/Circle")) { halfSize = +template.get("Footprint/Circle/@radius"); halfDepth = halfSize; halfWidth = halfSize; } // res is a measure of the amount of resources around, and maxRes is the max value taken into account // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement const maxRes = 10; const maxWater = 16; let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null; let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null; // Normalisation factors (only guessed, no attempt to optimize them) let factor = proxyAccess ? 1 : oversea ? 0.2 : 40; for (let j = 0; j < territoryMap.length; ++j) { if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) continue; let score = 0; if (!proxyAccess && !oversea) { // if not in our (or allied) territory, we do not want it too far to be able to defend it score = this.getFrontierProximity(gameState, j); if (score > 4) continue; score *= factor; } let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; if (wantedSea && navalPassMap[i] != wantedSea) continue; let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // If proximity is given, we look for the nearest point if (proxyAccess) score = API3.VectorDistance(this.metadata.proximity, pos); // Bonus for resources score += 20 * (maxRes - res); if (oversea) { // Not much farther to one of our cc than to enemy ones let enemyDist; let ownDist; for (let cc of ccEnts.values()) { let owner = cc.owner(); if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) continue; let dist = API3.SquareVectorDistance(pos, cc.position()); if (owner == PlayerID && (!ownDist || dist < ownDist)) ownDist = dist; if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) enemyDist = dist; } if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) continue; // And maximize distance for trade. let dockDist = 0; for (let dock of docks.values()) { if (PETRA.getSeaAccess(gameState, dock) != navalPassMap[i]) continue; let dist = API3.SquareVectorDistance(pos, dock.position()); if (dist > dockDist) dockDist = dist; } if (dockDist > 0) { dockDist = Math.sqrt(dockDist); if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away continue; score += factor * (width * cellSize - dockDist); } } // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] & PETRA.fullBorder_Mask) score += 20; // Do a pre-selection, supposing we will have the best possible water if (bestIdx !== undefined && score > bestVal + 5 * maxWater) continue; let x = (i % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; let angle = this.getDockAngle(gameState, x, z, halfSize); if (angle == false) continue; let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) continue; // Final selection now that the checkDockPlacement water is known if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) continue; if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = score + maxWater - ret.water; bestIdx = i; bestJdx = j; bestAngle = angle; bestLand = ret.land; bestWater = ret.water; } if (bestVal < 0) return false; // if no good place with enough water around and still in first phase, wait for expansion at the next phase if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Assign this dock to a base let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; if (!baseIndex) baseIndex = -2; // We'll do an anchorless base around it return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; /** * Find a good island to build a dock. */ PETRA.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template) { let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); if (!docks.hasEntities()) return; let passabilityMap = gameState.getPassabilityMap(); let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let land = {}; let found; for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { if (gameState.ai.accessibility.regionType[i] != "land" || cellArea * gameState.ai.accessibility.regionSize[i] < 3600) continue; let keep = true; for (let dock of docks.values()) { if (PETRA.getLandAccess(gameState, dock) != i) continue; keep = false; break; } if (!keep) continue; let sea; for (let cc of ccEnts.values()) { let ccAccess = PETRA.getLandAccess(gameState, cc); if (ccAccess != i) { if (cc.owner() == PlayerID && !sea) sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); continue; } // Docks on island where we have a cc are already done elsewhere if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) { keep = false; break; } } if (!keep || !sea) continue; land[i] = true; found = true; } if (!found) return; if (!gameState.ai.HQ.navalMap) API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there "); let oldTemplate = this.template; let oldMetadata = this.metadata; this.template = template; let pos; this.metadata = { "land": land, "oversea": true }; pos = this.findDockPosition(gameState); if (pos) { let type = template.templateName(); let builder = gameState.findBuilder(type); this.metadata.base = pos.base; // Adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } this.template = oldTemplate; this.metadata = oldMetadata; }; /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ PETRA.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let seaRef = gameState.ai.accessibility.navalPassMap[k]; if (seaRef < 2) return false; const numPoints = 16; for (let dist = 0; dist < 4; ++dist) { let waterPoints = []; for (let i = 0; i < numPoints; ++i) { let angle = 2 * Math.PI * i / numPoints; pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; pos = gameState.ai.accessibility.gamePosToMapPos(pos); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.navalPassMap[j] == seaRef) waterPoints.push(i); } let length = waterPoints.length; if (!length) continue; let consec = []; for (let i = 0; i < length; ++i) { let count = 0; for (let j = 0; j < length-1; ++j) { if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } let start = 0; let count = 0; for (let c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; } return false; }; /** * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js * to determine the special dock requirements * returns {"land": land index for this dock, "water": amount of water around this spot} */ PETRA.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) { let sz = halfDepth * Math.sin(angle); let cz = halfDepth * Math.cos(angle); // center back position let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); let j = pos[0] + pos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[j]; if (land < 2) return null; // center front position pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) return null; // additional constraints compared to BuildRestriction.js to assure we have enough place to build let sw = halfWidth * Math.cos(angle) * 3 / 4; let cw = halfWidth * Math.sin(angle) * 3 / 4; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; let water = 0; let sp = 15 * Math.sin(angle); let cp = 15 * Math.cos(angle); for (let i = 1; i < 5; ++i) { pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; water += 4; } return { "land": land, "water": water }; }; /** * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension * if the (object) wantedLand is given, this nearest land should have one of these accessibility * if wantedSea is given, this tile should be inside this sea */ const around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], [-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]]; PETRA.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) { let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let dimLand = dimension + 1.5 * cellSize; let dimSea = dimension + 2 * cellSize; let accessibility = gameState.ai.accessibility; let x = (j%width + 0.5) * cellSize; let z = (Math.floor(j/width) + 0.5) * cellSize; let pos = accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*accessibility.width; let landPass = accessibility.landPassMap[k]; if (landPass > 1 && wantedLand && !wantedLand[landPass] || landPass < 2 && accessibility.navalPassMap[k] < 2) return false; for (let a of around) { pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); if (pos[0] < 0 || pos[0] >= accessibility.width) continue; if (pos[1] < 0 || pos[1] >= accessibility.height) continue; k = pos[0] + pos[1]*accessibility.width; landPass = accessibility.landPassMap[k]; if (landPass < 2 || wantedLand && !wantedLand[landPass]) continue; pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); if (pos[0] < 0 || pos[0] >= accessibility.width) continue; if (pos[1] < 0 || pos[1] >= accessibility.height) continue; k = pos[0] + pos[1]*accessibility.width; if (wantedSea && accessibility.navalPassMap[k] != wantedSea || !wantedSea && accessibility.navalPassMap[k] < 2) continue; return true; } return false; }; /** * return a measure of the proximity to our frontier (including our allies) * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m */ PETRA.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) { let alliedVictory = gameState.getAlliedVictory(); let territoryMap = gameState.ai.HQ.territoryMap; let territoryOwner = territoryMap.getOwnerIndex(j); if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) return 0; let borderMap = gameState.ai.HQ.borderMap; let width = territoryMap.width; let step = Math.round(24 / territoryMap.cellSize); let ix = j % width; let iz = Math.floor(j / width); let best = 5; for (let a of around) { for (let i = 1; i < 5; ++i) { let jx = ix + Math.round(i*step*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(i*step*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) { best = Math.min(best, i); break; } } if (best == 1) break; } return best; }; /** * get the sum of the resources (except food) around, inside a given radius * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood */ PETRA.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) { let resourceMaps = gameState.sharedScript.resourceMaps; let w = resourceMaps.wood.width; let cellSize = resourceMaps.wood.cellSize; let size = Math.floor(radius / cellSize); let ix = i % w; let iy = Math.floor(i / w); let total = 0; let nbcell = 0; for (let k of types) { if (k == "food" || !resourceMaps[k]) continue; let weigh0 = k == "wood" ? 2 : 1; for (let dy = 0; dy <= size; ++dy) { let dxmax = size - dy; let ky = iy + dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } if (dy == 0) continue; ky = iy - dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } } } return nbcell ? total / nbcell : 0; }; PETRA.ConstructionPlan.prototype.isGo = function(gameState) { if (this.goRequirement && this.goRequirement == "houseNeeded") { if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/house") && !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/apartment")) return false; if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return false; let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); for (let ent of gameState.getOwnFoundations().values()) { let template = gameState.getBuiltTemplate(ent.templateName()); if (template) freeSlots += template.getPopulationBonus(); } if (gameState.ai.HQ.saveResources) return freeSlots <= 10; if (gameState.getPopulation() > 55) return freeSlots <= 21; if (gameState.getPopulation() > 30) return freeSlots <= 15; return freeSlots <= 10; } return true; }; PETRA.ConstructionPlan.prototype.onStart = function(gameState) { if (this.queueToReset) gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); }; PETRA.ConstructionPlan.prototype.Serialize = function() { return { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position, "goRequirement": this.goRequirement || undefined, "queueToReset": this.queueToReset || undefined }; }; PETRA.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; this.cost = new API3.Resources(); this.cost.Deserialize(data.cost); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 25584) @@ -1,572 +1,572 @@ /** * Determines the strategy to adopt when starting a new game, * depending on the initial conditions */ PETRA.HQ.prototype.gameAnalysis = function(gameState) { // Analysis of the terrain and the different access regions if (!this.regionAnalysis(gameState)) return; this.attackManager.init(gameState); this.buildManager.init(gameState); this.navalManager.init(gameState); this.tradeManager.init(gameState); this.diplomacyManager.init(gameState); // Make a list of buildable structures from the config file this.structureAnalysis(gameState); // Let's get our initial situation here. let nobase = new PETRA.BaseManager(gameState, this.Config); nobase.init(gameState); nobase.accessIndex = 0; this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) if (cc.foundationProgress() === undefined) this.createBase(gameState, cc); else this.createBase(gameState, cc, "unconstructed"); this.updateTerritories(gameState); // Assign entities and resources in the different bases this.assignStartingEntities(gameState); // Sandbox difficulty should not try to expand this.canExpand = this.Config.difficulty != 0; // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks this.canBuildUnits = true; if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities()) { let template = gameState.applyCiv("structures/{civ}/civil_centre"); if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState)) { if (this.Config.debug > 1) API3.warn(" this AI is unable to produce any units"); this.canBuildUnits = false; this.dispatchUnits(gameState); } else this.buildFirstBase(gameState); } // configure our first base strategy if (this.baseManagers.length > 1) this.configFirstBase(gameState); }; /** * Assign the starting entities to the different bases */ PETRA.HQ.prototype.assignStartingEntities = function(gameState) { for (let ent of gameState.getOwnEntities().values()) { // do not affect merchant ship immediately to trade as they may-be useful for transport - if (ent.hasClass("Trader") && !ent.hasClass("Ship")) + if (ent.hasClasses(["Trader+!Ship"])) this.tradeManager.assignTrader(ent); let pos = ent.position(); if (!pos) { // TODO should support recursive garrisoning. Make a warning for now if (ent.isGarrisonHolder() && ent.garrisoned().length) API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); continue; } // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; if (land > 1 && !this.landRegions[land]) this.landRegions[land] = true; let sea = gameState.ai.accessibility.navalPassMap[index]; if (sea > 1 && !this.navalRegions[sea]) this.navalRegions[sea] = true; // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport // when a construction will start (see createTransportIfNeeded) if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) for (let id of ent.garrisoned()) ent.unload(id); let bestbase; let territorypos = this.territoryMap.gamePosToMapPos(pos); let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; for (let i = 1; i < this.baseManagers.length; ++i) { let base = this.baseManagers[i]; if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) && base.territoryIndices.indexOf(territoryIndex) == -1) continue; base.assignEntity(gameState, ent); bestbase = base; break; } if (!bestbase) // entity outside our territory { if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes()) bestbase = this.createBase(gameState, ent, "anchorless"); else bestbase = PETRA.getBestBase(gameState, ent) || this.baseManagers[0]; bestbase.assignEntity(gameState, ent); } // now assign entities garrisoned inside this entity if (ent.isGarrisonHolder() && ent.garrisoned().length) for (let id of ent.garrisoned()) bestbase.assignEntity(gameState, gameState.getEntityById(id)); // and find something useful to do if we already have a base if (pos && bestbase.ID !== this.baseManagers[0].ID) { bestbase.assignRolelessUnits(gameState, [ent]); if (ent.getMetadata(PlayerID, "role") === "worker") { bestbase.reassignIdleWorkers(gameState, [ent]); bestbase.workerObject.update(gameState, ent); } } } }; /** * determine the main land Index (or water index if none) * as well as the list of allowed (land andf water) regions */ PETRA.HQ.prototype.regionAnalysis = function(gameState) { let accessibility = gameState.ai.accessibility; let landIndex; let seaIndex; let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { let land = accessibility.getAccessValue(cc.position()); if (land > 1) { landIndex = land; break; } } if (!landIndex) { let civ = gameState.getPlayerCiv(); for (let ent of gameState.getOwnEntities().values()) { if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ)) continue; let land = accessibility.getAccessValue(ent.position()); if (land > 1) { landIndex = land; break; } let sea = accessibility.getAccessValue(ent.position(), true); if (!seaIndex && sea > 1) seaIndex = sea; } } if (!landIndex && !seaIndex) { API3.warn("Petra error: it does not know how to interpret this map"); return false; } let passabilityMap = gameState.getPassabilityMap(); let totalSize = passabilityMap.width * passabilityMap.width; let minLandSize = Math.floor(0.1*totalSize); let minWaterSize = Math.floor(0.2*totalSize); let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; for (let i = 0; i < accessibility.regionSize.length; ++i) { if (landIndex && i == landIndex) this.landRegions[i] = true; else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320) { if (landIndex) { let sea = this.getSeaBetweenIndices(gameState, landIndex, i); if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize)) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[sea] = true; } } else { let traject = accessibility.getTrajectToIndex(seaIndex, i); if (traject && traject.length === 2) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[seaIndex] = true; } } } else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize) { this.navalMap = true; this.navalRegions[i] = true; } else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600) this.navalRegions[i] = true; } if (this.Config.debug < 3) return true; for (let region in this.landRegions) API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); API3.warn(" navalMap " + this.navalMap); API3.warn(" landRegions " + uneval(this.landRegions)); API3.warn(" navalRegions " + uneval(this.navalRegions)); return true; }; /** * load units and buildings from the config files * TODO: change that to something dynamic */ PETRA.HQ.prototype.structureAnalysis = function(gameState) { let civref = gameState.playerData.civ; let civ = civref in this.Config.buildings ? civref : 'default'; this.bAdvanced = []; for (let building of this.Config.buildings[civ]) if (gameState.isTemplateAvailable(gameState.applyCiv(building))) this.bAdvanced.push(gameState.applyCiv(building)); }; /** * build our first base * if not enough resource, try first to do a dock */ PETRA.HQ.prototype.buildFirstBase = function(gameState) { if (gameState.ai.queues.civilCentre.hasQueuedUnits()) return; let templateName = gameState.applyCiv("structures/{civ}/civil_centre"); if (gameState.isTemplateDisabled(templateName)) return; let template = gameState.getTemplate(templateName); if (!template) return; let total = gameState.getResources(); let goal = "civil_centre"; if (!total.canAfford(new API3.Resources(template.cost()))) { let totalExpected = gameState.getResources(); // Check for treasures around available in some maps at startup for (let ent of gameState.getOwnUnits().values()) { if (!ent.position()) continue; // If we can get a treasure around, just do it if (ent.isIdle()) PETRA.gatherTreasure(gameState, ent); // Then count the resources from the treasures being collected let treasureId = ent.getMetadata(PlayerID, "treasure"); if (!treasureId) continue; let treasure = gameState.getEntityById(treasureId); if (!treasure) continue; let types = treasure.treasureResources(); for (let type in types) if (type in totalExpected) totalExpected[type] += types[type]; // If we can collect enough resources from these treasures, wait for them. if (totalExpected.canAfford(new API3.Resources(template.cost()))) return; } // not enough resource to build a cc, try with a dock to accumulate resources if none yet if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities()) { if (gameState.ai.queues.dock.hasQueuedUnits()) return; templateName = gameState.applyCiv("structures/{civ}/dock"); if (gameState.isTemplateDisabled(templateName)) return; template = gameState.getTemplate(templateName); if (!template || !total.canAfford(new API3.Resources(template.cost()))) return; goal = "dock"; } } if (!this.canBuild(gameState, templateName)) return; // We first choose as startingPoint the point where we have the more units let startingPoint = []; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Worker")) continue; if (PETRA.isFastMoving(ent)) continue; let pos = ent.position(); if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (!holder || !holder.position()) continue; pos = holder.position(); } let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1] * gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; let sea = gameState.ai.accessibility.navalPassMap[index]; let found = false; for (let point of startingPoint) { if (land !== point.land || sea !== point.sea) continue; if (API3.SquareVectorDistance(point.pos, pos) > 2500) continue; point.weight += 1; found = true; break; } if (!found) startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 }); } if (!startingPoint.length) return; let imax = 0; for (let i = 1; i < startingPoint.length; ++i) if (startingPoint[i].weight > startingPoint[imax].weight) imax = i; if (goal == "dock") { let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined; gameState.ai.queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/dock", { "sea": sea, "proximity": startingPoint[imax].pos })); } else gameState.ai.queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); }; /** * set strategy if game without construction: * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack * - otherwise all units will attack */ PETRA.HQ.prototype.dispatchUnits = function(gameState) { let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); if (allycc.length) { if (this.Config.debug > 1) API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); let units = gameState.getOwnUnits(); let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); let num1 = Math.floor(num / 2); let num2 = num1; // first pass to affect ranged infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(ent => { + units.filter(API3.Filters.byClasses(["Infantry+Ranged"])).forEach(ent => { if (!num || !num1) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = PETRA.getLandAccess(gameState, ent); for (let cc of allycc) { if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access) continue; --num; --num1; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5); break; } }); // second pass to affect melee infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(ent => { + units.filter(API3.Filters.byClasses(["Infantry+Melee"])).forEach(ent => { if (!num || !num2) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = PETRA.getLandAccess(gameState, ent); for (let cc of allycc) { if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access) continue; --num; --num2; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5); break; } }); // and now complete the affectation, including all support units units.forEach(ent => { if (!num && !ent.hasClass("Support")) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = PETRA.getLandAccess(gameState, ent); for (let cc of allycc) { if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access) continue; if (!ent.hasClass("Support")) --num; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5); break; } }); } }; /** * configure our first base expansion * - if on a small island, favor fishing * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) */ PETRA.HQ.prototype.configFirstBase = function(gameState) { if (this.baseManagers.length < 2) return; this.firstBaseConfig = true; let startingSize = 0; let startingLand = []; for (let region in this.landRegions) { for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex != +region) continue; startingSize += gameState.ai.accessibility.regionSize[region]; startingLand.push(base.accessIndex); break; } } let cell = gameState.getPassabilityMap().cellSize; startingSize = startingSize * cell * cell; if (this.Config.debug > 1) API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); if (startingSize < 25000) { this.saveSpace = true; this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); let num = Math.max(this.Config.Economy.targetNumFishers, 2); for (let land of startingLand) { for (let sea of gameState.ai.accessibility.regionLinks[land]) if (gameState.ai.HQ.navalRegions[sea]) this.navalManager.updateFishingBoats(sea, num); } this.maxFields = 1; this.needCorral = true; } else if (startingSize < 60000) this.maxFields = 2; else this.maxFields = false; // - count the available food resource, and react accordingly let startingFood = gameState.getResources().food; let check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { for (let base of this.baseManagers) { for (let supply of base.dropsiteSupplies.food[proxim]) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; startingFood += supply.ent.resourceSupplyAmount(); } } } if (startingFood < 800) { if (startingSize < 25000) { this.needFish = true; this.Config.Economy.popForDock = 1; } else this.needFarm = true; } // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) let startingWood = gameState.getResources().wood; check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { for (let base of this.baseManagers) { for (let supply of base.dropsiteSupplies.wood[proxim]) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; startingWood += supply.ent.resourceSupplyAmount(); } } } if (this.Config.debug > 1) API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); if (startingWood < 6000) { this.saveResources = true; this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand if (startingWood < 2000 && this.needFarm) { this.needCorral = true; this.needFarm = false; } } if (startingWood > 8500 && this.canBuildUnits) { let allowed = Math.ceil((startingWood - 8500) / 3000); // Not useful to prepare rushing if too long ceasefire if (gameState.isCeasefireActive()) { if (gameState.ceasefireTimeRemaining > 900) allowed = 0; else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1) allowed = 1; } this.attackManager.setRushes(allowed); } // immediatly build a wood dropsite if possible. if (!gameState.getOwnEntitiesByClass("DropsiteWood", true).hasEntities()) { const newDP = this.baseManagers[1].findBestDropsiteAndLocation(gameState, "wood"); if (newDP.quality > 40 && this.canBuild(gameState, newDP.templateName)) { // if we start with enough workers, put our available resources in this first dropsite // same thing if our pop exceed the allowed one, as we will need several houses let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length; if (numWorkers > 12 && newDP.quality > 60 || gameState.getPopulation() > gameState.getPopulationLimit() + 20) { const cost = new API3.Resources(gameState.getTemplate(newDP.templateName).cost()); gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); } gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.baseManagers[1].ID }, newDP.pos)); } } // and build immediately a corral if needed if (this.needCorral) { const template = gameState.applyCiv("structures/{civ}/corral"); if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 25583) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 25584) @@ -1,1103 +1,1103 @@ /** * This class makes a worker do as instructed by the economy manager */ PETRA.Worker = function(base) { this.ent = undefined; this.base = base; this.baseID = base.ID; }; PETRA.Worker.prototype.update = function(gameState, ent) { if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return; let subrole = ent.getMetadata(PlayerID, "subrole"); // If we are waiting for a transport or we are sailing, just wait if (ent.getMetadata(PlayerID, "transport") !== undefined) { // Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board if (subrole == "builder" && ent.getMetadata(PlayerID, "target-foundation") !== undefined) { let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); if (!target && plan && plan.state == "boarding" && ent.position()) plan.removeUnit(gameState, ent); } // and gatherer if there are no more dropsite accessible in the base the ent is going to if (subrole == "gatherer" || subrole == "hunter") { let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); if (plan.state == "boarding" && ent.position()) { let hasDropsite = false; let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food"; for (let structure of gameState.getOwnStructures().values()) { if (PETRA.getLandAccess(gameState, structure) != plan.endIndex) continue; let resourceDropsiteTypes = PETRA.getBuiltEntity(gameState, structure).resourceDropsiteTypes(); if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) continue; hasDropsite = true; break; } if (!hasDropsite) { for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values()) { if (!unit.position() || PETRA.getLandAccess(gameState, unit) != plan.endIndex) continue; let resourceDropsiteTypes = unit.resourceDropsiteTypes(); if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) continue; hasDropsite = true; break; } } if (!hasDropsite) plan.removeUnit(gameState, ent); } } if (ent.getMetadata(PlayerID, "transport") !== undefined) return; } this.entAccess = PETRA.getLandAccess(gameState, ent); // base 0 for unassigned entities has no accessIndex, so take the one from the entity if (this.baseID == gameState.ai.HQ.baseManagers[0].ID) this.baseAccess = this.entAccess; else this.baseAccess = this.base.accessIndex; if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ... { ent.setMetadata(PlayerID, "subrole", "idle"); this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } this.ent = ent; let unitAIState = ent.unitAIState(); if ((subrole == "hunter" || subrole == "gatherer") && (unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || unitAIState == "INDIVIDUAL.COMBAT.APPROACHING")) { if (this.isInaccessibleSupply(gameState)) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target) { // Check that we have not drifted too far when hunting let target = gameState.getEntityById(orderData.target); if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food") { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(target.position()); if (gameState.isPlayerEnemy(territoryOwner)) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } else if (!gameState.isPlayerAlly(territoryOwner)) { let distanceSquare = PETRA.isFastMoving(ent) ? 90000 : 30000; let targetAccess = PETRA.getLandAccess(gameState, target); let foodDropsites = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); let hasFoodDropsiteWithinDistance = false; for (let dropsite of foodDropsites.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (targetAccess != PETRA.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(target.position(), dropsite.position()) < distanceSquare) { hasFoodDropsiteWithinDistance = true; break; } } if (!hasFoodDropsiteWithinDistance) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } } } } } } else if (ent.getMetadata(PlayerID, "approachingTarget")) { ent.setMetadata(PlayerID, "approachingTarget", undefined); ent.setMetadata(PlayerID, "alreadyTried", undefined); } let unitAIStateOrder = unitAIState.split(".")[1]; // If we're fighting or hunting, let's not start gathering except if inaccessible target // but for fishers where UnitAI must have made us target a moving whale. // Also, if we are attacking, do not capture if (unitAIStateOrder == "COMBAT") { if (subrole == "fisher") this.startFishing(gameState); else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length && !ent.getMetadata(PlayerID, "PartOfArmy")) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target) { let target = gameState.getEntityById(orderData.target); if (target && (!target.position() || PETRA.getLandAccess(gameState, target) != this.entAccess)) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } } } else if (unitAIState == "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length && !ent.getMetadata(PlayerID, "PartOfArmy")) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") { // If we are here, an enemy structure must have targeted one of our workers // and UnitAI sent it fight back with allowCapture=true let target = gameState.getEntityById(orderData.target); if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner())) ent.attack(orderData.target, PETRA.allowCapture(gameState, ent, target)); } } return; } // Okay so we have a few tasks. // If we're gathering, we'll check that we haven't run idle. // And we'll also check that we're gathering a resource we want to gather. if (subrole == "gatherer") { if (ent.isIdle()) { // if we aren't storing resources or it's the same type as what we're about to gather, // let's just pick a new resource. // TODO if we already carry the max we can -> returnresources if (!ent.resourceCarrying() || !ent.resourceCarrying().length || ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type")) { this.startGathering(gameState); } else if (!PETRA.returnResources(gameState, ent)) // try to deposit resources { // no dropsite, abandon old resources and start gathering new ones this.startGathering(gameState); } } else if (unitAIStateOrder == "GATHER") { // we're already gathering. But let's check if there is nothing better // in case UnitAI did something bad if (ent.unitAIOrderData().length) { let supplyId = ent.unitAIOrderData()[0].target; let supply = gameState.getEntityById(supplyId); - if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") && + if (supply && !supply.hasClasses(["Field", "Animal"]) && supplyId != ent.getMetadata(PlayerID, "supply")) { let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { let gatherType = ent.getMetadata(PlayerID, "gather-type"); let nearby = this.base.dropsiteSupplies[gatherType].nearby; if (nearby.some(sup => sup.id == supplyId)) ent.setMetadata(PlayerID, "supply", supplyId); else if (nearby.length) { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { let medium = this.base.dropsiteSupplies[gatherType].medium; if (medium.length && !medium.some(sup => sup.id == supplyId)) { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else ent.setMetadata(PlayerID, "supply", supplyId); } } } } if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { if (gameState.ai.playedTurn % 10 == 0) { // Check from time to time that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite)) PETRA.returnResources(gameState, this.ent); } // If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full. // Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be // reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one). let gatherType = ent.getMetadata(PlayerID, "gather-type"); let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup; if (influenceGroup && influenceGroup == "sparse") { let supplyId = ent.getMetadata(PlayerID, "supply"); if (supplyId) { let nearby = this.base.dropsiteSupplies[gatherType].nearby; if (!nearby.some(sup => sup.id == supplyId)) { if (nearby.length) ent.setMetadata(PlayerID, "supply", undefined); else { let medium = this.base.dropsiteSupplies[gatherType].medium; if (!medium.some(sup => sup.id == supplyId) && medium.length) ent.setMetadata(PlayerID, "supply", undefined); } } } } } } } else if (subrole == "builder") { if (unitAIStateOrder == "REPAIR") { // Update our target in case UnitAI sent us to a different foundation because of autocontinue // and abandon it if UnitAI has sent us to build a field (as we build them only when needed) if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target) { let targetId = ent.unitAIOrderData()[0].target; let target = gameState.getEntityById(targetId); if (target && !target.hasClass("Field")) { ent.setMetadata(PlayerID, "target-foundation", targetId); return; } ent.setMetadata(PlayerID, "target-foundation", undefined); ent.setMetadata(PlayerID, "subrole", "idle"); ent.stopMoving(); if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } } // Otherwise check that the target still exists (useful in REPAIR.APPROACHING) let targetId = ent.getMetadata(PlayerID, "target-foundation"); if (targetId && gameState.getEntityById(targetId)) return; ent.stopMoving(); } // okay so apparently we aren't working. // Unless we've been explicitely told to keep our role, make us idle. let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) { ent.setMetadata(PlayerID, "subrole", "idle"); ent.setMetadata(PlayerID, "target-foundation", undefined); if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } } else { let goalAccess = PETRA.getLandAccess(gameState, target); let queued = PETRA.returnResources(gameState, ent); if (this.entAccess == goalAccess) ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, goalAccess, target.position()); } } else if (subrole == "hunter") { let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20)) { if (!this.startHunting(gameState)) { // nothing to hunt around. Try another region if any let nowhereToHunt = true; for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; let basePos = base.anchor.position(); if (this.startHunting(gameState, basePos)) { ent.setMetadata(PlayerID, "base", base.ID); if (base.accessIndex == this.entAccess) ent.move(basePos[0], basePos[1]); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, base.accessIndex, basePos); nowhereToHunt = false; break; } } if (nowhereToHunt) ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime); } } else // Perform some sanity checks { if (unitAIStateOrder == "GATHER") { // we may have drifted towards ennemy territory during the hunt, if yes go home let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally this.startHunting(gameState); else if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { // Check that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite)) PETRA.returnResources(gameState, ent); } } } } else if (subrole == "fisher") { if (ent.isIdle()) this.startFishing(gameState); else // if we have drifted towards ennemy territory during the fishing, go home { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally this.startFishing(gameState); } } }; PETRA.Worker.prototype.retryWorking = function(gameState, subrole) { switch (subrole) { case "gatherer": return this.startGathering(gameState); case "hunter": return this.startHunting(gameState); case "fisher": return this.startFishing(gameState); case "builder": return this.startBuilding(gameState); default: return false; } }; PETRA.Worker.prototype.startBuilding = function(gameState) { let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) return false; if (PETRA.getLandAccess(gameState, target) != this.entAccess) return false; this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses return true; }; PETRA.Worker.prototype.startGathering = function(gameState) { // First look for possible treasure if any if (PETRA.gatherTreasure(gameState, this.ent)) return true; let resource = this.ent.getMetadata(PlayerID, "gather-type"); // If we are gathering food, try to hunt first if (resource == "food" && this.startHunting(gameState)) return true; let findSupply = function(ent, supplies) { let ret = false; let gatherRates = ent.resourceGatherRates(); for (let i = 0; i < supplies.length; ++i) { // exhausted resource, remove it from this list if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id)) { supplies.splice(i--, 1); continue; } if (PETRA.IsSupplyFull(gameState, supplies[i].ent)) continue; let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) continue; let supplyType = supplies[i].ent.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) continue; // check if available resource is worth one additionnal gatherer (except for farms) let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id); if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 && supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30) continue; // not in ennemy territory let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally continue; gameState.ai.HQ.AddTCGatherer(supplies[i].id); ent.setMetadata(PlayerID, "supply", supplies[i].id); ret = supplies[i].ent; break; } return ret; }; let navalManager = gameState.ai.HQ.navalManager; let supply; // first look in our own base if accessible from our present position if (this.baseAccess == this.entAccess) { supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.gather(supply); return true; } // --> for food, try to gather from fields if any, otherwise build one if any if (resource == "food") { supply = this.gatherNearestField(gameState, this.baseID); if (supply) { this.ent.gather(supply); return true; } supply = this.buildAnyField(gameState, this.baseID); if (supply) { this.ent.repair(supply); return true; } } supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium); if (supply) { this.ent.gather(supply); return true; } } // So if we're here we have checked our whole base for a proper resource (or it was not accessible) // --> check other bases directly accessible for (let base of gameState.ai.HQ.baseManagers) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any { for (let base of gameState.ai.HQ.baseManagers) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; supply = this.gatherNearestField(gameState, base.ID); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } supply = this.buildAnyField(gameState, base.ID); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.repair(supply); return true; } } } for (let base of gameState.ai.HQ.baseManagers) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } // Okay may-be we haven't found any appropriate dropsite anywhere. // Try to help building one if any accessible foundation available let foundations = gameState.getOwnFoundations().toEntityArray(); let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { if (!foundation || PETRA.getLandAccess(gameState, foundation) != this.entAccess) return false; let structure = gameState.getBuiltTemplate(foundation.templateName()); if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) { if (foundation.getMetadata(PlayerID, "base") != this.baseID) this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); this.ent.setMetadata(PlayerID, "subrole", "builder"); this.ent.repair(foundation); return true; } return false; }, this); if (shouldBuild) return true; // Still nothing ... try bases which need a transport for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex == this.entAccess) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any { for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex == this.entAccess) continue; supply = this.gatherNearestField(gameState, base.ID); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } supply = this.buildAnyField(gameState, base.ID); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } } for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex == this.entAccess) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } // Okay so we haven't found any appropriate dropsite anywhere. // Try to help building one if any non-accessible foundation available shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { if (!foundation || PETRA.getLandAccess(gameState, foundation) == this.entAccess) return false; let structure = gameState.getBuiltTemplate(foundation.templateName()); if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) { let foundationAccess = PETRA.getLandAccess(gameState, foundation); if (navalManager.requireTransport(gameState, this.ent, this.entAccess, foundationAccess, foundation.position())) { if (foundation.getMetadata(PlayerID, "base") != this.baseID) this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); this.ent.setMetadata(PlayerID, "subrole", "builder"); return true; } } return false; }, this); if (shouldBuild) return true; // Still nothing, we look now for faraway resources, first in the accessible ones, then in the others // except for food when farms or corrals can be used let allowDistant = true; if (resource == "food") { if (gameState.ai.HQ.turnCache.allowDistantFood === undefined) gameState.ai.HQ.turnCache.allowDistantFood = !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field") && !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"); allowDistant = gameState.ai.HQ.turnCache.allowDistantFood; } if (allowDistant) { if (this.baseAccess == this.entAccess) { supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway); if (supply) { this.ent.gather(supply); return true; } } for (let base of gameState.ai.HQ.baseManagers) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex == this.entAccess) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } } // If we are here, we have nothing left to gather ... certainly no more resources of this type gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime; if (gameState.ai.Config.debug > 2) API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); this.ent.setMetadata(PlayerID, "subrole", "idle"); return false; }; /** * if position is given, we only check if we could hunt from this position but do nothing * otherwise the position of the entity is taken, and if something is found, we directly start the hunt */ PETRA.Worker.prototype.startHunting = function(gameState, position) { // First look for possible treasure if any if (!position && PETRA.gatherTreasure(gameState, this.ent)) return true; let resources = gameState.getHuntableSupplies(); if (!resources.hasEntities()) return false; let nearestSupplyDist = Math.min(); let nearestSupply; let isFastMoving = PETRA.isFastMoving(this.ent); let isRanged = this.ent.hasClass("Ranged"); let entPosition = position ? position : this.ent.position(); let foodDropsites = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare) { for (let dropsite of foodDropsites.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (supplyAccess != PETRA.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) return true; } return false; }; let gatherRates = this.ent.resourceGatherRates(); for (let supply of resources.values()) { if (!supply.position()) continue; let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) continue; let supplyType = supply.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) continue; if (PETRA.IsSupplyFull(gameState, supply)) continue; // Check if available resource is worth one additionnal gatherer (except for farms). let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) continue; let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; // Only FastMoving and Ranged units should hunt fleeing animals. if (canFlee && !isFastMoving && !isRanged) continue; let supplyAccess = PETRA.getLandAccess(gameState, supply); if (supplyAccess != this.entAccess) continue; // measure the distance to the resource. let dist = API3.SquareVectorDistance(entPosition, supply.position()); if (dist > nearestSupplyDist) continue; // Only FastMoving should hunt faraway. if (!isFastMoving && dist > 25000) continue; // Avoid enemy territory. let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // Player is its own ally. continue; // And if in ally territory, don't hunt this ally's cattle. if (territoryOwner != 0 && territoryOwner != PlayerID && supply.owner() == territoryOwner) continue; // Only FastMoving should hunt far from dropsite (specially for non-Domestic animals which flee). if (!isFastMoving && canFlee && territoryOwner == 0) continue; let distanceSquare = isFastMoving ? 35000 : (canFlee ? 7000 : 12000); if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) continue; nearestSupplyDist = dist; nearestSupply = supply; } if (nearestSupply) { if (position) return true; gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); return true; } return false; }; PETRA.Worker.prototype.startFishing = function(gameState) { if (!this.ent.position()) return false; let resources = gameState.getFishableSupplies(); if (!resources.hasEntities()) { gameState.ai.HQ.navalManager.resetFishingBoats(gameState); this.ent.destroy(); return false; } let nearestSupplyDist = Math.min(); let nearestSupply; let fisherSea = PETRA.getSeaAccess(gameState, this.ent); let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")). filter(API3.Filters.byClass("Dock")).toEntityArray(); let nearestDropsiteDist = function(supply) { let distMin = 1000000; let pos = supply.position(); for (let dropsite of fishDropsites) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (fisherSea != PETRA.getSeaAccess(gameState, dropsite)) continue; distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); } return distMin; }; let exhausted = true; let gatherRates = this.ent.resourceGatherRates(); resources.forEach(function(supply) { if (!supply.position()) return; // check that it is accessible if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea) return; exhausted = false; let supplyType = supply.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) return; if (PETRA.IsSupplyFull(gameState, supply)) return; // check if available resource is worth one additionnal gatherer (except for farms) let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) return; // Avoid ennemy territory if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply)) return; // measure the distance from the resource to the nearest dropsite let dist = nearestDropsiteDist(supply); if (dist > nearestSupplyDist) return; nearestSupplyDist = dist; nearestSupply = supply; }); if (exhausted) { gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea); this.ent.destroy(); return false; } if (nearestSupply) { gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); return true; } if (this.ent.getMetadata(PlayerID, "subrole") == "fisher") this.ent.setMetadata(PlayerID, "subrole", "idle"); return false; }; PETRA.Worker.prototype.gatherNearestField = function(gameState, baseID) { let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); let bestFarm; let gatherRates = this.ent.resourceGatherRates(); for (let field of ownFields.values()) { if (PETRA.IsSupplyFull(gameState, field)) continue; let supplyType = field.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) continue; let rate = 1; let diminishing = field.getDiminishingReturns(); if (diminishing < 1) { let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id()); if (num > 0) rate = Math.pow(diminishing, num); } // Add a penalty distance depending on rate let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000; if (!bestFarm || dist < bestFarm.dist) bestFarm = { "ent": field, "dist": dist, "rate": rate }; } // If other field foundations available, better build them when rate becomes too small if (!bestFarm || bestFarm.rate < 0.70 && gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities()) return false; gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id()); this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); return bestFarm.ent; }; /** * WARNING with the present options of AI orders, the unit will not gather after building the farm. * This is done by calling the gatherNearestField function when construction is completed. */ PETRA.Worker.prototype.buildAnyField = function(gameState, baseID) { if (!this.ent.isBuilder()) return false; let bestFarmEnt = false; let bestFarmDist = 10000000; let pos = this.ent.position(); for (let found of gameState.getOwnFoundations().values()) { if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field")) continue; let current = found.getBuildersNb(); if (current === undefined || current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers()) continue; let dist = API3.SquareVectorDistance(found.position(), pos); if (dist > bestFarmDist) continue; bestFarmEnt = found; bestFarmDist = dist; } return bestFarmEnt; }; /** * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions. * For the time being, we move towards the nearest gatherer (providing him a dropsite). * BaseManager does also use that function to deal with its mobile dropsites. */ PETRA.Worker.prototype.moveToGatherer = function(gameState, ent, forced) { let pos = ent.position(); if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5)) return; let gatherers = this.base.workersBySubrole(gameState, "gatherer"); let dist = Math.min(); let destination; let access = PETRA.getLandAccess(gameState, ent); let types = ent.resourceDropsiteTypes(); for (let gatherer of gatherers.values()) { let gathererType = gatherer.getMetadata(PlayerID, "gather-type"); if (!gathererType || types.indexOf(gathererType) == -1) continue; if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined || PETRA.getLandAccess(gameState, gatherer) != access || gatherer.isIdle()) continue; let distance = API3.SquareVectorDistance(pos, gatherer.position()); if (distance > dist) continue; dist = distance; destination = gatherer.position(); } ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5)); if (destination && dist > 10) ent.move(destination[0], destination[1]); }; /** * Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes * inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case * it will be cleared later). */ PETRA.Worker.prototype.isInaccessibleSupply = function(gameState) { if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) return false; let targetId = this.ent.unitAIOrderData()[0].target; let target = gameState.getEntityById(targetId); if (!target) return true; if (!target.resourceSupplyType()) return false; let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; if (!approachingTarget || approachingTarget != targetId) { this.ent.setMetadata(PlayerID, "approachingTarget", targetId); this.ent.setMetadata(PlayerID, "approachingTime", undefined); this.ent.setMetadata(PlayerID, "approachingPos", undefined); this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount); let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried"); if (alreadyTried && alreadyTried != targetId) this.ent.setMetadata(PlayerID, "alreadyTried", undefined); } let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore"); if (carriedBefore != carriedAmount) { this.ent.setMetadata(PlayerID, "approachingTarget", undefined); this.ent.setMetadata(PlayerID, "alreadyTried", undefined); if (target.getMetadata(PlayerID, "inaccessibleTime")) target.setMetadata(PlayerID, "inaccessibleTime", 0); return false; } let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) return true; let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3) { let presentPos = this.ent.position(); let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) { this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); this.ent.setMetadata(PlayerID, "approachingPos", presentPos); return false; } if (gameState.ai.elapsedTime - approachingTime > 10) { if (this.ent.getMetadata(PlayerID, "alreadyTried")) { target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); return true; } // let's try again to reach it this.ent.setMetadata(PlayerID, "alreadyTried", targetId); this.ent.setMetadata(PlayerID, "approachingTarget", undefined); this.ent.gather(target); return false; } } return false; };