1
0
forked from 0ad/0ad

[PetraAI] - Manage bases in a separate BasesManager.

The HQ should only care about high-level stuff, hence something like
managing/looping individual bases is now done in a `BasesManager`,
similar to the `AttackManager`.

Differential revision: https://code.wildfiregames.com/D4192
Comments by: @Angen
Fixes: #6185

This was SVN commit r25876.
This commit is contained in:
Freagarach 2021-08-29 06:47:05 +00:00
parent 2c4427b488
commit 4e664dd712
13 changed files with 1002 additions and 758 deletions

View File

@ -342,7 +342,7 @@ PETRA.AttackManager.prototype.update = function(gameState, queues, events)
(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.hasPotentialBase()) // 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);

View File

@ -32,7 +32,7 @@ PETRA.AttackPlan = function(gameState, Config, uniqueID, type, data)
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 @@ PETRA.AttackPlan.prototype.chooseTarget = function(gameState)
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 @@ PETRA.AttackPlan.prototype.Abort = function(gameState)
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 @@ PETRA.AttackPlan.prototype.checkEvents = function(gameState, events)
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;

View File

@ -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 @@ PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
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 @@ PETRA.BaseManager.prototype.anchorLost = function(gameState, ent)
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 @@ PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, drops
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 @@ PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resou
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 @@ PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues)
{
if (type == "food")
{
const prox = ["nearby"];
if (gameState.currentPhase() < 2)
prox.push("medium");
if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms.
{
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 @@ PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues)
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 @@ PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
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 @@ PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
// 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 @@ PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, numbe
--num;
ent.stopMoving();
ent.setMetadata(PlayerID, "gather-type", to);
gameState.ai.HQ.AddTCResGatherer(to);
this.basesManager.AddTCResGatherer(to);
}
return num;
};
@ -645,11 +640,11 @@ PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorker
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 @@ PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
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 @@ PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
maxTotalBuilders = Math.max(maxTotalBuilders, 15);
}
// if no base yet, everybody should build
if (gameState.ai.HQ.numActiveBases() == 0)
if (!this.basesManager.hasActiveBase())
{
targetNB = workers.length;
maxTotalBuilders = targetNB;
@ -947,11 +941,11 @@ PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
/** 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.hasActiveBase())
{
for (let ent of this.units.values())
{
@ -965,7 +959,7 @@ PETRA.BaseManager.prototype.update = function(gameState, queues, events)
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 @@ PETRA.BaseManager.prototype.update = function(gameState, queues, events)
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 @@ PETRA.BaseManager.prototype.update = function(gameState, queues, events)
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 {

View File

@ -0,0 +1,792 @@
/**
* 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)
{
const check = "resourceGatherer-" + resource;
if (this.turnCache[check])
++this.turnCache[check];
else
this.turnCache[check] = 1;
if (this.turnCache.currentRates)
this.turnCache.currentRates[resource] += 0.5;
};
PETRA.BasesManager.prototype.GetTCResGatherer = function(resource)
{
const check = "resourceGatherer-" + resource;
if (this.turnCache[check])
return this.turnCache[check];
return 0;
};
/**
* flag a resource as exhausted
*/
PETRA.BasesManager.prototype.isResourceExhausted = function(resource)
{
const check = "exhausted-" + resource;
if (this.turnCache[check] == undefined)
this.turnCache[check] = this.basesManager.isResourceExhausted(resource);
return this.turnCache[check];
};
/**
* returns the number of bases with a cc
* ActiveBases includes only those with a built cc
* PotentialBases includes also those with a cc in construction
*/
PETRA.BasesManager.prototype.numActiveBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.active;
};
PETRA.BasesManager.prototype.hasActiveBase = function()
{
return !!this.numActiveBases();
};
PETRA.BasesManager.prototype.numPotentialBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.potential;
};
PETRA.BasesManager.prototype.hasPotentialBase = function()
{
return !!this.numPotentialBases();
};
/**
* Updates the number of active and potential bases.
* .potential {number} - Bases that may or may not still be a foundation.
* .active {number} - Usable bases.
*/
PETRA.BasesManager.prototype.updateBaseCache = function()
{
this.turnCache.base = { "active": 0, "potential": 0 };
for (const base of this.baseManagers)
{
if (!base.anchor)
continue;
++this.turnCache.base.potential;
if (base.anchor.foundationProgress() === undefined)
++this.turnCache.base.active;
}
};
PETRA.BasesManager.prototype.resetBaseCache = function()
{
this.turnCache.base = undefined;
};
PETRA.BasesManager.prototype.baselessBase = function()
{
return this.noBase;
};
/**
* @param {number} baseID
* @return {Object} - The base corresponding to baseID.
*/
PETRA.BasesManager.prototype.getBaseByID = function(baseID)
{
if (this.noBase.ID === baseID)
return this.noBase;
return this.baseManagers.find(base => base.ID === baseID);
};
/**
* flag a resource as exhausted
*/
PETRA.BasesManager.prototype.isResourceExhausted = function(resource)
{
return this.baseManagers.every(base =>
!base.dropsiteSupplies[resource].nearby.length &&
!base.dropsiteSupplies[resource].medium.length &&
!base.dropsiteSupplies[resource].faraway.length);
};
/**
* Count gatherers returning resources in the number of gatherers of resourceSupplies
* to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps).
*/
PETRA.BasesManager.prototype.assignGatherers = function()
{
for (const base of this.baseManagers)
for (const worker of base.workers.values())
{
if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1)
continue;
const orders = worker.unitAIOrderData();
if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply"))
continue;
this.AddTCGatherer(orders[1].target);
}
};
/**
* Assign an entity to the closest base.
* Used by the starting strategy.
*/
PETRA.BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex)
{
let bestbase;
for (const base of this.baseManagers)
{
if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) &&
base.territoryIndices.indexOf(territoryIndex) == -1)
continue;
base.assignEntity(gameState, ent);
bestbase = base;
break;
}
if (!bestbase) // entity outside our territory
{
if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes())
bestbase = this.createBase(gameState, ent, "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);
}
};

