Removes older AIs and APIs.
For the sake of it, we should have a minimal AI sometimes. This was SVN commit r14553.
This commit is contained in:
parent
ede4f32bf2
commit
a6032ddd3f
@ -1,311 +0,0 @@
|
|||||||
var PlayerID = -1;
|
|
||||||
|
|
||||||
function BaseAI(settings)
|
|
||||||
{
|
|
||||||
if (!settings)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Make some properties non-enumerable, so they won't be serialised
|
|
||||||
Object.defineProperty(this, "_player", {value: settings.player, enumerable: false});
|
|
||||||
Object.defineProperty(this, "_templates", {value: settings.templates, enumerable: false});
|
|
||||||
Object.defineProperty(this, "_derivedTemplates", {value: {}, enumerable: false});
|
|
||||||
|
|
||||||
this._entityMetadata = {};
|
|
||||||
|
|
||||||
this._entityCollections = [];
|
|
||||||
this._entityCollectionsByDynProp = {};
|
|
||||||
this._entityCollectionsUID = 0;
|
|
||||||
|
|
||||||
this.turn = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return a simple object (using no classes etc) that will be serialized
|
|
||||||
//into saved games
|
|
||||||
BaseAI.prototype.Serialize = function()
|
|
||||||
{
|
|
||||||
var rawEntities = {};
|
|
||||||
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
rawEntities[id] = this._entities[id]._entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
_rawEntities: rawEntities,
|
|
||||||
_entityMetadata: this._entityMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: ought to get the AI script subclass to serialize its own state
|
|
||||||
};
|
|
||||||
|
|
||||||
//Called after the constructor when loading a saved game, with 'data' being
|
|
||||||
//whatever Serialize() returned
|
|
||||||
BaseAI.prototype.Deserialize = function(data)
|
|
||||||
{
|
|
||||||
var rawEntities = data._rawEntities;
|
|
||||||
this._entityMetadata = data._entityMetadata;
|
|
||||||
this._entities = {}
|
|
||||||
|
|
||||||
for (var id in rawEntities)
|
|
||||||
{
|
|
||||||
this._entities[id] = new Entity(this, rawEntities[id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: ought to get the AI script subclass to deserialize its own state
|
|
||||||
};
|
|
||||||
|
|
||||||
// Components that will be disabled in foundation entity templates.
|
|
||||||
// (This is a bit yucky and fragile since it's the inverse of
|
|
||||||
// CCmpTemplateManager::CopyFoundationSubset and only includes components
|
|
||||||
// that our EntityTemplate class currently uses.)
|
|
||||||
var g_FoundationForbiddenComponents = {
|
|
||||||
"ProductionQueue": 1,
|
|
||||||
"ResourceSupply": 1,
|
|
||||||
"ResourceDropsite": 1,
|
|
||||||
"GarrisonHolder": 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Components that will be disabled in resource entity templates.
|
|
||||||
// Roughly the inverse of CCmpTemplateManager::CopyResourceSubset.
|
|
||||||
var g_ResourceForbiddenComponents = {
|
|
||||||
"Cost": 1,
|
|
||||||
"Decay": 1,
|
|
||||||
"Health": 1,
|
|
||||||
"UnitAI": 1,
|
|
||||||
"UnitMotion": 1,
|
|
||||||
"Vision": 1
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.GetTemplate = function(name)
|
|
||||||
{
|
|
||||||
if (this._templates[name])
|
|
||||||
return this._templates[name];
|
|
||||||
|
|
||||||
if (this._derivedTemplates[name])
|
|
||||||
return this._derivedTemplates[name];
|
|
||||||
|
|
||||||
// If this is a foundation template, construct it automatically
|
|
||||||
if (name.indexOf("foundation|") !== -1)
|
|
||||||
{
|
|
||||||
var base = this.GetTemplate(name.substr(11));
|
|
||||||
|
|
||||||
var foundation = {};
|
|
||||||
for (var key in base)
|
|
||||||
if (!g_FoundationForbiddenComponents[key])
|
|
||||||
foundation[key] = base[key];
|
|
||||||
|
|
||||||
this._derivedTemplates[name] = foundation;
|
|
||||||
return foundation;
|
|
||||||
}
|
|
||||||
else if (name.indexOf("resource|") !== -1)
|
|
||||||
{
|
|
||||||
var base = this.GetTemplate(name.substr(9));
|
|
||||||
|
|
||||||
var resource = {};
|
|
||||||
for (var key in base)
|
|
||||||
if (!g_ResourceForbiddenComponents[key])
|
|
||||||
resource[key] = base[key];
|
|
||||||
|
|
||||||
this._derivedTemplates[name] = resource;
|
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
error("Tried to retrieve invalid template '"+name+"'");
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.HandleMessage = function(state, playerID)
|
|
||||||
{
|
|
||||||
PlayerID = playerID;
|
|
||||||
if (!this._entities)
|
|
||||||
{
|
|
||||||
// Do a (shallow) clone of all the initial entity properties (in order
|
|
||||||
// to copy into our own script context and minimise cross-context
|
|
||||||
// weirdness)
|
|
||||||
this._entities = {};
|
|
||||||
for (var id in state.entities)
|
|
||||||
{
|
|
||||||
this._entities[id] = new Entity(this, state.entities[id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.ApplyEntitiesDelta(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStart("HandleMessage setup");
|
|
||||||
|
|
||||||
this.entities = new EntityCollection(this, this._entities);
|
|
||||||
this.events = state.events;
|
|
||||||
this.passabilityClasses = state.passabilityClasses;
|
|
||||||
this.passabilityMap = state.passabilityMap;
|
|
||||||
this.player = this._player;
|
|
||||||
this.playerData = state.players[this._player];
|
|
||||||
this.templates = this._templates;
|
|
||||||
this.territoryMap = state.territoryMap;
|
|
||||||
this.timeElapsed = state.timeElapsed;
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
this.OnUpdate();
|
|
||||||
|
|
||||||
// Clean up temporary properties, so they don't disturb the serializer
|
|
||||||
delete this.entities;
|
|
||||||
delete this.events;
|
|
||||||
delete this.passabilityClasses;
|
|
||||||
delete this.passabilityMap;
|
|
||||||
delete this.player;
|
|
||||||
delete this.playerData;
|
|
||||||
delete this.templates;
|
|
||||||
delete this.territoryMap;
|
|
||||||
delete this.timeElapsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.ApplyEntitiesDelta = function(state)
|
|
||||||
{
|
|
||||||
Engine.ProfileStart("ApplyEntitiesDelta");
|
|
||||||
|
|
||||||
for each (var evt in state.events)
|
|
||||||
{
|
|
||||||
if (evt.type == "Create")
|
|
||||||
{
|
|
||||||
if (! state.entities[evt.msg.entity])
|
|
||||||
{
|
|
||||||
continue; // Sometimes there are things like foundations which get destroyed too fast
|
|
||||||
}
|
|
||||||
|
|
||||||
this._entities[evt.msg.entity] = new Entity(this, state.entities[evt.msg.entity]);
|
|
||||||
|
|
||||||
// Update all the entity collections since the create operation affects static properties as well as dynamic
|
|
||||||
for each (var entCollection in this._entityCollections)
|
|
||||||
{
|
|
||||||
entCollection.updateEnt(this._entities[evt.msg.entity]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (evt.type == "Destroy")
|
|
||||||
{
|
|
||||||
if (!this._entities[evt.msg.entity])
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// The entity was destroyed but its data may still be useful, so
|
|
||||||
// remember the entity and this AI's metadata concerning it
|
|
||||||
evt.msg.metadata = (evt.msg.metadata || []);
|
|
||||||
evt.msg.entityObj = (evt.msg.entityObj || this._entities[evt.msg.entity]);
|
|
||||||
evt.msg.metadata[this._player] = this._entityMetadata[evt.msg.entity];
|
|
||||||
|
|
||||||
for each (var entCol in this._entityCollections)
|
|
||||||
{
|
|
||||||
entCol.removeEnt(this._entities[evt.msg.entity]);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this._entities[evt.msg.entity];
|
|
||||||
delete this._entityMetadata[evt.msg.entity];
|
|
||||||
}
|
|
||||||
else if (evt.type == "TrainingFinished")
|
|
||||||
{
|
|
||||||
// Apply metadata stored in training queues, but only if they
|
|
||||||
// look like they were added by us
|
|
||||||
if (evt.msg.owner === this._player)
|
|
||||||
{
|
|
||||||
for each (var ent in evt.msg.entities)
|
|
||||||
{
|
|
||||||
for (var key in evt.msg.metadata)
|
|
||||||
{
|
|
||||||
this.setMetadata(this._entities[ent], key, evt.msg.metadata[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var id in state.entities)
|
|
||||||
{
|
|
||||||
var changes = state.entities[id];
|
|
||||||
|
|
||||||
for (var prop in changes)
|
|
||||||
{
|
|
||||||
this._entities[id]._entity[prop] = changes[prop];
|
|
||||||
this.updateEntityCollections(prop, this._entities[id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.OnUpdate = function()
|
|
||||||
{ // AIs override this function
|
|
||||||
// They should do at least this.turn++;
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.chat = function(message)
|
|
||||||
{
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "chat", "message": message});
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.registerUpdatingEntityCollection = function(entCollection)
|
|
||||||
{
|
|
||||||
entCollection.setUID(this._entityCollectionsUID);
|
|
||||||
this._entityCollections.push(entCollection);
|
|
||||||
|
|
||||||
for each (var prop in entCollection.dynamicProperties())
|
|
||||||
{
|
|
||||||
this._entityCollectionsByDynProp[prop] = this._entityCollectionsByDynProp[prop] || [];
|
|
||||||
this._entityCollectionsByDynProp[prop].push(entCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._entityCollectionsUID++;
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.removeUpdatingEntityCollection = function(entCollection)
|
|
||||||
{
|
|
||||||
for (var i in this._entityCollections)
|
|
||||||
{
|
|
||||||
if (this._entityCollections[i].getUID() === entCollection.getUID())
|
|
||||||
{
|
|
||||||
this._entityCollections.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for each (var prop in entCollection.dynamicProperties())
|
|
||||||
{
|
|
||||||
for (var i in this._entityCollectionsByDynProp[prop])
|
|
||||||
{
|
|
||||||
if (this._entityCollectionsByDynProp[prop][i].getUID() === entCollection.getUID())
|
|
||||||
{
|
|
||||||
this._entityCollectionsByDynProp[prop].splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.updateEntityCollections = function(property, ent)
|
|
||||||
{
|
|
||||||
if (this._entityCollectionsByDynProp[property] !== undefined)
|
|
||||||
{
|
|
||||||
for each (var entCollection in this._entityCollectionsByDynProp[property])
|
|
||||||
{
|
|
||||||
entCollection.updateEnt(ent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseAI.prototype.setMetadata = function(ent, key, value)
|
|
||||||
{
|
|
||||||
var metadata = this._entityMetadata[ent.id()];
|
|
||||||
if (!metadata)
|
|
||||||
metadata = this._entityMetadata[ent.id()] = {};
|
|
||||||
metadata[key] = value;
|
|
||||||
|
|
||||||
this.updateEntityCollections('metadata', ent);
|
|
||||||
this.updateEntityCollections('metadata.' + key, ent);
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseAI.prototype.getMetadata = function(ent, key)
|
|
||||||
{
|
|
||||||
var metadata = this._entityMetadata[ent.id()];
|
|
||||||
|
|
||||||
if (!metadata || !(key in metadata))
|
|
||||||
return undefined;
|
|
||||||
return metadata[key];
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides a nicer syntax for defining classes,
|
|
||||||
* with support for OO-style inheritance.
|
|
||||||
*/
|
|
||||||
function Class(data)
|
|
||||||
{
|
|
||||||
var ctor;
|
|
||||||
if (data._init)
|
|
||||||
ctor = data._init;
|
|
||||||
else
|
|
||||||
ctor = function() { };
|
|
||||||
|
|
||||||
if (data._super)
|
|
||||||
{
|
|
||||||
ctor.prototype = { "__proto__": data._super.prototype };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var key in data)
|
|
||||||
{
|
|
||||||
ctor.prototype[key] = data[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test inheritance:
|
|
||||||
var A = Class({foo:1, bar:10});
|
|
||||||
print((new A).foo+" "+(new A).bar+"\n");
|
|
||||||
var B = Class({foo:2, bar:20});
|
|
||||||
print((new A).foo+" "+(new A).bar+"\n");
|
|
||||||
print((new B).foo+" "+(new B).bar+"\n");
|
|
||||||
var C = Class({_super:A, foo:3});
|
|
||||||
print((new A).foo+" "+(new A).bar+"\n");
|
|
||||||
print((new B).foo+" "+(new B).bar+"\n");
|
|
||||||
print((new C).foo+" "+(new C).bar+"\n");
|
|
||||||
//*/
|
|
@ -1,454 +0,0 @@
|
|||||||
var EntityTemplate = Class({
|
|
||||||
|
|
||||||
_init: function(template)
|
|
||||||
{
|
|
||||||
this._template = template;
|
|
||||||
},
|
|
||||||
|
|
||||||
rank: function() {
|
|
||||||
if (!this._template.Identity)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Identity.Rank;
|
|
||||||
},
|
|
||||||
|
|
||||||
classes: function() {
|
|
||||||
if (!this._template.Identity || !this._template.Identity.Classes || !this._template.Identity.Classes._string)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Identity.Classes._string.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasClass: function(name) {
|
|
||||||
var classes = this.classes();
|
|
||||||
return (classes && classes.indexOf(name) != -1);
|
|
||||||
},
|
|
||||||
|
|
||||||
civ: function() {
|
|
||||||
if (!this._template.Identity)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Identity.Civ;
|
|
||||||
},
|
|
||||||
|
|
||||||
cost: function() {
|
|
||||||
if (!this._template.Cost)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
var ret = {};
|
|
||||||
for (var type in this._template.Cost.Resources)
|
|
||||||
ret[type] = +this._template.Cost.Resources[type];
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the radius of a circle surrounding this entity's
|
|
||||||
* obstruction shape, or undefined if no obstruction.
|
|
||||||
*/
|
|
||||||
obstructionRadius: function() {
|
|
||||||
if (!this._template.Obstruction)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
if (this._template.Obstruction.Static)
|
|
||||||
{
|
|
||||||
var w = +this._template.Obstruction.Static["@width"];
|
|
||||||
var h = +this._template.Obstruction.Static["@depth"];
|
|
||||||
return Math.sqrt(w*w + h*h) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._template.Obstruction.Unit)
|
|
||||||
return +this._template.Obstruction.Unit["@radius"];
|
|
||||||
|
|
||||||
return 0; // this should never happen
|
|
||||||
},
|
|
||||||
|
|
||||||
maxHitpoints: function()
|
|
||||||
{
|
|
||||||
if (this._template.Health !== undefined)
|
|
||||||
return this._template.Health.Max;
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
isHealable: function()
|
|
||||||
{
|
|
||||||
if (this._template.Health !== undefined)
|
|
||||||
return this._template.Health.Unhealable !== "true";
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
isRepairable: function()
|
|
||||||
{
|
|
||||||
if (this._template.Health !== undefined)
|
|
||||||
return this._template.Health.Repairable === "true";
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
armourStrengths: function() {
|
|
||||||
if (!this._template.Armour)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
hack: +this._template.Armour.Hack,
|
|
||||||
pierce: +this._template.Armour.Pierce,
|
|
||||||
crush: +this._template.Armour.Crush
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
attackTypes: function() {
|
|
||||||
if (!this._template.Attack)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
var ret = [];
|
|
||||||
for (var type in this._template.Attack)
|
|
||||||
ret.push(type);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
attackRange: function(type) {
|
|
||||||
if (!this._template.Attack || !this._template.Attack[type])
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
max: +this._template.Attack[type].MaxRange,
|
|
||||||
min: +(this._template.Attack[type].MinRange || 0)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
attackStrengths: function(type) {
|
|
||||||
if (!this._template.Attack || !this._template.Attack[type])
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
hack: +(this._template.Attack[type].Hack || 0),
|
|
||||||
pierce: +(this._template.Attack[type].Pierce || 0),
|
|
||||||
crush: +(this._template.Attack[type].Crush || 0)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
attackTimes: function(type) {
|
|
||||||
if (!this._template.Attack || !this._template.Attack[type])
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
prepare: +(this._template.Attack[type].PrepareTime || 0),
|
|
||||||
repeat: +(this._template.Attack[type].RepeatTime || 1000)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
buildableEntities: function() {
|
|
||||||
if (!this._template.Builder)
|
|
||||||
return undefined;
|
|
||||||
if (!this._template.Builder.Entities._string)
|
|
||||||
return [];
|
|
||||||
var civ = this.civ();
|
|
||||||
var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
|
|
||||||
return templates; // TODO: map to Entity?
|
|
||||||
},
|
|
||||||
|
|
||||||
trainableEntities: function() {
|
|
||||||
if (!this._template.ProductionQueue || !this._template.ProductionQueue.Entities || !this._template.ProductionQueue.Entities._string)
|
|
||||||
return undefined;
|
|
||||||
var civ = this.civ();
|
|
||||||
var templates = this._template.ProductionQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
|
|
||||||
return templates;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceSupplyType: function() {
|
|
||||||
if (!this._template.ResourceSupply)
|
|
||||||
return undefined;
|
|
||||||
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
|
|
||||||
return { "generic": type, "specific": subtype };
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceSupplyMax: function() {
|
|
||||||
if (!this._template.ResourceSupply)
|
|
||||||
return undefined;
|
|
||||||
return +this._template.ResourceSupply.Amount;
|
|
||||||
},
|
|
||||||
|
|
||||||
maxGatherers: function()
|
|
||||||
{
|
|
||||||
if (this._template.ResourceSupply !== undefined)
|
|
||||||
return this._template.ResourceSupply.MaxGatherers;
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceGatherRates: function() {
|
|
||||||
if (!this._template.ResourceGatherer)
|
|
||||||
return undefined;
|
|
||||||
var ret = {};
|
|
||||||
for (var r in this._template.ResourceGatherer.Rates)
|
|
||||||
ret[r] = this._template.ResourceGatherer.Rates[r] * this._template.ResourceGatherer.BaseSpeed;
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceDropsiteTypes: function() {
|
|
||||||
if (!this._template.ResourceDropsite)
|
|
||||||
return undefined;
|
|
||||||
return this._template.ResourceDropsite.Types.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
garrisonableClasses: function() {
|
|
||||||
if (!this._template.GarrisonHolder || !this._template.GarrisonHolder.List._string)
|
|
||||||
return undefined;
|
|
||||||
return this._template.GarrisonHolder.List._string.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether this is an animal that is too difficult to hunt.
|
|
||||||
* (Currently this includes all non-domestic animals.)
|
|
||||||
*/
|
|
||||||
isUnhuntable: function() {
|
|
||||||
if (!this._template.UnitAI || !this._template.UnitAI.NaturalBehaviour)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Ideally other animals should be huntable, but e.g. skittish animals
|
|
||||||
// must be hunted by ranged units, and some animals may be too tough.
|
|
||||||
return (this._template.UnitAI.NaturalBehaviour != "domestic");
|
|
||||||
},
|
|
||||||
|
|
||||||
buildCategory: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Category)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.Category;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildDistance: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Distance)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.Distance;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildPlacementType: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.PlacementType)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.PlacementType;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildTerritories: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Territory)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.Territory.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasBuildTerritory: function(territory) {
|
|
||||||
var territories = this.buildTerritories();
|
|
||||||
return (territories && territories.indexOf(territory) != -1);
|
|
||||||
},
|
|
||||||
|
|
||||||
visionRange: function() {
|
|
||||||
if (!this._template.Vision)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Vision.Range;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var Entity = Class({
|
|
||||||
_super: EntityTemplate,
|
|
||||||
|
|
||||||
_init: function(baseAI, entity)
|
|
||||||
{
|
|
||||||
this._super.call(this, baseAI.GetTemplate(entity.template));
|
|
||||||
|
|
||||||
this._ai = baseAI;
|
|
||||||
this._templateName = entity.template;
|
|
||||||
this._entity = entity;
|
|
||||||
},
|
|
||||||
|
|
||||||
toString: function() {
|
|
||||||
return "[Entity " + this.id() + " " + this.templateName() + "]";
|
|
||||||
},
|
|
||||||
|
|
||||||
id: function() {
|
|
||||||
return this._entity.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
templateName: function() {
|
|
||||||
return this._templateName;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns extra data that the AI scripts have associated with this entity,
|
|
||||||
* for arbitrary local annotations.
|
|
||||||
* (This data is not shared with any other AI scripts.)
|
|
||||||
*/
|
|
||||||
getMetadata: function(key) {
|
|
||||||
return this._ai.getMetadata(this, key);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets extra data to be associated with this entity.
|
|
||||||
*/
|
|
||||||
setMetadata: function(key, value) {
|
|
||||||
this._ai.setMetadata(this, key, value);
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteMetadata: function() {
|
|
||||||
delete this._ai._entityMetadata[this.id()];
|
|
||||||
},
|
|
||||||
|
|
||||||
position: function() { return this._entity.position; },
|
|
||||||
|
|
||||||
isIdle: function() {
|
|
||||||
if (typeof this._entity.idle === "undefined")
|
|
||||||
return undefined;
|
|
||||||
return this._entity.idle;
|
|
||||||
},
|
|
||||||
|
|
||||||
unitAIState: function() { return this._entity.unitAIState; },
|
|
||||||
unitAIOrderData: function() { return this._entity.unitAIOrderData; },
|
|
||||||
hitpoints: function() { if (this._entity.hitpoints !== undefined) return this._entity.hitpoints; return undefined; },
|
|
||||||
isHurt: function() { return this.hitpoints() < this.maxHitpoints(); },
|
|
||||||
needsHeal: function() { return this.isHurt() && this.isHealable(); },
|
|
||||||
needsRepair: function() { return this.isHurt() && this.isRepairable(); },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current training queue state, of the form
|
|
||||||
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
|
|
||||||
*/
|
|
||||||
trainingQueue: function() {
|
|
||||||
var queue = this._entity.trainingQueue;
|
|
||||||
return queue;
|
|
||||||
},
|
|
||||||
|
|
||||||
trainingQueueTime: function() {
|
|
||||||
var queue = this._entity.trainingQueue;
|
|
||||||
if (!queue)
|
|
||||||
return undefined;
|
|
||||||
// TODO: compute total time for units in training queue
|
|
||||||
return queue.length;
|
|
||||||
},
|
|
||||||
|
|
||||||
foundationProgress: function() {
|
|
||||||
if (typeof this._entity.foundationProgress === "undefined")
|
|
||||||
return undefined;
|
|
||||||
return this._entity.foundationProgress;
|
|
||||||
},
|
|
||||||
|
|
||||||
owner: function() {
|
|
||||||
return this._entity.owner;
|
|
||||||
},
|
|
||||||
isOwn: function() {
|
|
||||||
if (typeof this._entity.owner === "undefined")
|
|
||||||
return false;
|
|
||||||
return this._entity.owner === this._ai._player;
|
|
||||||
},
|
|
||||||
isFriendly: function() {
|
|
||||||
return this.isOwn(); // TODO: diplomacy
|
|
||||||
},
|
|
||||||
isEnemy: function() {
|
|
||||||
return !this.isOwn(); // TODO: diplomacy
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceSupplyAmount: function() {
|
|
||||||
if(this._entity.resourceSupplyAmount === undefined)
|
|
||||||
return undefined;
|
|
||||||
return this._entity.resourceSupplyAmount;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceSupplyGatherers: function()
|
|
||||||
{
|
|
||||||
if (this._entity.resourceSupplyGatherers !== undefined)
|
|
||||||
return this._entity.resourceSupplyGatherers;
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
|
|
||||||
isFull: function()
|
|
||||||
{
|
|
||||||
if (this._entity.resourceSupplyGatherers !== undefined)
|
|
||||||
return (this.maxGatherers() === this._entity.resourceSupplyGatherers.length);
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceCarrying: function() {
|
|
||||||
if(this._entity.resourceCarrying === undefined)
|
|
||||||
return undefined;
|
|
||||||
return this._entity.resourceCarrying;
|
|
||||||
},
|
|
||||||
|
|
||||||
garrisoned: function() { return new EntityCollection(this._ai, this._entity.garrisoned); },
|
|
||||||
|
|
||||||
// TODO: visibility
|
|
||||||
|
|
||||||
move: function(x, z, queued) {
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
garrison: function(target) {
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
attack: function(unitId) {
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "attack", "entities": [this.id()], "target": unitId, "queued": false});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
gather: function(target, queued) {
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
repair: function(target, queued) {
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
returnResources: function(target, queued) {
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: function() {
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "delete-entities", "entities": [this.id()]});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
train: function(type, count, metadata)
|
|
||||||
{
|
|
||||||
var trainable = this.trainableEntities();
|
|
||||||
if (!trainable)
|
|
||||||
{
|
|
||||||
error("Called train("+type+", "+count+") on non-training entity "+this);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (trainable.indexOf(type) === -1)
|
|
||||||
{
|
|
||||||
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.PostCommand(PlayerID, {
|
|
||||||
"type": "train",
|
|
||||||
"entities": [this.id()],
|
|
||||||
"template": type,
|
|
||||||
"count": count,
|
|
||||||
"metadata": metadata
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
construct: function(template, x, z, angle) {
|
|
||||||
// TODO: verify this unit can construct this, just for internal
|
|
||||||
// sanity-checking and error reporting
|
|
||||||
|
|
||||||
Engine.PostCommand(PlayerID, {
|
|
||||||
"type": "construct",
|
|
||||||
"entities": [this.id()],
|
|
||||||
"template": template,
|
|
||||||
"x": x,
|
|
||||||
"z": z,
|
|
||||||
"angle": angle,
|
|
||||||
"autorepair": false,
|
|
||||||
"autocontinue": false,
|
|
||||||
"queued": false
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
|||||||
function EntityCollection(baseAI, entities, filters)
|
|
||||||
{
|
|
||||||
this._ai = baseAI;
|
|
||||||
this._entities = entities;
|
|
||||||
|
|
||||||
this._filters = filters || [];
|
|
||||||
|
|
||||||
// Compute length lazily on demand, since it can be
|
|
||||||
// expensive for large collections
|
|
||||||
this._length = undefined;
|
|
||||||
Object.defineProperty(this, "length", {
|
|
||||||
get: function () {
|
|
||||||
if (this._length === undefined)
|
|
||||||
{
|
|
||||||
this._length = 0;
|
|
||||||
for (var id in entities)
|
|
||||||
++this._length;
|
|
||||||
}
|
|
||||||
return this._length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityCollection.prototype.toIdArray = function()
|
|
||||||
{
|
|
||||||
var ret = [];
|
|
||||||
for (var id in this._entities)
|
|
||||||
ret.push(+id);
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.toEntityArray = function()
|
|
||||||
{
|
|
||||||
var ret = [];
|
|
||||||
for each (var ent in this._entities)
|
|
||||||
ret.push(ent);
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.toString = function()
|
|
||||||
{
|
|
||||||
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the (at most) n entities nearest to targetPos.
|
|
||||||
*/
|
|
||||||
EntityCollection.prototype.filterNearest = function(targetPos, n)
|
|
||||||
{
|
|
||||||
// Compute the distance of each entity
|
|
||||||
var data = []; // [ [id, ent, distance], ... ]
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
if (ent.position())
|
|
||||||
data.push([id, ent, VectorDistance(targetPos, ent.position())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by increasing distance
|
|
||||||
data.sort(function (a, b) { return (a[2] - b[2]); });
|
|
||||||
|
|
||||||
// Extract the first n
|
|
||||||
var ret = {};
|
|
||||||
for each (var val in data.slice(0, n))
|
|
||||||
ret[val[0]] = val[1];
|
|
||||||
|
|
||||||
return new EntityCollection(this._ai, ret);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.filter = function(filter, thisp)
|
|
||||||
{
|
|
||||||
if (typeof(filter) == "function")
|
|
||||||
filter = {"func": filter, "dynamicProperties": []};
|
|
||||||
|
|
||||||
var ret = {};
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
if (filter.func.call(thisp, ent, id, this))
|
|
||||||
ret[id] = ent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EntityCollection(this._ai, ret, this._filters.concat([filter]));
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.filter_raw = function(callback, thisp)
|
|
||||||
{
|
|
||||||
var ret = {};
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
var val = this._entities[id]._entity;
|
|
||||||
if (callback.call(thisp, val, id, this))
|
|
||||||
ret[id] = ent;
|
|
||||||
}
|
|
||||||
return new EntityCollection(this._ai, ret);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.forEach = function(callback, thisp)
|
|
||||||
{
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
callback.call(thisp, ent, id, this);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.move = function(x, z, queued)
|
|
||||||
{
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued});
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.destroy = function()
|
|
||||||
{
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "delete-entities", "entities": this.toIdArray()});
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Removes an entity from the collection, returns true if the entity was a member, false otherwise
|
|
||||||
EntityCollection.prototype.removeEnt = function(ent)
|
|
||||||
{
|
|
||||||
if (this._entities[ent.id()])
|
|
||||||
{
|
|
||||||
// Checking length may initialize it, so do it before deleting.
|
|
||||||
if (this.length !== undefined)
|
|
||||||
this._length--;
|
|
||||||
delete this._entities[ent.id()];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Adds an entity to the collection, returns true if the entity was not member, false otherwise
|
|
||||||
EntityCollection.prototype.addEnt = function(ent)
|
|
||||||
{
|
|
||||||
if (this._entities[ent.id()])
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Checking length may initialize it, so do it before adding.
|
|
||||||
if (this.length !== undefined)
|
|
||||||
this._length++;
|
|
||||||
this._entities[ent.id()] = ent;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Checks the entity against the filters, and adds or removes it appropriately, returns true if the
|
|
||||||
// entity collection was modified.
|
|
||||||
EntityCollection.prototype.updateEnt = function(ent)
|
|
||||||
{
|
|
||||||
var passesFilters = true;
|
|
||||||
for each (var filter in this._filters)
|
|
||||||
{
|
|
||||||
passesFilters = passesFilters && filter.func(ent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passesFilters)
|
|
||||||
{
|
|
||||||
return this.addEnt(ent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return this.removeEnt(ent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.registerUpdates = function()
|
|
||||||
{
|
|
||||||
this._ai.registerUpdatingEntityCollection(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.dynamicProperties = function()
|
|
||||||
{
|
|
||||||
var ret = [];
|
|
||||||
for each (var filter in this._filters)
|
|
||||||
{
|
|
||||||
ret = ret.concat(filter.dynamicProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.setUID = function(id)
|
|
||||||
{
|
|
||||||
this._UID = id;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.getUID = function()
|
|
||||||
{
|
|
||||||
return this._UID;
|
|
||||||
};
|
|
@ -1,196 +0,0 @@
|
|||||||
var Filters = {
|
|
||||||
byType: function(type){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return ent.templateName() === type;
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
byClass: function(cls){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return ent.hasClass(cls);
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
byClassesAnd: function(clsList){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
var ret = true;
|
|
||||||
for (var i in clsList){
|
|
||||||
ret = ret && ent.hasClass(clsList[i]);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
byClassesOr: function(clsList){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
var ret = false;
|
|
||||||
for (var i in clsList){
|
|
||||||
ret = ret || ent.hasClass(clsList[i]);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
byMetadata: function(key, value){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return (ent.getMetadata(key) == value);
|
|
||||||
},
|
|
||||||
"dynamicProperties": ['metadata.' + key]};
|
|
||||||
},
|
|
||||||
|
|
||||||
and: function(filter1, filter2){
|
|
||||||
return {"func": function(ent){
|
|
||||||
return filter1.func(ent) && filter2.func(ent);
|
|
||||||
},
|
|
||||||
"dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)};
|
|
||||||
},
|
|
||||||
|
|
||||||
or: function(filter1, filter2){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return filter1.func(ent) || filter2.func(ent);
|
|
||||||
},
|
|
||||||
"dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)};
|
|
||||||
},
|
|
||||||
|
|
||||||
not: function(filter){
|
|
||||||
return {"func": function(ent){
|
|
||||||
return !filter.func(ent);
|
|
||||||
},
|
|
||||||
"dynamicProperties": filter.dynamicProperties};
|
|
||||||
},
|
|
||||||
|
|
||||||
byOwner: function(owner){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return (ent.owner() === owner);
|
|
||||||
},
|
|
||||||
"dynamicProperties": ['owner']};
|
|
||||||
},
|
|
||||||
|
|
||||||
byNotOwner: function(owner){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return (ent.owner() !== owner);
|
|
||||||
},
|
|
||||||
"dynamicProperties": ['owner']};
|
|
||||||
},
|
|
||||||
|
|
||||||
byOwners: function(owners){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
for (var i in owners){
|
|
||||||
if (ent.owner() == +owners[i]){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
"dynamicProperties": ['owner']};
|
|
||||||
},
|
|
||||||
|
|
||||||
byTrainingQueue: function(){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return ent.trainingQueue();
|
|
||||||
},
|
|
||||||
"dynamicProperties": ['trainingQueue']};
|
|
||||||
},
|
|
||||||
|
|
||||||
isSoldier: function(){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return Filters.byClassesOr(["CitizenSoldier", "Super"])(ent);
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
isIdle: function(){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
return ent.isIdle();
|
|
||||||
},
|
|
||||||
"dynamicProperties": ['idle']};
|
|
||||||
},
|
|
||||||
|
|
||||||
isFoundation: function(){
|
|
||||||
return {"func": function(ent){
|
|
||||||
return ent.foundationProgress() !== undefined;
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
byDistance: function(startPoint, dist){
|
|
||||||
return {"func": function(ent){
|
|
||||||
if (ent.position() === undefined){
|
|
||||||
return false;
|
|
||||||
}else{
|
|
||||||
return (SquareVectorDistance(startPoint, ent.position()) < dist*dist);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dynamicProperties": ['position']};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Distance filter with no auto updating, use with care
|
|
||||||
byStaticDistance: function(startPoint, dist){
|
|
||||||
return {"func": function(ent){
|
|
||||||
if (!ent.position()){
|
|
||||||
return false;
|
|
||||||
}else{
|
|
||||||
return (SquareVectorDistance(startPoint, ent.position()) < dist*dist);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
isDropsite: function(resourceType){
|
|
||||||
return {"func": function(ent){
|
|
||||||
return (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resourceType) !== -1);
|
|
||||||
},
|
|
||||||
"dynamicProperties": []};
|
|
||||||
},
|
|
||||||
|
|
||||||
byResource: function(resourceType){
|
|
||||||
return {"func" : function(ent){
|
|
||||||
var type = ent.resourceSupplyType();
|
|
||||||
if (!type)
|
|
||||||
return false;
|
|
||||||
var amount = ent.resourceSupplyMax();
|
|
||||||
if (!amount)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Skip targets that are too hard to hunt
|
|
||||||
if (ent.isUnhuntable())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// And don't go for the bloody fish! TODO: better accessibility checks
|
|
||||||
if (ent.hasClass("SeaCreature")){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't go for floating treasures since we won't be able to reach them and it kills the pathfinder.
|
|
||||||
if (ent.templateName() == "other/special_treasure_shipwreck_debris" ||
|
|
||||||
ent.templateName() == "other/special_treasure_shipwreck" ){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't gather enemy farms
|
|
||||||
if (!ent.isOwn() && ent.owner() !== 0){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ent.getMetadata("inaccessible") === true){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// too many workers trying to gather from this resource
|
|
||||||
if (ent.getMetadata("full") === true){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.generic == "treasure"){
|
|
||||||
return (resourceType == type.specific);
|
|
||||||
} else {
|
|
||||||
return (resourceType == type.generic);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dynamicProperties": [/*"resourceSupplyAmount", */"owner", "metadata.inaccessible", "metadata.full"]};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,51 +0,0 @@
|
|||||||
function VectorDistance(a, b)
|
|
||||||
{
|
|
||||||
var dx = a[0] - b[0];
|
|
||||||
var dz = a[1] - b[1];
|
|
||||||
return Math.sqrt(dx*dx + dz*dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SquareVectorDistance(a, b)//A sqrtless vector calculator, to see if that improves speed at all.
|
|
||||||
{
|
|
||||||
var dx = a[0] - b[0];
|
|
||||||
var dz = a[1] - b[1];
|
|
||||||
return (dx*dx + dz*dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MemoizeInit(obj)
|
|
||||||
{
|
|
||||||
obj._memoizeCache = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function Memoize(funcname, func)
|
|
||||||
{
|
|
||||||
return function() {
|
|
||||||
var args = funcname + '|' + Array.prototype.join.call(arguments, '|');
|
|
||||||
if (args in this._memoizeCache)
|
|
||||||
return this._memoizeCache[args];
|
|
||||||
|
|
||||||
var ret = func.apply(this, arguments);
|
|
||||||
this._memoizeCache[args] = ret;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShallowClone(obj)
|
|
||||||
{
|
|
||||||
var ret = {};
|
|
||||||
for (var k in obj)
|
|
||||||
ret[k] = obj[k];
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Picks a random element from an array
|
|
||||||
function PickRandom(list){
|
|
||||||
if (list.length === 0)
|
|
||||||
{
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return list[Math.floor(Math.random()*list.length)];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,218 +0,0 @@
|
|||||||
function BaseAI(settings)
|
|
||||||
{
|
|
||||||
if (!settings)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Copies of static engine data (not serialized)
|
|
||||||
this._player = settings.player;
|
|
||||||
this._templates = settings.templates;
|
|
||||||
this._derivedTemplates = {};
|
|
||||||
|
|
||||||
// Representation of the current world state (requires serialization)
|
|
||||||
this._rawEntities = null;
|
|
||||||
this._ownEntities = {};
|
|
||||||
this._entityMetadata = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a simple object (using no classes etc) that will be serialized
|
|
||||||
// into saved games
|
|
||||||
BaseAI.prototype.Serialize = function()
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
_rawEntities: this._rawEntities,
|
|
||||||
_ownEntities: this._ownEntities,
|
|
||||||
_entityMetadata: this._entityMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: ought to get the AI script subclass to serialize its own state
|
|
||||||
};
|
|
||||||
|
|
||||||
// Called after the constructor when loading a saved game, with 'data' being
|
|
||||||
// whatever Serialize() returned
|
|
||||||
BaseAI.prototype.Deserialize = function(data)
|
|
||||||
{
|
|
||||||
this._rawEntities = data._rawEntities;
|
|
||||||
this._ownEntities = data._ownEntities;
|
|
||||||
this._entityMetadata = data._entityMetadata;
|
|
||||||
|
|
||||||
// TODO: ought to get the AI script subclass to deserialize its own state
|
|
||||||
};
|
|
||||||
|
|
||||||
// Components that will be disabled in foundation entity templates.
|
|
||||||
// (This is a bit yucky and fragile since it's the inverse of
|
|
||||||
// CCmpTemplateManager::CopyFoundationSubset and only includes components
|
|
||||||
// that our EntityTemplate class currently uses.)
|
|
||||||
var g_FoundationForbiddenComponents = {
|
|
||||||
"ProductionQueue": 1,
|
|
||||||
"ResourceSupply": 1,
|
|
||||||
"ResourceDropsite": 1,
|
|
||||||
"GarrisonHolder": 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Components that will be disabled in resource entity templates.
|
|
||||||
// Roughly the inverse of CCmpTemplateManager::CopyResourceSubset.
|
|
||||||
var g_ResourceForbiddenComponents = {
|
|
||||||
"Cost": 1,
|
|
||||||
"Decay": 1,
|
|
||||||
"Health": 1,
|
|
||||||
"UnitAI": 1,
|
|
||||||
"UnitMotion": 1,
|
|
||||||
"Vision": 1
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.GetTemplate = function(name)
|
|
||||||
{
|
|
||||||
if (this._templates[name])
|
|
||||||
return this._templates[name];
|
|
||||||
|
|
||||||
if (this._derivedTemplates[name])
|
|
||||||
return this._derivedTemplates[name];
|
|
||||||
|
|
||||||
// If this is a foundation template, construct it automatically
|
|
||||||
if (name.indexOf("foundation|") !== -1)
|
|
||||||
{
|
|
||||||
var base = this.GetTemplate(name.substr(11));
|
|
||||||
|
|
||||||
var foundation = {};
|
|
||||||
for (var key in base)
|
|
||||||
if (!g_FoundationForbiddenComponents[key])
|
|
||||||
foundation[key] = base[key];
|
|
||||||
|
|
||||||
this._derivedTemplates[name] = foundation;
|
|
||||||
return foundation;
|
|
||||||
}
|
|
||||||
else if (name.indexOf("resource|") !== -1)
|
|
||||||
{
|
|
||||||
var base = this.GetTemplate(name.substr(9));
|
|
||||||
|
|
||||||
var resource = {};
|
|
||||||
for (var key in base)
|
|
||||||
if (!g_ResourceForbiddenComponents[key])
|
|
||||||
resource[key] = base[key];
|
|
||||||
|
|
||||||
this._derivedTemplates[name] = resource;
|
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
error("Tried to retrieve invalid template '"+name+"'");
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.HandleMessage = function(state)
|
|
||||||
{
|
|
||||||
if (!this._rawEntities)
|
|
||||||
{
|
|
||||||
// Do a (shallow) clone of all the initial entity properties (in order
|
|
||||||
// to copy into our own script context and minimise cross-context
|
|
||||||
// weirdness), and remember the entities owned by our player
|
|
||||||
this._rawEntities = {};
|
|
||||||
for (var id in state.entities)
|
|
||||||
{
|
|
||||||
var ent = state.entities[id];
|
|
||||||
|
|
||||||
this._rawEntities[id] = {};
|
|
||||||
for (var prop in ent)
|
|
||||||
this._rawEntities[id][prop] = ent[prop];
|
|
||||||
|
|
||||||
if (ent.owner === this._player)
|
|
||||||
this._ownEntities[id] = this._rawEntities[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.ApplyEntitiesDelta(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStart("HandleMessage setup");
|
|
||||||
|
|
||||||
this.entities = new EntityCollection(this, this._rawEntities);
|
|
||||||
this.events = state.events;
|
|
||||||
this.passabilityClasses = state.passabilityClasses;
|
|
||||||
this.passabilityMap = state.passabilityMap;
|
|
||||||
this.player = this._player;
|
|
||||||
this.playerData = state.players[this._player];
|
|
||||||
this.templates = this._templates;
|
|
||||||
this.territoryMap = state.territoryMap;
|
|
||||||
this.timeElapsed = state.timeElapsed;
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
this.OnUpdate();
|
|
||||||
|
|
||||||
// Clean up temporary properties, so they don't disturb the serializer
|
|
||||||
delete this.entities;
|
|
||||||
delete this.events;
|
|
||||||
delete this.passabilityClasses;
|
|
||||||
delete this.passabilityMap;
|
|
||||||
delete this.player;
|
|
||||||
delete this.playerData;
|
|
||||||
delete this.templates;
|
|
||||||
delete this.territoryMap;
|
|
||||||
delete this.timeElapsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.ApplyEntitiesDelta = function(state)
|
|
||||||
{
|
|
||||||
Engine.ProfileStart("ApplyEntitiesDelta");
|
|
||||||
|
|
||||||
for each (var evt in state.events)
|
|
||||||
{
|
|
||||||
if (evt.type == "Create")
|
|
||||||
{
|
|
||||||
this._rawEntities[evt.msg.entity] = {};
|
|
||||||
}
|
|
||||||
else if (evt.type == "Destroy")
|
|
||||||
{
|
|
||||||
// The entity was destroyed but its data may still be useful, so
|
|
||||||
// remember the raw entity and this AI's metadata concerning it
|
|
||||||
evt.msg.metadata = (evt.msg.metadata || []);
|
|
||||||
evt.msg.rawEntity = (evt.msg.rawEntity || this._rawEntities[evt.msg.entity]);
|
|
||||||
evt.msg.metadata[this._player] = this._entityMetadata[evt.msg.entity];
|
|
||||||
|
|
||||||
delete this._rawEntities[evt.msg.entity];
|
|
||||||
delete this._entityMetadata[evt.msg.entity];
|
|
||||||
delete this._ownEntities[evt.msg.entity];
|
|
||||||
}
|
|
||||||
else if (evt.type == "TrainingFinished")
|
|
||||||
{
|
|
||||||
// Apply metadata stored in training queues, but only if they
|
|
||||||
// look like they were added by us
|
|
||||||
if (evt.msg.owner === this._player)
|
|
||||||
for each (var ent in evt.msg.entities)
|
|
||||||
this._entityMetadata[ent] = ShallowClone(evt.msg.metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var id in state.entities)
|
|
||||||
{
|
|
||||||
var changes = state.entities[id];
|
|
||||||
|
|
||||||
if ("owner" in changes)
|
|
||||||
{
|
|
||||||
var wasOurs = (this._rawEntities[id].owner !== undefined
|
|
||||||
&& this._rawEntities[id].owner === this._player);
|
|
||||||
|
|
||||||
var isOurs = (changes.owner === this._player);
|
|
||||||
|
|
||||||
if (wasOurs && !isOurs)
|
|
||||||
delete this._ownEntities[id];
|
|
||||||
else if (!wasOurs && isOurs)
|
|
||||||
this._ownEntities[id] = this._rawEntities[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var prop in changes)
|
|
||||||
this._rawEntities[id][prop] = changes[prop];
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.OnUpdate = function()
|
|
||||||
{
|
|
||||||
// AIs override this function
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseAI.prototype.chat = function(message)
|
|
||||||
{
|
|
||||||
Engine.PostCommand({"type": "chat", "message": message});
|
|
||||||
};
|
|
@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides a nicer syntax for defining classes,
|
|
||||||
* with support for OO-style inheritance.
|
|
||||||
*/
|
|
||||||
function Class(data)
|
|
||||||
{
|
|
||||||
var ctor;
|
|
||||||
if (data._init)
|
|
||||||
ctor = data._init;
|
|
||||||
else
|
|
||||||
ctor = function() { };
|
|
||||||
|
|
||||||
if (data._super)
|
|
||||||
{
|
|
||||||
ctor.prototype = { "__proto__": data._super.prototype };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var key in data)
|
|
||||||
{
|
|
||||||
ctor.prototype[key] = data[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test inheritance:
|
|
||||||
var A = Class({foo:1, bar:10});
|
|
||||||
print((new A).foo+" "+(new A).bar+"\n");
|
|
||||||
var B = Class({foo:2, bar:20});
|
|
||||||
print((new A).foo+" "+(new A).bar+"\n");
|
|
||||||
print((new B).foo+" "+(new B).bar+"\n");
|
|
||||||
var C = Class({_super:A, foo:3});
|
|
||||||
print((new A).foo+" "+(new A).bar+"\n");
|
|
||||||
print((new B).foo+" "+(new B).bar+"\n");
|
|
||||||
print((new C).foo+" "+(new C).bar+"\n");
|
|
||||||
//*/
|
|
@ -1,453 +0,0 @@
|
|||||||
var EntityTemplate = Class({
|
|
||||||
|
|
||||||
_init: function(template) {
|
|
||||||
this._template = template;
|
|
||||||
},
|
|
||||||
|
|
||||||
rank: function() {
|
|
||||||
if (!this._template.Identity)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Identity.Rank;
|
|
||||||
},
|
|
||||||
|
|
||||||
classes: function() {
|
|
||||||
if (!this._template.Identity || !this._template.Identity.Classes || !this._template.Identity.Classes._string)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Identity.Classes._string.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasClass: function(name) {
|
|
||||||
var classes = this.classes();
|
|
||||||
return (classes && classes.indexOf(name) != -1);
|
|
||||||
},
|
|
||||||
|
|
||||||
civ: function() {
|
|
||||||
if (!this._template.Identity)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Identity.Civ;
|
|
||||||
},
|
|
||||||
|
|
||||||
cost: function() {
|
|
||||||
if (!this._template.Cost)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
var ret = {};
|
|
||||||
for (var type in this._template.Cost.Resources)
|
|
||||||
ret[type] = +this._template.Cost.Resources[type];
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the radius of a circle surrounding this entity's
|
|
||||||
* obstruction shape, or undefined if no obstruction.
|
|
||||||
*/
|
|
||||||
obstructionRadius: function() {
|
|
||||||
if (!this._template.Obstruction)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
if (this._template.Obstruction.Static)
|
|
||||||
{
|
|
||||||
var w = +this._template.Obstruction.Static["@width"];
|
|
||||||
var h = +this._template.Obstruction.Static["@depth"];
|
|
||||||
return Math.sqrt(w*w + h*h) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._template.Obstruction.Unit)
|
|
||||||
return +this._template.Obstruction.Unit["@radius"];
|
|
||||||
|
|
||||||
return 0; // this should never happen
|
|
||||||
},
|
|
||||||
|
|
||||||
maxHitpoints: function() { return this._template.Health.Max; },
|
|
||||||
isHealable: function() { return this._template.Health.Unhealable !== "true"; },
|
|
||||||
isRepairable: function() { return this._template.Health.Repairable === "true"; },
|
|
||||||
|
|
||||||
|
|
||||||
armourStrengths: function() {
|
|
||||||
if (!this._template.Armour)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
hack: +this._template.Armour.Hack,
|
|
||||||
pierce: +this._template.Armour.Pierce,
|
|
||||||
crush: +this._template.Armour.Crush
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
attackTypes: function() {
|
|
||||||
if (!this._template.Attack)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
var ret = [];
|
|
||||||
for (var type in this._template.Attack)
|
|
||||||
ret.push(type);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
attackRange: function(type) {
|
|
||||||
if (!this._template.Attack || !this._template.Attack[type])
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
max: +this._template.Attack[type].MaxRange,
|
|
||||||
min: +(this._template.Attack[type].MinRange || 0)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
attackStrengths: function(type) {
|
|
||||||
if (!this._template.Attack || !this._template.Attack[type])
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
hack: +(this._template.Attack[type].Hack || 0),
|
|
||||||
pierce: +(this._template.Attack[type].Pierce || 0),
|
|
||||||
crush: +(this._template.Attack[type].Crush || 0)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
attackTimes: function(type) {
|
|
||||||
if (!this._template.Attack || !this._template.Attack[type])
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
prepare: +(this._template.Attack[type].PrepareTime || 0),
|
|
||||||
repeat: +(this._template.Attack[type].RepeatTime || 1000)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
buildableEntities: function() {
|
|
||||||
if (!this._template.Builder)
|
|
||||||
return undefined;
|
|
||||||
if (!this._template.Builder.Entities._string)
|
|
||||||
return [];
|
|
||||||
var civ = this.civ();
|
|
||||||
var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
|
|
||||||
return templates; // TODO: map to Entity?
|
|
||||||
},
|
|
||||||
|
|
||||||
trainableEntities: function() {
|
|
||||||
if (!this._template.ProductionQueue || !this._template.ProductionQueue.Entities || !this._template.ProductionQueue.Entities._string)
|
|
||||||
return undefined;
|
|
||||||
var civ = this.civ();
|
|
||||||
var templates = this._template.ProductionQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
|
|
||||||
return templates;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceSupplyType: function() {
|
|
||||||
if (!this._template.ResourceSupply)
|
|
||||||
return undefined;
|
|
||||||
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
|
|
||||||
return { "generic": type, "specific": subtype };
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceSupplyMax: function() {
|
|
||||||
if (!this._template.ResourceSupply)
|
|
||||||
return undefined;
|
|
||||||
return +this._template.ResourceSupply.Amount;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceGatherRates: function() {
|
|
||||||
if (!this._template.ResourceGatherer)
|
|
||||||
return undefined;
|
|
||||||
var ret = {};
|
|
||||||
for (var r in this._template.ResourceGatherer.Rates)
|
|
||||||
ret[r] = this._template.ResourceGatherer.Rates[r] * this._template.ResourceGatherer.BaseSpeed;
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceDropsiteTypes: function() {
|
|
||||||
if (!this._template.ResourceDropsite)
|
|
||||||
return undefined;
|
|
||||||
return this._template.ResourceDropsite.Types.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
garrisonableClasses: function() {
|
|
||||||
if (!this._template.GarrisonHolder || !this._template.GarrisonHolder.List._string)
|
|
||||||
return undefined;
|
|
||||||
return this._template.GarrisonHolder.List._string.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
garrisonMax: function() {
|
|
||||||
if (!this._template.GarrisonHolder)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return this._template.GarrisonHolder.Max;
|
|
||||||
},
|
|
||||||
|
|
||||||
//"Population Bonus" is how much a building raises your population cap.
|
|
||||||
getPopulationBonus: function() {
|
|
||||||
if (!this._template.Cost)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return this._template.Cost.PopulationBonus;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether this is an animal that is too difficult to hunt.
|
|
||||||
* (Currently this includes all non-domestic animals.)
|
|
||||||
*/
|
|
||||||
isUnhuntable: function() {
|
|
||||||
if (!this._template.UnitAI || !this._template.UnitAI.NaturalBehaviour)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Ideally other animals should be huntable, but e.g. skittish animals
|
|
||||||
// must be hunted by ranged units, and some animals may be too tough.
|
|
||||||
return (this._template.UnitAI.NaturalBehaviour != "domestic");
|
|
||||||
},
|
|
||||||
|
|
||||||
buildCategory: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Category)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.Category;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildDistance: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Distance)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.Distance;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildPlacementType: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.PlacementType)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.PlacementType;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildTerritories: function() {
|
|
||||||
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Territory)
|
|
||||||
return undefined;
|
|
||||||
return this._template.BuildRestrictions.Territory.split(/\s+/);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasBuildTerritory: function(territory) {
|
|
||||||
var territories = this.buildTerritories();
|
|
||||||
return (territories && territories.indexOf(territory) != -1);
|
|
||||||
},
|
|
||||||
|
|
||||||
visionRange: function() {
|
|
||||||
if (!this._template.Vision)
|
|
||||||
return undefined;
|
|
||||||
return this._template.Vision.Range;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var Entity = Class({
|
|
||||||
_super: EntityTemplate,
|
|
||||||
|
|
||||||
_init: function(baseAI, entity) {
|
|
||||||
this._super.call(this, baseAI.GetTemplate(entity.template));
|
|
||||||
|
|
||||||
this._ai = baseAI;
|
|
||||||
this._templateName = entity.template;
|
|
||||||
this._entity = entity;
|
|
||||||
},
|
|
||||||
|
|
||||||
toString: function() {
|
|
||||||
return "[Entity " + this.id() + " " + this.templateName() + "]";
|
|
||||||
},
|
|
||||||
|
|
||||||
id: function() {
|
|
||||||
return this._entity.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
templateName: function() {
|
|
||||||
return this._templateName;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns extra data that the AI scripts have associated with this entity,
|
|
||||||
* for arbitrary local annotations.
|
|
||||||
* (This data is not shared with any other AI scripts.)
|
|
||||||
*/
|
|
||||||
getMetadata: function(id) {
|
|
||||||
var metadata = this._ai._entityMetadata[this.id()];
|
|
||||||
if (!metadata || !(id in metadata))
|
|
||||||
return undefined;
|
|
||||||
return metadata[id];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets extra data to be associated with this entity.
|
|
||||||
*/
|
|
||||||
setMetadata: function(id, value) {
|
|
||||||
var metadata = this._ai._entityMetadata[this.id()];
|
|
||||||
if (!metadata)
|
|
||||||
metadata = this._ai._entityMetadata[this.id()] = {};
|
|
||||||
metadata[id] = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
position: function() { return this._entity.position; },
|
|
||||||
|
|
||||||
isIdle: function() {
|
|
||||||
if (this._entity.idle === undefined)
|
|
||||||
return undefined; // Prevent warning about reference to undefined property
|
|
||||||
return this._entity.idle;
|
|
||||||
},
|
|
||||||
|
|
||||||
hitpoints: function() { return this._entity.hitpoints; },
|
|
||||||
isHurt: function() { return this.hitpoints() < this.maxHitpoints(); },
|
|
||||||
needsHeal: function() { return this.isHurt() && this.isHealable(); },
|
|
||||||
needsRepair: function() { return this.isHurt() && this.isRepairable(); },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current training queue state, of the form
|
|
||||||
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
|
|
||||||
*/
|
|
||||||
trainingQueue: function() {
|
|
||||||
var queue = this._entity.trainingQueue;
|
|
||||||
return queue;
|
|
||||||
},
|
|
||||||
|
|
||||||
trainingQueueTime: function() {
|
|
||||||
var queue = this._entity.trainingQueue;
|
|
||||||
if (!queue)
|
|
||||||
return undefined;
|
|
||||||
// TODO: compute total time for units in training queue
|
|
||||||
return queue.length;
|
|
||||||
},
|
|
||||||
|
|
||||||
foundationProgress: function() {
|
|
||||||
if (this._entity.foundationProgress === undefined)
|
|
||||||
return undefined; // Prevent warning about reference to undefined property
|
|
||||||
return this._entity.foundationProgress;
|
|
||||||
},
|
|
||||||
|
|
||||||
owner: function() {
|
|
||||||
return this._entity.owner;
|
|
||||||
},
|
|
||||||
|
|
||||||
isOwn: function() {
|
|
||||||
if (this._entity.owner === undefined)
|
|
||||||
return false;
|
|
||||||
return this._entity.owner === this._ai._player;
|
|
||||||
},
|
|
||||||
|
|
||||||
isFriendly: function() {
|
|
||||||
return this.isOwn(); // TODO: diplomacy
|
|
||||||
},
|
|
||||||
|
|
||||||
isEnemy: function() {
|
|
||||||
return !this.isOwn(); // TODO: diplomacy
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceSupplyAmount: function() { return this._entity.resourceSupplyAmount; },
|
|
||||||
|
|
||||||
resourceCarrying: function() { return this._entity.resourceCarrying; },
|
|
||||||
|
|
||||||
//Garrison related
|
|
||||||
|
|
||||||
garrisoned: function() { return new EntityCollection(this._ai, this._entity.garrisoned); },
|
|
||||||
|
|
||||||
garrisonSpaceAvailable: function()
|
|
||||||
{
|
|
||||||
return (this.garrisonMax() - this.garrisoned().length);
|
|
||||||
},
|
|
||||||
|
|
||||||
canGarrisonInto: function(target) {
|
|
||||||
var allowedClasses = target.garrisonableClasses();
|
|
||||||
// return false if the target is full or doesn't have any allowed classes
|
|
||||||
if (target.garrisonSpaceAvaliable() <=0 || allowedClasses === undefined)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check that this unit is a member of at least one of the allowed garrison classes
|
|
||||||
var hasClasses = this.classes();
|
|
||||||
for (var i = 0; i < hasClasses.length; i++)
|
|
||||||
{
|
|
||||||
var hasClass = hasClasses[i];
|
|
||||||
if (allowedClasses.indexOf(hasClass) >= 0)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: visibility
|
|
||||||
|
|
||||||
attack: function(target) {
|
|
||||||
Engine.PostCommand({"type": "attack", "entities": [this.id()], "target": target.id(), "queued": false});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
move: function(x, z, queued) {
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
gather: function(target, queued) {
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand({"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
repair: function(target, queued) {
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand({"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: function() {
|
|
||||||
Engine.PostCommand({"type": "delete-entities", "entities": [this.id()]});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
train: function(type, count, metadata) {
|
|
||||||
var trainable = this.trainableEntities();
|
|
||||||
if (!trainable)
|
|
||||||
{
|
|
||||||
error("Called train("+type+", "+count+") on non-training entity "+this);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (trainable.indexOf(type) === -1)
|
|
||||||
{
|
|
||||||
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.PostCommand({
|
|
||||||
"type": "train",
|
|
||||||
"entities": [this.id()],
|
|
||||||
"template": type,
|
|
||||||
"count": count,
|
|
||||||
"metadata": metadata
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
//ungarrison a specific unit in this building
|
|
||||||
unload: function(unit) {
|
|
||||||
if (!this._template.GarrisonHolder)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Engine.PostCommand({"type": "unload", "garrisonHolder": this.id(), "entities": [unit.id()]});
|
|
||||||
},
|
|
||||||
|
|
||||||
unloadAll: function() {
|
|
||||||
if (!this._template.GarrisonHolder)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Engine.PostCommand({"type": "unload-all", "garrisonHolder": this.id()});
|
|
||||||
},
|
|
||||||
|
|
||||||
construct: function(template, x, z, angle) {
|
|
||||||
// TODO: verify this unit can construct this, just for internal
|
|
||||||
// sanity-checking and error reporting
|
|
||||||
|
|
||||||
Engine.PostCommand({
|
|
||||||
"type": "construct",
|
|
||||||
"entities": [this.id()],
|
|
||||||
"template": template,
|
|
||||||
"x": x,
|
|
||||||
"z": z,
|
|
||||||
"angle": angle,
|
|
||||||
"autorepair": false,
|
|
||||||
"autocontinue": false,
|
|
||||||
"queued": false
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
|||||||
function EntityCollection(baseAI, entities)
|
|
||||||
{
|
|
||||||
this._ai = baseAI;
|
|
||||||
this._entities = entities;
|
|
||||||
|
|
||||||
// Compute length lazily on demand, since it can be
|
|
||||||
// expensive for large collections
|
|
||||||
var length = undefined;
|
|
||||||
Object.defineProperty(this, "length", {
|
|
||||||
get: function () {
|
|
||||||
if (length === undefined)
|
|
||||||
{
|
|
||||||
length = 0;
|
|
||||||
for (var id in entities)
|
|
||||||
++length;
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityCollection.prototype.toIdArray = function()
|
|
||||||
{
|
|
||||||
var ret = [];
|
|
||||||
for (var id in this._entities)
|
|
||||||
ret.push(+id);
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.toEntityArray = function()
|
|
||||||
{
|
|
||||||
var ret = [];
|
|
||||||
for each (var ent in this._entities)
|
|
||||||
ret.push(new Entity(this._ai, ent));
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.toString = function()
|
|
||||||
{
|
|
||||||
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the (at most) n entities nearest to targetPos.
|
|
||||||
*/
|
|
||||||
EntityCollection.prototype.filterNearest = function(targetPos, n)
|
|
||||||
{
|
|
||||||
// Compute the distance of each entity
|
|
||||||
var data = []; // [ [id, ent, distance], ... ]
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
if (ent.position)
|
|
||||||
data.push([id, ent, VectorDistance(targetPos, ent.position)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by increasing distance
|
|
||||||
data.sort(function (a, b) { return (a[2] - b[2]); });
|
|
||||||
|
|
||||||
// Extract the first n
|
|
||||||
var ret = {};
|
|
||||||
for each (var val in data.slice(0, n))
|
|
||||||
ret[val[0]] = val[1];
|
|
||||||
|
|
||||||
return new EntityCollection(this._ai, ret);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.filter = function(callback, thisp)
|
|
||||||
{
|
|
||||||
var ret = {};
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
var val = new Entity(this._ai, ent);
|
|
||||||
if (callback.call(thisp, val, id, this))
|
|
||||||
ret[id] = ent;
|
|
||||||
}
|
|
||||||
return new EntityCollection(this._ai, ret);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.filter_raw = function(callback, thisp)
|
|
||||||
{
|
|
||||||
var ret = {};
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
if (callback.call(thisp, ent, id, this))
|
|
||||||
ret[id] = ent;
|
|
||||||
}
|
|
||||||
return new EntityCollection(this._ai, ret);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.forEach = function(callback, thisp)
|
|
||||||
{
|
|
||||||
for (var id in this._entities)
|
|
||||||
{
|
|
||||||
var ent = this._entities[id];
|
|
||||||
var val = new Entity(this._ai, ent);
|
|
||||||
callback.call(thisp, val, id, this);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.move = function(x, z, queued)
|
|
||||||
{
|
|
||||||
queued = queued || false;
|
|
||||||
Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued});
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCollection.prototype.destroy = function()
|
|
||||||
{
|
|
||||||
Engine.PostCommand({"type": "delete-entities", "entities": this.toIdArray()});
|
|
||||||
return this;
|
|
||||||
};
|
|
@ -1,51 +0,0 @@
|
|||||||
function VectorDistance(a, b)
|
|
||||||
{
|
|
||||||
var dx = a[0] - b[0];
|
|
||||||
var dz = a[1] - b[1];
|
|
||||||
return Math.sqrt(dx*dx + dz*dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SquareVectorDistance(a, b)//A sqrtless vector calculator, to see if that improves speed at all.
|
|
||||||
{
|
|
||||||
var dx = a[0] - b[0];
|
|
||||||
var dz = a[1] - b[1];
|
|
||||||
return (dx*dx + dz*dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MemoizeInit(obj)
|
|
||||||
{
|
|
||||||
obj._memoizeCache = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function Memoize(funcname, func)
|
|
||||||
{
|
|
||||||
return function() {
|
|
||||||
var args = funcname + '|' + Array.prototype.join.call(arguments, '|');
|
|
||||||
if (args in this._memoizeCache)
|
|
||||||
return this._memoizeCache[args];
|
|
||||||
|
|
||||||
var ret = func.apply(this, arguments);
|
|
||||||
this._memoizeCache[args] = ret;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShallowClone(obj)
|
|
||||||
{
|
|
||||||
var ret = {};
|
|
||||||
for (var k in obj)
|
|
||||||
ret[k] = obj[k];
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Picks a random element from an array
|
|
||||||
function PickRandom(list){
|
|
||||||
if (list.length === 0)
|
|
||||||
{
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return list[Math.floor(Math.random()*list.length)];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
Engine.IncludeModule("common-api-v2");
|
|
@ -1,205 +0,0 @@
|
|||||||
function AttackMoveToCC(gameState, militaryManager){
|
|
||||||
this.minAttackSize = 20;
|
|
||||||
this.maxAttackSize = 60;
|
|
||||||
this.idList=[];
|
|
||||||
|
|
||||||
this.previousTime = 0;
|
|
||||||
this.state = "unexecuted";
|
|
||||||
|
|
||||||
this.healthRecord = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns true if the attack can be executed at the current time
|
|
||||||
AttackMoveToCC.prototype.canExecute = function(gameState, militaryManager){
|
|
||||||
var enemyStrength = militaryManager.measureEnemyStrength(gameState);
|
|
||||||
var enemyCount = militaryManager.measureEnemyCount(gameState);
|
|
||||||
|
|
||||||
// We require our army to be >= this strength
|
|
||||||
var targetStrength = enemyStrength * 1.5;
|
|
||||||
|
|
||||||
var availableCount = militaryManager.countAvailableUnits();
|
|
||||||
var availableStrength = militaryManager.measureAvailableStrength();
|
|
||||||
|
|
||||||
debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount);
|
|
||||||
debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength);
|
|
||||||
|
|
||||||
return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize)
|
|
||||||
|| availableCount >= this.maxAttackSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Executes the attack plan, after this is executed the update function will be run every turn
|
|
||||||
AttackMoveToCC.prototype.execute = function(gameState, militaryManager){
|
|
||||||
var availableCount = militaryManager.countAvailableUnits();
|
|
||||||
this.idList = militaryManager.getAvailableUnits(availableCount);
|
|
||||||
|
|
||||||
var pending = EntityCollectionFromIds(gameState, this.idList);
|
|
||||||
|
|
||||||
// Find the critical enemy buildings we could attack
|
|
||||||
var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical");
|
|
||||||
// If there are no critical structures, attack anything else that's critical
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = gameState.entities.filter(function(ent) {
|
|
||||||
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0 && ent.position());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If there's nothing, attack anything else that's less critical
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = militaryManager.getEnemyBuildings(gameState,"Town");
|
|
||||||
}
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = militaryManager.getEnemyBuildings(gameState,"Village");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a target, move to it
|
|
||||||
if (targets.length) {
|
|
||||||
// Add an attack role so the economic manager doesn't try and use them
|
|
||||||
pending.forEach(function(ent) {
|
|
||||||
ent.setMetadata("role", "attack");
|
|
||||||
});
|
|
||||||
|
|
||||||
var curPos = pending.getCentrePosition();
|
|
||||||
|
|
||||||
var target = targets.toEntityArray()[0];
|
|
||||||
this.targetPos = target.position();
|
|
||||||
|
|
||||||
// Find possible distinct paths to the enemy
|
|
||||||
var pathFinder = new PathFinder(gameState);
|
|
||||||
var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos);
|
|
||||||
if (! pathsToEnemy){
|
|
||||||
pathsToEnemy = [this.targetPos];
|
|
||||||
}
|
|
||||||
|
|
||||||
var rand = Math.floor(Math.random() * pathsToEnemy.length);
|
|
||||||
this.path = pathsToEnemy[rand];
|
|
||||||
|
|
||||||
pending.move(this.path[0][0], this.path[0][1]);
|
|
||||||
} else if (targets.length == 0 ) {
|
|
||||||
gameState.ai.gameFinished = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = "walking";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Runs every turn after the attack is executed
|
|
||||||
// This removes idle units from the attack
|
|
||||||
AttackMoveToCC.prototype.update = function(gameState, militaryManager, events){
|
|
||||||
|
|
||||||
// keep the list of units in good order by pruning ids with no corresponding entities (i.e. dead units)
|
|
||||||
var removeList = [];
|
|
||||||
var totalHealth = 0;
|
|
||||||
for (var idKey in this.idList){
|
|
||||||
var id = this.idList[idKey];
|
|
||||||
var ent = militaryManager.entity(id);
|
|
||||||
if (ent === undefined){
|
|
||||||
removeList.push(id);
|
|
||||||
}else{
|
|
||||||
if (ent.hitpoints()){
|
|
||||||
totalHealth += ent.hitpoints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var i in removeList){
|
|
||||||
this.idList.splice(this.idList.indexOf(removeList[i]),1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var units = EntityCollectionFromIds(gameState, this.idList);
|
|
||||||
|
|
||||||
if (this.path.length === 0){
|
|
||||||
var idleCount = 0;
|
|
||||||
var self = this;
|
|
||||||
units.forEach(function(ent){
|
|
||||||
if (ent.isIdle()){
|
|
||||||
if (ent.position() && VectorDistance(ent.position(), self.targetPos) > 30){
|
|
||||||
ent.move(self.targetPos[0], self.targetPos[1]);
|
|
||||||
}else{
|
|
||||||
militaryManager.unassignUnit(ent.id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deltaHealth = 0;
|
|
||||||
var deltaTime = 1;
|
|
||||||
var time = gameState.getTimeElapsed();
|
|
||||||
this.healthRecord.push([totalHealth, time]);
|
|
||||||
if (this.healthRecord.length > 1){
|
|
||||||
for (var i = this.healthRecord.length - 1; i >= 0; i--){
|
|
||||||
deltaHealth = totalHealth - this.healthRecord[i][0];
|
|
||||||
deltaTime = time - this.healthRecord[i][1];
|
|
||||||
if (this.healthRecord[i][1] < time - 5*1000){
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var numUnits = this.idList.length;
|
|
||||||
if (numUnits < 1) return;
|
|
||||||
var damageRate = -deltaHealth / deltaTime * 1000;
|
|
||||||
var centrePos = units.getCentrePosition();
|
|
||||||
if (! centrePos) return;
|
|
||||||
|
|
||||||
var idleCount = 0;
|
|
||||||
// Looks for idle units away from the formations centre
|
|
||||||
for (var idKey in this.idList){
|
|
||||||
var id = this.idList[idKey];
|
|
||||||
var ent = militaryManager.entity(id);
|
|
||||||
if (ent.isIdle()){
|
|
||||||
if (ent.position() && VectorDistance(ent.position(), centrePos) > 20){
|
|
||||||
var dist = VectorDistance(ent.position(), centrePos);
|
|
||||||
var vector = [centrePos[0] - ent.position()[0], centrePos[1] - ent.position()[1]];
|
|
||||||
vector[0] *= 10/dist;
|
|
||||||
vector[1] *= 10/dist;
|
|
||||||
ent.move(centrePos[0] + vector[0], centrePos[1] + vector[1]);
|
|
||||||
}else{
|
|
||||||
idleCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((damageRate / Math.sqrt(numUnits)) > 2){
|
|
||||||
if (this.state === "walking"){
|
|
||||||
var sumAttackerPos = [0,0];
|
|
||||||
var numAttackers = 0;
|
|
||||||
|
|
||||||
for (var key in events){
|
|
||||||
var e = events[key];
|
|
||||||
//{type:"Attacked", msg:{attacker:736, target:1133, type:"Melee"}}
|
|
||||||
if (e.type === "Attacked" && e.msg){
|
|
||||||
if (this.idList.indexOf(e.msg.target) !== -1){
|
|
||||||
var attacker = militaryManager.entity(e.msg.attacker);
|
|
||||||
if (attacker && attacker.position()){
|
|
||||||
sumAttackerPos[0] += attacker.position()[0];
|
|
||||||
sumAttackerPos[1] += attacker.position()[1];
|
|
||||||
numAttackers += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (numAttackers > 0){
|
|
||||||
var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers];
|
|
||||||
// Stop moving
|
|
||||||
units.move(centrePos[0], centrePos[1]);
|
|
||||||
this.state = "attacking";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if (this.state === "attacking"){
|
|
||||||
units.move(this.path[0][0], this.path[0][1]);
|
|
||||||
this.state = "walking";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state === "walking"){
|
|
||||||
if (VectorDistance(centrePos, this.path[0]) < 20 || idleCount/numUnits > 0.8){
|
|
||||||
this.path.shift();
|
|
||||||
if (this.path.length > 0){
|
|
||||||
units.move(this.path[0][0], this.path[0][1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.previousTime = time;
|
|
||||||
this.previousHealth = totalHealth;
|
|
||||||
};
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
|||||||
function AttackMoveToLocation(gameState, Config, militaryManager, minAttackSize, maxAttackSize, targetFinder){
|
|
||||||
|
|
||||||
this.Config = Config;
|
|
||||||
this.minAttackSize = minAttackSize || Config.attack.minAttackSize;
|
|
||||||
this.maxAttackSize = maxAttackSize || Config.attack.maxAttackSize;
|
|
||||||
this.idList=[];
|
|
||||||
|
|
||||||
this.previousTime = 0;
|
|
||||||
this.state = "unexecuted";
|
|
||||||
|
|
||||||
this.targetFinder = targetFinder || this.defaultTargetFinder;
|
|
||||||
|
|
||||||
this.healthRecord = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns true if the attack can be executed at the current time
|
|
||||||
AttackMoveToLocation.prototype.canExecute = function(gameState, militaryManager){
|
|
||||||
var enemyStrength = militaryManager.measureEnemyStrength(gameState);
|
|
||||||
var enemyCount = militaryManager.measureEnemyCount(gameState);
|
|
||||||
|
|
||||||
// We require our army to be >= this strength
|
|
||||||
var targetStrength = enemyStrength * this.Config.attack.enemyRatio;
|
|
||||||
|
|
||||||
var availableCount = militaryManager.countAvailableUnits();
|
|
||||||
var availableStrength = militaryManager.measureAvailableStrength();
|
|
||||||
|
|
||||||
debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount);
|
|
||||||
debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength);
|
|
||||||
|
|
||||||
return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize)
|
|
||||||
|| availableCount >= this.maxAttackSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default target finder aims for conquest critical targets
|
|
||||||
AttackMoveToLocation.prototype.defaultTargetFinder = function(gameState, militaryManager){
|
|
||||||
// Find the critical enemy buildings we could attack
|
|
||||||
var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical");
|
|
||||||
// If there are no critical structures, attack anything else that's critical
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = gameState.entities.filter(function(ent) {
|
|
||||||
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0 && ent.position());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If there's nothing, attack anything else that's less critical
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = militaryManager.getEnemyBuildings(gameState,"Town");
|
|
||||||
}
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = militaryManager.getEnemyBuildings(gameState,"Village");
|
|
||||||
}
|
|
||||||
return targets;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Executes the attack plan, after this is executed the update function will be run every turn
|
|
||||||
AttackMoveToLocation.prototype.execute = function(gameState, militaryManager){
|
|
||||||
var availableCount = militaryManager.countAvailableUnits();
|
|
||||||
var numWanted = Math.min(availableCount, this.maxAttackSize);
|
|
||||||
this.idList = militaryManager.getAvailableUnits(numWanted);
|
|
||||||
|
|
||||||
var pending = EntityCollectionFromIds(gameState, this.idList);
|
|
||||||
|
|
||||||
var targets = this.targetFinder(gameState, militaryManager);
|
|
||||||
|
|
||||||
if (targets.length === 0){
|
|
||||||
targets = this.defaultTargetFinder(gameState, militaryManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a target, move to it
|
|
||||||
if (targets.length) {
|
|
||||||
// Add an attack role so the economic manager doesn't try and use them
|
|
||||||
pending.forEach(function(ent) {
|
|
||||||
ent.setMetadata("role", "attack");
|
|
||||||
});
|
|
||||||
|
|
||||||
var curPos = pending.getCentrePosition();
|
|
||||||
|
|
||||||
// pick a random target from the list
|
|
||||||
this.targetPos = undefined;
|
|
||||||
var count = 0;
|
|
||||||
while (!this.targetPos) {
|
|
||||||
var rand = Math.floor((Math.random()*targets.length));
|
|
||||||
var target = targets.toEntityArray()[rand];
|
|
||||||
this.targetPos = target.position();
|
|
||||||
count++;
|
|
||||||
if (count > 1000) {
|
|
||||||
warn("No target with a valid position found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find possible distinct paths to the enemy
|
|
||||||
var pathFinder = new PathFinder(gameState);
|
|
||||||
var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos);
|
|
||||||
if (!pathsToEnemy || !pathsToEnemy[0] || pathsToEnemy[0][0] === undefined || pathsToEnemy[0][1] === undefined) {
|
|
||||||
pathsToEnemy = [[this.targetPos]];
|
|
||||||
}
|
|
||||||
|
|
||||||
var randPath = Math.floor(Math.random() * pathsToEnemy.length);
|
|
||||||
this.path = pathsToEnemy[randPath];
|
|
||||||
|
|
||||||
pending.move(this.path[0][0], this.path[0][1]);
|
|
||||||
} else if (targets.length == 0 ) {
|
|
||||||
gameState.ai.gameFinished = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = "walking";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Runs every turn after the attack is executed
|
|
||||||
// This removes idle units from the attack
|
|
||||||
AttackMoveToLocation.prototype.update = function(gameState, militaryManager, events){
|
|
||||||
|
|
||||||
if (!this.targetPos){
|
|
||||||
for (var idKey in this.idList){
|
|
||||||
var id = this.idList[idKey];
|
|
||||||
militaryManager.unassignUnit(id);
|
|
||||||
}
|
|
||||||
this.idList = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep the list of units in good order by pruning ids with no corresponding entities (i.e. dead units)
|
|
||||||
var removeList = [];
|
|
||||||
var totalHealth = 0;
|
|
||||||
for (var idKey in this.idList){
|
|
||||||
var id = this.idList[idKey];
|
|
||||||
var ent = militaryManager.entity(id);
|
|
||||||
if (ent === undefined){
|
|
||||||
removeList.push(id);
|
|
||||||
}else{
|
|
||||||
if (ent.hitpoints()){
|
|
||||||
totalHealth += ent.hitpoints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var i in removeList){
|
|
||||||
this.idList.splice(this.idList.indexOf(removeList[i]),1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var units = EntityCollectionFromIds(gameState, this.idList);
|
|
||||||
|
|
||||||
if (!this.path || this.path.length === 0){
|
|
||||||
var idleCount = 0;
|
|
||||||
var self = this;
|
|
||||||
units.forEach(function(ent){
|
|
||||||
if (ent.isIdle()){
|
|
||||||
if (ent.position() && VectorDistance(ent.position(), self.targetPos) > 30){
|
|
||||||
ent.move(self.targetPos[0], self.targetPos[1]);
|
|
||||||
}else{
|
|
||||||
militaryManager.unassignUnit(ent.id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deltaHealth = 0;
|
|
||||||
var deltaTime = 1;
|
|
||||||
var time = gameState.getTimeElapsed();
|
|
||||||
this.healthRecord.push([totalHealth, time]);
|
|
||||||
if (this.healthRecord.length > 1){
|
|
||||||
for (var i = this.healthRecord.length - 1; i >= 0; i--){
|
|
||||||
deltaHealth = totalHealth - this.healthRecord[i][0];
|
|
||||||
deltaTime = time - this.healthRecord[i][1];
|
|
||||||
if (this.healthRecord[i][1] < time - 5*1000){
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var numUnits = this.idList.length;
|
|
||||||
if (numUnits < 1) return;
|
|
||||||
var damageRate = -deltaHealth / deltaTime * 1000;
|
|
||||||
var centrePos = units.getCentrePosition();
|
|
||||||
if (! centrePos) return;
|
|
||||||
|
|
||||||
var idleCount = 0;
|
|
||||||
// Looks for idle units away from the formations centre
|
|
||||||
for (var idKey in this.idList){
|
|
||||||
var id = this.idList[idKey];
|
|
||||||
var ent = militaryManager.entity(id);
|
|
||||||
if (ent.isIdle()){
|
|
||||||
if (ent.position() && VectorDistance(ent.position(), centrePos) > 20){
|
|
||||||
var dist = VectorDistance(ent.position(), centrePos);
|
|
||||||
var vector = [centrePos[0] - ent.position()[0], centrePos[1] - ent.position()[1]];
|
|
||||||
vector[0] *= 10/dist;
|
|
||||||
vector[1] *= 10/dist;
|
|
||||||
ent.move(centrePos[0] + vector[0], centrePos[1] + vector[1]);
|
|
||||||
}else{
|
|
||||||
idleCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((damageRate / Math.sqrt(numUnits)) > 2){
|
|
||||||
if (this.state === "walking"){
|
|
||||||
var sumAttackerPos = [0,0];
|
|
||||||
var numAttackers = 0;
|
|
||||||
|
|
||||||
for (var key in events){
|
|
||||||
var e = events[key];
|
|
||||||
//{type:"Attacked", msg:{attacker:736, target:1133, type:"Melee"}}
|
|
||||||
if (e.type === "Attacked" && e.msg){
|
|
||||||
if (this.idList.indexOf(e.msg.target) !== -1){
|
|
||||||
var attacker = militaryManager.entity(e.msg.attacker);
|
|
||||||
if (attacker && attacker.position()){
|
|
||||||
sumAttackerPos[0] += attacker.position()[0];
|
|
||||||
sumAttackerPos[1] += attacker.position()[1];
|
|
||||||
numAttackers += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (numAttackers > 0){
|
|
||||||
var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers];
|
|
||||||
// Stop moving
|
|
||||||
units.move(centrePos[0], centrePos[1]);
|
|
||||||
this.state = "attacking";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if (this.state === "attacking"){
|
|
||||||
units.move(this.path[0][0], this.path[0][1]);
|
|
||||||
this.state = "walking";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state === "walking"){
|
|
||||||
if (VectorDistance(centrePos, this.path[0]) < 20 || idleCount/numUnits > 0.8){
|
|
||||||
this.path.shift();
|
|
||||||
if (this.path.length > 0){
|
|
||||||
units.move(this.path[0][0], this.path[0][1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.previousTime = time;
|
|
||||||
this.previousHealth = totalHealth;
|
|
||||||
};
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
|||||||
function Config() {
|
|
||||||
this.debug = false
|
|
||||||
|
|
||||||
this.attack = {
|
|
||||||
"minAttackSize" : 20, // attackMoveToLocation
|
|
||||||
"maxAttackSize" : 60, // attackMoveToLocation
|
|
||||||
"enemyRatio" : 1.5, // attackMoveToLocation
|
|
||||||
"groupSize" : 10 // military
|
|
||||||
};
|
|
||||||
|
|
||||||
// defence
|
|
||||||
this.defence = {
|
|
||||||
"acquireDistance" : 220,
|
|
||||||
"releaseDistance" : 250,
|
|
||||||
"groupRadius" : 20,
|
|
||||||
"groupBreakRadius" : 40,
|
|
||||||
"groupMergeRadius" : 10,
|
|
||||||
"defenderRatio" : 2
|
|
||||||
};
|
|
||||||
|
|
||||||
// military
|
|
||||||
this.buildings = {
|
|
||||||
"moderate" : {
|
|
||||||
"default" : [ "structures/{civ}_barracks" ]
|
|
||||||
},
|
|
||||||
"advanced" : {
|
|
||||||
"hele" : [ "structures/{civ}_gymnasion", "structures/{civ}_fortress" ],
|
|
||||||
"athen" : [ "structures/{civ}_gymnasion", "structures/{civ}_fortress" ],
|
|
||||||
"spart" : [ "structures/{civ}_syssiton", "structures/{civ}_fortress" ],
|
|
||||||
"mace" : [ "structures/{civ}_fortress" ],
|
|
||||||
"cart" : [ "structures/{civ}_fortress", "structures/{civ}_embassy_celtic",
|
|
||||||
"structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ],
|
|
||||||
"celt" : [ "structures/{civ}_kennel", "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ],
|
|
||||||
"iber" : [ "structures/{civ}_fortress" ],
|
|
||||||
"pers" : [ "structures/{civ}_fortress", "structures/{civ}_stables", "structures/{civ}_apadana" ],
|
|
||||||
"rome" : [ "structures/{civ}_army_camp", "structures/{civ}_fortress" ]
|
|
||||||
},
|
|
||||||
"fort" : {
|
|
||||||
"default" : [ "structures/{civ}_fortress" ],
|
|
||||||
"celt" : [ "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// qbot
|
|
||||||
this.priorities = { // Note these are dynamic, you are only setting the initial values
|
|
||||||
"house" : 500,
|
|
||||||
"citizenSoldier" : 100,
|
|
||||||
"villager" : 100,
|
|
||||||
"economicBuilding" : 30,
|
|
||||||
"field" : 20,
|
|
||||||
"advancedSoldier" : 30,
|
|
||||||
"siege" : 10,
|
|
||||||
"militaryBuilding" : 50,
|
|
||||||
"defenceBuilding" : 17,
|
|
||||||
"civilCentre" : 1000
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "qBot",
|
|
||||||
"description": "Quantumstate's improved version of the Test Bot",
|
|
||||||
"constructor": "QBotAI",
|
|
||||||
"useShared" : false
|
|
||||||
}
|
|
@ -1,278 +0,0 @@
|
|||||||
function Defence(Config){
|
|
||||||
this.ACQUIRE_DIST = Config.defence.acquireDistance;
|
|
||||||
this.RELEASE_DIST = Config.defence.releaseDistance;
|
|
||||||
|
|
||||||
this.GROUP_RADIUS = Config.defence.groupRadius; // units will be added to a group if they are within this radius
|
|
||||||
this.GROUP_BREAK_RADIUS = Config.defence.groupBreakRadius; // units will leave a group if they are outside of this radius
|
|
||||||
this.GROUP_MERGE_RADIUS = Config.defence.groupMergeRadius; // Two groups with centres this far apart will be merged
|
|
||||||
|
|
||||||
this.DEFENCE_RATIO = Config.defence.defenderRatio; // How many defenders we want per attacker. Need to balance fewer losses vs. lost economy
|
|
||||||
|
|
||||||
// These are objects with the keys being entity ids and values being the entity objects
|
|
||||||
// NOTE: It is assumed that all attackers have a valid position, the attackers list must be kept up to date so this
|
|
||||||
// property is maintained
|
|
||||||
this.attackers = {}; // Enemy soldiers which are attacking our base
|
|
||||||
this.defenders = {}; // Our soldiers currently being used for defence
|
|
||||||
|
|
||||||
// A list of groups, enemy soldiers are clumped together in groups.
|
|
||||||
this.groups = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
Defence.prototype.update = function(gameState, events, militaryManager){
|
|
||||||
Engine.ProfileStart("Defence Manager");
|
|
||||||
var enemyTroops = militaryManager.getEnemySoldiers();
|
|
||||||
|
|
||||||
this.updateAttackers(gameState, events, enemyTroops);
|
|
||||||
this.updateGroups();
|
|
||||||
var unassignedDefenders = this.updateDefenders(gameState);
|
|
||||||
this.assignDefenders(gameState, militaryManager, unassignedDefenders);
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
Defence.prototype.assignDefenders = function(gameState, militaryManager, unassignedDefenders){
|
|
||||||
var numAttackers = Object.keys(this.attackers).length;
|
|
||||||
var numDefenders = Object.keys(this.defenders).length;
|
|
||||||
var numUnassignedDefenders = unassignedDefenders.length;
|
|
||||||
var numAssignedDefenders = numDefenders - numUnassignedDefenders;
|
|
||||||
|
|
||||||
// TODO: this is non optimal, we may have unevenly distributed defenders
|
|
||||||
// Unassign defenders which aren't needed
|
|
||||||
if (numAttackers * this.DEFENCE_RATIO <= numAssignedDefenders){
|
|
||||||
militaryManager.unassignUnits(unassignedDefenders);
|
|
||||||
|
|
||||||
var CCs = gameState.getOwnEntities().filter(Filters.byClass("CivCentre"));
|
|
||||||
|
|
||||||
for (var i in unassignedDefenders){
|
|
||||||
var pos = this.defenders[unassignedDefenders[i]].position();
|
|
||||||
|
|
||||||
// Move back to nearest CC
|
|
||||||
if (pos){
|
|
||||||
var nearestCCArray = CCs.filterNearest(pos, 1).toEntityArray();
|
|
||||||
if (nearestCCArray.length > 0){
|
|
||||||
var movePos = nearestCCArray[0].position();
|
|
||||||
this.defenders[unassignedDefenders[i]].move(movePos[0], movePos[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete this.defenders[unassignedDefenders[i]];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check to see if we need to recruit more defenders
|
|
||||||
if (numAttackers * this.DEFENCE_RATIO > numDefenders){
|
|
||||||
var numNeeded = Math.ceil(numAttackers * this.DEFENCE_RATIO - numDefenders);
|
|
||||||
var numIdleAvailable = militaryManager.countAvailableUnits(Filters.isIdle());
|
|
||||||
|
|
||||||
if (numIdleAvailable > numNeeded){
|
|
||||||
var newUnits = militaryManager.getAvailableUnits(numNeeded, Filters.isIdle());
|
|
||||||
for (var i in newUnits){
|
|
||||||
var ent = gameState.getEntityById(newUnits[i]);
|
|
||||||
}
|
|
||||||
unassignedDefenders = unassignedDefenders.concat(newUnits);
|
|
||||||
}else{
|
|
||||||
var newUnits = militaryManager.getAvailableUnits(numNeeded);
|
|
||||||
for (var i in newUnits){
|
|
||||||
var ent = gameState.getEntityById(newUnits[i]);
|
|
||||||
ent.setMetadata("initialPosition", ent.position());
|
|
||||||
}
|
|
||||||
unassignedDefenders = unassignedDefenders.concat(newUnits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now distribute the unassigned defenders among the attacking groups.
|
|
||||||
for (var i in unassignedDefenders){
|
|
||||||
var id = unassignedDefenders[i];
|
|
||||||
var ent = gameState.getEntityById(id);
|
|
||||||
if (!ent.position()){
|
|
||||||
debug("Defender with no position! (shouldn't happen)");
|
|
||||||
debug(ent);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minDist = Math.min();
|
|
||||||
var closestGroup = undefined;
|
|
||||||
for (var j in this.groups){
|
|
||||||
var dist = VectorDistance(this.groups[j].position, ent.position());
|
|
||||||
if (dist < minDist && this.groups[j].members.length * this.DEFENCE_RATIO > this.groups[j].defenders.length){
|
|
||||||
minDist = dist;
|
|
||||||
closestGroup = this.groups[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closestGroup !== undefined){
|
|
||||||
var rand = Math.floor(Math.random()*closestGroup.members.length);
|
|
||||||
ent.attack(closestGroup.members[rand]);
|
|
||||||
this.defenders[id] = ent;
|
|
||||||
closestGroup.defenders.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Defence.prototype.updateDefenders = function(gameState){
|
|
||||||
var newDefenders = {};
|
|
||||||
var unassignedDefenders = [];
|
|
||||||
|
|
||||||
for (var i in this.groups){
|
|
||||||
this.removeDestroyed(gameState, this.groups[i].defenders);
|
|
||||||
for (var j in this.groups[i].defenders){
|
|
||||||
var id = this.groups[i].defenders[j];
|
|
||||||
newDefenders[id] = this.defenders[id];
|
|
||||||
var ent = gameState.getEntityById(id);
|
|
||||||
|
|
||||||
// If the defender is idle then set it to attack another member of the group it is targetting
|
|
||||||
if (ent && ent.isIdle()){
|
|
||||||
var rand = Math.floor(Math.random()*this.groups[i].members.length);
|
|
||||||
ent.attack(this.groups[i].members[rand]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var id in this.defenders){
|
|
||||||
if (!gameState.getEntityById(id)){
|
|
||||||
delete this.defenders[id];
|
|
||||||
} else if (!newDefenders[id]){
|
|
||||||
unassignedDefenders.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return unassignedDefenders;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns an entity collection of key buildings which should be defended.
|
|
||||||
// Currently just returns civ centres
|
|
||||||
Defence.prototype.getKeyBuildings = function(gameState){
|
|
||||||
return gameState.getOwnEntities().filter(Filters.byClass("CivCentre"));
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This function puts all attacking enemy troops into this.attackers, the list from the turn before is put into
|
|
||||||
* this.oldAttackers, also any new attackers have their id's listed in this.newAttackers.
|
|
||||||
*/
|
|
||||||
Defence.prototype.updateAttackers = function(gameState, events, enemyTroops){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var keyBuildings = this.getKeyBuildings(gameState);
|
|
||||||
|
|
||||||
this.newAttackers = [];
|
|
||||||
this.oldAttackers = this.attackers;
|
|
||||||
this.attackers = {};
|
|
||||||
|
|
||||||
enemyTroops.forEach(function(ent){
|
|
||||||
if (ent.position()){
|
|
||||||
var minDist = Math.min();
|
|
||||||
keyBuildings.forEach(function(building){
|
|
||||||
if (building.position() && VectorDistance(ent.position(), building.position()) < minDist){
|
|
||||||
minDist = VectorDistance(ent.position(), building.position());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (self.oldAttackers[ent.id()]){
|
|
||||||
if (minDist < self.RELEASE_DIST){
|
|
||||||
self.attackers[ent.id()] = ent;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if (minDist < self.ACQUIRE_DIST){
|
|
||||||
self.attackers[ent.id()] = ent;
|
|
||||||
self.newAttackers.push(ent.id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Defence.prototype.removeDestroyed = function(gameState, entList){
|
|
||||||
for (var i = 0; i < entList.length; i++){
|
|
||||||
if (!gameState.getEntityById(entList[i])){
|
|
||||||
entList.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Defence.prototype.updateGroups = function(){
|
|
||||||
// clean up groups by removing members and removing empty groups
|
|
||||||
for (var i = 0; i < this.groups.length; i++){
|
|
||||||
var group = this.groups[i];
|
|
||||||
// remove members which are no longer attackers
|
|
||||||
for (var j = 0; j < group.members.length; j++){
|
|
||||||
if (!this.attackers[group.members[j]]){
|
|
||||||
group.members.splice(j, 1);
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// recalculate centre of group
|
|
||||||
group.sumPosition = [0,0];
|
|
||||||
for (var j = 0; j < group.members.length; j++){
|
|
||||||
group.sumPosition[0] += this.attackers[group.members[j]].position()[0];
|
|
||||||
group.sumPosition[1] += this.attackers[group.members[j]].position()[1];
|
|
||||||
}
|
|
||||||
group.position[0] = group.sumPosition[0]/group.members.length;
|
|
||||||
group.position[1] = group.sumPosition[1]/group.members.length;
|
|
||||||
|
|
||||||
// remove members that are too far away
|
|
||||||
for (var j = 0; j < group.members.length; j++){
|
|
||||||
if ( VectorDistance(this.attackers[group.members[j]].position(), group.position) > this.GROUP_BREAK_RADIUS){
|
|
||||||
this.newAttackers.push(group.members[j]);
|
|
||||||
group.sumPosition[0] -= this.attackers[group.members[j]].position()[0];
|
|
||||||
group.sumPosition[1] -= this.attackers[group.members[j]].position()[1];
|
|
||||||
group.members.splice(j, 1);
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.members.length === 0){
|
|
||||||
this.groups.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
|
|
||||||
group.position[0] = group.sumPosition[0]/group.members.length;
|
|
||||||
group.position[1] = group.sumPosition[1]/group.members.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add ungrouped attackers to groups
|
|
||||||
for (var j in this.newAttackers){
|
|
||||||
var ent = this.attackers[this.newAttackers[j]];
|
|
||||||
var foundGroup = false;
|
|
||||||
for (var i in this.groups){
|
|
||||||
if (VectorDistance(ent.position(), this.groups[i].position) <= this.GROUP_RADIUS){
|
|
||||||
this.groups[i].members.push(ent.id());
|
|
||||||
|
|
||||||
this.groups[i].sumPosition[0] += ent.position()[0];
|
|
||||||
this.groups[i].sumPosition[1] += ent.position()[1];
|
|
||||||
this.groups[i].position[0] = this.groups[i].sumPosition[0]/this.groups[i].members.length;
|
|
||||||
this.groups[i].position[1] = this.groups[i].sumPosition[1]/this.groups[i].members.length;
|
|
||||||
|
|
||||||
foundGroup = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundGroup){
|
|
||||||
this.groups.push({"members": [ent.id()],
|
|
||||||
"position": [ent.position()[0], ent.position()[1]],
|
|
||||||
"sumPosition": [ent.position()[0], ent.position()[1]],
|
|
||||||
"defenders": []});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge groups which are close together
|
|
||||||
for (var i = 0; i < this.groups.length; i++){
|
|
||||||
for (var j = 0; j < this.groups.length; j++){
|
|
||||||
if (this.groups[i].members.length < this.groups[j].members.length){
|
|
||||||
if (VectorDistance(this.groups[i].position, this.groups[j].position) < this.GROUP_MERGE_RADIUS){
|
|
||||||
this.groups[j].members = this.groups[i].members.concat(this.groups[j].members);
|
|
||||||
this.groups[j].defenders = this.groups[i].defenders.concat(this.groups[j].defenders);
|
|
||||||
|
|
||||||
this.groups[j].sumPosition[0] += this.groups[i].sumPosition[0];
|
|
||||||
this.groups[j].sumPosition[1] += this.groups[i].sumPosition[1];
|
|
||||||
this.groups[j].position[0] = this.groups[j].sumPosition[0]/this.groups[j].members.length;
|
|
||||||
this.groups[j].position[1] = this.groups[j].sumPosition[1]/this.groups[j].members.length;
|
|
||||||
|
|
||||||
this.groups.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,543 +0,0 @@
|
|||||||
var EconomyManager = function() {
|
|
||||||
this.targetNumBuilders = 5; // number of workers we want building stuff
|
|
||||||
this.targetNumFields = 3;
|
|
||||||
|
|
||||||
this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal
|
|
||||||
|
|
||||||
this.setCount = 0; //stops villagers being reassigned to other resources too frequently, count a set number of
|
|
||||||
//turns before trying to reassign them.
|
|
||||||
|
|
||||||
this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1};
|
|
||||||
};
|
|
||||||
// More initialisation for stuff that needs the gameState
|
|
||||||
EconomyManager.prototype.init = function(gameState){
|
|
||||||
this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()/3), 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.trainMoreWorkers = function(gameState, queues) {
|
|
||||||
// Count the workers in the world and in progress
|
|
||||||
var numWorkers = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen"));
|
|
||||||
numWorkers += queues.villager.countTotalQueuedUnits();
|
|
||||||
|
|
||||||
// If we have too few, train more
|
|
||||||
if (numWorkers < this.targetNumWorkers) {
|
|
||||||
for ( var i = 0; i < this.targetNumWorkers - numWorkers; i++) {
|
|
||||||
queues.villager.addItem(new UnitTrainingPlan(gameState, "units/{civ}_support_female_citizen", {
|
|
||||||
"role" : "worker"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pick the resource which most needs another worker
|
|
||||||
EconomyManager.prototype.pickMostNeededResources = function(gameState) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Find what resource type we're most in need of
|
|
||||||
if (!gameState.turnCache["gather-weights-calculated"]){
|
|
||||||
this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState);
|
|
||||||
gameState.turnCache["gather-weights-calculated"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var numGatherers = {};
|
|
||||||
for ( var type in this.gatherWeights){
|
|
||||||
numGatherers[type] = gameState.updatingCollection("workers-gathering-" + type,
|
|
||||||
Filters.byMetadata("gather-type", type), gameState.getOwnEntitiesByRole("worker")).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
var types = Object.keys(this.gatherWeights);
|
|
||||||
types.sort(function(a, b) {
|
|
||||||
// Prefer fewer gatherers (divided by weight)
|
|
||||||
var va = numGatherers[a] / (self.gatherWeights[a]+1);
|
|
||||||
var vb = numGatherers[b] / (self.gatherWeights[b]+1);
|
|
||||||
return va-vb;
|
|
||||||
});
|
|
||||||
|
|
||||||
return types;
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.reassignRolelessUnits = function(gameState) {
|
|
||||||
//TODO: Move this out of the economic section
|
|
||||||
var roleless = gameState.getOwnEntitiesByRole(undefined);
|
|
||||||
|
|
||||||
roleless.forEach(function(ent) {
|
|
||||||
if (ent.hasClass("Worker")){
|
|
||||||
ent.setMetadata("role", "worker");
|
|
||||||
}else if(ent.hasClass("CitizenSoldier") || ent.hasClass("Champion")){
|
|
||||||
ent.setMetadata("role", "soldier");
|
|
||||||
}else{
|
|
||||||
ent.setMetadata("role", "unknown");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the numbers of workers on the resources is unbalanced then set some of workers to idle so
|
|
||||||
// they can be reassigned by reassignIdleWorkers.
|
|
||||||
EconomyManager.prototype.setWorkersIdleByPriority = function(gameState){
|
|
||||||
this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState);
|
|
||||||
|
|
||||||
var numGatherers = {};
|
|
||||||
var totalGatherers = 0;
|
|
||||||
var totalWeight = 0;
|
|
||||||
for ( var type in this.gatherWeights){
|
|
||||||
numGatherers[type] = 0;
|
|
||||||
totalWeight += this.gatherWeights[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
gameState.getOwnEntitiesByRole("worker").forEach(function(ent) {
|
|
||||||
if (ent.getMetadata("subrole") === "gatherer"){
|
|
||||||
numGatherers[ent.getMetadata("gather-type")] += 1;
|
|
||||||
totalGatherers += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for ( var type in this.gatherWeights){
|
|
||||||
var allocation = Math.floor(totalGatherers * (this.gatherWeights[type]/totalWeight));
|
|
||||||
if (allocation < numGatherers[type]){
|
|
||||||
var numToTake = numGatherers[type] - allocation;
|
|
||||||
gameState.getOwnEntitiesByRole("worker").forEach(function(ent) {
|
|
||||||
if (ent.getMetadata("subrole") === "gatherer" && ent.getMetadata("gather-type") === type && numToTake > 0){
|
|
||||||
ent.setMetadata("subrole", "idle");
|
|
||||||
numToTake -= 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.reassignIdleWorkers = function(gameState) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Search for idle workers, and tell them to gather resources based on demand
|
|
||||||
var filter = Filters.or(Filters.isIdle(), Filters.byMetadata("subrole", "idle"));
|
|
||||||
var idleWorkers = gameState.updatingCollection("idle-workers", filter, gameState.getOwnEntitiesByRole("worker"));
|
|
||||||
|
|
||||||
if (idleWorkers.length) {
|
|
||||||
var resourceSupplies;
|
|
||||||
|
|
||||||
idleWorkers.forEach(function(ent) {
|
|
||||||
// Check that the worker isn't garrisoned
|
|
||||||
if (ent.position() === undefined){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var types = self.pickMostNeededResources(gameState);
|
|
||||||
|
|
||||||
ent.setMetadata("subrole", "gatherer");
|
|
||||||
ent.setMetadata("gather-type", types[0]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.workersBySubrole = function(gameState, subrole) {
|
|
||||||
var workers = gameState.getOwnEntitiesByRole("worker");
|
|
||||||
return gameState.updatingCollection("subrole-" + subrole, Filters.byMetadata("subrole", subrole), workers);
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.assignToFoundations = function(gameState) {
|
|
||||||
// If we have some foundations, and we don't have enough
|
|
||||||
// builder-workers,
|
|
||||||
// try reassigning some other workers who are nearby
|
|
||||||
|
|
||||||
var foundations = gameState.getOwnFoundations();
|
|
||||||
|
|
||||||
// Check if nothing to build
|
|
||||||
if (!foundations.length){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var workers = gameState.getOwnEntitiesByRole("worker");
|
|
||||||
|
|
||||||
var builderWorkers = this.workersBySubrole(gameState, "builder");
|
|
||||||
|
|
||||||
// Check if enough builders
|
|
||||||
var extraNeeded = this.targetNumBuilders - builderWorkers.length;
|
|
||||||
if (extraNeeded <= 0){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pick non-builders who are closest to the first foundation,
|
|
||||||
// and tell them to start building it
|
|
||||||
|
|
||||||
var target = foundations.toEntityArray()[0];
|
|
||||||
|
|
||||||
var nonBuilderWorkers = workers.filter(function(ent) {
|
|
||||||
// check position so garrisoned units aren't tasked
|
|
||||||
return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), extraNeeded);
|
|
||||||
|
|
||||||
// Order each builder individually, not as a formation
|
|
||||||
nearestNonBuilders.forEach(function(ent) {
|
|
||||||
ent.setMetadata("subrole", "builder");
|
|
||||||
ent.setMetadata("target-foundation", target);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.buildMoreFields = function(gameState, queues) {
|
|
||||||
// give time for treasures to be gathered
|
|
||||||
if (gameState.getTimeElapsed() < 30 * 1000)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var numFood = 0;
|
|
||||||
|
|
||||||
gameState.updatingCollection("active-dropsite-food", Filters.byMetadata("active-dropsite-food", true),
|
|
||||||
gameState.getOwnDropsites("food")).forEach(function (dropsite){
|
|
||||||
numFood += dropsite.getMetadata("nearby-resources-food").length;
|
|
||||||
});
|
|
||||||
|
|
||||||
numFood += gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_field"));
|
|
||||||
numFood += queues.field.totalLength();
|
|
||||||
|
|
||||||
for ( var i = numFood; i < this.targetNumFields; i++) {
|
|
||||||
queues.field.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_field"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If all the CC's are destroyed then build a new one
|
|
||||||
EconomyManager.prototype.buildNewCC= function(gameState, queues) {
|
|
||||||
var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre"));
|
|
||||||
numCCs += queues.civilCentre.totalLength();
|
|
||||||
|
|
||||||
for ( var i = numCCs; i < 1; i++) {
|
|
||||||
queues.civilCentre.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//creates and maintains a map of tree density
|
|
||||||
EconomyManager.prototype.updateResourceMaps = function(gameState, events){
|
|
||||||
// The weight of the influence function is amountOfResource/decreaseFactor
|
|
||||||
var decreaseFactor = {'wood': 15, 'stone': 100, 'metal': 100, 'food': 20};
|
|
||||||
// This is the maximum radius of the influence
|
|
||||||
var radius = {'wood':13, 'stone': 10, 'metal': 10, 'food': 10};
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
for (var resource in radius){
|
|
||||||
// if there is no resourceMap create one with an influence for everything with that resource
|
|
||||||
if (! this.resourceMaps[resource]){
|
|
||||||
this.resourceMaps[resource] = new Map(gameState);
|
|
||||||
|
|
||||||
var supplies = gameState.getResourceSupplies(resource);
|
|
||||||
supplies.forEach(function(ent){
|
|
||||||
if (!ent.position()){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var x = Math.round(ent.position()[0] / gameState.cellSize);
|
|
||||||
var z = Math.round(ent.position()[1] / gameState.cellSize);
|
|
||||||
var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]);
|
|
||||||
self.resourceMaps[resource].addInfluence(x, z, radius[resource], strength);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// TODO: fix for treasure and move out of loop
|
|
||||||
// Look for destroy events and subtract the entities original influence from the resourceMap
|
|
||||||
for (var i in events) {
|
|
||||||
var e = events[i];
|
|
||||||
|
|
||||||
if (e.type === "Destroy") {
|
|
||||||
if (e.msg.entityObj){
|
|
||||||
var ent = e.msg.entityObj;
|
|
||||||
if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){
|
|
||||||
var x = Math.round(ent.position()[0] / gameState.cellSize);
|
|
||||||
var z = Math.round(ent.position()[1] / gameState.cellSize);
|
|
||||||
var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]);
|
|
||||||
this.resourceMaps[resource].addInfluence(x, z, radius[resource], -strength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else if (e.type === "Create") {
|
|
||||||
if (e.msg.entityObj){
|
|
||||||
var ent = e.msg.entityObj;
|
|
||||||
if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){
|
|
||||||
var x = Math.round(ent.position()[0] / gameState.cellSize);
|
|
||||||
var z = Math.round(ent.position()[1] / gameState.cellSize);
|
|
||||||
var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]);
|
|
||||||
this.resourceMaps[resource].addInfluence(x, z, radius[resource], strength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//this.resourceMaps['wood'].dumpIm("tree_density.png");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the position of the best place to build a new dropsite for the specified resource
|
|
||||||
EconomyManager.prototype.getBestResourceBuildSpot = function(gameState, resource){
|
|
||||||
// A map which gives a positive weight for all CCs and adds a negative weight near all dropsites
|
|
||||||
var friendlyTiles = new Map(gameState);
|
|
||||||
gameState.getOwnEntities().forEach(function(ent) {
|
|
||||||
// We want to build near a CC of ours
|
|
||||||
if (ent.hasClass("CivCentre")){
|
|
||||||
var infl = 200;
|
|
||||||
|
|
||||||
var pos = ent.position();
|
|
||||||
var x = Math.round(pos[0] / gameState.cellSize);
|
|
||||||
var z = Math.round(pos[1] / gameState.cellSize);
|
|
||||||
friendlyTiles.addInfluence(x, z, infl, 0.1 * infl);
|
|
||||||
friendlyTiles.addInfluence(x, z, infl/2, 0.1 * infl);
|
|
||||||
}
|
|
||||||
// We don't want multiple dropsites at one spot so add a negative for all dropsites
|
|
||||||
if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){
|
|
||||||
var infl = 20;
|
|
||||||
|
|
||||||
var pos = ent.position();
|
|
||||||
var x = Math.round(pos[0] / gameState.cellSize);
|
|
||||||
var z = Math.round(pos[1] / gameState.cellSize);
|
|
||||||
|
|
||||||
friendlyTiles.addInfluence(x, z, infl, -50, 'quadratic');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Multiply by tree density to get a combination of the two maps
|
|
||||||
friendlyTiles.multiply(this.resourceMaps[resource]);
|
|
||||||
|
|
||||||
//friendlyTiles.dumpIm(resource + "_density_fade.png", 10000);
|
|
||||||
|
|
||||||
var obstructions = Map.createObstructionMap(gameState);
|
|
||||||
obstructions.expandInfluences();
|
|
||||||
|
|
||||||
var bestIdx = friendlyTiles.findBestTile(4, obstructions)[0];
|
|
||||||
|
|
||||||
// Convert from 1d map pixel coordinates to game engine coordinates
|
|
||||||
var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize;
|
|
||||||
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize;
|
|
||||||
return [x,z];
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.updateResourceConcentrations = function(gameState){
|
|
||||||
var self = this;
|
|
||||||
var resources = ["food", "wood", "stone", "metal"];
|
|
||||||
for (var key in resources){
|
|
||||||
var resource = resources[key];
|
|
||||||
gameState.getOwnEntities().forEach(function(ent) {
|
|
||||||
if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){
|
|
||||||
var radius = 14;
|
|
||||||
|
|
||||||
var pos = ent.position();
|
|
||||||
var x = Math.round(pos[0] / gameState.cellSize);
|
|
||||||
var z = Math.round(pos[1] / gameState.cellSize);
|
|
||||||
|
|
||||||
var quantity = self.resourceMaps[resource].sumInfluence(x, z, radius);
|
|
||||||
|
|
||||||
ent.setMetadata("resourceQuantity_" + resource, quantity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stores lists of nearby resources
|
|
||||||
EconomyManager.prototype.updateNearbyResources = function(gameState){
|
|
||||||
var self = this;
|
|
||||||
var resources = ["food", "wood", "stone", "metal"];
|
|
||||||
var resourceSupplies;
|
|
||||||
var radius = 100;
|
|
||||||
for (var key in resources){
|
|
||||||
var resource = resources[key];
|
|
||||||
|
|
||||||
gameState.getOwnDropsites(resource).forEach(function(ent) {
|
|
||||||
if (ent.getMetadata("nearby-resources-" + resource) === undefined){
|
|
||||||
var filterPos = Filters.byStaticDistance(ent.position(), radius);
|
|
||||||
|
|
||||||
var collection = gameState.getResourceSupplies(resource).filter(filterPos);
|
|
||||||
collection.registerUpdates();
|
|
||||||
|
|
||||||
ent.setMetadata("nearby-resources-" + resource, collection);
|
|
||||||
ent.setMetadata("active-dropsite-" + resource, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ent.getMetadata("nearby-resources-" + resource).length === 0){
|
|
||||||
ent.setMetadata("active-dropsite-" + resource, false);
|
|
||||||
}else{
|
|
||||||
ent.setMetadata("active-dropsite-" + resource, true);
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
// Make resources glow wildly
|
|
||||||
if (resource == "food"){
|
|
||||||
ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (resource == "wood"){
|
|
||||||
ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (resource == "metal"){
|
|
||||||
ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]});
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//return the number of resource dropsites with an acceptable amount of the resource nearby
|
|
||||||
EconomyManager.prototype.checkResourceConcentrations = function(gameState, resource){
|
|
||||||
//TODO: make these values adaptive
|
|
||||||
var requiredInfluence = {wood: 16000, stone: 300, metal: 300};
|
|
||||||
var count = 0;
|
|
||||||
gameState.getOwnEntities().forEach(function(ent) {
|
|
||||||
if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){
|
|
||||||
var quantity = ent.getMetadata("resourceQuantity_" + resource);
|
|
||||||
|
|
||||||
if (quantity >= requiredInfluence[resource]){
|
|
||||||
count ++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.buildMarket = function(gameState, queues){
|
|
||||||
if (gameState.getTimeElapsed() > 600 * 1000){
|
|
||||||
if (queues.economicBuilding.totalLength() === 0 &&
|
|
||||||
gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 0){
|
|
||||||
//only ever build one storehouse/CC/market at a time
|
|
||||||
queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_market"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.buildDropsites = function(gameState, queues){
|
|
||||||
if (queues.economicBuilding.totalLength() === 0 &&
|
|
||||||
gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_storehouse")) === 0 &&
|
|
||||||
gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_civil_centre")) === 0){
|
|
||||||
//only ever build one storehouse/CC/market at a time
|
|
||||||
if (gameState.getTimeElapsed() > 30 * 1000){
|
|
||||||
for (var resource in this.dropsiteNumbers){
|
|
||||||
if (this.checkResourceConcentrations(gameState, resource) < this.dropsiteNumbers[resource]){
|
|
||||||
var spot = this.getBestResourceBuildSpot(gameState, resource);
|
|
||||||
|
|
||||||
var myCivCentres = gameState.getOwnEntities().filter(function(ent) {
|
|
||||||
if (!ent.hasClass("CivCentre") || ent.position() === undefined){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var dx = (spot[0]-ent.position()[0]);
|
|
||||||
var dy = (spot[1]-ent.position()[1]);
|
|
||||||
var dist2 = dx*dx + dy*dy;
|
|
||||||
return (ent.hasClass("CivCentre") && dist2 < 180*180);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (myCivCentres.length === 0){
|
|
||||||
queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot));
|
|
||||||
}else{
|
|
||||||
queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_storehouse", spot));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EconomyManager.prototype.update = function(gameState, queues, events) {
|
|
||||||
Engine.ProfileStart("economy update");
|
|
||||||
|
|
||||||
this.reassignRolelessUnits(gameState);
|
|
||||||
|
|
||||||
this.buildNewCC(gameState,queues);
|
|
||||||
|
|
||||||
Engine.ProfileStart("Train workers and build farms");
|
|
||||||
this.trainMoreWorkers(gameState, queues);
|
|
||||||
|
|
||||||
this.buildMoreFields(gameState, queues);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
//Later in the game we want to build stuff faster.
|
|
||||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.5) {
|
|
||||||
this.targetNumBuilders = 10;
|
|
||||||
}else{
|
|
||||||
this.targetNumBuilders = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.8) {
|
|
||||||
this.dropsiteNumbers = {wood: 3, stone: 2, metal: 2};
|
|
||||||
}else{
|
|
||||||
this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1};
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStart("Update Resource Maps and Concentrations");
|
|
||||||
this.updateResourceMaps(gameState, events);
|
|
||||||
this.updateResourceConcentrations(gameState);
|
|
||||||
this.updateNearbyResources(gameState);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStart("Build new Dropsites");
|
|
||||||
this.buildDropsites(gameState, queues);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
this.buildMarket(gameState, queues);
|
|
||||||
|
|
||||||
// TODO: implement a timer based system for this
|
|
||||||
this.setCount += 1;
|
|
||||||
if (this.setCount >= 20){
|
|
||||||
this.setWorkersIdleByPriority(gameState);
|
|
||||||
this.setCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStart("Reassign Idle Workers");
|
|
||||||
this.reassignIdleWorkers(gameState);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStart("Swap Workers");
|
|
||||||
var gathererGroups = {};
|
|
||||||
gameState.getOwnEntitiesByRole("worker").forEach(function(ent){
|
|
||||||
var key = uneval(ent.resourceGatherRates());
|
|
||||||
if (!gathererGroups[key]){
|
|
||||||
gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []};
|
|
||||||
}
|
|
||||||
if (ent.getMetadata("gather-type") in gathererGroups[key]){
|
|
||||||
gathererGroups[key][ent.getMetadata("gather-type")].push(ent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (var i in gathererGroups){
|
|
||||||
for (var j in gathererGroups){
|
|
||||||
var a = eval(i);
|
|
||||||
var b = eval(j);
|
|
||||||
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("gather-type", "food");
|
|
||||||
gathererGroups[j]["food"][k].setMetadata("gather-type", "wood");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStart("Assign builders");
|
|
||||||
this.assignToFoundations(gameState);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStart("Run Workers");
|
|
||||||
gameState.getOwnEntitiesByRole("worker").forEach(function(ent){
|
|
||||||
if (!ent.getMetadata("worker-object")){
|
|
||||||
ent.setMetadata("worker-object", new Worker(ent));
|
|
||||||
}
|
|
||||||
ent.getMetadata("worker-object").update(gameState);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gatherer count updates for non-workers
|
|
||||||
var filter = Filters.and(Filters.not(Filters.byMetadata("worker-object", undefined)),
|
|
||||||
Filters.not(Filters.byMetadata("role", "worker")));
|
|
||||||
gameState.updatingCollection("reassigned-workers", filter, gameState.getOwnEntities()).forEach(function(ent){
|
|
||||||
ent.getMetadata("worker-object").updateGathererCounts(gameState);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gatherer count updates for destroyed units
|
|
||||||
for (var i in events) {
|
|
||||||
var e = events[i];
|
|
||||||
|
|
||||||
if (e.type === "Destroy") {
|
|
||||||
if (e.msg.metadata && e.msg.metadata[gameState.getPlayerID()] && e.msg.metadata[gameState.getPlayerID()]["worker-object"]){
|
|
||||||
e.msg.metadata[gameState.getPlayerID()]["worker-object"].updateGathererCounts(gameState, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
@ -1,14 +0,0 @@
|
|||||||
Entity.prototype.deleteMetadata = function(id) {
|
|
||||||
delete this._ai._entityMetadata[this.id()];
|
|
||||||
};
|
|
||||||
|
|
||||||
Entity.prototype.garrison = function(target) {
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false});
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
Entity.prototype.attack = function(unitId)
|
|
||||||
{
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "attack", "entities": [this.id()], "target": unitId, "queued": false});
|
|
||||||
return this;
|
|
||||||
};
|
|
@ -1,40 +0,0 @@
|
|||||||
EntityCollection.prototype.attack = function(unit)
|
|
||||||
{
|
|
||||||
var unitId;
|
|
||||||
if (typeOf(unit) === "Entity"){
|
|
||||||
unitId = unit.id();
|
|
||||||
}else{
|
|
||||||
unitId = unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.PostCommand(PlayerID, {"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
function EntityCollectionFromIds(gameState, idList){
|
|
||||||
var ents = {};
|
|
||||||
for (var i in idList){
|
|
||||||
var id = idList[i];
|
|
||||||
if (gameState.entities._entities[id]) {
|
|
||||||
ents[id] = gameState.entities._entities[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new EntityCollection(gameState.ai, ents);
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityCollection.prototype.getCentrePosition = function(){
|
|
||||||
var sumPos = [0, 0];
|
|
||||||
var count = 0;
|
|
||||||
this.forEach(function(ent){
|
|
||||||
if (ent.position()){
|
|
||||||
sumPos[0] += ent.position()[0];
|
|
||||||
sumPos[1] += ent.position()[1];
|
|
||||||
count ++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (count === 0){
|
|
||||||
return undefined;
|
|
||||||
}else{
|
|
||||||
return [sumPos[0]/count, sumPos[1]/count];
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,319 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides an API for the rest of the AI scripts to query the world state at a
|
|
||||||
* higher level than the raw data.
|
|
||||||
*/
|
|
||||||
var GameState = function(ai) {
|
|
||||||
MemoizeInit(this);
|
|
||||||
|
|
||||||
this.ai = ai;
|
|
||||||
this.timeElapsed = ai.timeElapsed;
|
|
||||||
this.templates = ai.templates;
|
|
||||||
this.entities = ai.entities;
|
|
||||||
this.player = ai.player;
|
|
||||||
this.playerData = ai.playerData;
|
|
||||||
this.buildingsBuilt = 0;
|
|
||||||
|
|
||||||
if (!this.ai._gameStateStore){
|
|
||||||
this.ai._gameStateStore = {};
|
|
||||||
}
|
|
||||||
this.store = this.ai._gameStateStore;
|
|
||||||
|
|
||||||
this.cellSize = 4; // Size of each map tile
|
|
||||||
|
|
||||||
this.turnCache = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.updatingCollection = function(id, filter, collection){
|
|
||||||
if (!this.store[id]){
|
|
||||||
this.store[id] = collection.filter(filter);
|
|
||||||
this.store[id].registerUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.store[id];
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getTimeElapsed = function() {
|
|
||||||
return this.timeElapsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getTemplate = function(type) {
|
|
||||||
if (!this.templates[type]){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EntityTemplate(this.templates[type]);
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.applyCiv = function(str) {
|
|
||||||
return str.replace(/\{civ\}/g, this.playerData.civ);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Resources}
|
|
||||||
*/
|
|
||||||
GameState.prototype.getResources = function() {
|
|
||||||
return new Resources(this.playerData.resourceCounts);
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getMap = function() {
|
|
||||||
return this.ai.passabilityMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getTerritoryMap = function() {
|
|
||||||
return Map.createTerritoryMap(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getPopulation = function() {
|
|
||||||
return this.playerData.popCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getPopulationLimit = function() {
|
|
||||||
return this.playerData.popLimit;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getPopulationMax = function() {
|
|
||||||
return this.playerData.popMax;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getPassabilityClassMask = function(name) {
|
|
||||||
if (!(name in this.ai.passabilityClasses)){
|
|
||||||
error("Tried to use invalid passability class name '" + name + "'");
|
|
||||||
}
|
|
||||||
return this.ai.passabilityClasses[name];
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getPlayerID = function() {
|
|
||||||
return this.player;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.isPlayerAlly = function(id) {
|
|
||||||
return this.playerData.isAlly[id];
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.isPlayerEnemy = function(id) {
|
|
||||||
return this.playerData.isEnemy[id];
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getEnemies = function(){
|
|
||||||
var ret = [];
|
|
||||||
for (var i in this.playerData.isEnemy){
|
|
||||||
if (this.playerData.isEnemy[i]){
|
|
||||||
ret.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.isEntityAlly = function(ent) {
|
|
||||||
if (ent && ent.owner && (typeof ent.owner) === "function"){
|
|
||||||
return this.playerData.isAlly[ent.owner()];
|
|
||||||
} else if (ent && ent.owner){
|
|
||||||
return this.playerData.isAlly[ent.owner];
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.isEntityEnemy = function(ent) {
|
|
||||||
if (ent && ent.owner && (typeof ent.owner) === "function"){
|
|
||||||
return this.playerData.isEnemy[ent.owner()];
|
|
||||||
} else if (ent && ent.owner){
|
|
||||||
return this.playerData.isEnemy[ent.owner];
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.isEntityOwn = function(ent) {
|
|
||||||
if (ent && ent.owner && (typeof ent.owner) === "function"){
|
|
||||||
return ent.owner() == this.player;
|
|
||||||
} else if (ent && ent.owner){
|
|
||||||
return ent.owner == this.player;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getOwnEntities = function() {
|
|
||||||
if (!this.store.ownEntities){
|
|
||||||
this.store.ownEntities = this.getEntities().filter(Filters.byOwner(this.player));
|
|
||||||
this.store.ownEntities.registerUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.store.ownEntities;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getEnemyEntities = function() {
|
|
||||||
var diplomacyChange = false;
|
|
||||||
var enemies = this.getEnemies();
|
|
||||||
if (this.store.enemies){
|
|
||||||
if (this.store.enemies.length != enemies.length){
|
|
||||||
diplomacyChange = true;
|
|
||||||
}else{
|
|
||||||
for (var i = 0; i < enemies.length; i++){
|
|
||||||
if (enemies[i] !== this.store.enemies[i]){
|
|
||||||
diplomacyChange = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (diplomacyChange || !this.store.enemyEntities){
|
|
||||||
var filter = Filters.byOwners(enemies);
|
|
||||||
this.store.enemyEntities = this.getEntities().filter(filter);
|
|
||||||
this.store.enemyEntities.registerUpdates();
|
|
||||||
this.store.enemies = enemies;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.store.enemyEntities;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getEntities = function() {
|
|
||||||
return this.entities;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getEntityById = function(id){
|
|
||||||
if (this.entities._entities[id]) {
|
|
||||||
return this.entities._entities[id];
|
|
||||||
}else{
|
|
||||||
//debug("Entity " + id + " requested does not exist");
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getOwnEntitiesByMetadata = function(key, value){
|
|
||||||
if (!this.store[key + "-" + value]){
|
|
||||||
var filter = Filters.byMetadata(key, value);
|
|
||||||
this.store[key + "-" + value] = this.getOwnEntities().filter(filter);
|
|
||||||
this.store[key + "-" + value].registerUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.store[key + "-" + value];
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getOwnEntitiesByRole = function(role){
|
|
||||||
return this.getOwnEntitiesByMetadata("role", role);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: fix this so it picks up not in use training stuff
|
|
||||||
GameState.prototype.getOwnTrainingFacilities = function(){
|
|
||||||
return this.updatingCollection("own-training-facilities", Filters.byTrainingQueue(), this.getOwnEntities());
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getOwnEntitiesByType = function(type){
|
|
||||||
var filter = Filters.byType(type);
|
|
||||||
return this.updatingCollection("own-by-type-" + type, filter, this.getOwnEntities());
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.countEntitiesByType = function(type) {
|
|
||||||
return this.getOwnEntitiesByType(type).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.countEntitiesAndQueuedByType = function(type) {
|
|
||||||
var count = this.countEntitiesByType(type);
|
|
||||||
|
|
||||||
// Count building foundations
|
|
||||||
count += this.countEntitiesByType("foundation|" + type);
|
|
||||||
|
|
||||||
// Count animal resources
|
|
||||||
count += this.countEntitiesByType("resource|" + type);
|
|
||||||
|
|
||||||
// Count entities in building production queues
|
|
||||||
this.getOwnTrainingFacilities().forEach(function(ent){
|
|
||||||
ent.trainingQueue().forEach(function(item) {
|
|
||||||
if (item.template == type){
|
|
||||||
count += item.count;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.countFoundationsWithType = function(type) {
|
|
||||||
var foundationType = "foundation|" + type;
|
|
||||||
var count = 0;
|
|
||||||
this.getOwnEntities().forEach(function(ent) {
|
|
||||||
var t = ent.templateName();
|
|
||||||
if (t == foundationType)
|
|
||||||
++count;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.countOwnEntitiesByRole = function(role) {
|
|
||||||
return this.getOwnEntitiesByRole(role).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) {
|
|
||||||
var count = this.countOwnEntitiesByRole(role);
|
|
||||||
|
|
||||||
// Count entities in building production queues
|
|
||||||
this.getOwnTrainingFacilities().forEach(function(ent) {
|
|
||||||
ent.trainingQueue().forEach(function(item) {
|
|
||||||
if (item.metadata && item.metadata.role == role)
|
|
||||||
count += item.count;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find buildings that are capable of training the given unit type, and aren't
|
|
||||||
* already too busy.
|
|
||||||
*/
|
|
||||||
GameState.prototype.findTrainers = function(template) {
|
|
||||||
var maxQueueLength = 2; // avoid tying up resources in giant training queues
|
|
||||||
|
|
||||||
return this.getOwnTrainingFacilities().filter(function(ent) {
|
|
||||||
|
|
||||||
var trainable = ent.trainableEntities();
|
|
||||||
if (!trainable || trainable.indexOf(template) == -1)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var queue = ent.trainingQueue();
|
|
||||||
if (queue) {
|
|
||||||
if (queue.length >= maxQueueLength)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find units that are capable of constructing the given building type.
|
|
||||||
*/
|
|
||||||
GameState.prototype.findBuilders = function(template) {
|
|
||||||
return this.getOwnEntities().filter(function(ent) {
|
|
||||||
|
|
||||||
var buildable = ent.buildableEntities();
|
|
||||||
if (!buildable || buildable.indexOf(template) == -1)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getOwnFoundations = function() {
|
|
||||||
return this.updatingCollection("ownFoundations", Filters.isFoundation(), this.getOwnEntities());
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getOwnDropsites = function(resource){
|
|
||||||
return this.updatingCollection("dropsite-own-" + resource, Filters.isDropsite(resource), this.getOwnEntities());
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getResourceSupplies = function(resource){
|
|
||||||
return this.updatingCollection("resource-" + resource, Filters.byResource(resource), this.getEntities());
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getEntityLimits = function() {
|
|
||||||
return this.playerData.entityLimits;
|
|
||||||
};
|
|
||||||
|
|
||||||
GameState.prototype.getEntityCounts = function() {
|
|
||||||
return this.playerData.entityCounts;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Checks whether the maximum number of buildings have been constructed for a certain catergory
|
|
||||||
GameState.prototype.isEntityLimitReached = function(category) {
|
|
||||||
if(this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
|
|
||||||
return false;
|
|
||||||
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
// Decides when to a new house needs to be built
|
|
||||||
var HousingManager = function() {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
HousingManager.prototype.buildMoreHouses = function(gameState, queues) {
|
|
||||||
// temporary 'remaining population space' based check, need to do
|
|
||||||
// predictive in future
|
|
||||||
if (gameState.getPopulationLimit() - gameState.getPopulation() < 20
|
|
||||||
&& gameState.getPopulationLimit() < gameState.getPopulationMax()) {
|
|
||||||
var numConstructing = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"));
|
|
||||||
var numPlanned = queues.house.totalLength();
|
|
||||||
|
|
||||||
var additional = Math.ceil((20 - (gameState.getPopulationLimit() - gameState.getPopulation())) / 10)
|
|
||||||
- numConstructing - numPlanned;
|
|
||||||
|
|
||||||
for ( var i = 0; i < additional; i++) {
|
|
||||||
queues.house.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_house"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
HousingManager.prototype.update = function(gameState, queues) {
|
|
||||||
Engine.ProfileStart("housing update");
|
|
||||||
|
|
||||||
this.buildMoreHouses(gameState, queues);
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
@ -1,339 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 2, June 1991
|
|
||||||
|
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
|
||||||
the GNU Lesser General Public License instead.) You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
this service if you wish), that you receive source code or can get it
|
|
||||||
if you want it, that you can change the software or use pieces of it
|
|
||||||
in new free programs; and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. You must make sure that they, too, receive or can get the
|
|
||||||
source code. And you must show them these terms so they know their
|
|
||||||
rights.
|
|
||||||
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Program's
|
|
||||||
source code as you receive it, in any medium, provided that you
|
|
||||||
conspicuously and appropriately publish on each copy an appropriate
|
|
||||||
copyright notice and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
b) You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
|
|
||||||
c) If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
|
|
||||||
a) Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
|
|
||||||
b) Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
|
|
||||||
c) Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major components (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
4. You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
|
|
||||||
5. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
|
|
||||||
6. Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
|
|
||||||
7. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
8. If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
|
|
||||||
9. The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
|
|
||||||
10. If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
|
||||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
|
||||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
|
||||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
|
||||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
|
||||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
|
||||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
|
||||||
REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
|
||||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
|
||||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
|
||||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
|
||||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along
|
|
||||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program is interactive, make it output a short notice like this
|
|
||||||
when it starts in an interactive mode:
|
|
||||||
|
|
||||||
Gnomovision version 69, Copyright (C) year name of author
|
|
||||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, the commands you use may
|
|
||||||
be called something other than `show w' and `show c'; they could even be
|
|
||||||
mouse-clicks or menu items--whatever suits your program.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
|
||||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
|
||||||
|
|
||||||
<signature of Ty Coon>, 1 April 1989
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
This General Public License does not permit incorporating your program into
|
|
||||||
proprietary programs. If your program is a subroutine library, you may
|
|
||||||
consider it more useful to permit linking proprietary applications with the
|
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License.
|
|
@ -1,263 +0,0 @@
|
|||||||
const TERRITORY_PLAYER_MASK = 0x3F;
|
|
||||||
|
|
||||||
//TODO: Make this cope with negative cell values
|
|
||||||
function Map(gameState, originalMap){
|
|
||||||
// get the map to find out the correct dimensions
|
|
||||||
var gameMap = gameState.getMap();
|
|
||||||
this.width = gameMap.width;
|
|
||||||
this.height = gameMap.height;
|
|
||||||
this.length = gameMap.data.length;
|
|
||||||
if (originalMap){
|
|
||||||
this.map = originalMap;
|
|
||||||
}else{
|
|
||||||
this.map = new Uint16Array(this.length);
|
|
||||||
}
|
|
||||||
this.cellSize = gameState.cellSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map.prototype.gamePosToMapPos = function(p){
|
|
||||||
return [Math.round(p[0]/this.cellSize), Math.round(p[1]/this.cellSize)];
|
|
||||||
};
|
|
||||||
|
|
||||||
Map.prototype.point = function(p){
|
|
||||||
var q = this.gamePosToMapPos(p);
|
|
||||||
return this.map[q[0] + this.width * q[1]];
|
|
||||||
};
|
|
||||||
|
|
||||||
Map.createObstructionMap = function(gameState, template){
|
|
||||||
var passabilityMap = gameState.getMap();
|
|
||||||
var territoryMap = gameState.ai.territoryMap;
|
|
||||||
|
|
||||||
// default values
|
|
||||||
var placementType = "land";
|
|
||||||
var buildOwn = true;
|
|
||||||
var buildAlly = true;
|
|
||||||
var buildNeutral = true;
|
|
||||||
var buildEnemy = false;
|
|
||||||
// If there is a template then replace the defaults
|
|
||||||
if (template){
|
|
||||||
placementType = template.buildPlacementType();
|
|
||||||
buildOwn = template.hasBuildTerritory("own");
|
|
||||||
buildAlly = template.hasBuildTerritory("ally");
|
|
||||||
buildNeutral = template.hasBuildTerritory("neutral");
|
|
||||||
buildEnemy = template.hasBuildTerritory("enemy");
|
|
||||||
}
|
|
||||||
|
|
||||||
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction");
|
|
||||||
// Only accept valid land tiles (we don't handle docks yet)
|
|
||||||
switch(placementType){
|
|
||||||
case "shore":
|
|
||||||
obstructionMask |= gameState.getPassabilityClassMask("building-shore");
|
|
||||||
break;
|
|
||||||
case "land":
|
|
||||||
default:
|
|
||||||
obstructionMask |= gameState.getPassabilityClassMask("building-land");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerID = gameState.getPlayerID();
|
|
||||||
|
|
||||||
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
|
|
||||||
for (var i = 0; i < passabilityMap.data.length; ++i)
|
|
||||||
{
|
|
||||||
var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK);
|
|
||||||
var invalidTerritory = (
|
|
||||||
(!buildOwn && tilePlayer == playerID) ||
|
|
||||||
(!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) ||
|
|
||||||
(!buildNeutral && tilePlayer == 0) ||
|
|
||||||
(!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
|
|
||||||
);
|
|
||||||
var tileAccessible = (gameState.ai.accessibility.map[i] == 1);
|
|
||||||
obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 65535;
|
|
||||||
}
|
|
||||||
|
|
||||||
var map = new Map(gameState, obstructionTiles);
|
|
||||||
|
|
||||||
if (template && template.buildDistance()){
|
|
||||||
var minDist = template.buildDistance().MinDistance;
|
|
||||||
var category = template.buildDistance().FromCategory;
|
|
||||||
if (minDist !== undefined && category !== undefined){
|
|
||||||
gameState.getOwnEntities().forEach(function(ent) {
|
|
||||||
if (ent.buildCategory() === category && ent.position()){
|
|
||||||
var pos = ent.position();
|
|
||||||
var x = Math.round(pos[0] / gameState.cellSize);
|
|
||||||
var z = Math.round(pos[1] / gameState.cellSize);
|
|
||||||
map.addInfluence(x, z, minDist/gameState.cellSize, -65535, 'constant');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
|
|
||||||
Map.createTerritoryMap = function(gameState) {
|
|
||||||
var map = gameState.ai.territoryMap;
|
|
||||||
|
|
||||||
var ret = new Map(gameState, map.data);
|
|
||||||
|
|
||||||
ret.getOwner = function(p) {
|
|
||||||
return this.point(p) & TERRITORY_PLAYER_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) {
|
|
||||||
strength = strength ? strength : maxDist;
|
|
||||||
type = type ? type : 'linear';
|
|
||||||
|
|
||||||
var x0 = Math.max(0, cx - maxDist);
|
|
||||||
var y0 = Math.max(0, cy - maxDist);
|
|
||||||
var x1 = Math.min(this.width, cx + maxDist);
|
|
||||||
var y1 = Math.min(this.height, cy + maxDist);
|
|
||||||
var maxDist2 = maxDist * maxDist;
|
|
||||||
|
|
||||||
var str = 0;
|
|
||||||
switch (type){
|
|
||||||
case 'linear':
|
|
||||||
str = strength / maxDist;
|
|
||||||
break;
|
|
||||||
case 'quadratic':
|
|
||||||
str = strength / maxDist2;
|
|
||||||
break;
|
|
||||||
case 'constant':
|
|
||||||
str = strength;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( var y = y0; y < y1; ++y) {
|
|
||||||
for ( var x = x0; x < x1; ++x) {
|
|
||||||
var dx = x - cx;
|
|
||||||
var dy = y - cy;
|
|
||||||
var r2 = dx*dx + dy*dy;
|
|
||||||
if (r2 < maxDist2){
|
|
||||||
var quant = 0;
|
|
||||||
switch (type){
|
|
||||||
case 'linear':
|
|
||||||
var r = Math.sqrt(r2);
|
|
||||||
quant = str * (maxDist - r);
|
|
||||||
break;
|
|
||||||
case 'quadratic':
|
|
||||||
quant = str * (maxDist2 - r2);
|
|
||||||
break;
|
|
||||||
case 'constant':
|
|
||||||
quant = str;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-1 * quant > this.map[x + y * this.width]){
|
|
||||||
this.map[x + y * this.width] = 0; //set anything which would have gone negative to 0
|
|
||||||
}else{
|
|
||||||
this.map[x + y * this.width] += quant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Map.prototype.sumInfluence = function(cx, cy, radius){
|
|
||||||
var x0 = Math.max(0, cx - radius);
|
|
||||||
var y0 = Math.max(0, cy - radius);
|
|
||||||
var x1 = Math.min(this.width, cx + radius);
|
|
||||||
var y1 = Math.min(this.height, cy + radius);
|
|
||||||
var radius2 = radius * radius;
|
|
||||||
|
|
||||||
var sum = 0;
|
|
||||||
|
|
||||||
for ( var y = y0; y < y1; ++y) {
|
|
||||||
for ( var x = x0; x < x1; ++x) {
|
|
||||||
var dx = x - cx;
|
|
||||||
var dy = y - cy;
|
|
||||||
var r2 = dx*dx + dy*dy;
|
|
||||||
if (r2 < radius2){
|
|
||||||
sum += this.map[x + y * this.width];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make each cell's 16-bit value at least one greater than each of its
|
|
||||||
* neighbours' values. (If the grid is initialised with 0s and 65535s, the
|
|
||||||
* result of each cell is its Manhattan distance to the nearest 0.)
|
|
||||||
*
|
|
||||||
* TODO: maybe this should be 8-bit (and clamp at 255)?
|
|
||||||
*/
|
|
||||||
Map.prototype.expandInfluences = function() {
|
|
||||||
var w = this.width;
|
|
||||||
var h = this.height;
|
|
||||||
var grid = this.map;
|
|
||||||
for ( var y = 0; y < h; ++y) {
|
|
||||||
var min = 65535;
|
|
||||||
for ( var x = 0; x < w; ++x) {
|
|
||||||
var g = grid[x + y * w];
|
|
||||||
if (g > min)
|
|
||||||
grid[x + y * w] = min;
|
|
||||||
else if (g < min)
|
|
||||||
min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( var x = w - 2; x >= 0; --x) {
|
|
||||||
var g = grid[x + y * w];
|
|
||||||
if (g > min)
|
|
||||||
grid[x + y * w] = min;
|
|
||||||
else if (g < min)
|
|
||||||
min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( var x = 0; x < w; ++x) {
|
|
||||||
var min = 65535;
|
|
||||||
for ( var y = 0; y < h; ++y) {
|
|
||||||
var g = grid[x + y * w];
|
|
||||||
if (g > min)
|
|
||||||
grid[x + y * w] = min;
|
|
||||||
else if (g < min)
|
|
||||||
min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( var y = h - 2; y >= 0; --y) {
|
|
||||||
var g = grid[x + y * w];
|
|
||||||
if (g > min)
|
|
||||||
grid[x + y * w] = min;
|
|
||||||
else if (g < min)
|
|
||||||
min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Map.prototype.findBestTile = function(radius, obstructionTiles){
|
|
||||||
// Find the best non-obstructed tile
|
|
||||||
var bestIdx = 0;
|
|
||||||
var bestVal = -1;
|
|
||||||
for ( var i = 0; i < this.length; ++i) {
|
|
||||||
if (obstructionTiles.map[i] > radius) {
|
|
||||||
var v = this.map[i];
|
|
||||||
if (v > bestVal) {
|
|
||||||
bestVal = v;
|
|
||||||
bestIdx = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [bestIdx, bestVal];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Multiplies current map by the parameter map pixelwise
|
|
||||||
Map.prototype.multiply = function(map){
|
|
||||||
for (var i = 0; i < this.length; i++){
|
|
||||||
this.map[i] *= map.map[i];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Map.prototype.dumpIm = function(name, threshold){
|
|
||||||
name = name ? name : "default.png";
|
|
||||||
threshold = threshold ? threshold : 256;
|
|
||||||
Engine.DumpImage(name, this.map, this.width, this.height, threshold);
|
|
||||||
};
|
|
@ -1,461 +0,0 @@
|
|||||||
/*
|
|
||||||
* Military strategy:
|
|
||||||
* * Try training an attack squad of a specified size
|
|
||||||
* * When it's the appropriate size, send it to attack the enemy
|
|
||||||
* * Repeat forever
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
var MilitaryAttackManager = function(Config) {
|
|
||||||
|
|
||||||
this.Config = Config
|
|
||||||
// these use the structure soldiers[unitId] = true|false to register the units
|
|
||||||
this.attackManagers = [AttackMoveToLocation];
|
|
||||||
this.availableAttacks = [];
|
|
||||||
this.currentAttacks = [];
|
|
||||||
|
|
||||||
// Counts how many attacks we have sent at the enemy.
|
|
||||||
this.attackCount = 0;
|
|
||||||
this.lastAttackTime = 0;
|
|
||||||
|
|
||||||
this.defenceManager = new Defence(Config);
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.init = function(gameState) {
|
|
||||||
var civ = gameState.playerData.civ;
|
|
||||||
|
|
||||||
// load units and buildings from the config files
|
|
||||||
|
|
||||||
if (civ in this.Config.buildings.moderate){
|
|
||||||
this.bModerate = this.Config.buildings.moderate[civ];
|
|
||||||
}else{
|
|
||||||
this.bModerate = this.Config.buildings.moderate['default'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (civ in this.Config.buildings.advanced){
|
|
||||||
this.bAdvanced = this.Config.buildings.advanced[civ];
|
|
||||||
}else{
|
|
||||||
this.bAdvanced = this.Config.buildings.advanced['default'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (civ in this.Config.buildings.fort){
|
|
||||||
this.bFort = this.Config.buildings.fort[civ];
|
|
||||||
}else{
|
|
||||||
this.bFort = this.Config.buildings.fort['default'];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i in this.bAdvanced){
|
|
||||||
this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]);
|
|
||||||
}
|
|
||||||
for (var i in this.bFort){
|
|
||||||
this.bFort[i] = gameState.applyCiv(this.bFort[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getEconomicTargets = function(gameState, militaryManager){
|
|
||||||
return militaryManager.getEnemyBuildings(gameState, "Economic");
|
|
||||||
};
|
|
||||||
// TODO: figure out how to make this generic
|
|
||||||
for (var i in this.attackManagers){
|
|
||||||
this.availableAttacks[i] = new this.attackManagers[i](gameState, this.Config, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
var enemies = gameState.getEnemyEntities();
|
|
||||||
var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]);
|
|
||||||
this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes
|
|
||||||
this.enemySoldiers.registerUpdates();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param (GameState) gameState
|
|
||||||
* @param (string) soldierTypes
|
|
||||||
* @returns array of soldiers for which training buildings exist
|
|
||||||
*/
|
|
||||||
MilitaryAttackManager.prototype.findTrainableUnits = function(gameState, soldierType){
|
|
||||||
var allTrainable = [];
|
|
||||||
gameState.getOwnEntities().forEach(function(ent) {
|
|
||||||
var trainable = ent.trainableEntities();
|
|
||||||
for (var i in trainable){
|
|
||||||
if (allTrainable.indexOf(trainable[i]) === -1){
|
|
||||||
allTrainable.push(trainable[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var ret = [];
|
|
||||||
for (var i in allTrainable){
|
|
||||||
var template = gameState.getTemplate(allTrainable[i]);
|
|
||||||
if (soldierType == this.getSoldierType(template)){
|
|
||||||
ret.push(allTrainable[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the type of a soldier, either citizenSoldier, advanced or siege
|
|
||||||
MilitaryAttackManager.prototype.getSoldierType = function(ent){
|
|
||||||
if (ent.hasClass("Hero")){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (ent.hasClass("CitizenSoldier") && !ent.hasClass("Cavalry")){
|
|
||||||
return "citizenSoldier";
|
|
||||||
}else if (ent.hasClass("Champion") || ent.hasClass("CitizenSoldier")){
|
|
||||||
return "advanced";
|
|
||||||
}else if (ent.hasClass("Siege")){
|
|
||||||
return "siege";
|
|
||||||
}else{
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unit type we should begin training. (Currently this is whatever
|
|
||||||
* we have least of.)
|
|
||||||
*/
|
|
||||||
MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, soldierType) {
|
|
||||||
var units = this.findTrainableUnits(gameState, soldierType);
|
|
||||||
// Count each type
|
|
||||||
var types = [];
|
|
||||||
for ( var tKey in units) {
|
|
||||||
var t = units[tKey];
|
|
||||||
types.push([t, gameState.countEntitiesAndQueuedByType(gameState.applyCiv(t))
|
|
||||||
+ queue.countAllByType(gameState.applyCiv(t)) ]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by increasing count
|
|
||||||
types.sort(function(a, b) {
|
|
||||||
return a[1] - b[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (types.length === 0){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return types[0][0];
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
|
|
||||||
var soldiers = gameState.getOwnEntitiesByRole("soldier");
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
soldiers.forEach(function(ent) {
|
|
||||||
ent.setMetadata("role", "military");
|
|
||||||
ent.setMetadata("military", "unassigned");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// return count of enemy buildings for a given building class
|
|
||||||
MilitaryAttackManager.prototype.getEnemyBuildings = function(gameState,cls) {
|
|
||||||
var targets = gameState.entities.filter(function(ent) {
|
|
||||||
return (gameState.isEntityEnemy(ent) && ent.hasClass("Structure") && ent.hasClass(cls) && ent.owner() !== 0 && ent.position());
|
|
||||||
});
|
|
||||||
return targets;
|
|
||||||
};
|
|
||||||
|
|
||||||
// return n available units and makes these units unavailable
|
|
||||||
MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
|
|
||||||
var ret = [];
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var units = undefined;
|
|
||||||
|
|
||||||
if (filter){
|
|
||||||
units = this.getUnassignedUnits().filter(filter);
|
|
||||||
}else{
|
|
||||||
units = this.getUnassignedUnits();
|
|
||||||
}
|
|
||||||
|
|
||||||
units.forEach(function(ent){
|
|
||||||
ret.push(ent.id());
|
|
||||||
ent.setMetadata("military", "assigned");
|
|
||||||
ent.setMetadata("role", "military");
|
|
||||||
count++;
|
|
||||||
if (count >= n) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Takes a single unit id, and marks it unassigned
|
|
||||||
MilitaryAttackManager.prototype.unassignUnit = function(unit){
|
|
||||||
this.entity(unit).setMetadata("military", "unassigned");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Takes an array of unit id's and marks all of them unassigned
|
|
||||||
MilitaryAttackManager.prototype.unassignUnits = function(units){
|
|
||||||
for (var i in units){
|
|
||||||
this.unassignUnit(units[i]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.getUnassignedUnits = function(){
|
|
||||||
return this.gameState.getOwnEntitiesByMetadata("military", "unassigned");
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.countAvailableUnits = function(filter){
|
|
||||||
var count = 0;
|
|
||||||
if (filter){
|
|
||||||
return this.getUnassignedUnits().filter(filter).length;
|
|
||||||
}else{
|
|
||||||
return this.getUnassignedUnits().length;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Takes an entity id and returns an entity object or undefined if there is no entity with that id
|
|
||||||
// Also sends a debug message warning if the id has no entity
|
|
||||||
MilitaryAttackManager.prototype.entity = function(id) {
|
|
||||||
return this.gameState.getEntityById(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the military strength of unit
|
|
||||||
MilitaryAttackManager.prototype.getUnitStrength = function(ent){
|
|
||||||
var strength = 0.0;
|
|
||||||
var attackTypes = ent.attackTypes();
|
|
||||||
var armourStrength = ent.armourStrengths();
|
|
||||||
var hp = 2 * ent.hitpoints() / (160 + 1*ent.maxHitpoints()); //100 = typical number of hitpoints
|
|
||||||
for (var typeKey in attackTypes) {
|
|
||||||
var type = attackTypes[typeKey];
|
|
||||||
var attackStrength = ent.attackStrengths(type);
|
|
||||||
var attackRange = ent.attackRange(type);
|
|
||||||
var attackTimes = ent.attackTimes(type);
|
|
||||||
for (var str in attackStrength) {
|
|
||||||
var val = parseFloat(attackStrength[str]);
|
|
||||||
switch (str) {
|
|
||||||
case "crush":
|
|
||||||
strength += (val * 0.085) / 3;
|
|
||||||
break;
|
|
||||||
case "hack":
|
|
||||||
strength += (val * 0.075) / 3;
|
|
||||||
break;
|
|
||||||
case "pierce":
|
|
||||||
strength += (val * 0.065) / 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (attackRange){
|
|
||||||
strength += (attackRange.max * 0.0125) ;
|
|
||||||
}
|
|
||||||
for (var str in attackTimes) {
|
|
||||||
var val = parseFloat(attackTimes[str]);
|
|
||||||
switch (str){
|
|
||||||
case "repeat":
|
|
||||||
strength += (val / 100000);
|
|
||||||
break;
|
|
||||||
case "prepare":
|
|
||||||
strength -= (val / 100000);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var str in armourStrength) {
|
|
||||||
var val = parseFloat(armourStrength[str]);
|
|
||||||
switch (str) {
|
|
||||||
case "crush":
|
|
||||||
strength += (val * 0.085) / 3;
|
|
||||||
break;
|
|
||||||
case "hack":
|
|
||||||
strength += (val * 0.075) / 3;
|
|
||||||
break;
|
|
||||||
case "pierce":
|
|
||||||
strength += (val * 0.065) / 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strength * hp;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the strength of the available units of ai army
|
|
||||||
MilitaryAttackManager.prototype.measureAvailableStrength = function(){
|
|
||||||
var strength = 0.0;
|
|
||||||
var self = this;
|
|
||||||
this.getUnassignedUnits(this.gameState).forEach(function(ent){
|
|
||||||
strength += self.getUnitStrength(ent);
|
|
||||||
});
|
|
||||||
return strength;
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.getEnemySoldiers = function(){
|
|
||||||
return this.enemySoldiers;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the number of units in the largest enemy army
|
|
||||||
MilitaryAttackManager.prototype.measureEnemyCount = function(gameState){
|
|
||||||
// Measure enemy units
|
|
||||||
var isEnemy = gameState.playerData.isEnemy;
|
|
||||||
var enemyCount = [];
|
|
||||||
var maxCount = 0;
|
|
||||||
for ( var i = 1; i < isEnemy.length; i++) {
|
|
||||||
enemyCount[i] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through the enemy soldiers and add one to the count for that soldiers player's count
|
|
||||||
this.enemySoldiers.forEach(function(ent) {
|
|
||||||
enemyCount[ent.owner()]++;
|
|
||||||
|
|
||||||
if (enemyCount[ent.owner()] > maxCount) {
|
|
||||||
maxCount = enemyCount[ent.owner()];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return maxCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the strength of the largest enemy army
|
|
||||||
MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
|
|
||||||
// Measure enemy strength
|
|
||||||
var isEnemy = gameState.playerData.isEnemy;
|
|
||||||
var enemyStrength = [];
|
|
||||||
var maxStrength = 0;
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
for ( var i = 1; i < isEnemy.length; i++) {
|
|
||||||
enemyStrength[i] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through the enemy soldiers and add the strength to that soldiers player's total strength
|
|
||||||
this.enemySoldiers.forEach(function(ent) {
|
|
||||||
enemyStrength[ent.owner()] += self.getUnitStrength(ent);
|
|
||||||
|
|
||||||
if (enemyStrength[ent.owner()] > maxStrength) {
|
|
||||||
maxStrength = enemyStrength[ent.owner()];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return maxStrength;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Adds towers to the defenceBuilding queue
|
|
||||||
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
|
|
||||||
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'))
|
|
||||||
+ queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"]) {
|
|
||||||
|
|
||||||
|
|
||||||
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
|
|
||||||
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata("defenseTower") !== true){
|
|
||||||
var position = dropsiteEnt.position();
|
|
||||||
if (position){
|
|
||||||
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_defense_tower', position));
|
|
||||||
}
|
|
||||||
dropsiteEnt.setMetadata("defenseTower", true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var numFortresses = 0;
|
|
||||||
for (var i in this.bFort){
|
|
||||||
numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numFortresses + queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["Fortress"]) {
|
|
||||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules["economy"].targetNumWorkers * 0.5){
|
|
||||||
if (gameState.getTimeElapsed() > 350 * 1000 * numFortresses){
|
|
||||||
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){
|
|
||||||
var position = gameState.ai.pathsToMe.shift();
|
|
||||||
// TODO: pick a fort randomly from the list.
|
|
||||||
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0], position));
|
|
||||||
}else{
|
|
||||||
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState, queues) {
|
|
||||||
// Build more military buildings
|
|
||||||
// TODO: make military building better
|
|
||||||
Engine.ProfileStart("Build buildings");
|
|
||||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 30) {
|
|
||||||
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0]))
|
|
||||||
+ queues.militaryBuilding.totalLength() < 1) {
|
|
||||||
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//build advanced military buildings
|
|
||||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) >
|
|
||||||
gameState.ai.modules["economy"].targetNumWorkers * 0.7){
|
|
||||||
if (queues.militaryBuilding.totalLength() === 0){
|
|
||||||
for (var i in this.bAdvanced){
|
|
||||||
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){
|
|
||||||
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.trainMilitaryUnits = function(gameState, queues){
|
|
||||||
Engine.ProfileStart("Train Units");
|
|
||||||
// Continually try training new units, in batches of 5
|
|
||||||
if (queues.citizenSoldier.length() < 6) {
|
|
||||||
var newUnit = this.findBestNewUnit(gameState, queues.citizenSoldier, "citizenSoldier");
|
|
||||||
if (newUnit){
|
|
||||||
queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
|
||||||
"role" : "soldier"
|
|
||||||
}, 5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (queues.advancedSoldier.length() < 2) {
|
|
||||||
var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, "advanced");
|
|
||||||
if (newUnit){
|
|
||||||
queues.advancedSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
|
||||||
"role" : "soldier"
|
|
||||||
}, 5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (queues.siege.length() < 4) {
|
|
||||||
var newUnit = this.findBestNewUnit(gameState, queues.siege, "siege");
|
|
||||||
if (newUnit){
|
|
||||||
queues.siege.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
|
||||||
"role" : "soldier"
|
|
||||||
}, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
|
|
||||||
var self = this;
|
|
||||||
Engine.ProfileStart("military update");
|
|
||||||
this.gameState = gameState;
|
|
||||||
|
|
||||||
this.registerSoldiers(gameState);
|
|
||||||
|
|
||||||
this.trainMilitaryUnits(gameState, queues);
|
|
||||||
|
|
||||||
this.constructTrainingBuildings(gameState, queues);
|
|
||||||
|
|
||||||
this.buildDefences(gameState, queues);
|
|
||||||
|
|
||||||
this.defenceManager.update(gameState, events, this);
|
|
||||||
|
|
||||||
Engine.ProfileStart("Plan new attacks");
|
|
||||||
// Look for attack plans which can be executed, only do this once every minute
|
|
||||||
for (var i = 0; i < this.availableAttacks.length; i++){
|
|
||||||
if (this.availableAttacks[i].canExecute(gameState, this)){
|
|
||||||
this.availableAttacks[i].execute(gameState, this);
|
|
||||||
this.currentAttacks.push(this.availableAttacks[i]);
|
|
||||||
//debug("Attacking!");
|
|
||||||
}
|
|
||||||
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this.Config, this));
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStart("Update attacks");
|
|
||||||
// Keep current attacks updated
|
|
||||||
for (var i in this.currentAttacks){
|
|
||||||
this.currentAttacks[i].update(gameState, this, events);
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStart("Use idle military as workers");
|
|
||||||
// Set unassigned to be workers TODO: fix this so it doesn't scan all units every time
|
|
||||||
this.getUnassignedUnits(gameState).forEach(function(ent){
|
|
||||||
if (self.getSoldierType(ent) === "citizenSoldier"){
|
|
||||||
ent.setMetadata("role", "worker");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
@ -1,124 +0,0 @@
|
|||||||
var BuildingConstructionPlan = function(gameState, type, position) {
|
|
||||||
this.type = gameState.applyCiv(type);
|
|
||||||
this.position = position;
|
|
||||||
|
|
||||||
var template = gameState.getTemplate(this.type);
|
|
||||||
if (!template) {
|
|
||||||
this.invalidTemplate = true;
|
|
||||||
debug("Cannot build " + this.type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.category = "building";
|
|
||||||
this.cost = new Resources(template.cost());
|
|
||||||
this.number = 1; // The number of buildings to build
|
|
||||||
};
|
|
||||||
|
|
||||||
BuildingConstructionPlan.prototype.canExecute = function(gameState) {
|
|
||||||
if (this.invalidTemplate){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: verify numeric limits etc
|
|
||||||
|
|
||||||
var builders = gameState.findBuilders(this.type);
|
|
||||||
|
|
||||||
return (builders.length != 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
BuildingConstructionPlan.prototype.execute = function(gameState) {
|
|
||||||
|
|
||||||
var builders = gameState.findBuilders(this.type).toEntityArray();
|
|
||||||
|
|
||||||
// We don't care which builder we assign, since they won't actually
|
|
||||||
// do the building themselves - all we care about is that there is
|
|
||||||
// some unit that can start the foundation
|
|
||||||
|
|
||||||
var pos = this.findGoodPosition(gameState);
|
|
||||||
if (!pos){
|
|
||||||
debug("No room to place " + this.type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
builders[0].construct(this.type, pos.x, pos.z, pos.angle);
|
|
||||||
};
|
|
||||||
|
|
||||||
BuildingConstructionPlan.prototype.getCost = function() {
|
|
||||||
return this.cost;
|
|
||||||
};
|
|
||||||
|
|
||||||
BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) {
|
|
||||||
var template = gameState.getTemplate(this.type);
|
|
||||||
|
|
||||||
var cellSize = gameState.cellSize; // size of each tile
|
|
||||||
|
|
||||||
// First, find all tiles that are far enough away from obstructions:
|
|
||||||
|
|
||||||
var obstructionMap = Map.createObstructionMap(gameState,template);
|
|
||||||
|
|
||||||
//obstructionMap.dumpIm("obstructions.png");
|
|
||||||
|
|
||||||
obstructionMap.expandInfluences();
|
|
||||||
|
|
||||||
// Compute each tile's closeness to friendly structures:
|
|
||||||
|
|
||||||
var friendlyTiles = new Map(gameState);
|
|
||||||
|
|
||||||
// If a position was specified then place the building as close to it as possible
|
|
||||||
if (this.position){
|
|
||||||
var x = Math.round(this.position[0] / cellSize);
|
|
||||||
var z = Math.round(this.position[1] / cellSize);
|
|
||||||
friendlyTiles.addInfluence(x, z, 200);
|
|
||||||
//friendlyTiles.dumpIm("pos.png", 200);
|
|
||||||
}else{
|
|
||||||
// No position was specified so try and find a sensible place to build
|
|
||||||
gameState.getOwnEntities().forEach(function(ent) {
|
|
||||||
if (ent.hasClass("Structure")) {
|
|
||||||
var infl = 32;
|
|
||||||
if (ent.hasClass("CivCentre"))
|
|
||||||
infl *= 4;
|
|
||||||
|
|
||||||
var pos = ent.position();
|
|
||||||
var x = Math.round(pos[0] / cellSize);
|
|
||||||
var z = Math.round(pos[1] / cellSize);
|
|
||||||
if (template._template.BuildRestrictions.Category === "Field"){
|
|
||||||
// Only care about being near a place where we can deposit food for fields
|
|
||||||
if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1){
|
|
||||||
friendlyTiles.addInfluence(x, z, infl, infl);
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
friendlyTiles.addInfluence(x, z, infl);
|
|
||||||
// If this is not a field add a negative influence near the CivCentre because we want to leave this
|
|
||||||
// area for fields.
|
|
||||||
if (ent.hasClass("CivCentre")){
|
|
||||||
friendlyTiles.addInfluence(x, z, infl/8, -infl/2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, this
|
|
||||||
// allows room for units to walk between buildings.
|
|
||||||
var radius = Math.ceil(template.obstructionRadius() / cellSize) + 2;
|
|
||||||
|
|
||||||
// Find the best non-obstructed tile
|
|
||||||
var bestTile = friendlyTiles.findBestTile(radius, obstructionMap);
|
|
||||||
var bestIdx = bestTile[0];
|
|
||||||
var bestVal = bestTile[1];
|
|
||||||
|
|
||||||
if (bestVal === -1){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var x = ((bestIdx % friendlyTiles.width) + 0.5) * cellSize;
|
|
||||||
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * cellSize;
|
|
||||||
|
|
||||||
// default angle
|
|
||||||
var angle = 3*Math.PI/4;
|
|
||||||
|
|
||||||
return {
|
|
||||||
"x" : x,
|
|
||||||
"z" : z,
|
|
||||||
"angle" : angle
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,56 +0,0 @@
|
|||||||
var UnitTrainingPlan = function(gameState, type, metadata, number) {
|
|
||||||
this.type = gameState.applyCiv(type);
|
|
||||||
this.metadata = metadata;
|
|
||||||
|
|
||||||
var template = gameState.getTemplate(this.type);
|
|
||||||
if (!template) {
|
|
||||||
this.invalidTemplate = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.category= "unit";
|
|
||||||
this.cost = new Resources(template.cost(), template._template.Cost.Population);
|
|
||||||
if (!number){
|
|
||||||
this.number = 1;
|
|
||||||
}else{
|
|
||||||
this.number = number;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
UnitTrainingPlan.prototype.canExecute = function(gameState) {
|
|
||||||
if (this.invalidTemplate)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// TODO: we should probably check pop caps
|
|
||||||
|
|
||||||
var trainers = gameState.findTrainers(this.type);
|
|
||||||
|
|
||||||
return (trainers.length != 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
UnitTrainingPlan.prototype.execute = function(gameState) {
|
|
||||||
//warn("Executing UnitTrainingPlan " + uneval(this));
|
|
||||||
|
|
||||||
var trainers = gameState.findTrainers(this.type).toEntityArray();
|
|
||||||
|
|
||||||
// Prefer training buildings with short queues
|
|
||||||
// (TODO: this should also account for units added to the queue by
|
|
||||||
// plans that have already been executed this turn)
|
|
||||||
if (trainers.length > 0){
|
|
||||||
trainers.sort(function(a, b) {
|
|
||||||
return a.trainingQueueTime() - b.trainingQueueTime();
|
|
||||||
});
|
|
||||||
|
|
||||||
trainers[0].train(this.type, this.number, this.metadata);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
UnitTrainingPlan.prototype.getCost = function(){
|
|
||||||
var multCost = new Resources();
|
|
||||||
multCost.add(this.cost);
|
|
||||||
multCost.multiply(this.number);
|
|
||||||
return multCost;
|
|
||||||
};
|
|
||||||
|
|
||||||
UnitTrainingPlan.prototype.addItem = function(){
|
|
||||||
this.number += 1;
|
|
||||||
};
|
|
@ -1,184 +0,0 @@
|
|||||||
var g_debugEnabled = false;
|
|
||||||
|
|
||||||
function QBotAI(settings) {
|
|
||||||
BaseAI.call(this, settings);
|
|
||||||
|
|
||||||
this.turn = 0;
|
|
||||||
|
|
||||||
this.Config = new Config();
|
|
||||||
|
|
||||||
this.modules = {
|
|
||||||
"economy": new EconomyManager(),
|
|
||||||
"military": new MilitaryAttackManager(this.Config),
|
|
||||||
"housing": new HousingManager()
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// this.queues cannot be modified past initialisation or queue-manager will break
|
|
||||||
this.queues = {
|
|
||||||
house : new Queue(),
|
|
||||||
citizenSoldier : new Queue(),
|
|
||||||
villager : new Queue(),
|
|
||||||
economicBuilding : new Queue(),
|
|
||||||
field : new Queue(),
|
|
||||||
advancedSoldier : new Queue(),
|
|
||||||
siege : new Queue(),
|
|
||||||
militaryBuilding : new Queue(),
|
|
||||||
defenceBuilding : new Queue(),
|
|
||||||
civilCentre: new Queue()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.productionQueues = [];
|
|
||||||
|
|
||||||
this.priorities = this.Config.priorities;
|
|
||||||
|
|
||||||
this.queueManager = new QueueManager(this.queues, this.priorities);
|
|
||||||
|
|
||||||
this.firstTime = true;
|
|
||||||
|
|
||||||
this.savedEvents = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
QBotAI.prototype = new BaseAI();
|
|
||||||
|
|
||||||
//Some modules need the gameState to fully initialise
|
|
||||||
QBotAI.prototype.runInit = function(gameState){
|
|
||||||
if (this.firstTime){
|
|
||||||
for (var i in this.modules){
|
|
||||||
if (this.modules[i].init){
|
|
||||||
this.modules[i].init(gameState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timer = new Timer();
|
|
||||||
|
|
||||||
this.firstTime = false;
|
|
||||||
|
|
||||||
var myKeyEntities = gameState.getOwnEntities().filter(function(ent) {
|
|
||||||
return ent.hasClass("CivCentre");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (myKeyEntities.length == 0){
|
|
||||||
myKeyEntities = gameState.getOwnEntities();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var filter = Filters.byClass("CivCentre");
|
|
||||||
var enemyKeyEntities = gameState.getEnemyEntities().filter(filter);
|
|
||||||
|
|
||||||
if (enemyKeyEntities.length == 0){
|
|
||||||
enemyKeyEntities = gameState.getEnemyEntities();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accessibility = new Accessibility(gameState, myKeyEntities.toEntityArray()[0].position());
|
|
||||||
|
|
||||||
if (enemyKeyEntities.length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var pathFinder = new PathFinder(gameState);
|
|
||||||
this.pathsToMe = pathFinder.getPaths(enemyKeyEntities.toEntityArray()[0].position(), myKeyEntities.toEntityArray()[0].position(), 'entryPoints');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
QBotAI.prototype.OnUpdate = function() {
|
|
||||||
if (this.gameFinished){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.events.length > 0){
|
|
||||||
this.savedEvents = this.savedEvents.concat(this.events);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the update every n turns, offset depending on player ID to balance
|
|
||||||
// the load
|
|
||||||
if ((this.turn + this.player) % 10 == 0) {
|
|
||||||
Engine.ProfileStart("qBot");
|
|
||||||
|
|
||||||
var gameState = new GameState(this);
|
|
||||||
|
|
||||||
if (gameState.getOwnEntities().length === 0){
|
|
||||||
Engine.ProfileStop();
|
|
||||||
return; // With no entities to control the AI cannot do anything
|
|
||||||
}
|
|
||||||
|
|
||||||
this.runInit(gameState);
|
|
||||||
|
|
||||||
for (var i in this.modules){
|
|
||||||
this.modules[i].update(gameState, this.queues, this.savedEvents);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateDynamicPriorities(gameState, this.queues);
|
|
||||||
|
|
||||||
this.queueManager.update(gameState);
|
|
||||||
|
|
||||||
// Generate some entropy in the random numbers (against humans) until the engine gets random initialised numbers
|
|
||||||
// TODO: remove this when the engine gives a random seed
|
|
||||||
var n = this.savedEvents.length % 29;
|
|
||||||
for (var i = 0; i < n; i++){
|
|
||||||
Math.random();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.savedEvents;
|
|
||||||
this.savedEvents = [];
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.turn++;
|
|
||||||
};
|
|
||||||
|
|
||||||
QBotAI.prototype.updateDynamicPriorities = function(gameState, queues){
|
|
||||||
// Dynamically change priorities
|
|
||||||
Engine.ProfileStart("Change Priorities");
|
|
||||||
var females = gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen"));
|
|
||||||
var femalesTarget = this.modules["economy"].targetNumWorkers;
|
|
||||||
var enemyStrength = this.modules["military"].measureEnemyStrength(gameState);
|
|
||||||
var availableStrength = this.modules["military"].measureAvailableStrength();
|
|
||||||
|
|
||||||
var additionalPriority = (enemyStrength - availableStrength) * 5;
|
|
||||||
additionalPriority = Math.min(Math.max(additionalPriority, -50), 220);
|
|
||||||
|
|
||||||
var advancedProportion = (availableStrength / 40) * (females/femalesTarget);
|
|
||||||
advancedProportion = Math.min(advancedProportion, 0.7);
|
|
||||||
|
|
||||||
this.priorities.citizenSoldier = (1-advancedProportion) * (150 + additionalPriority) + 1;
|
|
||||||
this.priorities.advancedSoldier = advancedProportion * (150 + additionalPriority) + 1;
|
|
||||||
|
|
||||||
if (females/femalesTarget > 0.7){
|
|
||||||
this.priorities.defenceBuilding = 70;
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Remove override when the whole AI state is serialised
|
|
||||||
QBotAI.prototype.Deserialize = function(data)
|
|
||||||
{
|
|
||||||
BaseAI.prototype.Deserialize.call(this, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Override the default serializer
|
|
||||||
QBotAI.prototype.Serialize = function()
|
|
||||||
{
|
|
||||||
var ret = BaseAI.prototype.Serialize.call(this);
|
|
||||||
ret._entityMetadata = {};
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
function debug(output){
|
|
||||||
if (g_debugEnabled){
|
|
||||||
if (typeof output === "string"){
|
|
||||||
warn(output);
|
|
||||||
}else{
|
|
||||||
warn(uneval(output));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyPrototype(descendant, parent) {
|
|
||||||
var sConstructor = parent.toString();
|
|
||||||
var aMatch = sConstructor.match( /\s*function (.*)\(/ );
|
|
||||||
if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; }
|
|
||||||
for (var m in parent.prototype) {
|
|
||||||
descendant.prototype[m] = parent.prototype[m];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,302 +0,0 @@
|
|||||||
//This takes the input queues and picks which items to fund with resources until no more resources are left to distribute.
|
|
||||||
//
|
|
||||||
//In this manager all resources are 'flattened' into a single type=(food+wood+metal+stone+pop*50 (see resources.js))
|
|
||||||
//the following refers to this simple as resource
|
|
||||||
//
|
|
||||||
// Each queue has an account which records the amount of resource it can spend. If no queue has an affordable item
|
|
||||||
// then the amount of resource is increased to all accounts in direct proportion to the priority until an item on one
|
|
||||||
// of the queues becomes affordable.
|
|
||||||
//
|
|
||||||
// A consequence of the system is that a rarely used queue will end up with a very large account. I am unsure if this
|
|
||||||
// is good or bad or neither.
|
|
||||||
//
|
|
||||||
// Each queue object has two queues in it, one with items waiting for resources and the other with items which have been
|
|
||||||
// allocated resources and are due to be executed. The secondary queues are helpful because then units can be trained
|
|
||||||
// in groups of 5 and buildings are built once per turn to avoid placement clashes.
|
|
||||||
|
|
||||||
var QueueManager = function(queues, priorities) {
|
|
||||||
this.queues = queues;
|
|
||||||
this.priorities = priorities;
|
|
||||||
this.account = {};
|
|
||||||
for (var p in this.queues) {
|
|
||||||
this.account[p] = 0;
|
|
||||||
}
|
|
||||||
this.curItemQueue = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueManager.prototype.getAvailableResources = function(gameState) {
|
|
||||||
var resources = gameState.getResources();
|
|
||||||
for (var key in this.queues) {
|
|
||||||
resources.subtract(this.queues[key].outQueueCost());
|
|
||||||
}
|
|
||||||
return resources;
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueManager.prototype.futureNeeds = function(gameState) {
|
|
||||||
// Work out which plans will be executed next using priority and return the total cost of these plans
|
|
||||||
var recurse = function(queues, qm, number, depth){
|
|
||||||
var needs = new Resources();
|
|
||||||
var totalPriority = 0;
|
|
||||||
for (var i = 0; i < queues.length; i++){
|
|
||||||
totalPriority += qm.priorities[queues[i]];
|
|
||||||
}
|
|
||||||
for (var i = 0; i < queues.length; i++){
|
|
||||||
var num = Math.round(((qm.priorities[queues[i]]/totalPriority) * number));
|
|
||||||
if (num < qm.queues[queues[i]].countQueuedUnits()){
|
|
||||||
var cnt = 0;
|
|
||||||
for ( var j = 0; cnt < num; j++) {
|
|
||||||
cnt += qm.queues[queues[i]].queue[j].number;
|
|
||||||
needs.add(qm.queues[queues[i]].queue[j].getCost());
|
|
||||||
number -= qm.queues[queues[i]].queue[j].number;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
for ( var j = 0; j < qm.queues[queues[i]].length(); j++) {
|
|
||||||
needs.add(qm.queues[queues[i]].queue[j].getCost());
|
|
||||||
number -= qm.queues[queues[i]].queue[j].number;
|
|
||||||
}
|
|
||||||
queues.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check that more items were selected this call and that there are plans left to be allocated
|
|
||||||
// Also there is a fail-safe max depth
|
|
||||||
if (queues.length > 0 && number > 0 && depth < 20){
|
|
||||||
needs.add(recurse(queues, qm, number, depth + 1));
|
|
||||||
}
|
|
||||||
return needs;
|
|
||||||
};
|
|
||||||
|
|
||||||
//number of plans to look at
|
|
||||||
var current = this.getAvailableResources(gameState);
|
|
||||||
|
|
||||||
var futureNum = 20;
|
|
||||||
var queues = [];
|
|
||||||
for (var q in this.queues){
|
|
||||||
queues.push(q);
|
|
||||||
}
|
|
||||||
var needs = recurse(queues, this, futureNum, 0);
|
|
||||||
// Return predicted values minus the current stockpiles along with a base rater for all resources
|
|
||||||
return {
|
|
||||||
"food" : Math.max(needs.food - current.food, 0) + 150,
|
|
||||||
"wood" : Math.max(needs.wood + 15*needs.population - current.wood, 0) + 150, //TODO: read the house cost in case it changes in the future
|
|
||||||
"stone" : Math.max(needs.stone - current.stone, 0) + 50,
|
|
||||||
"metal" : Math.max(needs.metal - current.metal, 0) + 100
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// runs through the curItemQueue and allocates resources be sending the
|
|
||||||
// affordable plans to the Out Queues. Returns a list of the unneeded resources
|
|
||||||
// so they can be used by lower priority plans.
|
|
||||||
QueueManager.prototype.affordableToOutQueue = function(gameState) {
|
|
||||||
var availableRes = this.getAvailableResources(gameState);
|
|
||||||
if (this.curItemQueue.length === 0) {
|
|
||||||
return availableRes;
|
|
||||||
}
|
|
||||||
|
|
||||||
var resources = this.getAvailableResources(gameState);
|
|
||||||
|
|
||||||
// Check everything in the curItemQueue, if it is affordable then mark it
|
|
||||||
// for execution
|
|
||||||
for ( var i = 0; i < this.curItemQueue.length; i++) {
|
|
||||||
availableRes.subtract(this.queues[this.curItemQueue[i]].getNext().getCost());
|
|
||||||
if (resources.canAfford(this.queues[this.curItemQueue[i]].getNext().getCost())) {
|
|
||||||
this.account[this.curItemQueue[i]] -= this.queues[this.curItemQueue[i]].getNext().getCost().toInt();
|
|
||||||
this.queues[this.curItemQueue[i]].nextToOutQueue();
|
|
||||||
resources = this.getAvailableResources(gameState);
|
|
||||||
this.curItemQueue[i] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the spent items
|
|
||||||
var tmpQueue = [];
|
|
||||||
for ( var i = 0; i < this.curItemQueue.length; i++) {
|
|
||||||
if (this.curItemQueue[i] !== null) {
|
|
||||||
tmpQueue.push(this.curItemQueue[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.curItemQueue = tmpQueue;
|
|
||||||
|
|
||||||
return availableRes;
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueManager.prototype.onlyUsesSpareAndUpdateSpare = function(unitCost, spare){
|
|
||||||
// This allows plans to be given resources if there are >500 spare after all the
|
|
||||||
// higher priority plan queues have been looked at and there are still enough resources
|
|
||||||
// We make it >0 so that even if we have no stone available we can still have non stone
|
|
||||||
// plans being given resources.
|
|
||||||
var spareNonNegRes = {
|
|
||||||
food: Math.max(0, spare.food - 500),
|
|
||||||
wood: Math.max(0, spare.wood - 500),
|
|
||||||
stone: Math.max(0, spare.stone - 500),
|
|
||||||
metal: Math.max(0, spare.metal - 500)
|
|
||||||
};
|
|
||||||
var spareNonNeg = new Resources(spareNonNegRes);
|
|
||||||
var ret = false;
|
|
||||||
if (spareNonNeg.canAfford(unitCost)){
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no negative resources then there weren't any higher priority items so we
|
|
||||||
// definitely want to say that this can be added to the list.
|
|
||||||
var tmp = true;
|
|
||||||
for (var key in spare.types){
|
|
||||||
var type = spare.types[key];
|
|
||||||
if (spare[type] < 0){
|
|
||||||
tmp = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If either to the above sections returns true then
|
|
||||||
ret = ret || tmp;
|
|
||||||
|
|
||||||
spare.subtract(unitCost); // take the resources of the current unit from spare since this
|
|
||||||
// must be higher priority than any which are looked at
|
|
||||||
// afterwards.
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
String.prototype.rpad = function(padString, length) {
|
|
||||||
var str = this;
|
|
||||||
while (str.length < length)
|
|
||||||
str = str + padString;
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueManager.prototype.printQueues = function(gameState){
|
|
||||||
debug("OUTQUEUES");
|
|
||||||
for (var i in this.queues){
|
|
||||||
var qStr = "";
|
|
||||||
var q = this.queues[i];
|
|
||||||
for (var j in q.outQueue){
|
|
||||||
qStr += q.outQueue[j].type + " ";
|
|
||||||
if (q.outQueue[j].number)
|
|
||||||
qStr += "x" + q.outQueue[j].number;
|
|
||||||
}
|
|
||||||
if (qStr != ""){
|
|
||||||
debug((i + ":").rpad(" ", 20) + qStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug("INQUEUES");
|
|
||||||
for (var i in this.queues){
|
|
||||||
var qStr = "";
|
|
||||||
var q = this.queues[i];
|
|
||||||
for (var j in q.queue){
|
|
||||||
qStr += q.queue[j].type + " ";
|
|
||||||
if (q.queue[j].number)
|
|
||||||
qStr += "x" + q.queue[j].number;
|
|
||||||
qStr += " ";
|
|
||||||
}
|
|
||||||
if (qStr != ""){
|
|
||||||
debug((i + ":").rpad(" ", 20) + qStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug("Accounts: " + uneval(this.account));
|
|
||||||
debug("Needed Resources:" + uneval(this.futureNeeds(gameState)));
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueManager.prototype.update = function(gameState) {
|
|
||||||
|
|
||||||
for (var i in this.priorities){
|
|
||||||
if (!(this.priorities[i] > 0)){
|
|
||||||
this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero.
|
|
||||||
warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStart("Queue Manager");
|
|
||||||
//this.printQueues(gameState);
|
|
||||||
|
|
||||||
Engine.ProfileStart("Pick items from queues");
|
|
||||||
// See if there is a high priority item from last time.
|
|
||||||
this.affordableToOutQueue(gameState);
|
|
||||||
do {
|
|
||||||
// pick out all affordable items, and list the ratios of (needed
|
|
||||||
// cost)/priority for unaffordable items.
|
|
||||||
var ratio = {};
|
|
||||||
var ratioMin = 1000000;
|
|
||||||
var ratioMinQueue = undefined;
|
|
||||||
for (var p in this.queues) {
|
|
||||||
if (this.queues[p].length() > 0 && this.curItemQueue.indexOf(p) === -1) {
|
|
||||||
var cost = this.queues[p].getNext().getCost().toInt();
|
|
||||||
if (cost < this.account[p]) {
|
|
||||||
this.curItemQueue.push(p);
|
|
||||||
// break;
|
|
||||||
} else {
|
|
||||||
ratio[p] = (cost - this.account[p]) / this.priorities[p];
|
|
||||||
if (ratio[p] < ratioMin) {
|
|
||||||
ratioMin = ratio[p];
|
|
||||||
ratioMinQueue = p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks to see that there is an item in at least one queue, otherwise
|
|
||||||
// breaks the loop.
|
|
||||||
if (this.curItemQueue.length === 0 && ratioMinQueue === undefined) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var availableRes = this.affordableToOutQueue(gameState);
|
|
||||||
|
|
||||||
var allSpare = availableRes["food"] > 0 && availableRes["wood"] > 0 && availableRes["stone"] > 0 && availableRes["metal"] > 0;
|
|
||||||
// if there are no affordable items use any resources which aren't
|
|
||||||
// wanted by a higher priority item
|
|
||||||
if ((availableRes["food"] > 0 || availableRes["wood"] > 0 || availableRes["stone"] > 0 || availableRes["metal"] > 0)
|
|
||||||
&& ratioMinQueue !== undefined) {
|
|
||||||
while (Object.keys(ratio).length > 0 && (availableRes["food"] > 0 || availableRes["wood"] > 0 || availableRes["stone"] > 0 || availableRes["metal"] > 0)){
|
|
||||||
ratioMin = Math.min(); //biggest value
|
|
||||||
for (var key in ratio){
|
|
||||||
if (ratio[key] < ratioMin){
|
|
||||||
ratioMin = ratio[key];
|
|
||||||
ratioMinQueue = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.onlyUsesSpareAndUpdateSpare(this.queues[ratioMinQueue].getNext().getCost(), availableRes)){
|
|
||||||
if (allSpare){
|
|
||||||
for (var p in this.queues) {
|
|
||||||
this.account[p] += ratioMin * this.priorities[p];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//this.account[ratioMinQueue] -= this.queues[ratioMinQueue].getNext().getCost().toInt();
|
|
||||||
this.curItemQueue.push(ratioMinQueue);
|
|
||||||
allSpare = availableRes["food"] > 0 && availableRes["wood"] > 0 && availableRes["stone"] > 0 && availableRes["metal"] > 0;
|
|
||||||
}
|
|
||||||
delete ratio[ratioMinQueue];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
this.affordableToOutQueue(gameState);
|
|
||||||
} while (this.curItemQueue.length === 0);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
Engine.ProfileStart("Execute items");
|
|
||||||
|
|
||||||
// Handle output queues by executing items where possible
|
|
||||||
for (var p in this.queues) {
|
|
||||||
while (this.queues[p].outQueueLength() > 0) {
|
|
||||||
var next = this.queues[p].outQueueNext();
|
|
||||||
if (next.category === "building") {
|
|
||||||
if (gameState.buildingsBuilt == 0) {
|
|
||||||
if (this.queues[p].outQueueNext().canExecute(gameState)) {
|
|
||||||
this.queues[p].executeNext(gameState);
|
|
||||||
gameState.buildingsBuilt += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.queues[p].outQueueNext().canExecute(gameState)){
|
|
||||||
this.queues[p].executeNext(gameState);
|
|
||||||
}else{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Engine.ProfileStop();
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* Holds a list of wanted items to train or construct
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Queue = function() {
|
|
||||||
this.queue = [];
|
|
||||||
this.outQueue = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.addItem = function(plan) {
|
|
||||||
this.queue.push(plan);
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.getNext = function() {
|
|
||||||
if (this.queue.length > 0) {
|
|
||||||
return this.queue[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.outQueueNext = function(){
|
|
||||||
if (this.outQueue.length > 0) {
|
|
||||||
return this.outQueue[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.outQueueCost = function(){
|
|
||||||
var cost = new Resources();
|
|
||||||
for (var key in this.outQueue){
|
|
||||||
cost.add(this.outQueue[key].getCost());
|
|
||||||
}
|
|
||||||
return cost;
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.nextToOutQueue = function(){
|
|
||||||
if (this.queue.length > 0){
|
|
||||||
if (this.outQueue.length > 0 &&
|
|
||||||
this.getNext().category === "unit" &&
|
|
||||||
this.outQueue[this.outQueue.length-1].type === this.getNext().type &&
|
|
||||||
this.outQueue[this.outQueue.length-1].number < 5){
|
|
||||||
this.queue.shift();
|
|
||||||
this.outQueue[this.outQueue.length-1].addItem();
|
|
||||||
}else{
|
|
||||||
this.outQueue.push(this.queue.shift());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.executeNext = function(gameState) {
|
|
||||||
if (this.outQueue.length > 0) {
|
|
||||||
this.outQueue.shift().execute(gameState);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.length = function() {
|
|
||||||
return this.queue.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.countQueuedUnits = function(){
|
|
||||||
var count = 0;
|
|
||||||
for (var i in this.queue){
|
|
||||||
count += this.queue[i].number;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.countOutQueuedUnits = function(){
|
|
||||||
var count = 0;
|
|
||||||
for (var i in this.outQueue){
|
|
||||||
count += this.outQueue[i].number;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.countTotalQueuedUnits = function(){
|
|
||||||
var count = 0;
|
|
||||||
for (var i in this.queue){
|
|
||||||
count += this.queue[i].number;
|
|
||||||
}
|
|
||||||
for (var i in this.outQueue){
|
|
||||||
count += this.outQueue[i].number;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.totalLength = function(){
|
|
||||||
return this.queue.length + this.outQueue.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.outQueueLength = function(){
|
|
||||||
return this.outQueue.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.prototype.countAllByType = function(t){
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < this.queue.length; i++){
|
|
||||||
if (this.queue[i].type === t){
|
|
||||||
count += this.queue[i].number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var i = 0; i < this.outQueue.length; i++){
|
|
||||||
if (this.outQueue[i].type === t){
|
|
||||||
count += this.outQueue[i].number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
This is an AI for 0 A.D. (http://play0ad.com/) based on the testBot.
|
|
||||||
|
|
||||||
Install by placing the files into the data/mods/public/simulation/ai/qbot folder.
|
|
||||||
|
|
||||||
If you are developing you might find it helpful to change the debugOn line in qBot.js. This will make it spew random warnings depending on what I have been working on. Use the debug() function to make your own warnings.
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
|||||||
function Resources(amounts, population) {
|
|
||||||
if (amounts === undefined) {
|
|
||||||
amounts = {
|
|
||||||
food : 0,
|
|
||||||
wood : 0,
|
|
||||||
stone : 0,
|
|
||||||
metal : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
for ( var tKey in this.types) {
|
|
||||||
var t = this.types[tKey];
|
|
||||||
this[t] = amounts[t] || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (population > 0) {
|
|
||||||
this.population = parseInt(population);
|
|
||||||
} else {
|
|
||||||
this.population = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Resources.prototype.types = [ "food", "wood", "stone", "metal" ];
|
|
||||||
|
|
||||||
Resources.prototype.canAfford = function(that) {
|
|
||||||
for ( var tKey in this.types) {
|
|
||||||
var t = this.types[tKey];
|
|
||||||
if (this[t] < that[t]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
Resources.prototype.add = function(that) {
|
|
||||||
for ( var tKey in this.types) {
|
|
||||||
var t = this.types[tKey];
|
|
||||||
this[t] += that[t];
|
|
||||||
}
|
|
||||||
this.population += that.population;
|
|
||||||
};
|
|
||||||
|
|
||||||
Resources.prototype.subtract = function(that) {
|
|
||||||
for ( var tKey in this.types) {
|
|
||||||
var t = this.types[tKey];
|
|
||||||
this[t] -= that[t];
|
|
||||||
}
|
|
||||||
this.population += that.population;
|
|
||||||
};
|
|
||||||
|
|
||||||
Resources.prototype.multiply = function(n) {
|
|
||||||
for ( var tKey in this.types) {
|
|
||||||
var t = this.types[tKey];
|
|
||||||
this[t] *= n;
|
|
||||||
}
|
|
||||||
this.population *= n;
|
|
||||||
};
|
|
||||||
|
|
||||||
Resources.prototype.toInt = function() {
|
|
||||||
var sum = 0;
|
|
||||||
for ( var tKey in this.types) {
|
|
||||||
var t = this.types[tKey];
|
|
||||||
sum += this[t];
|
|
||||||
}
|
|
||||||
sum += this.population * 50; // based on typical unit costs
|
|
||||||
return sum;
|
|
||||||
};
|
|
@ -1,350 +0,0 @@
|
|||||||
/*
|
|
||||||
* TerrainAnalysis inherits from Map
|
|
||||||
*
|
|
||||||
* This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function.
|
|
||||||
* This is intended to be a base object for the terrain analysis modules to inherit from.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function TerrainAnalysis(gameState){
|
|
||||||
var passabilityMap = gameState.getMap();
|
|
||||||
|
|
||||||
var obstructionMask = gameState.getPassabilityClassMask("pathfinderObstruction");
|
|
||||||
obstructionMask |= gameState.getPassabilityClassMask("default");
|
|
||||||
|
|
||||||
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
|
|
||||||
for (var i = 0; i < passabilityMap.data.length; ++i)
|
|
||||||
{
|
|
||||||
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMask) ? 0 : 65535;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Map(gameState, obstructionTiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
copyPrototype(TerrainAnalysis, Map);
|
|
||||||
|
|
||||||
// Returns the (approximately) closest point which is passable by searching in a spiral pattern
|
|
||||||
TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, quick, limitDistance){
|
|
||||||
var w = this.width;
|
|
||||||
var p = startPoint;
|
|
||||||
var direction = 1;
|
|
||||||
|
|
||||||
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
|
|
||||||
this.map[p[0] + w*p[1]] != 0){
|
|
||||||
if (this.countConnected(p, 10) >= 10){
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 0;
|
|
||||||
// search in a spiral pattern.
|
|
||||||
for (var i = 1; i < w; i++){
|
|
||||||
for (var j = 0; j < 2; j++){
|
|
||||||
for (var k = 0; k < i; k++){
|
|
||||||
p[j] += direction;
|
|
||||||
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
|
|
||||||
this.map[p[0] + w*p[1]] != 0){
|
|
||||||
if (quick || this.countConnected(p, 10) >= 10){
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (limitDistance && count > 40){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
direction *= -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Counts how many accessible tiles there are connected to the start Point. If there are >= maxCount then it stops.
|
|
||||||
// This is inefficient for large areas so maxCount should be kept small for efficiency.
|
|
||||||
TerrainAnalysis.prototype.countConnected = function(startPoint, maxCount, curCount, checked){
|
|
||||||
curCount = curCount || 0;
|
|
||||||
checked = checked || [];
|
|
||||||
|
|
||||||
var w = this.width;
|
|
||||||
|
|
||||||
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
|
|
||||||
|
|
||||||
curCount += 1; // add 1 for the current point
|
|
||||||
checked.push(startPoint);
|
|
||||||
if (curCount >= maxCount){
|
|
||||||
return curCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i in positions){
|
|
||||||
var p = [startPoint[0] + positions[i][0], startPoint[1] + positions[i][1]];
|
|
||||||
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
|
|
||||||
this.map[p[0] + w*p[1]] != 0 && !(p in checked)){
|
|
||||||
curCount += this.countConnected(p, maxCount, curCount, checked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return curCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* PathFinder inherits from TerrainAnalysis
|
|
||||||
*
|
|
||||||
* Used to create a list of distinct paths between two points.
|
|
||||||
*
|
|
||||||
* Currently it works with a basic implementation which should be improved.
|
|
||||||
*
|
|
||||||
* TODO: Make this use territories.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
function PathFinder(gameState){
|
|
||||||
this.TerrainAnalysis(gameState);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyPrototype(PathFinder, TerrainAnalysis);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Returns a list of distinct paths to the destination. Currently paths are distinct if they are more than
|
|
||||||
* blockRadius apart at a distance of blockPlacementRadius from the destination. Where blockRadius and
|
|
||||||
* blockPlacementRadius are defined in walkGradient
|
|
||||||
*/
|
|
||||||
PathFinder.prototype.getPaths = function(start, end, mode){
|
|
||||||
var s = this.findClosestPassablePoint(this.gamePosToMapPos(start));
|
|
||||||
var e = this.findClosestPassablePoint(this.gamePosToMapPos(end));
|
|
||||||
|
|
||||||
if (!s || !e){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
var paths = [];
|
|
||||||
while (true){
|
|
||||||
this.makeGradient(s,e);
|
|
||||||
var curPath = this.walkGradient(e, mode);
|
|
||||||
|
|
||||||
if (curPath !== undefined){
|
|
||||||
paths.push(curPath);
|
|
||||||
}else{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wipeGradient();
|
|
||||||
}
|
|
||||||
|
|
||||||
//this.dumpIm("terrainanalysis.png", 511);
|
|
||||||
|
|
||||||
if (paths.length > 0){
|
|
||||||
return paths;
|
|
||||||
}else{
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Creates a potential gradient with the start point having the lowest potential
|
|
||||||
PathFinder.prototype.makeGradient = function(start, end){
|
|
||||||
var w = this.width;
|
|
||||||
var map = this.map;
|
|
||||||
|
|
||||||
// Holds the list of current points to work outwards from
|
|
||||||
var stack = [];
|
|
||||||
// We store the next level in its own stack
|
|
||||||
var newStack = [];
|
|
||||||
// Relative positions or new cells from the current one. We alternate between the adjacent 4 and 8 cells
|
|
||||||
// so that there is an average 1.5 distance for diagonals which is close to the actual sqrt(2) ~ 1.41
|
|
||||||
var positions = [[[0,1], [0,-1], [1,0], [-1,0]],
|
|
||||||
[[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]];
|
|
||||||
|
|
||||||
//Set the distance of the start point to be 1 to distinguish it from the impassable areas
|
|
||||||
map[start[0] + w*(start[1])] = 1;
|
|
||||||
stack.push(start);
|
|
||||||
|
|
||||||
// while there are new points being added to the stack
|
|
||||||
while (stack.length > 0){
|
|
||||||
//run through the current stack
|
|
||||||
while (stack.length > 0){
|
|
||||||
var cur = stack.pop();
|
|
||||||
// stop when we reach the end point
|
|
||||||
if (cur[0] == end[0] && cur[1] == end[1]){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dist = map[cur[0] + w*(cur[1])] + 1;
|
|
||||||
// Check the positions adjacent to the current cell
|
|
||||||
for (var i = 0; i < positions[dist % 2].length; i++){
|
|
||||||
var pos = positions[dist % 2][i];
|
|
||||||
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
|
|
||||||
if (cell >= 0 && cell < this.length && map[cell] > dist){
|
|
||||||
map[cell] = dist;
|
|
||||||
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Replace the old empty stack with the newly filled one.
|
|
||||||
stack = newStack;
|
|
||||||
newStack = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clears the map to just have the obstructions marked on it.
|
|
||||||
PathFinder.prototype.wipeGradient = function(){
|
|
||||||
for (var i = 0; i < this.length; i++){
|
|
||||||
if (this.map[i] > 0){
|
|
||||||
this.map[i] = 65535;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the path down a gradient from the start to the bottom of the gradient, returns a point for every 20 cells in normal mode
|
|
||||||
// in entryPoints mode this returns the point where the path enters the region near the destination, currently defined
|
|
||||||
// by blockPlacementRadius. Note doesn't return a path when the destination is within the blockpoint radius.
|
|
||||||
PathFinder.prototype.walkGradient = function(start, mode){
|
|
||||||
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
|
|
||||||
|
|
||||||
var path = [[start[0]*this.cellSize, start[1]*this.cellSize]];
|
|
||||||
|
|
||||||
var blockPoint = undefined;
|
|
||||||
var blockPlacementRadius = 45;
|
|
||||||
var blockRadius = 23;
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var cur = start;
|
|
||||||
var w = this.width;
|
|
||||||
var dist = this.map[cur[0] + w*cur[1]];
|
|
||||||
var moved = false;
|
|
||||||
while (this.map[cur[0] + w*cur[1]] !== 0){
|
|
||||||
for (var i = 0; i < positions.length; i++){
|
|
||||||
var pos = positions[i];
|
|
||||||
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
|
|
||||||
if (cell >= 0 && cell < this.length && this.map[cell] > 0 && this.map[cell] < dist){
|
|
||||||
dist = this.map[cell];
|
|
||||||
cur = [cur[0]+pos[0], cur[1]+pos[1]];
|
|
||||||
moved = true;
|
|
||||||
count++;
|
|
||||||
// Mark the point to put an obstruction at before calculating the next path
|
|
||||||
if (count === blockPlacementRadius){
|
|
||||||
blockPoint = cur;
|
|
||||||
}
|
|
||||||
// Add waypoints to the path, fairly well spaced apart.
|
|
||||||
if (count % 40 === 0){
|
|
||||||
path.unshift([cur[0]*this.cellSize, cur[1]*this.cellSize]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!moved){
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
moved = false;
|
|
||||||
}
|
|
||||||
if (blockPoint === undefined){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an obstruction to the map at the blockpoint so the next path will take a different route.
|
|
||||||
this.addInfluence(blockPoint[0], blockPoint[1], blockRadius, -1000000, 'constant');
|
|
||||||
if (mode === 'entryPoints'){
|
|
||||||
// returns the point where the path enters the blockPlacementRadius
|
|
||||||
return [blockPoint[0] * this.cellSize, blockPoint[1] * this.cellSize];
|
|
||||||
}else{
|
|
||||||
// return a path of points 20 squares apart on the route
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Would be used to calculate the width of a chokepoint
|
|
||||||
// NOTE: Doesn't currently work.
|
|
||||||
PathFinder.prototype.countAttached = function(pos){
|
|
||||||
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
|
|
||||||
var w = this.width;
|
|
||||||
var val = this.map[pos[0] + w*pos[1]];
|
|
||||||
|
|
||||||
var stack = [pos];
|
|
||||||
var used = {};
|
|
||||||
|
|
||||||
while (stack.length > 0){
|
|
||||||
var cur = stack.pop();
|
|
||||||
used[cur[0] + " " + cur[1]] = true;
|
|
||||||
for (var i = 0; i < positions.length; i++){
|
|
||||||
var p = positions[i];
|
|
||||||
var cell = cur[0]+p[0] + w*(cur[1]+p[1]);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Accessibility inherits from TerrainAnalysis
|
|
||||||
*
|
|
||||||
* Determines whether there is a path from one point to another. It is initialised with a single point (p1) and then
|
|
||||||
* can efficiently determine if another point is reachable from p1. Initialising the object is costly so it should be
|
|
||||||
* cached.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Accessibility(gameState, location){
|
|
||||||
this.TerrainAnalysis(gameState);
|
|
||||||
|
|
||||||
var start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
|
|
||||||
|
|
||||||
// Check that the accessible region is a decent size, otherwise obstacles close to the start point can create
|
|
||||||
// tiny accessible areas which makes the rest of the map inaceesible.
|
|
||||||
var iterations = 0;
|
|
||||||
while (this.floodFill(start) < 20 && iterations < 30){
|
|
||||||
this.map[start[0] + this.width*(start[1])] = 0;
|
|
||||||
start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
|
|
||||||
iterations += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
copyPrototype(Accessibility, TerrainAnalysis);
|
|
||||||
|
|
||||||
// Return true if the given point is accessible from the point given when initialising the Accessibility object. #
|
|
||||||
// If the given point is impassable the closest passable point is used.
|
|
||||||
Accessibility.prototype.isAccessible = function(position){
|
|
||||||
var s = this.findClosestPassablePoint(this.gamePosToMapPos(position), true, true);
|
|
||||||
if (!s)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return this.map[s[0] + this.width * s[1]] === 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// fill all of the accessible areas with value 1
|
|
||||||
Accessibility.prototype.floodFill = function(start){
|
|
||||||
var w = this.width;
|
|
||||||
var map = this.map;
|
|
||||||
|
|
||||||
// Holds the list of current points to work outwards from
|
|
||||||
var stack = [];
|
|
||||||
// We store new points to be added to the stack temporarily in here while we run through the current stack
|
|
||||||
var newStack = [];
|
|
||||||
// Relative positions or new cells from the current one.
|
|
||||||
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
|
|
||||||
|
|
||||||
// Set the start point to be accessible
|
|
||||||
map[start[0] + w*(start[1])] = 1;
|
|
||||||
stack.push(start);
|
|
||||||
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
// while there are new points being added to the stack
|
|
||||||
while (stack.length > 0){
|
|
||||||
//run through the current stack
|
|
||||||
while (stack.length > 0){
|
|
||||||
var cur = stack.pop();
|
|
||||||
|
|
||||||
// Check the positions adjacent to the current cell
|
|
||||||
for (var i = 0; i < positions.length; i++){
|
|
||||||
var pos = positions[i];
|
|
||||||
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
|
|
||||||
if (cell >= 0 && cell < this.length && map[cell] > 1){
|
|
||||||
map[cell] = 1;
|
|
||||||
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Replace the old empty stack with the newly filled one.
|
|
||||||
stack = newStack;
|
|
||||||
newStack = [];
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
};
|
|
@ -1,104 +0,0 @@
|
|||||||
//The Timer class // The instance of this class is created in the qBot object under the name 'timer'
|
|
||||||
//The methods that are available to call from this instance are:
|
|
||||||
//timer.setTimer : Creates a new timer with the given interval (miliseconds).
|
|
||||||
// Optionally set dalay or a limited repeat value.
|
|
||||||
//timer.checkTimer : Gives true if called at the time of the interval.
|
|
||||||
//timer.clearTimer : Deletes the timer permanently. No way to get the same timer back.
|
|
||||||
//timer.activateTimer : Sets the status of a deactivated timer to active.
|
|
||||||
//timer.deactivateTimer : Deactivates a timer. Deactivated timers will never give true.
|
|
||||||
|
|
||||||
|
|
||||||
//-EmjeR-// Timer class //
|
|
||||||
var Timer = function() {
|
|
||||||
///Private array.
|
|
||||||
var alarmList = [];
|
|
||||||
|
|
||||||
///Private methods
|
|
||||||
function num_alarms() {
|
|
||||||
return alarmList.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
function get_alarm(id) {
|
|
||||||
return alarmList[id];
|
|
||||||
};
|
|
||||||
|
|
||||||
function add_alarm(index, alarm) {
|
|
||||||
alarmList[index] = alarm;
|
|
||||||
};
|
|
||||||
|
|
||||||
function delete_alarm(id) {
|
|
||||||
// Set the array element to undefined
|
|
||||||
delete alarmList[id];
|
|
||||||
};
|
|
||||||
|
|
||||||
///Privileged methods
|
|
||||||
// Add an new alarm to the list
|
|
||||||
this.setTimer = function(gameState, interval, delay, repeat) {
|
|
||||||
delay = delay || 0;
|
|
||||||
repeat = repeat || -1;
|
|
||||||
|
|
||||||
var index = num_alarms();
|
|
||||||
|
|
||||||
//Add a new alarm to the list
|
|
||||||
add_alarm(index, new alarm(gameState, index, interval, delay, repeat));
|
|
||||||
return index;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Check if a alarm has reached its interval.
|
|
||||||
this.checkTimer = function(gameState,id) {
|
|
||||||
var alarm = get_alarm(id);
|
|
||||||
if (alarm === undefined)
|
|
||||||
return false;
|
|
||||||
if (!alarm.active)
|
|
||||||
return false;
|
|
||||||
var time = gameState.getTimeElapsed();
|
|
||||||
var alarmState = false;
|
|
||||||
|
|
||||||
// If repeat forever (repeat is -1). Or if the alarm has rung less times than repeat.
|
|
||||||
if (alarm.repeat < 0 || alarm.counter < alarm.repeat) {
|
|
||||||
var time_diffrence = time - alarm.start_time - alarm.delay - alarm.interval * alarm.counter;
|
|
||||||
if (time_diffrence > alarm.interval) {
|
|
||||||
alarmState = true;
|
|
||||||
alarm.counter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the alarm has rung 'alarm.repeat' times if so, delete the alarm.
|
|
||||||
if (alarm.counter >= alarm.repeat && alarm.repeat != -1) {
|
|
||||||
this.clearTimer(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return alarmState;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove an alarm from the list.
|
|
||||||
this.clearTimer = function(id) {
|
|
||||||
delete_alarm(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Activate a deactivated alarm.
|
|
||||||
this.activateTimer = function(id) {
|
|
||||||
var alarm = get_alarm(id);
|
|
||||||
alarm.active = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Deactivate an active alarm but don't delete it.
|
|
||||||
this.deactivateTimer = function(id) {
|
|
||||||
var alarm = get_alarm(id);
|
|
||||||
alarm.active = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
//-EmjeR-// Alarm class //
|
|
||||||
function alarm(gameState, id, interval, delay, repeat) {
|
|
||||||
this.id = id;
|
|
||||||
this.interval = interval;
|
|
||||||
this.delay = delay;
|
|
||||||
this.repeat = repeat;
|
|
||||||
|
|
||||||
this.start_time = gameState.getTimeElapsed();
|
|
||||||
this.active = true;
|
|
||||||
this.counter = 0;
|
|
||||||
};
|
|
@ -1,85 +0,0 @@
|
|||||||
var WalkToCC = function(gameState, militaryManager){
|
|
||||||
this.minAttackSize = 20;
|
|
||||||
this.maxAttackSize = 60;
|
|
||||||
this.idList=[];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns true if the attack can be executed at the current time
|
|
||||||
WalkToCC.prototype.canExecute = function(gameState, militaryManager){
|
|
||||||
var enemyStrength = militaryManager.measureEnemyStrength(gameState);
|
|
||||||
var enemyCount = militaryManager.measureEnemyCount(gameState);
|
|
||||||
|
|
||||||
// We require our army to be >= this strength
|
|
||||||
var targetStrength = enemyStrength * 1.5;
|
|
||||||
|
|
||||||
var availableCount = militaryManager.countAvailableUnits();
|
|
||||||
var availableStrength = militaryManager.measureAvailableStrength();
|
|
||||||
|
|
||||||
debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount);
|
|
||||||
debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength);
|
|
||||||
|
|
||||||
return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize)
|
|
||||||
|| availableCount >= this.maxAttackSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Executes the attack plan, after this is executed the update function will be run every turn
|
|
||||||
WalkToCC.prototype.execute = function(gameState, militaryManager){
|
|
||||||
var availableCount = militaryManager.countAvailableUnits();
|
|
||||||
this.idList = militaryManager.getAvailableUnits(availableCount);
|
|
||||||
|
|
||||||
var pending = EntityCollectionFromIds(gameState, this.idList);
|
|
||||||
|
|
||||||
// Find the critical enemy buildings we could attack
|
|
||||||
var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical");
|
|
||||||
// If there are no critical structures, attack anything else that's critical
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = gameState.entities.filter(function(ent) {
|
|
||||||
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If there's nothing, attack anything else that's less critical
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = militaryManager.getEnemyBuildings(gameState,"Town");
|
|
||||||
}
|
|
||||||
if (targets.length == 0) {
|
|
||||||
targets = militaryManager.getEnemyBuildings(gameState,"Village");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a target, move to it
|
|
||||||
if (targets.length) {
|
|
||||||
// Remove the pending role
|
|
||||||
pending.forEach(function(ent) {
|
|
||||||
ent.setMetadata("role", "attack");
|
|
||||||
});
|
|
||||||
|
|
||||||
var target = targets.toEntityArray()[0];
|
|
||||||
var targetPos = target.position();
|
|
||||||
|
|
||||||
// TODO: this should be an attack-move command
|
|
||||||
pending.move(targetPos[0], targetPos[1]);
|
|
||||||
} else if (targets.length == 0 ) {
|
|
||||||
gameState.ai.gameFinished = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Runs every turn after the attack is executed
|
|
||||||
// This removes idle units from the attack
|
|
||||||
WalkToCC.prototype.update = function(gameState, militaryManager){
|
|
||||||
var removeList = [];
|
|
||||||
for (var idKey in this.idList){
|
|
||||||
var id = this.idList[idKey];
|
|
||||||
var ent = militaryManager.entity(id);
|
|
||||||
if(ent)
|
|
||||||
{
|
|
||||||
if(ent.isIdle()) {
|
|
||||||
militaryManager.unassignUnit(id);
|
|
||||||
removeList.push(id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
removeList.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var i in removeList){
|
|
||||||
this.idList.splice(this.idList.indexOf(removeList[i]),1);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,251 +0,0 @@
|
|||||||
/**
|
|
||||||
* This class makes a worker do as instructed by the economy manager
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Worker = function(ent) {
|
|
||||||
this.ent = ent;
|
|
||||||
this.approachCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
Worker.prototype.update = function(gameState) {
|
|
||||||
var subrole = this.ent.getMetadata("subrole");
|
|
||||||
|
|
||||||
if (!this.ent.position()){
|
|
||||||
// If the worker has no position then no work can be done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subrole === "gatherer"){
|
|
||||||
if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0].type
|
|
||||||
&& this.getResourceType(this.ent.unitAIOrderData()[0].type) === this.ent.getMetadata("gather-type"))
|
|
||||||
&& !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){
|
|
||||||
// TODO: handle combat for hunting animals
|
|
||||||
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 ||
|
|
||||||
this.ent.resourceCarrying()[0].type === this.ent.getMetadata("gather-type")){
|
|
||||||
Engine.ProfileStart("Start Gathering");
|
|
||||||
this.startGathering(gameState);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
} else if (this.ent.unitAIState().split(".")[1] !== "RETURNRESOURCE") {
|
|
||||||
// Should deposit resources
|
|
||||||
Engine.ProfileStart("Return Resources");
|
|
||||||
this.returnResources(gameState);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startApproachingResourceTime = gameState.getTimeElapsed();
|
|
||||||
|
|
||||||
//Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]});
|
|
||||||
}else{
|
|
||||||
// If we haven't reached the resource in 2 minutes twice in a row and none of the resource has been
|
|
||||||
// gathered then mark it as inaccessible.
|
|
||||||
if (gameState.getTimeElapsed() - this.startApproachingResourceTime > 120000){
|
|
||||||
if (this.gatheringFrom){
|
|
||||||
var ent = gameState.getEntityById(this.gatheringFrom);
|
|
||||||
if (ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax()){
|
|
||||||
if (this.approachCount > 0){
|
|
||||||
ent.setMetadata("inaccessible", true);
|
|
||||||
this.ent.setMetadata("subrole", "idle");
|
|
||||||
}
|
|
||||||
this.approachCount++;
|
|
||||||
}else{
|
|
||||||
this.approachCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startApproachingResourceTime = gameState.getTimeElapsed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else if(subrole === "builder"){
|
|
||||||
if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){
|
|
||||||
var target = this.ent.getMetadata("target-foundation");
|
|
||||||
this.ent.repair(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]});
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStart("Update Gatherer Counts");
|
|
||||||
this.updateGathererCounts(gameState);
|
|
||||||
Engine.ProfileStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
Worker.prototype.updateGathererCounts = function(gameState, dead){
|
|
||||||
// update gatherer counts for the resources
|
|
||||||
if (this.ent.unitAIState().split(".")[2] === "GATHERING" && !dead){
|
|
||||||
if (this.gatheringFrom !== this.ent.unitAIOrderData()[0].target){
|
|
||||||
if (this.gatheringFrom){
|
|
||||||
var ent = gameState.getEntityById(this.gatheringFrom);
|
|
||||||
if (ent){
|
|
||||||
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
|
|
||||||
this.markFull(ent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.gatheringFrom = this.ent.unitAIOrderData()[0].target;
|
|
||||||
if (this.gatheringFrom){
|
|
||||||
var ent = gameState.getEntityById(this.gatheringFrom);
|
|
||||||
if (ent){
|
|
||||||
ent.setMetadata("gatherer-count", (ent.getMetadata("gatherer-count") || 0) + 1);
|
|
||||||
this.markFull(ent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if (this.gatheringFrom){
|
|
||||||
var ent = gameState.getEntityById(this.gatheringFrom);
|
|
||||||
if (ent){
|
|
||||||
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
|
|
||||||
this.markFull(ent);
|
|
||||||
}
|
|
||||||
this.gatheringFrom = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Worker.prototype.markFull = function(ent){
|
|
||||||
var maxCounts = {"food": 20, "wood": 5, "metal": 20, "stone": 20, "treasure": 1};
|
|
||||||
if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[ent.resourceSupplyType().generic]){
|
|
||||||
if (!ent.getMetadata("full")){
|
|
||||||
ent.setMetadata("full", true);
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if (ent.getMetadata("full")){
|
|
||||||
ent.setMetadata("full", false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Worker.prototype.startGathering = function(gameState){
|
|
||||||
var resource = this.ent.getMetadata("gather-type");
|
|
||||||
var ent = this.ent;
|
|
||||||
|
|
||||||
if (!ent.position()){
|
|
||||||
// TODO: work out what to do when entity has no position
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
gameState.updatingCollection("active-dropsite-" + resource, Filters.byMetadata("active-dropsite-" + resource, true),
|
|
||||||
gameState.getOwnDropsites(resource)).forEach(function (dropsite){
|
|
||||||
if (dropsite.position()){
|
|
||||||
var dist = VectorDistance(ent.position(), dropsite.position());
|
|
||||||
if (dist < minDropsiteDist){
|
|
||||||
minDropsiteDist = dist;
|
|
||||||
nearestResources = dropsite.getMetadata("nearby-resources-" + resource);
|
|
||||||
nearestDropsite = dropsite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!nearestResources || nearestResources.length === 0){
|
|
||||||
nearestResources = gameState.getResourceSupplies(resource);
|
|
||||||
gameState.getOwnDropsites(resource).forEach(function (dropsite){
|
|
||||||
if (dropsite.position()){
|
|
||||||
var dist = VectorDistance(ent.position(), dropsite.position());
|
|
||||||
if (dist < minDropsiteDist){
|
|
||||||
minDropsiteDist = dist;
|
|
||||||
nearestDropsite = dropsite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nearestResources.length === 0){
|
|
||||||
debug("No " + resource + " found! (1)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var supplies = [];
|
|
||||||
var nearestSupplyDist = Math.min();
|
|
||||||
var nearestSupply = undefined;
|
|
||||||
|
|
||||||
nearestResources.forEach(function(supply) {
|
|
||||||
// TODO: handle enemy territories
|
|
||||||
|
|
||||||
if (!supply.position()){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supply.isFull() === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// measure the distance to the resource
|
|
||||||
var dist = VectorDistance(supply.position(), ent.position());
|
|
||||||
// Add on a factor for the nearest dropsite if one exists
|
|
||||||
if (nearestDropsite){
|
|
||||||
dist += 5 * VectorDistance(supply.position(), nearestDropsite.position());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go for treasure as a priority
|
|
||||||
if (dist < 1000 && supply.resourceSupplyType().generic == "treasure"){
|
|
||||||
dist /= 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dist < nearestSupplyDist){
|
|
||||||
nearestSupplyDist = dist;
|
|
||||||
nearestSupply = supply;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nearestSupply) {
|
|
||||||
var pos = nearestSupply.position();
|
|
||||||
var territoryOwner = gameState.getTerritoryMap().getOwner(pos);
|
|
||||||
if (!gameState.ai.accessibility.isAccessible(pos) ||
|
|
||||||
(territoryOwner != gameState.getPlayerID() && territoryOwner != 0)){
|
|
||||||
nearestSupply.setMetadata("inaccessible", true);
|
|
||||||
}else{
|
|
||||||
ent.gather(nearestSupply);
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
debug("No " + resource + " found! (2)");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Makes the worker deposit the currently carried resources at the closest dropsite
|
|
||||||
Worker.prototype.returnResources = function(gameState){
|
|
||||||
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
var closestDropsite = undefined;
|
|
||||||
var dist = Math.min();
|
|
||||||
gameState.getOwnDropsites(resource).forEach(function(dropsite){
|
|
||||||
if (dropsite.position()){
|
|
||||||
var d = VectorDistance(self.ent.position(), dropsite.position());
|
|
||||||
if (d < dist){
|
|
||||||
dist = d;
|
|
||||||
closestDropsite = dropsite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!closestDropsite){
|
|
||||||
debug("No dropsite found for " + resource);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ent.returnResources(closestDropsite);
|
|
||||||
};
|
|
||||||
|
|
||||||
Worker.prototype.getResourceType = function(type){
|
|
||||||
if (!type || !type.generic){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.generic === "treasure"){
|
|
||||||
return type.specific;
|
|
||||||
}else{
|
|
||||||
return type.generic;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Scaredy Bot",
|
|
||||||
"description": "An AI that is terrified by the mere possibility of having to fight.",
|
|
||||||
"constructor": "ScaredyBotAI",
|
|
||||||
"hidden": true
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
Engine.IncludeModule("common-api");
|
|
||||||
|
|
||||||
function ScaredyBotAI(settings)
|
|
||||||
{
|
|
||||||
// warn("Constructing ScaredyBotAI for player "+settings.player);
|
|
||||||
|
|
||||||
BaseAI.call(this, settings);
|
|
||||||
|
|
||||||
this.turn = 0;
|
|
||||||
this.suicideTurn = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaredyBotAI.prototype = new BaseAI();
|
|
||||||
|
|
||||||
ScaredyBotAI.prototype.OnUpdate = function()
|
|
||||||
{
|
|
||||||
if (this.turn == 0)
|
|
||||||
this.chat("Good morning.");
|
|
||||||
|
|
||||||
if (this.turn == this.suicideTurn)
|
|
||||||
{
|
|
||||||
this.chat("I quake in my boots! My troops cannot hope to survive against a power such as yours.");
|
|
||||||
|
|
||||||
this.entities.filter(function(ent) { return ent.isOwn(); }).destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.turn++;
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
Engine.IncludeModule("common-api");
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Demo Bot",
|
|
||||||
"description": "A simple opponent, mainly designed for testing our AI scripting framework.",
|
|
||||||
"constructor": "TestBotAI",
|
|
||||||
"hidden": true
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
var EconomyManager = Class({
|
|
||||||
|
|
||||||
_init: function()
|
|
||||||
{
|
|
||||||
this.targetNumWorkers = 30; // minimum number of workers we want
|
|
||||||
this.targetNumBuilders = 5; // number of workers we want working on construction
|
|
||||||
|
|
||||||
// (This is a stupid design where we just construct certain numbers
|
|
||||||
// of certain buildings in sequence)
|
|
||||||
this.targetBuildings = [
|
|
||||||
{
|
|
||||||
"template": "structures/{civ}_civil_centre",
|
|
||||||
"priority": 500,
|
|
||||||
"count": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"template": "structures/{civ}_house",
|
|
||||||
"priority": 100,
|
|
||||||
"count": 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"template": "structures/{civ}_barracks",
|
|
||||||
"priority": 75,
|
|
||||||
"count": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"template": "structures/{civ}_field",
|
|
||||||
"priority": 50,
|
|
||||||
"count": 5,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Relative proportions of workers to assign to each resource type
|
|
||||||
this.gatherWeights = {
|
|
||||||
"food": 150,
|
|
||||||
"wood": 100,
|
|
||||||
"stone": 50,
|
|
||||||
"metal": 100,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
buildMoreBuildings: function(gameState, planGroups)
|
|
||||||
{
|
|
||||||
// Limit ourselves to constructing one building at a time
|
|
||||||
if (gameState.findFoundations().length)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for each (var building in this.targetBuildings)
|
|
||||||
{
|
|
||||||
var numBuildings = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(building.template));
|
|
||||||
|
|
||||||
// If we have too few, build another
|
|
||||||
if (numBuildings < building.count)
|
|
||||||
{
|
|
||||||
planGroups.economyConstruction.addPlan(building.priority,
|
|
||||||
new BuildingConstructionPlan(gameState, building.template)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
trainMoreWorkers: function(gameState, planGroups)
|
|
||||||
{
|
|
||||||
// Count the workers in the world and in progress
|
|
||||||
var numWorkers = gameState.countEntitiesAndQueuedWithRole("worker");
|
|
||||||
|
|
||||||
// If we have too few, train another
|
|
||||||
// print("Want "+this.targetNumWorkers+" workers; got "+numWorkers+"\n");
|
|
||||||
if (numWorkers < this.targetNumWorkers)
|
|
||||||
{
|
|
||||||
planGroups.economyPersonnel.addPlan(100,
|
|
||||||
new UnitTrainingPlan(gameState,
|
|
||||||
"units/{civ}_support_female_citizen", 1, { "role": "worker" })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
pickMostNeededResources: function(gameState)
|
|
||||||
{
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Find what resource type we're most in need of
|
|
||||||
var numGatherers = {};
|
|
||||||
for (var type in this.gatherWeights)
|
|
||||||
numGatherers[type] = 0;
|
|
||||||
|
|
||||||
gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) {
|
|
||||||
if (ent.getMetadata("subrole") === "gatherer")
|
|
||||||
numGatherers[ent.getMetadata("gather-type")] += 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
var types = Object.keys(this.gatherWeights);
|
|
||||||
types.sort(function(a, b) {
|
|
||||||
// Prefer fewer gatherers (divided by weight)
|
|
||||||
var va = numGatherers[a] / self.gatherWeights[a];
|
|
||||||
var vb = numGatherers[b] / self.gatherWeights[b];
|
|
||||||
return va - vb;
|
|
||||||
});
|
|
||||||
|
|
||||||
return types;
|
|
||||||
},
|
|
||||||
|
|
||||||
reassignRolelessUnits: function(gameState)
|
|
||||||
{
|
|
||||||
var roleless = gameState.getOwnEntitiesWithRole(undefined);
|
|
||||||
|
|
||||||
roleless.forEach(function(ent) {
|
|
||||||
if (ent.hasClass("Worker"))
|
|
||||||
ent.setMetadata("role", "worker");
|
|
||||||
else
|
|
||||||
ent.setMetadata("role", "unknown");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
reassignIdleWorkers: function(gameState, planGroups)
|
|
||||||
{
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Search for idle workers, and tell them to gather resources
|
|
||||||
// Maybe just pick a random nearby resource type at first;
|
|
||||||
// later we could add some timer that redistributes workers based on
|
|
||||||
// resource demand.
|
|
||||||
|
|
||||||
var idleWorkers = gameState.getOwnEntitiesWithRole("worker").filter(function(ent) {
|
|
||||||
return ent.isIdle();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (idleWorkers.length)
|
|
||||||
{
|
|
||||||
var resourceSupplies = gameState.findResourceSupplies();
|
|
||||||
|
|
||||||
idleWorkers.forEach(function(ent) {
|
|
||||||
|
|
||||||
var types = self.pickMostNeededResources(gameState);
|
|
||||||
for each (var type in types)
|
|
||||||
{
|
|
||||||
// Make sure there's actually some of that type
|
|
||||||
if (!resourceSupplies[type])
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Pick the closest one.
|
|
||||||
// TODO: we should care about distance to dropsites, not (just) to the worker,
|
|
||||||
// and gather rates of workers
|
|
||||||
|
|
||||||
var workerPosition = ent.position();
|
|
||||||
var supplies = [];
|
|
||||||
resourceSupplies[type].forEach(function(supply) {
|
|
||||||
// Skip targets that are too hard to hunt
|
|
||||||
if (supply.entity.isUnhuntable())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var dist = VectorDistance(supply.position, workerPosition);
|
|
||||||
|
|
||||||
// Skip targets that are far too far away (e.g. in the enemy base)
|
|
||||||
if (dist > 512)
|
|
||||||
return;
|
|
||||||
|
|
||||||
supplies.push({ dist: dist, entity: supply.entity });
|
|
||||||
});
|
|
||||||
|
|
||||||
supplies.sort(function (a, b) {
|
|
||||||
// Prefer smaller distances
|
|
||||||
if (a.dist != b.dist)
|
|
||||||
return a.dist - b.dist;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start gathering
|
|
||||||
if (supplies.length)
|
|
||||||
{
|
|
||||||
ent.gather(supplies[0].entity);
|
|
||||||
ent.setMetadata("subrole", "gatherer");
|
|
||||||
ent.setMetadata("gather-type", type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't find any types to gather
|
|
||||||
ent.setMetadata("subrole", "idle");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
assignToFoundations: function(gameState, planGroups)
|
|
||||||
{
|
|
||||||
// If we have some foundations, and we don't have enough builder-workers,
|
|
||||||
// try reassigning some other workers who are nearby
|
|
||||||
|
|
||||||
var foundations = gameState.findFoundations();
|
|
||||||
|
|
||||||
// Check if nothing to build
|
|
||||||
if (!foundations.length)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var workers = gameState.getOwnEntitiesWithRole("worker");
|
|
||||||
|
|
||||||
var builderWorkers = workers.filter(function(ent) {
|
|
||||||
return (ent.getMetadata("subrole") === "builder");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if enough builders
|
|
||||||
var extraNeeded = this.targetNumBuilders - builderWorkers.length;
|
|
||||||
if (extraNeeded <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Pick non-builders who are closest to the first foundation,
|
|
||||||
// and tell them to start building it
|
|
||||||
|
|
||||||
var target = foundations.toEntityArray()[0];
|
|
||||||
|
|
||||||
var nonBuilderWorkers = workers.filter(function(ent) {
|
|
||||||
return (ent.getMetadata("subrole") !== "builder");
|
|
||||||
});
|
|
||||||
|
|
||||||
var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), extraNeeded);
|
|
||||||
|
|
||||||
// Order each builder individually, not as a formation
|
|
||||||
nearestNonBuilders.forEach(function(ent) {
|
|
||||||
ent.repair(target);
|
|
||||||
ent.setMetadata("subrole", "builder");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
update: function(gameState, planGroups)
|
|
||||||
{
|
|
||||||
Engine.ProfileStart("economy update");
|
|
||||||
|
|
||||||
this.reassignRolelessUnits(gameState);
|
|
||||||
|
|
||||||
this.buildMoreBuildings(gameState, planGroups);
|
|
||||||
|
|
||||||
this.trainMoreWorkers(gameState, planGroups);
|
|
||||||
|
|
||||||
this.reassignIdleWorkers(gameState, planGroups);
|
|
||||||
|
|
||||||
this.assignToFoundations(gameState, planGroups);
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
@ -1,297 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides an API for the rest of the AI scripts to query the world state
|
|
||||||
* at a higher level than the raw data.
|
|
||||||
*/
|
|
||||||
var GameState = Class({
|
|
||||||
|
|
||||||
_init: function(ai)
|
|
||||||
{
|
|
||||||
MemoizeInit(this);
|
|
||||||
|
|
||||||
this.ai = ai;
|
|
||||||
this.timeElapsed = ai.timeElapsed;
|
|
||||||
this.templates = ai.templates;
|
|
||||||
this.entities = ai.entities;
|
|
||||||
this.player = ai.player;
|
|
||||||
this.playerData = ai.playerData;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTimeElapsed: function()
|
|
||||||
{
|
|
||||||
return this.timeElapsed;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTemplate: function(type)
|
|
||||||
{
|
|
||||||
if (!this.templates[type])
|
|
||||||
return null;
|
|
||||||
return new EntityTemplate(this.templates[type]);
|
|
||||||
},
|
|
||||||
|
|
||||||
applyCiv: function(str)
|
|
||||||
{
|
|
||||||
return str.replace(/\{civ\}/g, this.playerData.civ);
|
|
||||||
},
|
|
||||||
|
|
||||||
getResources: function()
|
|
||||||
{
|
|
||||||
return new Resources(this.playerData.resourceCounts);
|
|
||||||
},
|
|
||||||
|
|
||||||
getPassabilityMap: function()
|
|
||||||
{
|
|
||||||
return this.ai.passabilityMap;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPassabilityClassMask: function(name)
|
|
||||||
{
|
|
||||||
if (!(name in this.ai.passabilityClasses))
|
|
||||||
error("Tried to use invalid passability class name '"+name+"'");
|
|
||||||
return this.ai.passabilityClasses[name];
|
|
||||||
},
|
|
||||||
|
|
||||||
getTerritoryMap: function()
|
|
||||||
{
|
|
||||||
return this.ai.territoryMap;
|
|
||||||
},
|
|
||||||
|
|
||||||
getOwnEntities: (function()
|
|
||||||
{
|
|
||||||
return new EntityCollection(this.ai, this.ai._ownEntities);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getOwnEntitiesWithRole: Memoize('getOwnEntitiesWithRole', function(role)
|
|
||||||
{
|
|
||||||
var metas = this.ai._entityMetadata;
|
|
||||||
if (role === undefined)
|
|
||||||
return this.getOwnEntities().filter_raw(function(ent) {
|
|
||||||
var metadata = metas[ent.id];
|
|
||||||
if (!metadata || !('role' in metadata))
|
|
||||||
return true;
|
|
||||||
return (metadata.role === undefined);
|
|
||||||
});
|
|
||||||
else
|
|
||||||
return this.getOwnEntities().filter_raw(function(ent) {
|
|
||||||
var metadata = metas[ent.id];
|
|
||||||
if (!metadata || !('role' in metadata))
|
|
||||||
return false;
|
|
||||||
return (metadata.role === role);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
countEntitiesWithType: function(type)
|
|
||||||
{
|
|
||||||
var count = 0;
|
|
||||||
this.getOwnEntities().forEach(function(ent) {
|
|
||||||
var t = ent.templateName();
|
|
||||||
if (t == type)
|
|
||||||
++count;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
},
|
|
||||||
|
|
||||||
countEntitiesAndQueuedWithType: function(type)
|
|
||||||
{
|
|
||||||
var foundationType = "foundation|" + type;
|
|
||||||
var resourceType = "resource|" + type;
|
|
||||||
var count = 0;
|
|
||||||
this.getOwnEntities().forEach(function(ent) {
|
|
||||||
|
|
||||||
var t = ent.templateName();
|
|
||||||
if (t == type || t == foundationType || t == resourceType)
|
|
||||||
++count;
|
|
||||||
|
|
||||||
var queue = ent.trainingQueue();
|
|
||||||
if (queue)
|
|
||||||
{
|
|
||||||
queue.forEach(function(item) {
|
|
||||||
if (item.template == type)
|
|
||||||
count += item.count;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
},
|
|
||||||
|
|
||||||
countEntitiesAndQueuedWithRole: function(role)
|
|
||||||
{
|
|
||||||
var count = 0;
|
|
||||||
this.getOwnEntities().forEach(function(ent) {
|
|
||||||
|
|
||||||
if (ent.getMetadata("role") == role)
|
|
||||||
++count;
|
|
||||||
|
|
||||||
var queue = ent.trainingQueue();
|
|
||||||
if (queue)
|
|
||||||
{
|
|
||||||
queue.forEach(function(item) {
|
|
||||||
if (item.metadata && item.metadata.role == role)
|
|
||||||
count += item.count;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find buildings that are capable of training the given unit type,
|
|
||||||
* and aren't already too busy.
|
|
||||||
*/
|
|
||||||
findTrainers: function(template)
|
|
||||||
{
|
|
||||||
var maxQueueLength = 3; // avoid tying up resources in giant training queues
|
|
||||||
|
|
||||||
return this.getOwnEntities().filter(function(ent) {
|
|
||||||
|
|
||||||
var trainable = ent.trainableEntities();
|
|
||||||
if (!trainable || trainable.indexOf(template) == -1)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var queue = ent.trainingQueue();
|
|
||||||
if (queue)
|
|
||||||
{
|
|
||||||
if (queue.length >= maxQueueLength)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find units that are capable of constructing the given building type.
|
|
||||||
*/
|
|
||||||
findBuilders: function(template)
|
|
||||||
{
|
|
||||||
return this.getOwnEntities().filter(function(ent) {
|
|
||||||
|
|
||||||
var buildable = ent.buildableEntities();
|
|
||||||
if (!buildable || buildable.indexOf(template) == -1)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
findFoundations: function(template)
|
|
||||||
{
|
|
||||||
return this.getOwnEntities().filter(function(ent) {
|
|
||||||
return (ent.foundationProgress() !== undefined);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
findResourceSupplies: function()
|
|
||||||
{
|
|
||||||
var supplies = {};
|
|
||||||
this.entities.forEach(function(ent) {
|
|
||||||
var type = ent.resourceSupplyType();
|
|
||||||
if (!type)
|
|
||||||
return;
|
|
||||||
var amount = ent.resourceSupplyAmount();
|
|
||||||
if (!amount)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var reportedType;
|
|
||||||
if (type.generic == "treasure")
|
|
||||||
reportedType = type.specific;
|
|
||||||
else
|
|
||||||
reportedType = type.generic;
|
|
||||||
|
|
||||||
if (!supplies[reportedType])
|
|
||||||
supplies[reportedType] = [];
|
|
||||||
|
|
||||||
supplies[reportedType].push({
|
|
||||||
"entity": ent,
|
|
||||||
"amount": amount,
|
|
||||||
"type": type,
|
|
||||||
"position": ent.position(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return supplies;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPopulationLimit: function()
|
|
||||||
{
|
|
||||||
return this.playerData.popLimit;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPopulation: function()
|
|
||||||
{
|
|
||||||
return this.playerData.popCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPlayerID: function()
|
|
||||||
{
|
|
||||||
return this.player;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the player with the given id is an ally of the AI player
|
|
||||||
*/
|
|
||||||
isPlayerAlly: function(id)
|
|
||||||
{
|
|
||||||
return this.playerData.isAlly[id];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the player with the given id is an enemy of the AI player
|
|
||||||
*/
|
|
||||||
isPlayerEnemy: function(id)
|
|
||||||
{
|
|
||||||
return this.playerData.isEnemy[id];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether an Entity object is owned by an ally of the AI player (or self)
|
|
||||||
*/
|
|
||||||
isEntityAlly: function(ent)
|
|
||||||
{
|
|
||||||
return (ent && ent.owner() !== undefined && this.playerData.isAlly[ent.owner()]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether an Entity object is owned by an enemy of the AI player
|
|
||||||
*/
|
|
||||||
isEntityEnemy: function(ent)
|
|
||||||
{
|
|
||||||
return (ent && ent.owner() !== undefined && this.playerData.isEnemy[ent.owner()]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether an Entity object is owned by the AI player
|
|
||||||
*/
|
|
||||||
isEntityOwn: function(ent)
|
|
||||||
{
|
|
||||||
return (ent && ent.owner() !== undefined && ent.owner() == this.player);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns player build limits
|
|
||||||
* an object where each key is a category corresponding to a build limit for the player.
|
|
||||||
*/
|
|
||||||
getEntityLimits: function()
|
|
||||||
{
|
|
||||||
return this.playerData.entityLimits;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns player entity counts
|
|
||||||
* an object where each key is a category corresponding to the current entity count for the player.
|
|
||||||
*/
|
|
||||||
getEntityCounts: function()
|
|
||||||
{
|
|
||||||
return this.playerData.entityCounts;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the player's entity limit has been reached for the given category.
|
|
||||||
* The category comes from the entity template, specifically the
|
|
||||||
* BuildRestrictions/TrainingRestrictions components.
|
|
||||||
*/
|
|
||||||
isEntityLimitReached: function(category)
|
|
||||||
{
|
|
||||||
if (this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
|
|
||||||
return false;
|
|
||||||
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Military strategy:
|
|
||||||
* * Try training an attack squad of a specified size
|
|
||||||
* * When it's the appropriate size, send it to attack the enemy
|
|
||||||
* * Repeat forever
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
var MilitaryAttackManager = Class({
|
|
||||||
|
|
||||||
_init: function()
|
|
||||||
{
|
|
||||||
this.targetSquadSize = 10;
|
|
||||||
this.squadTypes = [
|
|
||||||
"units/{civ}_infantry_spearman_b",
|
|
||||||
"units/{civ}_infantry_javelinist_b",
|
|
||||||
// "units/{civ}_infantry_archer_b", // TODO: should only include this if hele
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unit type we should begin training.
|
|
||||||
* (Currently this is whatever we have least of.)
|
|
||||||
*/
|
|
||||||
findBestNewUnit: function(gameState)
|
|
||||||
{
|
|
||||||
// Count each type
|
|
||||||
var types = [];
|
|
||||||
for each (var t in this.squadTypes)
|
|
||||||
types.push([t, gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(t))]);
|
|
||||||
|
|
||||||
// Sort by increasing count
|
|
||||||
types.sort(function (a, b) { return a[1] - b[1]; });
|
|
||||||
|
|
||||||
// TODO: we shouldn't return units that we don't have any
|
|
||||||
// buildings capable of training
|
|
||||||
|
|
||||||
return types[0][0];
|
|
||||||
},
|
|
||||||
|
|
||||||
update: function(gameState, planGroups)
|
|
||||||
{
|
|
||||||
// Pause for a minute before starting any work, to give the economy a chance
|
|
||||||
// to start up
|
|
||||||
if (gameState.getTimeElapsed() < 60*1000)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Engine.ProfileStart("military update");
|
|
||||||
|
|
||||||
// Continually try training new units, in batches of 5
|
|
||||||
planGroups.militaryPersonnel.addPlan(100,
|
|
||||||
new UnitTrainingPlan(gameState,
|
|
||||||
this.findBestNewUnit(gameState), 5, { "role": "attack-pending" })
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find the units ready to join the attack
|
|
||||||
var pending = gameState.getOwnEntitiesWithRole("attack-pending");
|
|
||||||
|
|
||||||
// If we have enough units yet, start the attack
|
|
||||||
if (pending.length >= this.targetSquadSize)
|
|
||||||
{
|
|
||||||
// Find the enemy CCs we could attack
|
|
||||||
var targets = gameState.entities.filter(function(ent) {
|
|
||||||
return (ent.isEnemy() && ent.hasClass("CivCentre"));
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there's no CCs, attack anything else that's critical
|
|
||||||
if (targets.length == 0)
|
|
||||||
{
|
|
||||||
targets = gameState.entities.filter(function(ent) {
|
|
||||||
return (ent.isEnemy() && ent.hasClass("ConquestCritical"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a target, move to it
|
|
||||||
if (targets.length)
|
|
||||||
{
|
|
||||||
// Remove the pending role
|
|
||||||
pending.forEach(function(ent) {
|
|
||||||
ent.setMetadata("role", "attack");
|
|
||||||
});
|
|
||||||
|
|
||||||
var target = targets.toEntityArray()[0];
|
|
||||||
var targetPos = target.position();
|
|
||||||
|
|
||||||
// TODO: this should be an attack-move command
|
|
||||||
pending.move(targetPos[0], targetPos[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
@ -1,228 +0,0 @@
|
|||||||
var BuildingConstructionPlan = Class({
|
|
||||||
|
|
||||||
_init: function(gameState, type)
|
|
||||||
{
|
|
||||||
this.type = gameState.applyCiv(type);
|
|
||||||
|
|
||||||
var template = gameState.getTemplate(this.type);
|
|
||||||
if (!template)
|
|
||||||
{
|
|
||||||
this.invalidTemplate = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cost = new Resources(template.cost());
|
|
||||||
},
|
|
||||||
|
|
||||||
canExecute: function(gameState)
|
|
||||||
{
|
|
||||||
if (this.invalidTemplate)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// TODO: verify numeric limits etc
|
|
||||||
|
|
||||||
var builders = gameState.findBuilders(this.type);
|
|
||||||
|
|
||||||
return (builders.length != 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
execute: function(gameState)
|
|
||||||
{
|
|
||||||
// warn("Executing BuildingConstructionPlan "+uneval(this));
|
|
||||||
|
|
||||||
var builders = gameState.findBuilders(this.type).toEntityArray();
|
|
||||||
|
|
||||||
// We don't care which builder we assign, since they won't actually
|
|
||||||
// do the building themselves - all we care about is that there is
|
|
||||||
// some unit that can start the foundation
|
|
||||||
|
|
||||||
var pos = this.findGoodPosition(gameState);
|
|
||||||
|
|
||||||
builders[0].construct(this.type, pos.x, pos.z, pos.angle);
|
|
||||||
},
|
|
||||||
|
|
||||||
getCost: function()
|
|
||||||
{
|
|
||||||
return this.cost;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make each cell's 16-bit value at least one greater than each of its
|
|
||||||
* neighbours' values. (If the grid is initialised with 0s and 65535s,
|
|
||||||
* the result of each cell is its Manhattan distance to the nearest 0.)
|
|
||||||
*
|
|
||||||
* TODO: maybe this should be 8-bit (and clamp at 255)?
|
|
||||||
*/
|
|
||||||
expandInfluences: function(grid, w, h)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < h; ++y)
|
|
||||||
{
|
|
||||||
var min = 65535;
|
|
||||||
for (var x = 0; x < w; ++x)
|
|
||||||
{
|
|
||||||
var g = grid[x + y*w];
|
|
||||||
if (g > min) grid[x + y*w] = min;
|
|
||||||
else if (g < min) min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var x = w-2; x >= 0; --x)
|
|
||||||
{
|
|
||||||
var g = grid[x + y*w];
|
|
||||||
if (g > min) grid[x + y*w] = min;
|
|
||||||
else if (g < min) min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var x = 0; x < w; ++x)
|
|
||||||
{
|
|
||||||
var min = 65535;
|
|
||||||
for (var y = 0; y < h; ++y)
|
|
||||||
{
|
|
||||||
var g = grid[x + y*w];
|
|
||||||
if (g > min) grid[x + y*w] = min;
|
|
||||||
else if (g < min) min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var y = h-2; y >= 0; --y)
|
|
||||||
{
|
|
||||||
var g = grid[x + y*w];
|
|
||||||
if (g > min) grid[x + y*w] = min;
|
|
||||||
else if (g < min) min = g;
|
|
||||||
++min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a circular linear-falloff shape to a grid.
|
|
||||||
*/
|
|
||||||
addInfluence: function(grid, w, h, cx, cy, maxDist)
|
|
||||||
{
|
|
||||||
var x0 = Math.max(0, cx - maxDist);
|
|
||||||
var y0 = Math.max(0, cy - maxDist);
|
|
||||||
var x1 = Math.min(w, cx + maxDist);
|
|
||||||
var y1 = Math.min(h, cy + maxDist);
|
|
||||||
for (var y = y0; y < y1; ++y)
|
|
||||||
{
|
|
||||||
for (var x = x0; x < x1; ++x)
|
|
||||||
{
|
|
||||||
var dx = x - cx;
|
|
||||||
var dy = y - cy;
|
|
||||||
var r = Math.sqrt(dx*dx + dy*dy);
|
|
||||||
if (r < maxDist)
|
|
||||||
grid[x + y*w] += maxDist - r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
findGoodPosition: function(gameState)
|
|
||||||
{
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var cellSize = 4; // size of each tile
|
|
||||||
|
|
||||||
var template = gameState.getTemplate(this.type);
|
|
||||||
|
|
||||||
// Find all tiles in valid territory that are far enough away from obstructions:
|
|
||||||
var passabilityMap = gameState.getPassabilityMap();
|
|
||||||
var territoryMap = gameState.getTerritoryMap();
|
|
||||||
const TERRITORY_PLAYER_MASK = 0x3F;
|
|
||||||
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction");
|
|
||||||
|
|
||||||
if (passabilityMap.data.length != territoryMap.data.length)
|
|
||||||
error("passability and territory data are not matched!");
|
|
||||||
|
|
||||||
// See BuildRestrictions.js
|
|
||||||
switch(template.buildPlacementType())
|
|
||||||
{
|
|
||||||
case "shore":
|
|
||||||
obstructionMask |= gameState.getPassabilityClassMask("building-shore");
|
|
||||||
break;
|
|
||||||
case "land":
|
|
||||||
default:
|
|
||||||
obstructionMask |= gameState.getPassabilityClassMask("building-land");
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerID = gameState.getPlayerID();
|
|
||||||
var buildOwn = template.hasBuildTerritory("own");
|
|
||||||
var buildAlly = template.hasBuildTerritory("ally");
|
|
||||||
var buildNeutral = template.hasBuildTerritory("neutral");
|
|
||||||
var buildEnemy = template.hasBuildTerritory("enemy");
|
|
||||||
|
|
||||||
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
|
|
||||||
for (var i = 0; i < passabilityMap.data.length; ++i)
|
|
||||||
{
|
|
||||||
var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK);
|
|
||||||
var invalidTerritory = (
|
|
||||||
(!buildOwn && tilePlayer == playerID) ||
|
|
||||||
(!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) ||
|
|
||||||
(!buildNeutral && tilePlayer == 0) ||
|
|
||||||
(!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer !=0)
|
|
||||||
);
|
|
||||||
obstructionTiles[i] = (invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 65535;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Engine.DumpImage("obstruction"+playerID+".png", obstructionTiles, passabilityMap.width, passabilityMap.height, 64);
|
|
||||||
|
|
||||||
this.expandInfluences(obstructionTiles, passabilityMap.width, passabilityMap.height);
|
|
||||||
|
|
||||||
// TODO: handle distance restrictions for e.g. CivCentres
|
|
||||||
|
|
||||||
// Compute each tile's closeness to friendly structures:
|
|
||||||
|
|
||||||
var friendlyTiles = new Uint16Array(passabilityMap.data.length);
|
|
||||||
|
|
||||||
gameState.getOwnEntities().forEach(function(ent) {
|
|
||||||
if (ent.hasClass("Structure"))
|
|
||||||
{
|
|
||||||
var infl = 32;
|
|
||||||
if (ent.hasClass("CivCentre"))
|
|
||||||
infl *= 4;
|
|
||||||
|
|
||||||
var pos = ent.position();
|
|
||||||
var x = Math.round(pos[0] / cellSize);
|
|
||||||
var z = Math.round(pos[1] / cellSize);
|
|
||||||
self.addInfluence(friendlyTiles, passabilityMap.width, passabilityMap.height, x, z, infl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find target building's approximate obstruction radius,
|
|
||||||
// and expand by a bit to make sure we're not too close
|
|
||||||
var radius = Math.ceil(template.obstructionRadius() / cellSize) + 2;
|
|
||||||
|
|
||||||
// Find the best non-obstructed tile
|
|
||||||
var bestIdx = 0;
|
|
||||||
var bestVal = -1;
|
|
||||||
for (var i = 0; i < passabilityMap.data.length; ++i)
|
|
||||||
{
|
|
||||||
if (obstructionTiles[i] > radius)
|
|
||||||
{
|
|
||||||
var v = friendlyTiles[i];
|
|
||||||
if (v > bestVal)
|
|
||||||
{
|
|
||||||
bestVal = v;
|
|
||||||
bestIdx = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var x = ((bestIdx % passabilityMap.width) + 0.5) * cellSize;
|
|
||||||
var z = (Math.floor(bestIdx / passabilityMap.width) + 0.5) * cellSize;
|
|
||||||
|
|
||||||
// Engine.DumpImage("influences"+playerID+".png", obstructionTiles, passabilityMap.width, passabilityMap.height, 32);
|
|
||||||
// Engine.DumpImage("friendly"+playerID+".png", friendlyTiles, passabilityMap.width, passabilityMap.height, 256);
|
|
||||||
|
|
||||||
// TODO: special dock placement requirements
|
|
||||||
|
|
||||||
// Fixed angle to match fixed starting cam
|
|
||||||
var angle = 0.75*Math.PI;
|
|
||||||
|
|
||||||
return {
|
|
||||||
"x": x,
|
|
||||||
"z": z,
|
|
||||||
"angle": angle
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,52 +0,0 @@
|
|||||||
var UnitTrainingPlan = Class({
|
|
||||||
|
|
||||||
_init: function(gameState, type, amount, metadata)
|
|
||||||
{
|
|
||||||
this.type = gameState.applyCiv(type);
|
|
||||||
this.amount = amount;
|
|
||||||
this.metadata = metadata;
|
|
||||||
|
|
||||||
var template = gameState.getTemplate(this.type);
|
|
||||||
if (!template)
|
|
||||||
{
|
|
||||||
this.invalidTemplate = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cost = new Resources(template.cost());
|
|
||||||
this.cost.multiply(amount); // (assume no batch discount)
|
|
||||||
},
|
|
||||||
|
|
||||||
canExecute: function(gameState)
|
|
||||||
{
|
|
||||||
if (this.invalidTemplate)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// TODO: we should probably check pop caps
|
|
||||||
|
|
||||||
var trainers = gameState.findTrainers(this.type);
|
|
||||||
|
|
||||||
return (trainers.length != 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
execute: function(gameState)
|
|
||||||
{
|
|
||||||
// warn("Executing UnitTrainingPlan "+uneval(this));
|
|
||||||
|
|
||||||
var trainers = gameState.findTrainers(this.type).toEntityArray();
|
|
||||||
|
|
||||||
// Prefer training buildings with short queues
|
|
||||||
// (TODO: this should also account for units added to the queue by
|
|
||||||
// plans that have already been executed this turn)
|
|
||||||
trainers.sort(function(a, b) {
|
|
||||||
return a.trainingQueueTime() - b.trainingQueueTime();
|
|
||||||
});
|
|
||||||
|
|
||||||
trainers[0].train(this.type, this.amount, this.metadata);
|
|
||||||
},
|
|
||||||
|
|
||||||
getCost: function()
|
|
||||||
{
|
|
||||||
return this.cost;
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,67 +0,0 @@
|
|||||||
/**
|
|
||||||
* All plan classes must implement this interface.
|
|
||||||
*/
|
|
||||||
var IPlan = Class({
|
|
||||||
|
|
||||||
_init: function() { /* ... */ },
|
|
||||||
|
|
||||||
canExecute: function(gameState) { /* ... */ },
|
|
||||||
|
|
||||||
execute: function(gameState) { /* ... */ },
|
|
||||||
|
|
||||||
getCost: function() { /* ... */ },
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a prioritised collection of plans.
|
|
||||||
*/
|
|
||||||
var PlanGroup = Class({
|
|
||||||
|
|
||||||
_init: function()
|
|
||||||
{
|
|
||||||
this.escrow = new Resources({});
|
|
||||||
this.plans = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
addPlan: function(priority, plan)
|
|
||||||
{
|
|
||||||
this.plans.push({"priority": priority, "plan": plan});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes all plans that we can afford, ordered by priority,
|
|
||||||
* and returns the highest-priority plan we couldn't afford (or null
|
|
||||||
* if none).
|
|
||||||
*/
|
|
||||||
executePlans: function(gameState)
|
|
||||||
{
|
|
||||||
// Ignore impossible plans
|
|
||||||
var plans = this.plans.filter(function(p) { return p.plan.canExecute(gameState); });
|
|
||||||
|
|
||||||
// Sort by decreasing priority
|
|
||||||
plans.sort(function(a, b) { return b.priority - a.priority; });
|
|
||||||
|
|
||||||
// Execute as many plans as we can afford
|
|
||||||
while (plans.length && this.escrow.canAfford(plans[0].plan.getCost()))
|
|
||||||
{
|
|
||||||
var plan = plans.shift().plan;
|
|
||||||
this.escrow.subtract(plan.getCost());
|
|
||||||
plan.execute(gameState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plans.length)
|
|
||||||
return plans[0];
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPlans: function()
|
|
||||||
{
|
|
||||||
this.plans = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
getEscrow: function()
|
|
||||||
{
|
|
||||||
return this.escrow;
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,36 +0,0 @@
|
|||||||
var Resources = Class({
|
|
||||||
|
|
||||||
types: ["food", "wood", "stone", "metal"],
|
|
||||||
|
|
||||||
_init: function(amounts)
|
|
||||||
{
|
|
||||||
for each (var t in this.types)
|
|
||||||
this[t] = amounts[t] || 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
canAfford: function(that)
|
|
||||||
{
|
|
||||||
for each (var t in this.types)
|
|
||||||
if (this[t] < that[t])
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
add: function(that)
|
|
||||||
{
|
|
||||||
for each (var t in this.types)
|
|
||||||
this[t] += that[t];
|
|
||||||
},
|
|
||||||
|
|
||||||
subtract: function(that)
|
|
||||||
{
|
|
||||||
for each (var t in this.types)
|
|
||||||
this[t] -= that[t];
|
|
||||||
},
|
|
||||||
|
|
||||||
multiply: function(n)
|
|
||||||
{
|
|
||||||
for each (var t in this.types)
|
|
||||||
this[t] *= n;
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
* This is a primitive initial attempt at an AI player.
|
|
||||||
* The design isn't great and maybe the whole thing should be rewritten -
|
|
||||||
* the aim here is just to have something that basically works, and to
|
|
||||||
* learn more about what's really needed for a decent AI design.
|
|
||||||
*
|
|
||||||
* The basic idea is we have a collection of independent modules
|
|
||||||
* (EconomyManager, etc) which produce a list of plans.
|
|
||||||
* The modules are mostly stateless - each turn they look at the current
|
|
||||||
* world state, and produce some plans that will improve the state.
|
|
||||||
* E.g. if there's too few worker units, they'll do a plan to train
|
|
||||||
* another one. Plans are discarded after the current turn, if they
|
|
||||||
* haven't been executed.
|
|
||||||
*
|
|
||||||
* Plans are grouped into a small number of PlanGroups, and for each
|
|
||||||
* group we try to execute the highest-priority plans.
|
|
||||||
* If some plan groups need more resources to execute their highest-priority
|
|
||||||
* plan, we'll distribute any unallocated resources to that group's
|
|
||||||
* escrow account. Eventually they'll accumulate enough to afford their plan.
|
|
||||||
* (The purpose is to ensure resources are shared fairly between all the
|
|
||||||
* plan groups - none of them should be starved even if they're trying to
|
|
||||||
* execute a really expensive plan.)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Lots of things we should fix:
|
|
||||||
*
|
|
||||||
* * Find entities with no assigned role, and give them something to do
|
|
||||||
* * Keep some units back for defence
|
|
||||||
* * Consistent terminology (type vs template etc)
|
|
||||||
* * ...
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
function TestBotAI(settings)
|
|
||||||
{
|
|
||||||
// warn("Constructing TestBotAI for player "+settings.player);
|
|
||||||
|
|
||||||
BaseAI.call(this, settings);
|
|
||||||
|
|
||||||
this.turn = 0;
|
|
||||||
|
|
||||||
this.modules = [
|
|
||||||
new EconomyManager(),
|
|
||||||
new MilitaryAttackManager(),
|
|
||||||
];
|
|
||||||
|
|
||||||
this.planGroups = {
|
|
||||||
economyPersonnel: new PlanGroup(),
|
|
||||||
economyConstruction: new PlanGroup(),
|
|
||||||
militaryPersonnel: new PlanGroup(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
TestBotAI.prototype = new BaseAI();
|
|
||||||
|
|
||||||
TestBotAI.prototype.ShareResources = function(remainingResources, unaffordablePlans)
|
|
||||||
{
|
|
||||||
// Share our remaining resources among the plangroups that need
|
|
||||||
// to accumulate more resources, in proportion to their priorities
|
|
||||||
|
|
||||||
for each (var type in remainingResources.types)
|
|
||||||
{
|
|
||||||
// Skip resource types where we don't have any spare
|
|
||||||
if (remainingResources[type] <= 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Find the plans that require some of this resource type,
|
|
||||||
// and the sum of their priorities
|
|
||||||
var ps = [];
|
|
||||||
var sumPriority = 0;
|
|
||||||
for each (var p in unaffordablePlans)
|
|
||||||
{
|
|
||||||
if (p.plan.getCost()[type] > p.group.getEscrow()[type])
|
|
||||||
{
|
|
||||||
ps.push(p);
|
|
||||||
sumPriority += p.priority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid divisions-by-zero
|
|
||||||
if (!sumPriority)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Share resources by priority, clamped to the amount the plan actually needs
|
|
||||||
for each (var p in ps)
|
|
||||||
{
|
|
||||||
var amount = Math.floor(remainingResources[type] * p.priority / sumPriority);
|
|
||||||
var max = p.plan.getCost()[type] - p.group.getEscrow()[type];
|
|
||||||
p.group.getEscrow()[type] += Math.min(max, amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TestBotAI.prototype.OnUpdate = function()
|
|
||||||
{
|
|
||||||
// Run the update every n turns, offset depending on player ID to balance the load
|
|
||||||
if ((this.turn + this.player) % 4 == 0)
|
|
||||||
{
|
|
||||||
var gameState = new GameState(this);
|
|
||||||
|
|
||||||
// Find the resources we have this turn that haven't already
|
|
||||||
// been allocated to an escrow account.
|
|
||||||
// (We need to do this before executing any plans, because those will
|
|
||||||
// distort the escrow figures.)
|
|
||||||
var remainingResources = gameState.getResources();
|
|
||||||
for each (var planGroup in this.planGroups)
|
|
||||||
remainingResources.subtract(planGroup.getEscrow());
|
|
||||||
|
|
||||||
Engine.ProfileStart("plan setup");
|
|
||||||
|
|
||||||
// Compute plans from each module
|
|
||||||
for each (var module in this.modules)
|
|
||||||
module.update(gameState, this.planGroups);
|
|
||||||
|
|
||||||
// print(uneval(this.planGroups)+"\n");
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
Engine.ProfileStart("plan execute");
|
|
||||||
|
|
||||||
// Execute as many plans as possible, and keep a record of
|
|
||||||
// which ones we can't afford yet
|
|
||||||
var unaffordablePlans = [];
|
|
||||||
for each (var planGroup in this.planGroups)
|
|
||||||
{
|
|
||||||
var plan = planGroup.executePlans(gameState);
|
|
||||||
if (plan)
|
|
||||||
unaffordablePlans.push({"group": planGroup, "priority": plan.priority, "plan": plan.plan});
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.ProfileStop();
|
|
||||||
|
|
||||||
this.ShareResources(remainingResources, unaffordablePlans);
|
|
||||||
|
|
||||||
// print(uneval(this.planGroups)+"\n");
|
|
||||||
|
|
||||||
// Reset the temporary plan data
|
|
||||||
for each (var planGroup in this.planGroups)
|
|
||||||
planGroup.resetPlans();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.turn++;
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user