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 + +