View File

@ -250,7 +250,7 @@ PETRA.DefenseManager.prototype.checkEnemyUnits = function(gameState)
this.makeIntoArmy(gameState, ent.id());
}
if (i != 0 || this.armies.length > 1 || gameState.ai.HQ.numActiveBases() == 0)
if (i != 0 || this.armies.length > 1 || !gameState.ai.HQ.hasActiveBase())
return;
// Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay)
// and attack it only if useful (and capturable) or dangereous.
@ -588,7 +588,7 @@ PETRA.DefenseManager.prototype.checkEvents = function(gameState, events)
{
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)

View File

@ -254,13 +254,13 @@ PETRA.returnResources = function(gameState, ent)
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 @@ PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclud
{
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 @@ PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclud
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 @@ PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclud
bestbase = base;
}
if (!bestbase && !ent.hasClass("Structure"))
bestbase = gameState.ai.HQ.baseManagers[0];
bestbase = gameState.ai.HQ.basesManager.baselessBase();
return bestbase;
};

View File

@ -21,7 +21,6 @@ PETRA.HQ = function(Config)
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 @@ PETRA.HQ = function(Config)
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 = function(Config)
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,92 +73,10 @@ PETRA.HQ.prototype.init = function(gameState, queues)
*/
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
@ -182,11 +97,8 @@ PETRA.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2)
return undefined;
};
/** TODO check if the new anchorless bases should be added to addBase */
PETRA.HQ.prototype.checkEvents = function(gameState, events)
{
let addBase = false;
this.buildManager.checkEvents(gameState, events);
if (events.TerritoriesChanged.length || events.DiplomacyChanged.length)
@ -201,76 +113,7 @@ PETRA.HQ.prototype.checkEvents = function(gameState, events)
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 @@ PETRA.HQ.prototype.checkEvents = function(gameState, events)
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 @@ PETRA.HQ.prototype.checkEvents = function(gameState, events)
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 @@ PETRA.HQ.prototype.checkEvents = function(gameState, events)
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 @@ PETRA.HQ.prototype.checkEvents = function(gameState, events)
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.checkEvents = function(gameState, events)
}
};
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.findBestTrainableUnit = function(gameState, classes, requirem
*/
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.getTotalResourceLevel = function(gameState)
*/
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 @@ PETRA.HQ.prototype.findEconomicCCLocation = function(gameState, template, resour
// 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 @@ PETRA.HQ.prototype.findStrategicCCLocation = function(gameState, template)
// 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 @@ PETRA.HQ.prototype.findMarketLocation = function(gameState, template)
// 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 @@ PETRA.HQ.prototype.findMarketLocation = function(gameState, template)
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 @@ PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template)
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 @@ PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template)
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)
@ -1770,7 +1480,7 @@ PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues)
if (queues.civilCentre.hasQueuedUnits())
return;
// First build one cc if all have been destroyed
if (this.numPotentialBases() == 0)
if (!this.hasPotentialBase())
{
this.buildFirstBase(gameState);
return;
@ -1800,7 +1510,7 @@ PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues)
PETRA.HQ.prototype.buildNewBase = function(gameState, queues, resource)
{
if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))
if (this.hasPotentialBase() && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))
return false;
if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits())
return false;
@ -2073,7 +1783,7 @@ PETRA.HQ.prototype.trainEmergencyUnits = function(gameState, positions)
{
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;
@ -2195,7 +1905,7 @@ PETRA.HQ.prototype.canBuild = function(gameState, structure)
return false;
}
if (this.numActiveBases() < 1)
if (!this.hasActiveBase())
{
// if no base, check that we can build outside our territory
let buildTerritories = template.buildTerritories();
@ -2237,23 +1947,7 @@ PETRA.HQ.prototype.updateTerritories = function(gameState)
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 @@ PETRA.HQ.prototype.updateTerritories = function(gameState)
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 @@ PETRA.HQ.prototype.updateTerritories = function(gameState)
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,22 @@ PETRA.HQ.prototype.getBaseByID = function(baseID)
*/
PETRA.HQ.prototype.numActiveBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.active;
return this.basesManager.numActiveBases();
};
PETRA.HQ.prototype.hasActiveBase = function()
{
return this.basesManager.hasActiveBase();
};
PETRA.HQ.prototype.numPotentialBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.potential;
return this.basesManager.numPotentialBases();
};
PETRA.HQ.prototype.updateBaseCache = function()
PETRA.HQ.prototype.hasPotentialBase = 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.hasPotentialBase();
};
PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius)
@ -2528,76 +2111,6 @@ PETRA.HQ.prototype.updateCaptureStrength = function(gameState)
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 +2169,20 @@ PETRA.HQ.prototype.getAccountedWorkers = function(gameState)
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
@ -2700,7 +2227,7 @@ PETRA.HQ.prototype.update = function(gameState, queues, events)
else
this.researchManager.checkPhase(gameState, queues);
if (this.numActiveBases() > 0)
if (this.hasActiveBase())
{
if (gameState.ai.playedTurn % 4 == 0)
this.trainMoreWorkers(gameState, queues);
@ -2718,7 +2245,7 @@ PETRA.HQ.prototype.update = function(gameState, queues, events)
this.researchManager.update(gameState, queues);
}
if (this.numPotentialBases() < 1 ||
if (!this.hasPotentialBase() ||
this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1)
this.checkBaseExpansion(gameState, queues);
@ -2750,21 +2277,11 @@ PETRA.HQ.prototype.update = function(gameState, queues, events)
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);
if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits))
if (this.Config.difficulty > 0 && (this.hasActiveBase() || !this.canBuildUnits))
this.attackManager.update(gameState, queues, events);
this.diplomacyManager.update(gameState, events);
@ -2782,7 +2299,6 @@ PETRA.HQ.prototype.Serialize = function()
{
let properties = {
"phasing": this.phasing,
"currentBase": this.currentBase,
"lastFailedGather": this.lastFailedGather,
"firstBaseConfig": this.firstBaseConfig,
"supportRatio": this.supportRatio,
@ -2807,15 +2323,11 @@ PETRA.HQ.prototype.Serialize = function()
"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 +2342,7 @@ PETRA.HQ.prototype.Serialize = function()
return {
"properties": properties,
"baseManagers": baseManagers,
"basesManager": this.basesManager.Serialize(),
"attackManager": this.attackManager.Serialize(),
"buildManager": this.buildManager.Serialize(),
"defenseManager": this.defenseManager.Serialize(),
@ -2848,16 +2360,10 @@ PETRA.HQ.prototype.Deserialize = function(gameState, data)
for (let key in data.properties)
this[key] = data.properties[key];
this.baseManagers = [];
for (let base of data.baseManagers)
{
// the first call to deserialize set the ID base needed by entitycollections
let newbase = new PETRA.BaseManager(gameState, this.Config);
newbase.Deserialize(gameState, base);
newbase.init(gameState);
newbase.Deserialize(gameState, base);
this.baseManagers.push(newbase);
}
this.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);

View File

@ -708,7 +708,7 @@ PETRA.NavalManager.prototype.moveApart = function(gameState)
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.hasPotentialBase())
return;
if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock)
@ -718,7 +718,7 @@ PETRA.NavalManager.prototype.buildNavalStructures = function(gameState, queues)
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 @@ PETRA.NavalManager.prototype.buildNavalStructures = function(gameState, queues)
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");

