AI common-api-v2 and a qbot which works with the new API but it not fully transitioned to make use of it properly
This was SVN commit r11429.
This commit is contained in:
parent
21a39dedfb
commit
7eb5480494
250
binaries/data/mods/public/simulation/ai/common-api-v2/base.js
Normal file
250
binaries/data/mods/public/simulation/ai/common-api-v2/base.js
Normal file
@ -0,0 +1,250 @@
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
"TrainingQueue": 1,
|
||||
"ResourceSupply": 1,
|
||||
"ResourceDropsite": 1,
|
||||
"GarrisonHolder": 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.substr(0, 11) === "foundation|")
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
error("Tried to retrieve invalid template '"+name+"'");
|
||||
return null;
|
||||
};
|
||||
|
||||
BaseAI.prototype.HandleMessage = function(state)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var ent = new Entity(this, state.entities[id]);
|
||||
|
||||
this._entities[id] = ent;
|
||||
}
|
||||
}
|
||||
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 (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
|
||||
};
|
||||
|
||||
BaseAI.prototype.chat = function(message)
|
||||
{
|
||||
Engine.PostCommand({"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])
|
||||
{
|
||||
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];
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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");
|
||||
//*/
|
409
binaries/data/mods/public/simulation/ai/common-api-v2/entity.js
Normal file
409
binaries/data/mods/public/simulation/ai/common-api-v2/entity.js
Normal file
@ -0,0 +1,409 @@
|
||||
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)
|
||||
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.Healable === "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;
|
||||
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.TrainingQueue)
|
||||
return undefined;
|
||||
var civ = this.civ();
|
||||
var templates = this._template.TrainingQueue.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)
|
||||
return undefined;
|
||||
return this._template.GarrisonHolder.List._string.split(/\s+/);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this is an animal that is too difficult to hunt.
|
||||
* (Currently this just includes skittish animals, which are probably
|
||||
* too fast to chase.)
|
||||
*/
|
||||
isUnhuntable: function() {
|
||||
if (!this._template.UnitAI || !this._template.UnitAI.NaturalBehaviour)
|
||||
return false;
|
||||
|
||||
// return (this._template.UnitAI.NaturalBehaviour == "skittish");
|
||||
// Actually, since the AI is currently rubbish at hunting, skip all animals
|
||||
// that aren't really weak:
|
||||
return this._template.Health.Max >= 10;
|
||||
},
|
||||
|
||||
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() { 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 (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() { return this._entity.resourceSupplyAmount; },
|
||||
|
||||
resourceCarrying: function() { 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({"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued});
|
||||
return this;
|
||||
},
|
||||
|
||||
garrison: function(target) {
|
||||
Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false});
|
||||
return this;
|
||||
},
|
||||
|
||||
attack: function(unitId) {
|
||||
Engine.PostCommand({"type": "attack", "entities": [this.id()], "target": unitId, "queued": false});
|
||||
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",
|
||||
"entity": 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({
|
||||
"type": "construct",
|
||||
"entities": [this.id()],
|
||||
"template": template,
|
||||
"x": x,
|
||||
"z": z,
|
||||
"angle": angle,
|
||||
"autorepair": false,
|
||||
"autocontinue": false,
|
||||
"queued": false
|
||||
});
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
@ -0,0 +1,196 @@
|
||||
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({"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;
|
||||
};
|
||||
|
||||
// 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()])
|
||||
{
|
||||
delete this._entities[ent.id()];
|
||||
this._length--;
|
||||
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
|
||||
{
|
||||
this._entities[ent.id()] = ent;
|
||||
this._length++;
|
||||
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;
|
||||
};
|
172
binaries/data/mods/public/simulation/ai/common-api-v2/filters.js
Normal file
172
binaries/data/mods/public/simulation/ai/common-api-v2/filters.js
Normal file
@ -0,0 +1,172 @@
|
||||
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)};
|
||||
},
|
||||
|
||||
byOwner: function(owner){
|
||||
return {"func" : function(ent){
|
||||
return (ent.owner() === owner);
|
||||
},
|
||||
"dynamicProperties": ['owner']};
|
||||
},
|
||||
|
||||
byOwners: function(owners){
|
||||
return {"func" : function(ent){
|
||||
return (owners.indexOf(ent.owner()) !== -1);
|
||||
},
|
||||
"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()){
|
||||
return false;
|
||||
}else{
|
||||
return (VectorDistance(startPoint, ent.position()) < 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 (VectorDistance(startPoint, ent.position()) < 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.resourceSupplyAmount();
|
||||
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;
|
||||
}
|
||||
|
||||
if (type.generic == "treasure"){
|
||||
return (resourceType == type.specific);
|
||||
} else {
|
||||
return (resourceType == type.generic);
|
||||
}
|
||||
},
|
||||
"dynamicProperties": ["resourceSupplyAmount", "owner"]};
|
||||
}
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
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 +1 @@
|
||||
Engine.IncludeModule("common-api");
|
||||
Engine.IncludeModule("common-api-v2");
|
@ -109,6 +109,14 @@ AttackMoveToLocation.prototype.execute = function(gameState, militaryManager){
|
||||
// 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;
|
||||
|
@ -36,52 +36,6 @@ var baseConfig = {
|
||||
}
|
||||
},
|
||||
|
||||
"units" : {
|
||||
"citizenSoldier" : {
|
||||
"default" : [ "units/{civ}_infantry_spearman_b", "units/{civ}_infantry_slinger_b",
|
||||
"units/{civ}_infantry_swordsman_b", "units/{civ}_infantry_javelinist_b",
|
||||
"units/{civ}_infantry_archer_b" ],
|
||||
"hele" : [ "units/hele_infantry_spearman_b", "units/hele_infantry_javelinist_b",
|
||||
"units/hele_infantry_archer_b" ],
|
||||
"cart" : [ "units/cart_infantry_spearman_b", "units/cart_infantry_archer_b" ],
|
||||
"celt" : [ "units/celt_infantry_spearman_b", "units/celt_infantry_javelinist_b" ],
|
||||
"iber" : [ "units/iber_infantry_spearman_b", "units/iber_infantry_slinger_b",
|
||||
"units/iber_infantry_swordsman_b", "units/iber_infantry_javelinist_b" ],
|
||||
"pers" : [ "units/pers_infantry_spearman_b", "units/pers_infantry_archer_b",
|
||||
"units/pers_infantry_javelinist_b" ],
|
||||
"rome" : [ "units/rome_infantry_swordsman_b", "units/rome_infantry_spearman_a",
|
||||
"units/rome_infantry_javelinist_b" ]
|
||||
},
|
||||
"advanced" : {
|
||||
"default" : [ "units/{civ}_cavalry_spearman_b", "units/{civ}_cavalry_javelinist_b",
|
||||
"units/{civ}_champion_cavalry", "units/{civ}_champion_infantry" ],
|
||||
"hele" : [ "units/hele_cavalry_swordsman_b", "units/hele_cavalry_javelinist_b",
|
||||
"units/hele_champion_cavalry_mace", "units/hele_champion_infantry_mace",
|
||||
"units/hele_champion_infantry_polis", "units/hele_champion_ranged_polis",
|
||||
"units/thebes_sacred_band_hoplitai", "units/thespian_melanochitones",
|
||||
"units/sparta_hellenistic_phalangitai", "units/thrace_black_cloak" ],
|
||||
"cart" : [ "units/cart_cavalry_javelinist_b", "units/cart_champion_cavalry",
|
||||
"units/cart_infantry_swordsman_2_b", "units/cart_cavalry_spearman_b",
|
||||
"units/cart_infantry_javelinist_b", "units/cart_infantry_slinger_b",
|
||||
"units/cart_cavalry_swordsman_b", "units/cart_infantry_swordsman_b",
|
||||
"units/cart_cavalry_swordsman_2_b", "units/cart_sacred_band_cavalry" ],
|
||||
"celt" : [ "units/celt_cavalry_javelinist_b", "units/celt_cavalry_swordsman_b", "celt_cavalry_spearman_b",
|
||||
"units/celt_champion_cavalry_gaul", "units/celt_champion_infantry_gaul",
|
||||
"units/celt_champion_cavalry_brit", "units/celt_champion_infantry_brit", "units/celt_fanatic" ],
|
||||
"iber" : [ "units/iber_cavalry_spearman_b", "units/iber_champion_cavalry", "units/iber_champion_infantry" ],
|
||||
"pers" : [ "units/pers_cavalry_javelinist_b", "units/pers_champion_infantry",
|
||||
"units/pers_champion_cavalry", "units/pers_cavalry_spearman_b", "units/pers_cavalry_swordsman_b",
|
||||
"units/pers_cavalry_javelinist_b", "units/pers_cavalry_archer_b", "units/pers_kardakes_hoplite",
|
||||
"units/pers_kardakes_skirmisher", "units/pers_war_elephant" ],
|
||||
"rome" : [ "units/rome_cavalry_spearman_b", "units/rome_champion_infantry", "units/rome_champion_cavalry" ]
|
||||
},
|
||||
"siege" : {
|
||||
"default" : [ "units/{civ}_mechanical_siege_oxybeles", "units/{civ}_mechanical_siege_lithobolos",
|
||||
"units/{civ}_mechanical_siege_ballista", "units/{civ}_mechanical_siege_ram",
|
||||
"units/{civ}_mechanical_siege_scorpio" ]
|
||||
}
|
||||
},
|
||||
|
||||
// qbot
|
||||
"priorities" : { // Note these are dynamic, you are only setting the initial values
|
||||
"house" : 500,
|
||||
|
@ -16,7 +16,7 @@ EconomyManager.prototype.init = function(gameState){
|
||||
|
||||
EconomyManager.prototype.trainMoreWorkers = function(gameState, queues) {
|
||||
// Count the workers in the world and in progress
|
||||
var numWorkers = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv("units/{civ}_support_female_citizen"));
|
||||
var numWorkers = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen"));
|
||||
numWorkers += queues.villager.countTotalQueuedUnits();
|
||||
|
||||
// If we have too few, train more
|
||||
@ -35,16 +35,16 @@ EconomyManager.prototype.pickMostNeededResources = function(gameState) {
|
||||
var self = this;
|
||||
|
||||
// Find what resource type we're most in need of
|
||||
this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState);
|
||||
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] = 0;
|
||||
|
||||
gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) {
|
||||
if (ent.getMetadata("subrole") === "gatherer")
|
||||
numGatherers[ent.getMetadata("gather-type")] += 1;
|
||||
});
|
||||
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) {
|
||||
@ -59,7 +59,7 @@ EconomyManager.prototype.pickMostNeededResources = function(gameState) {
|
||||
|
||||
EconomyManager.prototype.reassignRolelessUnits = function(gameState) {
|
||||
//TODO: Move this out of the economic section
|
||||
var roleless = gameState.getOwnEntitiesWithRole(undefined);
|
||||
var roleless = gameState.getOwnEntitiesByRole(undefined);
|
||||
|
||||
roleless.forEach(function(ent) {
|
||||
if (ent.hasClass("Worker")){
|
||||
@ -85,7 +85,7 @@ EconomyManager.prototype.setWorkersIdleByPriority = function(gameState){
|
||||
totalWeight += this.gatherWeights[type];
|
||||
}
|
||||
|
||||
gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) {
|
||||
gameState.getOwnEntitiesByRole("worker").forEach(function(ent) {
|
||||
if (ent.getMetadata("subrole") === "gatherer"){
|
||||
numGatherers[ent.getMetadata("gather-type")] += 1;
|
||||
totalGatherers += 1;
|
||||
@ -96,7 +96,7 @@ EconomyManager.prototype.setWorkersIdleByPriority = function(gameState){
|
||||
var allocation = Math.floor(totalGatherers * (this.gatherWeights[type]/totalWeight));
|
||||
if (allocation < numGatherers[type]){
|
||||
var numToTake = numGatherers[type] - allocation;
|
||||
gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) {
|
||||
gameState.getOwnEntitiesByRole("worker").forEach(function(ent) {
|
||||
if (ent.getMetadata("subrole") === "gatherer" && ent.getMetadata("gather-type") === type && numToTake > 0){
|
||||
ent.setMetadata("subrole", "idle");
|
||||
numToTake -= 1;
|
||||
@ -111,176 +111,47 @@ EconomyManager.prototype.reassignIdleWorkers = function(gameState) {
|
||||
var self = this;
|
||||
|
||||
// Search for idle workers, and tell them to gather resources based on demand
|
||||
|
||||
var idleWorkers = gameState.getOwnEntitiesWithRole("worker").filter(function(ent) {
|
||||
return (ent.isIdle() || ent.getMetadata("subrole") === "idle");
|
||||
});
|
||||
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;
|
||||
var territoryMap = Map.createTerritoryMap(gameState);
|
||||
//var territoryMap = Map.createTerritoryMap(gameState);
|
||||
|
||||
idleWorkers.forEach(function(ent) {
|
||||
// Check that the worker isn't garrisoned
|
||||
if (ent.position() === undefined){
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var types = self.pickMostNeededResources(gameState);
|
||||
//debug("Most Needed Resources: " + uneval(types));
|
||||
for ( var typeKey in types) {
|
||||
var type = types[typeKey];
|
||||
|
||||
// TODO: we should care about gather rates of workers
|
||||
|
||||
// Find the nearest dropsite for this resource from the worker
|
||||
var nearestDropsite = undefined;
|
||||
var nearbyResources = undefined;
|
||||
var minDropsiteDist = Math.min(); // set to infinity initially
|
||||
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
|
||||
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.resourceDropsiteTypes().indexOf(type) !== -1){
|
||||
var nearby = dropsiteEnt.getMetadata("nearbyResources_" + type);
|
||||
if (dropsiteEnt.position() && nearby && nearby.length > 0){
|
||||
var dist = VectorDistance(ent.position(), dropsiteEnt.position());
|
||||
if (dist < minDropsiteDist){
|
||||
nearestDropsite = dropsiteEnt;
|
||||
minDropsiteDist = dist;
|
||||
nearbyResources = nearby;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!nearbyResources){
|
||||
resourceSupplies = resourceSupplies || gameState.findResourceSupplies();
|
||||
nearbyResources = resourceSupplies[type];
|
||||
}
|
||||
|
||||
// Make sure there are actually some resources of that type
|
||||
if (!nearbyResources){
|
||||
debug("No " + type + " found! (1)");
|
||||
continue;
|
||||
}
|
||||
var numSupplies = nearbyResources.length;
|
||||
|
||||
var workerPosition = ent.position();
|
||||
var supplies = [];
|
||||
var count = 0;
|
||||
|
||||
while (supplies.length == 0 && count <= 1){
|
||||
if (count != 0){
|
||||
resourceSupplies = resourceSupplies || gameState.findResourceSupplies();
|
||||
nearbyResources = resourceSupplies[type];
|
||||
if (!nearbyResources){
|
||||
debug("No " + type + " found! (2)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
count += 1;
|
||||
nearbyResources.forEach(function(supply) {
|
||||
if (! supply.entity){
|
||||
supply = {
|
||||
"entity" : supply,
|
||||
"amount" : supply.resourceSupplyAmount(),
|
||||
"type" : supply.resourceSupplyType(),
|
||||
"position" : supply.position()
|
||||
};
|
||||
}
|
||||
|
||||
// Skip targets that are too hard to hunt
|
||||
if (supply.entity.isUnhuntable()){
|
||||
return;
|
||||
}
|
||||
|
||||
// And don't go for the bloody fish! TODO: better accessibility checks
|
||||
if (supply.entity.hasClass("SeaCreature")){
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't go for floating treasures since we won't be able to reach them and it kills the pathfinder.
|
||||
if (supply.entity.templateName() == "other/special_treasure_shipwreck_debris" ||
|
||||
supply.entity.templateName() == "other/special_treasure_shipwreck" ){
|
||||
return;
|
||||
}
|
||||
|
||||
// Check we can actually reach the resource
|
||||
if (!gameState.ai.accessibility.isAccessible(supply.position)){
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't gather in enemy territory
|
||||
var territory = territoryMap.point(supply.position);
|
||||
if (territory != 0 && gameState.isPlayerEnemy(territory)){
|
||||
return;
|
||||
}
|
||||
|
||||
// measure the distance to the resource
|
||||
var dist = VectorDistance(supply.position, workerPosition);
|
||||
// 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 < 1200 && supply.type.generic == "treasure"){
|
||||
dist /= 1000;
|
||||
}
|
||||
|
||||
// Skip targets that are far too far away (e.g. in the
|
||||
// enemy base), only do this for common supplies
|
||||
if (dist > 6072 && numSupplies > 100){
|
||||
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 0;
|
||||
});
|
||||
|
||||
// Start gathering the best resource (by distance from the dropsite and unit)
|
||||
if (supplies.length) {
|
||||
ent.gather(supplies[0].entity);
|
||||
ent.setMetadata("subrole", "gatherer");
|
||||
ent.setMetadata("gather-type", type);
|
||||
return;
|
||||
}else{
|
||||
debug("No " + type + " found! (3)");
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find any types to gather
|
||||
ent.setMetadata("subrole", "idle");
|
||||
|
||||
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.findFoundations();
|
||||
var foundations = gameState.getOwnFoundations();
|
||||
|
||||
// Check if nothing to build
|
||||
if (!foundations.length){
|
||||
return;
|
||||
}
|
||||
|
||||
var workers = gameState.getOwnEntitiesWithRole("worker");
|
||||
var workers = gameState.getOwnEntitiesByRole("worker");
|
||||
|
||||
var builderWorkers = workers.filter(function(ent) {
|
||||
return (ent.getMetadata("subrole") === "builder");
|
||||
});
|
||||
var builderWorkers = this.workersBySubrole(gameState, "builder");
|
||||
|
||||
// Check if enough builders
|
||||
var extraNeeded = this.targetNumBuilders - builderWorkers.length;
|
||||
@ -302,8 +173,8 @@ EconomyManager.prototype.assignToFoundations = function(gameState) {
|
||||
|
||||
// Order each builder individually, not as a formation
|
||||
nearestNonBuilders.forEach(function(ent) {
|
||||
ent.repair(target);
|
||||
ent.setMetadata("subrole", "builder");
|
||||
ent.setMetadata("target-foundation", target);
|
||||
});
|
||||
};
|
||||
|
||||
@ -311,7 +182,7 @@ EconomyManager.prototype.buildMoreFields = function(gameState, queues) {
|
||||
// give time for treasures to be gathered
|
||||
if (gameState.getTimeElapsed() < 30 * 1000)
|
||||
return;
|
||||
var numFields = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv("structures/{civ}_field"));
|
||||
var numFields = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_field"));
|
||||
numFields += queues.field.totalLength();
|
||||
|
||||
for ( var i = numFields; i < this.targetNumFields; i++) {
|
||||
@ -321,7 +192,7 @@ EconomyManager.prototype.buildMoreFields = function(gameState, queues) {
|
||||
|
||||
// If all the CC's are destroyed then build a new one
|
||||
EconomyManager.prototype.buildNewCC= function(gameState, queues) {
|
||||
var numCCs = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv("structures/{civ}_civil_centre"));
|
||||
var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre"));
|
||||
numCCs += queues.civilCentre.totalLength();
|
||||
|
||||
for ( var i = numCCs; i < 1; i++) {
|
||||
@ -336,29 +207,32 @@ EconomyManager.prototype.updateResourceMaps = function(gameState, events){
|
||||
// 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.findResourceSupplies();
|
||||
if (supplies[resource]){
|
||||
for (var i in supplies[resource]){
|
||||
var current = supplies[resource][i];
|
||||
var x = Math.round(current.position[0] / gameState.cellSize);
|
||||
var z = Math.round(current.position[1] / gameState.cellSize);
|
||||
var strength = Math.round(current.entity.resourceSupplyMax()/decreaseFactor[resource]);
|
||||
this.resourceMaps[resource].addInfluence(x, z, radius[resource], strength);
|
||||
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.rawEntity.template){
|
||||
var ent = new Entity(gameState.ai, e.msg.rawEntity);
|
||||
if (e.msg.entityObj){
|
||||
var ent = e.msg.entityObj;
|
||||
if (ent && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){
|
||||
var x = Math.round(ent.position()[0] / gameState.cellSize);
|
||||
var z = Math.round(ent.position()[1] / gameState.cellSize);
|
||||
@ -370,7 +244,7 @@ EconomyManager.prototype.updateResourceMaps = function(gameState, events){
|
||||
}
|
||||
}
|
||||
|
||||
//this.resourceMaps[resource].dumpIm("tree_density.png");
|
||||
//this.resourceMaps['wood'].dumpIm("tree_density.png");
|
||||
};
|
||||
|
||||
// Returns the position of the best place to build a new dropsite for the specified resource
|
||||
@ -445,20 +319,20 @@ EconomyManager.prototype.updateNearbyResources = function(gameState){
|
||||
var radius = 64;
|
||||
for (key in resources){
|
||||
var resource = resources[key];
|
||||
gameState.getOwnEntities().forEach(function(ent) {
|
||||
if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1
|
||||
&& ent.getMetadata("nearbyResources_" + resource) === undefined){
|
||||
if (!ent.position()){
|
||||
return;
|
||||
}
|
||||
|
||||
gameState.getOwnDropsites(resource).forEach(function(ent) {
|
||||
if (ent.getMetadata("nearby-resources-" + resource) === undefined){
|
||||
var filterPos = Filters.byStaticDistance(ent.position(), radius);
|
||||
|
||||
var filterRes = Filters.byResource(resource);
|
||||
var filterPos = Filters.byDistance(ent.position(), radius);
|
||||
var filter = Filters.and(filterRes, filterPos);
|
||||
var collection = gameState.getResourceSupplies(resource).filter(filterPos);
|
||||
collection.registerUpdates();
|
||||
|
||||
var collection = new EntityCollection(gameState.ai, gameState.entities._entities, filter, gameState);
|
||||
|
||||
ent.setMetadata("nearbyResources_" + resource, collection);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -527,13 +401,13 @@ EconomyManager.prototype.update = function(gameState, queues, events) {
|
||||
Engine.ProfileStop();
|
||||
|
||||
//Later in the game we want to build stuff faster.
|
||||
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.5) {
|
||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.5) {
|
||||
this.targetNumBuilders = 10;
|
||||
}else{
|
||||
this.targetNumBuilders = 5;
|
||||
}
|
||||
|
||||
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.8) {
|
||||
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};
|
||||
@ -545,8 +419,9 @@ EconomyManager.prototype.update = function(gameState, queues, events) {
|
||||
this.updateNearbyResources(gameState);
|
||||
Engine.ProfileStop();
|
||||
|
||||
Engine.ProfileStart("Build new Dropsites");
|
||||
this.buildDropsites(gameState, queues);
|
||||
|
||||
Engine.ProfileStop();
|
||||
|
||||
// TODO: implement a timer based system for this
|
||||
this.setCount += 1;
|
||||
@ -562,6 +437,13 @@ EconomyManager.prototype.update = function(gameState, queues, events) {
|
||||
Engine.ProfileStart("Assign builders");
|
||||
this.assignToFoundations(gameState);
|
||||
Engine.ProfileStop();
|
||||
|
||||
Engine.ProfileStart("Run Workers");
|
||||
gameState.getOwnEntitiesByRole("worker").forEach(function(ent){
|
||||
var worker = new Worker(ent);
|
||||
worker.update(gameState);
|
||||
});
|
||||
Engine.ProfileStop();
|
||||
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
@ -22,11 +22,6 @@ function EntityCollectionFromIds(gameState, idList){
|
||||
return new EntityCollection(gameState.ai, ents);
|
||||
}
|
||||
|
||||
EntityCollection.prototype.attackMove = function(x, z){
|
||||
Engine.PostCommand({"type": "attack-move", "entities": this.toIdArray(), "x": x, "z": z, "queued": false});
|
||||
return this;
|
||||
};
|
||||
|
||||
// Do naughty stuff to replace the entity collection constructor for updating entity collections
|
||||
var tmpEntityCollection = function(baseAI, entities, filter, gameState){
|
||||
this._ai = baseAI;
|
||||
@ -56,11 +51,11 @@ var tmpEntityCollection = function(baseAI, entities, filter, gameState){
|
||||
});
|
||||
};
|
||||
|
||||
tmpEntityCollection.prototype = new EntityCollection;
|
||||
EntityCollection = tmpEntityCollection;
|
||||
//tmpEntityCollection.prototype = new EntityCollection;
|
||||
//EntityCollection = tmpEntityCollection;
|
||||
|
||||
// Keeps an EntityCollection with a filter function up to date by watching for events
|
||||
EntityCollection.prototype.update = function(gameState, events){
|
||||
tmpEntityCollection.prototype.update = function(gameState, events){
|
||||
if (!this.filterFunc)
|
||||
return;
|
||||
for (var i in events){
|
||||
|
@ -1,83 +0,0 @@
|
||||
var Filters = {
|
||||
byClass: function(cls){
|
||||
return function(ent){
|
||||
return ent.hasClass(cls);
|
||||
};
|
||||
},
|
||||
|
||||
byClassesAnd: function(clsList){
|
||||
return function(ent){
|
||||
var ret = true;
|
||||
for (var i in clsList){
|
||||
ret = ret && ent.hasClass(clsList[i]);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
},
|
||||
|
||||
byClassesOr: function(clsList){
|
||||
return function(ent){
|
||||
var ret = false;
|
||||
for (var i in clsList){
|
||||
ret = ret || ent.hasClass(clsList[i]);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
},
|
||||
|
||||
and: function(filter1, filter2){
|
||||
return function(ent, gameState){
|
||||
return filter1(ent, gameState) && filter2(ent, gameState);
|
||||
};
|
||||
},
|
||||
|
||||
or: function(filter1, filter2){
|
||||
return function(ent, gameState){
|
||||
return filter1(ent, gameState) || filter2(ent, gameState);
|
||||
};
|
||||
},
|
||||
|
||||
isEnemy: function(){
|
||||
return function(ent, gameState){
|
||||
return gameState.isEntityEnemy(ent);
|
||||
};
|
||||
},
|
||||
|
||||
isSoldier: function(){
|
||||
return function(ent){
|
||||
return Filters.byClassesOr(["CitizenSoldier", "Super"])(ent);
|
||||
};
|
||||
},
|
||||
|
||||
isIdle: function(){
|
||||
return function(ent){
|
||||
return ent.isIdle();
|
||||
};
|
||||
},
|
||||
|
||||
byDistance: function(startPoint, dist){
|
||||
return function(ent){
|
||||
if (!ent.position()){
|
||||
return false;
|
||||
}else{
|
||||
return (VectorDistance(startPoint, ent.position()) < dist);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
byResource: function(resourceType){
|
||||
return function(ent){
|
||||
var type = ent.resourceSupplyType();
|
||||
if (!type)
|
||||
return false;
|
||||
var amount = ent.resourceSupplyAmount();
|
||||
if (!amount)
|
||||
return false;
|
||||
|
||||
if (type.generic == "treasure")
|
||||
return (resourceType == type.specific);
|
||||
else
|
||||
return (resourceType == type.generic);
|
||||
};
|
||||
}
|
||||
};
|
@ -13,7 +13,23 @@ var GameState = function(ai) {
|
||||
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() {
|
||||
@ -21,8 +37,10 @@ GameState.prototype.getTimeElapsed = function() {
|
||||
};
|
||||
|
||||
GameState.prototype.getTemplate = function(type) {
|
||||
if (!this.templates[type])
|
||||
if (!this.templates[type]){
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EntityTemplate(this.templates[type]);
|
||||
};
|
||||
|
||||
@ -58,8 +76,9 @@ GameState.prototype.getPopulationMax = function() {
|
||||
};
|
||||
|
||||
GameState.prototype.getPassabilityClassMask = function(name) {
|
||||
if (!(name in this.ai.passabilityClasses))
|
||||
if (!(name in this.ai.passabilityClasses)){
|
||||
error("Tried to use invalid passability class name '" + name + "'");
|
||||
}
|
||||
return this.ai.passabilityClasses[name];
|
||||
};
|
||||
|
||||
@ -75,6 +94,16 @@ GameState.prototype.isPlayerEnemy = function(id) {
|
||||
return this.playerData.isEnemy[id];
|
||||
};
|
||||
|
||||
GameState.prototype.getEnemies = function(){
|
||||
var ret = [];
|
||||
for (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()];
|
||||
@ -103,7 +132,36 @@ GameState.prototype.isEntityOwn = function(ent) {
|
||||
};
|
||||
|
||||
GameState.prototype.getOwnEntities = function() {
|
||||
return new EntityCollection(this.ai, this.ai._ownEntities);
|
||||
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() {
|
||||
@ -119,51 +177,49 @@ GameState.prototype.getEntityById = function(id){
|
||||
return undefined;
|
||||
};
|
||||
|
||||
GameState.prototype.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);
|
||||
});
|
||||
});
|
||||
|
||||
GameState.prototype.countEntitiesWithType = function(type) {
|
||||
var count = 0;
|
||||
this.getOwnEntities().forEach(function(ent) {
|
||||
var t = ent.templateName();
|
||||
if (t == type)
|
||||
++count;
|
||||
});
|
||||
return count;
|
||||
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.countEntitiesAndQueuedWithType = function(type) {
|
||||
var foundationType = "foundation|" + type;
|
||||
var count = 0;
|
||||
this.getOwnEntities().forEach(function(ent) {
|
||||
GameState.prototype.getOwnEntitiesByRole = function(role){
|
||||
return this.getOwnEntitiesByMetadata("role", role);
|
||||
};
|
||||
|
||||
var t = ent.templateName();
|
||||
if (t == type || t == foundationType)
|
||||
++count;
|
||||
// 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());
|
||||
};
|
||||
|
||||
var queue = ent.trainingQueue();
|
||||
if (queue) {
|
||||
queue.forEach(function(item) {
|
||||
if (item.template == type)
|
||||
count += item.count;
|
||||
});
|
||||
}
|
||||
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 entities in building production queues
|
||||
this.getOwnTrainingFacilities().forEach(function(ent){
|
||||
ent.trainingQueue().forEach(function(item) {
|
||||
if (item.template == type){
|
||||
count += item.count;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
@ -178,20 +234,19 @@ GameState.prototype.countFoundationsWithType = function(type) {
|
||||
return count;
|
||||
};
|
||||
|
||||
GameState.prototype.countEntitiesAndQueuedWithRole = function(role) {
|
||||
var count = 0;
|
||||
this.getOwnEntities().forEach(function(ent) {
|
||||
GameState.prototype.countOwnEntitiesByRole = function(role) {
|
||||
return this.getOwnEntitiesByRole(role).length;
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
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;
|
||||
};
|
||||
@ -201,10 +256,9 @@ GameState.prototype.countEntitiesAndQueuedWithRole = function(role) {
|
||||
* already too busy.
|
||||
*/
|
||||
GameState.prototype.findTrainers = function(template) {
|
||||
var maxQueueLength = 2; // avoid tying up resources in giant training
|
||||
// queues
|
||||
|
||||
return this.getOwnEntities().filter(function(ent) {
|
||||
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)
|
||||
@ -234,41 +288,17 @@ GameState.prototype.findBuilders = function(template) {
|
||||
});
|
||||
};
|
||||
|
||||
GameState.prototype.findFoundations = function(template) {
|
||||
return this.getOwnEntities().filter(function(ent) {
|
||||
return (ent.foundationProgress() !== undefined);
|
||||
});
|
||||
GameState.prototype.getOwnFoundations = function() {
|
||||
return this.updatingCollection("ownFoundations", Filters.isFoundation(), this.getOwnEntities());
|
||||
};
|
||||
|
||||
GameState.prototype.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;
|
||||
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.getBuildLimits = function() {
|
||||
return this.playerData.buildLimits;
|
||||
@ -278,6 +308,7 @@ GameState.prototype.getBuildCounts = function() {
|
||||
return this.playerData.buildCounts;
|
||||
};
|
||||
|
||||
// Checks whether the maximum number of buildings have been cnstructed for a certain catergory
|
||||
GameState.prototype.isBuildLimitReached = function(category) {
|
||||
if(this.playerData.buildLimits[category] === undefined || this.playerData.buildCounts[category] === undefined)
|
||||
return false;
|
||||
@ -285,4 +316,4 @@ GameState.prototype.isBuildLimitReached = function(category) {
|
||||
return (this.playerData.buildCounts[category] >= this.playerData.buildCounts["CivilCentre"]*this.playerData.buildLimits[category].LimitPerCivCentre);
|
||||
else
|
||||
return (this.playerData.buildCounts[category] >= this.playerData.buildLimits[category]);
|
||||
};
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ HousingManager.prototype.buildMoreHouses = function(gameState, queues) {
|
||||
// predictive in future
|
||||
if (gameState.getPopulationLimit() - gameState.getPopulation() < 20
|
||||
&& gameState.getPopulationLimit() < gameState.getPopulationMax()) {
|
||||
var numConstructing = gameState.countEntitiesWithType(gameState.applyCiv("foundation|structures/{civ}_house"));
|
||||
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)
|
||||
|
@ -8,12 +8,6 @@
|
||||
|
||||
var MilitaryAttackManager = function() {
|
||||
// these use the structure soldiers[unitId] = true|false to register the units
|
||||
this.soldiers = {};
|
||||
this.assigned = {};
|
||||
this.unassigned = {};
|
||||
this.garrisoned = {};
|
||||
this.enemyAttackers = {};
|
||||
|
||||
this.attackManagers = [AttackMoveToLocation];
|
||||
this.availableAttacks = [];
|
||||
this.currentAttacks = [];
|
||||
@ -29,23 +23,6 @@ MilitaryAttackManager.prototype.init = function(gameState) {
|
||||
var civ = gameState.playerData.civ;
|
||||
|
||||
// load units and buildings from the config files
|
||||
if (civ in Config.units.citizenSoldier){
|
||||
this.uCitizenSoldier = Config.units.citizenSoldier[civ];
|
||||
}else{
|
||||
this.uCitizenSoldier = Config.units.citizenSoldier['default'];
|
||||
}
|
||||
|
||||
if (civ in Config.units.advanced){
|
||||
this.uAdvanced = Config.units.advanced[civ];
|
||||
}else{
|
||||
this.uAdvanced = Config.units.advanced['default'];
|
||||
}
|
||||
|
||||
if (civ in Config.units.siege){
|
||||
this.uSiege = Config.units.siege[civ];
|
||||
}else{
|
||||
this.uSiege = Config.units.siege['default'];
|
||||
}
|
||||
|
||||
if (civ in Config.buildings.moderate){
|
||||
this.bModerate = Config.buildings.moderate[civ];
|
||||
@ -65,15 +42,6 @@ MilitaryAttackManager.prototype.init = function(gameState) {
|
||||
this.bFort = Config.buildings.fort['default'];
|
||||
}
|
||||
|
||||
for (var i in this.uCitizenSoldier){
|
||||
this.uCitizenSoldier[i] = gameState.applyCiv(this.uCitizenSoldier[i]);
|
||||
}
|
||||
for (var i in this.uAdvanced){
|
||||
this.uAdvanced[i] = gameState.applyCiv(this.uAdvanced[i]);
|
||||
}
|
||||
for (var i in this.uSiege){
|
||||
this.uSiege[i] = gameState.applyCiv(this.uSiege[i]);
|
||||
}
|
||||
for (var i in this.bAdvanced){
|
||||
this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]);
|
||||
}
|
||||
@ -89,41 +57,62 @@ MilitaryAttackManager.prototype.init = function(gameState) {
|
||||
this.availableAttacks[i] = new this.attackManagers[i](gameState, this);
|
||||
}
|
||||
|
||||
var filter = Filters.and(Filters.isEnemy(), Filters.byClassesOr(["CitizenSoldier", "Super", "Siege"]));
|
||||
this.enemySoldiers = new EntityCollection(gameState.ai, gameState.entities._entities, filter, gameState);
|
||||
var enemies = gameState.getEnemyEntities();
|
||||
var filter = Filters.byClassesOr(["CitizenSoldier", "Super", "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, soldierTypes){
|
||||
var ret = [];
|
||||
MilitaryAttackManager.prototype.findTrainableUnits = function(gameState, soldierType){
|
||||
var allTrainable = [];
|
||||
gameState.getOwnEntities().forEach(function(ent) {
|
||||
var trainable = ent.trainableEntities();
|
||||
for (var i in trainable){
|
||||
if (soldierTypes.indexOf(trainable[i]) !== -1){
|
||||
if (ret.indexOf(trainable[i]) === -1){
|
||||
ret.push(trainable[i]);
|
||||
}
|
||||
}
|
||||
if (allTrainable.indexOf(trainable[i]) === -1){
|
||||
allTrainable.push(trainable[i]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
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("CitizenSoldier") && !ent.hasClass("Cavalry")){
|
||||
return "citizenSoldier";
|
||||
}else if (ent.hasClass("Super") || ent.hasClass("Hero") || 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, soldierTypes) {
|
||||
var units = this.findTrainableUnits(gameState, soldierTypes);
|
||||
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.countEntitiesAndQueuedWithType(gameState.applyCiv(t))
|
||||
types.push([t, gameState.countEntitiesAndQueuedByType(gameState.applyCiv(t))
|
||||
+ queue.countAllByType(gameState.applyCiv(t)) ]);
|
||||
}
|
||||
|
||||
@ -139,91 +128,15 @@ MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, sol
|
||||
};
|
||||
|
||||
MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
|
||||
var soldiers = gameState.getOwnEntitiesWithRole("soldier");
|
||||
var soldiers = gameState.getOwnEntitiesByRole("soldier");
|
||||
var self = this;
|
||||
|
||||
soldiers.forEach(function(ent) {
|
||||
ent.setMetadata("role", "registeredSoldier");
|
||||
self.soldiers[ent.id()] = true;
|
||||
self.unassigned[ent.id()] = true;
|
||||
ent.setMetadata("role", "military");
|
||||
ent.setMetadata("military", "unassigned");
|
||||
});
|
||||
};
|
||||
|
||||
// Ungarrisons all units
|
||||
MilitaryAttackManager.prototype.ungarrisonAll = function(gameState) {
|
||||
debug("ungarrison units");
|
||||
|
||||
this.getGarrisonBuildings(gameState).forEach(function(bldg){
|
||||
bldg.unloadAll();
|
||||
});
|
||||
|
||||
for ( var i in this.garrisoned) {
|
||||
if(this.assigned[i]) {
|
||||
this.unassignUnit(i);
|
||||
}
|
||||
if (this.entity(i)){
|
||||
this.entity(i).setMetadata("subrole","idle");
|
||||
}
|
||||
}
|
||||
this.garrisoned = {};
|
||||
};
|
||||
|
||||
//Garrisons citizens
|
||||
MilitaryAttackManager.prototype.garrisonCitizens = function(gameState) {
|
||||
var self = this;
|
||||
debug("garrison Citizens");
|
||||
gameState.getOwnEntities().forEach(function(ent) {
|
||||
var dogarrison = false;
|
||||
// Look for workers which have a position (i.e. not garrisoned)
|
||||
if(ent.hasClass("Worker") && ent.position()) {
|
||||
for (id in self.enemyAttackers) {
|
||||
if(self.entity(id).visionRange() >= VectorDistance(self.entity(id).position(),ent.position())) {
|
||||
dogarrison = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(dogarrison) {
|
||||
self.garrisonUnit(gameState,ent.id());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// garrison the soldiers
|
||||
MilitaryAttackManager.prototype.garrisonSoldiers = function(gameState) {
|
||||
debug("garrison Soldiers");
|
||||
var units = this.getAvailableUnits(this.countAvailableUnits());
|
||||
for (var i in units) {
|
||||
this.garrisonUnit(gameState,units[i]);
|
||||
if(!this.garrisoned[units[i]]) {
|
||||
this.unassignUnit(units[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MilitaryAttackManager.prototype.garrisonUnit = function(gameState,id) {
|
||||
if (this.entity(id).position() === undefined){
|
||||
return;
|
||||
}
|
||||
var garrisonBuildings = this.getGarrisonBuildings(gameState).toEntityArray();
|
||||
var bldgDistance = [];
|
||||
for (var i in garrisonBuildings) {
|
||||
var bldg = garrisonBuildings[i];
|
||||
if(bldg.garrisoned().length <= bldg.garrisonMax()) {
|
||||
bldgDistance.push([i,VectorDistance(bldg.position(),this.entity(id).position())]);
|
||||
}
|
||||
}
|
||||
if(bldgDistance.length > 0) {
|
||||
bldgDistance.sort(function(a,b) { return (a[1]-b[1]); });
|
||||
var building = garrisonBuildings[bldgDistance[0][0]];
|
||||
//debug("garrison id "+id+"into building "+building.id()+"walking distance "+bldgDistance[0][1]);
|
||||
this.entity(id).garrison(building);
|
||||
this.garrisoned[id] = true;
|
||||
this.entity(id).setMetadata("subrole","garrison");
|
||||
}
|
||||
};
|
||||
|
||||
// return count of enemy buildings for a given building class
|
||||
MilitaryAttackManager.prototype.getEnemyBuildings = function(gameState,cls) {
|
||||
var targets = gameState.entities.filter(function(ent) {
|
||||
@ -232,96 +145,63 @@ MilitaryAttackManager.prototype.getEnemyBuildings = function(gameState,cls) {
|
||||
return targets;
|
||||
};
|
||||
|
||||
// return count of own buildings for a given building class
|
||||
MilitaryAttackManager.prototype.getGarrisonBuildings = function(gameState) {
|
||||
var targets = gameState.getOwnEntities().filter(function(ent) {
|
||||
return (ent.hasClass("Structure") && ent.garrisonableClasses());
|
||||
});
|
||||
return targets;
|
||||
};
|
||||
|
||||
// return n available units and makes these units unavailable
|
||||
MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
|
||||
var ret = [];
|
||||
var count = 0;
|
||||
for (var i in this.unassigned) {
|
||||
if (this.unassigned[i]){
|
||||
var ent = this.entity(i);
|
||||
if (filter){
|
||||
if (!filter(ent)){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ret.push(+i);
|
||||
delete this.unassigned[i];
|
||||
this.assigned[i] = true;
|
||||
ent.setMetadata("role", "assigned");
|
||||
ent.setMetadata("subrole", "unavailable");
|
||||
count++;
|
||||
if (count >= n) {
|
||||
break;
|
||||
this.getUnassignedUnits().forEach(function(ent){
|
||||
if (filter){
|
||||
if (!filter(ent)){
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.unassigned[unit] = true;
|
||||
this.assigned[unit] = false;
|
||||
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.unassigned[units[i]] = true;
|
||||
this.assigned[units[i]] = false;
|
||||
this.unassignUnit(units[i]);
|
||||
}
|
||||
};
|
||||
|
||||
MilitaryAttackManager.prototype.getUnassignedUnits = function(){
|
||||
return this.gameState.getOwnEntitiesByMetadata("military", "unassigned");
|
||||
};
|
||||
|
||||
MilitaryAttackManager.prototype.countAvailableUnits = function(filter){
|
||||
var count = 0;
|
||||
for (var i in this.unassigned){
|
||||
if (this.unassigned[i]){
|
||||
if (filter){
|
||||
if (filter(this.entity(i))){
|
||||
count += 1;
|
||||
}
|
||||
}else{
|
||||
count += 1;
|
||||
this.getUnassignedUnits().forEach(function(ent){
|
||||
if (filter){
|
||||
if (filter(ent)){
|
||||
count += 1;
|
||||
}
|
||||
}else{
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
MilitaryAttackManager.prototype.handleEvents = function(gameState, events) {
|
||||
var myCivCentres = gameState.getOwnEntities().filter(function(ent) {
|
||||
return ent.hasClass("CivCentre");
|
||||
}).toEntityArray();
|
||||
var pos = undefined;
|
||||
if (myCivCentres.length > 0 && myCivCentres[0].position()){
|
||||
pos = myCivCentres[0].position();
|
||||
}
|
||||
|
||||
for (var i in events) {
|
||||
var e = events[i];
|
||||
|
||||
if (e.type === "Destroy") {
|
||||
var id = e.msg.entity;
|
||||
delete this.unassigned[id];
|
||||
delete this.assigned[id];
|
||||
delete this.soldiers[id];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Takes an entity id and returns an entity object or false 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) {
|
||||
if (this.gameState.entities._entities[id]) {
|
||||
return new Entity(this.gameState.ai, this.gameState.entities._entities[id]);
|
||||
return this.gameState.entities._entities[id]; // TODO: make this nicer
|
||||
}else{
|
||||
//debug("Entity " + id + " requested does not exist");
|
||||
}
|
||||
@ -388,11 +268,10 @@ MilitaryAttackManager.prototype.getUnitStrength = function(ent){
|
||||
// Returns the strength of the available units of ai army
|
||||
MilitaryAttackManager.prototype.measureAvailableStrength = function(){
|
||||
var strength = 0.0;
|
||||
for (var i in this.unassigned){
|
||||
if (this.unassigned[i] && this.entity(i)){
|
||||
strength += this.getUnitStrength(this.entity(i));
|
||||
}
|
||||
}
|
||||
var self = this;
|
||||
this.getUnassignedUnits(this.gameState).forEach(function(ent){
|
||||
strength += self.getUnitStrength(ent);
|
||||
});
|
||||
return strength;
|
||||
};
|
||||
|
||||
@ -448,7 +327,7 @@ MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
|
||||
|
||||
// Adds towers to the defenceBuilding queue
|
||||
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
|
||||
if (gameState.countEntitiesAndQueuedWithType(gameState.applyCiv('structures/{civ}_defense_tower'))
|
||||
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'))
|
||||
+ queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["DefenseTower"]) {
|
||||
|
||||
|
||||
@ -465,11 +344,11 @@ MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
|
||||
|
||||
var numFortresses = 0;
|
||||
for (var i in this.bFort){
|
||||
numFortresses += gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(this.bFort[i]));
|
||||
numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]));
|
||||
}
|
||||
|
||||
if (numFortresses + queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["Fortress"]) {
|
||||
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules[0].targetNumWorkers * 0.5){
|
||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules[0].targetNumWorkers * 0.5){
|
||||
if (gameState.getTimeElapsed() > 350 * 1000 * numFortresses){
|
||||
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){
|
||||
var position = gameState.ai.pathsToMe.shift();
|
||||
@ -484,12 +363,10 @@ MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
|
||||
};
|
||||
|
||||
MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
|
||||
|
||||
var self = this;
|
||||
Engine.ProfileStart("military update");
|
||||
this.gameState = gameState;
|
||||
|
||||
this.handleEvents(gameState, events);
|
||||
|
||||
// this.attackElephants(gameState);
|
||||
this.registerSoldiers(gameState);
|
||||
//this.defence(gameState);
|
||||
@ -497,9 +374,10 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
|
||||
|
||||
this.defenceManager.update(gameState, events, this);
|
||||
|
||||
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, this.uCitizenSoldier);
|
||||
var newUnit = this.findBestNewUnit(gameState, queues.citizenSoldier, "citizenSoldier");
|
||||
if (newUnit){
|
||||
queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
||||
"role" : "soldier"
|
||||
@ -507,7 +385,7 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
|
||||
}
|
||||
}
|
||||
if (queues.advancedSoldier.length() < 2) {
|
||||
var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, this.uAdvanced);
|
||||
var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, "advanced");
|
||||
if (newUnit){
|
||||
queues.advancedSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
||||
"role" : "soldier"
|
||||
@ -515,59 +393,68 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
|
||||
}
|
||||
}
|
||||
if (queues.siege.length() < 4) {
|
||||
var newUnit = this.findBestNewUnit(gameState, queues.siege, this.uSiege);
|
||||
var newUnit = this.findBestNewUnit(gameState, queues.siege, "siege");
|
||||
if (newUnit){
|
||||
queues.siege.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
||||
"role" : "soldier"
|
||||
}, 2));
|
||||
}
|
||||
}
|
||||
|
||||
Engine.ProfileStop();
|
||||
|
||||
// Build more military buildings
|
||||
// TODO: make military building better
|
||||
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 30) {
|
||||
if (gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(this.bModerate[0]))
|
||||
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.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) >
|
||||
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) >
|
||||
gameState.ai.modules[0].targetNumWorkers * 0.7){
|
||||
if (queues.militaryBuilding.totalLength() === 0){
|
||||
for (var i in this.bAdvanced){
|
||||
if (gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(this.bAdvanced[i])) < 1){
|
||||
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){
|
||||
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Engine.ProfileStop();
|
||||
|
||||
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!");
|
||||
//debug("Attacking!");
|
||||
}
|
||||
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this));
|
||||
}
|
||||
|
||||
Engine.ProfileStop();
|
||||
|
||||
Engine.ProfileStart("Update attacks");
|
||||
// Keep current attacks updated
|
||||
for (i in this.currentAttacks){
|
||||
this.currentAttacks[i].update(gameState, this, events);
|
||||
}
|
||||
Engine.ProfileStop();
|
||||
|
||||
// Set unassigned to be workers
|
||||
for (var i in this.unassigned){
|
||||
if (this.entity(i).hasClass("CitizenSoldier") && ! this.entity(i).hasClass("Cavalry")){
|
||||
this.entity(i).setMetadata("role", "worker");
|
||||
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();
|
||||
|
||||
// Dynamically change priorities
|
||||
|
||||
var females = gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen"));
|
||||
Engine.ProfileStart("Change Priorities");
|
||||
var females = gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen"));
|
||||
var femalesTarget = gameState.ai.modules[0].targetNumWorkers;
|
||||
var enemyStrength = this.measureEnemyStrength(gameState);
|
||||
var availableStrength = this.measureAvailableStrength();
|
||||
@ -582,6 +469,7 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
|
||||
if (females/femalesTarget > 0.7){
|
||||
gameState.ai.priorities.defenceBuilding = 70;
|
||||
}
|
||||
Engine.ProfileStop();
|
||||
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
@ -56,21 +56,19 @@ QBotAI.prototype.runInit = function(gameState){
|
||||
myKeyEntities = gameState.getOwnEntities();
|
||||
}
|
||||
|
||||
var filter = Filters.and(Filters.isEnemy(), Filters.byClass("CivCentre"));
|
||||
var enemyKeyEntities = gameState.getEntities().filter(function(ent) {
|
||||
return ent.hasClass("CivCentre") && gameState.isEntityEnemy(ent);
|
||||
});
|
||||
|
||||
var filter = Filters.byClass("CivCentre");
|
||||
var enemyKeyEntities = gameState.getEnemyEntities().filter(filter);
|
||||
|
||||
if (enemyKeyEntities.length == 0){
|
||||
enemyKeyEntities = gameState.getEntities().filter(function(ent) {
|
||||
return gameState.isEntityEnemy(ent);
|
||||
});
|
||||
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');
|
||||
}
|
||||
@ -159,11 +157,11 @@ function debug(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];
|
||||
}
|
||||
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,11 +1,11 @@
|
||||
//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).
|
||||
// Optional 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.
|
||||
//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 //
|
||||
|
115
binaries/data/mods/public/simulation/ai/qbot/worker.js
Normal file
115
binaries/data/mods/public/simulation/ai/qbot/worker.js
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* This class makes a worker do as instructed by the economy manager
|
||||
*/
|
||||
|
||||
var Worker = function(ent) {
|
||||
this.ent = ent;
|
||||
};
|
||||
|
||||
Worker.prototype.update = function(gameState) {
|
||||
var subrole = this.ent.getMetadata("subrole");
|
||||
|
||||
if (subrole === "gatherer"){
|
||||
if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.getResourceType(this.ent.unitAIOrderData().type) === this.ent.getMetadata("gather-type"))
|
||||
&& !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){
|
||||
// TODO: handle combat for hunting animals
|
||||
Engine.ProfileStart("Start Gathering");
|
||||
this.startGathering(gameState);
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
}else if(subrole === "builder"){
|
||||
if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){
|
||||
var target = this.ent.getMetadata("target-foundation");
|
||||
this.ent.repair(target);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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.postion(), dropsite.position());
|
||||
if (dist < minDropsiteDist){
|
||||
minDropsiteDist = dist;
|
||||
nearestResources = dropsite.getMetadata("nearby-resources-" + type);
|
||||
nearestDropsite = dropsite;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!nearestResources){
|
||||
nearestResources = gameState.getResourceSupplies(resource);
|
||||
}
|
||||
|
||||
if (nearestResources.length === 0){
|
||||
warn("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;
|
||||
}
|
||||
|
||||
// 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 < 200 && supply.resourceSupplyType().generic == "treasure"){
|
||||
dist /= 1000;
|
||||
}
|
||||
|
||||
// Skip targets that are far too far away (e.g. in the
|
||||
// enemy base), only do this for common supplies
|
||||
if (dist > 600){
|
||||
return;
|
||||
}
|
||||
|
||||
if (dist < nearestSupplyDist){
|
||||
nearestSupplyDist = dist;
|
||||
nearestSupply = supply;
|
||||
}
|
||||
});
|
||||
|
||||
if (nearestSupply) {
|
||||
ent.gather(nearestSupply);
|
||||
}else{
|
||||
debug("No " + resource + " found! (2)");
|
||||
}
|
||||
};
|
||||
|
||||
Worker.prototype.getResourceType = function(type){
|
||||
if (!type || !type.generic){
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (type.generic === "treasure"){
|
||||
return type.specific;
|
||||
}else{
|
||||
return type.generic;
|
||||
}
|
||||
};
|
@ -105,6 +105,20 @@ AIProxy.prototype.OnUnitIdleChanged = function(msg)
|
||||
this.changes.idle = msg.idle;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnUnitAIStateChanged = function(msg)
|
||||
{
|
||||
this.NotifyChange();
|
||||
|
||||
this.changes.unitAIState = msg.to;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnUnitAIOrderDataChanged = function(msg)
|
||||
{
|
||||
this.NotifyChange();
|
||||
|
||||
this.changes.unitAIOrderData = msg.to;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnTrainingQueueChanged = function(msg)
|
||||
{
|
||||
this.NotifyChange();
|
||||
@ -168,6 +182,10 @@ AIProxy.prototype.GetFullRepresentation = function()
|
||||
{
|
||||
// Updated by OnUnitIdleChanged
|
||||
ret.idle = cmpUnitAI.IsIdle();
|
||||
// Updated by OnUnitAIStateChanged
|
||||
ret.unitAIState = cmpUnitAI.GetCurrentState();
|
||||
// Updated by OnUnitAIOrderDataChanged
|
||||
ret.unitAIOrderData = cmpUnitAI.GetOrderData();
|
||||
}
|
||||
|
||||
var cmpTrainingQueue = Engine.QueryInterface(this.entity, IID_TrainingQueue);
|
||||
|
@ -1486,13 +1486,18 @@ UnitAI.prototype.IsGarrisoned = function()
|
||||
UnitAI.prototype.OnCreate = function()
|
||||
{
|
||||
if (this.IsAnimal())
|
||||
UnitFsm.Init(this, "ANIMAL.FEEDING");
|
||||
UnitFsm.Init(this, "ANIMAL.FEEDING", this.StateChanged);
|
||||
else if (this.IsFormationController())
|
||||
UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
|
||||
UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE", this.StateChanged);
|
||||
else
|
||||
UnitFsm.Init(this, "INDIVIDUAL.IDLE");
|
||||
UnitFsm.Init(this, "INDIVIDUAL.IDLE", this.StateChanged);
|
||||
};
|
||||
|
||||
UnitAI.prototype.StateChanged = function()
|
||||
{
|
||||
Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": this.GetCurrentState()});
|
||||
}
|
||||
|
||||
UnitAI.prototype.OnOwnershipChanged = function(msg)
|
||||
{
|
||||
this.SetupRangeQuery();
|
||||
@ -1557,6 +1562,11 @@ UnitAI.prototype.DeferMessage = function(msg)
|
||||
UnitFsm.DeferMessage(this, msg);
|
||||
};
|
||||
|
||||
UnitAI.prototype.GetCurrentState = function()
|
||||
{
|
||||
return UnitFsm.GetCurrentState(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Call when the current order has been completed (or failed).
|
||||
* Removes the current order from the queue, and processes the
|
||||
@ -1576,6 +1586,8 @@ UnitAI.prototype.FinishOrder = function()
|
||||
var ret = UnitFsm.ProcessMessage(this,
|
||||
{"type": "Order."+this.order.type, "data": this.order.data}
|
||||
);
|
||||
|
||||
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData()});
|
||||
|
||||
// If the order was rejected then immediately take it off
|
||||
// and process the remaining queue
|
||||
@ -1610,6 +1622,8 @@ UnitAI.prototype.PushOrder = function(type, data)
|
||||
var ret = UnitFsm.ProcessMessage(this,
|
||||
{"type": "Order."+this.order.type, "data": this.order.data}
|
||||
);
|
||||
|
||||
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData()});
|
||||
|
||||
// If the order was rejected then immediately take it off
|
||||
// and process the remaining queue
|
||||
@ -1640,6 +1654,8 @@ UnitAI.prototype.PushOrderFront = function(type, data)
|
||||
var ret = UnitFsm.ProcessMessage(this,
|
||||
{"type": "Order."+this.order.type, "data": this.order.data}
|
||||
);
|
||||
|
||||
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData()});
|
||||
|
||||
// If the order was rejected then immediately take it off again;
|
||||
// assume the previous active order is still valid (the short-lived
|
||||
@ -1682,6 +1698,16 @@ UnitAI.prototype.AddOrders = function(orders)
|
||||
}
|
||||
}
|
||||
|
||||
UnitAI.prototype.GetOrderData = function()
|
||||
{
|
||||
if (this.order && this.order.data)
|
||||
{
|
||||
return eval(uneval(this.order.data)); // return a copy
|
||||
}
|
||||
else
|
||||
return undefined;
|
||||
}
|
||||
|
||||
UnitAI.prototype.TimerHandler = function(data, lateness)
|
||||
{
|
||||
// Reset the timer
|
||||
|
@ -3,3 +3,9 @@ Engine.RegisterInterface("UnitAI");
|
||||
// Message of the form { "idle": true },
|
||||
// sent whenever the unit's idle status changes.
|
||||
Engine.RegisterMessageType("UnitIdleChanged");
|
||||
// Message of the form { "to": "STATE.NAME" }.
|
||||
// sent whenever the units changes state
|
||||
Engine.RegisterMessageType("UnitAIStateChanged");
|
||||
// Message of the form { "to": orderData }.
|
||||
// sent whenever the units changes state
|
||||
Engine.RegisterMessageType("UnitAIOrderDataChanged");
|
||||
|
@ -224,13 +224,15 @@ function FSM(spec)
|
||||
process(this, spec, [], {});
|
||||
}
|
||||
|
||||
FSM.prototype.Init = function(obj, initialState)
|
||||
FSM.prototype.Init = function(obj, initialState, stateChangedCallback)
|
||||
{
|
||||
this.deferFromState = undefined;
|
||||
|
||||
obj.fsmStateName = "";
|
||||
obj.fsmNextState = undefined;
|
||||
this.SwitchToNextState(obj, initialState);
|
||||
|
||||
this.stateChangedCallback = stateChangedCallback;
|
||||
};
|
||||
|
||||
FSM.prototype.SetNextState = function(obj, state)
|
||||
@ -305,6 +307,11 @@ FSM.prototype.LookupState = function(currentStateName, stateName)
|
||||
return stateName;
|
||||
};
|
||||
|
||||
FSM.prototype.GetCurrentState = function(obj)
|
||||
{
|
||||
return obj.fsmStateName;
|
||||
};
|
||||
|
||||
FSM.prototype.SwitchToNextState = function(obj, nextStateName)
|
||||
{
|
||||
var fromState = this.decompose[obj.fsmStateName];
|
||||
@ -329,7 +336,11 @@ FSM.prototype.SwitchToNextState = function(obj, nextStateName)
|
||||
{
|
||||
obj.fsmStateName = fromState[i];
|
||||
if (leave.apply(obj))
|
||||
{
|
||||
if (this.stateChangedCallback)
|
||||
this.stateChangedCallback.apply(obj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,11 +351,18 @@ FSM.prototype.SwitchToNextState = function(obj, nextStateName)
|
||||
{
|
||||
obj.fsmStateName = toState[i];
|
||||
if (enter.apply(obj))
|
||||
{
|
||||
if (this.stateChangedCallback)
|
||||
this.stateChangedCallback.apply(obj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obj.fsmStateName = nextStateName;
|
||||
|
||||
if (this.stateChangedCallback)
|
||||
this.stateChangedCallback.apply(obj);
|
||||
};
|
||||
|
||||
Engine.RegisterGlobal("FSM", FSM);
|
||||
|
Loading…
Reference in New Issue
Block a user