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 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 20039) @@ -1,988 +1,990 @@ var API3 = function(m) { // defines a template. // It's completely raw data, except it's slightly cleverer now and then. m.Template = m.Class({ _init: function(sharedAI, templateName, template) { this._templateName = templateName; this._template = template; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; this._tpCache = new Map(); }, // helper function to return a template value, optionally adjusting for tech. // TODO: there's no support for "_string" values here. get: function(string) { let value = this._template; if (this._entityModif && this._entityModif.has(string)) return this._entityModif.get(string); else if (this._templateModif) { let owner = this._entity ? this._entity.owner : PlayerID; if (this._templateModif[owner] && this._templateModif[owner].has(string)) return this._templateModif[owner].get(string); } if (!this._tpCache.has(string)) { let args = string.split("/"); for (let arg of args) { if (value[arg]) value = value[arg]; else { value = undefined; break; } } this._tpCache.set(string, value); } return this._tpCache.get(string); }, genericName: function() { return this.get("Identity/GenericName"); }, rank: function() { return this.get("Identity/Rank"); }, classes: function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, 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; }, hasClass: function(name) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; return classes && classes.indexOf(name) !== -1; }, hasClasses: function(array) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; if (!classes) return false; for (let cls of array) if (classes.indexOf(cls) === -1) return false; return true; }, civ: function() { return this.get("Identity/Civ"); }, "cost": function(productionQueue) { if (!this.get("Cost")) return undefined; let ret = {}; for (let type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, "costSum": function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return undefined; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1); }, /** * Returns the radius of a circle surrounding this entity's * obstruction shape, or undefined if no obstruction. */ 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 Math.sqrt(w*w + h*h) / 2; } if (this.get("Obstruction/Unit")) return +this.get("Obstruction/Unit/@radius"); return 0; // this should never happen }, /** * Returns the radius of a circle surrounding this entity's * footprint. */ footprintRadius: function() { if (!this.get("Footprint")) return undefined; if (this.get("Footprint/Square")) { let w = +this.get("Footprint/Square/@width"); let h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w*w + h*h) / 2; } if (this.get("Footprint/Circle")) return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, maxHitpoints: function() { return +(this.get("Health/Max") || 0); }, isHealable: function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, isRepairable: function() { return this.get("Repairable") !== undefined; }, getPopulationBonus: function() { return +this.get("Cost/PopulationBonus"); }, armourStrengths: function() { if (!this.get("Armour")) return undefined; return { hack: +this.get("Armour/Hack"), pierce: +this.get("Armour/Pierce"), crush: +this.get("Armour/Crush") }; }, attackTypes: function() { if (!this.get("Attack")) return undefined; let ret = []; for (let type in this.get("Attack")) ret.push(type); return ret; }, attackRange: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { max: +this.get("Attack/" + type +"/MaxRange"), min: +(this.get("Attack/" + type +"/MinRange") || 0) }; }, attackStrengths: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { hack: +(this.get("Attack/" + type + "/Hack") || 0), pierce: +(this.get("Attack/" + type + "/Pierce") || 0), crush: +(this.get("Attack/" + type + "/Crush") || 0) }; }, captureStrength: function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Value") || 0; }, attackTimes: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { prepare: +(this.get("Attack/" + type + "/PrepareTime") || 0), repeat: +(this.get("Attack/" + type + "/RepeatTime") || 1000) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses- , multiplier], … ]. getCounteredClasses: function() { if (!this.get("Attack")) return undefined; let Classes = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses" + b +"/Multiplier")]); } } return Classes; }, // returns true if the entity counters those classes. // TODO: refine using the multiplier countersClasses: function(classes) { if (!this.get("Attack")) return false; let mcounter = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) mcounter.concat(bonusClasses.split(" ")); } } for (let i in classes) { if (mcounter.indexOf(classes[i]) !== -1) return true; } return false; }, // returns, if it exists, the multiplier from each attack against a given class "getMultiplierAgainst": function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; if (this.get("Attack/" + type + "/Bonuses")) { for (let b in this.get("Attack/" + type + "/Bonuses")) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (!bonusClasses) continue; for (let bcl of bonusClasses.split(" ")) if (bcl === againstClass) return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier"); } } return 1; }, "buildableEntities": function() { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; let civ = this.civ(); return templates.replace(/\{civ\}/g, civ).split(/\s+/); }, "trainableEntities": function(civ) { let templates = this.get("ProductionQueue/Entities/_string"); if (!templates) return undefined; if (civ) templates = templates.replace(/\{civ\}/g, civ); return templates.split(/\s+/); }, "researchableTechs": function(civ) { let templates = this.get("ProductionQueue/Technologies/_string"); if (!templates) return undefined; if (civ) templates = templates.replace(/\{civ\}/g, civ); return templates.split(/\s+/); }, resourceSupplyType: function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); return { "generic": type, "specific": subtype }; }, // will return either "food", "wood", "stone", "metal" and not treasure. getResourceType: function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, resourceSupplyMax: function() { return +this.get("ResourceSupply/Amount"); }, maxGatherers: function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }, resourceGatherRates: function() { if (!this.get("ResourceGatherer")) return undefined; let ret = {}; let baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (let r in this.get("ResourceGatherer/Rates")) ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed; return ret; }, resourceDropsiteTypes: function() { if (!this.get("ResourceDropsite")) return undefined; let types = this.get("ResourceDropsite/Types"); return types ? types.split(/\s+/) : []; }, garrisonableClasses: function() { return this.get("GarrisonHolder/List/_string"); }, garrisonMax: function() { return this.get("GarrisonHolder/Max"); }, garrisonEjectHealth: function() { return +this.get("GarrisonHolder/EjectHealth"); }, getDefaultArrow: function() { return +this.get("BuildingAI/DefaultArrowCount"); }, getArrowMultiplier: function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, getGarrisonArrowClasses: function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, buffHeal: function() { return +this.get("GarrisonHolder/BuffHeal"); }, promotion: function() { return this.get("Promotion/Entity"); }, /** * Returns whether this is an animal that is too difficult to hunt. * (Any non domestic currently.) */ isHuntable: function() { if(!this.get("ResourceSupply/KillBeforeGather")) return false; // special case: rabbits too difficult to hunt for such a small food amount let specificName = this.get("Identity/SpecificName"); if (specificName && specificName === "Rabbit") return false; // do not hunt retaliating animals (animals without UnitAI are dead animals) let behaviour = this.get("UnitAI/NaturalBehaviour"); return !this.get("UnitAI") || !(behaviour === "violent" || behaviour === "aggressive" || behaviour === "defensive"); }, walkSpeed: function() { return +this.get("UnitMotion/WalkSpeed"); }, trainingCategory: function() { return this.get("TrainingRestrictions/Category"); }, buildCategory: function() { return this.get("BuildRestrictions/Category"); }, "buildTime": function(productionQueue) { let time = +this.get("Cost/BuildTime"); if (productionQueue) time *= productionQueue.techCostMultiplier("time"); return time; }, "buildDistance": function() { let distance = this.get("BuildRestrictions/Distance"); if (!distance) return undefined; let ret = {}; for (let key in distance) ret[key] = this.get("BuildRestrictions/Distance/" + key); return ret; }, buildPlacementType: function() { return this.get("BuildRestrictions/PlacementType"); }, buildTerritories: function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory")) return undefined; return this.get("BuildRestrictions/Territory").split(/\s+/); }, hasBuildTerritory: function(territory) { let territories = this.buildTerritories(); return territories && territories.indexOf(territory) !== -1; }, hasTerritoryInfluence: function() { return this.get("TerritoryInfluence") !== undefined; }, hasDefensiveFire: function() { if (!this.get("Attack/Ranged")) return false; return this.getDefaultArrow() || this.getArrowMultiplier(); }, territoryInfluenceRadius: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); return -1; }, territoryInfluenceWeight: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); return -1; }, territoryDecayRate: function() { return +(this.get("TerritoryDecay/DecayRate") || 0); }, defaultRegenRate: function() { return +(this.get("Capturable/RegenRate") || 0); }, garrisonRegenRate: function() { return +(this.get("Capturable/GarrisonRegenRate") || 0); }, visionRange: function() { return +this.get("Vision/Range"); }, gainMultiplier: function() { return +this.get("Trader/GainMultiplier"); } }); // 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; }, templateName: function() { return this._templateName; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data should not be shared with any other AI scripts.) */ getMetadata: function(player, key) { return this._ai.getMetadata(player, this, key); }, /** * Sets extra data to be associated with this entity. */ setMetadata: function(player, key, value) { this._ai.setMetadata(player, this, key, value); }, deleteAllMetadata: function(player) { delete this._ai._entityMetadata[player][this.id()]; }, deleteMetadata: function(player, key) { this._ai.deleteMetadata(player, this, key); }, position: function() { return this._entity.position; }, angle: function() { return this._entity.angle; }, isIdle: function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, "getStance": function() { return this._entity.stance !== undefined ? this._entity.stance : undefined; }, unitAIState: function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; }, unitAIOrderData: function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; }, hitpoints: function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; }, isHurt: function() { return this.hitpoints() < this.maxHitpoints(); }, healthLevel: function() { return this.hitpoints() / this.maxHitpoints(); }, needsHeal: function() { return this.isHurt() && this.isHealable(); }, needsRepair: function() { return this.isHurt() && this.isRepairable(); }, decaying: function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; }, capturePoints: function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; }, "isSharedDropsite": function() { return this._entity.sharedDropsite === true; }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ trainingQueue: function() { let queue = this._entity.trainingQueue; return queue; }, trainingQueueTime: function() { let queue = this._entity.trainingQueue; if (!queue) return undefined; let time = 0; for (let item of queue) time += item.timeRemaining; return time/1000; }, foundationProgress: function() { if (this._entity.foundationProgress === undefined) return undefined; return this._entity.foundationProgress; }, getBuilders: function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return []; return this._entity.foundationBuilders; }, getBuildersNb: function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return 0; return this._entity.foundationBuilders.length; }, owner: function() { return this._entity.owner; }, isOwn: function(player) { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === player; }, resourceSupplyAmount: function() { if (this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, resourceSupplyNumGatherers: function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this._entity.resourceSupplyNumGatherers; return undefined; }, isFull: function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, resourceCarrying: function() { if (this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, currentGatherRate: function() { // returns the gather rate for the current target if applicable. if (!this.get("ResourceGatherer")) return undefined; if (this.unitAIOrderData().length && (this.unitAIState().split(".")[1] === "GATHER" || this.unitAIState().split(".")[1] === "RETURNRESOURCE")) { let res; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] === "GATHER" && this.unitAIOrderData()[0].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[0].target); else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[1].target); if (!res) return 0; let type = res.resourceSupplyType(); if (!type) return 0; if (type.generic === "treasure") return 1000; let tstring = type.generic + "." + type.specific; let rate = +this.get("ResourceGatherer/BaseSpeed"); rate *= +this.get("ResourceGatherer/Rates/" +tstring); if (rate) return rate; return 0; } return undefined; }, 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; }, garrisoned: function() { return this._entity.garrisoned; }, canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { if (!this.get("Attack")) return false; for (let type in this.get("Attack")) { if (type === "Slaughter") continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses)) return true; } return false; }, /** * returns true if the entity can capture the given target entity * if no target is given, returns true if the entity has the Capture attack */ "canCapture": function(target) { if (!this.get("Attack/Capture")) return false; if (!target) return true; let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string"); return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return this.get("Garrisonable") !== "false"; }, move: function(x, z, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, moveToRange: function(x, z, min, max, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); return this; }, attackMove: function(x, z, targetClasses, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued }); return this; }, // violent, aggressive, defensive, passive, standground setStance: function(stance, queued = false) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID,{"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued }); return this; }, stopMoving: function() { Engine.PostCommand(PlayerID,{"type": "stop", "entities": [this.id()], "queued": false}); }, unload: function(id) { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload", "garrisonHolder": this.id(), "entities": [id]}); return this; }, // Unloads all owned units, don't unload allies unloadAll: function() { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload-all-by-owner", "garrisonHolders": [this.id()]}); return this; }, garrison: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": queued}); return this; }, attack: function(unitId, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued}); return this; }, // moveApart from a point in the opposite direction with a distance dist moveApart: function(point, dist) { if (this.position() !== undefined) { let direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; let norm = m.VectorDistance(point, this.position()); if (norm === 0) direction = [1, 0]; else { direction[0] /= norm; direction[1] /= norm; } Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false}); } return this; }, // Flees from a unit in the opposite direction. flee: function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0], this.position()[1] - unitToFleeFrom.position()[1]]; let dist = m.VectorDistance(unitToFleeFrom.position(), this.position() ); FleeDirection[0] = 40 * FleeDirection[0]/dist; FleeDirection[1] = 40 * FleeDirection[1]/dist; Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false}); } return this; }, gather: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, repair: function(target, autocontinue = false, queued = false) { Engine.PostCommand(PlayerID,{"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued}); return this; }, returnResources: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, destroy: function() { Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": [this.id()] }); return this; }, barter: function(buyType, sellType, amount) { Engine.PostCommand(PlayerID,{"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount }); return this; }, tradeRoute: function(target, source) { Engine.PostCommand(PlayerID,{"type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false }); return this; }, setRallyPoint: function(target, command) { let data = {"command": command, "target": target.id()}; Engine.PostCommand(PlayerID, {"type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data}); return this; }, unsetRallyPoint: function() { Engine.PostCommand(PlayerID, {"type": "unset-rallypoint", "entities": [this.id()]}); return this; }, train: function(civ, type, count, metadata, promotedTypes) { let trainable = this.trainableEntities(civ); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) === -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } Engine.PostCommand(PlayerID,{ "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata, "promoted": promotedTypes }); return this; }, construct: function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting Engine.PostCommand(PlayerID,{ "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false, "metadata" : metadata // can be undefined }); return this; }, research: function(template) { Engine.PostCommand(PlayerID,{ "type": "research", "entity": this.id(), "template": template }); return this; }, stopProduction: function(id) { Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": id }); return this; }, stopAllProduction: function(percentToStopAt) { let queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (let item of queue) if (item.progress < percentToStopAt) Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": item.id }); return this; }, "guard": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "removeGuard": function() { Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] }); return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20039) @@ -1,923 +1,963 @@ 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.techTemplates = SharedScript._techTemplates; this.entities = SharedScript.entities; this.player = player; this.playerData = SharedScript.playersData[this.player]; this.gameType = SharedScript.gameType; this.alliedVictory = SharedScript.alliedVictory; this.ceasefireActive = SharedScript.ceasefireActive; // get the list of possible phases for this civ: // we assume all of them are researchable from the civil centre this.phases = [{ name: "phase_village" }, { name: "phase_town" }, { name: "phase_city" }]; let cctemplate = this.getTemplate(this.applyCiv("structures/{civ}_civil_centre")); if (!cctemplate) return; let civ = this.getPlayerCiv(); let techs = cctemplate.researchableTechs(civ); for (let phase of this.phases) { phase.requirements = []; let k = techs.indexOf(phase.name); if (k !== -1) { let reqs = DeriveTechnologyRequirements(this.getTemplate(techs[k])._template, civ); if (reqs) { phase.requirements = reqs; continue; } } for (let tech of techs) { - let template = (this.getTemplate(tech))._template; + let template = this.getTemplate(tech)._template; if (template.replaces && template.replaces.indexOf(phase.name) != -1) { let reqs = DeriveTechnologyRequirements(template, civ); if (reqs) { phase.name = tech; phase.requirements = reqs; break; } } } } + // Then check if this mod has an additionnal phase + for (let tech of techs) + { + let template = this.getTemplate(tech)._template; + if (!template.supersedes || template.supersedes != this.phases[2].name) + continue; + let reqs = DeriveTechnologyRequirements(template, civ); + if (reqs) + this.phases.push({ "name": tech, "requirements": reqs }); + break; + } }; m.GameState.prototype.update = function(SharedScript) { this.timeElapsed = SharedScript.timeElapsed; this.playerData = SharedScript.playersData[this.player]; this.ceasefireActive = SharedScript.ceasefireActive; }; m.GameState.prototype.updatingCollection = function(id, filter, collection) { let gid = this.player + "-" + id; // automatically add the player ID return this.updatingGlobalCollection(gid, filter, collection); }; m.GameState.prototype.destroyCollection = function(id) { let gid = this.player + "-" + id; // automatically add the player ID this.destroyGlobalCollection(gid); }; m.GameState.prototype.getEC = function(id) { let gid = this.player + "-" + id; // automatically add the player ID return this.getGEC(gid); }; m.GameState.prototype.updatingGlobalCollection = function(id, filter, collection) { if (this.EntCollecNames.has(id)) return this.EntCollecNames.get(id); let newCollection = collection !== undefined ? collection.filter(filter) : this.entities.filter(filter); newCollection.registerUpdates(); this.EntCollecNames.set(id, newCollection); return newCollection; }; m.GameState.prototype.destroyGlobalCollection = function(id) { if (!this.EntCollecNames.has(id)) return; this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(id)); this.EntCollecNames.delete(id); }; m.GameState.prototype.getGEC = function(id) { if (!this.EntCollecNames.has(id)) return undefined; return this.EntCollecNames.get(id); }; m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; m.GameState.prototype.getBarterPrices = function() { return this.playerData.barterPrices; }; m.GameState.prototype.getGameType = function() { return this.gameType; }; m.GameState.prototype.getAlliedVictory = function() { return this.alliedVictory; }; m.GameState.prototype.isCeasefireActive = function() { return this.ceasefireActive; }; m.GameState.prototype.getTemplate = function(type) { if (this.techTemplates[type] !== undefined) return new m.Technology(this.techTemplates, type); if (!this.templates[type]) return null; return new m.Template(this.sharedScript, type, this.templates[type]); }; 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.townPhase = function() +m.GameState.prototype.getNumberOfPhases = function() { - return this.phases[1].name; + return this.phases.length; }; -m.GameState.prototype.cityPhase = function() +m.GameState.prototype.getPhaseName = function(i) { - return this.phases[2].name; + 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[template] !== undefined; }; /** true if started or queued */ m.GameState.prototype.isResearching = function(template) { return this.playerData.researchStarted[template] !== undefined || this.playerData.researchQueued[template] !== undefined; }; /** 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[techTemplateName] || this.playerData.researchStarted[techTemplateName] || this.playerData.researchedTechs[techTemplateName]) return false; - if (noRequirementCheck === true) + 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[other] || this.playerData.researchStarted[other] || this.playerData.researchedTechs[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[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]; }; 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) { if (this.entities._entities.has(+id)) return this.entities._entities.get(+id); return undefined; }; m.GameState.prototype.getEntities = function(id) { if (id === undefined) return this.entities; return this.updatingGlobalCollection("" + 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("" + this.player + "-entities", m.Filters.byOwner(this.player)); }; m.GameState.prototype.getOwnStructures = function() { return this.updatingGlobalCollection("" + this.player + "-structures", m.Filters.byClass("Structure"), this.getOwnEntities()); }; m.GameState.prototype.getOwnUnits = function() { return this.updatingGlobalCollection("" + 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() { return this.updatingCollection("ally-structures", m.Filters.byClass("Structure"), this.getAllyEntities()); }; m.GameState.prototype.resetAllyStructures = function() { this.destroyCollection("ally-structures"); }; 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("enemy-structures", m.Filters.byClass("Structure"), this.getEnemyEntities()); return this.updatingGlobalCollection("" + enemyID + "-structures", m.Filters.byClass("Structure"), this.getEntities(enemyID)); }; m.GameState.prototype.resetEnemyStructures = function() { this.destroyCollection("enemy-structures"); }; m.GameState.prototype.getEnemyUnits = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Unit")); return this.updatingGlobalCollection("" + 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 === true) 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 === true) 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("" + this.player + "-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities()); }; m.GameState.prototype.getOwnResearchFacilities = function() { return this.updatingGlobalCollection("" + this.player + "-research-facilities", m.Filters.byResearchAvailable(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 === true) 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("" + this.player + "-foundations", m.Filters.isFoundation(), this.getOwnStructures()); }; m.GameState.prototype.getOwnDropsites = function(resource) { if (resource !== undefined) 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 !== undefined) 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.getOwnStructures().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(); for (let trainable of allTrainable) { if (this.isTemplateDisabled(trainable)) continue; let template = this.getTemplate(trainable); if (!template || !template.available(this)) continue; let okay = true; for (let clas of classes) { if (template.hasClass(clas)) continue; okay = false; break; } if (!okay) continue; for (let clas of anticlasses) { if (!template.hasClass(clas)) continue; okay = false; break; } if (!okay) 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(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) && template._templateName != this.townPhase() && - template._templateName != this.cityPhase()) + 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) { for (let ent of this.getOwnUnits().values()) { let buildable = ent.buildableEntities(); 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 civ = this.playerData.civ; for (let ent of this.getOwnResearchFacilities().values()) { let techs = ent.researchableTechs(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(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; }); }; +/** + * Get any buildable structure with a given class + * TODO when several available, choose the best one + */ +m.GameState.prototype.findStructureWithClass = function(classes) +{ + let entTemplates = new Set(); + for (let ent of this.getOwnUnits().values()) + { + if (entTemplates.has(ent.templateName())) + continue; + let buildables = ent.buildableEntities(); + for (let buildable of buildables) + { + if (this.isTemplateDisabled(buildable)) + continue; + let template = this.getTemplate(buildable); + if (!template || !template.available(this)) + continue; + if (MatchesClassList(template.classes(), classes)) + return buildable; + } + entTemplates.add(ent.templateName()); + } + return undefined; +}; + m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; m.GameState.prototype.isTemplateDisabled = function(template) { if (!this.playerData.disabledTemplates[template]) return false; return this.playerData.disabledTemplates[template]; }; /** 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); return { "navalGainMultiplier": shipMerchantTemplate && shipMerchantTemplate.gainMultiplier(), "landGainMultiplier": supportTraderTemplate && supportTraderTemplate.gainMultiplier() }; }; 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 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 20039) @@ -1,634 +1,634 @@ var PETRA = function(m) { /** Attack Manager */ m.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.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 */ m.AttackManager.prototype.init = function(gameState) { this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); this.outOfPlan.registerUpdates(); }; m.AttackManager.prototype.setRushes = function(allowed) { if (this.Config.personality.aggressive > 0.8 && allowed > 2) { this.maxRushes = 3; this.rushSize = [ 16, 20, 24 ]; } else if (this.Config.personality.aggressive > 0.6 && allowed > 1) { this.maxRushes = 2; this.rushSize = [ 18, 22 ]; } else if (this.Config.personality.aggressive > 0.3 && allowed > 0) { this.maxRushes = 1; this.rushSize = [ 20 ]; } }; m.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) m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); }; /** * Some functions are run every turn * Others once in a while */ m.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) m.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 m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Headquarters: 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))) { - if ((barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) || + 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 m.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); } }; m.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; }; m.AttackManager.prototype.pausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(true); }; m.AttackManager.prototype.unpausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(false); }; m.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); }; m.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); }; m.AttackManager.prototype.getAttackInPreparation = function(type) { if (!this.upcomingAttacks[type].length) return undefined; return this.upcomingAttacks[type][0]; }; /** * 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. */ m.AttackManager.prototype.getEnemyPlayer = function(gameState, attack) { let enemyPlayer; // first check if there is a preferred enemy based on our victory conditions if (gameState.getGameType() === "wonder") { let moreAdvanced; let enemyWonder; let wonders = gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")); for (let wonder of wonders.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; } } else if (gameState.getGameType() === "capture_the_relic") { // Target the player with the most relics (including gaia) 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.gameTypeManager.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.gameTypeManager.resetCaptureGaiaRelic(gameState); return enemyPlayer; } } let veto = {}; for (let i in this.defeated) veto[i] = true; // No rush if enemy too well defended (i.e. iberians) if (attack.type === "Rush") { for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || veto[i]) continue; if (this.defeated[i]) continue; let enemyDefense = 0; for (let ent of gameState.getEnemyStructures(i).values()) if (ent.hasClass("Tower") || ent.hasClass("Fortress")) enemyDefense++; if (enemyDefense > 6) veto[i] = true; } } // then if not a huge attack, continue attacking our previous target as long as it has some entities, // otherwise target the most accessible one if (attack.type !== "HugeAttack") { if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && !this.defeated[this.currentEnemyPlayer] && gameState.isPlayerEnemy(this.currentEnemyPlayer) && gameState.getEntities(this.currentEnemyPlayer).hasEntities()) return this.currentEnemyPlayer; let distmin; let ccmin; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let ourcc of ccEnts.values()) { if (ourcc.owner() !== PlayerID) continue; let ourPos = ourcc.position(); let access = gameState.ai.accessibility.getAccessValue(ourPos); for (let enemycc of ccEnts.values()) { if (veto[enemycc.owner()]) continue; if (!gameState.isPlayerEnemy(enemycc.owner())) continue; let enemyPos = enemycc.position(); if (access !== gameState.ai.accessibility.getAccessValue(enemyPos)) continue; let dist = API3.SquareVectorDistance(ourPos, enemyPos); 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 < max) continue; max = enemyCount; enemyPlayer = i; } if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; }; /** f.e. if we have changed diplomacy with another player. */ m.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); } } }; m.AttackManager.prototype.raidTargetEntity = function(gameState, ent) { let data = { "target": ent }; let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Headquarters: Raiding plan " + this.totalNumber); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Raid.push(attackPlan); } this.raidNumber++; }; /** * 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 */ m.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 m.AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData); if (attackPlan.failed) return false; this.totalNumber++; attackPlan.init(gameState); this.startedAttacks[attackType].push(attackPlan); 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); if (unit && attackPlan.isAvailableUnit(gameState, unit)) { unit.setMetadata(PlayerID, "plan", attackPlan.name); attackPlan.unitCollection.updateEnt(unit); } } } if (!attackPlan.unitCollection.hasEntities()) { attackPlan.Abort(gameState); return false; } attackPlan.targetPlayer = target.owner(); attackPlan.targetPos = pos; attackPlan.target = target; attackPlan.state = "arrived"; return true; }; m.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 }; }; m.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 m.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 m.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.startedAttacks[key].push(attack); } } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 20039) @@ -1,220 +1,230 @@ var PETRA = function(m) { m.Config = function(difficulty) { // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. this.difficulty = difficulty !== undefined ? difficulty : 3; // debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug this.debug = 0; - this.chat = true; // false to prevent AI's chats + this.chat = true; // false to prevent AI's chats - this.popScaling = 1; // scale factor depending on the max population + this.popScaling = 1; // scale factor depending on the max population this.Military = { - "towerLapseTime" : 90, // Time to wait between building 2 towers - "fortressLapseTime" : 390, // Time to wait between building 2 fortresses + "towerLapseTime" : 90, // Time to wait between building 2 towers + "fortressLapseTime" : 390, // Time to wait between building 2 fortresses "popForBarracks1" : 25, "popForBarracks2" : 95, "popForBlacksmith" : 65, "numSentryTowers" : 1 }; this.Economy = { - "popForTown" : 40, // How many units we want before aging to town. - "workForCity" : 80, // How many workers we want before aging to city. - "cityPhase" : 840, // time to start trying to reach city phase + "popPhase2" : 35, // How many units we want before aging to phase2. + "workPhase3" : 65, // How many workers we want before aging to phase3. + "workPhase4" : 80, // How many workers we want before aging to phase4 or higher. "popForMarket" : 50, "popForDock" : 25, - "targetNumWorkers" : 40, // dummy, will be changed later - "targetNumTraders" : 5, // Target number of traders - "targetNumFishers" : 1, // Target number of fishers per sea - "supportRatio" : 0.3, // fraction of support workers among the workforce + "targetNumWorkers" : 40,// dummy, will be changed later + "targetNumTraders" : 5, // Target number of traders + "targetNumFishers" : 1, // Target number of fishers per sea + "supportRatio" : 0.3, // fraction of support workers among the workforce "provisionFields" : 2 }; // Note: attack settings are set directly in attack_plan.js // defense this.Defense = { "defenseRatio" : 2, // ratio of defenders/attackers. "armyCompactSize" : 2000, // squared. Half-diameter of an army. - "armyBreakawaySize" : 3500, // squared. + "armyBreakawaySize" : 3500, // squared. "armyMergeSize" : 1400 // squared. }; this.buildings = { "advanced": { "default": [], "athen": [ "structures/{civ}_gymnasion", "structures/{civ}_prytaneion", "structures/{civ}_theatron" ], "brit": [ "structures/{civ}_rotarymill" ], "cart": [ "structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ], "gaul": [ "structures/{civ}_rotarymill", "structures/{civ}_tavern" ], "iber": [ "structures/{civ}_monument" ], "mace": [ "structures/{civ}_siege_workshop", "structures/{civ}_library", "structures/{civ}_theatron" ], "maur": [ "structures/{civ}_elephant_stables", "structures/{civ}_pillar_ashoka" ], "pers": [ "structures/{civ}_stables", "structures/{civ}_apadana", "structures/{civ}_hall"], "ptol": [ "structures/{civ}_library" ], "rome": [ "structures/{civ}_army_camp" ], "sele": [ "structures/{civ}_library" ], "spart": [ "structures/{civ}_syssiton", "structures/{civ}_theatron" ] } }; this.priorities = { "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food "citizenSoldier": 60, "trader": 50, "healer": 20, "ships": 70, "house": 350, "dropsites": 200, "field": 400, "dock": 90, "corral": 60, "economicBuilding": 90, "militaryBuilding": 130, "defenseBuilding": 70, "civilCentre": 950, "majorTech": 700, "minorTech": 40, + "wonder": 1000, "emergency": 1000 // used only in emergency situations, should be the highest one }; this.personality = { "aggressive": 0.5, "cooperative": 0.5, "defensive": 0.5 }; // See m.QueueManager.prototype.wantedGatherRates() this.queues = { "firstTurn": { "food": 10, "wood": 10, "default": 0 }, "short": { "food": 200, "wood": 200, "default": 100 }, "medium": { "default": 0 }, "long": { "default": 0 } }; this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; }; m.Config.prototype.setConfig = function(gameState) { // initialize personality traits if (this.difficulty > 1) { this.personality.aggressive = randFloat(0, 1); this.personality.cooperative = randFloat(0, 1); this.personality.defensive = randFloat(0, 1); } else { this.personality.aggressive = 0.1; this.personality.cooperative = 0.9; } if (gameState.getAlliedVictory()) this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15); // changing settings based on difficulty or personality if (this.difficulty < 2) { this.Economy.cityPhase = 240000; this.Economy.supportRatio = 0.5; this.Economy.provisionFields = 1; this.Military.numSentryTowers = this.personality.defensive > 0.66 ? 1 : 0; } else if (this.difficulty < 3) { this.Economy.cityPhase = 1800; this.Economy.supportRatio = 0.4; this.Economy.provisionFields = 1; this.Military.numSentryTowers = this.personality.defensive > 0.66 ? 1 : 0; } else { this.Military.towerLapseTime += Math.round(20*(this.personality.defensive - 0.5)); this.Military.fortressLapseTime += Math.round(60*(this.personality.defensive - 0.5)); if (this.difficulty == 3) this.Military.numSentryTowers = 1; else this.Military.numSentryTowers = 2; if (this.personality.defensive > 0.66) ++this.Military.numSentryTowers; else if (this.personality.defensive < 0.33) --this.Military.numSentryTowers; if (this.personality.aggressive > 0.7) { this.Military.popForBarracks1 = 12; - this.Economy.popForTown = 55; + this.Economy.popPhase2 = 50; this.Economy.popForMarket = 60; this.priorities.defenseBuilding = 60; this.priorities.healer = 10; } } let maxPop = gameState.getPopulationMax(); if (this.difficulty < 2) this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); else if (this.difficulty < 3) this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2))); else this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3))); this.Economy.targetNumTraders = 2 + this.difficulty; + if (gameState.getGameType() === "wonder") + { + this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3); + this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4); + } + if (maxPop < 300) { this.popScaling = Math.sqrt(maxPop / 300); this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5)); this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3)); this.Military.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * this.popScaling), 30), Math.floor(maxPop/2)); - this.Economy.popForTown = Math.min(Math.max(Math.floor(this.Economy.popForTown * this.popScaling), 25), Math.floor(maxPop/2)); - this.Economy.workForCity = Math.min(Math.max(Math.floor(this.Economy.workForCity * this.popScaling), 50), Math.floor(maxPop*2/3)); + this.Economy.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2)); + this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3)); + this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), Math.floor(maxPop*2/3)); this.Economy.popForMarket = Math.min(Math.max(Math.floor(this.Economy.popForMarket * this.popScaling), 25), Math.floor(maxPop/2)); this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling); } - this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popForTown); + this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popPhase2); + this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers); + this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers); if (this.debug < 2) return; API3.warn(" >>> Petra bot: personality = " + uneval(this.personality)); }; m.Config.prototype.Serialize = function() { var data = {}; for (let key in this) if (this.hasOwnProperty(key) && key != "debug") data[key] = this[key]; return data; }; m.Config.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/gameTypeManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/gameTypeManager.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/gameTypeManager.js (revision 20039) @@ -1,620 +1,618 @@ var PETRA = function(m) { /** * Handle events that are important to specific gameTypes * In regicide, train and manage healer guards and military guards for the hero. */ m.GameTypeManager = function(Config) { this.Config = Config; this.criticalEnts = new Map(); // Holds ids of all ents who are (or can be) guarding and if the ent is currently guarding this.guardEnts = new Map(); this.healersPerCriticalEnt = 2 + Math.round(this.Config.personality.defensive * 2); this.tryCaptureGaiaRelic = false; this.tryCaptureGaiaRelicLapseTime = -1; this.tryCaptureGaiaRelicLocal = false; this.tryCaptureGaiaRelicLocalLapseTime = -1; // Gaia relics which we are targeting currently and have not captured yet this.targetedGaiaRelics = new Set(); }; /** * Cache the ids of any inital gameType-critical entities. * In regicide, these are the inital heroes that the player starts with. */ m.GameTypeManager.prototype.init = function(gameState) { if (gameState.getGameType() === "wonder") { for (let wonder of gameState.getOwnEntitiesByClass("Wonder", true).values()) this.criticalEnts.set(wonder.id(), { "guardsAssigned": 0, "guards": new Map() }); } if (gameState.getGameType() === "regicide") { for (let hero of gameState.getOwnEntitiesByClass("Hero", true).values()) { let defaultStance = hero.hasClass("Soldier") ? "aggressive" : "passive"; if (hero.getStance() !== defaultStance) hero.setStance(defaultStance); this.criticalEnts.set(hero.id(), { "garrisonEmergency": false, "healersAssigned": 0, "guardsAssigned": 0, // for non-healer guards "guards": new Map() // ids of ents who are currently guarding this hero }); } } }; /** * In regicide mode, if the hero has less than 70% health, try to garrison it in a healing structure * If it is less than 40%, try to garrison in the closest possible structure * If the hero cannot garrison, retreat it to the closest base */ m.GameTypeManager.prototype.checkEvents = function(gameState, events) { if (gameState.getGameType() === "wonder") { for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() === undefined || !ent.hasClass("Wonder")) continue; // Let's get a few units from other bases to build the wonder. let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); let builders = gameState.ai.HQ.bulkPickWorkers(gameState, base, 10); if (builders) for (let worker of builders.values()) { worker.setMetadata(PlayerID, "base", base.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); } } for (let evt of events.ConstructionFinished) { if (!evt || !evt.newentity) continue; let ent = gameState.getEntityById(evt.newentity); if (ent && ent.isOwn(PlayerID) && ent.hasClass("Wonder")) this.criticalEnts.set(ent.id(), { "guardsAssigned": 0, "guards": new Map() }); } } if (gameState.getGameType() === "regicide") { for (let evt of events.Attacked) { if (!this.criticalEnts.has(evt.target)) continue; let target = gameState.getEntityById(evt.target); if (!target || !target.position() || target.healthLevel() > this.Config.garrisonHealthLevel.high) continue; let plan = target.getMetadata(PlayerID, "plan"); let hero = this.criticalEnts.get(evt.target); if (plan !== -2 && plan !== -3) { target.stopMoving(); if (plan >= 0) { let attackPlan = gameState.ai.HQ.attackManager.getPlan(plan); if (attackPlan) attackPlan.removeUnit(target, true); } if (target.getMetadata(PlayerID, "PartOfArmy")) { let army = gameState.ai.HQ.defenseManager.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); if (army) army.removeOwn(gameState, target.id()); } hero.garrisonEmergency = target.healthLevel() < this.Config.garrisonHealthLevel.low; this.pickCriticalEntRetreatLocation(gameState, target, hero.garrisonEmergency); } else if (target.healthLevel() < this.Config.garrisonHealthLevel.low && !hero.garrisonEmergency) { // the hero is severely wounded, try to retreat/garrison quicker gameState.ai.HQ.garrisonManager.cancelGarrison(target); this.pickCriticalEntRetreatLocation(gameState, target, true); hero.garrisonEmergency = true; } } for (let evt of events.TrainingFinished) for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "role") === "criticalEntHealer") this.assignGuardToCriticalEnt(gameState, ent); } for (let evt of events.Garrison) { if (!this.criticalEnts.has(evt.entity)) continue; let hero = this.criticalEnts.get(evt.entity); if (hero.garrisonEmergency) hero.garrisonEmergency = false; let holderEnt = gameState.getEntityById(evt.holder); if (!holderEnt) continue; if (holderEnt.hasClass("Ship")) { // If the hero is garrisoned on a ship, remove its guards for (let guardId of hero.guards.keys()) { let guardEnt = gameState.getEntityById(guardId); if (!guardEnt) continue; guardEnt.removeGuard(); this.guardEnts.set(guardId, false); } hero.guards.clear(); continue; } // Move the current guards to the garrison location. // TODO: try to garrison them with the critical ent. for (let guardId of hero.guards.keys()) { let guardEnt = gameState.getEntityById(guardId); if (!guardEnt) continue; let plan = guardEnt.getMetadata(PlayerID, "plan"); // Current military guards (with Soldier class) will have been assigned plan metadata, but healer guards // are not assigned a plan, and so they could be already moving to garrison somewhere due to low health. if (!guardEnt.hasClass("Soldier") && (plan === -2 || plan === -3)) continue; let pos = holderEnt.position(); let radius = holderEnt.obstructionRadius(); if (pos) guardEnt.moveToRange(pos[0], pos[1], radius, radius + 5); } } } // Check if new healers/guards need to be assigned to an ent for (let evt of events.Destroy) { if (!evt.entityObj || evt.entityObj.owner() !== PlayerID) continue; let entId = evt.entityObj.id(); if (this.criticalEnts.has(entId)) { this.removeGuardsFromCriticalEnt(gameState, entId); continue; } if (!this.guardEnts.has(entId)) continue; for (let data of this.criticalEnts.values()) if (data.guards.has(entId)) { data.guards.delete(entId); if (evt.entityObj.hasClass("Healer")) --data.healersAssigned; else --data.guardsAssigned; break; } this.guardEnts.delete(entId); } for (let evt of events.UnGarrison) { if (!this.guardEnts.has(evt.entity) && !this.criticalEnts.has(evt.entity)) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; // If this ent travelled to a criticalEnt's accessValue, try again to assign as a guard if ((ent.getMetadata(PlayerID, "role") === "criticalEntHealer" || ent.getMetadata(PlayerID, "role") === "criticalEntGuard") && !this.guardEnts.get(evt.entity)) { this.assignGuardToCriticalEnt(gameState, ent, ent.getMetadata(PlayerID, "guardedEnt")); continue; } if (!this.criticalEnts.has(evt.entity)) continue; // If this is a hero, try to assign ents that should be guarding it, but couldn't previously let criticalEnt = this.criticalEnts.get(evt.entity); for (let [id, isGuarding] of this.guardEnts) { if (criticalEnt.guards.size >= this.healersPerCriticalEnt) break; if (!isGuarding) { let guardEnt = gameState.getEntityById(id); if (guardEnt) this.assignGuardToCriticalEnt(gameState, guardEnt, evt.entity); } } } for (let evt of events.OwnershipChanged) { if (evt.from === PlayerID && this.criticalEnts.has(evt.entity)) { this.removeGuardsFromCriticalEnt(gameState, evt.entity); continue; } if (evt.from === 0 && this.targetedGaiaRelics.has(evt.entity)) this.targetedGaiaRelics.delete(evt.entity); if (evt.to !== PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (ent && (gameState.getGameType() === "wonder" && ent.hasClass("Wonder") || gameState.getGameType() === "capture_the_relic" && ent.hasClass("Relic"))) { this.criticalEnts.set(ent.id(), { "guardsAssigned": 0, "guards": new Map() }); // Move captured relics to the closest base if (ent.hasClass("Unit")) { this.pickCriticalEntRetreatLocation(gameState, ent, false); if (evt.from === 0) gameState.ai.HQ.attackManager.cancelAttacksAgainstPlayer(gameState, evt.from); } if (ent.hasClass("Relic")) this.targetedGaiaRelics.delete(ent.id()); } } }; m.GameTypeManager.prototype.removeGuardsFromCriticalEnt = function(gameState, criticalEntId) { for (let [guardId, role] of this.criticalEnts.get(criticalEntId).guards) { let guardEnt = gameState.getEntityById(guardId); if (!guardEnt) continue; if (role === "healer") this.guardEnts.set(guardId, false); else { guardEnt.setMetadata(PlayerID, "plan", -1); guardEnt.setMetadata(PlayerID, "role", undefined); this.guardEnts.delete(guardId); } if (guardEnt.getMetadata(PlayerID, "guardedEnt")) guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); } this.criticalEnts.delete(criticalEntId); }; m.GameTypeManager.prototype.buildWonder = function(gameState, queues) { if (queues.wonder && queues.wonder.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || !gameState.ai.HQ.canBuild(gameState, "structures/{civ}_wonder")) return; - if (!queues.wonder) - gameState.ai.queueManager.addQueue("wonder", 1000); queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); }; /** * Try to keep some military units guarding any criticalEnts, if we can afford it. * If we have too low a population and require units for other needs, remove guards so they can be reassigned. * TODO: Swap citizen soldier guards with champions if they become available. */ m.GameTypeManager.prototype.manageCriticalEntGuards = function(gameState) { let numWorkers = gameState.getOwnEntitiesByRole("worker", true).length; if (numWorkers < 20) { for (let data of this.criticalEnts.values()) { for (let guardId of data.guards.keys()) { let guardEnt = gameState.getEntityById(guardId); if (!guardEnt || !guardEnt.hasClass("CitizenSoldier") || guardEnt.getMetadata(PlayerID, "role") !== "criticalEntGuard") continue; guardEnt.removeGuard(); guardEnt.setMetadata(PlayerID, "plan", -1); guardEnt.setMetadata(PlayerID, "role", undefined); this.guardEnts.delete(guardId); --data.guardsAssigned; if (guardEnt.getMetadata(PlayerID, "guardedEnt")) guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); if (++numWorkers >= 20) break; } if (numWorkers >= 20) break; } } for (let [id, data] of this.criticalEnts) { let criticalEnt = gameState.getEntityById(id); if (!criticalEnt) continue; let militaryGuardsPerCriticalEnt = (criticalEnt.hasClass("Wonder") ? 10 : 4) + Math.round(this.Config.personality.defensive * 5); if (data.guardsAssigned >= militaryGuardsPerCriticalEnt) continue; // First try to pick guards in the criticalEnt's accessIndex, to avoid unnecessary transports for (let checkForSameAccess of [true, false]) { // First try to assign any Champion units we might have for (let entity of gameState.getOwnEntitiesByClass("Champion", true).values()) { if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) continue; if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt) break; } if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= 25) break; for (let entity of gameState.ai.HQ.attackManager.outOfPlan.values()) { if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) continue; --numWorkers; if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= 25) break; } if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= 25) break; for (let entity of gameState.getOwnEntitiesByClass("Soldier", true).values()) { if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) continue; --numWorkers; if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= 25) break; } if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= 25) break; } } }; m.GameTypeManager.prototype.tryAssignMilitaryGuard = function(gameState, guardEnt, criticalEnt, checkForSameAccess) { if (guardEnt.getMetadata(PlayerID, "plan") !== undefined || guardEnt.getMetadata(PlayerID, "transport") !== undefined || this.criticalEnts.has(guardEnt.id()) || checkForSameAccess && (!guardEnt.position() || !criticalEnt.position() || m.getLandAccess(gameState, criticalEnt) !== m.getLandAccess(gameState, guardEnt))) return false; if (!this.assignGuardToCriticalEnt(gameState, guardEnt, criticalEnt.id())) return false; guardEnt.setMetadata(PlayerID, "plan", -2); guardEnt.setMetadata(PlayerID, "role", "criticalEntGuard"); return true; }; m.GameTypeManager.prototype.pickCriticalEntRetreatLocation = function(gameState, criticalEnt, emergency) { gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, criticalEnt, emergency); let plan = criticalEnt.getMetadata(PlayerID, "plan"); if (plan === -2 || plan === -3) return; if (this.criticalEnts.get(criticalEnt.id()).garrisonEmergency) this.criticalEnts.get(criticalEnt.id()).garrisonEmergency = false; // Couldn't find a place to garrison, so the ent will flee from attacks if (!criticalEnt.hasClass("Relic") && criticalEnt.getStance() !== "passive") criticalEnt.setStance("passive"); let accessIndex = gameState.ai.accessibility.getAccessValue(criticalEnt.position()); let bestBase = m.getBestBase(gameState, criticalEnt, true); if (bestBase.accessIndex == accessIndex) { let bestBasePos = bestBase.anchor.position(); criticalEnt.move(bestBasePos[0], bestBasePos[1]); } }; /** * The number of healers trained per critical ent (dependent on the defensive trait) * may not be the number of healers actually guarding an ent at any one time. */ m.GameTypeManager.prototype.trainCriticalEntHealer = function(gameState, queues, id) { if (gameState.ai.HQ.saveResources || !gameState.getOwnEntitiesByClass("Temple", true).hasEntities()) return; let template = gameState.applyCiv("units/{civ}_support_healer_b"); queues.healer.addPlan(new m.TrainingPlan(gameState, template, { "role": "criticalEntHealer", "base": 0 }, 1, 1)); ++this.criticalEnts.get(id).healersAssigned; }; /** * Only send the guard command if the guard's accessIndex is the same as the critical ent * and the critical ent has a position (i.e. not garrisoned). * Request a transport if the accessIndex value is different, and if a transport is needed, * the guardEnt will be given metadata describing which entity it is being sent to guard, * which will be used once its transport has finished. * Return false if the guardEnt is not a valid guard unit (i.e. cannot guard or is being transported). */ m.GameTypeManager.prototype.assignGuardToCriticalEnt = function(gameState, guardEnt, criticalEntId) { if (guardEnt.getMetadata(PlayerID, "transport") !== undefined || !guardEnt.canGuard()) return false; if (!criticalEntId) { // Assign to the critical ent with the fewest guards let min = Math.min(); for (let [id, data] of this.criticalEnts) { if (data.guards.size > min) continue; criticalEntId = id; min = data.guards.size; } } if (!criticalEntId) { if (guardEnt.getMetadata(PlayerID, "guardedEnt")) guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); return false; } let criticalEnt = gameState.getEntityById(criticalEntId); if (!criticalEnt || !criticalEnt.position() || !guardEnt.position()) { this.guardEnts.set(guardEnt.id(), false); return false; } if (guardEnt.getMetadata(PlayerID, "guardedEnt") !== criticalEntId) guardEnt.setMetadata(PlayerID, "guardedEnt", criticalEntId); let guardEntAccess = gameState.ai.accessibility.getAccessValue(guardEnt.position()); let criticalEntAccess = gameState.ai.accessibility.getAccessValue(criticalEnt.position()); if (guardEntAccess === criticalEntAccess) { let queued = m.returnResources(gameState, guardEnt); guardEnt.guard(criticalEnt, queued); let guardRole = guardEnt.getMetadata(PlayerID, "role") === "criticalEntHealer" ? "healer" : "guard"; this.criticalEnts.get(criticalEntId).guards.set(guardEnt.id(), guardRole); // Switch this guard ent to the criticalEnt's base if (criticalEnt.hasClass("Structure") && criticalEnt.getMetadata(PlayerID, "base") !== undefined) guardEnt.setMetadata(PlayerID, "base", criticalEnt.getMetadata(PlayerID, "base")); } else gameState.ai.HQ.navalManager.requireTransport(gameState, guardEnt, guardEntAccess, criticalEntAccess, criticalEnt.position()); this.guardEnts.set(guardEnt.id(), guardEntAccess === criticalEntAccess); return true; }; m.GameTypeManager.prototype.resetCaptureGaiaRelic = function(gameState) { // Do not capture gaia relics too frequently as the ai has access to the entire map this.tryCaptureGaiaRelicLapseTime = gameState.ai.elapsedTime + 300 - 30 * (this.Config.difficulty - 3); this.tryCaptureGaiaRelic = false; }; m.GameTypeManager.prototype.update = function(gameState, events, queues) { // Wait a turn for trigger scripts to spawn any critical ents (i.e. in regicide) if (gameState.ai.playedTurn === 1) this.init(gameState); this.checkEvents(gameState, events); if (gameState.getGameType() === "wonder" && gameState.ai.playedTurn % 10 === 0) { this.buildWonder(gameState, queues); this.manageCriticalEntGuards(gameState); } if (gameState.getGameType() === "regicide" && gameState.ai.playedTurn % 10 === 0) { for (let [id, data] of this.criticalEnts) { let ent = gameState.getEntityById(id); if (ent && ent.healthLevel() > this.Config.garrisonHealthLevel.high && ent.hasClass("Soldier") && ent.getStance() !== "aggressive") ent.setStance("aggressive"); if (data.healersAssigned < this.healersPerCriticalEnt && this.guardEnts.size < Math.min(gameState.getPopulationMax() / 10, gameState.getPopulation() / 4)) this.trainCriticalEntHealer(gameState, queues, id); } this.manageCriticalEntGuards(gameState); } if (gameState.getGameType() === "capture_the_relic" && gameState.ai.playedTurn % 10 === 0) { this.manageCriticalEntGuards(gameState); if (!this.tryCaptureGaiaRelic && gameState.ai.elapsedTime > this.tryCaptureGaiaRelicLapseTime) this.tryCaptureGaiaRelic = true; // Look for some relic that may be on our territory at game-start or if our territory boundaries change if (!this.tryCaptureGaiaRelicLocal && gameState.ai.elapsedTime > this.tryCaptureGaiaRelicLocalLapseTime) { let allRelics = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")); for (let relic of allRelics.values()) { let relicPosition = relic.position(); if (this.targetedGaiaRelics.has(relic.id()) || relic.owner() !== 0 || !relicPosition || gameState.ai.HQ.territoryMap.getOwner(relicPosition) !== PlayerID) continue; gameState.ai.HQ.attackManager.raidTargetEntity(gameState, relic); this.targetedGaiaRelics.add(relic.id()); this.tryCaptureGaiaRelicLocal = false; this.tryCaptureGaiaRelicLocalLapseTime = gameState.ai.elapsedTime + 60 - 30 * (this.Config.difficulty - 3); break; } } } }; m.GameTypeManager.prototype.Serialize = function() { return { "criticalEnts": this.criticalEnts, "guardEnts": this.guardEnts, "healersPerCriticalEnt": this.healersPerCriticalEnt, "tryCaptureGaiaRelic": this.tryCaptureGaiaRelic, "tryCaptureGaiaRelicLapseTime": this.tryCaptureGaiaRelicLapseTime, "tryCaptureGaiaRelicLocal": this.tryCaptureGaiaRelicLocal, "tryCaptureGaiaRelicLocalLapseTime": this.tryCaptureGaiaRelicLocalLapseTime, "targetedGaiaRelics": this.targetedGaiaRelics }; }; m.GameTypeManager.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 20039) @@ -1,2410 +1,2495 @@ var PETRA = function(m) { /** * 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. */ m.HQ = function(Config) { this.Config = Config; - - this.econState = "growth"; // existing values: growth, townPhasing and cityPhasing. - this.currentPhase = undefined; + this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i // Cache the rates. this.turnCache = {}; // Some resources objects (will be filled in init) this.wantedRates = {}; this.currentRates = {}; this.lastFailedGather = {}; // workers configuration this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; this.stopBuilding = new Map(); // list of buildings to stop (temporarily) production because no room this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime this.towerStartTime = 0; // stone defense towers, will start as soon as available this.towerLapseTime = this.Config.Military.towerLapseTime; this.fortressStartTime = 0; // will start as soon as available 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 m.AttackManager(this.Config); this.defenseManager = new m.DefenseManager(this.Config); this.tradeManager = new m.TradeManager(this.Config); this.navalManager = new m.NavalManager(this.Config); this.researchManager = new m.ResearchManager(this.Config); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.garrisonManager = new m.GarrisonManager(this.Config); this.gameTypeManager = new m.GameTypeManager(this.Config); this.capturableTargets = new Map(); this.capturableTargetsTime = 0; }; /** More initialisation for stuff that needs the gameState */ m.HQ.prototype.init = function(gameState, queues) { this.territoryMap = m.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 = m.createBorderMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; for (let res of gameState.sharedScript.resourceInfo.codes) { this.wantedRates[res] = 0; this.currentRates[res] = 0; } this.treasures = gameState.getEntities().filter(function (ent) { let type = ent.resourceSupplyType(); return type && type.generic === "treasure"; }); this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); }; /** * initialization needed after deserialization (only called when deserialization) */ m.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("Elephant")) 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); }; /** * 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 */ m.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; }; m.HQ.prototype.checkEvents = function (gameState, events, queues) { if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); for (let evt of events.DiplomacyChanged) { if (evt.player !== PlayerID && evt.otherPlayer !== PlayerID) continue; gameState.resetAllyStructures(); gameState.resetEnemyStructures(); break; } 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.isOwn(PlayerID) || ent.foundationProgress() === undefined) continue; if (ent.getMetadata(PlayerID, "base") == -1) { // Okay so let's try to create a new base around this. let newbase = new m.BaseManager(gameState, this.Config); newbase.init(gameState, "unconstructed"); newbase.setAnchor(gameState, ent); this.baseManagers.push(newbase); // 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(function (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) { // Let's check if we have a building set to create a new base. // TODO: move to the base manager. if (evt.newentity) { if (evt.newentity === evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.isOwn(PlayerID)) continue; if (ent.getMetadata(PlayerID, "baseAnchor") === true) { let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (base.constructing) base.constructing = false; base.anchor = ent; base.anchorId = evt.newentity; base.buildings.updateEnt(ent); if (base.ID === this.baseManagers[1].ID) { // 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; } } } } for (let evt of events.OwnershipChanged) // capture events { if (evt.to !== PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; if (ent.position()) ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(ent.position())); if (ent.hasClass("Unit")) { m.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")) ent.setMetadata(PlayerID, "sea", gameState.ai.accessibility.getAccessValue(ent.position(), true)); if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) ent.setMetadata(PlayerID, "plan", -1); continue; } if (ent.hasClass("CivCentre")) // build a new base around it { let newbase = new m.BaseManager(gameState, this.Config); if (ent.foundationProgress() !== undefined) newbase.init(gameState, "unconstructed"); else newbase.init(gameState, "captured"); newbase.setAnchor(gameState, ent); this.baseManagers.push(newbase); newbase.assignEntity(gameState, ent); } else { // TODO should be reassigned later if a better base is captured m.getBestBase(gameState, ent).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 holderId = ent.unitAIOrderData()[0].target; let holder = gameState.getEntityById(holderId); 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 = m.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 = gameState.ai.accessibility.getAccessValue(pos); let distmin = Math.min(); let goal; for (let dropsite of dropsites.values()) { if (!dropsite.position() || dropsite.getMetadata(PlayerID, "access") !== 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); } // then deals with decaying structures 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.70; for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.90; break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; -/** Called by the "town phase" research plan once it's started */ -m.HQ.prototype.OnTownPhase = function(gameState) +/** Ensure that all requirements are met when phasing up*/ +m.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() && + !queues.defenseBuilding.hasQueuedUnits()) + { + if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() && + this.canBuild(gameState, "structures/{civ}_market")) + { + plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); + queue = "economicBuilding"; + break; + } + if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && + this.canBuild(gameState, "structures/{civ}_temple")) + { + plan = new m.ConstructionPlan(gameState, "structures/{civ}_temple"); + queue = "economicBuilding"; + break; + } + if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() && + this.canBuild(gameState, "structures/{civ}_blacksmith")) + { + plan = new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith"); + queue = "militaryBuilding"; + break; + } + if (this.canBuild(gameState, "structures/{civ}_defense_tower")) + { + plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); + queue = "defenseBuilding"; + 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 = gameState.findStructureWithClass([entityReq.class]); + if (structure && this.canBuild(gameState, structure)) + plan = new m.ConstructionPlan(gameState, structure); + } + } + + if (plan) + { + if (queue == "wonder") + { + gameState.ai.queueManager.changePriority("majorTech", 400); + plan.queueToReset = "majorTech"; + } + else + { + gameState.ai.queueManager.changePriority(queue, 1000); + plan.queueToReset = queue; + } + queues[queue].addPlan(plan); + return; + } + } }; -/** Called by the "city phase" research plan once it's started */ -m.HQ.prototype.OnCityPhase = function(gameState) +/** Called by any "phase" research plan once it's started */ +m.HQ.prototype.OnPhaseUp = function(gameState, phase) { }; /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ m.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // default template let requirementsDef = [["cost", 1], ["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 (function (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.popForTown + 10) + if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) return; - if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popForTown && - gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase()))) + 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. let supportRatio = gameState.isTemplateDisabled(gameState.applyCiv("structures/{civ}_field")) ? Math.min(this.supportRatio, 0.1) : this.supportRatio; let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * Math.atan(numberTotal/supportMax) / 1.570796; let template; if (numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["cost", 1], ["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 m.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); else if (template) queues.citizenSoldier.addPlan(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); }; /** picks the best template based on parameters and classes */ m.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 === 0) 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(function(a, b) { let aDivParam = 0; let bDivParam = 0; let aTopParam = 0; let bTopParam = 0; for (let param of parameters) { if (param[0] == "base") { aTopParam = param[1]; bTopParam = param[1]; } if (param[0] == "strength") { aTopParam += m.getMaxStrength(a[1]) * param[1]; bTopParam += m.getMaxStrength(b[1]) * param[1]; } if (param[0] == "siegeStrength") { aTopParam += m.getMaxStrength(a[1], "Structure") * param[1]; bTopParam += m.getMaxStrength(b[1], "Structure") * param[1]; } if (param[0] == "speed") { aTopParam += a[1].walkSpeed() * param[1]; bTopParam += b[1].walkSpeed() * param[1]; } if (param[0] == "cost") { aDivParam += a[1].costSum() * param[1]; bDivParam += b[1].costSum() * param[1]; } // requires a third parameter which is the resource if (param[0] == "costsResource") { if (a[1].cost()[param[2]]) aTopParam *= param[1]; if (b[1].cost()[param[2]]) bTopParam *= param[1]; } if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aTopParam *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bTopParam *= param[1]; } } return -aTopParam/(aDivParam+1) + bTopParam/(bDivParam+1); }); return units[0][0]; }; /** * returns an entity collection of workers through BaseManager.pickBuilders * TODO: when same accessIndex, sort by distance */ m.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(function (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) needed = number - workers.length; else break; } if (!workers.length) return false; return workers; }; m.HQ.prototype.getTotalResourceLevel = function(gameState) { let total = {}; for (let res of gameState.sharedScript.resourceInfo.codes) 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. */ m.HQ.prototype.GetCurrentGatherRates = function(gameState) { if (!this.turnCache.gatherRates) { for (let res in this.currentRates) this.currentRates[res] = 0.5 * this.GetTCResGatherer(res); for (let base of this.baseManagers) base.getGatherRates(gameState, this.currentRates); for (let res in this.currentRates) { if (this.currentRates[res] < 0) { if (this.Config.debug > 0) API3.warn("Petra: current rate for " + res + " < 0 with " + this.GetTCResGatherer(res) + " moved gatherers"); this.currentRates[res] = 0; } } this.turnCache.gatherRates = true; } return this.currentRates; }; /** * 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. */ m.HQ.prototype.pickMostNeededResources = function(gameState) { this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); let currentRates = this.GetCurrentGatherRates(gameState); let needed = []; for (let res in this.wantedRates) needed.push({ "type": res, "wanted": this.wantedRates[res], "current": currentRates[res] }); needed.sort((a, b) => { let va = Math.max(0, a.wanted - a.current) / (a.current + 1); let vb = Math.max(0, b.wanted - b.current) / (b.current + 1); // If they happen to be equal (generally this means "0" aka no need), make it fair. if (va === vb) return a.current - b.current; return vb - va; }); return needed; }; /** * Returns the best position to build a new Civil Centre * Whose primary function would be to reach new resources of type "resource". */ m.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 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. Engine.ProfileStart("findEconomicCCLocation"); // obstruction map let obstructions = m.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", "Elephant"]))); let ccList = []; for (let cc of ccEnts.values()) ccList.push({"pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner())}); let dpList = []; for (let dp of dpEnts.values()) dpList.push({"pos": dp.position()}); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius() / 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() + 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; 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)]; 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(); for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { norm = 0; break; } if (!cc.ally) continue; if (dist < 40000) // Reject if too near from an allied cc { norm = 0; break; } if (dist < 62000) // Disfavor if quite near an allied cc norm *= 0.5; if (dist < minDist) minDist = dist; } if (norm === 0) continue; if (minDist > 170000 && !this.navalMap) // Reject if too far from any allied cc (not connected) { norm = 0; continue; } else if (minDist > 130000) // Disfavor if quite far from any allied cc { if (this.navalMap) { if (minDist > 250000) norm *= 0.5; else norm *= 0.8; } else norm *= 0.5; } 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] & m.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 (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); let cut = 60; if (fromStrategic || proximity) // be less restrictive cut = 30; 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 Centre * Whose primary function would be to assure territorial continuity with our allies */ m.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 = m.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() / obstructions.cellSize); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let currentVal, delta; let distcc0, distcc1, distcc2; 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)) + if (minDist < 1 || minDist > 170000 && !this.navalMap) continue; delta = Math.sqrt(distcc0) - 300; // favor a distance of 300 currentVal = delta*delta; delta = Math.sqrt(distcc1) - 300; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - 300; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] & m.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 * TODO check that it is on same accessIndex */ m.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (!markets.length) markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), 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]; // obstruction map let obstructions = m.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 radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); let isNavalMarket = template.hasClass("NavalMarket"); 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] & m.narrowFrontier_Mask) continue; if (this.basesMap.map[j] === 0) // only in our territory continue; // with enough room around to build the cc 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 gainMultiplier; for (let market of markets) { if (isNavalMarket && market.hasClass("NavalMarket")) { if (m.getSeaAccess(gameState, market) !== gameState.ai.accessibility.getAccessValue(pos, true)) continue; gainMultiplier = traderTemplatesGains.navalGainMultiplier; } else if (m.getLandAccess(gameState, market) === index && !m.isLineInsideEnemyTerritory(gameState, market.position(), pos)) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let val = API3.SquareVectorDistance(market.position(), pos) * gainMultiplier; if (val > maxVal) maxVal = val; } if (maxVal === 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; 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(bestVal / 10000); 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 BarterMarket if (expectedGain < this.tradeManager.minimalGain || (expectedGain < 8 && (!template.hasClass("BarterMarket") || gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()))) return false; 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], expectedGain]; }; /** * Returns the best position to build defensive buildings (fortress and towers) * Whose primary function is to defend our borders */ m.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(); let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClassesOr(["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"])); if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) return undefined; } enemyStructures = enemyStructures.toEntityArray(); let wonderMode = gameState.getGameType() === "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 = m.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() + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius() / 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] & m.fullFrontier_Mask)) continue; if (this.borderMap.map[j] & m.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]]; }; m.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("BarterMarket", true).hasEntities()) return; // Try to build a temple earlier if in regicide to recruit healer guards - // or if we are ready to switch to city phase but miss some town phase structure - if (gameState.currentPhase() < 3 && gameState.getGameType() !== "regicide") - { - if (gameState.currentPhase() < 2 || this.econState !== "cityPhasing") - return; - let requirements = gameState.getPhaseEntityRequirements(3); - if (!requirements.length) - return; - for (let entityReq of requirements) - if (gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length >= entityReq.count) - return; - } + if (this.currentPhase < 3 && gameState.getGameType() !== "regicide") + return; if (!this.canBuild(gameState, "structures/{civ}_temple")) return; queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_temple")); }; m.HQ.prototype.buildMarket = function(gameState, queues) { if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}_market")) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket")) { if (!this.navalMap && !queues.economicBuilding.paused) { // Put available resources in this market when not a naval map 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; } if (gameState.getPopulation() < this.Config.Economy.popForMarket) return; gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; /** Build a farmstead */ m.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 m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); }; /** Build a corral, and train animals there */ m.HQ.prototype.manageCorral = function(gameState, queues) { if (queues.corral.hasQueuedUnits()) return; let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; - if (nCorral === 0 || - (gameState.isTemplateDisabled(gameState.applyCiv("structures/{civ}_field")) && - nCorral < gameState.currentPhase() && gameState.getPopulation() > 30*nCorral)) + if (!nCorral || gameState.isTemplateDisabled(gameState.applyCiv("structures/{civ}_field")) && + nCorral < this.currentPhase && gameState.getPopulation() > 30*nCorral) { if (this.canBuild(gameState, "structures/{civ}_corral")) { queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral")); return; } - if (nCorral === 0) return; + if (!nCorral) + return; } // And train some animals for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) { if (corral.foundationProgress() !== undefined) continue; let trainables = corral.trainableEntities(); 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 m.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… */ m.HQ.prototype.buildMoreHouses = function(gameState, queues) { if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return; let numPlanned = queues.house.length(); - if (numPlanned < 3 || (numPlanned < 5 && gameState.getPopulation() > 80)) + if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) { let plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); // change the starting condition according to the situation. plan.goRequirement = "houseNeeded"; queues.house.addPlan(plan); } - if (numPlanned > 0 && this.econState == "townPhasing" && gameState.getPhaseEntityRequirements(2).length) + if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) { let houseTemplateName = gameState.applyCiv("structures/{civ}_house"); let houseTemplate = gameState.getTemplate(houseTemplateName); let needed = 0; - for (let entityReq of gameState.getPhaseEntityRequirements(2)) + 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.stopBuilding.has(houseTemplateName)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.stopBuilding.delete(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("structures/{civ}_house")); if (gameState.getPhaseEntityRequirements(2).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("structures/{civ}_house"); let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; let popBonus = gameState.getTemplate(house).getPopulationBonus(); let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - gameState.getPopulation(); let priority; if (freeSlots < 5) { if (this.stopBuilding.has(house)) { if (this.stopBuilding.get(house) > gameState.ai.elapsedTime) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to improve with technology"); this.researchManager.researchPopulationBonus(gameState, queues); } else { this.stopBuilding.delete(house); priority = 2*this.Config.priorities.house; } } 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. */ +/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ m.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.hasQueuedUnits()) return; - // first build one cc if all have been destroyed + // First build one cc if all have been destroyed let activeBases = this.numActiveBase(); if (activeBases === 0) { this.buildFirstBase(gameState); return; } - // then expand if we have not enough room available for buildings + // Then expand if we have not enough room available for buildings let nstopped = 0; for (let stopTime of this.stopBuilding.values()) { if (stopTime === Infinity || stopTime < gameState.ai.elapsedTime || ++nstopped < 2) continue; if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build " + uneval(this.stopBuilding)); this.buildNewBase(gameState, queues); return; } - // then expand if we have lots of units (threshold depending on the aggressivity value) + // 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 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); } }; m.HQ.prototype.buildNewBase = function(gameState, queues, resource) { - if (this.numActiveBase() > 0 && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase())) + if (this.numActiveBase() > 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 m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; /** Deals with building fortresses and towers along our border with enemies. */ m.HQ.prototype.buildDefenses = function(gameState, queues) { - if ((this.saveResources && !this.canBarter) || queues.defenseBuilding.hasQueuedUnits()) + if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) return; - if (!this.saveResources && (gameState.currentPhase() > 2 || gameState.isResearching(gameState.cityPhase()))) + 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.numActiveBase() + 1 + this.extraFortresses && 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 m.ConstructionPlan(gameState, "structures/{civ}_fortress"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); return; } } } - if (this.Config.Military.numSentryTowers && gameState.currentPhase() < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower")) + if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower")) { let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers 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 m.ConstructionPlan(gameState, "structures/{civ}_sentry_tower")); } return; } - if (gameState.currentPhase() < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) + if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("DefenseTower", true).filter(API3.Filters.byClass("Town")).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.numActiveBase() + 3 + this.extraTowers && gameState.getOwnFoundationsByClass("DefenseTower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; if (numTowers > 2 * this.numActiveBase() + 3) gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7*this.Config.priorities.defenseBuilding)); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); } }; m.HQ.prototype.buildBlacksmith = function(gameState, queues) { if (gameState.getPopulation() < this.Config.Military.popForBlacksmith || queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length) return; // build a market before the blacksmith if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) return; if (this.canBuild(gameState, "structures/{civ}_blacksmith")) queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); }; /** * Deals with constructing military buildings (barracks, stables…) * They are mostly defined by Config.js. This is unreliable since changes could be done easily. */ m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { - if ((this.saveResources && !this.canBarter) || queues.militaryBuilding.hasQueuedUnits()) + if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) return; if (this.canBuild(gameState, "structures/{civ}_barracks")) { let barrackNb = gameState.getOwnEntitiesByClass("Barracks", true).length; if (this.saveResources && barrackNb > 0) return; // first barracks. if (!barrackNb && (gameState.getPopulation() > this.Config.Military.popForBarracks1 || - (this.econState == "townPhasing" && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5))) + (this.phasing == 2 && + gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5))) { gameState.ai.queueManager.changePriority("militaryBuilding", 2*this.Config.priorities.militaryBuilding); let preferredBase = this.findBestBaseForMilitary(gameState); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase }); plan.queueToReset = "militaryBuilding"; queues.militaryBuilding.addPlan(plan); return; } // second barracks, then 3rd barrack, and optional 4th for some civs as they rely on barracks more. if (barrackNb == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); return; } if (barrackNb == 2 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 20) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); return; } if (barrackNb == 3 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 50 && (gameState.getPlayerCiv() === "gaul" || gameState.getPlayerCiv() === "brit" || gameState.getPlayerCiv() === "iber")) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); return; } } if (this.saveResources) return; - if (gameState.currentPhase() < 3 || gameState.getPopulation() < 80 || !this.bAdvanced.length) + if (this.currentPhase < 3 || gameState.getPopulation() < 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 && gameState.getPopulation() > 110)) + if (!nAdvanced || nAdvanced < this.bAdvanced.length && gameState.getPopulation() > 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; if (template.hasDefensiveFire() || template.trainableEntities()) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "preferredBase": preferredBase })); } else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); return; } } }; /** * Construct military building in bases nearest to the ennemies TODO revisit as the nearest one may not be accessible */ m.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase = 1; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); if (dist > distMin) continue; bestBase = cc.getMetadata(PlayerID, "base"); distMin = dist; } } return bestBase; }; /** * train with highest priority ranged infantry in the nearest civil centre from a given set of positions * and garrison them there for defense */ m.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 (base.anchor.getMetadata(PlayerID, "access") !== 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)) + 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.numberOfGarrisonedUnits(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")) continue; if (autogarrison && !MatchesClassList(template.classes(), 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 m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); return true; }; -m.HQ.prototype.canBuild = function(gameState, structure) +m.HQ.prototype.canBuild = function(gameState, structure, debug = false) { let type = gameState.applyCiv(structure); // available room to build it if (this.stopBuilding.has(type)) { if (this.stopBuilding.get(type) > gameState.ai.elapsedTime) return false; this.stopBuilding.delete(type); } if (gameState.isTemplateDisabled(type)) { this.stopBuilding.set(type, Infinity); return false; } let template = gameState.getTemplate(type); if (!template) this.stopBuilding.set(type, Infinity); if (!template || !template.available(gameState)) return false; if (!gameState.findBuilder(type)) { - this.stopBuild(gameState, type, 120); + this.stopBuilding.set(type, gameState.ai.elapsedTime + 120); return false; } if (this.numActiveBase() < 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.stopBuilding.set(type, gameState.ai.elapsedTime + 180); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) + { + this.stopBuilding.set(type, gameState.ai.elapsedTime + 60); return false; + } return true; }; m.HQ.prototype.stopBuild = function(gameState, structure, time=180) { let type = gameState.applyCiv(structure); if (this.stopBuilding.has(type)) this.stopBuilding.set(type, Math.max(this.stopBuilding.get(type), gameState.ai.elapsedTime + time)); else this.stopBuilding.set(type, gameState.ai.elapsedTime + time); }; m.HQ.prototype.restartBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.stopBuilding.has(type)) this.stopBuilding.delete(type); }; m.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] & m.outside_Mask) continue; if (this.borderMap.map[j] & m.fullFrontier_Mask) this.borderMap.map[j] &= ~m.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]); let index = base.territoryIndices.indexOf(j); if (index == -1) { API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); continue; } base.territoryIndices.splice(index, 1); 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] & m.outside_Mask) continue; let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner !== PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) { this.borderMap.map[j] |= m.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] & m.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] & m.narrowFrontier_Mask)) this.borderMap.map[j] |= m.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 for (let [type, stopTime] of this.stopBuilding) if (stopTime !== Infinity) this.stopBuilding.delete(type); // 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; }; /** * returns the base corresponding to baseID */ m.HQ.prototype.getBaseByID = function(baseID) { for (let base of this.baseManagers) if (base.ID === baseID) return base; API3.warn("Petra error: no base found with ID " + baseID); return undefined; }; /** * returns the number of active (i.e. with one cc) bases */ m.HQ.prototype.numActiveBase = function() { if (!this.turnCache.activeBase) { let num = 0; for (let base of this.baseManagers) if (base.anchor) ++num; this.turnCache.activeBase = num; } return this.turnCache.activeBase; }; m.HQ.prototype.resetActiveBase = function() { this.turnCache.activeBase = undefined; }; /** * Count gatherers returning resources in the number of gatherers of resourceSupplies * to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps). */ m.HQ.prototype.assignGatherers = function() { for (let base of this.baseManagers) { for (let worker of base.workers.values()) { if (worker.unitAIState().split(".")[1] !== "RETURNRESOURCE") 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); } } }; m.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 */ m.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; }; m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) { if (!this.turnCache.firingStructures) this.turnCache.firingStructures = gameState.updatingCollection("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 */ m.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() * m.getAttackBonus(ent, target, "Capture"), "ents": new Set([ent.id()]) }); else { let capturableTarget = this.capturableTargets.get(target.id()); capturableTarget.strength += ent.captureStrength() * m.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 = m.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. */ m.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. */ m.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; } }; m.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. */ m.HQ.prototype.AddTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) ++this.turnCache["resourceGatherer-" + resource]; else this.turnCache["resourceGatherer-" + resource] = 1; this.turnCache.gatherRates = false; }; m.HQ.prototype.GetTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) return this.turnCache["resourceGatherer-" + resource]; return 0; }; /** * Some functions are run every turn * Others once in a while */ m.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.turnCache = {}; this.territoryMap = m.createTerritoryMap(gameState); this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", 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(); - if (this.Config.debug > 1) + // 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; m.dumpEntity(ent); }); - } + } */ this.checkEvents(gameState, events, queues); - this.researchManager.checkPhase(gameState, queues); - - // TODO find a better way to update - if (this.currentPhase != gameState.currentPhase()) - this.currentPhase = gameState.currentPhase(); + if (this.phasing) + this.checkPhaseRequirements(gameState, queues); + else + this.researchManager.checkPhase(gameState, queues); if (this.numActiveBase() > 0) { - this.trainMoreWorkers(gameState, queues); + if (gameState.ai.playedTurn % 4 == 0) + this.trainMoreWorkers(gameState, queues); - if (gameState.ai.playedTurn % 2 == 1) + 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 (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1) this.researchManager.update(gameState, queues); } if (this.numActiveBase() < 1 || - (this.canExpand && gameState.ai.playedTurn % 10 == 7 && gameState.currentPhase() > 1)) + this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) this.checkBaseExpansion(gameState, queues); - if (gameState.currentPhase() > 1) + if (this.currentPhase > 1) { if (!this.canBarter) this.buildMarket(gameState, queues); if (!this.saveResources) { this.buildBlacksmith(gameState, queues); this.buildTemple(gameState, queues); } } this.tradeManager.update(gameState, events, queues); this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > 0) this.buildDefenses(gameState, queues); this.assignGatherers(); for (let i = 0; i < this.baseManagers.length; ++i) { this.baseManagers[i].checkEvents(gameState, events, queues); if ((i + gameState.ai.playedTurn)%this.baseManagers.length === 0) this.baseManagers[i].update(gameState, queues, events); } this.navalManager.update(gameState, queues, events); if (this.Config.difficulty > 0 && (this.numActiveBase() > 0 || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); this.gameTypeManager.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(); }; m.HQ.prototype.Serialize = function() { let properties = { - "econState": this.econState, - "currentPhase": this.currentPhase, + "phasing": this.phasing, "wantedRates": this.wantedRates, "currentRates": this.currentRates, "lastFailedGather": this.lastFailedGather, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, "stopBuilding": this.stopBuilding, "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, "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(" 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(" gameTypeManager " + uneval(this.gameTypeManager.Serialize())); } return { "properties": properties, "baseManagers": baseManagers, "attackManager": this.attackManager.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(), "gameTypeManager": this.gameTypeManager.Serialize(), }; }; m.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 m.BaseManager(gameState, this.Config); newbase.Deserialize(gameState, base); newbase.init(gameState); newbase.Deserialize(gameState, base); this.baseManagers.push(newbase); } this.navalManager = new m.NavalManager(this.Config); this.navalManager.init(gameState, true); this.navalManager.Deserialize(gameState, data.navalManager); this.attackManager = new m.AttackManager(this.Config); this.attackManager.Deserialize(gameState, data.attackManager); this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); this.defenseManager = new m.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); this.tradeManager = new m.TradeManager(this.Config); this.tradeManager.init(gameState); this.tradeManager.Deserialize(gameState, data.tradeManager); this.researchManager = new m.ResearchManager(this.Config); this.researchManager.Deserialize(data.researchManager); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.diplomacyManager.Deserialize(data.diplomacyManager); this.garrisonManager = new m.GarrisonManager(this.Config); this.garrisonManager.Deserialize(data.garrisonManager); this.gameTypeManager = new m.GameTypeManager(this.Config); this.gameTypeManager.Deserialize(data.gameTypeManager); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 20039) @@ -1,776 +1,776 @@ var PETRA = function(m) { /** * 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. */ m.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 */ m.NavalManager.prototype.init = function(gameState, deserializing) { // docks this.docks = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["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()) this.setShipIndex(gameState, ship); for (let dock of this.docks.values()) this.setAccessIndices(gameState, dock); }; m.NavalManager.prototype.updateFishingBoats = function(sea, num) { if (this.wantedFishShips[sea]) this.wantedFishShips[sea] = num; }; m.NavalManager.prototype.resetFishingBoats = function(gameState, sea) { if (sea !== undefined) this.wantedFishShips[sea] = 0; else this.wantedFishShips.fill(0); }; m.NavalManager.prototype.setAccessIndices = function(gameState, ent) { m.getLandAccess(gameState, ent); m.getSeaAccess(gameState, ent); }; m.NavalManager.prototype.setShipIndex = function(gameState, ship) { let sea = gameState.ai.accessibility.getAccessValue(ship.position(), true); ship.setMetadata(PlayerID, "sea", sea); }; /** Get the sea, cache it if not yet done and check if in opensea */ m.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 */ m.NavalManager.prototype.canFishSafely = function(gameState, fish) { if (fish.getMetadata(PlayerID, "opensea")) return true; 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 territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let radius = 140 / 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*t); let j = pos[1] + Math.round(a[1]*radius*t); if (i < 0 || i >= width || j < 0 || j >= width) break; 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 */ m.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) { let seas = gameState.ai.accessibility.regionLinks[region].slice(); this.docks.forEach(function (dock) { if (!dock.hasClass("Dock") || dock.getMetadata(PlayerID, "access") !== region) return; let i = seas.indexOf(dock.getMetadata(PlayerID, "sea")); if (i !== -1) seas.splice(i--,1); }); return seas; }; m.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"))) this.setAccessIndices(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; this.setShipIndex(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(function (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; let self = this; plan.units.forEach(function (ent) { if (!ent.position()) // unit from another ship of this plan ... do nothing return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); 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) self.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"))) this.setAccessIndices(gameState, ent); } }; m.NavalManager.prototype.getPlan = function(ID) { for (let plan of this.transportPlans) if (plan.ID === ID) return plan; return undefined; }; m.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 */ m.NavalManager.prototype.requireTransport = function(gameState, entity, startIndex, endIndex, endPos) { if (!entity.canGarrison()) return false; if (entity.getMetadata(PlayerID, "transport") !== undefined) { if (this.Config.debug > 0) API3.warn("Petra naval manager error: unit " + entity.id() + " has already required a transport"); return false; } for (let plan of this.transportPlans) { if (plan.startIndex !== startIndex || plan.endIndex !== endIndex) continue; if (plan.state !== "boarding") continue; plan.addUnit(entity, endPos); return true; } let plan = new m.TransportPlan(gameState, [entity], 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 */ m.NavalManager.prototype.splitTransport = function(gameState, plan) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan started <<<<"); let newplan = new m.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); let nbUnits = 0; plan.units.forEach(function (ent) { if (ent.getMetadata(PlayerID, "onBoard")) return; ++nbUnits; newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos")); }); if (this.Config.debug > 1) API3.warn(">>>> previous plan left with units " + plan.units.length); if (nbUnits) this.transportPlans.push(newplan); return nbUnits !== 0; }; /** * create a transport from a garrisoned ship to a land location * needed at start game when starting with a garrisoned ship */ m.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 m.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) m.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. m.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]; }; m.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 m.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 m.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); continue; } } } }; /** assigns free ships to plans that need some */ m.NavalManager.prototype.assignShipsToPlans = function(gameState) { for (let plan of this.transportPlans) if (plan.needTransportShips) plan.assignShip(gameState); }; /** let blocking ships move apart from active ships (waiting for a better pathfinder) */ m.NavalManager.prototype.moveApart = function(gameState) { let self = this; this.ships.forEach(function(ship) { if (ship.hasClass("FishingBoat")) // small ships should not be a problem return; let sea = ship.getMetadata(PlayerID, "sea"); if (ship.getMetadata(PlayerID, "transporter") === undefined) { if (ship.isIdle()) // do not stay idle near a dock to not disturb other ships { gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).forEach(function(dock) { if (dock.getMetadata(PlayerID, "sea") !== sea) return; if (API3.SquareVectorDistance(ship.position(), dock.position()) > 2500) return; ship.moveApart(dock.position(), 50); }); } return; } // if transporter ship not idle, move away other ships which could block it self.seaShips[sea].forEach(function(blockingShip) { if (blockingShip === ship || !blockingShip.isIdle()) return; if (API3.SquareVectorDistance(ship.position(), blockingShip.position()) > 900) return; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(ship.position(), 12); else blockingShip.moveApart(ship.position(), 6); }); }); gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).forEach(function(ship) { if (ship.getMetadata(PlayerID, "route") === undefined) return; let sea = ship.getMetadata(PlayerID, "sea"); self.seaShips[sea].forEach(function(blockingShip) { if (blockingShip === ship || !blockingShip.isIdle()) return; if (API3.SquareVectorDistance(ship.position(), blockingShip.position()) > 900) return; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(ship.position(), 12); else blockingShip.moveApart(ship.position(), 6); }); }); }; m.NavalManager.prototype.buildNavalStructures = function(gameState, queues) { if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) return; if (gameState.getPopulation() > this.Config.Economy.popForDock) { if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 && !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), 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 m.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": wantedLand, "sea": sea })); dockStarted = true; break; } } } } - if (gameState.currentPhase() < 2 || gameState.getPopulation() < this.Config.Economy.popForTown + 15 || + if (gameState.currentPhase() < 2 || gameState.getPopulation() < 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; 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 m.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); }; /** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ m.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; }; m.NavalManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Naval Manager update"); this.checkEvents(gameState, queues, events); // 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(); }; m.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 }; }; m.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 m.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); plan.Deserialize(dataPlan); plan.init(gameState); this.transportPlans.push(plan); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queue.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queue.js (revision 20039) @@ -1,165 +1,167 @@ var PETRA = function(m) { /** * Holds a list of wanted plans to train or construct */ m.Queue = function() { this.plans = []; this.paused = false; this.switched = 0; }; m.Queue.prototype.empty = function() { this.plans = []; }; m.Queue.prototype.addPlan = function(newPlan) { if (!newPlan) return; for (let plan of this.plans) { if (newPlan.category === "unit" && plan.type == newPlan.type && plan.number + newPlan.number <= plan.maxMerge) { plan.addItem(newPlan.number); return; } else if (newPlan.category === "technology" && plan.type === newPlan.type) return; } this.plans.push(newPlan); }; m.Queue.prototype.check= function(gameState) { while (this.plans.length > 0) { if (!this.plans[0].isInvalid(gameState)) return; - this.plans.shift(); + let plan = this.plans.shift(); + if (plan.queueToReset) + gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]); } }; m.Queue.prototype.getNext = function() { if (this.plans.length > 0) return this.plans[0]; return null; }; m.Queue.prototype.startNext = function(gameState) { if (this.plans.length > 0) { this.plans.shift().start(gameState); return true; } return false; }; /** * returns the maximal account we'll accept for this queue. * Currently all the cost of the first element and fraction of that of the second */ m.Queue.prototype.maxAccountWanted = function(gameState, fraction) { let cost = new API3.Resources(); if (this.plans.length > 0 && this.plans[0].isGo(gameState)) cost.add(this.plans[0].getCost()); if (this.plans.length > 1 && this.plans[1].isGo(gameState) && fraction > 0) { let costs = this.plans[1].getCost(); costs.multiply(fraction); cost.add(costs); } return cost; }; m.Queue.prototype.queueCost = function() { let cost = new API3.Resources(); for (let plan of this.plans) cost.add(plan.getCost()); return cost; }; m.Queue.prototype.length = function() { return this.plans.length; }; m.Queue.prototype.hasQueuedUnits = function() { return this.plans.length > 0; }; m.Queue.prototype.countQueuedUnits = function() { let count = 0; for (let plan of this.plans) count += plan.number; return count; }; m.Queue.prototype.hasQueuedUnitsWithClass = function(classe) { return this.plans.some(plan => plan.template && plan.template.hasClass(classe)); }; m.Queue.prototype.countQueuedUnitsWithClass = function(classe) { let count = 0; for (let plan of this.plans) if (plan.template && plan.template.hasClass(classe)) count += plan.number; return count; }; m.Queue.prototype.countQueuedUnitsWithMetadata = function(data, value) { let count = 0; for (let plan of this.plans) if (plan.metadata[data] && plan.metadata[data] == value) count += plan.number; return count; }; m.Queue.prototype.Serialize = function() { let plans = []; for (let plan of this.plans) plans.push(plan.Serialize()); return { "plans": plans, "paused": this.paused, "switched": this.switched }; }; m.Queue.prototype.Deserialize = function(gameState, data) { this.paused = data.paused; this.switched = data.switched; this.plans = []; for (let dataPlan of data.plans) { let plan; if (dataPlan.category == "unit") plan = new m.TrainingPlan(gameState, dataPlan.type); else if (dataPlan.category == "building") plan = new m.ConstructionPlan(gameState, dataPlan.type); else if (dataPlan.category == "technology") plan = new m.ResearchPlan(gameState, dataPlan.type); else { API3.warn("Petra deserialization error: plan unknown " + uneval(dataPlan)); continue; } plan.Deserialize(gameState, dataPlan); this.plans.push(plan); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 20039) @@ -1,599 +1,599 @@ var PETRA = function(m) { // This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. // // Currently this manager keeps accounts for each queue, split between the 4 main resources // // Each time resources are available (ie not in any account), it is split between the different queues // Mostly based on priority of the queue, and existing needs. // Each turn, the queue Manager checks if a queue can afford its next item, then it does. // // A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it // If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will // be able to benefit form the 500 food (even if they only needed food). // This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic. // // It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues // get some part of the total, and if all queues have 70% of their needs, nothing gets done // Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting. // // This system should be improved. It's probably not flexible enough. m.QueueManager = function(Config, queues) { this.Config = Config; this.queues = queues; this.priorities = {}; for (let i in Config.priorities) this.priorities[i] = Config.priorities[i]; this.accounts = {}; // the sorting is updated on priority change. this.queueArrays = []; for (let q in this.queues) { this.accounts[q] = new API3.Resources(); this.queueArrays.push([q, this.queues[q]]); } let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.getAvailableResources = function(gameState) { let resources = gameState.getResources(); for (let key in this.queues) resources.subtract(this.accounts[key]); return resources; }; m.QueueManager.prototype.getTotalAccountedResources = function() { let resources = new API3.Resources(); for (let key in this.queues) resources.add(this.accounts[key]); return resources; }; m.QueueManager.prototype.currentNeeds = function(gameState) { let needed = new API3.Resources(); //queueArrays because it's faster. for (let q of this.queueArrays) { let queue = q[1]; if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) continue; let costs = queue.plans[0].getCost(); needed.add(costs); } // get out current resources, not removing accounts. let current = gameState.getResources(); for (let res of needed.types) needed[res] = Math.max(0, needed[res] - current[res]); return needed; }; // calculate the gather rates we'd want to be able to start all elements in our queues // TODO: many things. m.QueueManager.prototype.wantedGatherRates = function(gameState) { // default values for first turn when we have not yet set our queues. if (gameState.ai.playedTurn === 0) { let ret = {}; for (let res of gameState.sharedScript.resourceInfo.codes) ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; return ret; } // get out current resources, not removing accounts. let current = gameState.getResources(); // short queue is the first item of a queue, assumed to be ready in 30s // medium queue is the second item of a queue, assumed to be ready in 60s // long queue contains the isGo=false items, assumed to be ready in 300s let totalShort = {}; let totalMedium = {}; let totalLong = {}; for (let res of gameState.sharedScript.resourceInfo.codes) { totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; } let total; //queueArrays because it's faster. for (let q of this.queueArrays) { let queue = q[1]; if (queue.paused) continue; for (let j = 0; j < queue.length(); ++j) { if (j > 1) break; let cost = queue.plans[j].getCost(); if (queue.plans[j].isGo(gameState)) { if (j === 0) total = totalShort; else total = totalMedium; } else total = totalLong; for (let type in total) total[type] += cost[type]; if (!queue.plans[j].isGo(gameState)) break; } } // global rates let rates = {}; let diff; for (let res of gameState.sharedScript.resourceInfo.codes) { if (current[res] > 0) { diff = Math.min(current[res], totalShort[res]); totalShort[res] -= diff; current[res] -= diff; if (current[res] > 0) { diff = Math.min(current[res], totalMedium[res]); totalMedium[res] -= diff; current[res] -= diff; if (current[res] > 0) totalLong[res] -= Math.min(current[res], totalLong[res]); } } rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; } return rates; }; m.QueueManager.prototype.printQueues = function(gameState) { let numWorkers = 0; gameState.getOwnUnits().forEach (function (ent) { if (ent.getMetadata(PlayerID, "role") === "worker" && ent.getMetadata(PlayerID, "plan") === undefined) numWorkers++; }); API3.warn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + numWorkers); for (let i in this.queues) { let q = this.queues[i]; if (q.hasQueuedUnits()) { API3.warn(i + ": ( with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")"); API3.warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); } for (let plan of q.plans) { let qStr = " " + plan.type + " "; if (plan.number) qStr += "x" + plan.number; qStr += " isGo " + plan.isGo(gameState); API3.warn(qStr); } } API3.warn("Accounts"); for (let p in this.accounts) API3.warn(p + ": " + uneval(this.accounts[p])); API3.warn("Current Resources: " + uneval(gameState.getResources())); API3.warn("Available Resources: " + uneval(this.getAvailableResources(gameState))); API3.warn("Wanted Gather Rates: " + uneval(this.wantedGatherRates(gameState))); API3.warn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); API3.warn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); API3.warn("------------------------------------"); }; m.QueueManager.prototype.clear = function() { for (let i in this.queues) this.queues[i].empty(); }; /** * set accounts of queue i from the unaccounted resources */ m.QueueManager.prototype.setAccounts = function(gameState, cost, i) { let available = this.getAvailableResources(gameState); for (let res of this.accounts[i].types) { if (this.accounts[i][res] >= cost[res]) continue; this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); } }; /** * transfer accounts from queue i to queue j */ m.QueueManager.prototype.transferAccounts = function(cost, i, j) { for (let res of this.accounts[i].types) { if (this.accounts[j][res] >= cost[res]) continue; let diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); this.accounts[i][res] -= diff; this.accounts[j][res] += diff; } }; /** * distribute the resources between the different queues according to their priorities */ m.QueueManager.prototype.distributeResources = function(gameState) { let availableRes = this.getAvailableResources(gameState); for (let res of availableRes.types) { if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) { let total = gameState.getResources()[res]; let scale = total / (total - availableRes[res]); availableRes[res] = total; for (let j in this.queues) { this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); availableRes[res] -= this.accounts[j][res]; } } if (!availableRes[res]) { this.switchResource(gameState, res); continue; } let totalPriority = 0; let tempPrio = {}; let maxNeed = {}; // Okay so this is where it gets complicated. // If a queue requires "res" for the next elements (in the queue) // And the account is not high enough for it. // Then we add it to the total priority. // To try and be clever, we don't want a long queue to hog all resources. So two things: // -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2). // -queues accounts are capped at "resources for the first + 60% of the next" // This avoids getting a high priority queue with many elements hogging all of one resource // uselessly while it awaits for other resources. for (let j in this.queues) { // returns exactly the correct amount, ie 0 if we're not go. let queueCost = this.queues[j].maxAccountWanted(gameState, 0.6); if (this.queues[j].hasQueuedUnits() && this.accounts[j][res] < queueCost[res] && !this.queues[j].paused) { // adding us to the list of queues that need an update. tempPrio[j] = this.priorities[j]; maxNeed[j] = queueCost[res] - this.accounts[j][res]; // if we have enough of that resource for our first item in the queue, diminish our priority. if (this.accounts[j][res] >= this.queues[j].getNext().getCost()[res]) tempPrio[j] /= 2; if (tempPrio[j]) totalPriority += tempPrio[j]; } else if (this.accounts[j][res] > queueCost[res]) { availableRes[res] += this.accounts[j][res] - queueCost[res]; this.accounts[j][res] = queueCost[res]; } } // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" // But we'll sometimes allow less if that would overflow. let available = availableRes[res]; let missing = false; for (let j in tempPrio) { // we'll add at much what can be allowed to this queue. let toAdd = Math.floor(availableRes[res] * tempPrio[j]/totalPriority); if (toAdd >= maxNeed[j]) toAdd = maxNeed[j]; else missing = true; this.accounts[j][res] += toAdd; maxNeed[j] -= toAdd; available -= toAdd; } if (missing && available > 0) // distribute the rest (due to floor) in any queue { for (let j in tempPrio) { let toAdd = Math.min(maxNeed[j], available); this.accounts[j][res] += toAdd; available -= toAdd; if (available <= 0) break; } } if (available < 0) API3.warn("Petra: problem with remaining " + res + " in queueManager " + available); } }; m.QueueManager.prototype.switchResource = function(gameState, res) { // We have no available resources, see if we can't "compact" them in one queue. // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. // TODO: this isn't perfect compression. for (let j in this.queues) { if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) continue; let queue = this.queues[j]; let queueCost = queue.maxAccountWanted(gameState, 0); if (this.accounts[j][res] >= queueCost[res]) continue; for (let i in this.queues) { if (i === j) continue; let otherQueue = this.queues[i]; if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) continue; if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) continue; let diff = queueCost[res] - this.accounts[j][res]; this.accounts[j][res] += diff; this.accounts[i][res] -= diff; ++otherQueue.switched; if (this.Config.debug > 2) API3.warn ("switching queue " + res + " from " + i + " to " + j + " in amount " + diff); break; } } }; // Start the next item in the queue if we can afford it. m.QueueManager.prototype.startNextItems = function(gameState) { for (let q of this.queueArrays) { let name = q[0]; let queue = q[1]; if (queue.hasQueuedUnits() && !queue.paused) { let item = queue.getNext(); if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) { // canStart may update the cost because of the costMultiplier so we must check it again if (this.accounts[name].canAfford(item.getCost())) { this.finishingTime = gameState.ai.elapsedTime; this.accounts[name].subtract(item.getCost()); queue.startNext(gameState); queue.switched = 0; } } } else if (!queue.hasQueuedUnits()) { this.accounts[name].reset(); queue.switched = 0; } } }; m.QueueManager.prototype.update = function(gameState) { Engine.ProfileStart("Queue Manager"); for (let i in this.queues) { this.queues[i].check(gameState); // do basic sanity checks on the queue if (this.priorities[i] > 0) continue; API3.warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. } // Pause or unpause queues depending on the situation this.checkPausedQueues(gameState); // Let's assign resources to plans that need them this.distributeResources(gameState); // Start the next item in the queue if we can afford it. this.startNextItems(gameState); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) this.printQueues(gameState); Engine.ProfileStop(); }; // Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training m.QueueManager.prototype.checkPausedQueues = function(gameState) { let numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker"); - let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popForTown); + let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2); for (let q in this.queues) { let toBePaused = false; if (gameState.ai.HQ.numActiveBase() === 0) toBePaused = q !== "dock" && q !== "civilCentre"; else if (numWorkers < workersMin / 3) toBePaused = q !== "citizenSoldier" && q !== "villager" && q !== "emergency"; else if (numWorkers < workersMin * 2 / 3) toBePaused = q === "civilCentre" || q === "economicBuilding" || q === "militaryBuilding" || q === "defenseBuilding" || q === "healer" || q === "majorTech" || q === "minorTech" || q.indexOf("plan_") !== -1; else if (numWorkers < workersMin) toBePaused = q === "civilCentre" || q === "defenseBuilding" || q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; if (toBePaused) { if (q === "field" && gameState.ai.HQ.needFarm && !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) toBePaused = false; if (q === "corral" && gameState.ai.HQ.needCorral && !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) toBePaused = false; if (q === "dock" && gameState.ai.HQ.needFish && !gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).hasEntities()) toBePaused = false; if (q === "ships" && gameState.ai.HQ.needFish && !gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byClass("FishingBoat")).hasEntities()) toBePaused = false; } let queue = this.queues[q]; if (!queue.paused && toBePaused) { queue.paused = true; this.accounts[q].reset(); } else if (queue.paused && !toBePaused) queue.paused = false; // And reduce the batch sizes of attack queues if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) { queue.plans[0].number = 1; if (queue.plans[1]) queue.plans[1].number = 1; } } }; m.QueueManager.prototype.canAfford = function(queue, cost) { if (!this.accounts[queue]) return false; return this.accounts[queue].canAfford(cost); }; m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) { if (!this.queues[queue]) return; this.queues[queue].paused = true; if (scrapAccounts) this.accounts[queue].reset(); }; m.QueueManager.prototype.unpauseQueue = function(queue) { if (this.queues[queue]) this.queues[queue].paused = false; }; m.QueueManager.prototype.pauseAll = function(scrapAccounts, but) { for (let q in this.queues) { if (q == but) continue; if (scrapAccounts) this.accounts[q].reset(); this.queues[q].paused = true; } }; m.QueueManager.prototype.unpauseAll = function(but) { for (let q in this.queues) if (q != but) this.queues[q].paused = false; }; m.QueueManager.prototype.addQueue = function(queueName, priority) { if (this.queues[queueName] !== undefined) return; this.queues[queueName] = new m.Queue(); this.priorities[queueName] = priority; this.accounts[queueName] = new API3.Resources(); this.queueArrays = []; for (let q in this.queues) this.queueArrays.push([q, this.queues[q]]); let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.removeQueue = function(queueName) { if (this.queues[queueName] === undefined) return; delete this.queues[queueName]; delete this.priorities[queueName]; delete this.accounts[queueName]; this.queueArrays = []; for (let q in this.queues) this.queueArrays.push([q, this.queues[q]]); let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.getPriority = function(queueName) { return this.priorities[queueName]; }; m.QueueManager.prototype.changePriority = function(queueName, newPriority) { if (this.Config.debug > 1) API3.warn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + " to " + newPriority); if (this.queues[queueName] !== undefined) this.priorities[queueName] = newPriority; let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.Serialize = function() { let accounts = {}; let queues = {}; for (let q in this.queues) { queues[q] = this.queues[q].Serialize(); accounts[q] = this.accounts[q].Serialize(); if (this.Config.debug == -100) API3.warn("queueManager serialization: queue " + q + " >>> " + uneval(queues[q]) + " with accounts " + uneval(accounts[q])); } return { "priorities": this.priorities, "queues": queues, "accounts": accounts }; }; m.QueueManager.prototype.Deserialize = function(gameState, data) { this.priorities = data.priorities; this.queues = {}; this.accounts = {}; // the sorting is updated on priority change. this.queueArrays = []; for (let q in data.queues) { this.queues[q] = new m.Queue(); this.queues[q].Deserialize(gameState, data.queues[q]); this.accounts[q] = new API3.Resources(); this.accounts[q].Deserialize(data.accounts[q]); this.queueArrays.push([q, this.queues[q]]); } this.queueArrays.sort((a,b) => data.priorities[b[0]] - data.priorities[a[0]]); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 20039) @@ -1,822 +1,822 @@ var PETRA = function(m) { /** * Defines a construction plan, ie a building. * We'll try to fing a good position if non has been provided */ m.ConstructionPlan = function(gameState, type, metadata, position) { if (!m.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); m.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.findBuilder(this.type) !== undefined; }; m.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 let builder = gameState.findBuilder(this.type); let pos = this.findGoodPosition(gameState); if (!pos) { gameState.ai.HQ.stopBuild(gameState, this.type); Engine.ProfileStop(); return; } if (this.metadata && this.metadata.expectedGain) { // 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)) + 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); }; m.ConstructionPlan.prototype.findGoodPosition = function(gameState) { let template = this.template; if (template.buildPlacementType() === "shore") return this.findDockPosition(gameState); if (template.hasClass("Storehouse") && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = gameState.ai.HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; let newpos = base.findBestDropsiteLocation(gameState, type); 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 = gameState.ai.HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = gameState.ai.HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; return false; } else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) { let pos = gameState.ai.HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; if (template.hasClass("DefenseTower") || gameState.getPlayerCiv() === "mace" || gameState.getPlayerCiv() === "maur" || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_fortress"), true) > 0 || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_army_camp"), true) > 0) return false; // if this fortress is our first siege unit builder, just try the standard placement as we want siege units } else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before { let pos = gameState.ai.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. townPhasing without enough village building), do not apply these constraints + // 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 (gameState.ai.HQ.basesMap.map[j] == base) placement.map[j] = 45; } else { for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] !== 0) placement.map[j] = 45; } if (!gameState.ai.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); if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1) { if (template.hasClass("Field") || template.hasClass("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("StoneWall") || ent.hasClass("Gates")) 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("StoneWall") || ent.hasClass("Gates")))) placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.genericName() == "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; placement.map[j] = value >= 0 ? value : 0; if (gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) placement.map[j] /= 2; // we need space around farmstead, so disfavor map border } } } // 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("BarterMarket"); let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); let preferredBase = this.metadata && this.metadata.preferredBase; if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] != base) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] & m.border_Mask) placement.map[j] += 50; else if (disfavorBorder && !(gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) && placement.map[j] > 0) placement.map[j] += 10; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] === 0) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] & m.border_Mask) placement.map[j] += 50; else if (disfavorBorder && !(gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) && placement.map[j] > 0) placement.map[j] += 10; if (preferredBase && gameState.ai.HQ.basesMap.map[j] == this.metadata.preferredBase) placement.map[j] += 200; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } // 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 = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let radius = 0; if (template.hasClass("Fortress") || this.type === gameState.applyCiv("structures/{civ}_siege_workshop") || this.type === gameState.applyCiv("structures/{civ}_elephant_stables")) radius = Math.floor((template.obstructionRadius() + 12) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && !template.hasClass("Field")) radius = Math.ceil((template.obstructionRadius() + 4) / obstructions.cellSize); else radius = Math.ceil((template.obstructionRadius() + 0.5) / obstructions.cellSize); let bestTile; let bestVal; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); bestVal = bestTile[1]; } if (bestVal === undefined || bestVal === -1) { bestTile = placement.findBestTile(radius, obstructions); bestVal = bestTile[1]; } let bestIdx = bestTile[0]; if (bestVal <= 0) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined) { let secondBest = obstructions.findNearestObstructed(bestIdx, radius); if (secondBest >= 0) { x = (secondBest % obstructions.width + 0.5) * obstructions.cellSize; z = (Math.floor(secondBest / 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": gameState.ai.HQ.basesMap.map[territoryIndex] }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory */ m.ConstructionPlan.prototype.findDockPosition = function(gameState) { let template = this.template; let territoryMap = gameState.ai.HQ.territoryMap; let obstructions = m.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; if (nbShips === 0 && proxyAccess && proxyAccess > 1) { wantedLand = {}; wantedLand[proxyAccess] = true; } let dropsiteTypes = template.resourceDropsiteTypes(); let radius = Math.ceil(template.obstructionRadius() / 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; for (let j = 0; j < territoryMap.length; ++j) { if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) continue; let dist; if (!proxyAccess) { // if not in our (or allied) territory, we do not want it too far to be able to defend it dist = this.getFrontierProximity(gameState, j); if (dist > 4) continue; } 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 (proxyAccess) { // if proximity is given, we look for the nearest point dist = API3.SquareVectorDistance(this.metadata.proximity, pos); dist = Math.sqrt(dist) + 20 * (maxRes - res); } else dist += 0.6 * (maxRes - res); // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) dist += 2; // do a pre-selection, supposing we will have the best possible water if (bestIdx !== undefined && dist > bestVal + 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]) continue; // final selection now that the checkDockPlacement water is known if (bestIdx !== undefined && dist + 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 = dist + 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) { for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex !== bestLand) continue; baseIndex = base.ID; break; } if (!baseIndex) { if (gameState.ai.HQ.numActiveBase() > 0) API3.warn("Petra: dock constructed without base index " + baseIndex); else baseIndex = gameState.ai.HQ.baseManagers[0].ID; } } return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ m.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} */ m.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] ]; m.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) { let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let dist = dimension + 2*cellSize; let x = (j%width + 0.5) * cellSize; let z = (Math.floor(j/width) + 0.5) * cellSize; for (let a of around) { let pos = gameState.ai.accessibility.gamePosToMapPos([x + dist*a[0], z + dist*a[1]]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) continue; if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let landPass = gameState.ai.accessibility.landPassMap[k]; - if (landPass < 2 || (wantedLand && !wantedLand[landPass])) + if (landPass < 2 || wantedLand && !wantedLand[landPass]) continue; pos = gameState.ai.accessibility.gamePosToMapPos([x - dist*a[0], z - dist*a[1]]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) continue; if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; k = pos[0] + pos[1]*gameState.ai.accessibility.width; if (wantedSea && gameState.ai.accessibility.navalPassMap[k] !== wantedSea) continue; else if (!wantedSea && gameState.ai.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 */ m.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] & m.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 */ m.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; }; m.ConstructionPlan.prototype.isGo = function(gameState) { if (this.goRequirement && this.goRequirement === "houseNeeded") { if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house")) return false; if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return false; let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); for (let ent of gameState.getOwnFoundations().values()) freeSlots += ent.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; }; m.ConstructionPlan.prototype.onStart = function(gameState) { if (this.queueToReset) gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); }; m.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 }; }; m.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; let cost = new API3.Resources(); cost.Deserialize(data.cost); this.cost = cost; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanResearch.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanResearch.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanResearch.js (revision 20039) @@ -1,132 +1,114 @@ var PETRA = function(m) { m.ResearchPlan = function(gameState, type, rush = false) { if (!m.QueuePlan.call(this, gameState, type, {})) return false; if (this.template.researchTime === undefined) return false; // Refine the estimated cost let researchers = this.getBestResearchers(gameState, true); if (researchers) this.cost = new API3.Resources(this.template.cost(researchers[0])); this.category = "technology"; this.rush = rush; return true; }; m.ResearchPlan.prototype = Object.create(m.QueuePlan.prototype); m.ResearchPlan.prototype.canStart = function(gameState) { this.researchers = this.getBestResearchers(gameState); if (!this.researchers) return false; this.cost = new API3.Resources(this.template.cost(this.researchers[0])); return true; }; m.ResearchPlan.prototype.getBestResearchers = function(gameState, noRequirementCheck = false) { let allResearchers = gameState.findResearchers(this.type, noRequirementCheck); if (!allResearchers || !allResearchers.hasEntities()) return undefined; // Keep only researchers with smallest cost let costMin = Math.min(); let researchers; for (let ent of allResearchers.values()) { let cost = this.template.costSum(ent); if (cost === costMin) researchers.push(ent); else if (cost < costMin) { costMin = cost; researchers = [ent]; } } return researchers; }; m.ResearchPlan.prototype.isInvalid = function(gameState) { return gameState.isResearched(this.type) || gameState.isResearching(this.type); }; m.ResearchPlan.prototype.start = function(gameState) { // Prefer researcher with shortest queues (no need to serialize this.researchers // as the functions canStart and start are always called on the same turn) this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime()); // Drop anything in the queue if we rush it. if (this.rush) this.researchers[0].stopAllProduction(0.45); this.researchers[0].research(this.type); this.onStart(gameState); }; -m.ResearchPlan.prototype.isGo = function(gameState) -{ - if (this.type === gameState.townPhase()) - { - let ret = gameState.getPopulation() >= gameState.ai.Config.Economy.popForTown; - if (ret && gameState.ai.HQ.econState !== "growth") - gameState.ai.HQ.econState = "growth"; - else if (!ret && gameState.ai.HQ.econState !== "townPhasing") - gameState.ai.HQ.econState = "townPhasing"; - return ret; - } - else if (this.type === gameState.cityPhase()) - gameState.ai.HQ.econState = "cityPhasing"; - return true; -}; - m.ResearchPlan.prototype.onStart = function(gameState) { if (this.queueToReset) gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); - if (this.type == gameState.townPhase()) - { - gameState.ai.HQ.econState = "growth"; - gameState.ai.HQ.OnTownPhase(gameState); - } - else if (this.type == gameState.cityPhase()) + for (let i = gameState.getNumberOfPhases(); i > 0; --i) { - gameState.ai.HQ.econState = "growth"; - gameState.ai.HQ.OnCityPhase(gameState); + if (this.type != gameState.getPhaseName(i)) + continue; + gameState.ai.HQ.phasing = 0; + gameState.ai.HQ.OnPhaseUp(gameState, i); + break; } }; m.ResearchPlan.prototype.Serialize = function() { return { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "rush": this.rush, "queueToReset": this.queueToReset || undefined }; }; m.ResearchPlan.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; let cost = new API3.Resources(); cost.Deserialize(data.cost); this.cost = cost; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js (revision 20039) @@ -1,241 +1,244 @@ var PETRA = function(m) { /** * Manage the research */ m.ResearchManager = function(Config) { this.Config = Config; }; /** * Check if we can go to the next phase */ m.ResearchManager.prototype.checkPhase = function(gameState, queues) { if (queues.majorTech.hasQueuedUnits()) return; + // Don't try to phase up if already trying to gather resources for a civil-centre or wonder + if (queues.civilCentre.hasQueuedUnits() || queues.wonder.hasQueuedUnits()) + return; - let townPhase = gameState.townPhase(); - let cityPhase = gameState.cityPhase(); + let currentPhaseIndex = gameState.currentPhase(); + let nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1); + if (!nextPhaseName) + return; - if (gameState.canResearch(townPhase,true) && gameState.getPopulation() >= this.Config.Economy.popForTown - 10 && - gameState.hasResearchers(townPhase, true)) - { - let plan = new m.ResearchPlan(gameState, townPhase, true); - queues.majorTech.addPlan(plan); - } - else if (gameState.canResearch(cityPhase,true) && gameState.ai.elapsedTime > this.Config.Economy.cityPhase && - gameState.getOwnEntitiesByRole("worker", true).length > this.Config.Economy.workForCity && - gameState.hasResearchers(cityPhase, true) && !queues.civilCentre.hasQueuedUnits()) + let petraRequirements = + currentPhaseIndex == 1 && gameState.getPopulation() >= this.Config.Economy.popPhase2 || + currentPhaseIndex == 2 && gameState.getOwnEntitiesByRole("worker", true).length > this.Config.Economy.workPhase3 || + currentPhaseIndex >= 3 && gameState.getOwnEntitiesByRole("worker", true).length > this.Config.Economy.workPhase4; + if (petraRequirements && gameState.hasResearchers(nextPhaseName, true)) { - let plan = new m.ResearchPlan(gameState, cityPhase, true); - queues.majorTech.addPlan(plan); + gameState.ai.HQ.phasing = currentPhaseIndex + 1; + // Reset the queue priority in case it was changed during a previous phase update + gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech); + queues.majorTech.addPlan(new m.ResearchPlan(gameState, nextPhaseName, true)); } }; m.ResearchManager.prototype.researchPopulationBonus = function(gameState, queues) { if (queues.minorTech.hasQueuedUnits()) return; let techs = gameState.findAvailableTech(); for (let tech of techs) { if (!tech[1]._template.modifications) continue; // TODO may-be loop on all modifs and check if the effect if positive ? if (tech[1]._template.modifications[0].value !== "Cost/PopulationBonus") continue; queues.minorTech.addPlan(new m.ResearchPlan(gameState, tech[0])); break; } }; m.ResearchManager.prototype.researchTradeBonus = function(gameState, queues) { if (queues.minorTech.hasQueuedUnits()) return; let techs = gameState.findAvailableTech(); for (let tech of techs) { if (!tech[1]._template.modifications || !tech[1]._template.affects) continue; if (tech[1]._template.affects.indexOf("Trader") === -1) continue; // TODO may-be loop on all modifs and check if the effect if positive ? if (tech[1]._template.modifications[0].value !== "UnitMotion/WalkSpeed" && tech[1]._template.modifications[0].value !== "Trader/GainMultiplier") continue; queues.minorTech.addPlan(new m.ResearchPlan(gameState, tech[0])); break; } }; /** Techs to be searched for as soon as they are available */ m.ResearchManager.prototype.researchWantedTechs = function(gameState, techs) { let phase1 = gameState.currentPhase() === 1; let available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; let numWorkers = phase1 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; for (let tech of techs) { if (!tech[1]._template.modifications) continue; let template = tech[1]._template; if (phase1) { let cost = template.cost; let costMax = 0; for (let res in cost) costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); if (10*numWorkers < costMax) continue; } for (let i in template.modifications) { if (gameState.ai.HQ.navalMap && template.modifications[i].value === "ResourceGatherer/Rates/food.fish") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "ResourceGatherer/Rates/food.fruit") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "ResourceGatherer/Rates/food.grain") return { "name": tech[0], "increasePriority": false }; else if (template.modifications[i].value === "ResourceGatherer/Rates/wood.tree") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value.startsWith("ResourceGatherer/Capacities")) return { "name": tech[0], "increasePriority": false }; else if (template.modifications[i].value === "Attack/Ranged/MaxRange") return { "name": tech[0], "increasePriority": false }; } } return null; }; /** Techs to be searched for as soon as they are available, but only after phase 2 */ m.ResearchManager.prototype.researchPreferredTechs = function(gameState, techs) { let phase2 = gameState.currentPhase() === 2; let available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; let numWorkers = phase2 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; for (let tech of techs) { if (!tech[1]._template.modifications) continue; let template = tech[1]._template; if (phase2) { let cost = template.cost; let costMax = 0; for (let res in cost) costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); if (10*numWorkers < costMax) continue; } for (let i in template.modifications) { if (template.modifications[i].value === "ResourceGatherer/Rates/stone.rock") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "ResourceGatherer/Rates/metal.ore") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "BuildingAI/DefaultArrowCount") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "Health/RegenRate") return { "name": tech[0], "increasePriority": false }; else if (template.modifications[i].value === "Health/IdleRegenRate") return { "name": tech[0], "increasePriority": false }; } } return null; }; m.ResearchManager.prototype.update = function(gameState, queues) { if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits()) return; let techs = gameState.findAvailableTech(); let techName = this.researchWantedTechs(gameState, techs); if (techName) { if (techName.increasePriority) { gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); let plan = new m.ResearchPlan(gameState, techName.name); plan.queueToReset = "minorTech"; queues.minorTech.addPlan(plan); } else queues.minorTech.addPlan(new m.ResearchPlan(gameState, techName.name)); return; } if (gameState.currentPhase() < 2) return; techName = this.researchPreferredTechs(gameState, techs); if (techName) { if (techName.increasePriority) { gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); let plan = new m.ResearchPlan(gameState, techName.name); plan.queueToReset = "minorTech"; queues.minorTech.addPlan(plan); } else queues.minorTech.addPlan(new m.ResearchPlan(gameState, techName.name)); return; } if (gameState.currentPhase() < 3) return; // remove some techs not yet used by this AI // remove also sharedLos if we have no ally for (let i = 0; i < techs.length; ++i) { let template = techs[i][1]._template; if (template.affects && template.affects.length === 1 && (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "StoneWall")) { techs.splice(i--, 1); continue; } if (template.modifications && template.modifications.length === 1 && template.modifications[0].value === "Player/sharedLos" && !gameState.hasAllies()) { techs.splice(i--, 1); continue; } } if (!techs.length) return; // randomly pick one. No worries about pairs in that case. queues.minorTech.addPlan(new m.ResearchPlan(gameState, pickRandom(techs)[0])); }; m.ResearchManager.prototype.CostSum = function(cost) { let costSum = 0; for (let res in cost) costSum += cost[res]; return costSum; }; m.ResearchManager.prototype.Serialize = function() { return {}; }; m.ResearchManager.prototype.Deserialize = function(data) { }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 20039) @@ -1,533 +1,533 @@ var PETRA = function(m) { /** * determines the strategy to adopt when starting a new game, depending on the initial conditions */ m.HQ.prototype.gameAnalysis = function(gameState) { // Analysis of the terrain and the different access regions if (!this.regionAnalysis(gameState)) return; this.attackManager.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 m.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()) { let newbase = new m.BaseManager(gameState, this.Config); newbase.init(gameState); newbase.setAnchor(gameState, cc); this.baseManagers.push(newbase); } 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.isTemplateDisabled(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 */ m.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")) 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); ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(pos)); 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 (base.territoryIndices.indexOf(territoryIndex) === -1) continue; base.assignEntity(gameState, ent); bestbase = base; break; } if (!bestbase) // entity outside our territory { bestbase = m.getBestBase(gameState, ent); 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 */ m.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 */ m.HQ.prototype.structureAnalysis = function(gameState) { let civref = gameState.playerData.civ; let civ = civref in this.Config.buildings.advanced ? civref : 'default'; this.bAdvanced = []; for (let advanced of this.Config.buildings.advanced[civ]) if (!gameState.isTemplateDisabled(gameState.applyCiv(advanced))) this.bAdvanced.push(gameState.applyCiv(advanced)); }; /** * build our first base * if not enough resource, try first to do a dock */ m.HQ.prototype.buildFirstBase = function(gameState) { let total = gameState.getResources(); let template = gameState.applyCiv("structures/{civ}_civil_centre"); if (gameState.isTemplateDisabled(template)) return; template = gameState.getTemplate(template); let goal = "civil_centre"; let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); if (!total.canAfford(new API3.Resources(template.cost())) && !docks.hasEntities()) { // not enough resource to build a cc, try with a dock to accumulate resources if none yet if (gameState.ai.queues.dock.hasQueuedUnits()) return; template = gameState.applyCiv("structures/{civ}_dock"); if (gameState.isTemplateDisabled(template)) return; template = gameState.getTemplate(template); if (!total.canAfford(new API3.Resources(template.cost()))) return; goal = "dock"; } // 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") && !(ent.hasClass("Support") && ent.hasClass("Elephant"))) continue; if (ent.hasClass("Cavalry")) continue; let pos = ent.position(); if (!pos) { let holder = m.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 m.ConstructionPlan(gameState, "structures/{civ}_dock", { "sea": sea, "proximity": startingPoint[imax].pos })); } else gameState.ai.queues.civilCentre.addPlan(new m.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 */ m.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(function (ent) { if (!num || !num1) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != 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); break; } }); // second pass to affect melee infantry units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(function (ent) { if (!num || !num2) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != 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); break; } }); // and now complete the affectation, including all support units units.forEach(function (ent) { if (!num && !ent.hasClass("Support")) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != 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); 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) */ m.HQ.prototype.configFirstBase = function(gameState) { if (this.baseManagers.length < 2) return; 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 < 24000) { 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); } } // - count the available wood 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 < 24000) { 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.popForTown = Math.floor(0.75 * this.Config.Economy.popForTown); // Switch to town phase sooner to be able to expand + 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); this.attackManager.setRushes(allowed); } // immediatly build a wood dropsite if possible. let template = gameState.applyCiv("structures/{civ}_storehouse"); if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template)) { let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); if (newDP.quality > 40) { // 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) { let cost = new API3.Resources(gameState.getTemplate(template).cost()); gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); } gameState.ai.queues.dropsites.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); } } // and build immediately a corral if needed if (this.needCorral) { template = gameState.applyCiv("structures/{civ}_corral"); if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) gameState.ai.queues.corral.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 20038) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 20039) @@ -1,995 +1,995 @@ var PETRA = function(m) { /** * This class makes a worker do as instructed by the economy manager */ m.Worker = function(base) { this.ent = undefined; this.base = base; this.baseID = base.ID; }; m.Worker.prototype.update = function(gameState, ent) { if (!ent.position() || ent.getMetadata(PlayerID, "plan") === -2 || ent.getMetadata(PlayerID, "plan") === -3) return; // If we are waiting for a transport or we are sailing, just wait if (ent.getMetadata(PlayerID, "transport") !== undefined) return; // 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.accessIndex = gameState.ai.accessibility.getAccessValue(ent.position()); else this.accessIndex = this.base.accessIndex; let subrole = ent.getMetadata(PlayerID, "subrole"); 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) && !this.retryGathering(gameState, subrole)) ent.stopMoving(); // Check that we have not drifted too far if (unitAIState === "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target) { let supply = gameState.getEntityById(orderData.target); if (supply && supply.resourceSupplyType() && supply.resourceSupplyType().generic === "food") { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (gameState.isPlayerEnemy(territoryOwner) && !this.retryGathering(gameState, subrole)) ent.stopMoving(); else if (!gameState.isPlayerAlly(territoryOwner)) { let distanceSquare = ent.hasClass("Cavalry") ? 90000 : 30000; let supplyAccess = gameState.ai.accessibility.getAccessValue(supply.position()); 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 (supplyAccess !== m.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(supply.position(), dropsite.position()) < distanceSquare) { hasFoodDropsiteWithinDistance = true; break; } } if (!hasFoodDropsiteWithinDistance && !this.retryGathering(gameState, subrole)) 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 // 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.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, m.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 (!m.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") && supply.resourceSupplyType().generic !== "treasure" && 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; let isNearby = nearby.some(sup => sup.id === supplyId); if (nearby.length === 0 || isNearby) ent.setMetadata(PlayerID, "supply", supplyId); else { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } } } } } else if (unitAIState === "INDIVIDUAL.RETURNRESOURCE.APPROACHING" && 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()) { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = dropsite.getMetadata(PlayerID, "access"); if (!goalAccess || dropsite.hasClass("Elephant")) { goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", goalAccess); } if (access !== goalAccess) m.returnResources(gameState, this.ent); } } } else if (subrole === "builder") { if (unitAIStateOrder === "REPAIR") { // update our target in case UnitAI sent us to a different foundation because of autocontinue if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && ent.getMetadata(PlayerID, "target-foundation") !== ent.unitAIOrderData()[0].target) ent.setMetadata(PlayerID, "target-foundation", ent.unitAIOrderData()[0].target); // and check that the target still exists (useful in REPAIR.APPROACHING) let target = ent.getMetadata(PlayerID, "target-foundation"); if (target && gameState.getEntityById(target)) 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)) + if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) { ent.setMetadata(PlayerID, "subrole", "idle"); ent.setMetadata(PlayerID, "target-foundation", undefined); // If worker elephant, move away to avoid being trapped in between constructions if (ent.hasClass("Elephant")) this.moveAway(gameState); else 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 access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = m.getLandAccess(gameState, target); let queued = m.returnResources(gameState, ent); if (access === goalAccess) ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, 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); let access = gameState.ai.accessibility.getAccessValue(ent.position()); if (base.accessIndex === access) ent.move(basePos[0], basePos[1]); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, base.accessIndex, basePos); nowhereToHunt = false; break; } } if (nowhereToHunt) ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime); } } else // Perform some sanity checks { if (unitAIStateOrder === "GATHER" || unitAIStateOrder === "RETURNRESOURCE") { // 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.RETURNRESOURCE.APPROACHING") { // Check that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position()) { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = dropsite.getMetadata(PlayerID, "access"); if (!goalAccess || dropsite.hasClass("Elephant")) { goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", goalAccess); } if (access !== goalAccess) m.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); } } }; m.Worker.prototype.retryGathering = function(gameState, subrole) { switch (subrole) { case "gatherer": return this.startGathering(gameState); case "hunter": return this.startHunting(gameState); case "fisher": return this.startFishing(gameState); default: return false; } }; m.Worker.prototype.startGathering = function(gameState) { let access = gameState.ai.accessibility.getAccessValue(this.ent.position()); // First look for possible treasure if any if (this.gatherTreasure(gameState)) 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; 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 (m.IsSupplyFull(gameState, supplies[i].ent)) continue; let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) 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.accessIndex === access) { 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 !== access) 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 !== access) 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 !== access) 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 || foundation.getMetadata(PlayerID, "access") !== access) return false; if (foundation.resourceDropsiteTypes() && foundation.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.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 === access) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); if (supply && navalManager.requireTransport(gameState, this.ent, access, 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 === access) continue; supply = this.gatherNearestField(gameState, base.ID); if (supply && navalManager.requireTransport(gameState, this.ent, access, 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, access, 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 === access) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); if (supply && navalManager.requireTransport(gameState, this.ent, access, 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 || foundation.getMetadata(PlayerID, "access") === access) return false; if (foundation.resourceDropsiteTypes() && foundation.resourceDropsiteTypes().indexOf(resource) !== -1) { let foundationAccess = m.getLandAccess(gameState, foundation); if (navalManager.requireTransport(gameState, this.ent, access, 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()); 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.accessIndex === access) { 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 !== access) 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 === access) continue; supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); if (supply && navalManager.requireTransport(gameState, this.ent, access, 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) 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 */ m.Worker.prototype.startHunting = function(gameState, position) { // First look for possible treasure if any if (!position && this.gatherTreasure(gameState)) return true; let resources = gameState.getHuntableSupplies(); if (!resources.hasEntities()) return false; let nearestSupplyDist = Math.min(); let nearestSupply; let isCavalry = this.ent.hasClass("Cavalry"); let isRanged = this.ent.hasClass("Ranged"); let entPosition = position ? position : this.ent.position(); let access = gameState.ai.accessibility.getAccessValue(entPosition); 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 !== m.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) return true; } return false; }; resources.forEach(function(supply) { if (!supply.position()) return; let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) return; if (m.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; let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; // Only cavalry and range units should hunt fleeing animals if (canFlee && !isCavalry && !isRanged) return; let supplyAccess = gameState.ai.accessibility.getAccessValue(supply.position()); if (supplyAccess !== access) return; // measure the distance to the resource let dist = API3.SquareVectorDistance(entPosition, supply.position()); if (dist > nearestSupplyDist) return; // Only cavalry should hunt faraway if (!isCavalry && dist > 25000) return; // Avoid ennemy territory let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally return; // And if in ally territory, don't hunt this ally's cattle if (territoryOwner !== 0 && territoryOwner !== PlayerID && supply.owner() === territoryOwner) return; // Only cavalry should hunt far from dropsite (specially for non domestic animals which flee) if (!isCavalry && canFlee && territoryOwner === 0) return; let distanceSquare = isCavalry ? 35000 : ( canFlee ? 7000 : 12000); if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) return; 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; }; m.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 = this.ent.getMetadata(PlayerID, "sea"); 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 !== m.getSeaAccess(gameState, dropsite)) continue; distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); } return distMin; }; let exhausted = true; 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; if (m.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; }; m.Worker.prototype.gatherNearestField = function(gameState, baseID) { let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); let bestFarmEnt = false; let bestFarmDist = 10000000; for (let field of ownFields.values()) { if (m.IsSupplyFull(gameState, field)) continue; let dist = API3.SquareVectorDistance(field.position(), this.ent.position()); if (dist < bestFarmDist) { bestFarmEnt = field; bestFarmDist = dist; } } if (bestFarmEnt) { gameState.ai.HQ.AddTCGatherer(bestFarmEnt.id()); this.ent.setMetadata(PlayerID, "supply", bestFarmEnt.id()); } return bestFarmEnt; }; /** * 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. */ m.Worker.prototype.buildAnyField = function(gameState, baseID) { if (!this.ent.isBuilder()) return false; let baseFoundations = gameState.getOwnFoundations().filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); let maxGatherers = gameState.getTemplate(gameState.applyCiv("structures/{civ}_field")).maxGatherers(); let bestFarmEnt = false; let bestFarmDist = 10000000; let pos = this.ent.position(); for (let found of baseFoundations.values()) { if (!found.hasClass("Field")) continue; let current = found.getBuildersNb(); if (current === undefined || current >= maxGatherers) continue; let dist = API3.SquareVectorDistance(found.position(), pos); if (dist > bestFarmDist) continue; bestFarmEnt = found; bestFarmDist = dist; } return bestFarmEnt; }; /** * Look for some treasure to gather */ m.Worker.prototype.gatherTreasure = function(gameState) { let rates = this.ent.resourceGatherRates(); if (!rates || !rates.treasure || rates.treasure <= 0) return false; let treasureFound; let distmin = Math.min(); let access = gameState.ai.accessibility.getAccessValue(this.ent.position()); for (let treasure of gameState.ai.HQ.treasures.values()) { if (m.IsSupplyFull(gameState, treasure)) continue; // let some time for the previous gatherer to reach the treasure befor trying again let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) continue; if (access !== m.getLandAccess(gameState, treasure)) continue; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) continue; let dist = API3.SquareVectorDistance(this.ent.position(), treasure.position()); if (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); this.ent.gather(treasureFound); gameState.ai.HQ.AddTCGatherer(treasureFound.id()); this.ent.setMetadata(PlayerID, "supply", treasureFound.id()); return true; }; /** * 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) */ m.Worker.prototype.moveAway = function(gameState) { let gatherers = this.base.workersBySubrole(gameState, "gatherer"); let pos = this.ent.position(); let dist = Math.min(); let destination = pos; for (let gatherer of gatherers.values()) { if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined) continue; if (gatherer.isIdle()) continue; let distance = API3.SquareVectorDistance(pos, gatherer.position()); if (distance > dist) continue; dist = distance; destination = gatherer.position(); } this.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). */ m.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; }; return m; }(PETRA);