View File

@ -419,7 +419,7 @@ PETRA.QueueManager.prototype.checkPausedQueues = function(gameState)
for (let q in this.queues)
{
let toBePaused = false;
if (gameState.ai.HQ.numPotentialBases() == 0)
if (!gameState.ai.HQ.hasPotentialBase())
toBePaused = q != "dock" && q != "civilCentre";
else if (numWorkers < workersMin / 3)
toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency";

View File

@ -197,13 +197,13 @@ PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
{
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 @@ PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
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 @@ PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
{
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 @@ PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
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 @@ PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
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 @@ PETRA.ConstructionPlan.prototype.findDockPosition = function(gameState)
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

View File

@ -19,16 +19,7 @@ PETRA.HQ.prototype.gameAnalysis = function(gameState)
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 @@ PETRA.HQ.prototype.gameAnalysis = function(gameState)
}
// configure our first base strategy
if (this.baseManagers.length > 1)
if (!this.hasPotentialBase())
this.configFirstBase(gameState);
};
@ -94,41 +85,10 @@ PETRA.HQ.prototype.assignStartingEntities = function(gameState)
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.dispatchUnits = function(gameState)
*/
PETRA.HQ.prototype.configFirstBase = function(gameState)
{
if (this.baseManagers.length < 2)
if (!this.hasPotentialBase())
return;
this.firstBaseConfig = true;
@ -443,7 +403,7 @@ PETRA.HQ.prototype.configFirstBase = function(gameState)
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 @@ PETRA.HQ.prototype.configFirstBase = function(gameState)
// - 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 @@ PETRA.HQ.prototype.configFirstBase = function(gameState)
}
// - 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 @@ PETRA.HQ.prototype.configFirstBase = function(gameState)
// 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 @@ PETRA.HQ.prototype.configFirstBase = function(gameState)
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 @@ PETRA.HQ.prototype.configFirstBase = function(gameState)
{
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 }));
}
};

