Initial terrible AI player scripts.
This was SVN commit r8891.
This commit is contained in:
parent
ff785853ad
commit
57e5bb878a
@ -6,6 +6,8 @@ function BaseAI(settings)
|
||||
// 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});
|
||||
|
||||
this._entityMetadata = {};
|
||||
}
|
||||
|
||||
BaseAI.prototype.HandleMessage = function(state)
|
||||
@ -15,24 +17,41 @@ BaseAI.prototype.HandleMessage = function(state)
|
||||
else
|
||||
this.ApplyEntitiesDelta(state);
|
||||
|
||||
//print("### "+uneval(state)+"\n\n");
|
||||
//print("@@@ "+uneval(this._rawEntities)+"\n\n");
|
||||
|
||||
this.entities = new EntityCollection(this, this._rawEntities);
|
||||
this.player = this._player;
|
||||
this.playerData = state.players[this._player];
|
||||
this.templates = this._templates;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
|
||||
this.OnUpdate();
|
||||
|
||||
// Clean up temporary properties, so they don't disturb the serializer
|
||||
delete this.entities;
|
||||
delete this.player;
|
||||
delete this.playerData;
|
||||
delete this.templates;
|
||||
delete this.timeElapsed;
|
||||
};
|
||||
|
||||
BaseAI.prototype.ApplyEntitiesDelta = function(state)
|
||||
{
|
||||
for each (var evt in state.events)
|
||||
{
|
||||
if (evt.type == "Destroy")
|
||||
if (evt.type == "Create")
|
||||
{
|
||||
this._rawEntities[evt.msg.entity] = {};
|
||||
}
|
||||
else if (evt.type == "Destroy")
|
||||
{
|
||||
delete this._rawEntities[evt.msg.entity];
|
||||
delete this._entityMetadata[evt.msg.entity];
|
||||
}
|
||||
else if (evt.type == "TrainingFinished")
|
||||
{
|
||||
for each (var ent in evt.msg.entities)
|
||||
{
|
||||
this._entityMetadata[ent] = evt.msg.metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
36
binaries/data/mods/public/simulation/ai/common-api/class.js
Normal file
36
binaries/data/mods/public/simulation/ai/common-api/class.js
Normal file
@ -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");
|
||||
//*/
|
@ -1,86 +1,85 @@
|
||||
function Entity(baseAI, entity)
|
||||
{
|
||||
this._ai = baseAI;
|
||||
this._entity = entity;
|
||||
this._template = baseAI._templates[entity.template];
|
||||
}
|
||||
var EntityTemplate = Class({
|
||||
|
||||
Entity.prototype = {
|
||||
get rank() {
|
||||
_init: function(template)
|
||||
{
|
||||
this._template = template;
|
||||
},
|
||||
|
||||
rank: function() {
|
||||
if (!this._template.Identity)
|
||||
return undefined;
|
||||
return this._template.Identity.Rank;
|
||||
},
|
||||
|
||||
get classes() {
|
||||
classes: function() {
|
||||
if (!this._template.Identity || !this._template.Identity.Classes)
|
||||
return undefined;
|
||||
return this._template.Identity.Classes._string.split(/\s+/);
|
||||
},
|
||||
|
||||
get civ() {
|
||||
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;
|
||||
|
||||
get position() { return this._entity.position; },
|
||||
var ret = {};
|
||||
for (var type in this._template.Cost.Resources)
|
||||
ret[type] = +this._template.Cost.Resources[type];
|
||||
return ret;
|
||||
},
|
||||
|
||||
|
||||
get hitpoints() { return this._entity.hitpoints; },
|
||||
get maxHitpoints() { return this._template.Health.Max; },
|
||||
get isHurt() { return this.hitpoints < this.maxHitpoints; },
|
||||
get needsHeal() { return this.isHurt && (this._template.Health.Healable == "true"); },
|
||||
get needsRepair() { return this.isHurt && (this._template.Health.Repairable == "true"); },
|
||||
maxHitpoints: function() { return this._template.Health.Max; },
|
||||
isHealable: function() { return this._template.Health.Healable === "true"; },
|
||||
isRepairable: function() { return this._template.Health.Repairable === "true"; },
|
||||
|
||||
|
||||
// TODO: attack, armour
|
||||
|
||||
|
||||
get buildableEntities() {
|
||||
buildableEntities: function() {
|
||||
if (!this._template.Builder)
|
||||
return undefined;
|
||||
var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, this.civ).split(/\s+/);
|
||||
var civ = this.civ();
|
||||
var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
|
||||
return templates; // TODO: map to Entity?
|
||||
},
|
||||
|
||||
get trainableEntities() {
|
||||
trainableEntities: function() {
|
||||
if (!this._template.TrainingQueue)
|
||||
return undefined;
|
||||
var templates = this._template.TrainingQueue.Entities._string.replace(/\{civ\}/g, this.civ).split(/\s+/);
|
||||
var civ = this.civ();
|
||||
var templates = this._template.TrainingQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
|
||||
return templates;
|
||||
},
|
||||
|
||||
|
||||
get trainingQueue() { return this._entity.trainingQueue; },
|
||||
|
||||
get foundationProgress() { return this._entities.foundationProgress; },
|
||||
|
||||
|
||||
get owner() { return this._entity.owner; },
|
||||
get isOwn() { return this._entity.owner == this._ai._player; },
|
||||
get isFriendly() { return this.isOwn; }, // TODO: diplomacy
|
||||
get isEnemy() { return !this.isOwn; }, // TODO: diplomacy
|
||||
|
||||
|
||||
get resourceSupplyType() {
|
||||
resourceSupplyType: function() {
|
||||
if (!this._template.ResourceSupply)
|
||||
return undefined;
|
||||
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
|
||||
return { "generic": type, "specific": subtype };
|
||||
},
|
||||
|
||||
get resourceSupplyMax() {
|
||||
resourceSupplyMax: function() {
|
||||
if (!this._template.ResourceSupply)
|
||||
return undefined;
|
||||
return +this._template.ResourceSupply.Amount;
|
||||
},
|
||||
|
||||
get resourceSupplyAmount() { return this._entity.resourceSupplyAmount; },
|
||||
|
||||
|
||||
get resourceGatherRates() {
|
||||
resourceGatherRates: function() {
|
||||
if (!this._template.ResourceGatherer)
|
||||
return undefined;
|
||||
var ret = {};
|
||||
@ -89,35 +88,161 @@ Entity.prototype = {
|
||||
return ret;
|
||||
},
|
||||
|
||||
get resourceCarrying() { return this._entity.resourceCarrying; },
|
||||
|
||||
|
||||
get resourceDropsiteTypes() {
|
||||
resourceDropsiteTypes: function() {
|
||||
if (!this._template.ResourceDropsite)
|
||||
return undefined;
|
||||
return this._template.ResourceDropsite.Types.split(/\s+/);
|
||||
},
|
||||
|
||||
|
||||
get garrisoned() { return new EntityCollection(this._ai, this._entity.garrisoned); },
|
||||
|
||||
get garrisonableClasses() {
|
||||
garrisonableClasses: function() {
|
||||
if (!this._template.GarrisonHolder)
|
||||
return undefined;
|
||||
return this._template.GarrisonHolder.List._string.split(/\s+/);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
var Entity = Class({
|
||||
_super: EntityTemplate,
|
||||
|
||||
_init: function(baseAI, entity)
|
||||
{
|
||||
this._super.call(this, baseAI._templates[entity.template]);
|
||||
|
||||
this._ai = baseAI;
|
||||
this._templateName = entity.template;
|
||||
this._entity = entity;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return "[Entity " + this.id() + " " + this.templateName() + "]";
|
||||
},
|
||||
|
||||
id: function() {
|
||||
return this._entity.id;
|
||||
},
|
||||
|
||||
templateName: function() {
|
||||
return this._templateName;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns extra data that the AI scripts have associated with this entity,
|
||||
* for arbitrary local annotations.
|
||||
* (This data is not shared with any other AI scripts.)
|
||||
*/
|
||||
getMetadata: function(id) {
|
||||
var metadata = this._ai._entityMetadata[this.id()];
|
||||
if (!metadata)
|
||||
return undefined;
|
||||
return metadata[id];
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets extra data to be associated with this entity.
|
||||
*/
|
||||
setMetadata: function(id, value) {
|
||||
var metadata = this._ai._entityMetadata[this.id()];
|
||||
if (!metadata)
|
||||
metadata = this._ai._entityMetadata[this.id()] = {};
|
||||
metadata[id] = value;
|
||||
},
|
||||
|
||||
position: function() { return this._entity.position; },
|
||||
|
||||
isIdle: function() {
|
||||
if (typeof this._entity.idle === "undefined")
|
||||
return undefined;
|
||||
return this._entity.idle;
|
||||
},
|
||||
|
||||
hitpoints: function() { return this._entity.hitpoints; },
|
||||
isHurt: function() { return this.hitpoints < this.maxHitpoints; },
|
||||
needsHeal: function() { return this.isHurt && this.isHealable; },
|
||||
needsRepair: function() { return this.isHurt && this.isRepairable; },
|
||||
|
||||
/**
|
||||
* Returns the current training queue state, of the form
|
||||
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
|
||||
*/
|
||||
trainingQueue: function() {
|
||||
var queue = this._entity.trainingQueue;
|
||||
return queue;
|
||||
},
|
||||
|
||||
trainingQueueTime: function() {
|
||||
var queue = this._entity.trainingQueue;
|
||||
if (!queue)
|
||||
return undefined;
|
||||
// TODO: compute total time for units in training queue
|
||||
return queue.length;
|
||||
},
|
||||
|
||||
foundationProgress: function() { 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) {
|
||||
Engine.PostCommand({"type": "walk", "entities": [this.entity.id], "x": x, "z": z, "queued": false});
|
||||
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": false});
|
||||
return this;
|
||||
},
|
||||
|
||||
gather: function(target) {
|
||||
Engine.PostCommand({"type": "gather", "entities": [this.id()], "target": target.id(), "queued": false});
|
||||
return this;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
Engine.PostCommand({"type": "delete-entities", "entities": [this.entity.id]});
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2,9 +2,14 @@ function EntityCollection(baseAI, entities)
|
||||
{
|
||||
this._ai = baseAI;
|
||||
this._entities = entities;
|
||||
|
||||
var length = 0;
|
||||
for (var id in entities)
|
||||
++length;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
EntityCollection.prototype.ToIdArray = function()
|
||||
EntityCollection.prototype.toIdArray = function()
|
||||
{
|
||||
var ret = [];
|
||||
for (var id in this._entities)
|
||||
@ -12,6 +17,19 @@ EntityCollection.prototype.ToIdArray = function()
|
||||
return ret;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toEntityArray = function()
|
||||
{
|
||||
var ret = [];
|
||||
for each (var ent in this._entities)
|
||||
ret.push(new Entity(this._ai, ent));
|
||||
return ret;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toString = function()
|
||||
{
|
||||
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
|
||||
};
|
||||
|
||||
EntityCollection.prototype.filter = function(callback, thisp)
|
||||
{
|
||||
var ret = {};
|
||||
@ -25,14 +43,25 @@ EntityCollection.prototype.filter = function(callback, thisp)
|
||||
return new EntityCollection(this._ai, ret);
|
||||
};
|
||||
|
||||
EntityCollection.prototype.forEach = function(callback, thisp)
|
||||
{
|
||||
for (var id in this._entities)
|
||||
{
|
||||
var ent = this._entities[id];
|
||||
var val = new Entity(this._ai, ent);
|
||||
callback.call(thisp, val, id, this);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.move = function(x, z)
|
||||
{
|
||||
Engine.PostCommand({"type": "walk", "entities": this.ToIdArray(), "x": x, "z": z, "queued": false});
|
||||
Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": false});
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.destroy = function()
|
||||
{
|
||||
Engine.PostCommand({"type": "delete-entities", "entities": this.ToIdArray()});
|
||||
Engine.PostCommand({"type": "delete-entities", "entities": this.toIdArray()});
|
||||
return this;
|
||||
};
|
||||
|
@ -0,0 +1,6 @@
|
||||
function VectorDistance(a, b)
|
||||
{
|
||||
var dx = a[0] - b[0];
|
||||
var dz = a[1] - b[1];
|
||||
return Math.sqrt(dx*dx + dz*dz);
|
||||
}
|
@ -21,7 +21,7 @@ ScaredyBotAI.prototype.OnUpdate = function()
|
||||
{
|
||||
this.chat("I quake in my boots! My troops cannot hope to survive against a power such as yours.");
|
||||
|
||||
this.entities.filter(function(ent) { return ent.isOwn; }).destroy();
|
||||
this.entities.filter(function(ent) { return ent.isOwn(); }).destroy();
|
||||
}
|
||||
|
||||
this.turn++;
|
||||
|
1
binaries/data/mods/public/simulation/ai/testbot/_init.js
Normal file
1
binaries/data/mods/public/simulation/ai/testbot/_init.js
Normal file
@ -0,0 +1 @@
|
||||
Engine.IncludeModule("common-api");
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Test Bot",
|
||||
"description": "A simple AI for testing the framework.",
|
||||
"constructor": "TestBotAI"
|
||||
}
|
69
binaries/data/mods/public/simulation/ai/testbot/economy.js
Normal file
69
binaries/data/mods/public/simulation/ai/testbot/economy.js
Normal file
@ -0,0 +1,69 @@
|
||||
var EconomyManager = Class({
|
||||
|
||||
_init: function()
|
||||
{
|
||||
this.targetNumWorkers = 10; // minimum number of workers we want
|
||||
},
|
||||
|
||||
update: function(gameState, planGroups)
|
||||
{
|
||||
// Count the workers in the world and in progress
|
||||
var numWorkers = gameState.countEntitiesAndQueuedWithRole("worker");
|
||||
|
||||
// If we have too few, train another
|
||||
// print("Want "+this.targetNumWorkers+" workers; got "+numWorkers+"\n");
|
||||
if (numWorkers < this.targetNumWorkers)
|
||||
{
|
||||
planGroups.economyPersonnel.addPlan(100,
|
||||
new UnitTrainingPlan(gameState,
|
||||
"units/hele_support_female_citizen", 1, { "role": "worker" })
|
||||
);
|
||||
}
|
||||
|
||||
// Search for idle workers, and tell them to gather resources
|
||||
// Maybe just pick a random nearby resource type at first;
|
||||
// later we could add some timer that redistributes workers based on
|
||||
// resource demand.
|
||||
|
||||
var idleWorkers = gameState.entities.filter(function(ent) {
|
||||
return (ent.getMetadata("role") === "worker" && ent.isIdle());
|
||||
});
|
||||
|
||||
if (idleWorkers.length)
|
||||
{
|
||||
var resourceSupplies = gameState.findResourceSupplies(gameState);
|
||||
|
||||
idleWorkers.forEach(function(ent) {
|
||||
// Pick a resource type at random
|
||||
// TODO: should limit to what this worker can gather
|
||||
var type = Resources.prototype.types[Math.floor(Math.random()*Resources.prototype.types.length)];
|
||||
|
||||
// Make sure there's actually some of that type
|
||||
// (We probably shouldn't pick impossible ones in the first place)
|
||||
if (!resourceSupplies[type])
|
||||
return;
|
||||
|
||||
// Pick the closest one.
|
||||
// TODO: we should care about distance to dropsites,
|
||||
// and gather rates of workers
|
||||
|
||||
var workerPosition = ent.position();
|
||||
var closestEntity = null;
|
||||
var closestDist = Infinity;
|
||||
resourceSupplies[type].forEach(function(supply) {
|
||||
var dist = VectorDistance(supply.position, workerPosition);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
closestEntity = supply.entity;
|
||||
}
|
||||
});
|
||||
|
||||
// Start gathering
|
||||
if (closestEntity)
|
||||
ent.gather(closestEntity);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
});
|
126
binaries/data/mods/public/simulation/ai/testbot/gamestate.js
Normal file
126
binaries/data/mods/public/simulation/ai/testbot/gamestate.js
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Provides an API for the rest of the AI scripts to query the world state
|
||||
* at a higher level than the raw data.
|
||||
*/
|
||||
var GameState = Class({
|
||||
|
||||
_init: function(timeElapsed, templates, entities, playerData)
|
||||
{
|
||||
this.timeElapsed = timeElapsed;
|
||||
this.templates = templates;
|
||||
this.entities = entities;
|
||||
this.playerData = playerData;
|
||||
},
|
||||
|
||||
getTimeElapsed: function()
|
||||
{
|
||||
return this.timeElapsed;
|
||||
},
|
||||
|
||||
getTemplate: function(type)
|
||||
{
|
||||
if (!this.templates[type])
|
||||
return null;
|
||||
return new EntityTemplate(this.templates[type]);
|
||||
},
|
||||
|
||||
getResources: function()
|
||||
{
|
||||
return new Resources(this.playerData.resourceCounts);
|
||||
},
|
||||
|
||||
getOwnEntities: function()
|
||||
{
|
||||
return this.entities.filter(function(ent) { return ent.isOwn(); });
|
||||
},
|
||||
|
||||
countEntitiesAndQueuedWithType: function(type)
|
||||
{
|
||||
var count = 0;
|
||||
this.getOwnEntities().forEach(function(ent) {
|
||||
|
||||
if (ent.templateName() == type)
|
||||
++count;
|
||||
|
||||
var queue = ent.trainingQueue();
|
||||
if (queue)
|
||||
{
|
||||
queue.forEach(function(item) {
|
||||
if (item.template == type)
|
||||
count += item.count;
|
||||
});
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
|
||||
countEntitiesAndQueuedWithRole: function(role)
|
||||
{
|
||||
var count = 0;
|
||||
this.getOwnEntities().forEach(function(ent) {
|
||||
|
||||
if (ent.getMetadata("role") == role)
|
||||
++count;
|
||||
|
||||
var queue = ent.trainingQueue();
|
||||
if (queue)
|
||||
{
|
||||
queue.forEach(function(item) {
|
||||
if (item.metadata && item.metadata.role == role)
|
||||
count += item.count;
|
||||
});
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find buildings that are capable of training the given unit type,
|
||||
* and aren't already too busy.
|
||||
*/
|
||||
findTrainers: function(template)
|
||||
{
|
||||
var maxQueueLength = 3; // avoid tying up resources in giant training queues
|
||||
|
||||
return this.getOwnEntities().filter(function(ent) {
|
||||
|
||||
var trainable = ent.trainableEntities();
|
||||
if (!trainable || trainable.indexOf(template) == -1)
|
||||
return false;
|
||||
|
||||
var queue = ent.trainingQueue();
|
||||
if (queue)
|
||||
{
|
||||
if (queue.length >= maxQueueLength)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
findResourceSupplies: function(gameState)
|
||||
{
|
||||
var supplies = {};
|
||||
this.entities.forEach(function(ent) {
|
||||
var type = ent.resourceSupplyType();
|
||||
if (!type)
|
||||
return;
|
||||
var amount = ent.resourceSupplyAmount();
|
||||
if (!amount)
|
||||
return;
|
||||
|
||||
if (!supplies[type.generic])
|
||||
supplies[type.generic] = [];
|
||||
|
||||
supplies[type.generic].push({
|
||||
"entity": ent,
|
||||
"amount": amount,
|
||||
"type": type,
|
||||
"position": ent.position(),
|
||||
});
|
||||
});
|
||||
return supplies;
|
||||
},
|
||||
|
||||
});
|
89
binaries/data/mods/public/simulation/ai/testbot/military.js
Normal file
89
binaries/data/mods/public/simulation/ai/testbot/military.js
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Military strategy:
|
||||
* * Try training an attack squad of a specified size
|
||||
* * When it's the appropriate size, send it to attack the enemy
|
||||
* * Repeat forever
|
||||
*
|
||||
*/
|
||||
|
||||
var MilitaryAttackManager = Class({
|
||||
|
||||
_init: function()
|
||||
{
|
||||
this.targetSquadSize = 10;
|
||||
this.squadTypes = [
|
||||
"units/hele_infantry_spearman_b",
|
||||
"units/hele_infantry_javelinist_b",
|
||||
"units/hele_infantry_archer_b",
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the unit type we should begin training.
|
||||
* (Currently this is whatever we have least of.)
|
||||
*/
|
||||
findBestNewUnit: function(gameState)
|
||||
{
|
||||
// Count each type
|
||||
var types = [];
|
||||
for each (var t in this.squadTypes)
|
||||
types.push([t, gameState.countEntitiesAndQueuedWithType(t)]);
|
||||
|
||||
// Sort by increasing count
|
||||
types.sort(function (a, b) { return a[1] - b[1]; });
|
||||
|
||||
return types[0][0];
|
||||
},
|
||||
|
||||
update: function(gameState, planGroups)
|
||||
{
|
||||
// Pause for a minute before starting any work, to give the economy a chance
|
||||
// to start up
|
||||
if (gameState.getTimeElapsed() < 60*1000)
|
||||
return;
|
||||
|
||||
// Continually try training new units, in batches of 5
|
||||
planGroups.militaryPersonnel.addPlan(100,
|
||||
new UnitTrainingPlan(gameState,
|
||||
this.findBestNewUnit(gameState), 5, { "role": "attack-pending" })
|
||||
);
|
||||
|
||||
// Find the units ready to join the attack
|
||||
var pending = gameState.entities.filter(function(ent) {
|
||||
return (ent.getMetadata("role") === "attack-pending");
|
||||
});
|
||||
|
||||
// If we have enough units yet, start the attack
|
||||
if (pending.length >= this.targetSquadSize)
|
||||
{
|
||||
// Find the enemy CCs we could attack
|
||||
var targets = gameState.entities.filter(function(ent) {
|
||||
return (ent.isEnemy() && ent.hasClass("CivCentre"));
|
||||
});
|
||||
|
||||
// If there's no CCs, attack anything else that's critical
|
||||
if (targets.length == 0)
|
||||
{
|
||||
targets = gameState.entities.filter(function(ent) {
|
||||
return (ent.isEnemy() && ent.hasClass("ConquestCritical"));
|
||||
});
|
||||
}
|
||||
|
||||
// If we have a target, move to it
|
||||
if (targets.length)
|
||||
{
|
||||
// Remove the pending role
|
||||
pending.forEach(function(ent) {
|
||||
ent.setMetadata("role", "attack");
|
||||
});
|
||||
|
||||
var target = targets.toEntityArray()[0];
|
||||
var targetPos = target.position();
|
||||
|
||||
// TODO: this should be an attack-move command
|
||||
pending.move(targetPos[0], targetPos[1]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
@ -0,0 +1,42 @@
|
||||
var UnitTrainingPlan = Class({
|
||||
|
||||
_init: function(gameState, type, amount, metadata)
|
||||
{
|
||||
this.type = type;
|
||||
this.amount = amount;
|
||||
this.metadata = metadata;
|
||||
|
||||
this.cost = new Resources(gameState.getTemplate(type).cost());
|
||||
this.cost.multiply(amount); // (assume no batch discount)
|
||||
},
|
||||
|
||||
canExecute: function(gameState)
|
||||
{
|
||||
// TODO: we should probably check pop caps
|
||||
|
||||
var trainers = gameState.findTrainers(this.type);
|
||||
|
||||
return (trainers.length != 0);
|
||||
},
|
||||
|
||||
execute: function(gameState)
|
||||
{
|
||||
// warn("Executing UnitTrainingPlan "+uneval(this));
|
||||
|
||||
var trainers = gameState.findTrainers(this.type).toEntityArray();
|
||||
|
||||
// Prefer training buildings with short queues
|
||||
// (TODO: this should also account for units added to the queue by
|
||||
// plans that have already been executed this turn)
|
||||
trainers.sort(function(a, b) {
|
||||
return a.trainingQueueTime() - b.trainingQueueTime();
|
||||
});
|
||||
|
||||
trainers[0].train(this.type, this.amount, this.metadata);
|
||||
},
|
||||
|
||||
getCost: function()
|
||||
{
|
||||
return this.cost;
|
||||
},
|
||||
});
|
67
binaries/data/mods/public/simulation/ai/testbot/plan.js
Normal file
67
binaries/data/mods/public/simulation/ai/testbot/plan.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* All plan classes must implement this interface.
|
||||
*/
|
||||
var IPlan = Class({
|
||||
|
||||
_init: function() { /* ... */ },
|
||||
|
||||
canExecute: function(gameState) { /* ... */ },
|
||||
|
||||
execute: function(gameState) { /* ... */ },
|
||||
|
||||
getCost: function() { /* ... */ },
|
||||
});
|
||||
|
||||
/**
|
||||
* Represents a prioritised collection of plans.
|
||||
*/
|
||||
var PlanGroup = Class({
|
||||
|
||||
_init: function()
|
||||
{
|
||||
this.escrow = new Resources({});
|
||||
this.plans = [];
|
||||
},
|
||||
|
||||
addPlan: function(priority, plan)
|
||||
{
|
||||
this.plans.push({"priority": priority, "plan": plan});
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes all plans that we can afford, ordered by priority,
|
||||
* and returns the highest-priority plan we couldn't afford (or null
|
||||
* if none).
|
||||
*/
|
||||
executePlans: function(gameState)
|
||||
{
|
||||
// Ignore impossible plans
|
||||
var plans = this.plans.filter(function(p) { return p.plan.canExecute(gameState); });
|
||||
|
||||
// Sort by decreasing priority
|
||||
plans.sort(function(a, b) { return a.priority > b.priority; });
|
||||
|
||||
// Execute as many plans as we can afford
|
||||
while (plans.length && this.escrow.canAfford(plans[0].plan.getCost()))
|
||||
{
|
||||
var plan = plans.shift().plan;
|
||||
this.escrow.subtract(plan.getCost());
|
||||
plan.execute(gameState);
|
||||
}
|
||||
|
||||
if (plans.length)
|
||||
return plans[0];
|
||||
else
|
||||
return null;
|
||||
},
|
||||
|
||||
resetPlans: function()
|
||||
{
|
||||
this.plans = [];
|
||||
},
|
||||
|
||||
getEscrow: function()
|
||||
{
|
||||
return this.escrow;
|
||||
},
|
||||
});
|
36
binaries/data/mods/public/simulation/ai/testbot/resources.js
Normal file
36
binaries/data/mods/public/simulation/ai/testbot/resources.js
Normal file
@ -0,0 +1,36 @@
|
||||
var Resources = Class({
|
||||
|
||||
types: ["food", "wood", "stone", "metal"],
|
||||
|
||||
_init: function(amounts)
|
||||
{
|
||||
for each (var t in this.types)
|
||||
this[t] = amounts[t] || 0;
|
||||
},
|
||||
|
||||
canAfford: function(that)
|
||||
{
|
||||
for each (var t in this.types)
|
||||
if (this[t] < that[t])
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
add: function(that)
|
||||
{
|
||||
for each (var t in this.types)
|
||||
this[t] += that[t];
|
||||
},
|
||||
|
||||
subtract: function(that)
|
||||
{
|
||||
for each (var t in this.types)
|
||||
this[t] -= that[t];
|
||||
},
|
||||
|
||||
multiply: function(n)
|
||||
{
|
||||
for each (var t in this.types)
|
||||
this[t] *= n;
|
||||
},
|
||||
});
|
138
binaries/data/mods/public/simulation/ai/testbot/testbot.js
Normal file
138
binaries/data/mods/public/simulation/ai/testbot/testbot.js
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* This is a primitive initial attempt at an AI player.
|
||||
* The design isn't great and maybe the whole thing should be rewritten -
|
||||
* the aim here is just to have something that basically works, and to
|
||||
* learn more about what's really needed for a decent AI design.
|
||||
*
|
||||
* The basic idea is we have a collection of independent modules
|
||||
* (EconomyManager, etc) which produce a list of plans.
|
||||
* The modules are mostly stateless - each turn they look at the current
|
||||
* world state, and produce some plans that will improve the state.
|
||||
* E.g. if there's too few worker units, they'll do a plan to train
|
||||
* another one. Plans are discarded after the current turn, if they
|
||||
* haven't been executed.
|
||||
*
|
||||
* Plans are grouped into a small number of PlanGroups, and for each
|
||||
* group we try to execute the highest-priority plans.
|
||||
* If some plan groups need more resources to execute their highest-priority
|
||||
* plan, we'll distribute any unallocated resources to that group's
|
||||
* escrow account. Eventually they'll accumulate enough to afford their plan.
|
||||
* (The purpose is to ensure resources are shared fairly between all the
|
||||
* plan groups - none of them should be starved even if they're trying to
|
||||
* execute a really expensive plan.)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Lots of things we should fix:
|
||||
*
|
||||
* * Construct buildings (houses, farms, barracks)
|
||||
* * Play as non-hele civs
|
||||
* * Find entities with no assigned role, and give them something to do
|
||||
* * Keep some units back for defence
|
||||
* * Consistent terminology (type vs template etc)
|
||||
* * ...
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
function TestBotAI(settings)
|
||||
{
|
||||
warn("Constructing TestBotAI for player "+settings.player);
|
||||
|
||||
BaseAI.call(this, settings);
|
||||
|
||||
this.turn = 0;
|
||||
|
||||
this.modules = [
|
||||
new EconomyManager(),
|
||||
new MilitaryAttackManager(),
|
||||
];
|
||||
|
||||
this.planGroups = {
|
||||
economyPersonnel: new PlanGroup(),
|
||||
militaryPersonnel: new PlanGroup(),
|
||||
};
|
||||
}
|
||||
|
||||
TestBotAI.prototype = new BaseAI();
|
||||
|
||||
TestBotAI.prototype.ShareResources = function(remainingResources, unaffordablePlans)
|
||||
{
|
||||
// Share our remaining resources among the plangroups that need
|
||||
// to accumulate more resources, in proportion to their priorities
|
||||
|
||||
for each (var type in remainingResources.types)
|
||||
{
|
||||
// Skip resource types where we don't have any spare
|
||||
if (remainingResources[type] <= 0)
|
||||
continue;
|
||||
|
||||
// Find the plans that require some of this resource type,
|
||||
// and the sum of their priorities
|
||||
var ps = [];
|
||||
var sumPriority = 0;
|
||||
for each (var p in unaffordablePlans)
|
||||
{
|
||||
if (p.plan.getCost()[type] > p.group.getEscrow()[type])
|
||||
{
|
||||
ps.push(p);
|
||||
sumPriority += p.priority;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid divisions-by-zero
|
||||
if (!sumPriority)
|
||||
continue;
|
||||
|
||||
// Share resources by priority, clamped to the amount the plan actually needs
|
||||
for each (var p in ps)
|
||||
{
|
||||
var amount = Math.floor(remainingResources[type] * p.priority / sumPriority);
|
||||
var max = p.plan.getCost()[type] - p.group.getEscrow()[type];
|
||||
p.group.getEscrow()[type] += Math.min(max, amount);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TestBotAI.prototype.OnUpdate = function()
|
||||
{
|
||||
// Run the update every n turns, offset depending on player ID to balance the load
|
||||
if ((this.turn + this.player) % 4 == 0)
|
||||
{
|
||||
var gameState = new GameState(this.timeElapsed, this.templates, this.entities, this.playerData);
|
||||
|
||||
// Find the resources we have this turn that haven't already
|
||||
// been allocated to an escrow account.
|
||||
// (We need to do this before executing any plans, because those will
|
||||
// distort the escrow figures.)
|
||||
var remainingResources = gameState.getResources();
|
||||
for each (var planGroup in this.planGroups)
|
||||
remainingResources.subtract(planGroup.getEscrow());
|
||||
|
||||
// Compute plans from each module
|
||||
for each (var module in this.modules)
|
||||
module.update(gameState, this.planGroups);
|
||||
|
||||
// print(uneval(this.planGroups)+"\n");
|
||||
|
||||
// Execute as many plans as possible, and keep a record of
|
||||
// which ones we can't afford yet
|
||||
var unaffordablePlans = [];
|
||||
for each (var planGroup in this.planGroups)
|
||||
{
|
||||
var plan = planGroup.executePlans(gameState);
|
||||
if (plan)
|
||||
unaffordablePlans.push({"group": planGroup, "priority": plan.priority, "plan": plan.plan});
|
||||
}
|
||||
|
||||
this.ShareResources(remainingResources, unaffordablePlans);
|
||||
|
||||
// print(uneval(this.planGroups)+"\n");
|
||||
|
||||
// Reset the temporary plan data
|
||||
for each (var planGroup in this.planGroups)
|
||||
planGroup.resetPlans();
|
||||
}
|
||||
|
||||
this.turn++;
|
||||
};
|
@ -33,35 +33,13 @@ AIInterface.prototype.GetRepresentation = function()
|
||||
return state;
|
||||
};
|
||||
|
||||
// Set up a load of event handlers to capture interesting things going on
|
||||
// in the world, which we will report to AI:
|
||||
// (This shouldn't include extremely high-frequency events, like PositionChanged,
|
||||
// because that would be very expensive and AI will rarely care about all those
|
||||
// events.)
|
||||
// AIProxy sets up a load of event handlers to capture interesting things going on
|
||||
// in the world, which we will report to AI. Handle those, and add a few more handlers
|
||||
// for events that AIProxy won't capture.
|
||||
|
||||
AIInterface.prototype.OnGlobalCreate = function(msg)
|
||||
AIInterface.prototype.PushEvent = function(type, msg)
|
||||
{
|
||||
this.events.push({"type": "Create", "msg": msg});
|
||||
};
|
||||
|
||||
AIInterface.prototype.OnGlobalDestroy = function(msg)
|
||||
{
|
||||
this.events.push({"type": "Destroy", "msg": msg});
|
||||
};
|
||||
|
||||
AIInterface.prototype.OnGlobalOwnershipChanged = function(msg)
|
||||
{
|
||||
this.events.push({"type": "OwnershipChanged", "msg": msg});
|
||||
};
|
||||
|
||||
AIInterface.prototype.OnGlobalAttacked = function(msg)
|
||||
{
|
||||
this.events.push({"type": "Attacked", "msg": msg});
|
||||
};
|
||||
|
||||
AIInterface.prototype.OnGlobalConstructionFinished = function(msg)
|
||||
{
|
||||
this.events.push({"type": "ConstructionFinished", "msg": msg});
|
||||
this.events.push({"type": type, "msg": msg});
|
||||
};
|
||||
|
||||
AIInterface.prototype.OnGlobalPlayerDefeated = function(msg)
|
||||
|
@ -58,6 +58,8 @@ AIProxy.prototype.GetRepresentation = function()
|
||||
return ret;
|
||||
};
|
||||
|
||||
// AI representation-updating event handlers:
|
||||
|
||||
AIProxy.prototype.OnPositionChanged = function(msg)
|
||||
{
|
||||
if (!this.changes)
|
||||
@ -85,6 +87,23 @@ AIProxy.prototype.OnOwnershipChanged = function(msg)
|
||||
this.changes.owner = msg.to;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnUnitIdleChanged = function(msg)
|
||||
{
|
||||
if (!this.changes)
|
||||
this.changes = {};
|
||||
|
||||
this.changes.idle = msg.idle;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnTrainingQueueChanged = function(msg)
|
||||
{
|
||||
if (!this.changes)
|
||||
this.changes = {};
|
||||
|
||||
var cmpTrainingQueue = Engine.QueryInterface(this.entity, IID_TrainingQueue);
|
||||
this.changes.trainingQueue = cmpTrainingQueue.GetQueue();
|
||||
}
|
||||
|
||||
// TODO: event handlers for all the other things
|
||||
|
||||
AIProxy.prototype.GetFullRepresentation = function()
|
||||
@ -127,9 +146,17 @@ AIProxy.prototype.GetFullRepresentation = function()
|
||||
ret.owner = cmpOwnership.GetOwner();
|
||||
}
|
||||
|
||||
var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
|
||||
if (cmpUnitAI)
|
||||
{
|
||||
// Updated by OnUnitIdleChanged
|
||||
ret.idle = cmpUnitAI.IsIdle();
|
||||
}
|
||||
|
||||
var cmpTrainingQueue = Engine.QueryInterface(this.entity, IID_TrainingQueue);
|
||||
if (cmpTrainingQueue)
|
||||
{
|
||||
// Updated by OnTrainingQueueChanged
|
||||
ret.trainingQueue = cmpTrainingQueue.GetQueue();
|
||||
}
|
||||
|
||||
@ -160,4 +187,41 @@ AIProxy.prototype.GetFullRepresentation = function()
|
||||
return ret;
|
||||
};
|
||||
|
||||
// AI event handlers:
|
||||
// (These are passed directly as events to the AI scripts, rather than updating
|
||||
// our proxy representation.)
|
||||
// (This shouldn't include extremely high-frequency events, like PositionChanged,
|
||||
// because that would be very expensive and AI will rarely care about all those
|
||||
// events.)
|
||||
|
||||
AIProxy.prototype.OnCreate = function(msg)
|
||||
{
|
||||
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
|
||||
cmpAIInterface.PushEvent("Create", msg);
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnDestroy = function(msg)
|
||||
{
|
||||
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
|
||||
cmpAIInterface.PushEvent("Destroy", msg);
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnAttacked = function(msg)
|
||||
{
|
||||
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
|
||||
cmpAIInterface.PushEvent("Attacked", msg);
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnConstructionFinished = function(msg)
|
||||
{
|
||||
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
|
||||
cmpAIInterface.PushEvent("ConstructionFinished", msg);
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnTrainingFinished = function(msg)
|
||||
{
|
||||
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
|
||||
cmpAIInterface.PushEvent("TrainingFinished", msg);
|
||||
};
|
||||
|
||||
Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy);
|
||||
|
@ -50,7 +50,7 @@ TrainingQueue.prototype.GetEntitiesList = function()
|
||||
return string.split(/\s+/);
|
||||
};
|
||||
|
||||
TrainingQueue.prototype.AddBatch = function(templateName, count)
|
||||
TrainingQueue.prototype.AddBatch = function(templateName, count, metadata)
|
||||
{
|
||||
// TODO: there should probably be a limit on the number of queued batches
|
||||
// TODO: there should be a way for the GUI to determine whether it's going
|
||||
@ -88,12 +88,14 @@ TrainingQueue.prototype.AddBatch = function(templateName, count)
|
||||
"player": cmpPlayer.GetPlayerID(),
|
||||
"template": templateName,
|
||||
"count": count,
|
||||
"metadata": metadata,
|
||||
"resources": costs,
|
||||
"population": population,
|
||||
"trainingStarted": false,
|
||||
"timeTotal": time*1000,
|
||||
"timeRemaining": time*1000,
|
||||
});
|
||||
Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { });
|
||||
|
||||
// If this is the first item in the queue, start the timer
|
||||
if (!this.timer)
|
||||
@ -132,6 +134,8 @@ TrainingQueue.prototype.RemoveBatch = function(id)
|
||||
// Remove from the queue
|
||||
// (We don't need to remove the timer - it'll expire if it discovers the queue is empty)
|
||||
this.queue.splice(i, 1);
|
||||
Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { });
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -146,6 +150,7 @@ TrainingQueue.prototype.GetQueue = function()
|
||||
"template": item.template,
|
||||
"count": item.count,
|
||||
"progress": 1-(item.timeRemaining/item.timeTotal),
|
||||
"metadata": item.metadata,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
@ -192,7 +197,7 @@ TrainingQueue.prototype.OnDestroy = function()
|
||||
};
|
||||
|
||||
|
||||
TrainingQueue.prototype.SpawnUnits = function(templateName, count)
|
||||
TrainingQueue.prototype.SpawnUnits = function(templateName, count, metadata)
|
||||
{
|
||||
var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
|
||||
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
@ -247,6 +252,8 @@ TrainingQueue.prototype.SpawnUnits = function(templateName, count)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": ents, "metadata": metadata });
|
||||
};
|
||||
|
||||
TrainingQueue.prototype.ProgressTimeout = function(data)
|
||||
@ -291,8 +298,9 @@ TrainingQueue.prototype.ProgressTimeout = function(data)
|
||||
// This item is finished now
|
||||
time -= item.timeRemaining;
|
||||
cmpPlayer.UnReservePopulationSlots(item.population);
|
||||
this.SpawnUnits(item.template, item.count);
|
||||
this.SpawnUnits(item.template, item.count, item.metadata);
|
||||
this.queue.shift();
|
||||
Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { });
|
||||
}
|
||||
|
||||
// If the queue's empty, delete the timer, else repeat it
|
||||
|
@ -310,6 +310,13 @@ var UnitFsmSpec = {
|
||||
// get stuck with an incorrect animation
|
||||
this.SelectAnimation("idle");
|
||||
|
||||
// The GUI and AI want to know when a unit is idle, but we don't
|
||||
// want to send frequent spurious messages if the unit's only
|
||||
// idle for an instant and will quickly go off and do something else.
|
||||
// So we'll set a timer here and only report the idle event if we
|
||||
// remain idle
|
||||
this.StartTimer(1000);
|
||||
|
||||
// If we entered the idle state we must have nothing better to do,
|
||||
// so immediately check whether there's anybody nearby to attack.
|
||||
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
|
||||
@ -328,6 +335,14 @@ var UnitFsmSpec = {
|
||||
"leave": function() {
|
||||
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
rangeMan.DisableActiveQuery(this.losRangeQuery);
|
||||
|
||||
this.StopTimer();
|
||||
|
||||
if (this.isIdle)
|
||||
{
|
||||
this.isIdle = false;
|
||||
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
|
||||
}
|
||||
},
|
||||
|
||||
"LosRangeUpdate": function(msg) {
|
||||
@ -337,6 +352,14 @@ var UnitFsmSpec = {
|
||||
this.AttackVisibleEntity(msg.data.added);
|
||||
}
|
||||
},
|
||||
|
||||
"Timer": function(msg) {
|
||||
if (!this.isIdle)
|
||||
{
|
||||
this.isIdle = true;
|
||||
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"WALKING": {
|
||||
@ -774,6 +797,7 @@ UnitAI.prototype.Init = function()
|
||||
this.orderQueue = []; // current order is at the front of the list
|
||||
this.order = undefined; // always == this.orderQueue[0]
|
||||
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
|
||||
this.isIdle = false;
|
||||
|
||||
this.SetStance("aggressive");
|
||||
};
|
||||
@ -783,6 +807,11 @@ UnitAI.prototype.IsFormationController = function()
|
||||
return (this.template.FormationController == "true");
|
||||
};
|
||||
|
||||
UnitAI.prototype.IsIdle = function()
|
||||
{
|
||||
return this.isIdle;
|
||||
};
|
||||
|
||||
UnitAI.prototype.OnCreate = function()
|
||||
{
|
||||
if (this.IsFormationController())
|
||||
@ -925,8 +954,15 @@ UnitAI.prototype.ReplaceOrder = function(type, data)
|
||||
UnitAI.prototype.TimerHandler = function(data, lateness)
|
||||
{
|
||||
// Reset the timer
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data);
|
||||
if (data.timerRepeat === undefined)
|
||||
{
|
||||
this.timer = undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data);
|
||||
}
|
||||
|
||||
UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
|
||||
};
|
||||
|
@ -1 +1,9 @@
|
||||
Engine.RegisterInterface("TrainingQueue");
|
||||
|
||||
// Message of the form { } (use GetQueue if you want the current details),
|
||||
// sent to the current entity whenever the training queue changes.
|
||||
Engine.RegisterMessageType("TrainingQueueChanged");
|
||||
|
||||
// Message of the form { entities: [id, ...], metadata: ... }
|
||||
// sent to the current entity whenever a unit has been trained.
|
||||
Engine.RegisterMessageType("TrainingFinished");
|
||||
|
@ -1 +1,5 @@
|
||||
Engine.RegisterInterface("UnitAI");
|
||||
|
||||
// Message of the form { "idle": true },
|
||||
// sent whenever the unit's idle status changes.
|
||||
Engine.RegisterMessageType("UnitIdleChanged");
|
||||
|
@ -55,7 +55,7 @@ function ProcessCommand(player, cmd)
|
||||
case "train":
|
||||
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
|
||||
if (queue)
|
||||
queue.AddBatch(cmd.template, +cmd.count);
|
||||
queue.AddBatch(cmd.template, +cmd.count, cmd.metadata);
|
||||
break;
|
||||
|
||||
case "stop-train":
|
||||
|
@ -488,6 +488,7 @@ void CCmpTemplateManager::CopyFoundationSubset(CParamNode& out, const CParamNode
|
||||
permittedComponentTypes.insert("Cost");
|
||||
permittedComponentTypes.insert("Sound");
|
||||
permittedComponentTypes.insert("Vision");
|
||||
permittedComponentTypes.insert("AIProxy");
|
||||
|
||||
CParamNode::LoadXMLString(out, "<Entity/>");
|
||||
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
|
||||
|
Loading…
Reference in New Issue
Block a user