diff --git a/binaries/data/mods/_test.sim/simulation/templates/unitobstruct.xml b/binaries/data/mods/_test.sim/simulation/templates/unitobstruct.xml
index 307a00620f..2ff4c456f0 100644
--- a/binaries/data/mods/_test.sim/simulation/templates/unitobstruct.xml
+++ b/binaries/data/mods/_test.sim/simulation/templates/unitobstruct.xml
@@ -16,5 +16,10 @@
+ true
+ true
+ false
+ false
+ true
diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js
index 4b067d87b1..21579bc75f 100644
--- a/binaries/data/mods/public/gui/session/input.js
+++ b/binaries/data/mods/public/gui/session/input.js
@@ -313,6 +313,8 @@ function tryPlaceBuilding(queued)
"z": placementPosition.z,
"angle": placementAngle,
"entities": selection,
+ "autorepair": true,
+ "autocontinue": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
diff --git a/binaries/data/mods/public/simulation/ai/common-api/base.js b/binaries/data/mods/public/simulation/ai/common-api/base.js
index 8ca1f0f1f4..208e30218c 100644
--- a/binaries/data/mods/public/simulation/ai/common-api/base.js
+++ b/binaries/data/mods/public/simulation/ai/common-api/base.js
@@ -6,10 +6,48 @@ 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});
+ Object.defineProperty(this, "_derivedTemplates", {value: {}, enumerable: false});
this._entityMetadata = {};
}
+// 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._rawEntities)
@@ -22,6 +60,8 @@ BaseAI.prototype.HandleMessage = function(state)
this.playerData = state.players[this._player];
this.templates = this._templates;
this.timeElapsed = state.timeElapsed;
+ this.map = state.map;
+ this.passabilityClasses = state.passabilityClasses;
this.OnUpdate();
@@ -31,6 +71,8 @@ BaseAI.prototype.HandleMessage = function(state)
delete this.playerData;
delete this.templates;
delete this.timeElapsed;
+ delete this.map;
+ delete this.passabilityClasses;
};
BaseAI.prototype.ApplyEntitiesDelta = function(state)
@@ -48,10 +90,11 @@ BaseAI.prototype.ApplyEntitiesDelta = function(state)
}
else if (evt.type == "TrainingFinished")
{
- for each (var ent in evt.msg.entities)
- {
- this._entityMetadata[ent] = evt.msg.metadata;
- }
+ // Apply metadata stored in training queues, but only if they
+ // look like they were added by us
+ if (evt.msg.owner == this._player)
+ for each (var ent in evt.msg.entities)
+ this._entityMetadata[ent] = evt.msg.metadata;
}
}
diff --git a/binaries/data/mods/public/simulation/ai/common-api/entity.js b/binaries/data/mods/public/simulation/ai/common-api/entity.js
index f7918b0b6d..301e164e99 100644
--- a/binaries/data/mods/public/simulation/ai/common-api/entity.js
+++ b/binaries/data/mods/public/simulation/ai/common-api/entity.js
@@ -38,6 +38,26 @@ var EntityTemplate = Class({
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"; },
@@ -103,12 +123,13 @@ var EntityTemplate = Class({
});
+
var Entity = Class({
_super: EntityTemplate,
_init: function(baseAI, entity)
{
- this._super.call(this, baseAI._templates[entity.template]);
+ this._super.call(this, baseAI.GetTemplate(entity.template));
this._ai = baseAI;
this._templateName = entity.template;
@@ -134,7 +155,7 @@ var Entity = Class({
*/
getMetadata: function(id) {
var metadata = this._ai._entityMetadata[this.id()];
- if (!metadata)
+ if (!metadata || !(id in metadata))
return undefined;
return metadata[id];
},
@@ -179,7 +200,11 @@ var Entity = Class({
return queue.length;
},
- foundationProgress: function() { return this._entity.foundationProgress; },
+ foundationProgress: function() {
+ if (typeof this._entity.foundationProgress === "undefined")
+ return undefined;
+ return this._entity.foundationProgress;
+ },
owner: function() {
return this._entity.owner;
@@ -216,6 +241,11 @@ var Entity = Class({
return this;
},
+ repair: function(target) {
+ Engine.PostCommand({"type": "repair", "entities": [this.id()], "target": target.id(), "queued": false});
+ return this;
+ },
+
destroy: function() {
Engine.PostCommand({"type": "delete-entities", "entities": [this.id()]});
return this;
@@ -244,5 +274,23 @@ var Entity = Class({
});
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;
+ },
});
diff --git a/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js b/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
index 9d38437aa0..7848b3472b 100644
--- a/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
+++ b/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
@@ -30,6 +30,31 @@ EntityCollection.prototype.toString = function()
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
};
+/**
+ * Returns the (at most) n entities nearest to targetPos.
+ */
+EntityCollection.prototype.filterNearest = function(targetPos, n)
+{
+ // Compute the distance of each entity
+ var data = []; // [ [id, ent, distance], ... ]
+ for (var id in this._entities)
+ {
+ var ent = this._entities[id];
+ if (ent.position)
+ data.push([id, ent, VectorDistance(targetPos, ent.position)]);
+ }
+
+ // Sort by increasing distance
+ data.sort(function (a, b) { return (a[2] - b[2]); });
+
+ // Extract the first n
+ var ret = {};
+ for each (var val in data.slice(0, n))
+ ret[val[0]] = val[1];
+
+ return new EntityCollection(this._ai, ret);
+};
+
EntityCollection.prototype.filter = function(callback, thisp)
{
var ret = {};
diff --git a/binaries/data/mods/public/simulation/ai/testbot/economy.js b/binaries/data/mods/public/simulation/ai/testbot/economy.js
index 5693fa0f43..1a74503969 100644
--- a/binaries/data/mods/public/simulation/ai/testbot/economy.js
+++ b/binaries/data/mods/public/simulation/ai/testbot/economy.js
@@ -2,10 +2,64 @@ var EconomyManager = Class({
_init: function()
{
- this.targetNumWorkers = 10; // minimum number of workers we want
+ this.targetNumWorkers = 30; // minimum number of workers we want
+ this.targetNumBuilders = 5; // number of workers we want working on construction
+
+ // (This is a stupid design where we just construct certain numbers
+ // of certain buildings in sequence)
+ this.targetBuildings = [
+ {
+ "template": "structures/{civ}_civil_centre",
+ "priority": 500,
+ "count": 1,
+ },
+ {
+ "template": "structures/{civ}_house",
+ "priority": 100,
+ "count": 5,
+ },
+ {
+ "template": "structures/{civ}_barracks",
+ "priority": 75,
+ "count": 1,
+ },
+ {
+ "template": "structures/{civ}_field",
+ "priority": 50,
+ "count": 5,
+ },
+ ];
+
+ // Relative proportions of workers to assign to each resource type
+ this.gatherWeights = {
+ "food": 150,
+ "wood": 100,
+ "stone": 50,
+ "metal": 100,
+ };
},
- update: function(gameState, planGroups)
+ buildMoreBuildings: function(gameState, planGroups)
+ {
+ // Limit ourselves to constructing one building at a time
+ if (gameState.findFoundations().length)
+ return;
+
+ for each (var building in this.targetBuildings)
+ {
+ var numBuildings = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(building.template));
+
+ // If we have too few, build another
+ if (numBuildings < building.count)
+ {
+ planGroups.economyConstruction.addPlan(building.priority,
+ new BuildingConstructionPlan(gameState, building.template)
+ );
+ }
+ }
+ },
+
+ trainMoreWorkers: function(gameState, planGroups)
{
// Count the workers in the world and in progress
var numWorkers = gameState.countEntitiesAndQueuedWithRole("worker");
@@ -16,27 +70,58 @@ var EconomyManager = Class({
{
planGroups.economyPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
- "units/hele_support_female_citizen", 1, { "role": "worker" })
+ "units/{civ}_support_female_citizen", 1, { "role": "worker" })
);
}
+ },
+
+ pickMostNeededResource: function(gameState)
+ {
+ // Find what resource type we're most in need of
+ var numGatherers = {};
+ for (var type in this.gatherWeights)
+ numGatherers[type] = 0;
+
+ gameState.getOwnEntities().forEach(function(ent) {
+ if (ent.getMetadata("role") === "worker" && ent.getMetadata("subrole") === "gatherer")
+ numGatherers[ent.getMetadata("gather-type")] += 1;
+ });
+
+ var bestType = "food";
+ var bestTypeVal = Infinity; // num gatherers divided by weight
+ for (var type in this.gatherWeights)
+ {
+ var v = numGatherers[type] / this.gatherWeights[type];
+ if (v < bestTypeVal)
+ {
+ bestTypeVal = v;
+ bestType = type;
+ }
+ }
+
+ return bestType;
+ },
+
+ reassignIdleWorkers: function(gameState, planGroups)
+ {
+ var self = this;
// Search for idle workers, and tell them to gather resources
// Maybe just pick a random nearby resource type at first;
// later we could add some timer that redistributes workers based on
// resource demand.
- var idleWorkers = gameState.entities.filter(function(ent) {
+ var idleWorkers = gameState.getOwnEntities().filter(function(ent) {
return (ent.getMetadata("role") === "worker" && ent.isIdle());
});
if (idleWorkers.length)
{
- var resourceSupplies = gameState.findResourceSupplies(gameState);
+ var resourceSupplies = gameState.findResourceSupplies();
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)];
+
+ var type = self.pickMostNeededResource(gameState);
// Make sure there's actually some of that type
// (We probably shouldn't pick impossible ones in the first place)
@@ -61,9 +146,66 @@ var EconomyManager = Class({
// Start gathering
if (closestEntity)
+ {
ent.gather(closestEntity);
+ ent.setMetadata("subrole", "gatherer");
+ ent.setMetadata("gather-type", type);
+ }
});
}
},
+ assignToFoundations: function(gameState, planGroups)
+ {
+ // If we have some foundations, and we don't have enough builder-workers,
+ // try reassigning some other workers who are nearby
+
+ var foundations = gameState.findFoundations();
+
+ // Check if nothing to build
+ if (!foundations.length)
+ return;
+
+ var workers = gameState.getOwnEntities().filter(function(ent) {
+ return (ent.getMetadata("role") === "worker");
+ });
+
+ var builderWorkers = workers.filter(function(ent) {
+ return (ent.getMetadata("subrole") === "builder");
+ });
+
+ // Check if enough builders
+ var extraNeeded = this.targetNumBuilders - builderWorkers.length;
+ if (extraNeeded <= 0)
+ return;
+
+ // Pick non-builders who are closest to the first foundation,
+ // and tell them to start building it
+
+ var target = foundations.toEntityArray()[0];
+
+ var nonBuilderWorkers = workers.filter(function(ent) {
+ return (ent.getMetadata("subrole") !== "builder");
+ });
+
+ var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), extraNeeded);
+
+ // Order each builder individually, not as a formation
+ nearestNonBuilders.forEach(function(ent) {
+ ent.repair(target);
+ ent.setMetadata("subrole", "builder");
+ });
+ },
+
+ update: function(gameState, planGroups)
+ {
+ this.buildMoreBuildings(gameState, planGroups);
+
+ this.trainMoreWorkers(gameState, planGroups);
+
+ this.reassignIdleWorkers(gameState, planGroups);
+
+ this.assignToFoundations(gameState, planGroups);
+ },
+
});
diff --git a/binaries/data/mods/public/simulation/ai/testbot/gamestate.js b/binaries/data/mods/public/simulation/ai/testbot/gamestate.js
index a119f4df0b..a36af85119 100644
--- a/binaries/data/mods/public/simulation/ai/testbot/gamestate.js
+++ b/binaries/data/mods/public/simulation/ai/testbot/gamestate.js
@@ -4,12 +4,13 @@
*/
var GameState = Class({
- _init: function(timeElapsed, templates, entities, playerData)
+ _init: function(ai)
{
- this.timeElapsed = timeElapsed;
- this.templates = templates;
- this.entities = entities;
- this.playerData = playerData;
+ this.ai = ai;
+ this.timeElapsed = ai.timeElapsed;
+ this.templates = ai.templates;
+ this.entities = ai.entities;
+ this.playerData = ai.playerData;
},
getTimeElapsed: function()
@@ -24,22 +25,52 @@ var GameState = Class({
return new EntityTemplate(this.templates[type]);
},
+ applyCiv: function(str)
+ {
+ return str.replace(/\{civ\}/g, this.playerData.civ);
+ },
+
getResources: function()
{
return new Resources(this.playerData.resourceCounts);
},
+ getMap: function()
+ {
+ return this.ai.map;
+ },
+
+ getPassabilityClassMask: function(name)
+ {
+ if (!(name in this.ai.passabilityClasses))
+ error("Tried to use invalid passability class name '"+name+"'");
+ return this.ai.passabilityClasses[name];
+ },
+
getOwnEntities: function()
{
return this.entities.filter(function(ent) { return ent.isOwn(); });
},
- countEntitiesAndQueuedWithType: function(type)
+ countEntitiesWithType: function(type)
{
var count = 0;
this.getOwnEntities().forEach(function(ent) {
+ var t = ent.templateName();
+ if (t == type)
+ ++count;
+ });
+ return count;
+ },
- if (ent.templateName() == type)
+ countEntitiesAndQueuedWithType: function(type)
+ {
+ var foundationType = "foundation|" + type;
+ var count = 0;
+ this.getOwnEntities().forEach(function(ent) {
+
+ var t = ent.templateName();
+ if (t == type || t == foundationType)
++count;
var queue = ent.trainingQueue();
@@ -99,7 +130,29 @@ var GameState = Class({
});
},
- findResourceSupplies: function(gameState)
+ /**
+ * Find units that are capable of constructing the given building type.
+ */
+ findBuilders: function(template)
+ {
+ return this.getOwnEntities().filter(function(ent) {
+
+ var buildable = ent.buildableEntities();
+ if (!buildable || buildable.indexOf(template) == -1)
+ return false;
+
+ return true;
+ });
+ },
+
+ findFoundations: function(template)
+ {
+ return this.getOwnEntities().filter(function(ent) {
+ return (typeof ent.foundationProgress() !== "undefined");
+ });
+ },
+
+ findResourceSupplies: function()
{
var supplies = {};
this.entities.forEach(function(ent) {
@@ -122,5 +175,4 @@ var GameState = Class({
});
return supplies;
},
-
});
diff --git a/binaries/data/mods/public/simulation/ai/testbot/military.js b/binaries/data/mods/public/simulation/ai/testbot/military.js
index 222de08084..d9f4c927db 100644
--- a/binaries/data/mods/public/simulation/ai/testbot/military.js
+++ b/binaries/data/mods/public/simulation/ai/testbot/military.js
@@ -12,9 +12,9 @@ var MilitaryAttackManager = Class({
{
this.targetSquadSize = 10;
this.squadTypes = [
- "units/hele_infantry_spearman_b",
- "units/hele_infantry_javelinist_b",
- "units/hele_infantry_archer_b",
+ "units/{civ}_infantry_spearman_b",
+ "units/{civ}_infantry_javelinist_b",
+// "units/{civ}_infantry_archer_b", // TODO: should only include this if hele
];
},
@@ -32,6 +32,9 @@ var MilitaryAttackManager = Class({
// Sort by increasing count
types.sort(function (a, b) { return a[1] - b[1]; });
+ // TODO: we shouldn't return units that we don't have any
+ // buildings capable of training
+
return types[0][0];
},
diff --git a/binaries/data/mods/public/simulation/ai/testbot/plan-building.js b/binaries/data/mods/public/simulation/ai/testbot/plan-building.js
new file mode 100644
index 0000000000..c1b8efd90c
--- /dev/null
+++ b/binaries/data/mods/public/simulation/ai/testbot/plan-building.js
@@ -0,0 +1,186 @@
+var BuildingConstructionPlan = Class({
+
+ _init: function(gameState, type)
+ {
+ this.type = gameState.applyCiv(type);
+
+ this.cost = new Resources(gameState.getTemplate(this.type).cost());
+ },
+
+ canExecute: function(gameState)
+ {
+ // TODO: verify numeric limits etc
+
+ var builders = gameState.findBuilders(this.type);
+
+ return (builders.length != 0);
+ },
+
+ execute: function(gameState)
+ {
+// warn("Executing BuildingConstructionPlan "+uneval(this));
+
+ var builders = gameState.findBuilders(this.type).toEntityArray();
+
+ // We don't care which builder we assign, since they won't actually
+ // do the building themselves - all we care about is that there is
+ // some unit that can start the foundation
+
+ var pos = this.findGoodPosition(gameState);
+
+ builders[0].construct(this.type, pos.x, pos.z, pos.angle);
+ },
+
+ getCost: function()
+ {
+ return this.cost;
+ },
+
+ /**
+ * Make each cell's 16-bit value at least one greater than each of its
+ * neighbours' values. (If the grid is initialised with 0s and 65535s,
+ * the result of each cell is its Manhattan distance to the nearest 0.)
+ *
+ * TODO: maybe this should be 8-bit (and clamp at 255)?
+ */
+ expandInfluences: function(grid, w, h)
+ {
+ for (var y = 0; y < h; ++y)
+ {
+ var min = 65535;
+ for (var x = 0; x < w; ++x)
+ {
+ var g = grid[x + y*w];
+ if (g > min) grid[x + y*w] = min;
+ else if (g < min) min = g;
+ ++min;
+ }
+
+ for (var x = w-2; x >= 0; --x)
+ {
+ var g = grid[x + y*w];
+ if (g > min) grid[x + y*w] = min;
+ else if (g < min) min = g;
+ ++min;
+ }
+ }
+
+ for (var x = 0; x < w; ++x)
+ {
+ var min = 65535;
+ for (var y = 0; y < h; ++y)
+ {
+ var g = grid[x + y*w];
+ if (g > min) grid[x + y*w] = min;
+ else if (g < min) min = g;
+ ++min;
+ }
+
+ for (var y = h-2; y >= 0; --y)
+ {
+ var g = grid[x + y*w];
+ if (g > min) grid[x + y*w] = min;
+ else if (g < min) min = g;
+ ++min;
+ }
+ }
+ },
+
+ /**
+ * Add a circular linear-falloff shape to a grid.
+ */
+ addInfluence: function(grid, w, h, cx, cy, maxDist)
+ {
+ var x0 = Math.max(0, cx - maxDist);
+ var y0 = Math.max(0, cy - maxDist);
+ var x1 = Math.min(w, cx + maxDist);
+ var y1 = Math.min(h, cy + maxDist);
+ for (var y = y0; y < y1; ++y)
+ {
+ for (var x = x0; x < x1; ++x)
+ {
+ var dx = x - cx;
+ var dy = y - cy;
+ var r = Math.sqrt(dx*dx + dy*dy);
+ if (r < maxDist)
+ grid[x + y*w] += maxDist - r;
+ }
+ }
+ },
+
+ findGoodPosition: function(gameState)
+ {
+ var self = this;
+
+ var cellSize = 4; // size of each tile
+
+ // First, find all tiles that are far enough away from obstructions:
+
+ var map = gameState.getMap();
+
+ var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction");
+ // Only accept valid land tiles (we don't handle docks yet)
+ obstructionMask |= gameState.getPassabilityClassMask("building-land");
+
+ var obstructionTiles = new Uint16Array(map.data.length);
+ for (var i = 0; i < map.data.length; ++i)
+ obstructionTiles[i] = (map.data[i] & obstructionMask) ? 0 : 65535;
+
+// Engine.DumpImage("tiles0.png", obstructionTiles, map.width, map.height, 64);
+
+ this.expandInfluences(obstructionTiles, map.width, map.height);
+
+ // Compute each tile's closeness to friendly structures:
+
+ var friendlyTiles = new Uint16Array(map.data.length);
+
+ gameState.getOwnEntities().forEach(function(ent) {
+ if (ent.hasClass("Structure"))
+ {
+ var infl = 32;
+ if (ent.hasClass("CivCentre"))
+ infl *= 4;
+
+ var pos = ent.position();
+ var x = Math.round(pos[0] / cellSize);
+ var z = Math.round(pos[1] / cellSize);
+ self.addInfluence(friendlyTiles, map.width, map.height, x, z, infl);
+ }
+ });
+
+ // Find target building's approximate obstruction radius,
+ // and expand by a bit to make sure we're not too close
+ var template = gameState.getTemplate(this.type);
+ var radius = Math.ceil(template.obstructionRadius() / cellSize) + 2;
+
+ // Find the best non-obstructed tile
+ var bestIdx = 0;
+ var bestVal = -1;
+ for (var i = 0; i < map.data.length; ++i)
+ {
+ if (obstructionTiles[i] > radius)
+ {
+ var v = friendlyTiles[i];
+ if (v > bestVal)
+ {
+ bestVal = v;
+ bestIdx = i;
+ }
+ }
+ }
+ var x = ((bestIdx % map.width) + 0.5) * cellSize;
+ var z = (Math.floor(bestIdx / map.width) + 0.5) * cellSize;
+
+// Engine.DumpImage("tiles1.png", obstructionTiles, map.width, map.height, 32);
+// Engine.DumpImage("tiles2.png", friendlyTiles, map.width, map.height, 256);
+
+ // Randomise the angle a little, to look less artificial
+ var angle = Math.PI + (Math.random()*2-1) * Math.PI/24;
+
+ return {
+ "x": x,
+ "z": z,
+ "angle": angle
+ };
+ },
+});
diff --git a/binaries/data/mods/public/simulation/ai/testbot/plan-training.js b/binaries/data/mods/public/simulation/ai/testbot/plan-training.js
index b5a7b9c413..a9cb741748 100644
--- a/binaries/data/mods/public/simulation/ai/testbot/plan-training.js
+++ b/binaries/data/mods/public/simulation/ai/testbot/plan-training.js
@@ -2,11 +2,11 @@ var UnitTrainingPlan = Class({
_init: function(gameState, type, amount, metadata)
{
- this.type = type;
+ this.type = gameState.applyCiv(type);
this.amount = amount;
this.metadata = metadata;
- this.cost = new Resources(gameState.getTemplate(type).cost());
+ this.cost = new Resources(gameState.getTemplate(this.type).cost());
this.cost.multiply(amount); // (assume no batch discount)
},
diff --git a/binaries/data/mods/public/simulation/ai/testbot/plan.js b/binaries/data/mods/public/simulation/ai/testbot/plan.js
index 97f06c9c4b..6a49552df4 100644
--- a/binaries/data/mods/public/simulation/ai/testbot/plan.js
+++ b/binaries/data/mods/public/simulation/ai/testbot/plan.js
@@ -39,7 +39,7 @@ var PlanGroup = Class({
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; });
+ plans.sort(function(a, b) { return b.priority - a.priority; });
// Execute as many plans as we can afford
while (plans.length && this.escrow.canAfford(plans[0].plan.getCost()))
diff --git a/binaries/data/mods/public/simulation/ai/testbot/testbot.js b/binaries/data/mods/public/simulation/ai/testbot/testbot.js
index 600d821cf3..9502e3536f 100644
--- a/binaries/data/mods/public/simulation/ai/testbot/testbot.js
+++ b/binaries/data/mods/public/simulation/ai/testbot/testbot.js
@@ -25,8 +25,6 @@
/*
* 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)
@@ -50,6 +48,7 @@ function TestBotAI(settings)
this.planGroups = {
economyPersonnel: new PlanGroup(),
+ economyConstruction: new PlanGroup(),
militaryPersonnel: new PlanGroup(),
};
}
@@ -99,7 +98,7 @@ 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);
+ var gameState = new GameState(this);
// Find the resources we have this turn that haven't already
// been allocated to an escrow account.
diff --git a/binaries/data/mods/public/simulation/components/AnimalAI.js b/binaries/data/mods/public/simulation/components/AnimalAI.js
index 33f4b27656..24c1778130 100644
--- a/binaries/data/mods/public/simulation/components/AnimalAI.js
+++ b/binaries/data/mods/public/simulation/components/AnimalAI.js
@@ -67,6 +67,13 @@ var AnimalFsmSpec = {
this.PlaySound("panic");
},
+ "LeaveFoundation": function(msg) {
+ // Run away from the foundation
+ this.MoveAwayFrom(msg.target, +this.template.FleeDistance);
+ this.SetNextState("FLEEING");
+ this.PlaySound("panic");
+ },
+
"ROAMING": {
"enter": function() {
// Walk in a random direction
@@ -134,6 +141,12 @@ var AnimalFsmSpec = {
// Do nothing, just let them kill us
},
+ "LeaveFoundation": function(msg) {
+ // Walk away from the foundation
+ this.MoveAwayFrom(msg.target, 4);
+ this.SetNextState("FLEEING");
+ },
+
"ROAMING": {
"enter": function() {
// Walk in a random direction
@@ -174,6 +187,16 @@ var AnimalFsmSpec = {
this.SetNextState("ROAMING");
},
},
+
+ "FLEEING": {
+ "enter": function() {
+ this.SelectAnimation("walk", false, this.GetWalkSpeed());
+ },
+
+ "MoveCompleted": function() {
+ this.SetNextState("ROAMING");
+ },
+ },
},
};
@@ -236,6 +259,11 @@ AnimalAI.prototype.TimerHandler = function(data, lateness)
AnimalFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
+AnimalAI.prototype.LeaveFoundation = function(target)
+{
+ AnimalFsm.ProcessMessage(this, {"type": "LeaveFoundation", "target": target});
+};
+
// Functions to be called by the FSM:
AnimalAI.prototype.GetWalkSpeed = function()
diff --git a/binaries/data/mods/public/simulation/components/Foundation.js b/binaries/data/mods/public/simulation/components/Foundation.js
index deeb3e13e4..ce94059d29 100644
--- a/binaries/data/mods/public/simulation/components/Foundation.js
+++ b/binaries/data/mods/public/simulation/components/Foundation.js
@@ -5,6 +5,13 @@ Foundation.prototype.Schema =
Foundation.prototype.Init = function()
{
+ // Foundations are initially 'uncommitted' and do not block unit movement at all
+ // (to prevent players exploiting free foundations to confuse enemy units).
+ // The first builder to reach the uncommitted foundation will tell friendly units
+ // and animals to move out of the way, then will commit the foundation and enable
+ // its obstruction once there's nothing in the way.
+ this.committed = false;
+
this.buildProgress = 0.0; // 0 <= progress <= 1
};
@@ -63,6 +70,46 @@ Foundation.prototype.Build = function(builderEnt, work)
if (this.buildProgress == 1.0)
return;
+ // Handle the initial 'committing' of the foundation
+ if (!this.committed)
+ {
+ var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
+ if (cmpObstruction)
+ {
+ // If there's any units in the way, ask them to move away
+ // and return early from this method.
+ // Otherwise enable this obstruction so it blocks any further
+ // units, and continue building.
+
+ var collisions = cmpObstruction.GetConstructionCollisions();
+ if (collisions.length)
+ {
+ for each (var ent in collisions)
+ {
+ var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ if (cmpUnitAI)
+ cmpUnitAI.LeaveFoundation(this.entity);
+
+ var cmpAnimalAI = Engine.QueryInterface(ent, IID_AnimalAI);
+ if (cmpAnimalAI)
+ cmpAnimalAI.LeaveFoundation(this.entity);
+
+ // TODO: What if an obstruction has no UnitAI/AnimalAI?
+ }
+
+ // TODO: maybe we should tell the builder to use a special
+ // animation to indicate they're waiting for people to get
+ // out the way
+
+ return;
+ }
+
+ cmpObstruction.SetActive(true);
+ }
+
+ this.committed = true;
+ }
+
// Calculate the amount of progress that will be added (where 1.0 = completion)
var cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
var amount = work / cmpCost.GetBuildTime();
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index 3fed57d5ca..8dfdc26e4d 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -412,7 +412,7 @@ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
// Check whether it's obstructed by other entities
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
- var colliding = (cmpObstruction && cmpObstruction.CheckCollisions());
+ var colliding = (cmpObstruction && cmpObstruction.CheckFoundationCollisions());
// Check whether it's in a visible region
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
diff --git a/binaries/data/mods/public/simulation/components/TrainingQueue.js b/binaries/data/mods/public/simulation/components/TrainingQueue.js
index 91ca31b3b7..7a7c4842cf 100644
--- a/binaries/data/mods/public/simulation/components/TrainingQueue.js
+++ b/binaries/data/mods/public/simulation/components/TrainingQueue.js
@@ -253,7 +253,11 @@ TrainingQueue.prototype.SpawnUnits = function(templateName, count, metadata)
}
}
- Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": ents, "metadata": metadata });
+ Engine.PostMessage(this.entity, MT_TrainingFinished, {
+ "entities": ents,
+ "owner": cmpOwnership.GetOwner(),
+ "metadata": metadata,
+ });
};
TrainingQueue.prototype.ProgressTimeout = function(data)
diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index 96fe02f6b2..b6001c95f9 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -59,6 +59,13 @@ var UnitFsmSpec = {
this.SetNextState("FORMATIONMEMBER.WALKING");
},
+ // Special orders:
+ // (these will be overridden by various states)
+
+ "Order.LeaveFoundation": function(msg) {
+ // Default behaviour is to ignore the order since we're busy
+ this.FinishOrder();
+ },
// Individual orders:
// (these will switch the unit out of formation mode)
@@ -360,6 +367,25 @@ var UnitFsmSpec = {
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
+
+ // Override the LeaveFoundation order since we're not doing
+ // anything more important
+ "Order.LeaveFoundation": function(msg) {
+ // Move a tile outside the building
+ var range = 4;
+ var ok = this.MoveToTargetRangeExplicit(this.order.data.target, range, range);
+ if (ok)
+ {
+ // We've started walking to the given point
+ this.SetNextState("INDIVIDUAL.WALKING");
+ }
+ else
+ {
+ // We are already at the target, or can't move at all
+ this.FinishOrder();
+ }
+ },
+
},
"WALKING": {
@@ -718,6 +744,9 @@ var UnitFsmSpec = {
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
+ // Save the current order's data in case we need it later
+ var oldAutocontinue = this.order.data.autocontinue;
+
// We finished building it.
// Switch to the next order (if any)
if (this.FinishOrder())
@@ -725,6 +754,11 @@ var UnitFsmSpec = {
// No remaining orders - pick a useful default behaviour
+ // If autocontinue explicitly disabled (e.g. by AI) then
+ // do nothing automatically
+ if (!oldAutocontinue)
+ return;
+
// If this building was e.g. a farm, we should start gathering from it
// if we are capable of doing so
if (this.CanGather(msg.data.newentity))
@@ -1226,6 +1260,12 @@ UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
+UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
+{
+ var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ return cmpUnitMotion.MoveToTargetRange(target, min, max);
+};
+
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
@@ -1322,6 +1362,7 @@ UnitAI.prototype.ComputeWalkingDistance = function()
break; // and continue the loop
case "WalkToTarget":
+ case "LeaveFoundation":
case "Attack":
case "Gather":
case "ReturnResource":
@@ -1370,6 +1411,15 @@ UnitAI.prototype.WalkToTarget = function(target, queued)
this.AddOrder("WalkToTarget", { "target": target }, queued);
};
+UnitAI.prototype.LeaveFoundation = function(target)
+{
+ // TODO: we should verify this is a friendly foundation, otherwise
+ // there's no reason we should let them build here
+
+ var queued = true;
+ this.AddOrder("LeaveFoundation", { "target": target }, queued);
+};
+
UnitAI.prototype.Attack = function(target, queued)
{
if (!this.CanAttack(target))
diff --git a/binaries/data/mods/public/simulation/data/pathfinder.xml b/binaries/data/mods/public/simulation/data/pathfinder.xml
index cd2c17f325..bebef737d5 100644
--- a/binaries/data/mods/public/simulation/data/pathfinder.xml
+++ b/binaries/data/mods/public/simulation/data/pathfinder.xml
@@ -1,6 +1,9 @@
+
+
+
2
1.0
@@ -8,6 +11,12 @@
1
+
+
+ 0
+ 1.0
+
+