View File

@ -253,7 +253,7 @@ PETRA.TransportPlan.prototype.cancelTransport = function(gameState)
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;

View File

@ -66,8 +66,8 @@ PETRA.Worker.prototype.update = function(gameState, ent)
}
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 @@ PETRA.Worker.prototype.update = function(gameState, ent)
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 @@ PETRA.Worker.prototype.update = function(gameState, ent)
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 @@ PETRA.Worker.prototype.update = function(gameState, ent)
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 @@ PETRA.Worker.prototype.update = function(gameState, ent)
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 @@ PETRA.Worker.prototype.update = function(gameState, ent)
{
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 @@ PETRA.Worker.prototype.update = function(gameState, ent)
{
// 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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
// 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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
}
// 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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
}
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
}
}
}
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
}
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
}
}
}
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
{
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 @@ PETRA.Worker.prototype.startGathering = function(gameState)
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 @@ PETRA.Worker.prototype.startHunting = function(gameState, position)
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 @@ PETRA.Worker.prototype.startHunting = function(gameState, position)
{
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 @@ PETRA.Worker.prototype.startFishing = function(gameState)
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 @@ PETRA.Worker.prototype.startFishing = function(gameState)
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 @@ PETRA.Worker.prototype.gatherNearestField = function(gameState, baseID)
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 @@ PETRA.Worker.prototype.gatherNearestField = function(gameState, baseID)
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;
};