0ad/binaries/data/mods/public/simulation/ai/aegis/base-manager.js
Yves 4a15ac406b Move debug function and debug flag to API3 and use them from Aegis.
Remove copyPrototype from Aegis because that function is currently not
used and is already in API3.

Refs #2322

This was SVN commit r14446.
2013-12-30 14:28:30 +00:00

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 (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 (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 (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 (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 (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 (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 (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 (i in this.dropsites)
{
for (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 (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 (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 (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 (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 (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 (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 (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 (i in types.types)
avgOverdraft += types[types.types[i]];
avgOverdraft /= 4;
for (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);