forked from 0ad/0ad
975 lines
36 KiB
JavaScript
975 lines
36 KiB
JavaScript
var AEGIS = function(m)
|
|
{
|
|
/* Base Manager
|
|
* Handles lower level economic stuffs.
|
|
* Some tasks:
|
|
-tasking workers: gathering/hunting/building/repairing?/scouting/plans.
|
|
-giving feedback/estimates on GR
|
|
-achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans, if I ever get any.
|
|
-getting good spots for dropsites
|
|
-managing dropsite use in the base
|
|
> warning HQ if we'll need more space
|
|
-updating whatever needs updating, keeping track of stuffs (rebuilding needs…)
|
|
*/
|
|
|
|
m.BaseManager = function(Config) {
|
|
this.Config = Config;
|
|
this.farmingFields = false;
|
|
this.ID = m.playerGlobals[PlayerID].uniqueIDBases++;
|
|
|
|
// anchor building: seen as the main building of the base. Needs to have territorial influence
|
|
this.anchor = undefined;
|
|
// list of IDs of buildings in our base that have a "territory pusher" function.
|
|
this.territoryBuildings = [];
|
|
|
|
// will tell if we should be considered as a source of X.
|
|
this.willGather = { "food": 0, "wood": 0, "stone":0, "metal": 0 };
|
|
|
|
this.isFarming = false;
|
|
this.isHunting = true;
|
|
|
|
this.constructing = false;
|
|
|
|
// vector for iterating, to check one use the HQ map.
|
|
this.territoryIndices = [];
|
|
};
|
|
|
|
m.BaseManager.prototype.init = function(gameState, events, unconstructed){
|
|
this.constructing = unconstructed;
|
|
// entitycollections
|
|
this.units = gameState.getOwnEntities().filter(API3.Filters.and(API3.Filters.byClass("Unit"),API3.Filters.byMetadata(PlayerID, "base", this.ID)));
|
|
this.buildings = gameState.getOwnEntities().filter(API3.Filters.and(API3.Filters.byClass("Structure"),API3.Filters.byMetadata(PlayerID, "base", this.ID)));
|
|
|
|
this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID,"role","worker"));
|
|
|
|
this.workers.allowQuickIter();
|
|
this.buildings.allowQuickIter();
|
|
this.units.allowQuickIter();
|
|
|
|
this.units.registerUpdates();
|
|
this.buildings.registerUpdates();
|
|
this.workers.registerUpdates();
|
|
|
|
// array of entity IDs, with each being
|
|
// { "food" : [close entities, semi-close entities, faraway entities, closeAmount, medianAmount, assignedWorkers collection ] … } (one per resource)
|
|
// note that "median amount" also counts the closeAmount.
|
|
this.dropsites = { };
|
|
|
|
// TODO: difficulty levels for this?
|
|
|
|
// smallRadius is the distance necessary to mark a resource as linked to a dropsite.
|
|
this.smallRadius = { 'food':40*40,'wood':45*45,'stone':40*40,'metal':40*40 };
|
|
// medRadius is the maximal distance for a link, albeit one that would still make us want to build a new dropsite.
|
|
this.medRadius = { 'food':70*70,'wood':70*70,'stone':80*80,'metal':80*80 };
|
|
// bigRadius is the distance for a weak link, mainly for optimizing search for resources when a DP is depleted.
|
|
this.bigRadius = { 'food':70*70,'wood':200*200,'stone':200*200,'metal':200*200 };
|
|
};
|
|
|
|
m.BaseManager.prototype.assignEntity = function(unit){
|
|
unit.setMetadata(PlayerID, "base", this.ID);
|
|
this.units.updateEnt(unit);
|
|
this.workers.updateEnt(unit);
|
|
this.buildings.updateEnt(unit);
|
|
// TODO: immediately assign it some task?
|
|
|
|
if (unit.hasClass("Structure") && unit.hasTerritoryInfluence() && this.territoryBuildings.indexOf(unit.id()) === -1)
|
|
this.territoryBuildings.push(unit.id());
|
|
};
|
|
|
|
m.BaseManager.prototype.setAnchor = function(anchorEntity) {
|
|
if (!anchorEntity.hasClass("Structure") || !anchorEntity.hasTerritoryInfluence())
|
|
{
|
|
warn("Error: Aegis' base " + this.ID + " has been assigned an anchor building that has no territorial influence. Please report this on the forum.")
|
|
return false;
|
|
}
|
|
this.anchor = anchorEntity;
|
|
this.anchor.setMetadata(PlayerID, "base", this.ID);
|
|
this.anchor.setMetadata(PlayerID, "baseAnchor", true);
|
|
this.buildings.updateEnt(this.anchor);
|
|
|
|
if (this.territoryBuildings.indexOf(this.anchor.id()) === -1)
|
|
this.territoryBuildings.push(this.anchor.id());
|
|
return true;
|
|
}
|
|
|
|
// affects the HQ map.
|
|
m.BaseManager.prototype.initTerritory = function(HQ, gameState) {
|
|
if (!this.anchor)
|
|
warn ("Error: Aegis tried to initialize the territory of base " + this.ID + " without assigning it an anchor building first");
|
|
var radius = Math.round((this.anchor.territoryInfluenceRadius() / 4.0) * 1.25);
|
|
|
|
var LandSize = gameState.sharedScript.accessibility.getRegionSize(this.anchor.position());
|
|
this.accessIndex = gameState.sharedScript.accessibility.getAccessValue(this.anchor.position());
|
|
|
|
if (LandSize < 6500)
|
|
{
|
|
// We're on a small land, we'll assign all territories in the vicinity.
|
|
// there's a slight chance we're on an elongated weird stuff, we'll just pump up a little the radius
|
|
radius = Math.round(radius*1.2);
|
|
}
|
|
var x = Math.round(this.anchor.position()[0]/gameState.cellSize);
|
|
var y = Math.round(this.anchor.position()[1]/gameState.cellSize);
|
|
|
|
this.territoryIndices = [];
|
|
|
|
var width = gameState.getMap().width;
|
|
for (var xi = -radius; xi <= radius; ++xi)
|
|
for (var yi = -radius; yi <= radius; ++yi)
|
|
if (xi*xi+yi*yi < radius*radius && HQ.basesMap.map[(x+xi) + (y+yi)*width] === 0)
|
|
{
|
|
if (this.accessIndex == gameState.sharedScript.accessibility.landPassMap[x+xi + width*(y+yi)])
|
|
{
|
|
this.territoryIndices.push((x+xi) + (y+yi)*width);
|
|
HQ.basesMap.map[(x+xi) + (y+yi)*width] = this.ID;
|
|
}
|
|
}
|
|
}
|
|
|
|
m.BaseManager.prototype.initGatheringFunctions = function(HQ, gameState, specTypes) {
|
|
// init our gathering functions.
|
|
var types = ["food","wood","stone","metal"];
|
|
if (specTypes !== undefined)
|
|
type = specTypes;
|
|
|
|
var self = this;
|
|
var count = 0;
|
|
|
|
for (var i in types)
|
|
{
|
|
var type = types[i];
|
|
// TODO: set us as "X" gatherer
|
|
|
|
this.buildings.filter(API3.Filters.isDropsite(type)).forEach(function(ent) { self.initializeDropsite(gameState, ent,type) });
|
|
|
|
if (this.getResourceLevel(gameState, type, "all") > 1000)
|
|
this.willGather[type] = 1;
|
|
}
|
|
if (this.willGather["food"] === 0)
|
|
{
|
|
var needFarm = true;
|
|
// Let's check again for food
|
|
for (var base in HQ.baseManagers)
|
|
if (HQ.baseManagers[base].willGather["food"] === 1)
|
|
needFarm = false;
|
|
if (needFarm)
|
|
this.willGather["food"] = 1;
|
|
}
|
|
m.debug ("food" + this.willGather["food"]);
|
|
m.debug (this.willGather["wood"]);
|
|
m.debug (this.willGather["stone"]);
|
|
m.debug (this.willGather["metal"]);
|
|
}
|
|
|
|
m.BaseManager.prototype.checkEvents = function (gameState, events, queues) {
|
|
for (var i in events)
|
|
{
|
|
if (events[i].type == "Destroy")
|
|
{
|
|
// let's check we haven't lost an important building here.
|
|
var evt = events[i];
|
|
if (evt.msg != undefined && !evt.msg.SuccessfulFoundation && evt.msg.entityObj != undefined && evt.msg.metadata !== undefined && evt.msg.metadata[PlayerID] &&
|
|
evt.msg.metadata[PlayerID]["base"] !== undefined && evt.msg.metadata[PlayerID]["base"] == this.ID)
|
|
{
|
|
var ent = evt.msg.entityObj;
|
|
if (ent.hasTerritoryInfluence())
|
|
this.territoryBuildings.splice(this.territoryBuildings.indexOf(ent.id()),1);
|
|
if (ent.resourceDropsiteTypes())
|
|
this.scrapDropsite(gameState, ent);
|
|
if (evt.msg.metadata[PlayerID]["baseAnchor"] && evt.msg.metadata[PlayerID]["baseAnchor"] == true)
|
|
{
|
|
// sounds like we lost our anchor. Let's try rebuilding it.
|
|
// TODO: currently the HQ manager sets us as initgathering, we probably ouht to do it
|
|
this.anchor = undefined;
|
|
|
|
this.constructing = true; // let's switch mode.
|
|
this.workers.forEach( function (worker) {
|
|
worker.stopMoving();
|
|
});
|
|
if (ent.hasClass("CivCentre"))
|
|
{
|
|
// TODO: might want to tell the queue manager to pause other stuffs if we are the only base.
|
|
queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true }, 0 , -1,ent.position()));
|
|
} else {
|
|
// TODO
|
|
queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true },0,-1,ent.position()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (var i in events)
|
|
{
|
|
if (events[i].type == "ConstructionFinished")
|
|
{
|
|
// let's check we haven't lost an important building here.
|
|
var evt = events[i];
|
|
if (evt.msg && evt.msg.newentity)
|
|
{
|
|
// TODO: we ought to add new resources or do something.
|
|
var ent = gameState.getEntityById(evt.msg.newentity);
|
|
|
|
if (ent === undefined)
|
|
continue;
|
|
|
|
if (ent.getMetadata(PlayerID,"base") == this.ID)
|
|
{
|
|
if(ent.hasTerritoryInfluence())
|
|
this.territoryBuildings.push(ent.id());
|
|
if (ent.resourceDropsiteTypes())
|
|
for (var ress in ent.resourceDropsiteTypes())
|
|
this.initializeDropsite(gameState, ent, ent.resourceDropsiteTypes()[ress]);
|
|
if (ent.resourceSupplyAmount() && ent.resourceSupplyType()["specific"] == "grain")
|
|
this.assignResourceToDP(gameState,ent);
|
|
}
|
|
}
|
|
} else if (events[i].type == "Create")
|
|
{
|
|
// Checking for resources.
|
|
var evt = events[i];
|
|
if (evt.msg && evt.msg.entity)
|
|
{
|
|
var ent = gameState.getEntityById(evt.msg.entity);
|
|
|
|
if (ent === undefined)
|
|
continue;
|
|
|
|
if (ent.resourceSupplyAmount() && ent.owner() == 0)
|
|
this.assignResourceToDP(gameState,ent);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// If no specific dropsite, it'll assign to the closest
|
|
m.BaseManager.prototype.assignResourceToDP = function (gameState, supply, specificDP) {
|
|
var type = supply.resourceSupplyType()["generic"];
|
|
if (type == "treasure")
|
|
type = supply.resourceSupplyType()["specific"];
|
|
if (!specificDP)
|
|
{
|
|
var closest = -1;
|
|
var dist = Math.min();
|
|
for (var i in this.dropsites)
|
|
{
|
|
var dp = gameState.getEntityById(i);
|
|
var distance = API3.SquareVectorDistance(supply.position(), dp.position());
|
|
if (distance < dist && distance < this.bigRadius[type])
|
|
{
|
|
closest = dp.id();
|
|
dist = distance;
|
|
}
|
|
}
|
|
if (closest !== -1)
|
|
{
|
|
supply.setMetadata(PlayerID, "linked-dropsite-close", (dist < this.smallRadius[type]) );
|
|
supply.setMetadata(PlayerID, "linked-dropsite-nearby", (dist < this.medRadius[type]) );
|
|
supply.setMetadata(PlayerID, "linked-dropsite", closest );
|
|
supply.setMetadata(PlayerID, "linked-dropsite-dist", +dist);
|
|
}
|
|
}
|
|
// TODO: ought to recount immediatly.
|
|
}
|
|
|
|
m.BaseManager.prototype.initializeDropsite = function (gameState, ent, type) {
|
|
var count = 0, farCount = 0;
|
|
var self = this;
|
|
|
|
var resources = gameState.getResourceSupplies(type);
|
|
|
|
// TODO: if we're initing, we should probably remove them anyway.
|
|
if (self.dropsites[ent.id()] === undefined || self.dropsites[ent.id()][type] === undefined) {
|
|
resources.filter( function (supply) { //}){
|
|
if (!supply.position() || !ent.position())
|
|
return;
|
|
var distance = API3.SquareVectorDistance(supply.position(), ent.position());
|
|
|
|
if (supply.getMetadata(PlayerID, "linked-dropsite") == undefined || supply.getMetadata(PlayerID, "linked-dropsite-dist") > distance) {
|
|
if (distance < self.bigRadius[type]) {
|
|
supply.setMetadata(PlayerID, "linked-dropsite-close", (distance < self.smallRadius[type]) );
|
|
supply.setMetadata(PlayerID, "linked-dropsite-nearby", (distance < self.medRadius[type]) );
|
|
supply.setMetadata(PlayerID, "linked-dropsite", ent.id() );
|
|
supply.setMetadata(PlayerID, "linked-dropsite-dist", +distance);
|
|
if(distance < self.smallRadius[type])
|
|
count += supply.resourceSupplyAmount();
|
|
if (distance < self.medRadius[type])
|
|
farCount += supply.resourceSupplyAmount();
|
|
}
|
|
}
|
|
});
|
|
// This one is both for the nearby and the linked
|
|
var filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite", ent.id());
|
|
var collection = resources.filter(filter);
|
|
collection.registerUpdates();
|
|
|
|
filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite-close",true);
|
|
var collection2 = collection.filter(filter);
|
|
collection2.registerUpdates();
|
|
|
|
filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite-nearby",true);
|
|
var collection3 = collection.filter(filter);
|
|
collection3.registerUpdates();
|
|
|
|
filter = API3.Filters.byMetadata(PlayerID, "linked-to-dropsite", ent.id());
|
|
var WkCollection = this.workers.filter(filter);
|
|
WkCollection.registerUpdates();
|
|
|
|
if (!self.dropsites[ent.id()])
|
|
self.dropsites[ent.id()] = {};
|
|
self.dropsites[ent.id()][type] = [collection2,collection3, collection, count, farCount, WkCollection];
|
|
|
|
// TODO: flag us on the SharedScript "type" map.
|
|
// TODO: get workers on those resources and do something with them.
|
|
}
|
|
|
|
if (m.DebugEnabled())
|
|
{
|
|
// Make resources glow wildly
|
|
if (type == "food") {
|
|
self.dropsites[ent.id()][type][2].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0,0]});
|
|
});
|
|
self.dropsites[ent.id()][type][1].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]});
|
|
});
|
|
self.dropsites[ent.id()][type][0].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]});
|
|
});
|
|
}
|
|
if (type == "wood") {
|
|
self.dropsites[ent.id()][type][2].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0]});
|
|
});
|
|
self.dropsites[ent.id()][type][1].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]});
|
|
});
|
|
self.dropsites[ent.id()][type][0].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]});
|
|
});
|
|
}
|
|
if (type == "stone") {
|
|
self.dropsites[ent.id()][type][2].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0.5,0]});
|
|
});
|
|
self.dropsites[ent.id()][type][1].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]});
|
|
});
|
|
self.dropsites[ent.id()][type][0].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,10,0]});
|
|
});
|
|
}
|
|
if (type == "metal") {
|
|
self.dropsites[ent.id()][type][2].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0.5]});
|
|
});
|
|
self.dropsites[ent.id()][type][1].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,2]});
|
|
});
|
|
self.dropsites[ent.id()][type][0].forEach(function(ent){
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,10]});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// completely and "safely" remove a dropsite from our list.
|
|
// this also removes any linked resource and so on.
|
|
// TODO: should re-add the resources to another dropsite.
|
|
m.BaseManager.prototype.scrapDropsite = function (gameState, ent) {
|
|
if (this.dropsites[ent.id()] === undefined)
|
|
return true;
|
|
|
|
for (var i in this.dropsites[ent.id()])
|
|
{
|
|
var type = i;
|
|
var dp = this.dropsites[ent.id()][i];
|
|
dp[2].forEach(function (supply) { //}){
|
|
supply.deleteMetadata(PlayerID,"linked-dropsite-nearby");
|
|
supply.deleteMetadata(PlayerID,"linked-dropsite-close");
|
|
supply.deleteMetadata(PlayerID,"linked-dropsite");
|
|
supply.deleteMetadata(PlayerID,"linked-dropsite-dist");
|
|
});
|
|
dp[5].forEach(function (worker) {
|
|
worker.deleteMetadata(PlayerID,"linked-to-dropsite");
|
|
// TODO: should probably stop the worker or something.
|
|
});
|
|
dp = [undefined, undefined, undefined, 0, 0, undefined];
|
|
delete this.dropsites[ent.id()][i];
|
|
}
|
|
this.dropsites[ent.id()] = undefined;
|
|
delete this.dropsites[ent.id()];
|
|
return true;
|
|
};
|
|
|
|
// Returns the position of the best place to build a new dropsite for the specified resource
|
|
m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource){
|
|
|
|
var storeHousePlate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse"));
|
|
|
|
// This builds a map. The procedure is fairly simple. It adds the resource maps
|
|
// (which are dynamically updated and are made so that they will facilitate DP placement)
|
|
// Then checks for a good spot in the territory. If none, and town/city phase, checks outside
|
|
// The AI will currently not build a CC if it wouldn't connect with an existing CC.
|
|
|
|
var territory = m.createTerritoryMap(gameState);
|
|
|
|
var obstructions = m.createObstructionMap(gameState,this.accessIndex,storeHousePlate);
|
|
obstructions.expandInfluences();
|
|
|
|
// copy the resource map as initialization.
|
|
var friendlyTiles = new API3.Map(gameState.sharedScript, gameState.sharedScript.resourceMaps[resource].map, true);
|
|
|
|
var DPFoundations = gameState.getOwnFoundations().filter(API3.Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse")));
|
|
|
|
// TODO: might be better to check dropsites someplace else.
|
|
// loop over this in this.terrytoryindices. It's usually a little too much, but it's always enough.
|
|
for (var p = 0; p < this.territoryIndices.length; ++p)
|
|
{
|
|
var j = this.territoryIndices[p];
|
|
friendlyTiles.map[j] *= 1.5;
|
|
|
|
// only add where the map is currently not null, ie in our territory and some "Resource" would be close.
|
|
// This makes the placement go from "OK" to "human-like".
|
|
for (var i in gameState.sharedScript.resourceMaps)
|
|
if (friendlyTiles.map[j] !== 0 && i !== "food")
|
|
friendlyTiles.map[j] += gameState.sharedScript.resourceMaps[i].map[j];
|
|
|
|
for (var i in this.dropsites)
|
|
{
|
|
var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)];
|
|
var dpPos = gameState.getEntityById(i).position();
|
|
if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250)
|
|
{
|
|
friendlyTiles.map[j] = 0;
|
|
continue;
|
|
} else if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450)
|
|
friendlyTiles.map[j] /= 2;
|
|
}
|
|
for (var i in DPFoundations._entities)
|
|
{
|
|
var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)];
|
|
var dpPos = gameState.getEntityById(i).position();
|
|
if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250)
|
|
friendlyTiles.map[j] = 0;
|
|
else if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450)
|
|
friendlyTiles.map[j] /= 2;
|
|
}
|
|
}
|
|
|
|
if (m.DebugEnabled())
|
|
friendlyTiles.dumpIm("DP_" + resource + "_" + gameState.getTimeElapsed() + ".png");
|
|
|
|
var best = friendlyTiles.findBestTile(2, obstructions); // try to find a spot to place a DP.
|
|
var bestIdx = best[0];
|
|
|
|
// tell the dropsite builder we haven't found anything satisfactory.
|
|
if (best[1] < 60)
|
|
return false;
|
|
|
|
var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize;
|
|
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize;
|
|
|
|
return [x,z];
|
|
};
|
|
|
|
// update the resource level of a dropsite.
|
|
m.BaseManager.prototype.updateDropsite = function (gameState, ent, type) {
|
|
if (this.dropsites[ent.id()] === undefined || this.dropsites[ent.id()][type] === undefined)
|
|
return undefined; // should initialize it first.
|
|
|
|
var count = 0, farCount = 0;
|
|
|
|
var resources = gameState.getResourceSupplies(type);
|
|
|
|
this.dropsites[ent.id()][type][1].forEach( function (supply) { //}){
|
|
farCount += supply.resourceSupplyAmount();
|
|
});
|
|
this.dropsites[ent.id()][type][0].forEach( function (supply) { //}){
|
|
count += supply.resourceSupplyAmount();
|
|
});
|
|
|
|
this.dropsites[ent.id()][type][3] = count;
|
|
this.dropsites[ent.id()][type][4] = farCount;
|
|
return true;
|
|
};
|
|
|
|
// Updates dropsites.
|
|
m.BaseManager.prototype.updateDropsites = function (gameState) {
|
|
// for each dropsite, recalculate
|
|
for (var i in this.dropsites)
|
|
{
|
|
for (var type in this.dropsites[i])
|
|
{
|
|
this.updateDropsite(gameState,gameState.getEntityById(i),type);
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
// TODO: ought to be cached or something probably
|
|
// Returns the number of slots available for workers here.
|
|
// we're assuming Max - 3 for metal/stone mines, and 20 for any dropsite that has wood.
|
|
// TODO: for wood might want to count the trees too.
|
|
// TODO: this returns "future" worker capacity, might want to have a current one.
|
|
m.BaseManager.prototype.getWorkerCapacity = function (gameState, type) {
|
|
var count = 0;
|
|
if (type == "food")
|
|
return 1000000; // TODO: perhaps return something sensible here.
|
|
if (type === "stone" || type === "metal")
|
|
{
|
|
for (var id in this.dropsites)
|
|
if (this.dropsites[id][type])
|
|
this.dropsites[id][type][1].forEach(function (ent) {// }){
|
|
if (ent.resourceSupplyAmount() > 500)
|
|
count += ent.maxGatherers() - 3;
|
|
});
|
|
} else if (type === "wood")
|
|
{
|
|
for (var id in this.dropsites)
|
|
if (this.dropsites[id][type] && (this.dropsites[id][type][4]) > 1000)
|
|
count += Math.min(15, this.dropsites[id][type][4] / 200);
|
|
}
|
|
return count;
|
|
};
|
|
|
|
// TODO: ought to be cached or something probably
|
|
// Returns the amount of resource left
|
|
m.BaseManager.prototype.getResourceLevel = function (gameState, type, searchType, threshold) {
|
|
var count = 0;
|
|
if (searchType == "all")
|
|
{
|
|
// return all resources in the base area.
|
|
gameState.getResourceSupplies(type).filter(API3.Filters.byTerritory(gameState.ai.HQ.basesMap, this.ID)).forEach( function (ent) { //}){
|
|
count += ent.resourceSupplyAmount();
|
|
});
|
|
return count;
|
|
}
|
|
if (searchType == "dropsites")
|
|
{
|
|
// for each dropsite, recalculate
|
|
for (var i in this.dropsites)
|
|
if (this.dropsites[i][type] !== undefined)
|
|
count += this.dropsites[i][type][4];
|
|
return count;
|
|
}
|
|
if (searchType == "dropsitesClose")
|
|
{
|
|
// for each dropsite, recalculate
|
|
for (var i in this.dropsites)
|
|
if (this.dropsites[i][type] !== undefined)
|
|
count += this.dropsites[i][type][3];
|
|
return count;
|
|
}
|
|
if (searchType == "dropsites-dpcount")
|
|
{
|
|
var seuil = 800;
|
|
if (threshold)
|
|
seuil = threshold;
|
|
// for each dropsite, recalculate
|
|
for (var i in this.dropsites)
|
|
if (this.dropsites[i][type] !== undefined)
|
|
{
|
|
if (this.dropsites[i][type][4] > seuil)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// check our resource levels and react accordingly
|
|
m.BaseManager.prototype.checkResourceLevels = function (gameState,queues) {
|
|
for (var type in this.willGather)
|
|
{
|
|
if (this.willGather[type] === 0)
|
|
continue;
|
|
if (type !== "food" && gameState.playedTurn % 10 === 4 && this.getResourceLevel(gameState,type, "all") < 200)
|
|
this.willGather[type] = 0; // won't gather at all
|
|
if (this.willGather[type] === 2)
|
|
continue;
|
|
var count = this.getResourceLevel(gameState,type, "dropsites");
|
|
if (type == "food")
|
|
{
|
|
if (!this.isFarming && count < 1600 && queues.field.length === 0)
|
|
{
|
|
// tell the queue manager we'll be trying to build fields shortly.
|
|
for (var i = 0; i < this.Config.Economy.initialFields;++i)
|
|
{
|
|
var plan = new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID });
|
|
plan.isGo = function() { return false; }; // don't start right away.
|
|
queues.field.addItem(plan);
|
|
}
|
|
} else if (!this.isFarming && count < 650)
|
|
{
|
|
for (var i in queues.field.queue)
|
|
queues.field.queue[i].isGo = function() { return true; }; // start them
|
|
this.isFarming = true;
|
|
}
|
|
if (this.isFarming)
|
|
{
|
|
var numFarms = 0;
|
|
this.buildings.filter(API3.Filters.byClass("Field")).forEach(function (field) {
|
|
if (field.resourceSupplyAmount() > 400)
|
|
numFarms++;
|
|
});
|
|
var numFd = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true);
|
|
numFarms += numFd;
|
|
numFarms += queues.field.countQueuedUnits();
|
|
|
|
// let's see if we need to push new farms.
|
|
if (numFd < 2)
|
|
if (numFarms < Math.round(this.gatherersByType(gameState, "food").length / 4.6) || numFarms < Math.round(this.workers.length / 15.0))
|
|
queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID }));
|
|
// TODO: refine count to only count my base.
|
|
}
|
|
} else if (queues.dropsites.length() === 0 && gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_storehouse")) === 0) {
|
|
var wantedDPs = Math.ceil(this.gatherersByType(gameState, type).length / 12.0);
|
|
var need = wantedDPs - this.getResourceLevel(gameState,type, "dropsites-dpcount",2000);
|
|
if (need > 0)
|
|
{
|
|
var pos = this.findBestDropsiteLocation(gameState, type);
|
|
if (!pos)
|
|
{
|
|
m.debug ("Found no right position for a " + type + " dropsite, going into \"noSpot\" mode");
|
|
this.willGather[type] = 2; // won't build
|
|
// TODO: tell the HQ we'll be needing a new base for this resource, or tell it we've ran out of resource Z.
|
|
} else {
|
|
m.debug ("planning new dropsite for " + type);
|
|
queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : this.ID }, 0, -1, pos));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
// let's return the estimated gather rates.
|
|
m.BaseManager.prototype.getGatherRates = function(gameState, currentRates) {
|
|
|
|
};
|
|
|
|
m.BaseManager.prototype.assignRolelessUnits = function(gameState) {
|
|
// TODO: make this cleverer.
|
|
var roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role")));
|
|
var self = this;
|
|
roleless.forEach(function(ent) {
|
|
if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier")) {
|
|
if (ent.hasClass("Cavalry") && !self.isHunting)
|
|
return;
|
|
ent.setMetadata(PlayerID, "role", "worker");
|
|
}
|
|
});
|
|
};
|
|
|
|
// If the numbers of workers on the resources is unbalanced then set some of workers to idle so
|
|
// they can be reassigned by reassignIdleWorkers.
|
|
// TODO: actually this probably should be in the HQ.
|
|
m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState){
|
|
var self = this;
|
|
if (gameState.currentPhase() < 2 && gameState.getTimeElapsed() < 360000)
|
|
return; // not in the first phase or the first 6 minutes.
|
|
|
|
var types = gameState.ai.queueManager.getAvailableResources(gameState);
|
|
|
|
var bestType = "";
|
|
|
|
var avgOverdraft = 0;
|
|
|
|
for (var i in types.types)
|
|
avgOverdraft += types[types.types[i]];
|
|
|
|
avgOverdraft /= 4;
|
|
|
|
for (var i in types.types)
|
|
if (types[types.types[i]] > avgOverdraft + 200 || (types[types.types[i]] > avgOverdraft && avgOverdraft > 200))
|
|
if (this.gatherersByType(gameState,types.types[i]).length > 0)
|
|
{
|
|
// TODO: perhaps change this?
|
|
var nb = 2;
|
|
this.gatherersByType(gameState,types.types[i]).forEach( function (ent) { //}){
|
|
if (nb > 0)
|
|
{
|
|
//m.debug ("Moving " +ent.id() + " from " + types.types[i]);
|
|
nb--;
|
|
// TODO: might want to direct assign.
|
|
ent.stopMoving();
|
|
ent.setMetadata(PlayerID, "subrole","idle");
|
|
}
|
|
});
|
|
}
|
|
//m.debug (currentRates);
|
|
};
|
|
|
|
// TODO: work on this.
|
|
m.BaseManager.prototype.reassignIdleWorkers = function(gameState) {
|
|
|
|
var self = this;
|
|
|
|
// Search for idle workers, and tell them to gather resources based on demand
|
|
var filter = API3.Filters.or(API3.Filters.byMetadata(PlayerID,"subrole","idle"), API3.Filters.not(API3.Filters.byHasMetadata(PlayerID,"subrole")));
|
|
var idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers);
|
|
|
|
if (idleWorkers.length) {
|
|
idleWorkers.forEach(function(ent) {
|
|
// Check that the worker isn't garrisoned
|
|
if (ent.position() === undefined){
|
|
return;
|
|
}
|
|
if (ent.hasClass("Worker")) {
|
|
var types = gameState.ai.HQ.pickMostNeededResources(gameState);
|
|
//m.debug ("assigning " +ent.id() + " to " + types[0]);
|
|
ent.setMetadata(PlayerID, "subrole", "gatherer");
|
|
ent.setMetadata(PlayerID, "gather-type", types[0]);
|
|
|
|
if (gameState.turnCache["gathererAssignementCache-" + types[0]])
|
|
gameState.turnCache["gathererAssignementCache-" + types[0]]++;
|
|
else
|
|
gameState.turnCache["gathererAssignementCache-" + types[0]] = 1;
|
|
// Okay let's now check we can actually remain here for that
|
|
if (self.willGather[types[0]] !== 1)
|
|
{
|
|
// TODO: if fail, we should probably pick the second most needed resource.
|
|
gameState.ai.HQ.switchWorkerBase(gameState, ent, types[0]);
|
|
}
|
|
} else {
|
|
ent.setMetadata(PlayerID, "subrole", "hunter");
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) {
|
|
return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers, true);
|
|
};
|
|
|
|
m.BaseManager.prototype.gatherersByType = function(gameState, type) {
|
|
return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer"));
|
|
};
|
|
|
|
|
|
// returns an entity collection of workers.
|
|
// They are idled immediatly and their subrole set to idle.
|
|
m.BaseManager.prototype.pickBuilders = function(gameState, number) {
|
|
var collec = new API3.EntityCollection(gameState.sharedScript);
|
|
// TODO: choose better.
|
|
var workers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry"))).toEntityArray();
|
|
workers.sort(function (a,b) {
|
|
var vala = 0, valb = 0;
|
|
if (a.getMetadata(PlayerID,"subrole") == "builder")
|
|
vala = 100;
|
|
if (b.getMetadata(PlayerID,"subrole") == "builder")
|
|
valb = 100;
|
|
if (a.getMetadata(PlayerID,"plan") != undefined)
|
|
vala = -100;
|
|
if (b.getMetadata(PlayerID,"plan") != undefined)
|
|
valb = -100;
|
|
return a < b
|
|
});
|
|
for (var i = 0; i < number; ++i)
|
|
{
|
|
workers[i].stopMoving();
|
|
workers[i].setMetadata(PlayerID, "subrole","idle");
|
|
collec.addEnt(workers[i]);
|
|
}
|
|
return collec;
|
|
}
|
|
|
|
m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) {
|
|
|
|
// If we have some foundations, and we don't have enough builder-workers,
|
|
// try reassigning some other workers who are nearby
|
|
|
|
// AI tries to use builders sensibly, not completely stopping its econ.
|
|
|
|
var self = this;
|
|
|
|
var foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(),API3.Filters.not(API3.Filters.byClass("Field")))).toEntityArray();
|
|
var damagedBuildings = this.buildings.filter(function (ent) { if (ent.needsRepair() && ent.getMetadata(PlayerID, "plan") == undefined) { return true; } return false; }).toEntityArray();
|
|
|
|
// Check if nothing to build
|
|
if (!foundations.length && !damagedBuildings.length){
|
|
return;
|
|
}
|
|
var workers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry")));
|
|
var builderWorkers = this.workersBySubrole(gameState, "builder");
|
|
var idleBuilderWorkers = this.workersBySubrole(gameState, "builder").filter(API3.Filters.isIdle());
|
|
|
|
// if we're constructing and we have the foundations to our base anchor, only try building that.
|
|
if (this.constructing == true && this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.byMetadata(PlayerID, "baseAnchor", true))).length !== 0)
|
|
{
|
|
foundations = this.buildings.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).toEntityArray();
|
|
var tID = foundations[0].id();
|
|
workers.forEach(function (ent) { //}){
|
|
var target = ent.getMetadata(PlayerID, "target-foundation");
|
|
if (target && target != tID)
|
|
{
|
|
ent.stopMoving();
|
|
ent.setMetadata(PlayerID, "target-foundation", tID);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (workers.length < 2)
|
|
{
|
|
var noobs = gameState.ai.HQ.bulkPickWorkers(gameState, this.ID, 2);
|
|
if(noobs)
|
|
{
|
|
noobs.forEach(function (worker) { //}){
|
|
worker.setMetadata(PlayerID,"base", self.ID);
|
|
worker.setMetadata(PlayerID,"subrole", "builder");
|
|
workers.updateEnt(worker);
|
|
builderWorkers.updateEnt(worker);
|
|
idleBuilderWorkers.updateEnt(worker);
|
|
});
|
|
}
|
|
}
|
|
var addedWorkers = 0;
|
|
|
|
var maxTotalBuilders = Math.ceil(workers.length * 0.15);
|
|
if (this.constructing == true && maxTotalBuilders < 15)
|
|
maxTotalBuilders = 15;
|
|
|
|
for (var i in foundations) {
|
|
var target = foundations[i];
|
|
// Removed: sometimes the AI would not notice it has empty unbuilt fields
|
|
//if (target._template.BuildRestrictions.Category === "Field")
|
|
// continue; // we do not build fields
|
|
|
|
var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
|
|
|
|
var targetNB = this.Config.Economy.targetNumBuilders; // TODO: dynamic that.
|
|
if (target.hasClass("CivCentre") || target.buildTime() > 150 || target.hasClass("House"))
|
|
targetNB *= 2;
|
|
if (target.getMetadata(PlayerID, "baseAnchor") == true)
|
|
targetNB = 15;
|
|
|
|
if (assigned < targetNB) {
|
|
if (builderWorkers.length - idleBuilderWorkers.length + addedWorkers < maxTotalBuilders) {
|
|
|
|
var addedToThis = 0;
|
|
|
|
idleBuilderWorkers.forEach(function(ent) {
|
|
if (ent.position() && API3.SquareVectorDistance(ent.position(), target.position()) < 10000 && assigned + addedToThis < targetNB)
|
|
{
|
|
addedWorkers++;
|
|
addedToThis++;
|
|
ent.setMetadata(PlayerID, "target-foundation", target.id());
|
|
}
|
|
});
|
|
if (assigned + addedToThis < targetNB)
|
|
{
|
|
var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); });
|
|
var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB - assigned - addedToThis);
|
|
nearestNonBuilders.forEach(function(ent) {
|
|
addedWorkers++;
|
|
addedToThis++;
|
|
ent.stopMoving();
|
|
ent.setMetadata(PlayerID, "subrole", "builder");
|
|
ent.setMetadata(PlayerID, "target-foundation", target.id());
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed.
|
|
for (var i in damagedBuildings) {
|
|
var target = damagedBuildings[i];
|
|
if (gameState.defcon() < 5) {
|
|
if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) {
|
|
continue;
|
|
}
|
|
} else if (noRepair && !target.hasClass("CivCentre"))
|
|
continue;
|
|
|
|
var territory = m.createTerritoryMap(gameState);
|
|
if (territory.getOwner(target.position()) !== PlayerID || territory.getOwner([target.position()[0] + 5, target.position()[1]]) !== PlayerID)
|
|
continue;
|
|
|
|
var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
|
|
if (assigned < this.targetNumBuilders/3) {
|
|
if (builderWorkers.length + addedWorkers < this.targetNumBuilders*2) {
|
|
|
|
var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); });
|
|
if (gameState.defcon() < 5)
|
|
nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); });
|
|
var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders/3 - assigned);
|
|
|
|
nearestNonBuilders.forEach(function(ent) {
|
|
ent.stopMoving();
|
|
addedWorkers++;
|
|
ent.setMetadata(PlayerID, "subrole", "builder");
|
|
ent.setMetadata(PlayerID, "target-foundation", target.id());
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
m.BaseManager.prototype.update = function(gameState, queues, events) {
|
|
Engine.ProfileStart("Base update - base " + this.ID);
|
|
var self = this;
|
|
|
|
this.updateDropsites(gameState);
|
|
this.checkResourceLevels(gameState, queues);
|
|
|
|
Engine.ProfileStart("Assign builders");
|
|
this.assignToFoundations(gameState);
|
|
Engine.ProfileStop()
|
|
|
|
// if (!this.constructing)
|
|
// {
|
|
if (gameState.ai.playedTurn % 2 === 0)
|
|
this.setWorkersIdleByPriority(gameState);
|
|
|
|
this.assignRolelessUnits(gameState);
|
|
|
|
/*Engine.ProfileStart("Swap Workers");
|
|
var gathererGroups = {};
|
|
gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ }){
|
|
if (ent.hasClass("Cavalry"))
|
|
return;
|
|
var key = uneval(ent.resourceGatherRates());
|
|
if (!gathererGroups[key]){
|
|
gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []};
|
|
}
|
|
if (ent.getMetadata(PlayerID, "gather-type") in gathererGroups[key]){
|
|
gathererGroups[key][ent.getMetadata(PlayerID, "gather-type")].push(ent);
|
|
}
|
|
});
|
|
for (var i in gathererGroups){
|
|
for (var j in gathererGroups){
|
|
var a = eval(i);
|
|
var b = eval(j);
|
|
if (a !== undefined && b !== undefined)
|
|
if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0
|
|
&& gathererGroups[j]["food"].length > 0){
|
|
for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){
|
|
gathererGroups[i]["wood"][k].setMetadata(PlayerID, "gather-type", "food");
|
|
gathererGroups[j]["food"][k].setMetadata(PlayerID, "gather-type", "wood");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Engine.ProfileStop();*/
|
|
|
|
// should probably be last to avoid reallocations of units that would have done stuffs otherwise.
|
|
Engine.ProfileStart("Assigning Workers");
|
|
this.reassignIdleWorkers(gameState);
|
|
Engine.ProfileStop();
|
|
// }
|
|
|
|
// TODO: do this incrementally a la defence.js
|
|
Engine.ProfileStart("Run Workers");
|
|
this.workers.forEach(function(ent) {
|
|
if (!ent.getMetadata(PlayerID, "worker-object"))
|
|
ent.setMetadata(PlayerID, "worker-object", new m.Worker(ent));
|
|
ent.getMetadata(PlayerID, "worker-object").update(self, gameState);
|
|
});
|
|
Engine.ProfileStop();
|
|
|
|
Engine.ProfileStop();
|
|
};
|
|
|
|
return m;
|
|
|
|
}(AEGIS);
|