Index: binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/attackManager.js +++ binaries/data/mods/public/simulation/ai/petra/attackManager.js @@ -342,7 +342,7 @@ (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12)) { if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || - !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack + !gameState.ai.HQ.numPotentialBases()) // if we have no base ... nothing else to do than attack { let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, type); Index: binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/attackPlan.js +++ binaries/data/mods/public/simulation/ai/petra/attackPlan.js @@ -32,7 +32,7 @@ let rallyPoint; let rallyAccess; let allAccesses = {}; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; @@ -816,7 +816,7 @@ let rallySame; let distminDiff = Math.min(); let rallyDiff; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { let anchor = base.anchor; if (!anchor || !anchor.position()) @@ -2001,7 +2001,7 @@ dist = API3.SquareVectorDistance(this.position, rallyPoint); } // Then check if we have a nearer base (in case this attack has captured one) - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; @@ -2083,7 +2083,7 @@ if (!gameState.isPlayerEnemy(ent.owner())) continue; let access = PETRA.getLandAccess(gameState, ent); - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; Index: binaries/data/mods/public/simulation/ai/petra/baseManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/baseManager.js +++ binaries/data/mods/public/simulation/ai/petra/baseManager.js @@ -10,10 +10,11 @@ * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) */ -PETRA.BaseManager = function(gameState, Config) +PETRA.BaseManager = function(gameState, basesManager) { - this.Config = Config; + 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; @@ -95,7 +96,7 @@ this.anchor = anchorEntity; this.anchorId = anchorEntity.id(); this.anchor.setMetadata(PlayerID, "baseAnchor", true); - gameState.ai.HQ.resetBaseCache(); + this.basesManager.resetBaseCache(); } anchorEntity.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(anchorEntity); @@ -109,7 +110,7 @@ this.anchor = undefined; this.anchorId = undefined; this.neededDefenders = 0; - gameState.ai.HQ.resetBaseCache(); + this.basesManager.resetBaseCache(); }; /** Set a building of an anchorless base */ @@ -148,7 +149,7 @@ let dropsiteId = dropsite.id(); this.dropsites[dropsiteId] = true; - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) + if (this.ID == this.basesManager.baselessBase().ID) accessIndex = PETRA.getLandAccess(gameState, dropsite); let maxDistResourceSquare = this.maxDistResourceSquare; @@ -357,27 +358,18 @@ return { "quality": bestVal, "pos": [x, z] }; }; -PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false) +PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, distances = ["nearby", "medium", "faraway"]) { let count = 0; let check = {}; - for (let supply of this.dropsiteSupplies[type].nearby) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - count += supply.ent.resourceSupplyAmount(); - } - if (nearbyOnly) - return count; - - for (let supply of this.dropsiteSupplies[type].medium) - { - if (check[supply.id]) - continue; - check[supply.id] = true; - count += 0.6*supply.ent.resourceSupplyAmount(); - } + 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; }; @@ -388,9 +380,12 @@ { if (type == "food") { + const prox = ["nearby"]; + if (gameState.currentPhase() > 1) + prox.push("medium"); if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms. { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted + 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(); @@ -420,7 +415,7 @@ if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted + 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 })); @@ -559,9 +554,9 @@ if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; // Ensure that the most wanted resource is not exhausted - if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type)) + if (moreNeed.type != "food" && this.basesManager.isResourceExhausted(moreNeed.type)) { - if (lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) + if (lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) continue; // And if so, move the gatherer to the less wanted one. @@ -573,7 +568,7 @@ // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1 // but we require a bit more to avoid too frequent changes if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 || - lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) + lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) { nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb); if (nb == 0) @@ -609,7 +604,7 @@ --num; ent.stopMoving(); ent.setMetadata(PlayerID, "gather-type", to); - gameState.ai.HQ.AddTCResGatherer(to); + this.basesManager.AddTCResGatherer(to); } return num; }; @@ -645,11 +640,11 @@ let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; - if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type)) + if (needed.type != "food" && this.basesManager.isResourceExhausted(needed.type)) continue; ent.setMetadata(PlayerID, "subrole", "gatherer"); ent.setMetadata(PlayerID, "gather-type", needed.type); - gameState.ai.HQ.AddTCResGatherer(needed.type); + this.basesManager.AddTCResGatherer(needed.type); break; } } @@ -749,7 +744,7 @@ if (workers.length < 3) { - let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2); + const fromOtherBase = this.basesManager.bulkPickWorkers(gameState, this, 2); if (fromOtherBase) { let baseID = this.ID; @@ -816,8 +811,7 @@ maxTotalBuilders = Math.max(maxTotalBuilders, 15); } - // if no base yet, everybody should build - if (gameState.ai.HQ.numActiveBases() == 0) + if (!this.basesManager.numActiveBases()) { targetNB = workers.length; maxTotalBuilders = targetNB; @@ -947,11 +941,11 @@ /** Return false when the base is not active (no workers on it) */ PETRA.BaseManager.prototype.update = function(gameState, queues, events) { - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units + if (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 (gameState.ai.HQ.numActiveBases() > 0) + if (this.basesManager.numActiveBases()) { for (let ent of this.units.values()) { @@ -965,7 +959,7 @@ if (!bestBase) { if (ent.hasClass("Dock")) - API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName()); + API3.warn("Petra: dock in 'noBase' baseManager. It may be useful to do an anchorless base for " + ent.templateName()); continue; } if (ent.resourceDropsiteTypes()) @@ -1056,7 +1050,7 @@ if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) continue; this.anchor.destroy(); - gameState.ai.HQ.resetBaseCache(); + this.basesManager.resetBaseCache(); break; } } @@ -1080,6 +1074,21 @@ 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 { Index: binaries/data/mods/public/simulation/ai/petra/basesManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/ai/petra/basesManager.js @@ -0,0 +1,785 @@ +/** + * 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); + this.noBase.accessIndex = 0; + + for (const cc of gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).values()) + if (cc.foundationProgress() === undefined) + this.createBase(gameState, cc); + else + this.createBase(gameState, cc, "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 + * allowedType: undefined => new base with an anchor + * "unconstructed" => new base with a foundation anchor + * "captured" => captured base with an anchor + * "anchorless" => anchorless base, currently with dock + */ +PETRA.BasesManager.prototype.createBase = function(gameState, ent, type) +{ + const access = PETRA.getLandAccess(gameState, ent); + let newbase; + for (const base of this.baseManagers) + { + if (base.accessIndex != access) + continue; + if (type != "anchorless" && base.anchor) + continue; + if (type != "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 != "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 (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, "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", "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, "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", "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, "unconstructed"); + else + { + newbase = this.createBase(gameState, ent, "captured"); + addBase = true; + } + newbase.assignEntity(gameState, ent); + } + else + { + let base; + // If dropsite on new island, create a base around it + if (!ent.decaying() && ent.resourceDropsiteTypes()) + base = this.createBase(gameState, ent, "anchorless"); + else + base = PETRA.getBestBase(gameState, ent) || this.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") == "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) +{ + if (this.turnCache["resourceGatherer-" + resource]) + ++this.turnCache["resourceGatherer-" + resource]; + else + this.turnCache["resourceGatherer-" + resource] = 1; + + if (this.turnCache.currentRates) + this.turnCache.currentRates[resource] += 0.5; +}; + +PETRA.BasesManager.prototype.GetTCResGatherer = function(resource) +{ + if (this.turnCache["resourceGatherer-" + resource]) + return this.turnCache["resourceGatherer-" + resource]; + + return 0; +}; + +/** + * flag a resource as exhausted + */ +PETRA.BasesManager.prototype.isResourceExhausted = function(resource) +{ + if (this.turnCache["exhausted-" + resource] == undefined) + this.turnCache["exhausted-" + resource] = this.basesManager.isResourceExhausted(resource); + + return this.turnCache["exhausted-" + resource]; +}; + +/** + * @return {Object} - The number of active and potential bases. + * return.potential {number} - Bases that may or may not still be a foundation. + * return.active {number} - Usable bases. + */ +PETRA.BasesManager.prototype.numberOfBases = function() +{ + const result = { "active": 0, "potential": 0 }; + for (const base of this.baseManagers) + { + if (!base.anchor) + continue; + ++result.potential; + if (base.anchor.foundationProgress() === undefined) + ++result.active; + } + return result; +}; + +/** + * 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.numPotentialBases = function() +{ + if (!this.turnCache.base) + this.updateBaseCache(); + return this.turnCache.base.potential; +}; + +PETRA.BasesManager.prototype.updateBaseCache = function() +{ + this.turnCache.base = this.numberOfBases(); +}; + +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, "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") === "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; + this.noBase.update(gameState, queues, events); + do + { + this.currentBase %= this.baseManagers.length; + activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); + --nbBases; + // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); + } + while (!activeBase && nbBases != 0); + + 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); + 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); + newbase.Deserialize(gameState, basedata); + this.baseManagers.push(newbase); + } +}; Index: binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/defenseManager.js +++ binaries/data/mods/public/simulation/ai/petra/defenseManager.js @@ -597,7 +597,7 @@ { let base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base")); if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) || - !base || gameState.ai.HQ.baseManagers.every(b => !b.anchor || b.accessIndex != base.accessIndex)) + !base || gameState.ai.HQ.baseManagers().every(b => !b.anchor || b.accessIndex != base.accessIndex)) { let capture = target.capturePoints(); if (!capture) Index: binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/entityExtend.js +++ binaries/data/mods/public/simulation/ai/petra/entityExtend.js @@ -254,13 +254,13 @@ PETRA.IsSupplyFull = function(gameState, ent) { return ent.isFull() === true || - ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers(); + ent.resourceSupplyNumGatherers() + gameState.ai.HQ.basesManager.GetTCGatherer(ent.id()) >= ent.maxGatherers(); }; /** * Get the best base (in terms of distance and accessIndex) for an entity. * It should be on the same accessIndex for structures. - * If nothing found, return the base[0] for units and undefined for structures. + * If nothing found, return the noBase for units and undefined for structures. * If exclude is given, we exclude the base with ID = exclude. */ PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false) @@ -274,7 +274,7 @@ { API3.warn("Petra error: entity without position, but not garrisoned"); PETRA.dumpEntity(ent); - return gameState.ai.HQ.baseManagers[0]; + return gameState.ai.HQ.basesManager.baselessBase(); } pos = holder.position(); accessIndex = PETRA.getLandAccess(gameState, holder); @@ -285,9 +285,9 @@ let distmin = Math.min(); let dist; let bestbase; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { - if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude) + if (base.ID == gameState.ai.HQ.basesManager.baselessBase().ID || exclude && base.ID == exclude) continue; if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined)) continue; @@ -319,7 +319,7 @@ bestbase = base; } if (!bestbase && !ent.hasClass("Structure")) - bestbase = gameState.ai.HQ.baseManagers[0]; + bestbase = gameState.ai.HQ.basesManager.baselessBase(); return bestbase; }; Index: binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -21,7 +21,6 @@ this.lastFailedGather = {}; this.firstBaseConfig = false; - this.currentBase = 0; // Only one base (from baseManager) is run every turn. // Workers configuration. this.targetNumWorkers = this.Config.Economy.targetNumWorkers; @@ -35,7 +34,7 @@ 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.basesManager = new PETRA.BasesManager(this.Config); this.attackManager = new PETRA.AttackManager(this.Config); this.buildManager = new PETRA.BuildManager(); this.defenseManager = new PETRA.DefenseManager(this.Config); @@ -54,8 +53,6 @@ PETRA.HQ.prototype.init = function(gameState, queues) { this.territoryMap = PETRA.createTerritoryMap(gameState); - // initialize base map. Each pixel is a base ID, or 0 if not or not accessible - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); // create borderMap: flag cells on the border of the map // then this map will be completed with our frontier in updateTerritories this.borderMap = PETRA.createBorderMap(gameState); @@ -76,93 +73,11 @@ */ PETRA.HQ.prototype.postinit = function(gameState) { - // Rebuild the base maps from the territory indices of each base - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - for (let base of this.baseManagers) - for (let j of base.territoryIndices) - this.basesMap.map[j] = base.ID; - - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) - continue; - // Entities which have been built or have changed ownership after the last AI turn have no base. - // they will be dealt with in the next checkEvents - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID === undefined) - continue; - let base = this.getBaseByID(baseID); - base.assignResourceToDropsite(gameState, ent); - } - + this.basesManager.postinit(gameState); this.updateTerritories(gameState); }; /** - * Create a new base in the baseManager: - * If an existing one without anchor already exist, use it. - * Otherwise create a new one. - * TODO when buildings, criteria should depend on distance - * allowedType: undefined => new base with an anchor - * "unconstructed" => new base with a foundation anchor - * "captured" => captured base with an anchor - * "anchorless" => anchorless base, currently with dock - */ -PETRA.HQ.prototype.createBase = function(gameState, ent, type) -{ - let access = PETRA.getLandAccess(gameState, ent); - let newbase; - for (let base of this.baseManagers) - { - if (base.accessIndex != access) - continue; - if (type != "anchorless" && base.anchor) - continue; - if (type != "anchorless") - { - // TODO we keep the fisrt one, we should rather use the nearest if buildings - // and possibly also cut on distance - newbase = base; - break; - } - else - { - // TODO here also test on distance instead of first - if (newbase && !base.anchor) - continue; - newbase = base; - if (newbase.anchor) - break; - } - } - - if (this.Config.debug > 0) - { - API3.warn(" ----------------------------------------------------------"); - API3.warn(" HQ createBase entrance avec access " + access + " and type " + type); - API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + - " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + - " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); - } - - if (!newbase) - { - newbase = new PETRA.BaseManager(gameState, this.Config); - newbase.init(gameState, type); - this.baseManagers.push(newbase); - } - else - newbase.reset(type); - - if (type != "anchorless") - newbase.setAnchor(gameState, ent); - else - newbase.setAnchorlessEntity(gameState, ent); - - return newbase; -}; - -/** * returns the sea index linking regions 1 and region 2 (supposed to be different land region) * otherwise return undefined * for the moment, only the case land-sea-land is supported @@ -182,11 +97,8 @@ return undefined; }; -/** TODO check if the new anchorless bases should be added to addBase */ PETRA.HQ.prototype.checkEvents = function(gameState, events) { - let addBase = false; - this.buildManager.checkEvents(gameState, events); if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) @@ -201,76 +113,7 @@ break; } - for (let evt of events.Destroy) - { - // Let's check we haven't lost an important building here. - if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && - evt.metadata[PlayerID].base) - { - let ent = evt.entityObj; - if (ent.owner() != PlayerID) - continue; - // A new base foundation was created and destroyed on the same (AI) turn - if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) - continue; - let base = this.getBaseByID(evt.metadata[PlayerID].base); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) - base.anchorLost(gameState, ent); - } - } - - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchorId || base.anchorId != evt.entity) - continue; - base.anchorId = evt.newentity; - base.anchor = ent; - } - - for (let evt of events.Create) - { - // Let's check if we have a valuable foundation needing builders quickly - // (normal foundations are taken care in baseManager.assignToFoundations) - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) - continue; - - if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc - { - // Okay so let's try to create a new base around this. - let newbase = this.createBase(gameState, ent, "unconstructed"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 10); - if (builders !== false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock - { - let newbase = this.createBase(gameState, ent, "anchorless"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 4); - if (builders != false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - } + this.basesManager.checkEvents(gameState, events); for (let evt of events.ConstructionFinished) { @@ -281,84 +124,17 @@ continue; if (ent.hasClass("Market") && this.maxFields) this.maxFields = false; - if (ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes()) - base.assignResourceToDropsite(gameState, ent); - - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - { - if (base.constructing) - base.constructing = false; - addBase = true; - } } for (let evt of events.OwnershipChanged) // capture events { - if (evt.from == PlayerID) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - base.anchorLost(gameState, ent); - } - if (evt.to != PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; - if (ent.hasClass("Unit")) + if (!ent.hasClass("Unit")) { - PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent); - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - if (ent.hasClass("Trader")) - { - ent.setMetadata(PlayerID, "role", "trader"); - ent.setMetadata(PlayerID, "route", undefined); - } - if (ent.hasClass("Worker")) - { - ent.setMetadata(PlayerID, "role", "worker"); - ent.setMetadata(PlayerID, "subrole", "idle"); - } - if (ent.hasClass("Ship")) - PETRA.setSeaAccess(gameState, ent); - if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) - ent.setMetadata(PlayerID, "plan", -1); - continue; - } - if (ent.hasClass("CivCentre")) // build a new base around it - { - let newbase; - if (ent.foundationProgress() !== undefined) - newbase = this.createBase(gameState, ent, "unconstructed"); - else - { - newbase = this.createBase(gameState, ent, "captured"); - addBase = true; - } - newbase.assignEntity(gameState, ent); - } - else - { - let base; - // If dropsite on new island, create a base around it - if (!ent.decaying() && ent.resourceDropsiteTypes()) - base = this.createBase(gameState, ent, "anchorless"); - else - base = PETRA.getBestBase(gameState, ent) || this.baseManagers[0]; - base.assignEntity(gameState, ent); if (ent.decaying()) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) @@ -366,7 +142,27 @@ if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } + continue; } + + ent.setMetadata(PlayerID, "role", undefined); + ent.setMetadata(PlayerID, "subrole", undefined); + ent.setMetadata(PlayerID, "plan", undefined); + ent.setMetadata(PlayerID, "PartOfArmy", undefined); + if (ent.hasClass("Trader")) + { + ent.setMetadata(PlayerID, "role", "trader"); + ent.setMetadata(PlayerID, "route", undefined); + } + if (ent.hasClass("Worker")) + { + ent.setMetadata(PlayerID, "role", "worker"); + ent.setMetadata(PlayerID, "subrole", "idle"); + } + if (ent.hasClass("Ship")) + PETRA.setSeaAccess(gameState, ent); + if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) + ent.setMetadata(PlayerID, "plan", -1); } // deal with the different rally points of training units: the rally point is set when the training starts @@ -417,43 +213,6 @@ if (!attack || attack.state != "unexecuted") ent.setMetadata(PlayerID, "plan", -1); } - // Assign it immediately to something useful to do - if (ent.getMetadata(PlayerID, "role") == "worker") - { - let base; - if (ent.getMetadata(PlayerID, "base") === undefined) - { - base = PETRA.getBestBase(gameState, ent); - base.assignEntity(gameState, ent); - } - else - base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.reassignIdleWorkers(gameState, [ent]); - base.workerObject.update(gameState, ent); - } - else if (ent.resourceSupplyType() && ent.position()) - { - let type = ent.resourceSupplyType(); - if (!type.generic) - continue; - let dropsites = gameState.getOwnDropsites(type.generic); - let pos = ent.position(); - let access = PETRA.getLandAccess(gameState, ent); - let distmin = Math.min(); - let goal; - for (let dropsite of dropsites.values()) - { - if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(pos, dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - goal = dropsite.position(); - } - if (goal) - ent.moveToRange(goal[0], goal[1]); - } } } @@ -473,22 +232,6 @@ this.garrisonManager.removeDecayingStructure(evt.entity); } - if (addBase) - { - if (!this.firstBaseConfig) - { - // This is our first base, let us configure our starting resources - this.configFirstBase(gameState); - } - else - { - // Let us hope this new base will fix our possible resource shortage - this.saveResources = undefined; - this.saveSpace = undefined; - this.maxFields = false; - } - } - // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) if (this.Config.difficulty < 2) return; @@ -529,6 +272,20 @@ } }; +PETRA.HQ.prototype.handleNewBase = function(gameState) +{ + if (!this.firstBaseConfig) + // This is our first base, let us configure our starting resources. + this.configFirstBase(gameState); + else + { + // Let us hope this new base will fix our possible resource shortage. + this.saveResources = undefined; + this.saveSpace = undefined; + this.maxFields = false; + } +}; + /** Ensure that all requirements are met when phasing up*/ PETRA.HQ.prototype.checkPhaseRequirements = function(gameState, queues) { @@ -803,44 +560,12 @@ */ PETRA.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) { - let accessIndex = baseRef.accessIndex; - if (!accessIndex) - return false; - // sorting bases by whether they are on the same accessindex or not. - let baseBest = this.baseManagers.slice().sort((a, b) => { - if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) - return -1; - else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) - return 1; - return 0; - }); - - let needed = number; - let workers = new API3.EntityCollection(gameState.sharedScript); - for (let base of baseBest) - { - if (base.ID == baseRef.ID) - continue; - base.pickBuilders(gameState, workers, needed); - if (workers.length >= number) - break; - needed = number - workers.length; - } - if (!workers.length) - return false; - return workers; + return this.basesManager.bulkPickWorkers(gameState, baseRef, number); }; -PETRA.HQ.prototype.getTotalResourceLevel = function(gameState) +PETRA.HQ.prototype.getTotalResourceLevel = function(gameState, resources, proximity) { - let total = {}; - for (let res of Resources.GetCodes()) - total[res] = 0; - for (let base of this.baseManagers) - for (let res in total) - total[res] += base.getResourceLevel(gameState, res); - - return total; + return this.basesManager.getTotalResourceLevel(gameState, resources, proximity); }; /** @@ -849,22 +574,7 @@ */ PETRA.HQ.prototype.GetCurrentGatherRates = function(gameState) { - if (!this.turnCache.currentRates) - { - let currentRates = {}; - for (let res of Resources.GetCodes()) - currentRates[res] = 0.5 * this.GetTCResGatherer(res); - - for (let base of this.baseManagers) - base.addGatherRates(gameState, currentRates); - - for (let res of Resources.GetCodes()) - currentRates[res] = Math.max(currentRates[res], 0); - - this.turnCache.currentRates = currentRates; - } - - return this.turnCache.currentRates; + return this.basesManager.GetCurrentGatherRates(gameState); }; /** @@ -1105,7 +815,7 @@ // 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) + for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex == indexIdx) continue; @@ -1248,7 +958,7 @@ // 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) + for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex == indexIdx) continue; @@ -1306,7 +1016,7 @@ // do not try on the narrow border of our territory if (this.borderMap.map[j] & PETRA.narrowFrontier_Mask) continue; - if (this.basesMap.map[j] == 0) // only in our territory + if (this.baseAtIndex(j) == 0) // only in our territory continue; // with enough room around to build the market let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); @@ -1376,7 +1086,7 @@ return false; } else - idx = this.basesMap.map[bestJdx]; + idx = this.baseAtIndex(bestJdx); let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; @@ -1451,7 +1161,7 @@ if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower) continue; } - if (this.basesMap.map[j] == 0) // inaccessible cell + if (this.baseAtIndex(j) == 0) // inaccessible cell continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); @@ -1518,7 +1228,7 @@ 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]]; + return [x, z, this.baseAtIndex(bestJdx)]; }; PETRA.HQ.prototype.buildTemple = function(gameState, queues) @@ -2073,7 +1783,7 @@ { let access = gameState.ai.accessibility.getAccessValue(pos); // check nearest base anchor - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; @@ -2237,23 +1947,7 @@ this.borderMap.map[j] &= ~PETRA.fullFrontier_Mask; // reset the frontier if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - // If this tile was already accounted, remove it - if (this.basesMap.map[j] == 0) - continue; - let base = this.getBaseByID(this.basesMap.map[j]); - if (base) - { - let index = base.territoryIndices.indexOf(j); - if (index != -1) - base.territoryIndices.splice(index, 1); - else - API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); - } - else - API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]); - this.basesMap.map[j] = 0; - } + this.basesManager.removeBaseFromTerritoryIndex(j); else { // Update the frontier @@ -2291,42 +1985,8 @@ if (onFrontier && !(this.borderMap.map[j] & PETRA.narrowFrontier_Mask)) this.borderMap.map[j] |= PETRA.largeFrontier_Mask; - // If this tile was not already accounted, add it. - if (this.basesMap.map[j] != 0) - continue; - let landPassable = false; - let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); - let access; - for (let k of ind) - { - if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) - continue; - landPassable = true; - access = gameState.ai.accessibility.landPassMap[k]; - break; - } - if (!landPassable) - continue; - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (!baseID) - continue; - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; - expansion++; + if (this.basesManager.addTerritoryIndexToBase(gameState, j, passabilityMap)) + expansion++; } } @@ -2340,57 +2000,12 @@ this.tradeManager.routeProspection = true; }; -/** Reassign territories when a base is going to be deleted */ -PETRA.HQ.prototype.reassignTerritories = function(deletedBase) -{ - let cellSize = this.territoryMap.cellSize; - let width = this.territoryMap.width; - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.basesMap.map[j] != deletedBase.ID) - continue; - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - API3.warn("Petra reassignTerritories: should never happen"); - this.basesMap.map[j] = 0; - continue; - } - - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != deletedBase.accessIndex) - continue; - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (baseID) - { - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; - } - else - this.basesMap.map[j] = 0; - } -}; - /** * returns the base corresponding to baseID */ PETRA.HQ.prototype.getBaseByID = function(baseID) { - for (let base of this.baseManagers) - if (base.ID == baseID) - return base; - - return undefined; + return this.basesManager.getBaseByID(baseID); }; /** @@ -2400,54 +2015,12 @@ */ PETRA.HQ.prototype.numActiveBases = function() { - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.active; + return this.basesManager.numActiveBases(); }; PETRA.HQ.prototype.numPotentialBases = function() { - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.potential; -}; - -PETRA.HQ.prototype.updateBaseCache = function() -{ - this.turnCache.base = { "active": 0, "potential": 0 }; - for (let base of this.baseManagers) - { - if (!base.anchor) - continue; - ++this.turnCache.base.potential; - if (base.anchor.foundationProgress() === undefined) - ++this.turnCache.base.active; - } -}; - -PETRA.HQ.prototype.resetBaseCache = function() -{ - this.turnCache.base = undefined; -}; - -/** - * Count gatherers returning resources in the number of gatherers of resourceSupplies - * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). - */ -PETRA.HQ.prototype.assignGatherers = function() -{ - for (let base of this.baseManagers) - { - for (let worker of base.workers.values()) - { - if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) - continue; - let orders = worker.unitAIOrderData(); - if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) - continue; - this.AddTCGatherer(orders[1].target); - } - } + return this.basesManager.numPotentialBases(); }; PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) @@ -2528,76 +2101,6 @@ this.capturableTargetsTime = gameState.ai.elapsedTime; }; -/** Some functions that register that we assigned a gatherer to a resource this turn */ - -/** add a gatherer to the turn cache for this supply. */ -PETRA.HQ.prototype.AddTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) - ++this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = 1; - } -}; - -/** remove a gatherer to the turn cache for this supply. */ -PETRA.HQ.prototype.RemoveTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - --this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = -1; - } -}; - -PETRA.HQ.prototype.GetTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - return this.turnCache.resourceGatherer[supplyID]; - - return 0; -}; - -/** The next two are to register that we assigned a gatherer to a resource this turn. */ -PETRA.HQ.prototype.AddTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - ++this.turnCache["resourceGatherer-" + resource]; - else - this.turnCache["resourceGatherer-" + resource] = 1; - - if (this.turnCache.currentRates) - this.turnCache.currentRates[resource] += 0.5; -}; - -PETRA.HQ.prototype.GetTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - return this.turnCache["resourceGatherer-" + resource]; - - return 0; -}; - -/** - * flag a resource as exhausted - */ -PETRA.HQ.prototype.isResourceExhausted = function(resource) -{ - if (this.turnCache["exhausted-" + resource] == undefined) - this.turnCache["exhausted-" + resource] = this.baseManagers.every(base => - !base.dropsiteSupplies[resource].nearby.length && - !base.dropsiteSupplies[resource].medium.length && - !base.dropsiteSupplies[resource].faraway.length); - - return this.turnCache["exhausted-" + resource]; -}; - /** * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) */ @@ -2656,6 +2159,20 @@ return this.turnCache.accountedWorkers; }; +PETRA.HQ.prototype.baseManagers = function() +{ + return this.basesManager.baseManagers; +}; + +/** + * @param {number} territoryIndex - The index to get the map for. + * @return {number} - The ID of the base at the given territory index. + */ +PETRA.HQ.prototype.baseAtIndex = function(territoryIndex) +{ + return this.basesManager.baseAtIndex(territoryIndex); +}; + /** * Some functions are run every turn * Others once in a while @@ -2750,17 +2267,7 @@ this.buildDefenses(gameState, queues); } - this.assignGatherers(); - let nbBases = this.baseManagers.length; - let activeBase; // We will loop only on 1 active base per turn - do - { - this.currentBase %= this.baseManagers.length; - activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); - --nbBases; - // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); - } - while (!activeBase && nbBases != 0); + this.basesManager.update(gameState, queues, events); this.navalManager.update(gameState, queues, events); @@ -2782,7 +2289,6 @@ { let properties = { "phasing": this.phasing, - "currentBase": this.currentBase, "lastFailedGather": this.lastFailedGather, "firstBaseConfig": this.firstBaseConfig, "supportRatio": this.supportRatio, @@ -2807,15 +2313,11 @@ "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(" baseManagers " + uneval(this.basesManager.Serialize())); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); @@ -2830,7 +2332,7 @@ return { "properties": properties, - "baseManagers": baseManagers, + "basesManager": this.basesManager.Serialize(), "attackManager": this.attackManager.Serialize(), "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), @@ -2848,16 +2350,10 @@ for (let key in data.properties) this[key] = data.properties[key]; - this.baseManagers = []; - for (let base of data.baseManagers) - { - // the first call to deserialize set the ID base needed by entitycollections - let newbase = new PETRA.BaseManager(gameState, this.Config); - newbase.Deserialize(gameState, base); - newbase.init(gameState); - newbase.Deserialize(gameState, base); - this.baseManagers.push(newbase); - } + + this.basesManager = new PETRA.BasesManager(this.Config); + this.basesManager.init(gameState); + this.basesManager.Deserialize(gameState, data.basesManager); this.navalManager = new PETRA.NavalManager(this.Config); this.navalManager.init(gameState, true); Index: binaries/data/mods/public/simulation/ai/petra/navalManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/navalManager.js +++ binaries/data/mods/public/simulation/ai/petra/navalManager.js @@ -708,7 +708,7 @@ PETRA.NavalManager.prototype.buildNavalStructures = function(gameState, queues) { - if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) + if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.numPotentialBases()) return; if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) @@ -718,7 +718,7 @@ gameState.ai.HQ.canBuild(gameState, "structures/{civ}/dock")) { let dockStarted = false; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (dockStarted) break; @@ -757,7 +757,7 @@ else return; let wantedLand = {}; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) if (base.anchor) wantedLand[base.accessIndex] = true; let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); Index: binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js +++ binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js @@ -197,13 +197,13 @@ { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] == base) + if (HQ.baseAtIndex(j) == base) placement.set(j, 45); } else { for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] != 0) + if (HQ.baseAtIndex(j) != 0) placement.set(j, 45); } @@ -266,7 +266,7 @@ let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { - if (HQ.basesMap.map[j] != base) + if (HQ.baseAtIndex(j) != base) placement.map[j] = 0; else if (placement.map[j] > 0) { @@ -286,7 +286,7 @@ { for (let j = 0; j < placement.map.length; ++j) { - if (HQ.basesMap.map[j] == 0) + if (HQ.baseAtIndex(j) == 0) placement.map[j] = 0; else if (placement.map[j] > 0) { @@ -299,7 +299,7 @@ let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; - else if (favoredBase && HQ.basesMap.map[j] == favoredBase) + else if (favoredBase && HQ.baseAtIndex(j) == favoredBase) placement.set(j, placement.map[j] + 100); } } @@ -346,7 +346,7 @@ let territorypos = placement.gamePosToMapPos([x, z]); let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; - return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] }; + return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.baseAtIndex(territoryIndex) }; }; /** @@ -524,7 +524,7 @@ 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]; + let baseIndex = gameState.ai.HQ.baseAtIndex(bestJdx); if (!baseIndex) baseIndex = -2; // We'll do an anchorless base around it Index: binaries/data/mods/public/simulation/ai/petra/startingStrategy.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/startingStrategy.js +++ binaries/data/mods/public/simulation/ai/petra/startingStrategy.js @@ -19,16 +19,7 @@ this.structureAnalysis(gameState); // Let's get our initial situation here. - let nobase = new PETRA.BaseManager(gameState, this.Config); - nobase.init(gameState); - nobase.accessIndex = 0; - this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - if (cc.foundationProgress() === undefined) - this.createBase(gameState, cc); - else - this.createBase(gameState, cc, "unconstructed"); + this.basesManager.init(gameState); this.updateTerritories(gameState); // Assign entities and resources in the different bases @@ -54,7 +45,7 @@ } // configure our first base strategy - if (this.baseManagers.length > 1) + if (this.numPotentialBases()) this.configFirstBase(gameState); }; @@ -94,41 +85,10 @@ for (let id of ent.garrisoned()) ent.unload(id); - let bestbase; let territorypos = this.territoryMap.gamePosToMapPos(pos); let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; - for (let i = 1; i < this.baseManagers.length; ++i) - { - let base = this.baseManagers[i]; - if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) && - base.territoryIndices.indexOf(territoryIndex) == -1) - continue; - base.assignEntity(gameState, ent); - bestbase = base; - break; - } - if (!bestbase) // entity outside our territory - { - if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes()) - bestbase = this.createBase(gameState, ent, "anchorless"); - else - bestbase = PETRA.getBestBase(gameState, ent) || this.baseManagers[0]; - bestbase.assignEntity(gameState, ent); - } - // now assign entities garrisoned inside this entity - if (ent.isGarrisonHolder() && ent.garrisoned().length) - for (let id of ent.garrisoned()) - bestbase.assignEntity(gameState, gameState.getEntityById(id)); - // and find something useful to do if we already have a base - if (pos && bestbase.ID !== this.baseManagers[0].ID) - { - bestbase.assignRolelessUnits(gameState, [ent]); - if (ent.getMetadata(PlayerID, "role") === "worker") - { - bestbase.reassignIdleWorkers(gameState, [ent]); - bestbase.workerObject.update(gameState, ent); - } - } + + this.basesManager.assignEntity(gameState, ent, territoryIndex); } }; @@ -434,7 +394,7 @@ */ PETRA.HQ.prototype.configFirstBase = function(gameState) { - if (this.baseManagers.length < 2) + if (!this.numPotentialBases()) return; this.firstBaseConfig = true; @@ -443,7 +403,7 @@ let startingLand = []; for (let region in this.landRegions) { - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex != +region) continue; @@ -477,20 +437,8 @@ // - count the available food resource, and react accordingly let startingFood = gameState.getResources().food; - let check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.food[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingFood += supply.ent.resourceSupplyAmount(); - } - } - } + startingFood += this.getTotalResourceLevel(gameState, ["food"], ["nearby", "medium", "faraway"]).food; + if (startingFood < 800) { if (startingSize < 25000) @@ -503,20 +451,8 @@ } // - 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(); - } - } - } + startingWood += this.getTotalResourceLevel(gameState, ["wood"], ["nearby", "medium", "faraway"]).wood; + if (this.Config.debug > 1) API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); if (startingWood < 6000) @@ -547,7 +483,7 @@ // immediatly build a wood dropsite if possible. if (!gameState.getOwnEntitiesByClass("DropsiteWood", true).hasEntities()) { - const newDP = this.baseManagers[1].findBestDropsiteAndLocation(gameState, "wood"); + const newDP = this.baseManagers()[0].findBestDropsiteAndLocation(gameState, "wood"); if (newDP.quality > 40 && this.canBuild(gameState, newDP.templateName)) { // if we start with enough workers, put our available resources in this first dropsite @@ -559,7 +495,7 @@ const cost = new API3.Resources(gameState.getTemplate(newDP.templateName).cost()); gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); } - gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.baseManagers[1].ID }, newDP.pos)); + gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.baseManagers()[0].ID }, newDP.pos)); } } // and build immediately a corral if needed @@ -567,6 +503,6 @@ { const template = gameState.applyCiv("structures/{civ}/corral"); if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) - gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); + gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers()[0].ID })); } }; Index: binaries/data/mods/public/simulation/ai/petra/transportPlan.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/transportPlan.js +++ binaries/data/mods/public/simulation/ai/petra/transportPlan.js @@ -253,7 +253,7 @@ let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); if (!base.anchor || !base.anchor.position()) { - for (let newbase of gameState.ai.HQ.baseManagers) + for (const newbase of gameState.ai.HQ.baseManagers()) { if (!newbase.anchor || !newbase.anchor.position()) continue; Index: binaries/data/mods/public/simulation/ai/petra/worker.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/worker.js +++ binaries/data/mods/public/simulation/ai/petra/worker.js @@ -66,8 +66,8 @@ } this.entAccess = PETRA.getLandAccess(gameState, ent); - // base 0 for unassigned entities has no accessIndex, so take the one from the entity - if (this.baseID == gameState.ai.HQ.baseManagers[0].ID) + // Base for unassigned entities has no accessIndex, so take the one from the entity. + if (this.baseID == gameState.ai.HQ.basesManager.baselessBase().ID) this.baseAccess = this.entAccess; else this.baseAccess = this.base.accessIndex; @@ -222,10 +222,10 @@ if (supply && !supply.hasClasses(["Field", "Animal"]) && supplyId != ent.getMetadata(PlayerID, "supply")) { - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supplyId); if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) { - gameState.ai.HQ.RemoveTCGatherer(supplyId); + this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else @@ -236,7 +236,7 @@ ent.setMetadata(PlayerID, "supply", supplyId); else if (nearby.length) { - gameState.ai.HQ.RemoveTCGatherer(supplyId); + this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else @@ -244,7 +244,7 @@ let medium = this.base.dropsiteSupplies[gatherType].medium; if (medium.length && !medium.some(sup => sup.id == supplyId)) { - gameState.ai.HQ.RemoveTCGatherer(supplyId); + this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else @@ -309,7 +309,7 @@ ent.setMetadata(PlayerID, "target-foundation", undefined); ent.setMetadata(PlayerID, "subrole", "idle"); ent.stopMoving(); - if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) + if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); @@ -330,7 +330,7 @@ { ent.setMetadata(PlayerID, "subrole", "idle"); ent.setMetadata(PlayerID, "target-foundation", undefined); - if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) + if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); @@ -357,7 +357,7 @@ { // nothing to hunt around. Try another region if any let nowhereToHunt = true; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; @@ -448,7 +448,8 @@ if (resource == "food" && this.startHunting(gameState)) return true; - let findSupply = function(ent, supplies) { + const findSupply = function(worker, supplies) { + const ent = worker.ent; let ret = false; let gatherRates = ent.resourceGatherRates(); for (let i = 0; i < supplies.length; ++i) @@ -468,7 +469,7 @@ if (!gatherRates[supplyType]) continue; // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id); + const nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + worker.base.GetTCGatherer(supplies[i].id); if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 && supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30) continue; @@ -476,7 +477,7 @@ 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); + worker.base.AddTCGatherer(supplies[i].id); ent.setMetadata(PlayerID, "supply", supplies[i].id); ret = supplies[i].ent; break; @@ -490,7 +491,7 @@ // first look in our own base if accessible from our present position if (this.baseAccess == this.entAccess) { - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby); + supply = findSupply(this, this.base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.gather(supply); @@ -512,7 +513,7 @@ return true; } } - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium); + supply = findSupply(this, this.base.dropsiteSupplies[resource].medium); if (supply) { this.ent.gather(supply); @@ -521,13 +522,13 @@ } // 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) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); + supply = findSupply(this, base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); @@ -537,7 +538,7 @@ } 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) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; @@ -559,13 +560,13 @@ } } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); + supply = findSupply(this, base.dropsiteSupplies[resource].medium); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); @@ -596,11 +597,11 @@ return true; // Still nothing ... try bases which need a transport - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); + supply = findSupply(this, base.dropsiteSupplies[resource].nearby); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) @@ -610,7 +611,7 @@ } 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) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; @@ -630,11 +631,11 @@ } } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); + supply = findSupply(this, base.dropsiteSupplies[resource].medium); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) @@ -680,20 +681,20 @@ { if (this.baseAccess == this.entAccess) { - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway); + supply = findSupply(this, this.base.dropsiteSupplies[resource].faraway); if (supply) { this.ent.gather(supply); return true; } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); + supply = findSupply(this, base.dropsiteSupplies[resource].faraway); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); @@ -701,11 +702,11 @@ return true; } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); + supply = findSupply(this, base.dropsiteSupplies[resource].faraway); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) @@ -781,7 +782,7 @@ if (PETRA.IsSupplyFull(gameState, supply)) continue; // Check if available resource is worth one additionnal gatherer (except for farms). - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) continue; @@ -826,7 +827,7 @@ { if (position) return true; - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); + this.base.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); @@ -893,7 +894,7 @@ if (PETRA.IsSupplyFull(gameState, supply)) return; // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) return; @@ -919,7 +920,7 @@ if (nearestSupply) { - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); + this.base.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); @@ -948,7 +949,7 @@ let diminishing = field.getDiminishingReturns(); if (diminishing < 1) { - let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id()); + const num = field.resourceSupplyNumGatherers() + this.base.GetTCGatherer(field.id()); if (num > 0) rate = Math.pow(diminishing, num); } @@ -961,7 +962,7 @@ if (!bestFarm || bestFarm.rate < 0.70 && gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities()) return false; - gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id()); + this.base.AddTCGatherer(bestFarm.ent.id()); this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); return bestFarm.ent; };