Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 26243) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 26244) @@ -1,1130 +1,1142 @@ /** * Base Manager * Handles lower level economic stuffs. * Some tasks: * -tasking workers: gathering/hunting/building/repairing?/scouting/plans. * -giving feedback/estimates on GR * -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans. * -getting good spots for dropsites * -managing dropsite use in the base * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) */ PETRA.BaseManager = function(gameState, basesManager) { this.Config = basesManager.Config; this.ID = gameState.ai.uniqueIDs.bases++; this.basesManager = basesManager; // anchor building: seen as the main building of the base. Needs to have territorial influence this.anchor = undefined; this.anchorId = undefined; this.accessIndex = undefined; // Maximum distance (from any dropsite) to look for resources // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max this.maxDistResourceSquare = 360*360; this.constructing = false; // Defenders to train in this cc when its construction is finished this.neededDefenders = this.Config.difficulty > PETRA.DIFFICULTY_EASY ? 3 + 2*(this.Config.difficulty - 3) : 0; // vector for iterating, to check one use the HQ map. this.territoryIndices = []; this.timeNextIdleCheck = 0; }; PETRA.BaseManager.STATE_WITH_ANCHOR = "anchored"; /** * New base with a foundation anchor. */ PETRA.BaseManager.STATE_UNCONSTRUCTED = "unconstructed"; /** * Captured base with an anchor. */ PETRA.BaseManager.STATE_CAPTURED = "captured"; /** * Anchorless base, currently with dock. */ PETRA.BaseManager.STATE_ANCHORLESS = "anchorless"; PETRA.BaseManager.prototype.init = function(gameState, state) { if (state === PETRA.BaseManager.STATE_UNCONSTRUCTED) this.constructing = true; else if (state !== PETRA.BaseManager.STATE_CAPTURED) this.neededDefenders = 0; this.workerObject = new PETRA.Worker(this); // entitycollections this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER)); this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.mobileDropsites = this.units.filter(API3.Filters.isDropsite()); this.units.registerUpdates(); this.workers.registerUpdates(); this.buildings.registerUpdates(); this.mobileDropsites.registerUpdates(); // array of entity IDs, with each being this.dropsites = {}; this.dropsiteSupplies = {}; this.gatherers = {}; for (let res of Resources.GetCodes()) { this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; } }; PETRA.BaseManager.prototype.reset = function(gameState, state) { if (state === PETRA.BaseManager.STATE_UNCONSTRUCTED) this.constructing = true; else this.constructing = false; if (state !== PETRA.BaseManager.STATE_CAPTURED || this.Config.difficulty < PETRA.DIFFICULTY_MEDIUM) this.neededDefenders = 0; else this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); }; PETRA.BaseManager.prototype.assignEntity = function(gameState, ent) { ent.setMetadata(PlayerID, "base", this.ID); this.units.updateEnt(ent); this.workers.updateEnt(ent); this.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit")) this.assignResourceToDropsite(gameState, ent); }; PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) { if (!anchorEntity.hasClass("CivCentre")) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor."); else { this.anchor = anchorEntity; this.anchorId = anchorEntity.id(); this.anchor.setMetadata(PlayerID, "baseAnchor", true); this.basesManager.resetBaseCache(); } anchorEntity.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(anchorEntity); this.accessIndex = PETRA.getLandAccess(gameState, anchorEntity); return true; }; /* we lost our anchor. Let's reassign our units and buildings */ PETRA.BaseManager.prototype.anchorLost = function(gameState, ent) { this.anchor = undefined; this.anchorId = undefined; this.neededDefenders = 0; this.basesManager.resetBaseCache(); }; /** Set a building of an anchorless base */ PETRA.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent) { if (!this.buildings.hasEntities()) { if (!PETRA.getBuiltEntity(gameState, ent).resourceDropsiteTypes()) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin."); this.accessIndex = PETRA.getLandAccess(gameState, ent); } else if (this.accessIndex !== PETRA.getLandAccess(gameState, ent)) API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + " has been assigned " + ent.templateName() + " with access" + PETRA.getLandAccess(gameState, ent)); ent.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(ent); return true; }; /** * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area. * Moving resources (animals) and buildable resources (fields) are treated elsewhere. */ PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite) { if (this.dropsites[dropsite.id()]) { if (this.Config.debug > 0) warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); return; } let accessIndex = this.accessIndex; let dropsitePos = dropsite.position(); let dropsiteId = dropsite.id(); this.dropsites[dropsiteId] = true; if (this.ID == this.basesManager.baselessBase().ID) accessIndex = PETRA.getLandAccess(gameState, dropsite); let maxDistResourceSquare = this.maxDistResourceSquare; for (let type of dropsite.resourceDropsiteTypes()) { let resources = gameState.getResourceSupplies(type); if (!resources.length) continue; let nearby = this.dropsiteSupplies[type].nearby; let medium = this.dropsiteSupplies[type].medium; let faraway = this.dropsiteSupplies[type].faraway; resources.forEach(function(supply) { if (!supply.position()) return; // Moving resources and fields are treated differently. if (supply.hasClasses(["Animal", "Field"])) return; // quick accessibility check if (PETRA.getLandAccess(gameState, supply) != accessIndex) return; let dist = API3.SquareVectorDistance(supply.position(), dropsitePos); if (dist < maxDistResourceSquare) { if (dist < maxDistResourceSquare/16) // distmax/4 nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else if (dist < maxDistResourceSquare/4) // distmax/2 medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); } }); nearby.sort((r1, r2) => r1.dist - r2.dist); medium.sort((r1, r2) => r1.dist - r2.dist); faraway.sort((r1, r2) => r1.dist - r2.dist); /* let debug = false; if (debug) { faraway.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); }); medium.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); }); nearby.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); }); } */ } // Allows all allies to use this dropsite except if base anchor to be sure to keep // a minimum of resources for this base Engine.PostCommand(PlayerID, { "type": "set-dropsite-sharing", "entities": [dropsiteId], "shared": dropsiteId != this.anchorId }); }; +PETRA.BaseManager.prototype.removeFromAssignedDropsite = function(ent) +{ + for (const type in this.dropsiteSupplies) + for (const proxim in this.dropsiteSupplies[type]) + { + const resourcesList = this.dropsiteSupplies[type][proxim]; + for (let i = 0; i < resourcesList.length; ++i) + if (resourcesList[i].id === ent.id()) + resourcesList.splice(i--, 1); + } +}; + // completely remove the dropsite resources from our list. PETRA.BaseManager.prototype.removeDropsite = function(gameState, ent) { if (!ent.id()) return; let removeSupply = function(entId, supply){ for (let i = 0; i < supply.length; ++i) { // exhausted resource, remove it from this list if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) supply.splice(i--, 1); // resource assigned to the removed dropsite, remove it else if (supply[i].dropsite == entId) supply.splice(i--, 1); } }; for (let type in this.dropsiteSupplies) { removeSupply(ent.id(), this.dropsiteSupplies[type].nearby); removeSupply(ent.id(), this.dropsiteSupplies[type].medium); removeSupply(ent.id(), this.dropsiteSupplies[type].faraway); } this.dropsites[ent.id()] = undefined; }; /** * @return {Object} - The position of the best place to build a new dropsite for the specified resource, * its quality and its template name. */ PETRA.BaseManager.prototype.findBestDropsiteAndLocation = function(gameState, resource) { let bestResult = { "quality": 0, "pos": [0, 0] }; for (const templateName of gameState.ai.HQ.buildManager.findStructuresByFilter(gameState, API3.Filters.isDropsite(resource))) { const dp = this.findBestDropsiteLocation(gameState, resource, templateName); if (dp.quality < bestResult.quality) continue; bestResult = dp; bestResult.templateName = templateName; } return bestResult; }; /** * Returns the position of the best place to build a new dropsite for the specified resource and dropsite template. */ PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource, templateName) { const template = gameState.getTemplate(gameState.applyCiv(templateName)); // CCs and Docks are handled elsewhere. if (template.hasClasses(["CivCentre", "Dock"])) return { "quality": 0, "pos": [0, 0] }; let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. let obstructions = PETRA.createObstructionMap(gameState, this.accessIndex, template); const dpEnts = gameState.getOwnStructures().filter(API3.Filters.isDropsite(resource)).toEntityArray(); // Foundations don't have the dropsite properties yet, so treat them separately. for (const foundation of gameState.getOwnFoundations().toEntityArray()) if (PETRA.getBuiltEntity(gameState, foundation).isResourceDropsite(resource)) dpEnts.push(foundation); let bestIdx; let bestVal = 0; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let cellSize = territoryMap.cellSize; const droppableResources = template.resourceDropsiteTypes(); for (let j of this.territoryIndices) { let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) // no room around continue; // We add 3 times the needed resource and once others that can be dropped here. let total = 2 * gameState.sharedScript.resourceMaps[resource].map[j]; for (const res in gameState.sharedScript.resourceMaps) if (droppableResources.indexOf(res) != -1) total += gameState.sharedScript.resourceMaps[res].map[j]; total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 if (total <= bestVal) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let dp of dpEnts) { let dpPos = dp.position(); if (!dpPos) continue; let dist = API3.SquareVectorDistance(dpPos, pos); if (dist < 3600) { total = 0; break; } else if (dist < 6400) total *= (Math.sqrt(dist)-60)/20; } if (total <= bestVal) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = total; bestIdx = i; } if (this.Config.debug > 2) warn(" for dropsite best is " + bestVal); if (bestVal <= 0) return { "quality": bestVal, "pos": [0, 0] }; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return { "quality": bestVal, "pos": [x, z] }; }; PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, distances = ["nearby", "medium", "faraway"]) { let count = 0; let check = {}; for (const proxim of distances) for (const supply of this.dropsiteSupplies[type][proxim]) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; count += supply.ent.resourceSupplyAmount(); } return count; }; /** check our resource levels and react accordingly */ PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues) { for (let type of Resources.GetCodes()) { if (type == "food") { const prox = ["nearby"]; if (gameState.currentPhase() < 2) prox.push("medium"); if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms. { const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations let numQueue = queues.field.countQueuedUnits(); // TODO if not yet farms, add a check on time used/lost and build farmstead if needed if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them { if (count < 600) { queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID })); gameState.ai.HQ.needFarm = true; } } else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) { let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length; let goal = this.Config.Economy.provisionFields; if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) goal = Math.max(goal-1, 1); if (numFound + numQueue < goal) queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID })); } else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID })); continue; } if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) { const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted if (count < 900) { queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID })); gameState.ai.HQ.needCorral = true; } } continue; } // Non food stuff if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() || gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities()) { this.gatherers[type].nextCheck = gameState.ai.playedTurn; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; continue; } if (gameState.ai.playedTurn < this.gatherers[type].nextCheck) continue; for (let ent of this.gatherersByType(gameState, type).values()) { if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") ++this.gatherers[type].used; else if (ent.unitAIState() == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") ++this.gatherers[type].lost; } // TODO add also a test on remaining resources. let total = this.gatherers[type].used + this.gatherers[type].lost; if (total > 150 || total > 60 && type != "wood") { let ratio = this.gatherers[type].lost / total; if (ratio > 0.15) { const newDP = this.findBestDropsiteAndLocation(gameState, type); if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos)); else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits()) { // No good dropsite, try to build a new base if no base already planned, // and if not possible, be less strict on dropsite quality. if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && newDP.quality > Math.min(25, 50*0.15/ratio) && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos)); } } this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; } else if (total == 0) this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; } }; /** Adds the estimated gather rates from this base to the currentRates */ PETRA.BaseManager.prototype.addGatherRates = function(gameState, currentRates) { for (let res in currentRates) { // I calculate the exact gathering rate for each unit. // I must then lower that to account for travel time. // Given that the faster you gather, the more travel time matters, // I use some logarithms. // TODO: this should take into account for unit speed and/or distance to target this.gatherersByType(gameState, res).forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); if (res == "food") { this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_HUNTER).forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_FISHER).forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); } } }; PETRA.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) { if (!roleless) roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values(); for (let ent of roleless) { if (ent.hasClasses(["Worker", "CitizenSoldier", "FishingBoat"])) ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER); } }; /** * If the numbers of workers on the resources is unbalanced then set some of workers to idle so * they can be reassigned by reassignIdleWorkers. * TODO: actually this probably should be in the HQ. */ PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) { this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; // change resource only towards one which is more needed, and if changing will not change this order let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); let sumWanted = 0; let sumCurrent = 0; for (let need of mostNeeded) { sumWanted += need.wanted; sumCurrent += need.current; } let scale = 1; if (sumWanted > 0) scale = sumCurrent / sumWanted; for (let i = mostNeeded.length-1; i > 0; --i) { let lessNeed = mostNeeded[i]; for (let j = 0; j < i; ++j) { let moreNeed = mostNeeded[j]; let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; // Ensure that the most wanted resource is not exhausted if (moreNeed.type != "food" && this.basesManager.isResourceExhausted(moreNeed.type)) { if (lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) continue; // And if so, move the gatherer to the less wanted one. nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb); if (nb == 0) return; } // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1 // but we require a bit more to avoid too frequent changes if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 || lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) { nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb); if (nb == 0) return; } } } }; /** * Switch some gatherers (limited to number) from resource "from" to resource "to" * and return remaining number of possible switches. * Prefer FemaleCitizen for food and CitizenSoldier for other resources. */ PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, number) { let num = number; let only; let gatherers = this.gatherersByType(gameState, from); if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities()) only = "CitizenSoldier"; else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities()) only = "FemaleCitizen"; for (let ent of gatherers.values()) { if (num == 0) return num; if (!ent.canGather(to)) continue; if (only && !ent.hasClass(only)) continue; --num; ent.stopMoving(); ent.setMetadata(PlayerID, "gather-type", to); this.basesManager.AddTCResGatherer(to); } return num; }; PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) { // Search for idle workers, and tell them to gather resources based on demand if (!idleWorkers) { const filter = API3.Filters.byMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE); idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); } for (let ent of idleWorkers) { // Check that the worker isn't garrisoned if (!ent.position()) continue; if (ent.hasClass("Worker")) { // Just emergency repairing here. It is better managed in assignToFoundations if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) ent.repair(this.anchor); else if (ent.isGatherer()) { let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); for (let needed of mostNeeded) { if (!ent.canGather(needed.type)) continue; let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; if (needed.type != "food" && this.basesManager.isResourceExhausted(needed.type)) continue; ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_GATHERER); ent.setMetadata(PlayerID, "gather-type", needed.type); this.basesManager.AddTCResGatherer(needed.type); break; } } } else if (PETRA.isFastMoving(ent) && ent.canGather("food") && ent.canAttackClass("Animal")) ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_HUNTER); else if (ent.hasClass("FishingBoat")) ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_FISHER); } }; PETRA.BaseManager.prototype.workersBySubrole = function(gameState, subrole) { return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers); }; PETRA.BaseManager.prototype.gatherersByType = function(gameState, type) { return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_GATHERER)); }; /** * returns an entity collection of workers. * They are idled immediatly and their subrole set to idle. */ PETRA.BaseManager.prototype.pickBuilders = function(gameState, workers, number) { let availableWorkers = this.workers.filter(ent => { if (!ent.position() || !ent.isBuilder()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); availableWorkers.sort((a, b) => { let vala = 0; let valb = 0; if (a.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER) vala = 100; if (b.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER) valb = 100; if (a.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_IDLE) vala = -50; if (b.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_IDLE) valb = -50; if (a.getMetadata(PlayerID, "plan") === undefined) vala = -20; if (b.getMetadata(PlayerID, "plan") === undefined) valb = -20; return vala - valb; }); let needed = Math.min(number, availableWorkers.length - 3); for (let i = 0; i < needed; ++i) { availableWorkers[i].stopMoving(); availableWorkers[i].setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE); workers.addEnt(availableWorkers[i]); } return; }; /** * If we have some foundations, and we don't have enough builder-workers, * try reassigning some other workers who are nearby * AI tries to use builders sensibly, not completely stopping its econ. */ PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) { let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field")))); let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); // Check if nothing to build if (!foundations.length && !damagedBuildings.length) return; let workers = this.workers.filter(ent => ent.isBuilder()); const builderWorkers = this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_BUILDER); let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle()); // if we're constructing and we have the foundations to our base anchor, only try building that. if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities()) { foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)); let tID = foundations.toEntityArray()[0].id(); workers.forEach(ent => { let target = ent.getMetadata(PlayerID, "target-foundation"); if (target && target != tID) { ent.stopMoving(); ent.setMetadata(PlayerID, "target-foundation", tID); } }); } if (workers.length < 3) { const fromOtherBase = this.basesManager.bulkPickWorkers(gameState, this, 2); if (fromOtherBase) { let baseID = this.ID; fromOtherBase.forEach(worker => { worker.setMetadata(PlayerID, "base", baseID); worker.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER); workers.updateEnt(worker); builderWorkers.updateEnt(worker); idleBuilderWorkers.updateEnt(worker); }); } } let builderTot = builderWorkers.length - idleBuilderWorkers.length; // Make the limit on number of builders depends on the available resources let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); let builderRatio = 1; for (let res of Resources.GetCodes()) { if (availableResources[res] < 200) { builderRatio = 0.2; break; } else if (availableResources[res] < 1000) builderRatio = Math.min(builderRatio, availableResources[res] / 1000); } for (let target of foundations.values()) { if (target.hasClass("Field")) continue; // we do not build fields if (gameState.ai.HQ.isNearInvadingArmy(target.position())) if (!target.hasClasses(["CivCentre", "Wall"]) && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; // if our territory has shrinked since this foundation was positioned, do not build it if (PETRA.isNotWorthBuilding(gameState, target)) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); if (maxTotalBuilders < 2 && workers.length > 1) maxTotalBuilders = 2; if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 && gameState.getPopulationLimit() < gameState.getPopulationMax()) maxTotalBuilders += 2; let targetNB = 2; if (target.hasClasses(["Fortress", "Wonder"]) || target.getMetadata(PlayerID, "phaseUp") == true) targetNB = 7; else if (target.hasClasses(["Barracks", "Range", "Stable", "Tower", "Market"])) targetNB = 4; else if (target.hasClasses(["House", "DropsiteWood"])) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { targetNB = 15; maxTotalBuilders = Math.max(maxTotalBuilders, 15); } if (!this.basesManager.hasActiveBase()) { targetNB = workers.length; maxTotalBuilders = targetNB; } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER) return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); let time = target.buildTime(); nonBuilderWorkers.sort((workerA, workerB) => { let coeffA = API3.SquareVectorDistance(target.position(), workerA.position()); if (workerA.getMetadata(PlayerID, "gather-type") == "food") coeffA *= 3; let coeffB = API3.SquareVectorDistance(target.position(), workerB.position()); if (workerB.getMetadata(PlayerID, "gather-type") == "food") coeffB *= 3; return coeffA - coeffB; }); let current = 0; let nonBuilderTot = nonBuilderWorkers.length; while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) { ++assigned; ++builderTot; let ent = nonBuilderWorkers[current++]; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER); ent.setMetadata(PlayerID, "target-foundation", target.id()); } } for (let target of damagedBuildings.values()) { // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building // that's being destroyed. if (gameState.ai.HQ.isNearInvadingArmy(target.position())) { if (target.healthLevel() > 0.5 || !target.hasClasses(["CivCentre", "Wall"]) && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; } else if (noRepair && !target.hasClass("CivCentre")) continue; if (target.decaying()) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); let targetNB = 1; if (target.hasClasses(["Fortress", "Wonder"])) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio)); targetNB = 5; if (target.healthLevel() < 0.3) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio)); targetNB = 7; } } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER) return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }); let num = Math.min(nonBuilderWorkers.length, targetNB-assigned); let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); nearestNonBuilders.forEach(function(ent) { ++assigned; ++builderTot; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER); ent.setMetadata(PlayerID, "target-foundation", target.id()); }); } }; /** Return false when the base is not active (no workers on it) */ PETRA.BaseManager.prototype.update = function(gameState, queues, events) { if (this.ID == this.basesManager.baselessBase().ID) { // if some active base, reassigns the workers/buildings // otherwise look for anything useful to do, i.e. treasures to gather if (this.basesManager.hasActiveBase()) { for (let ent of this.units.values()) { let bestBase = PETRA.getBestBase(gameState, ent); if (bestBase.ID != this.ID) bestBase.assignEntity(gameState, ent); } for (let ent of this.buildings.values()) { let bestBase = PETRA.getBestBase(gameState, ent); if (!bestBase) { if (ent.hasClass("Dock")) API3.warn("Petra: dock in 'noBase' baseManager. It may be useful to do an anchorless base for " + ent.templateName()); continue; } if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); bestBase.assignEntity(gameState, ent); } } else if (gameState.ai.HQ.canBuildUnits) { this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); } return false; } if (!this.anchor) // This anchor has been destroyed, but the base may still be usable { if (!this.buildings.hasEntities()) { // Reassign all remaining entities to its nearest base for (let ent of this.units.values()) { let base = PETRA.getBestBase(gameState, ent, false, this.ID); base.assignEntity(gameState, ent); } return false; } // If we have a base with anchor on the same land, reassign everything to it let reassignedBase; for (let ent of this.buildings.values()) { if (!ent.position()) continue; let base = PETRA.getBestBase(gameState, ent); if (base.anchor) reassignedBase = base; break; } if (reassignedBase) { for (let ent of this.units.values()) reassignedBase.assignEntity(gameState, ent); for (let ent of this.buildings.values()) { if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); reassignedBase.assignEntity(gameState, ent); } return false; } this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); return true; } Engine.ProfileStart("Base update - base " + this.ID); this.checkResourceLevels(gameState, queues); this.assignToFoundations(gameState); if (this.constructing) { let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); if(owner != 0 && !gameState.isPlayerAlly(owner)) { // we're in enemy territory. If we're too close from the enemy, destroy us. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (cc.owner() != owner) continue; if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) continue; this.anchor.destroy(); this.basesManager.resetBaseCache(); break; } } } else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) --this.neededDefenders; if (gameState.ai.elapsedTime > this.timeNextIdleCheck && (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); // check if workers can find something useful to do for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); Engine.ProfileStop(); return true; }; PETRA.BaseManager.prototype.AddTCGatherer = function(supplyID) { return this.basesManager.AddTCGatherer(supplyID); }; PETRA.BaseManager.prototype.RemoveTCGatherer = function(supplyID) { this.basesManager.RemoveTCGatherer(supplyID); }; PETRA.BaseManager.prototype.GetTCGatherer = function(supplyID) { return this.basesManager.GetTCGatherer(supplyID); }; PETRA.BaseManager.prototype.Serialize = function() { return { "ID": this.ID, "anchorId": this.anchorId, "accessIndex": this.accessIndex, "maxDistResourceSquare": this.maxDistResourceSquare, "constructing": this.constructing, "gatherers": this.gatherers, "neededDefenders": this.neededDefenders, "territoryIndices": this.territoryIndices, "timeNextIdleCheck": this.timeNextIdleCheck }; }; PETRA.BaseManager.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/basesManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/basesManager.js (revision 26243) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/basesManager.js (revision 26244) @@ -1,787 +1,789 @@ /** * Bases Manager * Manages the list of available bases and queries information from those (e.g. resource levels). * Only one base is run every turn. */ PETRA.BasesManager = function(Config) { this.Config = Config; this.currentBase = 0; // Cache some quantities for performance. this.turnCache = {}; // Deals with unit/structure without base. this.noBase = undefined; this.baseManagers = []; }; PETRA.BasesManager.prototype.init = function(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"); this.noBase = new PETRA.BaseManager(gameState, this); this.noBase.init(gameState, PETRA.BaseManager.STATE_WITH_ANCHOR); this.noBase.accessIndex = 0; for (const cc of gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).values()) if (cc.foundationProgress() === undefined) this.createBase(gameState, cc, PETRA.BaseManager.STATE_WITH_ANCHOR); else this.createBase(gameState, cc, PETRA.BaseManager.STATE_UNCONSTRUCTED); }; /** * Initialization needed after deserialization (only called when deserialising). */ PETRA.BasesManager.prototype.postinit = function(gameState) { // Rebuild the base maps from the territory indices of each base. this.basesMap = new API3.Map(gameState.sharedScript, "territory"); for (const base of this.baseManagers) for (const j of base.territoryIndices) this.basesMap.map[j] = base.ID; for (const ent of gameState.getOwnEntities().values()) { if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) continue; // Entities which have been built or have changed ownership after the last AI turn have no base. // they will be dealt with in the next checkEvents const baseID = ent.getMetadata(PlayerID, "base"); if (baseID === undefined) continue; const base = this.getBaseByID(baseID); base.assignResourceToDropsite(gameState, ent); } }; /** * Create a new base in the baseManager: * If an existing one without anchor already exist, use it. * Otherwise create a new one. * TODO when buildings, criteria should depend on distance */ PETRA.BasesManager.prototype.createBase = function(gameState, ent, type = PETRA.BaseManager.STATE_WITH_ANCHOR) { const access = PETRA.getLandAccess(gameState, ent); let newbase; for (const base of this.baseManagers) { if (base.accessIndex != access) continue; if (type !== PETRA.BaseManager.STATE_ANCHORLESS && base.anchor) continue; if (type !== PETRA.BaseManager.STATE_ANCHORLESS) { // TODO we keep the first one, we should rather use the nearest if buildings // and possibly also cut on distance newbase = base; break; } else { // TODO here also test on distance instead of first if (newbase && !base.anchor) continue; newbase = base; if (newbase.anchor) break; } } if (this.Config.debug > 0) { API3.warn(" ----------------------------------------------------------"); API3.warn(" BasesManager createBase entrance avec access " + access + " and type " + type); API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); } if (!newbase) { newbase = new PETRA.BaseManager(gameState, this); newbase.init(gameState, type); this.baseManagers.push(newbase); } else newbase.reset(type); if (type !== PETRA.BaseManager.STATE_ANCHORLESS) newbase.setAnchor(gameState, ent); else newbase.setAnchorlessEntity(gameState, ent); return newbase; }; /** TODO check if the new anchorless bases should be added to addBase */ PETRA.BasesManager.prototype.checkEvents = function(gameState, events) { let addBase = false; for (const evt of events.Destroy) { // Let's check we haven't lost an important building here. if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && evt.metadata[PlayerID].base) { const ent = evt.entityObj; + if (evt?.metadata?.[PlayerID]?.assignedResource) + this.getBaseByID(evt.metadata[PlayerID].base).removeFromAssignedDropsite(ent); if (ent.owner() != PlayerID) continue; // A new base foundation was created and destroyed on the same (AI) turn if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) continue; const base = this.getBaseByID(evt.metadata[PlayerID].base); if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) base.removeDropsite(gameState, ent); if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) base.anchorLost(gameState, ent); } } for (const evt of events.EntityRenamed) { const ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) continue; const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (!base.anchorId || base.anchorId != evt.entity) continue; base.anchorId = evt.newentity; base.anchor = ent; } for (const evt of events.Create) { // Let's check if we have a valuable foundation needing builders quickly // (normal foundations are taken care in baseManager.assignToFoundations) const ent = gameState.getEntityById(evt.entity); if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) continue; if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc { // Okay so let's try to create a new base around this. const newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_UNCONSTRUCTED); // Let's get a few units from other bases there to build this. const builders = this.bulkPickWorkers(gameState, newbase, 10); if (builders !== false) { builders.forEach(worker => { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock { const newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_ANCHORLESS); // Let's get a few units from other bases there to build this. const builders = this.bulkPickWorkers(gameState, newbase, 4); if (builders != false) { builders.forEach(worker => { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } for (const evt of events.ConstructionFinished) { if (evt.newentity == evt.entity) // repaired building continue; const ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID) continue; if (ent.getMetadata(PlayerID, "base") === undefined) continue; const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes()) base.assignResourceToDropsite(gameState, ent); if (ent.getMetadata(PlayerID, "baseAnchor") === true) { if (base.constructing) base.constructing = false; addBase = true; } } for (const evt of events.OwnershipChanged) { if (evt.from == PlayerID) { const ent = gameState.getEntityById(evt.entity); if (!ent || ent.getMetadata(PlayerID, "base") === undefined) continue; const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) base.removeDropsite(gameState, ent); if (ent.getMetadata(PlayerID, "baseAnchor") === true) base.anchorLost(gameState, ent); } if (evt.to != PlayerID) continue; const ent = gameState.getEntityById(evt.entity); if (!ent) continue; if (ent.hasClass("Unit")) { PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent); continue; } if (ent.hasClass("CivCentre")) // build a new base around it { let newbase; if (ent.foundationProgress() !== undefined) newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_UNCONSTRUCTED); else { newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_CAPTURED); addBase = true; } newbase.assignEntity(gameState, ent); } else { let base; // If dropsite on new island, create a base around it if (!ent.decaying() && ent.resourceDropsiteTypes()) base = this.createBase(gameState, ent, PETRA.BaseManager.STATE_ANCHORLESS); else base = PETRA.getBestBase(gameState, ent) || this.noBase; base.assignEntity(gameState, ent); } } for (const evt of events.TrainingFinished) { for (const entId of evt.entities) { const ent = gameState.getEntityById(entId); if (!ent || !ent.isOwn(PlayerID)) continue; // Assign it immediately to something useful to do. if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_WORKER) { let base; if (ent.getMetadata(PlayerID, "base") === undefined) { base = PETRA.getBestBase(gameState, ent); base.assignEntity(gameState, ent); } else base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.reassignIdleWorkers(gameState, [ent]); base.workerObject.update(gameState, ent); } else if (ent.resourceSupplyType() && ent.position()) { const type = ent.resourceSupplyType(); if (!type.generic) continue; const dropsites = gameState.getOwnDropsites(type.generic); const pos = ent.position(); const access = PETRA.getLandAccess(gameState, ent); let distmin = Math.min(); let goal; for (const dropsite of dropsites.values()) { if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access) continue; const dist = API3.SquareVectorDistance(pos, dropsite.position()); if (dist > distmin) continue; distmin = dist; goal = dropsite.position(); } if (goal) ent.moveToRange(goal[0], goal[1]); } } } if (addBase) gameState.ai.HQ.handleNewBase(gameState); }; /** * returns an entity collection of workers through BaseManager.pickBuilders * TODO: when same accessIndex, sort by distance */ PETRA.BasesManager.prototype.bulkPickWorkers = function(gameState, baseRef, number) { const accessIndex = baseRef.accessIndex; if (!accessIndex) return false; const baseBest = this.baseManagers.slice(); // We can also use workers without a base. baseBest.push(this.noBase); baseBest.sort((a, b) => { if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) return -1; else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) return 1; return 0; }); let needed = number; const workers = new API3.EntityCollection(gameState.sharedScript); for (const base of baseBest) { if (base.ID == baseRef.ID) continue; base.pickBuilders(gameState, workers, needed); if (workers.length >= number) break; needed = number - workers.length; } if (!workers.length) return false; return workers; }; /** * @return {Object} - Resources (estimation) still gatherable in our territory. */ PETRA.BasesManager.prototype.getTotalResourceLevel = function(gameState, resources = Resources.GetCodes(), proximity = ["nearby", "medium"]) { const total = {}; for (const res of resources) total[res] = 0; for (const base of this.baseManagers) for (const res in total) total[res] += base.getResourceLevel(gameState, res, proximity); return total; }; /** * Returns the current gather rate * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. */ PETRA.BasesManager.prototype.GetCurrentGatherRates = function(gameState) { if (!this.turnCache.currentRates) { const currentRates = {}; for (const res of Resources.GetCodes()) currentRates[res] = 0.5 * this.GetTCResGatherer(res); this.addGatherRates(gameState, currentRates); for (const res of Resources.GetCodes()) currentRates[res] = Math.max(currentRates[res], 0); this.turnCache.currentRates = currentRates; } return this.turnCache.currentRates; }; /** Some functions that register that we assigned a gatherer to a resource this turn */ /** Add a gatherer to the turn cache for this supply. */ PETRA.BasesManager.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 from the turn cache for this supply. */ PETRA.BasesManager.prototype.RemoveTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) --this.turnCache.resourceGatherer[supplyID]; else { if (!this.turnCache.resourceGatherer) this.turnCache.resourceGatherer = {}; this.turnCache.resourceGatherer[supplyID] = -1; } }; PETRA.BasesManager.prototype.GetTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) return this.turnCache.resourceGatherer[supplyID]; return 0; }; /** The next two are to register that we assigned a gatherer to a resource this turn. */ PETRA.BasesManager.prototype.AddTCResGatherer = function(resource) { const check = "resourceGatherer-" + resource; if (this.turnCache[check]) ++this.turnCache[check]; else this.turnCache[check] = 1; if (this.turnCache.currentRates) this.turnCache.currentRates[resource] += 0.5; }; PETRA.BasesManager.prototype.GetTCResGatherer = function(resource) { const check = "resourceGatherer-" + resource; if (this.turnCache[check]) return this.turnCache[check]; return 0; }; /** * flag a resource as exhausted */ PETRA.BasesManager.prototype.isResourceExhausted = function(resource) { const check = "exhausted-" + resource; if (this.turnCache[check] == undefined) this.turnCache[check] = this.basesManager.isResourceExhausted(resource); return this.turnCache[check]; }; /** * returns the number of bases with a cc * ActiveBases includes only those with a built cc * PotentialBases includes also those with a cc in construction */ PETRA.BasesManager.prototype.numActiveBases = function() { if (!this.turnCache.base) this.updateBaseCache(); return this.turnCache.base.active; }; PETRA.BasesManager.prototype.hasActiveBase = function() { return !!this.numActiveBases(); }; PETRA.BasesManager.prototype.numPotentialBases = function() { if (!this.turnCache.base) this.updateBaseCache(); return this.turnCache.base.potential; }; PETRA.BasesManager.prototype.hasPotentialBase = function() { return !!this.numPotentialBases(); }; /** * Updates the number of active and potential bases. * .potential {number} - Bases that may or may not still be a foundation. * .active {number} - Usable bases. */ PETRA.BasesManager.prototype.updateBaseCache = function() { this.turnCache.base = { "active": 0, "potential": 0 }; for (const base of this.baseManagers) { if (!base.anchor) continue; ++this.turnCache.base.potential; if (base.anchor.foundationProgress() === undefined) ++this.turnCache.base.active; } }; PETRA.BasesManager.prototype.resetBaseCache = function() { this.turnCache.base = undefined; }; PETRA.BasesManager.prototype.baselessBase = function() { return this.noBase; }; /** * @param {number} baseID * @return {Object} - The base corresponding to baseID. */ PETRA.BasesManager.prototype.getBaseByID = function(baseID) { if (this.noBase.ID === baseID) return this.noBase; return this.baseManagers.find(base => base.ID === baseID); }; /** * flag a resource as exhausted */ PETRA.BasesManager.prototype.isResourceExhausted = function(resource) { return this.baseManagers.every(base => !base.dropsiteSupplies[resource].nearby.length && !base.dropsiteSupplies[resource].medium.length && !base.dropsiteSupplies[resource].faraway.length); }; /** * Count gatherers returning resources in the number of gatherers of resourceSupplies * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). */ PETRA.BasesManager.prototype.assignGatherers = function() { for (const base of this.baseManagers) for (const worker of base.workers.values()) { if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) continue; const orders = worker.unitAIOrderData(); if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) continue; this.AddTCGatherer(orders[1].target); } }; /** * Assign an entity to the closest base. * Used by the starting strategy. */ PETRA.BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex) { let bestbase; for (const base of this.baseManagers) { if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) && base.territoryIndices.indexOf(territoryIndex) == -1) continue; base.assignEntity(gameState, ent); bestbase = base; break; } if (!bestbase) // entity outside our territory { if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes()) bestbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_ANCHORLESS); else bestbase = PETRA.getBestBase(gameState, ent) || this.noBase; bestbase.assignEntity(gameState, ent); } // now assign entities garrisoned inside this entity if (ent.isGarrisonHolder() && ent.garrisoned().length) for (const id of ent.garrisoned()) bestbase.assignEntity(gameState, gameState.getEntityById(id)); // and find something useful to do if we already have a base if (ent.position() && bestbase.ID !== this.noBase.ID) { bestbase.assignRolelessUnits(gameState, [ent]); if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_WORKER) { bestbase.reassignIdleWorkers(gameState, [ent]); bestbase.workerObject.update(gameState, ent); } } }; /** * Adds the gather rates of individual bases to a shared object. * @param {Object} gameState * @param {Object} rates - The rates to add the gather rates to. */ PETRA.BasesManager.prototype.addGatherRates = function(gameState, rates) { for (const base of this.baseManagers) base.addGatherRates(gameState, rates); }; /** * @param {number} territoryIndex * @return {number} - The ID of the base at the given territory index. */ PETRA.BasesManager.prototype.baseAtIndex = function(territoryIndex) { return this.basesMap.map[territoryIndex]; }; /** * @param {number} territoryIndex */ PETRA.BasesManager.prototype.removeBaseFromTerritoryIndex = function(territoryIndex) { const baseID = this.basesMap.map[territoryIndex]; if (baseID == 0) return; const base = this.getBaseByID(baseID); if (base) { const index = base.territoryIndices.indexOf(territoryIndex); if (index != -1) base.territoryIndices.splice(index, 1); else API3.warn(" problem in headquarters::updateTerritories for base " + baseID); } else API3.warn(" problem in headquarters::updateTerritories without base " + baseID); this.basesMap.map[territoryIndex] = 0; }; /** * @return {boolean} - Whether the index was added to a base. */ PETRA.BasesManager.prototype.addTerritoryIndexToBase = function(gameState, territoryIndex, passabilityMap) { if (this.baseAtIndex(territoryIndex) != 0) return false; let landPassable = false; const ind = API3.getMapIndices(territoryIndex, gameState.ai.HQ.territoryMap, passabilityMap); let access; for (const k of ind) { if (!gameState.ai.HQ.landRegions[gameState.ai.accessibility.landPassMap[k]]) continue; landPassable = true; access = gameState.ai.accessibility.landPassMap[k]; break; } if (!landPassable) return false; let distmin = Math.min(); let baseID; const pos = [gameState.ai.HQ.territoryMap.cellSize * (territoryIndex % gameState.ai.HQ.territoryMap.width + 0.5), gameState.ai.HQ.territoryMap.cellSize * (Math.floor(territoryIndex / gameState.ai.HQ.territoryMap.width) + 0.5)]; for (const base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; const dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (!baseID) return false; this.getBaseByID(baseID).territoryIndices.push(territoryIndex); this.basesMap.map[territoryIndex] = baseID; return true; }; /** Reassign territories when a base is going to be deleted */ PETRA.BasesManager.prototype.reassignTerritories = function(deletedBase, territoryMap) { const cellSize = territoryMap.cellSize; const width = territoryMap.width; for (let j = 0; j < territoryMap.length; ++j) { if (this.basesMap.map[j] != deletedBase.ID) continue; if (territoryMap.getOwnerIndex(j) != PlayerID) { API3.warn("Petra reassignTerritories: should never happen"); this.basesMap.map[j] = 0; continue; } let distmin = Math.min(); let baseID; const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (const base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != deletedBase.accessIndex) continue; const dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (baseID) { this.getBaseByID(baseID).territoryIndices.push(j); this.basesMap.map[j] = baseID; } else this.basesMap.map[j] = 0; } }; /** * We will loop only on one active base per turn. */ PETRA.BasesManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("BasesManager update"); this.turnCache = {}; this.assignGatherers(); let nbBases = this.baseManagers.length; let activeBase = false; this.noBase.update(gameState, queues, events); while (!activeBase && nbBases != 0) { this.currentBase %= this.baseManagers.length; activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); --nbBases; // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); } Engine.ProfileStop(); }; PETRA.BasesManager.prototype.Serialize = function() { const properties = { "currentBase": this.currentBase }; const baseManagers = []; for (const base of this.baseManagers) baseManagers.push(base.Serialize()); return { "properties": properties, "noBase": this.noBase.Serialize(), "baseManagers": baseManagers }; }; PETRA.BasesManager.prototype.Deserialize = function(gameState, data) { for (const key in data.properties) this[key] = data.properties[key]; this.noBase = new PETRA.BaseManager(gameState, this); this.noBase.Deserialize(gameState, data.noBase); this.noBase.init(gameState, PETRA.BaseManager.STATE_WITH_ANCHOR); this.noBase.Deserialize(gameState, data.noBase); this.baseManagers = []; for (const basedata of data.baseManagers) { // The first call to deserialize set the ID base needed by entitycollections. const newbase = new PETRA.BaseManager(gameState, this); newbase.Deserialize(gameState, basedata); newbase.init(gameState, PETRA.BaseManager.STATE_WITH_ANCHOR); newbase.Deserialize(gameState, basedata); this.baseManagers.push(newbase); } };