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.baseManagers()[1]) // if we have no base ... nothing else to do than attack { let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, type); 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 @@ -148,7 +148,7 @@ let dropsiteId = dropsite.id(); this.dropsites[dropsiteId] = true; - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) + if (this.ID == gameState.ai.HQ.baseManagers()[0].ID) accessIndex = PETRA.getLandAccess(gameState, dropsite); let maxDistResourceSquare = this.maxDistResourceSquare; @@ -357,27 +357,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 +379,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 +414,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 })); @@ -947,7 +941,7 @@ /** 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 == gameState.ai.HQ.baseManagers()[0].ID) // base for unaffected units { // if some active base, reassigns the workers/buildings // otherwise look for anything useful to do, i.e. treasures to gather Index: binaries/data/mods/public/simulation/ai/petra/basesManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/ai/petra/basesManager.js @@ -0,0 +1,583 @@ +/** + * Bases Manager + * Manages the list of available bases. + * Only one base is run every turn. + */ +PETRA.BasesManager = function(Config) +{ + this.Config = Config; + + this.currentBase = 0; + + // baseManagers[0] will deal with unit/structure without base. + 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"); + + // Create the "noBase" base. + const nobase = new PETRA.BaseManager(gameState, this.Config); + nobase.init(gameState); + nobase.accessIndex = 0; + this.baseManagers.push(nobase); + + const ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); + for (const cc of ccEnts.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.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; +}; + +/** 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; + 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 (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. + 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 + { + 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.baseManagers[0]; + 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; + // sorting bases by whether they are on the same accessindex or not. + const 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; + 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) +{ + const total = {}; + for (const res of Resources.GetCodes()) + total[res] = 0; + for (const base of this.baseManagers) + for (const res in total) + total[res] += base.getResourceLevel(gameState, res, ["nearby", "medium"]); + + return total; +}; + +/** + * @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; +}; + +/** + * @param {number} baseID + * @return {Object} - The base corresponding to baseID. + */ +PETRA.BasesManager.prototype.getBaseByID = function(baseID) +{ + 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); +}; + +/** + * Assign an entity to the closest base. + * Used by the starting strategy. + */ +PETRA.BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex) +{ + let bestbase; + for (let i = 1; i < this.baseManagers.length; ++i) + { + const 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 (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.baseManagers[0].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; +}; + +/** + * We will loop only on one active base per turn. + */ +PETRA.BasesManager.prototype.update = function(gameState, queues, events) +{ + let nbBases = this.baseManagers.length; + let activeBase; + do + { + this.currentBase %= this.baseManagers.length; + activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); + --nbBases; + // TODO what to do with HQ.reassignTerritories(this.baseManagers[this.currentBase]); + } + while (!activeBase && nbBases != 0); +}; + +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, + "baseManagers": baseManagers + }; +}; + +PETRA.BasesManager.prototype.Deserialize = function(gameState, data) +{ + for (const key in data.properties) + this[key] = data.properties[key]; + + 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.Config); + 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 @@ -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.baseManagers()[0]; } 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.baseManagers()[0].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.baseManagers()[0]; 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,22 @@ } }; +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) { @@ -804,44 +563,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) { - 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); }; /** @@ -856,8 +583,7 @@ for (let res of Resources.GetCodes()) currentRates[res] = 0.5 * this.GetTCResGatherer(res); - for (let base of this.baseManagers) - base.addGatherRates(gameState, currentRates); + this.basesManager.addGatherRates(gameState, currentRates); for (let res of Resources.GetCodes()) currentRates[res] = Math.max(currentRates[res], 0); @@ -1106,7 +832,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; @@ -1249,7 +975,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; @@ -1307,7 +1033,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.basesManager.basesMap.map[j] == 0) // only in our territory continue; // with enough room around to build the market let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); @@ -1377,7 +1103,7 @@ return false; } else - idx = this.basesMap.map[bestJdx]; + idx = this.basesManager.basesMap.map[bestJdx]; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; @@ -1452,7 +1178,7 @@ if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower) continue; } - if (this.basesMap.map[j] == 0) // inaccessible cell + if (this.basesManager.basesMap.map[j] == 0) // inaccessible cell continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); @@ -1519,7 +1245,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.basesManager.basesMap.map[bestJdx]]; }; PETRA.HQ.prototype.buildTemple = function(gameState, queues) @@ -2074,7 +1800,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; @@ -2238,23 +1964,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 @@ -2292,42 +2002,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++; } } @@ -2348,19 +2024,19 @@ let width = this.territoryMap.width; for (let j = 0; j < this.territoryMap.length; ++j) { - if (this.basesMap.map[j] != deletedBase.ID) + if (this.basesManager.basesMap.map[j] != deletedBase.ID) continue; if (this.territoryMap.getOwnerIndex(j) != PlayerID) { API3.warn("Petra reassignTerritories: should never happen"); - this.basesMap.map[j] = 0; + this.basesManager.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) + for (const base of this.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; @@ -2375,10 +2051,10 @@ if (baseID) { this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; + this.basesManager.basesMap.map[j] = baseID; } else - this.basesMap.map[j] = 0; + this.basesManager.basesMap.map[j] = 0; } }; @@ -2387,11 +2063,7 @@ */ 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); }; /** @@ -2415,15 +2087,7 @@ 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; - } + this.turnCache.base = this.basesManager.numberOfBases(); }; PETRA.HQ.prototype.resetBaseCache = function() @@ -2437,9 +2101,9 @@ */ PETRA.HQ.prototype.assignGatherers = function() { - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { - for (let worker of base.workers.values()) + for (const worker of base.workers.values()) { if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) continue; @@ -2591,10 +2255,7 @@ 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); + this.turnCache["exhausted-" + resource] = this.basesManager.isResourceExhausted(resource); return this.turnCache["exhausted-" + resource]; }; @@ -2657,6 +2318,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} + */ +PETRA.HQ.prototype.baseAtIndex = function(territoryIndex) +{ + return this.basesManager.baseAtIndex(territoryIndex); +}; + /** * Some functions are run every turn * Others once in a while @@ -2752,16 +2427,7 @@ } 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); @@ -2783,7 +2449,6 @@ { let properties = { "phasing": this.phasing, - "currentBase": this.currentBase, "lastFailedGather": this.lastFailedGather, "firstBaseConfig": this.firstBaseConfig, "supportRatio": this.supportRatio, @@ -2808,15 +2473,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())); @@ -2831,7 +2492,7 @@ return { "properties": properties, - "baseManagers": baseManagers, + "basesManager": this.basesManager.Serialize(), "attackManager": this.attackManager.Serialize(), "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), @@ -2849,16 +2510,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.baseManagers()[1]) 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 @@ -9,6 +9,7 @@ if (!this.regionAnalysis(gameState)) return; + this.basesManager.init(gameState); this.attackManager.init(gameState); this.buildManager.init(gameState); this.navalManager.init(gameState); @@ -19,16 +20,6 @@ this.structureAnalysis(gameState); // Let's get our initial situation here. - let nobase = new PETRA.BaseManager(gameState, this.Config); - nobase.init(gameState); - nobase.accessIndex = 0; - this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - if (cc.foundationProgress() === undefined) - this.createBase(gameState, cc); - else - this.createBase(gameState, cc, "unconstructed"); this.updateTerritories(gameState); // Assign entities and resources in the different bases @@ -54,7 +45,7 @@ } // configure our first base strategy - if (this.baseManagers.length > 1) + if (this.baseManagers().length > 1) 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.baseManagers().length < 2) 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; @@ -480,7 +440,7 @@ let check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { for (let supply of base.dropsiteSupplies.food[proxim]) { @@ -506,7 +466,7 @@ check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { for (let supply of base.dropsiteSupplies.wood[proxim]) { @@ -547,7 +507,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()[1].findBestDropsiteAndLocation(gameState, "wood"); if (newDP.quality > 40 && this.canBuild(gameState, newDP.templateName)) { // if we start with enough workers, put our available resources in this first dropsite @@ -559,7 +519,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()[1].ID }, newDP.pos)); } } // and build immediately a corral if needed @@ -567,6 +527,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()[1].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 @@ -67,7 +67,7 @@ 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) + if (this.baseID == gameState.ai.HQ.baseManagers()[0].ID) this.baseAccess = this.entAccess; else this.baseAccess = this.base.accessIndex; @@ -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.baseManagers()[0].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.baseManagers()[0].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; @@ -521,7 +521,7 @@ } // 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; @@ -537,7 +537,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,7 +559,7 @@ } } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; @@ -596,7 +596,7 @@ 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; @@ -610,7 +610,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,7 +630,7 @@ } } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; @@ -687,7 +687,7 @@ return true; } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; @@ -701,7 +701,7 @@ return true; } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue;