0ad/binaries/data/mods/public/simulation/ai/aegis/worker.js
wraitii ede4f32bf2 Change the defense system used by Aegis to use more modular armies. This should be faster and easier to extend, though right now it might not be as efficient as before.
Fix a few bugs, including a few bad ones in the economy.
Change the way messages are handled, should be marginally faster in the
later game.
Makes gatherers count limit be per-player (refs #1387 and #643).

This was SVN commit r14552.
2014-01-10 01:46:27 +00:00

646 lines
22 KiB
JavaScript

var AEGIS = function(m)
{
/**
* This class makes a worker do as instructed by the economy manager
*/
m.Worker = function(ent) {
this.ent = ent;
this.maxApproachTime = 45000;
this.unsatisfactoryResource = false; // if true we'll reguarly check if we can't have better now.
this.baseID = 0;
};
m.Worker.prototype.update = function(baseManager, gameState) {
this.baseID = baseManager.ID;
var subrole = this.ent.getMetadata(PlayerID, "subrole");
if (!this.ent.position() || (this.ent.getMetadata(PlayerID,"fleeing") && gameState.getTimeElapsed() - this.ent.getMetadata(PlayerID,"fleeing") < 8000)){
// If the worker has no position then no work can be done
return;
}
if (this.ent.getMetadata(PlayerID,"fleeing"))
this.ent.setMetadata(PlayerID,"fleeing", undefined);
// Okay so we have a few tasks.
// If we're gathering, we'll check that we haven't run idle.
// ANd we'll also check that we're gathering a resource we want to gather.
// If we're fighting, let's not start gathering, heh?
// TODO: remove this when we're hunting?
if (this.ent.unitAIState().split(".")[1] === "COMBAT" || this.ent.getMetadata(PlayerID, "role") === "transport")
{
return;
}
if (subrole === "gatherer") {
if (this.ent.isIdle()) {
// if we aren't storing resources or it's the same type as what we're about to gather,
// let's just pick a new resource.
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 ||
this.ent.resourceCarrying()[0].type === this.ent.getMetadata(PlayerID, "gather-type")){
Engine.ProfileStart("Start Gathering");
this.unsatisfactoryResource = false;
this.startGathering(baseManager,gameState);
Engine.ProfileStop();
this.startApproachingResourceTime = gameState.getTimeElapsed();
} else {
// Should deposit resources
Engine.ProfileStart("Return Resources");
if (!this.returnResources(gameState))
{
// no dropsite, abandon cargo.
// if we were ordered to gather something else, try that.
if (this.ent.resourceCarrying()[0].type !== this.ent.getMetadata(PlayerID, "gather-type"))
this.startGathering(baseManager,gameState);
else {
// okay so we haven't found a proper dropsite for the resource we're supposed to gather
// so let's get idle and the base manager will reassign us, hopefully well.
this.ent.setMetadata(PlayerID, "gather-type",undefined);
this.ent.setMetadata(PlayerID, "subrole", "idle");
this.ent.stopMoving();
}
}
Engine.ProfileStop();
}
// debug: show the resource we're gathering from
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]});
} else if (this.ent.unitAIState().split(".")[1] === "GATHER") {
// check for transport.
if (gameState.ai.playedTurn % 5 === 0)
{
if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[2] === "APPROACHING" && this.ent.unitAIOrderData()[0]["target"])
{
var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]);
if (ress !== undefined)
{
var index = gameState.ai.accessibility.getAccessValue(ress.position());
var mIndex = gameState.ai.accessibility.getAccessValue(this.ent.position());
if (index !== mIndex && index !== 1)
{
//gameState.ai.HQ.navalManager.askForTransport(this.ent.id(), this.ent.position(), ress.position());
}
}
}
}
/*
if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime)
{
if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER"
&& this.ent.unitAIOrderData()[0]["target"])
{
var ent = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]);
m.debug ("here " + this.startApproachingResourceAmount + "," + ent.resourceSupplyAmount());
if (ent && this.startApproachingResourceAmount == ent.resourceSupplyAmount() && this.startEnt == ent.id()) {
m.debug (ent.toString() + " is inaccessible");
ent.setMetadata(PlayerID, "inaccessible", true);
this.ent.flee(ent);
this.ent.setMetadata(PlayerID, "subrole", "idle");
}
}
}*/
// we're gathering. Let's check that it's not a resource we'd rather not gather from.
if ((this.ent.id() + gameState.ai.playedTurn) % 6 === 0 && this.checkUnsatisfactoryResource(gameState))
{
Engine.ProfileStart("Start Gathering");
this.startGathering(baseManager,gameState);
Engine.ProfileStop();
}
// TODO: reimplement the "reaching time" check.
/*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) {
if (this.gatheringFrom) {
var ent = gameState.getEntityById(this.gatheringFrom);
if ((ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax())) {
// if someone gathers from it, it's only that the pathfinder sucks.
m.debug (ent.toString() + " is inaccessible");
ent.setMetadata(PlayerID, "inaccessible", true);
this.ent.flee(ent);
this.ent.setMetadata(PlayerID, "subrole", "idle");
this.gatheringFrom = undefined;
}
}
}*/
} else if (this.ent.unitAIState().split(".")[1] === "COMBAT") {
/*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) {
var ent = gameState.getEntityById(this.ent.unitAIOrderData()[0].target);
if (ent && !ent.isHurt()) {
// if someone gathers from it, it's only that the pathfinder sucks.
m.debug (ent.toString() + " is inaccessible from Combat");
ent.setMetadata(PlayerID, "inaccessible", true);
this.ent.flee(ent);
this.ent.setMetadata(PlayerID, "subrole", "idle");
this.gatheringFrom = undefined;
}
}*/
}
} else if(subrole === "builder") {
// check for transport.
if (gameState.ai.playedTurn % 5 === 0)
{
if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[2] === "APPROACHING" && this.ent.unitAIOrderData()[0]["target"])
{
var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]);
if (ress !== undefined)
{
var index = gameState.ai.accessibility.getAccessValue(ress.position());
var mIndex = gameState.ai.accessibility.getAccessValue(this.ent.position());
if (index !== mIndex && index !== 1)
{
//gameState.ai.HQ.navalManager.askForTransport(this.ent.id(), this.ent.position(), ress.position());
}
}
}
}
if (this.ent.unitAIState().split(".")[1] !== "REPAIR") {
var target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation"));
// okay so apparently we aren't working.
// Unless we've been explicitely told to keep our role, make us idle.
if (!target || target.foundationProgress() === undefined && target.needsRepair() == false)
{
if (!this.ent.getMetadata(PlayerID, "keepSubrole"))
this.ent.setMetadata(PlayerID, "subrole", "idle");
}
else
this.ent.repair(target);
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]});
// TODO: we should maybe decide on our own to build other buildings, not rely on the assigntofoundation stuff.
} else if(subrole === "hunter") {
if (this.ent.isIdle()){
Engine.ProfileStart("Start Hunting");
this.startHunting(gameState, baseManager);
Engine.ProfileStop();
}
}
};
// check if our current resource is unsatisfactory
// this can happen in two ways:
// -either we were on an unsatisfactory resource last time we started gathering (this.unsatisfactoryResource)
// -Or we auto-moved to a bad resource thanks to the great UnitAI.
m.Worker.prototype.checkUnsatisfactoryResource = function(gameState) {
if (this.unsatisfactoryResource)
return true;
if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIState().split(".")[2] === "GATHERING" && this.ent.unitAIOrderData()[0]["target"])
{
var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]);
if (!ress || !ress.getMetadata(PlayerID,"linked-dropsite") || !ress.getMetadata(PlayerID,"linked-dropsite-nearby") || gameState.ai.accessibility.getAccessValue(ress.position()) === -1)
return true;
}
return false;
};
m.Worker.prototype.startGathering = function(baseManager, gameState) {
var resource = this.ent.getMetadata(PlayerID, "gather-type");
var ent = this.ent;
var self = this;
if (!ent.position()){
// TODO: work out what to do when entity has no position
return;
}
// TODO: this is not necessarily optimal.
// find closest dropsite which has nearby resources of the correct type
var minDropsiteDist = Math.min(); // set to infinity initially
var nearestResources = undefined;
var nearestDropsite = undefined;
// first step: count how many dropsites we have that have enough resources "close" to them.
// TODO: this is a huge part of multi-base support. Count only those in the same base as the worker.
var number = 0;
var ourDropsites = m.EntityCollectionFromIds(gameState,Object.keys(baseManager.dropsites));
if (ourDropsites.length === 0)
{
m.debug ("We do not have a dropsite for " + resource + ", aborting");
return;
}
var maxPerDP = 20;
if (resource === "food")
maxPerDP = 200;
ourDropsites.forEach(function (dropsite) {
if (baseManager.dropsites[dropsite.id()][resource] && baseManager.dropsites[dropsite.id()][resource][4] > 1000
&& baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP)
number++;
});
// Allright second step, if there are any such dropsites, we pick the closest.
// we pick one with a lot of resource, or we pick the only one available (if it's high enough, otherwise we'll see with "far" below).
if (number > 0)
{
ourDropsites.forEach(function (dropsite) { //}){
if (baseManager.dropsites[dropsite.id()][resource] === undefined)
return;
if (dropsite.position() && (baseManager.dropsites[dropsite.id()][resource][4] > 1000 || (number === 1 && baseManager.dropsites[dropsite.id()][resource][4] > 200) )
&& baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) {
var dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestResources = baseManager.dropsites[dropsite.id()][resource][1];
nearestDropsite = dropsite;
}
}
});
}
// we've found no fitting dropsites close enough from us.
// So'll try with far away.
if (!nearestResources || nearestResources.length === 0) {
ourDropsites.forEach(function (dropsite) { //}){
if (baseManager.dropsites[dropsite.id()][resource] === undefined)
return;
if (dropsite.position() && baseManager.dropsites[dropsite.id()][resource][4] > 400
&& baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) {
var dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestResources = baseManager.dropsites[dropsite.id()][resource][1];
nearestDropsite = dropsite;
}
}
});
}
if (!nearestResources || nearestResources.length === 0){
if (resource === "food")
if (this.buildAnyField(gameState))
return;
if (this.unsatisfactoryResource == true)
return; // we were already not satisfied, we're still not, change not.
if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource))
return;
//m.debug ("No fitting dropsite for " + resource + " found, iterating the map.");
nearestResources = gameState.getResourceSupplies(resource);
this.unsatisfactoryResource = true;
// TODO: should try setting up dropsites.
} else {
this.unsatisfactoryResource = false;
}
if (nearestResources.length === 0){
if (resource === "food")
{
if (this.buildAnyField(gameState))
return;
if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource))
return;
m.debug("No " + resource + " found! (1)");
}
else
{
if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource))
return;
m.debug("No " + resource + " found! (1)");
}
return;
}
//m.debug("Found " + nearestResources.length + "spots for " + resource);
/*if (!nearestDropsite) {
m.debug ("No dropsite for " +resource);
return;
}*/
var supplies = [];
var nearestSupplyDist = Math.min();
var nearestSupply = undefined;
// filter resources
// TODo: add a bonus for resources with a lot of resources left, perhaps, to spread gathering?
nearestResources.forEach(function(supply) { //}){
// sanity check, perhaps sheep could be garrisoned?
if (!supply.position()) {
//m.debug ("noposition");
return;
}
if (supply.getMetadata(PlayerID, "inaccessible") === true) {
//m.debug ("inaccessible");
return;
}
if (supply.isFull(PlayerID) === true
|| (gameState.turnCache["ressGathererNB"] && gameState.turnCache["ressGathererNB"][supply.id()]
&& gameState.turnCache["ressGathererNB"][supply.id()] + supply.resourceSupplyGatherers(PlayerID).length >= supply.maxGatherers()))
return;
// Don't gather enemy farms or farms from another base
if ((!supply.isOwn(PlayerID) && supply.owner() !== 0) || (supply.isOwn(PlayerID) && supply.getMetadata(PlayerID,"base") !== self.baseID)) {
//m.debug ("enemy");
return;
}
// quickscope accessbility check.
if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position())) {
//m.debug ("nopath");
return;
}
// some simple check for chickens: if they're in a square that's inaccessible, we won't gather from them.
if (supply.footprintRadius() < 1)
{
var fakeMap = new API3.Map(gameState.sharedScript,gameState.getMap().data);
var id = fakeMap.gamePosToMapPos(supply.position())[0] + fakeMap.width*fakeMap.gamePosToMapPos(supply.position())[1];
if ( (gameState.sharedScript.passabilityClasses["pathfinderObstruction"] & gameState.getMap().data[id]) )
{
supply.setMetadata(PlayerID, "inaccessible", true)
return;
}
}
// measure the distance to the resource (largely irrelevant)
var dist = API3.SquareVectorDistance(supply.position(), ent.position());
if (dist > 4900 && supply.hasClass("Animal"))
return;
// Add on a factor for the nearest dropsite if one exists
if (nearestDropsite !== undefined ){
dist += 4*API3.SquareVectorDistance(supply.position(), nearestDropsite.position());
dist /= 5.0;
}
var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply.position());
if (territoryOwner != PlayerID && territoryOwner != 0) {
dist *= 5.0;
//return;
} else if (dist < 40000 && supply.resourceSupplyType().generic == "treasure"){
// go for treasures if they're not in enemy territory
dist /= 1000;
}
if (dist < nearestSupplyDist) {
nearestSupplyDist = dist;
nearestSupply = supply;
}
});
if (nearestSupply) {
var pos = nearestSupply.position();
// find a fitting dropsites in case we haven't already.
if (!nearestDropsite) {
ourDropsites.forEach(function (dropsite){ //}){
if (dropsite.position()){
var dist = API3.SquareVectorDistance(pos, dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestDropsite = dropsite;
}
}
});
if (!nearestDropsite)
{
m.debug ("No dropsite for " +resource);
return;
}
}
// if the resource is far away, try to build a farm instead.
var tried = false;
if (resource === "food" && API3.SquareVectorDistance(pos,this.ent.position()) > 22500)
{
tried = this.buildAnyField(gameState);
if (!tried && API3.SquareVectorDistance(pos,this.ent.position()) > 62500) {
// TODO: ought to change behavior here.
return; // wait. a farm should appear.
}
}
if (!tried) {
if (!gameState.turnCache["ressGathererNB"])
{
gameState.turnCache["ressGathererNB"] = {};
gameState.turnCache["ressGathererNB"][nearestSupply.id()] = 1;
} else if (!gameState.turnCache["ressGathererNB"][nearestSupply.id()])
gameState.turnCache["ressGathererNB"][nearestSupply.id()] = 1;
else
gameState.turnCache["ressGathererNB"][nearestSupply.id()]++;
this.maxApproachTime = Math.max(30000, API3.VectorDistance(pos,this.ent.position()) * 5000);
this.startApproachingResourceAmount = ent.resourceSupplyAmount();
this.startEnt = ent.id();
ent.gather(nearestSupply);
ent.setMetadata(PlayerID, "target-foundation", undefined);
}
} else {
if (resource === "food" && this.buildAnyField(gameState))
return;
if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource))
return;
if (resource !== "food")
m.debug("No " + resource + " found! (2)");
// If we had a fitting closest dropsite with a lot of resources, mark it as not good. It means it's probably full. Then retry.
// it'll be resetted next time it's counted anyway.
if (nearestDropsite && nearestDropsite.getMetadata(PlayerID, "resource-quantity-" +resource)+nearestDropsite.getMetadata(PlayerID, "resource-quantity-far-" +resource) > 400)
{
nearestDropsite.setMetadata(PlayerID, "resource-quantity-" +resource, 0);
nearestDropsite.setMetadata(PlayerID, "resource-quantity-far-" +resource, 0);
this.startGathering(baseManager, gameState);
}
}
};
// Makes the worker deposit the currently carried resources at the closest dropsite
m.Worker.prototype.returnResources = function(gameState){
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){
return true; // assume we're OK.
}
var resource = this.ent.resourceCarrying()[0].type;
var self = this;
if (!this.ent.position()){
// TODO: work out what to do when entity has no position
return true;
}
var closestDropsite = undefined;
var dist = Math.min();
gameState.getOwnDropsites(resource).forEach(function(dropsite){
if (dropsite.position()){
var d = API3.SquareVectorDistance(self.ent.position(), dropsite.position());
if (d < dist){
dist = d;
closestDropsite = dropsite;
}
}
});
if (!closestDropsite){
m.debug("No dropsite found to deposit " + resource);
return false;
}
this.ent.returnResources(closestDropsite);
return true;
};
m.Worker.prototype.startHunting = function(gameState, baseManager){
var ent = this.ent;
if (!ent.position() || !baseManager.isHunting)
return;
// So here we're doing it basic. We check what we can hunt, we hunt it. No fancies.
var resources = gameState.getResourceSupplies("food");
if (resources.length === 0){
m.debug("No food found to hunt!");
return;
}
var supplies = [];
var nearestSupplyDist = Math.min();
var nearestSupply = undefined;
resources.forEach(function(supply) { //}){
if (!supply.position())
return;
if (supply.getMetadata(PlayerID, "inaccessible") === true)
return;
if (supply.isFull(PlayerID) === true)
return;
if (!supply.hasClass("Animal"))
return;
// measure the distance to the resource
var dist = API3.SquareVectorDistance(supply.position(), ent.position());
var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply.position());
if (territoryOwner != PlayerID && territoryOwner != 0) {
dist *= 3.0;
}
// quickscope accessbility check
if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position(),false, true))
return;
if (dist < nearestSupplyDist) {
nearestSupplyDist = dist;
nearestSupply = supply;
}
});
if (nearestSupply) {
var pos = nearestSupply.position();
var nearestDropsite = 0;
var minDropsiteDist = 1000000;
// find a fitting dropsites in case we haven't already.
gameState.getOwnDropsites("food").forEach(function (dropsite){ //}){
if (dropsite.position()){
var dist = API3.SquareVectorDistance(pos, dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestDropsite = dropsite;
}
}
});
if (!nearestDropsite)
{
baseManager.isHunting = false;
ent.setMetadata(PlayerID, "role", undefined);
m.debug ("No dropsite for hunting food");
return;
}
if (minDropsiteDist > 35000) {
baseManager.isHunting = false;
ent.setMetadata(PlayerID, "role", undefined);
} else {
ent.gather(nearestSupply);
ent.setMetadata(PlayerID, "target-foundation", undefined);
}
} else {
baseManager.isHunting = false;
ent.setMetadata(PlayerID, "role", undefined);
m.debug("No food found for hunting! (2)");
}
};
m.Worker.prototype.getResourceType = function(type){
if (!type || !type.generic){
return undefined;
}
if (type.generic === "treasure"){
return type.specific;
}else{
return type.generic;
}
};
m.Worker.prototype.getGatherRate = function(gameState) {
if (this.ent.getMetadata(PlayerID,"subrole") !== "gatherer")
return 0;
var rates = this.ent.resourceGatherRates();
if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0]["target"])
{
var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]);
if (!ress)
return 0;
var type = ress.resourceSupplyType();
if (type.generic == "treasure")
return 1000;
var tstring = type.generic + "." + type.specific;
//m.debug (+rates[tstring] + " for " + tstring + " for " + this.ent._templateName);
if (rates[tstring])
return rates[tstring];
return 0;
}
return 0;
};
m.Worker.prototype.buildAnyField = function(gameState){
var self = this;
var okay = false;
var foundations = gameState.getOwnFoundations().filter(API3.Filters.byMetadata(PlayerID,"base",this.baseID));
foundations.filterNearest(this.ent.position(), foundations.length);
foundations.forEach(function (found) {
if (found._template.BuildRestrictions.Category === "Field" && !okay) {
self.ent.repair(found);
okay = true;
return;
}
});
if (!okay)
{
var foundations = gameState.getOwnFoundations();
foundations.filterNearest(this.ent.position(), foundations.length);
foundations.forEach(function (found) {
if (found._template.BuildRestrictions.Category === "Field" && !okay) {
self.ent.repair(found);
self.ent.setMetadata(PlayerID,"base", found.getMetadata(PlayerID,"base"));
okay = true;
return;
}
});
}
return okay;
};
return m;
}(AEGIS);