Index: ps/trunk/binaries/data/mods/public/globalscripts/Resources.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Resources.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Resources.js (revision 20600)
@@ -1,86 +1,85 @@
/**
- * Since the AI context can't access JSON functions, it gets passed an object
- * containing the information from `GuiInterface.js::GetSimulationState()`.
+ * This class provides a cache to all resource names and properties defined by the JSON files.
*/
function Resources()
{
this.resourceData = [];
this.resourceDataObj = {};
this.resourceCodes = [];
this.resourceNames = {};
for (let filename of Engine.ListDirectoryFiles("simulation/data/resources/", "*.json", false))
{
let data = Engine.ReadJSONFile(filename);
if (!data)
continue;
if (data.code != data.code.toLowerCase())
warn("Resource codes should use lower case: " + data.code);
// Treasures are supported for every specified resource
if (data.code == "treasure")
{
error("Encountered resource with reserved keyword: " + data.code);
continue;
}
this.resourceData.push(data);
this.resourceDataObj[data.code] = data;
this.resourceCodes.push(data.code);
this.resourceNames[data.code] = data.name;
for (let subres in data.subtypes)
this.resourceNames[subres] = data.subtypes[subres];
}
// Sort arrays by specified order
let resSort = (a, b) =>
a.order < b.order ? -1 :
a.order > b.order ? +1 : 0;
this.resourceData.sort(resSort);
this.resourceCodes.sort((a, b) => resSort(
this.resourceData.find(resource => resource.code == a),
this.resourceData.find(resource => resource.code == b)
));
deepfreeze(this.resourceData);
deepfreeze(this.resourceDataObj);
deepfreeze(this.resourceCodes);
deepfreeze(this.resourceNames);
}
/**
* Returns the objects defined in the JSON files for all availbale resources,
* ordered as defined in these files.
*/
Resources.prototype.GetResources = function()
{
return this.resourceData;
};
/**
* Returns the object defined in the JSON file for the given resource.
*/
Resources.prototype.GetResource = function(type)
{
return this.resourceDataObj[type];
};
/**
* Returns an array containing all resource codes ordered as defined in the resource files.
* For example ["food", "wood", "stone", "metal"].
*/
Resources.prototype.GetCodes = function()
{
return this.resourceCodes;
};
/**
* Returns an object mapping resource codes to translatable resource names. Includes subtypes.
* For example { "food": "Food", "fish": "Fish", "fruit": "Fruit", "metal": "Metal", ... }
*/
Resources.prototype.GetNames = function()
{
return this.resourceNames;
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/resources.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/resources.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/resources.js (revision 20600)
@@ -1,68 +1,67 @@
+Resources = new Resources();
+
var API3 = function(m)
{
m.Resources = function(amounts = {}, population = 0)
{
- for (let key of this.types)
+ for (let key of Resources.GetCodes())
this[key] = amounts[key] || 0;
this.population = population > 0 ? population : 0;
};
-// This array will be filled in SharedScript.init
-m.Resources.prototype.types = [];
-
m.Resources.prototype.reset = function()
{
- for (let key of this.types)
+ for (let key of Resources.GetCodes())
this[key] = 0;
this.population = 0;
};
m.Resources.prototype.canAfford = function(that)
{
- for (let key of this.types)
+ for (let key of Resources.GetCodes())
if (this[key] < that[key])
return false;
return true;
};
m.Resources.prototype.add = function(that)
{
- for (let key of this.types)
+ for (let key of Resources.GetCodes())
this[key] += that[key];
this.population += that.population;
};
m.Resources.prototype.subtract = function(that)
{
- for (let key of this.types)
+ for (let key of Resources.GetCodes())
this[key] -= that[key];
this.population += that.population;
};
m.Resources.prototype.multiply = function(n)
{
- for (let key of this.types)
+ for (let key of Resources.GetCodes())
this[key] *= n;
this.population *= n;
};
m.Resources.prototype.Serialize = function()
{
let amounts = {};
- for (let key of this.types)
+ for (let key of Resources.GetCodes())
amounts[key] = this[key];
return { "amounts": amounts, "population": this.population };
};
m.Resources.prototype.Deserialize = function(data)
{
for (let key in data.amounts)
this[key] = data.amounts[key];
this.population = data.population;
};
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 20600)
@@ -1,398 +1,395 @@
var API3 = function(m)
{
/** Shared script handling templates and basic terrain analysis */
m.SharedScript = function(settings)
{
if (!settings)
return;
this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players)
this._templates = settings.templates;
this._techTemplates = settings.techTemplates;
this._entityMetadata = {};
for (let player of this._players)
this._entityMetadata[player] = {};
// array of entity collections
this._entityCollections = new Map();
this._entitiesModifications = new Map(); // entities modifications
this._templatesModifications = {}; // template modifications
// each name is a reference to the actual one.
this._entityCollectionsName = new Map();
this._entityCollectionsByDynProp = {};
this._entityCollectionsUID = 0;
};
/** Return a simple object (using no classes etc) that will be serialized into saved games */
m.SharedScript.prototype.Serialize = function()
{
return {
"players": this._players,
"techTemplates": this._techTemplates,
"templatesModifications": this._templatesModifications,
"entitiesModifications": this._entitiesModifications,
"metadata": this._entityMetadata
};
};
/**
* Called after the constructor when loading a saved game, with 'data' being
* whatever Serialize() returned
*/
m.SharedScript.prototype.Deserialize = function(data)
{
this._players = data.players;
this._techTemplates = data.techTemplates;
this._templatesModifications = data.templatesModifications;
this._entitiesModifications = data.entitiesModifications;
this._entityMetadata = data.metadata;
this.isDeserialized = true;
};
m.SharedScript.prototype.GetTemplate = function(name)
{
if (this._templates[name] === undefined)
this._templates[name] = Engine.GetTemplate(name) || null
return this._templates[name];
};
/**
* Initialize the shared component.
* We need to know the initial state of the game for this, as we will use it.
* This is called right at the end of the map generation.
*/
m.SharedScript.prototype.init = function(state, deserialization)
{
if (!deserialization)
this._entitiesModifications = new Map();
this.ApplyTemplatesDelta(state);
this.passabilityClasses = state.passabilityClasses;
this.playersData = state.players;
this.timeElapsed = state.timeElapsed;
this.circularMap = state.circularMap;
this.mapSize = state.mapSize;
this.gameType = state.gameType;
this.alliedVictory = state.alliedVictory;
this.ceasefireActive = state.ceasefireActive;
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
this.passabilityMap = state.passabilityMap;
if (this.mapSize % this.passabilityMap.width !== 0)
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width);
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
this.territoryMap = state.territoryMap;
if (this.mapSize % this.territoryMap.width !== 0)
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width);
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
/*
let landPassMap = new Uint8Array(this.passabilityMap.data.length);
let waterPassMap = new Uint8Array(this.passabilityMap.data.length);
let obstructionMaskLand = this.passabilityClasses["default-terrain-only"];
let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"];
for (let i = 0; i < this.passabilityMap.data.length; ++i)
{
landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
}
Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
*/
this._entities = new Map();
if (state.entities)
for (let id in state.entities)
this._entities.set(+id, new m.Entity(this, state.entities[id]));
// entity collection updated on create/destroy event.
this.entities = new m.EntityCollection(this, this._entities);
// create the terrain analyzer
this.terrainAnalyzer = new m.TerrainAnalysis();
this.terrainAnalyzer.init(this, state);
this.accessibility = new m.Accessibility();
this.accessibility.init(state, this.terrainAnalyzer);
- // Setup resources
- this.resourceInfo = state.resources;
- m.Resources.prototype.types = state.resources.codes;
// Resource types: ignore = not used for resource maps
// abundant = abundant resource with small amount each
// sparse = sparse resource, but huge amount each
// The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites)
// They are updated by checking for create and destroy events for all resources
this.normalizationFactor = { "abundant": 50, "sparse": 90 };
this.influenceRadius = { "abundant": 36, "sparse": 48 };
this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 };
this.resourceMaps = {}; // Contains maps showing the density of resources
this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement.
this.createResourceMaps();
this.gameState = {};
for (let player of this._players)
{
this.gameState[player] = new m.GameState();
this.gameState[player].init(this,state, player);
}
};
/**
* General update of the shared script, before each AI's update
* applies entity deltas, and each gamestate.
*/
m.SharedScript.prototype.onUpdate = function(state)
{
if (this.isDeserialized)
{
this.init(state, true);
this.isDeserialized = false;
}
// deals with updating based on create and destroy messages.
this.ApplyEntitiesDelta(state);
this.ApplyTemplatesDelta(state);
Engine.ProfileStart("onUpdate");
// those are dynamic and need to be reset as the "state" object moves in memory.
this.events = state.events;
this.passabilityClasses = state.passabilityClasses;
this.playersData = state.players;
this.timeElapsed = state.timeElapsed;
this.barterPrices = state.barterPrices;
this.ceasefireActive = state.ceasefireActive;
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
this.passabilityMap = state.passabilityMap;
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
this.territoryMap = state.territoryMap;
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
for (let i in this.gameState)
this.gameState[i].update(this);
// TODO: merge this with "ApplyEntitiesDelta" since after all they do the same.
this.updateResourceMaps(this.events);
Engine.ProfileStop();
};
m.SharedScript.prototype.ApplyEntitiesDelta = function(state)
{
Engine.ProfileStart("Shared ApplyEntitiesDelta");
let foundationFinished = {};
// by order of updating:
// we "Destroy" last because we want to be able to switch Metadata first.
for (let evt of state.events.Create)
{
if (!state.entities[evt.entity])
continue; // Sometimes there are things like foundations which get destroyed too fast
let entity = new m.Entity(this, state.entities[evt.entity]);
this._entities.set(evt.entity, entity);
this.entities.addEnt(entity);
// Update all the entity collections since the create operation affects static properties as well as dynamic
for (let entCol of this._entityCollections.values())
entCol.updateEnt(entity);
}
for (let evt of state.events.EntityRenamed)
{ // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly
for (let player of this._players)
{
this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity];
this._entityMetadata[player][evt.entity] = {};
}
}
for (let evt of state.events.TrainingFinished)
{ // Apply metadata stored in training queues
for (let entId of evt.entities)
if (this._entities.has(entId))
for (let key in evt.metadata)
this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]);
}
for (let evt of state.events.ConstructionFinished)
{
// metada are already moved by EntityRenamed when needed (i.e. construction, not repair)
if (evt.entity != evt.newentity)
foundationFinished[evt.entity] = true;
}
for (let evt of state.events.AIMetadata)
{
if (!this._entities.has(evt.id))
continue; // might happen in some rare cases of foundations getting destroyed, perhaps.
// Apply metadata (here for buildings for example)
for (let key in evt.metadata)
this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]);
}
for (let evt of state.events.Destroy)
{
if (!this._entities.has(evt.entity))
continue;// probably should remove the event.
if (foundationFinished[evt.entity])
evt.SuccessfulFoundation = true;
// The entity was destroyed but its data may still be useful, so
// remember the entity and this AI's metadata concerning it
evt.metadata = {};
evt.entityObj = this._entities.get(evt.entity);
for (let player of this._players)
evt.metadata[player] = this._entityMetadata[player][evt.entity];
let entity = this._entities.get(evt.entity);
for (let entCol of this._entityCollections.values())
entCol.removeEnt(entity);
this.entities.removeEnt(entity);
this._entities.delete(evt.entity);
this._entitiesModifications.delete(evt.entity);
for (let player of this._players)
delete this._entityMetadata[player][evt.entity];
}
for (let id in state.entities)
{
let changes = state.entities[id];
let entity = this._entities.get(+id);
for (let prop in changes)
{
entity._entity[prop] = changes[prop];
this.updateEntityCollections(prop, entity);
}
}
// apply per-entity aura-related changes.
// this supersedes tech-related changes.
for (let id in state.changedEntityTemplateInfo)
{
if (!this._entities.has(+id))
continue; // dead, presumably.
let changes = state.changedEntityTemplateInfo[id];
if (!this._entitiesModifications.has(+id))
this._entitiesModifications.set(+id, new Map());
let modif = this._entitiesModifications.get(+id);
for (let change of changes)
modif.set(change.variable, change.value);
}
Engine.ProfileStop();
};
m.SharedScript.prototype.ApplyTemplatesDelta = function(state)
{
Engine.ProfileStart("Shared ApplyTemplatesDelta");
for (let player in state.changedTemplateInfo)
{
let playerDiff = state.changedTemplateInfo[player];
for (let template in playerDiff)
{
let changes = playerDiff[template];
if (!this._templatesModifications[template])
this._templatesModifications[template] = {};
if (!this._templatesModifications[template][player])
this._templatesModifications[template][player] = new Map();
let modif = this._templatesModifications[template][player];
for (let change of changes)
modif.set(change.variable, change.value);
}
}
Engine.ProfileStop();
};
m.SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection)
{
entCollection.setUID(this._entityCollectionsUID);
this._entityCollections.set(this._entityCollectionsUID, entCollection);
for (let prop of entCollection.dynamicProperties())
{
if (!this._entityCollectionsByDynProp[prop])
this._entityCollectionsByDynProp[prop] = new Map();
this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection);
}
this._entityCollectionsUID++;
};
m.SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
{
let uid = entCollection.getUID();
if (this._entityCollections.has(uid))
this._entityCollections.delete(uid);
for (let prop of entCollection.dynamicProperties())
if (this._entityCollectionsByDynProp[prop].has(uid))
this._entityCollectionsByDynProp[prop].delete(uid);
};
m.SharedScript.prototype.updateEntityCollections = function(property, ent)
{
if (this._entityCollectionsByDynProp[property] === undefined)
return;
for (let entCol of this._entityCollectionsByDynProp[property].values())
entCol.updateEnt(ent);
};
m.SharedScript.prototype.setMetadata = function(player, ent, key, value)
{
let metadata = this._entityMetadata[player][ent.id()];
if (!metadata)
metadata = this._entityMetadata[player][ent.id()] = {};
metadata[key] = value;
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
};
m.SharedScript.prototype.getMetadata = function(player, ent, key)
{
let metadata = this._entityMetadata[player][ent.id()];
if (!metadata || !(key in metadata))
return undefined;
return metadata[key];
};
m.SharedScript.prototype.deleteMetadata = function(player, ent, key)
{
let metadata = this._entityMetadata[player][ent.id()];
if (!metadata || !(key in metadata))
return true;
metadata[key] = undefined;
delete metadata[key];
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
return true;
};
m.copyPrototype = function(descendant, parent)
{
let sConstructor = parent.toString();
let aMatch = sConstructor.match( /\s*function (.*)\(/ );
if ( aMatch != null )
descendant.prototype[aMatch[1]] = parent;
for (let p in parent.prototype)
descendant.prototype[p] = parent.prototype[p];
};
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js (revision 20600)
@@ -1,479 +1,479 @@
var API3 = function(m)
{
/**
* TerrainAnalysis, inheriting from the Map Component.
*
* This creates a suitable passability map.
* This is part of the Shared Script, and thus should only be used for things that are non-player specific.
* This.map is a map of the world, where particular stuffs are pointed with a value
* For example, impassable land is 0, water is 200, areas near tree (ie forest grounds) are 41…
* This is intended for use with 8 bit maps for reduced memory usage.
* Upgraded from QuantumState's original TerrainAnalysis for qBot.
*/
m.TerrainAnalysis = function()
{
};
m.copyPrototype(m.TerrainAnalysis, m.Map);
m.TerrainAnalysis.prototype.init = function(sharedScript, rawState)
{
let passabilityMap = rawState.passabilityMap;
this.width = passabilityMap.width;
this.height = passabilityMap.height;
this.cellSize = passabilityMap.cellSize;
let obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"];
let obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"];
let obstructionTiles = new Uint8Array(passabilityMap.data.length);
/* Generated map legend:
0 is impassable
200 is deep water (ie non-passable by land units)
201 is shallow water (passable by land units and water units)
255 is land (or extremely shallow water where ships can't go).
*/
for (let i = 0; i < passabilityMap.data.length; ++i)
{
// If impassable for land units, set to 0, else to 255.
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === 0)
obstructionTiles[i] = 200; // if navigable and not walkable (ie basic water), set to 200.
else if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === 255)
obstructionTiles[i] = 201; // navigable and walkable.
}
this.Map(rawState, "passability", obstructionTiles);
};
/**
* Accessibility inherits from TerrainAnalysis
*
* This can easily and efficiently determine if any two points are connected.
* it can also determine if any point is "probably" reachable, assuming the unit can get close enough
* for optimizations it's called after the TerrainAnalyser has finished initializing his map
* so this can use the land regions already.
*/
m.Accessibility = function()
{
};
m.copyPrototype(m.Accessibility, m.TerrainAnalysis);
m.Accessibility.prototype.init = function(rawState, terrainAnalyser)
{
this.Map(rawState, "passability", terrainAnalyser.map);
this.landPassMap = new Uint16Array(terrainAnalyser.length);
this.navalPassMap = new Uint16Array(terrainAnalyser.length);
this.maxRegions = 65535;
this.regionSize = [];
this.regionType = []; // "inaccessible", "land" or "water";
// ID of the region associated with an array of region IDs.
this.regionLinks = [];
// initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch);
// However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.)
// So start at 2.
this.regionID = 2;
for (let i = 0; i < this.landPassMap.length; ++i)
{
if (this.map[i] !== 0)
{ // any non-painted, non-inacessible area.
if (this.landPassMap[i] === 0 && this.floodFill(i,this.regionID,false))
this.regionType[this.regionID++] = "land";
if (this.navalPassMap[i] === 0 && this.floodFill(i,this.regionID,true))
this.regionType[this.regionID++] = "water";
}
else if (this.landPassMap[i] === 0)
{ // any non-painted, inacessible area.
this.floodFill(i,1,false);
this.floodFill(i,1,true);
}
}
// calculating region links. Regions only touching diagonaly are not linked.
// since we're checking all of them, we'll check from the top left to the bottom right
let w = this.width;
for (let x = 0; x < this.width-1; ++x)
{
for (let y = 0; y < this.height-1; ++y)
{
// checking right.
let thisLID = this.landPassMap[x+y*w];
let thisNID = this.navalPassMap[x+y*w];
let rightLID = this.landPassMap[x+1+y*w];
let rightNID = this.navalPassMap[x+1+y*w];
let bottomLID = this.landPassMap[x+y*w+w];
let bottomNID = this.navalPassMap[x+y*w+w];
if (thisLID > 1)
{
if (rightNID > 1)
if (this.regionLinks[thisLID].indexOf(rightNID) === -1)
this.regionLinks[thisLID].push(rightNID);
if (bottomNID > 1)
if (this.regionLinks[thisLID].indexOf(bottomNID) === -1)
this.regionLinks[thisLID].push(bottomNID);
}
if (thisNID > 1)
{
if (rightLID > 1)
if (this.regionLinks[thisNID].indexOf(rightLID) === -1)
this.regionLinks[thisNID].push(rightLID);
if (bottomLID > 1)
if (this.regionLinks[thisNID].indexOf(bottomLID) === -1)
this.regionLinks[thisNID].push(bottomLID);
if (thisLID > 1)
if (this.regionLinks[thisNID].indexOf(thisLID) === -1)
this.regionLinks[thisNID].push(thisLID);
}
}
}
//Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
//Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
};
m.Accessibility.prototype.getAccessValue = function(position, onWater)
{
let gamePos = this.gamePosToMapPos(position);
if (onWater)
return this.navalPassMap[gamePos[0] + this.width*gamePos[1]];
let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]];
if (ret === 1)
{
// quick spiral search.
let indx = [ [-1,-1],[-1,0],[-1,1],[0,1],[1,1],[1,0],[1,-1],[0,-1]];
for (let i of indx)
{
let id0 = gamePos[0] + i[0];
let id1 = gamePos[1] + i[1];
if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width)
continue;
ret = this.landPassMap[id0 + this.width*id1];
if (ret !== 1)
return ret;
}
}
return ret;
};
m.Accessibility.prototype.getTrajectTo = function(start, end)
{
let pstart = this.gamePosToMapPos(start);
let istart = pstart[0] + pstart[1]*this.width;
let pend = this.gamePosToMapPos(end);
let iend = pend[0] + pend[1]*this.width;
let onLand = true;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1)
onLand = false;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1)
return false;
let endRegion = this.landPassMap[iend];
if (endRegion <= 1 && this.navalPassMap[iend] > 1)
endRegion = this.navalPassMap[iend];
else if (endRegion <= 1)
return false;
let startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart];
return this.getTrajectToIndex(startRegion, endRegion);
};
/**
* Return a "path" of accessibility indexes from one point to another, including the start and the end indexes
* this can tell you what sea zone you need to have a dock on, for example.
* assumes a land unit unless start point is over deep water.
*/
m.Accessibility.prototype.getTrajectToIndex = function(istart, iend)
{
if (istart === iend)
return [istart];
let trajects = new Set();
let explored = new Set();
trajects.add([istart]);
explored.add(istart);
while (trajects.size)
{
for (let traj of trajects)
{
let ilast = traj[traj.length-1];
for (let inew of this.regionLinks[ilast])
{
if (inew === iend)
return traj.concat(iend);
if (explored.has(inew))
continue;
trajects.add(traj.concat(inew));
explored.add(inew);
}
trajects.delete(traj);
}
}
return undefined;
};
m.Accessibility.prototype.getRegionSize = function(position, onWater)
{
let pos = this.gamePosToMapPos(position);
let index = pos[0] + pos[1]*this.width;
let ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index];
if (this.regionSize[ID] === undefined)
return 0;
return this.regionSize[ID];
};
m.Accessibility.prototype.getRegionSizei = function(index, onWater)
{
if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined))
return 0;
if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]])
return this.regionSize[this.navalPassMap[index]];
return this.regionSize[this.landPassMap[index]];
};
/** Implementation of a fast flood fill. Reasonably good performances for JS. */
m.Accessibility.prototype.floodFill = function(startIndex, value, onWater)
{
if (value > this.maxRegions)
{
error("AI accessibility map: too many regions.");
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
}
if (!onWater && this.landPassMap[startIndex] !== 0 || onWater && this.navalPassMap[startIndex] !== 0 )
return false; // already painted.
let floodFor = "land";
if (this.map[startIndex] === 0)
{
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
}
if (onWater === true)
{
if (this.map[startIndex] !== 200 && this.map[startIndex] !== 201)
{
this.navalPassMap[startIndex] = 1; // impassable for naval
return false; // do nothing
}
floodFor = "water";
}
else if (this.map[startIndex] === 200)
{
this.landPassMap[startIndex] = 1; // impassable for land
return false;
}
// here we'll be able to start.
for (let i = this.regionSize.length; i <= value; ++i)
{
this.regionLinks.push([]);
this.regionSize.push(0);
this.regionType.push("inaccessible");
}
let w = this.width;
let h = this.height;
let y = 0;
// Get x and y from index
let IndexArray = [startIndex];
let newIndex;
while(IndexArray.length)
{
newIndex = IndexArray.pop();
y = 0;
let loop = false;
// vertical iteration
do {
--y;
loop = false;
let index = newIndex + w*y;
if (index < 0)
break;
if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200)
loop = true;
else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201))
loop = true;
else
break;
} while (loop === true); // should actually break
++y;
let reachLeft = false;
let reachRight = false;
let index;
do {
index = newIndex + w*y;
if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200)
{
this.landPassMap[index] = value;
this.regionSize[value]++;
}
else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201))
{
this.navalPassMap[index] = value;
this.regionSize[value]++;
}
else
break;
if (index%w > 0)
{
if (floodFor === "land" && this.landPassMap[index -1] === 0 && this.map[index -1] !== 0 && this.map[index -1] !== 200)
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index -1] === 0 && (this.map[index -1] === 200 || this.map[index -1] === 201))
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (reachLeft)
reachLeft = false;
}
if (index%w < w - 1)
{
if (floodFor === "land" && this.landPassMap[index +1] === 0 && this.map[index +1] !== 0 && this.map[index +1] !== 200)
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index +1] === 0 && (this.map[index +1] === 200 || this.map[index +1] === 201))
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (reachRight)
reachRight = false;
}
++y;
} while (index/w < h-1); // should actually break
}
return true;
};
/** creates a map of resource density */
m.SharedScript.prototype.createResourceMaps = function()
{
- for (let resource of this.resourceInfo.codes)
+ for (let resource of Resources.GetCodes())
{
- if (!(this.resourceInfo.aiInfluenceGroups[resource] in this.normalizationFactor))
+ if (!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
continue;
// if there is no resourceMap create one with an influence for everything with that resource
if (this.resourceMaps[resource])
continue;
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
// But at that point the precision is not really important anyway. And it saves memory.
this.resourceMaps[resource] = new m.Map(this, "resource");
this.ccResourceMaps[resource] = new m.Map(this, "resource");
}
for (let ent of this._entities.values())
{
if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
continue;
let cellSize = this.resourceMaps[resource].cellSize;
let x = Math.floor(ent.position()[0] / cellSize);
let z = Math.floor(ent.position()[1] / cellSize);
- let grp = this.resourceInfo.aiInfluenceGroups[resource];
+ let grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
let strength = Math.floor(ent.resourceSupplyMax() / this.normalizationFactor[grp]);
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2, "constant");
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2);
this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
}
};
/**
* creates and maintains a map of unused resource density
* this also takes dropsites into account.
* resources that are "part" of a dropsite are not counted.
*/
m.SharedScript.prototype.updateResourceMaps = function(events)
{
- for (let resource of this.resourceInfo.codes)
+ for (let resource of Resources.GetCodes())
{
- if (!(this.resourceInfo.aiInfluenceGroups[resource] in this.normalizationFactor))
+ if (!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
continue;
// if there is no resourceMap create one with an influence for everything with that resource
if (this.resourceMaps[resource])
continue;
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
// But at that point the precision is not really important anyway. And it saves memory.
this.resourceMaps[resource] = new m.Map(this, "resource");
this.ccResourceMaps[resource] = new m.Map(this, "resource");
}
// Look for destroy (or create) events and subtract (or add) the entities original influence from the resourceMap
for (let e of events.Destroy)
{
if (!e.entityObj)
continue;
let ent = e.entityObj;
if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
continue;
let cellSize = this.resourceMaps[resource].cellSize;
let x = Math.floor(ent.position()[0] / cellSize);
let z = Math.floor(ent.position()[1] / cellSize);
- let grp = this.resourceInfo.aiInfluenceGroups[resource];
+ let grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
let strength = -Math.floor(ent.resourceSupplyMax() / this.normalizationFactor[grp]);
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2, "constant");
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2);
this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
}
for (let e of events.Create)
{
if (!e.entity || !this._entities.has(e.entity))
continue;
let ent = this._entities.get(e.entity);
if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
continue;
let cellSize = this.resourceMaps[resource].cellSize;
let x = Math.floor(ent.position()[0] / cellSize);
let z = Math.floor(ent.position()[1] / cellSize);
- let grp = this.resourceInfo.aiInfluenceGroups[resource];
+ let grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
let strength = Math.floor(ent.resourceSupplyMax() / this.normalizationFactor[grp]);
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2, "constant");
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2);
this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
}
};
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 20600)
@@ -1,1007 +1,1007 @@
var PETRA = function(m)
{
/**
* Base Manager
* Handles lower level economic stuffs.
* Some tasks:
* -tasking workers: gathering/hunting/building/repairing?/scouting/plans.
* -giving feedback/estimates on GR
* -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans.
* -getting good spots for dropsites
* -managing dropsite use in the base
* -updating whatever needs updating, keeping track of stuffs (rebuilding needs…)
*/
m.BaseManager = function(gameState, Config)
{
this.Config = Config;
this.ID = gameState.ai.uniqueIDs.bases++;
// anchor building: seen as the main building of the base. Needs to have territorial influence
this.anchor = undefined;
this.anchorId = undefined;
this.accessIndex = undefined;
// Maximum distance (from any dropsite) to look for resources
// 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max
this.maxDistResourceSquare = 360*360;
this.constructing = false;
// Defenders to train in this cc when its construction is finished
this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0;
// vector for iterating, to check one use the HQ map.
this.territoryIndices = [];
this.timeNextIdleCheck = 0;
};
m.BaseManager.prototype.init = function(gameState, state)
{
if (state == "unconstructed")
this.constructing = true;
else if (state != "captured")
this.neededDefenders = 0;
this.workerObject = new m.Worker(this);
// entitycollections
this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker"));
this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.units.registerUpdates();
this.workers.registerUpdates();
this.buildings.registerUpdates();
// array of entity IDs, with each being
this.dropsites = {};
this.dropsiteSupplies = {};
this.gatherers = {};
- for (let res of gameState.sharedScript.resourceInfo.codes)
+ for (let res of Resources.GetCodes())
{
this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] };
this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 };
}
};
m.BaseManager.prototype.assignEntity = function(gameState, ent)
{
ent.setMetadata(PlayerID, "base", this.ID);
this.units.updateEnt(ent);
this.workers.updateEnt(ent);
this.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
this.assignResourceToDropsite(gameState, ent);
};
m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
{
if (!anchorEntity.hasClass("CivCentre"))
{
warn("Error: Petra base " + this.ID + " has been assigned an anchor that is not a civil centre.");
return false;
}
this.anchor = anchorEntity;
this.anchorId = anchorEntity.id();
this.anchor.setMetadata(PlayerID, "base", this.ID);
this.anchor.setMetadata(PlayerID, "baseAnchor", true);
this.buildings.updateEnt(this.anchor);
this.accessIndex = gameState.ai.accessibility.getAccessValue(this.anchor.position());
gameState.ai.HQ.resetBaseCache();
// in case some of our other bases were destroyed, reaffect these destroyed bases to this base
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.anchor || base.newbaseID)
continue;
base.newbaseID = this.ID;
}
return true;
};
/* we lost our anchor. Let's reaffect our units and buildings */
m.BaseManager.prototype.anchorLost = function (gameState, ent)
{
this.anchor = undefined;
this.anchorId = undefined;
this.neededDefenders = 0;
let bestbase = m.getBestBase(gameState, ent);
this.newbaseID = bestbase.ID;
for (let entity of this.units.values())
bestbase.assignEntity(gameState, entity);
for (let entity of this.buildings.values())
bestbase.assignEntity(gameState, entity);
gameState.ai.HQ.resetBaseCache();
};
/**
* Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
* Moving resources (animals) and buildable resources (fields) are treated elsewhere.
*/
m.BaseManager.prototype.assignResourceToDropsite = function (gameState, dropsite)
{
if (this.dropsites[dropsite.id()])
{
if (this.Config.debug > 0)
warn("assignResourceToDropsite: dropsite already in the list. Should never happen");
return;
}
let accessIndex = this.accessIndex;
let dropsitePos = dropsite.position();
let dropsiteId = dropsite.id();
this.dropsites[dropsiteId] = true;
if (this.ID === gameState.ai.HQ.baseManagers[0].ID)
{
accessIndex = dropsite.getMetadata(PlayerID, "access");
if (!accessIndex)
{
accessIndex = gameState.ai.accessibility.getAccessValue(dropsitePos);
dropsite.setMetadata(PlayerID, "access", accessIndex);
}
}
let maxDistResourceSquare = this.maxDistResourceSquare;
for (let type of dropsite.resourceDropsiteTypes())
{
let resources = gameState.getResourceSupplies(type);
if (!resources.length)
continue;
let nearby = this.dropsiteSupplies[type].nearby;
let medium = this.dropsiteSupplies[type].medium;
let faraway = this.dropsiteSupplies[type].faraway;
resources.forEach(function(supply)
{
if (!supply.position())
return;
if (supply.hasClass("Animal")) // moving resources are treated differently
return;
if (supply.hasClass("Field")) // fields are treated separately
return;
if (supply.resourceSupplyType().generic === "treasure") // treasures are treated separately
return;
// quick accessibility check
let access = supply.getMetadata(PlayerID, "access");
if (!access)
{
access = gameState.ai.accessibility.getAccessValue(supply.position());
supply.setMetadata(PlayerID, "access", access);
}
if (access !== accessIndex)
return;
let dist = API3.SquareVectorDistance(supply.position(), dropsitePos);
if (dist < maxDistResourceSquare)
{
if (dist < maxDistResourceSquare/16) // distmax/4
nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else if (dist < maxDistResourceSquare/4) // distmax/2
medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else
faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
}
});
nearby.sort((r1, r2) => r1.dist - r2.dist);
medium.sort((r1, r2) => r1.dist - r2.dist);
faraway.sort((r1, r2) => r1.dist - r2.dist);
/* let debug = false;
if (debug)
{
faraway.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]});
});
medium.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]});
});
nearby.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]});
});
} */
}
// Allows all allies to use this dropsite except if base anchor to be sure to keep
// a minimum of resources for this base
Engine.PostCommand(PlayerID, {
"type": "set-dropsite-sharing",
"entities": [dropsiteId],
"shared": dropsiteId !== this.anchorId
});
};
// completely remove the dropsite resources from our list.
m.BaseManager.prototype.removeDropsite = function (gameState, ent)
{
if (!ent.id())
return;
let removeSupply = function(entId, supply){
for (let i = 0; i < supply.length; ++i)
{
// exhausted resource, remove it from this list
if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
supply.splice(i--, 1);
// resource assigned to the removed dropsite, remove it
else if (supply[i].dropsite == entId)
supply.splice(i--, 1);
}
};
for (let type in this.dropsiteSupplies)
{
removeSupply(ent.id(), this.dropsiteSupplies[type].nearby);
removeSupply(ent.id(), this.dropsiteSupplies[type].medium);
removeSupply(ent.id(), this.dropsiteSupplies[type].faraway);
}
this.dropsites[ent.id()] = undefined;
return;
};
/**
* Returns the position of the best place to build a new dropsite for the specified resource
*/
m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
{
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse"));
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then checks for a good spot in the territory. If none, and town/city phase, checks outside
// The AI will currently not build a CC if it wouldn't connect with an existing CC.
let obstructions = m.createObstructionMap(gameState, this.accessIndex, template);
let dpEnts = gameState.getOwnEntitiesByClass("Storehouse", true).toEntityArray();
let ccEnts = gameState.getOwnEntitiesByClass("CivCentre", true).toEntityArray();
let bestIdx;
let bestVal = 0;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let territoryMap = gameState.ai.HQ.territoryMap;
let width = territoryMap.width;
let cellSize = territoryMap.cellSize;
for (let j of this.territoryIndices)
{
let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0) // no room around
continue;
// we add 3 times the needed resource and once the others (except food)
let total = 2*gameState.sharedScript.resourceMaps[resource].map[j];
for (let res in gameState.sharedScript.resourceMaps)
if (res !== "food")
total += gameState.sharedScript.resourceMaps[res].map[j];
total = 0.7*total; // Just a normalisation factor as the locateMap is limited to 255
if (total <= bestVal)
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let dp of dpEnts)
{
let dpPos = dp.position();
if (!dpPos)
continue;
let dist = API3.SquareVectorDistance(dpPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
for (let cc of ccEnts)
{
let ccPos = cc.position();
if (!ccPos)
continue;
let dist = API3.SquareVectorDistance(ccPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = total;
bestIdx = i;
}
if (this.Config.debug > 2)
warn(" for dropsite best is " + bestVal);
if (bestVal <= 0)
return {"quality": bestVal, "pos": [0, 0]};
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return { "quality": bestVal, "pos": [x, z] };
};
m.BaseManager.prototype.getResourceLevel = function (gameState, type, nearbyOnly = false)
{
let count = 0;
let check = {};
for (let supply of this.dropsiteSupplies[type].nearby)
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
count += supply.ent.resourceSupplyAmount();
}
if (nearbyOnly)
return count;
for (let supply of this.dropsiteSupplies[type].medium)
{
if (check[supply.id])
continue;
check[supply.id] = true;
count += 0.6*supply.ent.resourceSupplyAmount();
}
return count;
};
/** check our resource levels and react accordingly */
m.BaseManager.prototype.checkResourceLevels = function (gameState, queues)
{
- for (let type of gameState.sharedScript.resourceInfo.codes)
+ for (let type of Resources.GetCodes())
{
if (type === "food")
{
if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms.
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations
let numQueue = queues.field.countQueuedUnits();
// TODO if not yet farms, add a check on time used/lost and build farmstead if needed
if (numFarms + numQueue === 0) // starting game, rely on fruits as long as we have enough of them
{
if (count < 600)
{
queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base": this.ID }));
gameState.ai.HQ.needFarm = true;
}
}
else
{
let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length;
let goal = this.Config.Economy.provisionFields;
if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5)
goal = Math.max(goal-1, 1);
if (numFound + numQueue < goal)
queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base": this.ID }));
}
}
else if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) &&
!queues.corral.hasQueuedUnits() &&
!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral"))
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
if (count < 600)
{
queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral", { "base": this.ID }));
gameState.ai.HQ.needCorral = true;
}
}
}
else if (!queues.dropsites.hasQueuedUnits() && !gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities() &&
gameState.sharedScript.resourceMaps[type])
{
if (gameState.ai.playedTurn > this.gatherers[type].nextCheck)
{
let self = this;
this.gatherersByType(gameState, type).forEach(function (ent) {
if (ent.unitAIState() === "INDIVIDUAL.GATHER.GATHERING")
++self.gatherers[type].used;
else if (ent.unitAIState() === "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
++self.gatherers[type].lost;
});
// TODO add also a test on remaining resources
let total = this.gatherers[type].used + this.gatherers[type].lost;
if (total > 150 || total > 60 && type !== "wood")
{
let ratio = this.gatherers[type].lost / total;
if (ratio > 0.15)
{
let newDP = this.findBestDropsiteLocation(gameState, type);
if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits())
{
// No good dropsite, try to build a new base if no base already planned,
// and if not possible, be less strict on dropsite quality
if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) &&
newDP.quality > Math.min(25, 50*0.15/ratio) &&
gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
}
}
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
}
else if (total === 0)
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
}
}
else
{
this.gatherers[type].nextCheck = gameState.ai.playedTurn;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
}
}
};
/** Adds the estimated gather rates from this base to the currentRates */
m.BaseManager.prototype.getGatherRates = function(gameState, currentRates)
{
for (let res in currentRates)
{
// I calculate the exact gathering rate for each unit.
// I must then lower that to account for travel time.
// Given that the faster you gather, the more travel time matters,
// I use some logarithms.
// TODO: this should take into account for unit speed and/or distance to target
this.gatherersByType(gameState, res).forEach(function (ent) {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
if (res === "food")
{
this.workersBySubrole(gameState, "hunter").forEach(function (ent) {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
this.workersBySubrole(gameState, "fisher").forEach(function (ent) {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
}
}
};
m.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
{
if (!roleless)
roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values();
for (let ent of roleless)
{
if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "role", "worker");
else if (ent.hasClass("Support") && ent.hasClass("Elephant"))
ent.setMetadata(PlayerID, "role", "worker");
}
};
/**
* If the numbers of workers on the resources is unbalanced then set some of workers to idle so
* they can be reassigned by reassignIdleWorkers.
* TODO: actually this probably should be in the HQ.
*/
m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
{
this.timeNextIdleCheck = gameState.ai.elapsedTime + 8;
// change resource only towards one which is more needed, and if changing will not change this order
let nb = 1; // no more than 1 change per turn (otherwise we should update the rates)
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
let sumWanted = 0;
let sumCurrent = 0;
for (let need of mostNeeded)
{
sumWanted += need.wanted;
sumCurrent += need.current;
}
let scale = 1;
if (sumWanted > 0)
scale = sumCurrent / sumWanted;
for (let i = mostNeeded.length-1; i > 0; --i)
{
let lessNeed = mostNeeded[i];
for (let j = 0; j < i; ++j)
{
let moreNeed = mostNeeded[j];
let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
// If we assume a mean rate of 0.5 per gatherer, this diff should be > 1
// but we require a bit more to avoid too frequent changes
if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5)
{
let only;
// in average, females are less efficient for stone and metal, and citizenSoldiers for food
let gatherers = this.gatherersByType(gameState, lessNeed.type);
if (lessNeed.type == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities())
only = "CitizenSoldier";
else if (moreNeed.type == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities())
only = "FemaleCitizen";
gatherers.forEach( function (ent) {
if (!ent.canGather(moreNeed.type))
return;
if (nb === 0)
return;
if (only && !ent.hasClass(only))
return;
--nb;
ent.stopMoving();
ent.setMetadata(PlayerID, "gather-type", moreNeed.type);
gameState.ai.HQ.AddTCResGatherer(moreNeed.type);
});
if (nb === 0)
return;
}
}
}
};
m.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
{
// Search for idle workers, and tell them to gather resources based on demand
if (!idleWorkers)
{
let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle");
idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values();
}
for (let ent of idleWorkers)
{
// Check that the worker isn't garrisoned
if (!ent.position())
continue;
// Support elephant can only be builders
if (ent.hasClass("Support") && ent.hasClass("Elephant"))
{
ent.setMetadata(PlayerID, "subrole", "idle");
continue;
}
if (ent.hasClass("Worker"))
{
// Just emergency repairing here. It is better managed in assignToFoundations
if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() &&
gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2)
ent.repair(this.anchor);
else if (ent.isGatherer())
{
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
for (let needed of mostNeeded)
{
if (!ent.canGather(needed.type))
continue;
let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
ent.setMetadata(PlayerID, "subrole", "gatherer");
ent.setMetadata(PlayerID, "gather-type", needed.type);
gameState.ai.HQ.AddTCResGatherer(needed.type);
break;
}
}
}
else if (ent.hasClass("Cavalry"))
ent.setMetadata(PlayerID, "subrole", "hunter");
else if (ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "subrole", "fisher");
}
};
m.BaseManager.prototype.workersBySubrole = function(gameState, subrole)
{
return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers);
};
m.BaseManager.prototype.gatherersByType = function(gameState, type)
{
return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer"));
};
/**
* returns an entity collection of workers.
* They are idled immediatly and their subrole set to idle.
*/
m.BaseManager.prototype.pickBuilders = function(gameState, workers, number)
{
let availableWorkers = this.workers.filter(function (ent) {
if (!ent.position() || !ent.isBuilder())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
availableWorkers.sort(function (a,b) {
let vala = 0;
let valb = 0;
if (a.getMetadata(PlayerID, "subrole") == "builder")
vala = 100;
if (b.getMetadata(PlayerID, "subrole") == "builder")
valb = 100;
if (a.getMetadata(PlayerID, "subrole") == "idle")
vala = -50;
if (b.getMetadata(PlayerID, "subrole") == "idle")
valb = -50;
if (a.getMetadata(PlayerID, "plan") === undefined)
vala = -20;
if (b.getMetadata(PlayerID, "plan") === undefined)
valb = -20;
return vala - valb;
});
let needed = Math.min(number, availableWorkers.length - 3);
for (let i = 0; i < needed; ++i)
{
availableWorkers[i].stopMoving();
availableWorkers[i].setMetadata(PlayerID, "subrole", "idle");
workers.addEnt(availableWorkers[i]);
}
return;
};
/**
* If we have some foundations, and we don't have enough builder-workers,
* try reassigning some other workers who are nearby
* AI tries to use builders sensibly, not completely stopping its econ.
*/
m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
{
let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field"))));
let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair());
// Check if nothing to build
if (!foundations.length && !damagedBuildings.length)
return;
let workers = this.workers.filter(ent => ent.isBuilder());
let builderWorkers = this.workersBySubrole(gameState, "builder");
let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle());
// if we're constructing and we have the foundations to our base anchor, only try building that.
if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities())
{
foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true));
let tID = foundations.toEntityArray()[0].id();
workers.forEach(function (ent) {
let target = ent.getMetadata(PlayerID, "target-foundation");
if (target && target != tID)
{
ent.stopMoving();
ent.setMetadata(PlayerID, "target-foundation", tID);
}
});
}
if (workers.length < 3)
{
let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2);
if (fromOtherBase)
{
let baseID = this.ID;
fromOtherBase.forEach(function (worker) {
worker.setMetadata(PlayerID, "base", baseID);
worker.setMetadata(PlayerID, "subrole", "builder");
workers.updateEnt(worker);
builderWorkers.updateEnt(worker);
idleBuilderWorkers.updateEnt(worker);
});
}
}
let builderTot = builderWorkers.length - idleBuilderWorkers.length;
for (let target of foundations.values())
{
if (target.hasClass("Field"))
continue; // we do not build fields
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
if (!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && (!target.hasClass("Wonder") || gameState.getGameType() !== "wonder"))
continue;
// if our territory has shrinked since this foundation was positioned, do not build it
if (m.isNotWorthBuilding(gameState, target))
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * 0.2);
if (maxTotalBuilders < 2 && workers.length > 1)
maxTotalBuilders = 2;
if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 &&
gameState.getPopulationLimit() < gameState.getPopulationMax())
maxTotalBuilders = maxTotalBuilders + 2;
let targetNB = 2;
if (target.hasClass("House") || target.hasClass("DropsiteWood"))
targetNB = 3;
else if (target.hasClass("Barracks") || target.hasClass("DefenseTower") || target.hasClass("Market"))
targetNB = 4;
else if (target.hasClass("Fortress"))
targetNB = 7;
if (target.getMetadata(PlayerID, "baseAnchor") === true || target.hasClass("Wonder") && gameState.getGameType() === "wonder")
{
targetNB = 15;
maxTotalBuilders = Math.max(maxTotalBuilders, 15);
}
// if no base yet, everybody should build
if (gameState.ai.HQ.numActiveBases() == 0)
{
targetNB = workers.length;
maxTotalBuilders = targetNB;
}
if (assigned < targetNB)
{
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
assigned++;
builderTot++;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned < targetNB && builderTot < maxTotalBuilders)
{
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") === "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
let time = target.buildTime();
nonBuilderWorkers.sort(function (workerA,workerB)
{
let coeffA = API3.SquareVectorDistance(target.position(),workerA.position());
// elephant moves slowly, so when far away they are only useful if build time is long
if (workerA.hasClass("Elephant"))
coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time);
else if (workerA.getMetadata(PlayerID, "gather-type") === "food")
coeffA *= 3;
let coeffB = API3.SquareVectorDistance(target.position(),workerB.position());
if (workerB.hasClass("Elephant"))
coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time);
else if (workerB.getMetadata(PlayerID, "gather-type") === "food")
coeffB *= 3;
return coeffA - coeffB;
});
let current = 0;
let nonBuilderTot = nonBuilderWorkers.length;
while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot)
{
assigned++;
builderTot++;
let ent = nonBuilderWorkers[current++];
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
}
}
}
}
for (let target of damagedBuildings.values())
{
// don't repair if we're still under attack, unless it's a vital (civcentre or wall) building that's getting destroyed.
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
if (target.healthLevel() > 0.5 ||
!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && (!target.hasClass("Wonder") || gameState.getGameType() !== "wonder"))
continue;
else if (noRepair && !target.hasClass("CivCentre"))
continue;
if (target.decaying())
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * 0.2);
let targetNB = 1;
if (target.hasClass("Fortress"))
targetNB = 3;
if (target.getMetadata(PlayerID, "baseAnchor") === true || target.hasClass("Wonder") && gameState.getGameType() === "wonder")
{
maxTotalBuilders = Math.ceil(workers.length * 0.3);
targetNB = 5;
if (target.healthLevel() < 0.3)
{
maxTotalBuilders = Math.ceil(workers.length * 0.6);
targetNB = 7;
}
}
if (assigned < targetNB)
{
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
assigned++;
builderTot++;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned < targetNB && builderTot < maxTotalBuilders)
{
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") === "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
});
let num = Math.min(nonBuilderWorkers.length, targetNB-assigned);
let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num);
nearestNonBuilders.forEach(function(ent) {
assigned++;
builderTot++;
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
}
}
}
};
m.BaseManager.prototype.update = function(gameState, queues, events)
{
if (this.ID === gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units
{
// if some active base, reassigns the workers/buildings
// otherwise look for anything useful to do, i.e. treasures to gather
if (gameState.ai.HQ.numActiveBases() > 0)
{
for (let ent of this.units.values())
m.getBestBase(gameState, ent).assignEntity(gameState, ent);
for (let ent of this.buildings.values())
{
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
this.removeDropsite(gameState, ent);
m.getBestBase(gameState, ent).assignEntity(gameState, ent);
}
}
else if (gameState.ai.HQ.canBuildUnits)
{
this.assignToFoundations(gameState);
if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
}
return;
}
if (!this.anchor) // this base has been destroyed
{
// transfer possible remaining units (probably they were in training during previous transfers)
if (this.newbaseID)
{
let newbaseID = this.newbaseID;
for (let ent of this.units.values())
ent.setMetadata(PlayerID, "base", newbaseID);
for (let ent of this.buildings.values())
ent.setMetadata(PlayerID, "base", newbaseID);
}
return;
}
if (this.anchor.getMetadata(PlayerID, "access") != this.accessIndex)
API3.warn("Petra baseManager " + this.ID + " problem with accessIndex " + this.accessIndex +
" while metadata access is " + this.anchor.getMetadata(PlayerID, "access"));
Engine.ProfileStart("Base update - base " + this.ID);
this.checkResourceLevels(gameState, queues);
this.assignToFoundations(gameState);
if (this.constructing)
{
let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position());
if(owner !== 0 && !gameState.isPlayerAlly(owner))
{
// we're in enemy territory. If we're too close from the enemy, destroy us.
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
{
if (cc.owner() !== owner)
continue;
if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000)
continue;
this.anchor.destroy();
gameState.ai.HQ.resetBaseCache();
break;
}
}
}
else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()]))
--this.neededDefenders;
if (gameState.ai.elapsedTime > this.timeNextIdleCheck &&
(gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2))
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
// check if workers can find something useful to do
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
Engine.ProfileStop();
};
m.BaseManager.prototype.Serialize = function()
{
return {
"ID": this.ID,
"anchorId": this.anchorId,
"accessIndex": this.accessIndex,
"maxDistResourceSquare": this.maxDistResourceSquare,
"constructing": this.constructing,
"gatherers": this.gatherers,
"neededDefenders": this.neededDefenders,
"territoryIndices": this.territoryIndices,
"timeNextIdleCheck": this.timeNextIdleCheck
};
};
m.BaseManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
this[key] = data[key];
this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined;
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/chatHelper.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/chatHelper.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/chatHelper.js (revision 20600)
@@ -1,242 +1,242 @@
var PETRA = function(m)
{
m.launchAttackMessages = {
"hugeAttack": [
markForTranslation("I am starting a massive military campaign against %(_player_)s, come and join me."),
markForTranslation("I have set up a huge army to crush %(_player_)s. Join me and you will have your share of the loot.")
],
"other": [
markForTranslation("I am launching an attack against %(_player_)s."),
markForTranslation("I have just sent an army against %(_player_)s.")
]
};
m.answerRequestAttackMessages = {
"join": [
markForTranslation("Let me regroup my army and I will then join you against %(_player_)s."),
markForTranslation("I am finishing preparations to attack %(_player_)s.")
],
"decline": [
markForTranslation("Sorry, I do not have enough soldiers currently; but my next attack will target %(_player_)s."),
markForTranslation("Sorry, I still need to strengthen my army. However, I will attack %(_player_)s next.")
],
"other": [
markForTranslation("I cannot help you against %(_player_)s for the time being, I am planning to attack %(_player_2)s first.")
]
};
m.sentTributeMessages = [
markForTranslation("Here is a gift for you, %(_player_)s. Make good use of it."),
markForTranslation("I see you are in a bad situation, %(_player_)s. I hope this helps."),
markForTranslation("I can help you this time, %(_player_)s, but you should manage your resources more carefully in the future.")
];
m.requestTributeMessages = [
markForTranslation("I am in need of %(resource)s, can you help? I will make it up to you."),
markForTranslation("I would participate more efficiently in our common war effort if you could provide me some %(resource)s."),
markForTranslation("If you can spare me some %(resource)s, I will be able to strengthen my army.")
];
m.newTradeRouteMessages = [
markForTranslation("I have set up a new route with %(_player_)s. Trading will be profitable for all of us."),
markForTranslation("A new trade route is set up with %(_player_)s. Take your share of the profits.")
];
m.newDiplomacyMessages = {
"ally": [
markForTranslation("%(_player_)s and I are now allies.")
],
"neutral": [
markForTranslation("%(_player_)s and I are now neutral.")
],
"enemy": [
markForTranslation("%(_player_)s and I are now enemies.")
]
};
m.answerDiplomacyRequestMessages = {
"ally": {
"decline": [
markForTranslation("I cannot accept your offer to become allies, %(_player_)s.")
],
"declineSuggestNeutral": [
markForTranslation("I will not be your ally, %(_player_)s. However, I will consider a neutrality pact."),
markForTranslation("I reject your request for alliance, %(_player_)s, but we could become neutral."),
markForTranslation("%(_player_)s, only a neutrality agreement is conceivable to me.")
],
"declineRepeatedOffer": [
markForTranslation("Our previous alliance did not work out, %(_player_)s. I must decline your offer."),
markForTranslation("I won’t ally you again, %(_player_)s!"),
markForTranslation("No more alliances between us, %(_player_)s!"),
markForTranslation("Your request for peace means nothing to me anymore, %(_player_)s!"),
markForTranslation("My answer to your repeated peace proposal will remain war, %(_player_)s!")
],
"accept": [
markForTranslation("I will accept your offer to become allies, %(_player_)s. We will both benefit from this partnership."),
markForTranslation("An alliance between us is a good idea, %(_player_)s."),
markForTranslation("Let both of our people prosper from a peaceful association, %(_player_)s."),
markForTranslation("We have found common ground, %(_player_)s. I accept the alliance."),
markForTranslation("%(_player_)s, consider us allies from now on.")
],
"acceptWithTribute": [
markForTranslation("I will ally with you, %(_player_)s, but only if you send me a tribute of %(_amount_)s %(_resource_)s."),
markForTranslation("%(_player_)s, you must send me a tribute of %(_amount_)s %(_resource_)s before I accept an alliance with you."),
markForTranslation("Unless you send me %(_amount_)s %(_resource_)s, an alliance won’t be formed, %(_player_)s,")
],
"waitingForTribute": [
markForTranslation("%(_player_)s, my offer still stands. I will ally with you only if you send me a tribute of %(_amount_)s %(_resource_)s."),
markForTranslation("I’m still waiting for %(_amount_)s %(_resource_)s before accepting your alliance, %(_player_)s."),
markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.")
]
},
"neutral": {
"decline": [
markForTranslation("I will not become neutral with you, %(_player_)s."),
markForTranslation("%(_player_)s, I must decline your request for a neutrality pact.")
],
"declineRepeatedOffer": [
markForTranslation("Our previous neutrality agreement ended in failure, %(_player_)s; I will not consider another one.")
],
"accept": [
markForTranslation("I welcome your request for peace between our civilizations, %(_player_)s. I will accept."),
markForTranslation("%(_player_)s, I will accept your neutrality request. May both our civilizations benefit.")
],
"acceptWithTribute": [
markForTranslation("If you send me a tribute of %(_amount_)s %(_resource_)s, I will accept your neutrality request, %(_player_)s."),
markForTranslation("%(_player_)s, if you send me %(_amount_)s %(_resource_)s, I will accept a neutrality pact.")
],
"waitingForTribute": [
markForTranslation("%(_player_)s, I will not accept your neutrality request unless you tribute me %(_amount_)s %(_resource_)s soon."),
markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.")
]
}
};
m.sendDiplomacyRequestMessages = {
"ally": {
"sendRequest": [
markForTranslation("%(_player_)s, it would help both of our civilizations if we formed an alliance. If you become allies with me, I will respond in kind.")
],
"requestExpired": [
markForTranslation("%(_player_)s, my offer for an alliance has expired."),
markForTranslation("%(_player_)s, I have rescinded my previous offer for an alliance between us."),
]
},
"neutral": {
"sendRequest": [
markForTranslation("%(_player_)s, I would like to request a neutrality pact between our civilizations. If you become neutral with me, I will respond in kind."),
markForTranslation("%(_player_)s, it would be both to our benefit if we negotiated a neutrality pact. I will become neutral with you if you do the same.")
],
"requestExpired": [
markForTranslation("%(_player_)s, I have decided to revoke my offer for a neutrality pact."),
markForTranslation("%(_player_)s, as you have failed to respond to my request for peace between us, I have abrogated my offer."),
]
}
};
m.chatLaunchAttack = function(gameState, player, type)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/allies " + pickRandom(this.launchAttackMessages[type === "HugeAttack" ? "hugeAttack" : "other"]),
"translateMessage": true,
"translateParameters": ["_player_"],
"parameters": { "_player_": player }
});
};
m.chatAnswerRequestAttack = function(gameState, player, answer, other)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/allies " + pickRandom(this.answerRequestAttackMessages[answer]),
"translateMessage": true,
"translateParameters": answer != "other" ? ["_player_"] : ["_player_", "_player_2"],
"parameters": answer != "other" ? { "_player_": player } : { "_player_": player, "_player_2": other }
});
};
m.chatSentTribute = function(gameState, player)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/allies " + pickRandom(this.sentTributeMessages),
"translateMessage": true,
"translateParameters": ["_player_"],
"parameters": { "_player_": player }
});
};
m.chatRequestTribute = function(gameState, resource)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/allies " + pickRandom(this.requestTributeMessages),
"translateMessage": true,
"translateParameters": { "resource": "withinSentence" },
- "parameters": { "resource": gameState.sharedScript.resourceInfo.names[resource] }
+ "parameters": { "resource": Resources.GetNames()[resource] }
});
};
m.chatNewTradeRoute = function(gameState, player)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/allies " + pickRandom(this.newTradeRouteMessages),
"translateMessage": true,
"translateParameters": ["_player_"],
"parameters": { "_player_": player }
});
};
m.chatNewPhase = function(gameState, phase, status)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/allies " + pickRandom(this.newPhaseMessages[status]),
"translateMessage": true,
"translateParameters": ["phase"],
"parameters": { "phase": phase }
});
};
m.chatNewDiplomacy = function(gameState, player, newDiplomaticStance)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": pickRandom(this.newDiplomacyMessages[newDiplomaticStance]),
"translateMessage": true,
"translateParameters": ["_player_"],
"parameters": { "_player_": player }
});
};
m.chatAnswerRequestDiplomacy = function(gameState, player, requestType, response, requiredTribute)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/msg " + gameState.sharedScript.playersData[player].name + " " +
pickRandom(this.answerDiplomacyRequestMessages[requestType][response]),
"translateMessage": true,
"translateParameters": requiredTribute ? ["_amount_", "_resource_", "_player_"] : ["_player_"],
"parameters": requiredTribute ?
{ "_amount_": requiredTribute.wanted, "_resource_": requiredTribute.type, "_player_": player } :
{ "_player_": player }
});
};
m.chatNewRequestDiplomacy = function(gameState, player, requestType, status)
{
Engine.PostCommand(PlayerID, {
"type": "aichat",
"message": "/msg " + gameState.sharedScript.playersData[player].name + " " +
pickRandom(this.sendDiplomacyRequestMessages[requestType][status]),
"translateMessage": true,
"translateParameters": ["_player_"],
"parameters": { "_player_": player }
});
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 20600)
@@ -1,2649 +1,2649 @@
var PETRA = function(m)
{
/**
* Headquarters
* Deal with high level logic for the AI. Most of the interesting stuff gets done here.
* Some tasks:
* -defining RESS needs
* -BO decisions.
* > training workers
* > building stuff (though we'll send that to bases)
* -picking strategy (specific manager?)
* -diplomacy -> diplomacyManager
* -planning attacks -> attackManager
* -picking new CC locations.
*/
m.HQ = function(Config)
{
this.Config = Config;
this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i
// Cache various quantities.
this.turnCache = {};
// Some resources objects (will be filled in init)
this.wantedRates = {};
this.currentRates = {};
this.lastFailedGather = {};
this.firstBaseConfig = false;
// Workers configuration
this.targetNumWorkers = this.Config.Economy.targetNumWorkers;
this.supportRatio = this.Config.Economy.supportRatio;
this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime
this.towerStartTime = 0; // stone defense towers, will start as soon as available
this.towerLapseTime = this.Config.Military.towerLapseTime;
this.fortressStartTime = 0; // will start as soon as available
this.fortressLapseTime = this.Config.Military.fortressLapseTime;
this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive);
this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive);
this.baseManagers = [];
this.attackManager = new m.AttackManager(this.Config);
this.buildManager = new m.BuildManager();
this.defenseManager = new m.DefenseManager(this.Config);
this.tradeManager = new m.TradeManager(this.Config);
this.navalManager = new m.NavalManager(this.Config);
this.researchManager = new m.ResearchManager(this.Config);
this.diplomacyManager = new m.DiplomacyManager(this.Config);
this.garrisonManager = new m.GarrisonManager(this.Config);
this.gameTypeManager = new m.GameTypeManager(this.Config);
this.capturableTargets = new Map();
this.capturableTargetsTime = 0;
};
/** More initialisation for stuff that needs the gameState */
m.HQ.prototype.init = function(gameState, queues)
{
this.territoryMap = m.createTerritoryMap(gameState);
// initialize base map. Each pixel is a base ID, or 0 if not or not accessible
this.basesMap = new API3.Map(gameState.sharedScript, "territory");
// create borderMap: flag cells on the border of the map
// then this map will be completed with our frontier in updateTerritories
this.borderMap = m.createBorderMap(gameState);
// list of allowed regions
this.landRegions = {};
// try to determine if we have a water map
this.navalMap = false;
this.navalRegions = {};
- for (let res of gameState.sharedScript.resourceInfo.codes)
+ for (let res of Resources.GetCodes())
{
this.wantedRates[res] = 0;
this.currentRates[res] = 0;
}
this.treasures = gameState.getEntities().filter(function (ent) {
let type = ent.resourceSupplyType();
return type && type.generic === "treasure";
});
this.treasures.registerUpdates();
this.currentPhase = gameState.currentPhase();
this.decayingStructures = new Set();
};
/**
* initialization needed after deserialization (only called when deserialization)
*/
m.HQ.prototype.postinit = function(gameState)
{
// Rebuild the base maps from the territory indices of each base
this.basesMap = new API3.Map(gameState.sharedScript, "territory");
for (let base of this.baseManagers)
for (let j of base.territoryIndices)
this.basesMap.map[j] = base.ID;
for (let ent of gameState.getOwnEntities().values())
{
if (!ent.resourceDropsiteTypes() || ent.hasClass("Elephant"))
continue;
// Entities which have been built or have changed ownership after the last AI turn have no base.
// they will be dealt with in the next checkEvents
let baseID = ent.getMetadata(PlayerID, "base");
if (baseID === undefined)
continue;
let base = this.getBaseByID(baseID);
base.assignResourceToDropsite(gameState, ent);
}
this.updateTerritories(gameState);
};
/**
* returns the sea index linking regions 1 and region 2 (supposed to be different land region)
* otherwise return undefined
* for the moment, only the case land-sea-land is supported
*/
m.HQ.prototype.getSeaBetweenIndices = function (gameState, index1, index2)
{
let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2);
if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] === "water")
return path[1];
if (this.Config.debug > 1)
{
API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path));
API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1]));
API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2]));
}
return undefined;
};
m.HQ.prototype.checkEvents = function (gameState, events)
{
let addBase = false;
this.buildManager.checkEvents(gameState, events);
if (events.TerritoriesChanged.length || events.DiplomacyChanged.length)
this.updateTerritories(gameState);
for (let evt of events.DiplomacyChanged)
{
if (evt.player != PlayerID && evt.otherPlayer != PlayerID)
continue;
// Reset the entities collections which depend on diplomacy
gameState.resetOnDiplomacyChanged();
break;
}
for (let evt of events.Destroy)
{
// Let's check we haven't lost an important building here.
if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] &&
evt.metadata[PlayerID].base)
{
let ent = evt.entityObj;
if (ent.owner() != PlayerID)
continue;
// A new base foundation was created and destroyed on the same (AI) turn
if (evt.metadata[PlayerID].base == -1)
continue;
let base = this.getBaseByID(evt.metadata[PlayerID].base);
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
base.removeDropsite(gameState, ent);
if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true)
base.anchorLost(gameState, ent);
}
}
for (let evt of events.EntityRenamed)
{
let ent = gameState.getEntityById(evt.newentity);
if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
if (!base.anchorId || base.anchorId != evt.entity)
continue;
base.anchorId = evt.newentity;
base.anchor = ent;
}
for (let evt of events.Create)
{
// Let's check if we have a valuable foundation needing builders quickly
// (normal foundations are taken care in baseManager.assignToFoundations)
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined)
continue;
if (ent.getMetadata(PlayerID, "base") == -1)
{
// Okay so let's try to create a new base around this.
let newbase = new m.BaseManager(gameState, this.Config);
newbase.init(gameState, "unconstructed");
newbase.setAnchor(gameState, ent);
this.baseManagers.push(newbase);
// Let's get a few units from other bases there to build this.
let builders = this.bulkPickWorkers(gameState, newbase, 10);
if (builders !== false)
{
builders.forEach(function (worker) {
worker.setMetadata(PlayerID, "base", newbase.ID);
worker.setMetadata(PlayerID, "subrole", "builder");
worker.setMetadata(PlayerID, "target-foundation", ent.id());
});
}
}
}
for (let evt of events.ConstructionFinished)
{
if (evt.newentity == evt.entity) // repaired building
continue;
let ent = gameState.getEntityById(evt.newentity);
if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
base.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes())
base.assignResourceToDropsite(gameState, ent);
if (ent.getMetadata(PlayerID, "baseAnchor") === true)
{
if (base.constructing)
base.constructing = false;
addBase = true;
}
}
for (let evt of events.OwnershipChanged) // capture events
{
if (evt.from == PlayerID)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
base.removeDropsite(gameState, ent);
if (ent.getMetadata(PlayerID, "baseAnchor") === true)
base.anchorLost(gameState, ent);
}
if (evt.to != PlayerID)
continue;
let ent = gameState.getEntityById(evt.entity);
if (!ent)
continue;
if (ent.position())
ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(ent.position()));
if (ent.hasClass("Unit"))
{
m.getBestBase(gameState, ent).assignEntity(gameState, ent);
ent.setMetadata(PlayerID, "role", undefined);
ent.setMetadata(PlayerID, "subrole", undefined);
ent.setMetadata(PlayerID, "plan", undefined);
ent.setMetadata(PlayerID, "PartOfArmy", undefined);
if (ent.hasClass("Trader"))
{
ent.setMetadata(PlayerID, "role", "trader");
ent.setMetadata(PlayerID, "route", undefined);
}
if (ent.hasClass("Worker"))
{
ent.setMetadata(PlayerID, "role", "worker");
ent.setMetadata(PlayerID, "subrole", "idle");
}
if (ent.hasClass("Ship"))
ent.setMetadata(PlayerID, "sea", gameState.ai.accessibility.getAccessValue(ent.position(), true));
if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined)
ent.setMetadata(PlayerID, "plan", -1);
continue;
}
if (ent.hasClass("CivCentre")) // build a new base around it
{
let newbase = new m.BaseManager(gameState, this.Config);
if (ent.foundationProgress() !== undefined)
newbase.init(gameState, "unconstructed");
else
{
newbase.init(gameState, "captured");
addBase = true;
}
newbase.setAnchor(gameState, ent);
this.baseManagers.push(newbase);
newbase.assignEntity(gameState, ent);
}
else
{
// TODO should be reassigned later if a better base is captured
m.getBestBase(gameState, ent).assignEntity(gameState, ent);
if (ent.decaying())
{
if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true))
continue;
if (!this.decayingStructures.has(evt.entity))
this.decayingStructures.add(evt.entity);
}
}
}
// deal with the different rally points of training units: the rally point is set when the training starts
// for the time being, only autogarrison is used
for (let evt of events.TrainingStarted)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length)
continue;
let metadata = ent._entity.trainingQueue[0].metadata;
if (metadata && metadata.garrisonType)
ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison
else
ent.unsetRallyPoint();
}
for (let evt of events.TrainingFinished)
{
for (let entId of evt.entities)
{
let ent = gameState.getEntityById(entId);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent.position())
{
// we are autogarrisoned, check that the holder is registered in the garrisonManager
let holderId = ent.unitAIOrderData()[0].target;
let holder = gameState.getEntityById(holderId);
if (holder)
this.garrisonManager.registerHolder(gameState, holder);
}
else if (ent.getMetadata(PlayerID, "garrisonType"))
{
// we were supposed to be autogarrisoned, but this has failed (may-be full)
ent.setMetadata(PlayerID, "garrisonType", undefined);
}
// Check if this unit is no more needed in its attack plan
// (happen when the training ends after the attack is started or aborted)
let plan = ent.getMetadata(PlayerID, "plan");
if (plan !== undefined && plan >= 0)
{
let attack = this.attackManager.getPlan(plan);
if (!attack || attack.state !== "unexecuted")
ent.setMetadata(PlayerID, "plan", -1);
}
// Assign it immediately to something useful to do
if (ent.getMetadata(PlayerID, "role") === "worker")
{
let base;
if (ent.getMetadata(PlayerID, "base") === undefined)
{
base = m.getBestBase(gameState, ent);
base.assignEntity(gameState, ent);
}
else
base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
base.reassignIdleWorkers(gameState, [ent]);
base.workerObject.update(gameState, ent);
}
else if (ent.resourceSupplyType() && ent.position())
{
let type = ent.resourceSupplyType();
if (!type.generic)
continue;
let dropsites = gameState.getOwnDropsites(type.generic);
let pos = ent.position();
let access = gameState.ai.accessibility.getAccessValue(pos);
let distmin = Math.min();
let goal;
for (let dropsite of dropsites.values())
{
if (!dropsite.position() || dropsite.getMetadata(PlayerID, "access") !== access)
continue;
let dist = API3.SquareVectorDistance(pos, dropsite.position());
if (dist > distmin)
continue;
distmin = dist;
goal = dropsite.position();
}
if (goal)
ent.moveToRange(goal[0], goal[1]);
}
}
}
for (let evt of events.TerritoryDecayChanged)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined)
continue;
if (evt.to)
{
if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity))
continue;
if (!this.decayingStructures.has(evt.entity))
this.decayingStructures.add(evt.entity);
}
else if (ent.isGarrisonHolder())
this.garrisonManager.removeDecayingStructure(evt.entity);
}
if (addBase)
{
if (!this.firstBaseConfig)
{
// This is our first base, let us configure our starting resources
this.configFirstBase(gameState);
}
else
{
// Let us hope this new base will fix our possible resource shortage
this.saveResources = undefined;
this.saveSpace = undefined;
}
}
// Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties)
if (this.Config.difficulty < 2)
return;
for (let entId of this.decayingStructures)
{
let ent = gameState.getEntityById(entId);
if (ent && ent.decaying() && ent.isOwn(PlayerID))
{
let capture = ent.capturePoints();
if (!capture)
continue;
let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b);
if (captureRatio < 0.50)
continue;
let decayToGaia = true;
for (let i = 1; i < capture.length; ++i)
{
if (gameState.isPlayerAlly(i) || !capture[i])
continue;
decayToGaia = false;
break;
}
if (decayToGaia)
continue;
let ratioMax = 0.70 + randFloat(0., 0.1);
for (let evt of events.Attacked)
{
if (ent.id() != evt.target)
continue;
ratioMax = 0.85 + randFloat(0., 0.1);
break;
}
if (captureRatio > ratioMax)
continue;
ent.destroy();
}
this.decayingStructures.delete(entId);
}
};
/** Ensure that all requirements are met when phasing up*/
m.HQ.prototype.checkPhaseRequirements = function(gameState, queues)
{
if (gameState.getNumberOfPhases() == this.currentPhase)
return;
let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1);
let plan;
let queue;
for (let entityReq of requirements)
{
// Village requirements are met elsewhere by constructing more houses
if (entityReq.class === "Village" || entityReq.class === "NotField")
continue;
if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count)
continue;
switch (entityReq.class)
{
case "Town":
if (!queues.economicBuilding.hasQueuedUnits() &&
!queues.militaryBuilding.hasQueuedUnits() &&
!queues.defenseBuilding.hasQueuedUnits())
{
if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}_market"))
{
plan = new m.ConstructionPlan(gameState, "structures/{civ}_market");
queue = "economicBuilding";
break;
}
if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}_temple"))
{
plan = new m.ConstructionPlan(gameState, "structures/{civ}_temple");
queue = "economicBuilding";
break;
}
if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}_blacksmith"))
{
plan = new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith");
queue = "militaryBuilding";
break;
}
if (this.canBuild(gameState, "structures/{civ}_defense_tower"))
{
plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower");
queue = "defenseBuilding";
break;
}
}
break;
default:
// All classes not dealt with inside vanilla game.
// We put them for the time being on the economic queue, except if wonder
queue = entityReq.class === "Wonder" ? "wonder" : "economicBuilding";
if (!queues[queue].hasQueuedUnits())
{
let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]);
if (structure && this.canBuild(gameState, structure))
plan = new m.ConstructionPlan(gameState, structure);
}
}
if (plan)
{
if (queue == "wonder")
{
gameState.ai.queueManager.changePriority("majorTech", 400);
plan.queueToReset = "majorTech";
}
else
{
gameState.ai.queueManager.changePriority(queue, 1000);
plan.queueToReset = queue;
}
queues[queue].addPlan(plan);
return;
}
}
};
/** Called by any "phase" research plan once it's started */
m.HQ.prototype.OnPhaseUp = function(gameState, phase)
{
};
/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */
m.HQ.prototype.trainMoreWorkers = function(gameState, queues)
{
// default template
let requirementsDef = [ ["costsResource", 1, "food"] ];
let classesDef = ["Support", "Worker"];
let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef);
// counting the workers that aren't part of a plan
let numberOfWorkers = 0; // all workers
let numberOfSupports = 0; // only support workers (i.e. non fighting)
gameState.getOwnUnits().forEach (function (ent) {
if (ent.getMetadata(PlayerID, "role") === "worker" && ent.getMetadata(PlayerID, "plan") === undefined)
{
++numberOfWorkers;
if (ent.hasClass("Support"))
++numberOfSupports;
}
});
let numberInTraining = 0;
gameState.getOwnTrainingFacilities().forEach(function(ent) {
for (let item of ent.trainingQueue())
{
numberInTraining += item.count;
if (item.metadata && item.metadata.role && item.metadata.role === "worker" && item.metadata.plan === undefined)
{
numberOfWorkers += item.count;
if (item.metadata.support)
numberOfSupports += item.count;
}
}
});
// Anticipate the optimal batch size when this queue will start
// and adapt the batch size of the first and second queued workers to the present population
// to ease a possible recovery if our population was drastically reduced by an attack
// (need to go up to second queued as it is accounted in queueManager)
let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10));
if (queues.villager.plans[0])
{
queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size);
if (queues.villager.plans[1])
queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size);
}
if (queues.citizenSoldier.plans[0])
{
queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size);
if (queues.citizenSoldier.plans[1])
queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size);
}
let numberOfQueuedSupports = queues.villager.countQueuedUnits();
let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits();
let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers;
let numberTotal = numberOfWorkers + numberQueued;
if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10)
return;
if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 &&
this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))))
return;
if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15)
return;
// Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers
// with supportRatio fraction of support units. But we want to have more support (less cost) at startup.
// So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers))
// This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers
// and gives a ratio alfa at startup.
let supportRatio = this.supportRatio;
let alpha = 0.85;
if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")))
supportRatio = Math.min(this.supportRatio, 0.1);
if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length)
alpha = 0.7;
if (gameState.isCeasefireActive())
alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180;
let supportMax = supportRatio * this.targetNumWorkers;
let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax));
let template;
if (numberOfSupports + numberOfQueuedSupports > supportNum)
{
let requirements;
if (numberTotal < 45)
requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ];
else
requirements = [ ["strength", 1] ];
let classes = ["CitizenSoldier", "Infantry"];
// We want at least 33% ranged and 33% melee
classes.push(pickRandom(["Ranged", "Melee", "Infantry"]));
template = this.findBestTrainableUnit(gameState, classes, requirements);
}
// If the template variable is empty, the default unit (Support unit) will be used
// base "0" means automatic choice of base
if (!template && templateDef)
queues.villager.addPlan(new m.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size));
else if (template)
queues.citizenSoldier.addPlan(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size));
};
/** picks the best template based on parameters and classes */
m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements)
{
let units;
if (classes.indexOf("Hero") !== -1)
units = gameState.findTrainableUnits(classes, []);
else if (classes.indexOf("Siege") !== -1) // We do not want siege tower as AI does not know how to use it
units = gameState.findTrainableUnits(classes, ["SiegeTower"]);
else // We do not want hero when not explicitely specified
units = gameState.findTrainableUnits(classes, ["Hero"]);
if (units.length === 0)
return undefined;
let parameters = requirements.slice();
let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory
let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources
for (let type in remainingResources)
{
if (availableResources[type] > 800)
continue;
if (remainingResources[type] > 800)
continue;
let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2;
let toAdd = true;
for (let param of parameters)
{
if (param[0] !== "costsResource" || param[2] !== type)
continue;
param[1] = Math.min( param[1], costsResource );
toAdd = false;
break;
}
if (toAdd)
parameters.push( [ "costsResource", costsResource, type ] );
}
units.sort(function(a, b) {
let aCost = 1 + a[1].costSum();
let bCost = 1 + b[1].costSum();
let aValue = 0.1;
let bValue = 0.1;
for (let param of parameters)
{
if (param[0] == "strength")
{
aValue += m.getMaxStrength(a[1]) * param[1];
bValue += m.getMaxStrength(b[1]) * param[1];
}
else if (param[0] == "siegeStrength")
{
aValue += m.getMaxStrength(a[1], "Structure") * param[1];
bValue += m.getMaxStrength(b[1], "Structure") * param[1];
}
else if (param[0] == "speed")
{
aValue += a[1].walkSpeed() * param[1];
bValue += b[1].walkSpeed() * param[1];
}
else if (param[0] == "costsResource")
{
// requires a third parameter which is the resource
if (a[1].cost()[param[2]])
aValue *= param[1];
if (b[1].cost()[param[2]])
bValue *= param[1];
}
else if (param[0] == "canGather")
{
// checking against wood, could be anything else really.
if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"])
aValue *= param[1];
if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"])
bValue *= param[1];
}
else
API3.warn(" trainMoreUnits avec non prevu " + uneval(param));
}
return -aValue/aCost + bValue/bCost;
});
return units[0][0];
};
/**
* returns an entity collection of workers through BaseManager.pickBuilders
* TODO: when same accessIndex, sort by distance
*/
m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number)
{
let accessIndex = baseRef.accessIndex;
if (!accessIndex)
return false;
// sorting bases by whether they are on the same accessindex or not.
let baseBest = this.baseManagers.slice().sort(function (a,b) {
if (a.accessIndex == accessIndex && b.accessIndex != accessIndex)
return -1;
else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex)
return 1;
return 0;
});
let needed = number;
let workers = new API3.EntityCollection(gameState.sharedScript);
for (let base of baseBest)
{
if (base.ID === baseRef.ID)
continue;
base.pickBuilders(gameState, workers, needed);
if (workers.length < number)
needed = number - workers.length;
else
break;
}
if (!workers.length)
return false;
return workers;
};
m.HQ.prototype.getTotalResourceLevel = function(gameState)
{
let total = {};
- for (let res of gameState.sharedScript.resourceInfo.codes)
+ for (let res of Resources.GetCodes())
total[res] = 0;
for (let base of this.baseManagers)
for (let res in total)
total[res] += base.getResourceLevel(gameState, res);
return total;
};
/**
* returns the current gather rate
* This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that.
*/
m.HQ.prototype.GetCurrentGatherRates = function(gameState)
{
if (!this.turnCache.gatherRates)
{
for (let res in this.currentRates)
this.currentRates[res] = 0.5 * this.GetTCResGatherer(res);
for (let base of this.baseManagers)
base.getGatherRates(gameState, this.currentRates);
for (let res in this.currentRates)
{
if (this.currentRates[res] < 0)
{
if (this.Config.debug > 0)
API3.warn("Petra: current rate for " + res + " < 0 with " + this.GetTCResGatherer(res) + " moved gatherers");
this.currentRates[res] = 0;
}
}
this.turnCache.gatherRates = true;
}
return this.currentRates;
};
/**
* Pick the resource which most needs another worker
* How this works:
* We get the rates we would want to have to be able to deal with our plans
* We get our current rates
* We compare; we pick the one where the discrepancy is highest.
* Need to balance long-term needs and possible short-term needs.
*/
m.HQ.prototype.pickMostNeededResources = function(gameState)
{
this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState);
let currentRates = this.GetCurrentGatherRates(gameState);
let needed = [];
for (let res in this.wantedRates)
needed.push({ "type": res, "wanted": this.wantedRates[res], "current": currentRates[res] });
needed.sort((a, b) => {
let va = Math.max(0, a.wanted - a.current) / (a.current + 1);
let vb = Math.max(0, b.wanted - b.current) / (b.current + 1);
// If they happen to be equal (generally this means "0" aka no need), make it fair.
if (va === vb)
return a.current - b.current;
return vb - va;
});
return needed;
};
/**
* Returns the best position to build a new Civil Centre
* Whose primary function would be to reach new resources of type "resource".
*/
m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic)
{
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then look for a good spot.
Engine.ProfileStart("findEconomicCCLocation");
// obstruction map
let obstructions = m.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"])));
let ccList = [];
for (let cc of ccEnts.values())
ccList.push({"pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner())});
let dpList = [];
for (let dp of dpEnts.values())
dpList.push({"pos": dp.position()});
let bestIdx;
let bestVal;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let scale = 250 * 250;
let proxyAccess;
let nbShips = this.navalManager.transportShips.length;
if (proximity) // this is our first base
{
// if our first base, ensure room around
radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize);
// scale is the typical scale at which we want to find a location for our first base
// look for bigger scale if we start from a ship (access < 2) or from a small island
let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize;
proxyAccess = gameState.ai.accessibility.getAccessValue(proximity);
if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000)
scale = 400 * 400;
}
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.territoryMap.getOwnerIndex(j) !== 0)
continue;
// with enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// we require that it is accessible
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
if (proxyAccess && nbShips === 0 && proxyAccess !== index)
continue;
let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps
// checking distance to other cc
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
if (proximity) // this is our first cc, let's do it near our units
norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale;
else
{
let minDist = Math.min();
for (let cc of ccList)
{
let dist = API3.SquareVectorDistance(cc.pos, pos);
if (dist < 14000) // Reject if too near from any cc
{
norm = 0;
break;
}
if (!cc.ally)
continue;
if (dist < 40000) // Reject if too near from an allied cc
{
norm = 0;
break;
}
if (dist < 62000) // Disfavor if quite near an allied cc
norm *= 0.5;
if (dist < minDist)
minDist = dist;
}
if (norm === 0)
continue;
if (minDist > 170000 && !this.navalMap) // Reject if too far from any allied cc (not connected)
continue;
if (minDist > 130000) // Disfavor if quite far from any allied cc
{
if (this.navalMap)
{
if (minDist > 250000)
norm *= 0.5;
else
norm *= 0.8;
}
else
norm *= 0.5;
}
for (let dp of dpList)
{
let dist = API3.SquareVectorDistance(dp.pos, pos);
if (dist < 3600)
{
norm = 0;
break;
}
else if (dist < 6400)
norm *= 0.5;
}
if (norm === 0)
continue;
}
if (this.borderMap.map[j] & m.fullBorder_Mask) // disfavor the borders of the map
norm *= 0.5;
let val = 2*gameState.sharedScript.ccResourceMaps[resource].map[j];
for (let res in gameState.sharedScript.resourceMaps)
if (res !== "food")
val += gameState.sharedScript.ccResourceMaps[res].map[j];
val *= norm;
if (bestVal !== undefined && val < bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = val;
bestIdx = i;
}
Engine.ProfileStop();
if (bestVal === undefined)
return false;
let cut = 60;
if (fromStrategic || proximity) // be less restrictive
cut = 30;
if (this.Config.debug > 1)
API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal);
// not good enough.
if (bestVal < cut)
return false;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex === indexIdx)
continue;
let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
if (sea !== undefined)
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x, z];
};
/**
* Returns the best position to build a new Civil Centre
* Whose primary function would be to assure territorial continuity with our allies
*/
m.HQ.prototype.findStrategicCCLocation = function(gameState, template)
{
// This builds a map. The procedure is fairly simple.
// We minimize the Sum((dist-300)**2) where the sum is on the three nearest allied CC
// with the constraints that all CC have dist > 200 and at least one have dist < 400
// This needs at least 2 CC. Otherwise, go back to economic CC.
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let ccList = [];
let numAllyCC = 0;
for (let cc of ccEnts.values())
{
let ally = gameState.isPlayerAlly(cc.owner());
ccList.push({"pos": cc.position(), "ally": ally});
if (ally)
++numAllyCC;
}
if (numAllyCC < 2)
return this.findEconomicCCLocation(gameState, template, "wood", undefined, true);
Engine.ProfileStart("findStrategicCCLocation");
// obstruction map
let obstructions = m.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestVal;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let currentVal, delta;
let distcc0, distcc1, distcc2;
let favoredDistance = template.hasClass("Colony") ? 220 : 280;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.territoryMap.getOwnerIndex(j) !== 0)
continue;
// with enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// we require that it is accessible
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
// checking distances to other cc
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
let minDist = Math.min();
distcc0 = undefined;
for (let cc of ccList)
{
let dist = API3.SquareVectorDistance(cc.pos, pos);
if (dist < 14000) // Reject if too near from any cc
{
minDist = 0;
break;
}
if (!cc.ally)
continue;
if (dist < 62000) // Reject if quite near from ally cc
{
minDist = 0;
break;
}
if (dist < minDist)
minDist = dist;
if (!distcc0 || dist < distcc0)
{
distcc2 = distcc1;
distcc1 = distcc0;
distcc0 = dist;
}
else if (!distcc1 || dist < distcc1)
{
distcc2 = distcc1;
distcc1 = dist;
}
else if (!distcc2 || dist < distcc2)
distcc2 = dist;
}
if (minDist < 1 || minDist > 170000 && !this.navalMap)
continue;
delta = Math.sqrt(distcc0) - favoredDistance;
currentVal = delta*delta;
delta = Math.sqrt(distcc1) - favoredDistance;
currentVal += delta*delta;
if (distcc2)
{
delta = Math.sqrt(distcc2) - favoredDistance;
currentVal += delta*delta;
}
// disfavor border of the map
if (this.borderMap.map[j] & m.fullBorder_Mask)
currentVal += 10000;
if (bestVal !== undefined && currentVal > bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = currentVal;
bestIdx = i;
}
if (this.Config.debug > 1)
API3.warn("We've found a strategic base with bestVal = " + bestVal);
Engine.ProfileStop();
if (bestVal === undefined)
return undefined;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex === indexIdx)
continue;
let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
if (sea !== undefined)
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x, z];
};
/**
* Returns the best position to build a new market: if the allies already have a market, build it as far as possible
* from it, although not in our border to be able to defend it easily. If no allied market, our second market will
* follow the same logic.
* To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance
* for performance reasons.
*/
m.HQ.prototype.findMarketLocation = function(gameState, template)
{
let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray();
if (!markets.length)
markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray();
if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan
return [-1, -1, -1, 0];
// obstruction map
let obstructions = m.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestJdx;
let bestVal;
let bestDistSq;
let bestGainMult;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let isNavalMarket = template.hasClass("NavalMarket");
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let traderTemplatesGains = gameState.getTraderTemplatesGains();
for (let j = 0; j < this.territoryMap.length; ++j)
{
// do not try on the narrow border of our territory
if (this.borderMap.map[j] & m.narrowFrontier_Mask)
continue;
if (this.basesMap.map[j] === 0) // only in our territory
continue;
// with enough room around to build the market
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// checking distances to other markets
let maxVal = 0;
let maxDistSq;
let maxGainMult;
let gainMultiplier;
for (let market of markets)
{
if (isNavalMarket && market.hasClass("NavalMarket"))
{
if (m.getSeaAccess(gameState, market) !== gameState.ai.accessibility.getAccessValue(pos, true))
continue;
gainMultiplier = traderTemplatesGains.navalGainMultiplier;
}
else if (m.getLandAccess(gameState, market) === index &&
!m.isLineInsideEnemyTerritory(gameState, market.position(), pos))
gainMultiplier = traderTemplatesGains.landGainMultiplier;
else
continue;
if (!gainMultiplier)
continue;
let distSq = API3.SquareVectorDistance(market.position(), pos);
if (gainMultiplier * distSq > maxVal)
{
maxVal = gainMultiplier * distSq;
maxDistSq = distSq;
maxGainMult = gainMultiplier;
}
}
if (maxVal === 0)
continue;
if (bestVal !== undefined && maxVal < bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = maxVal;
bestDistSq = maxDistSq;
bestGainMult = maxGainMult;
bestIdx = i;
bestJdx = j;
}
if (this.Config.debug > 1)
API3.warn("We found a market position with bestVal = " + bestVal);
if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan
return [-1, -1, -1, 0];
let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize));
if (this.Config.debug > 1)
API3.warn("this would give a trading gain of " + expectedGain);
// do not keep it if gain is too small, except if this is our first BarterMarket
if (expectedGain < this.tradeManager.minimalGain ||
expectedGain < 8 && (!template.hasClass("BarterMarket") || gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()))
return false;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.basesMap.map[bestJdx], expectedGain];
};
/**
* Returns the best position to build defensive buildings (fortress and towers)
* Whose primary function is to defend our borders
*/
m.HQ.prototype.findDefensiveLocation = function(gameState, template)
{
// We take the point in our territory which is the nearest to any enemy cc
// but requiring a minimal distance with our other defensive structures
// and not in range of any enemy defensive structure to avoid building under fire.
let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray();
let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals
{
enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory())
enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities())
return undefined;
}
enemyStructures = enemyStructures.toEntityArray();
let wonderMode = gameState.getGameType() === "wonder";
let wonderDistmin;
let wonders;
if (wonderMode)
{
wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray();
wonderMode = wonders.length !== 0;
if (wonderMode)
wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius());
}
// obstruction map
let obstructions = m.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestJdx;
let bestVal;
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let isTower = template.hasClass("Tower");
let isFortress = template.hasClass("Fortress");
let radius;
if (isFortress)
radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize);
else
radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (!wonderMode)
{
// do not try if well inside or outside territory
if (!(this.borderMap.map[j] & m.fullFrontier_Mask))
continue;
if (this.borderMap.map[j] & m.largeFrontier_Mask && isTower)
continue;
}
if (this.basesMap.map[j] === 0) // inaccessible cell
continue;
// with enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// checking distances to other structures
let minDist = Math.min();
let dista = 0;
if (wonderMode)
{
dista = API3.SquareVectorDistance(wonders[0].position(), pos);
if (dista < wonderDistmin)
continue;
dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder
}
for (let str of enemyStructures)
{
if (str.foundationProgress() !== undefined)
continue;
let strPos = str.position();
if (!strPos)
continue;
let dist = API3.SquareVectorDistance(strPos, pos);
if (dist < 6400) // TODO check on true attack range instead of this 80*80
{
minDist = -1;
break;
}
if (str.hasClass("CivCentre") && dist + dista < minDist)
minDist = dist + dista;
}
if (minDist < 0)
continue;
let cutDist = 900; // 30*30 TODO maybe increase it
for (let str of ownStructures)
{
let strPos = str.position();
if (!strPos)
continue;
if (API3.SquareVectorDistance(strPos, pos) < cutDist)
{
minDist = -1;
break;
}
}
if (minDist < 0 || minDist === Math.min())
continue;
if (bestVal !== undefined && minDist > bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = minDist;
bestIdx = i;
bestJdx = j;
}
if (bestVal === undefined)
return undefined;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.basesMap.map[bestJdx]];
};
m.HQ.prototype.buildTemple = function(gameState, queues)
{
// at least one market (which have the same queue) should be build before any temple
if (queues.economicBuilding.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Temple", true).hasEntities() ||
!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())
return;
// Try to build a temple earlier if in regicide to recruit healer guards
if (this.currentPhase < 3 && gameState.getGameType() !== "regicide")
return;
if (!this.canBuild(gameState, "structures/{civ}_temple"))
return;
queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_temple"));
};
m.HQ.prototype.buildMarket = function(gameState, queues)
{
if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}_market"))
return;
if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket"))
{
if (!this.navalMap && !queues.economicBuilding.paused)
{
// Put available resources in this market when not a naval map
let queueManager = gameState.ai.queueManager;
let cost = queues.economicBuilding.plans[0].getCost();
queueManager.setAccounts(gameState, cost, "economicBuilding");
if (!queueManager.canAfford("economicBuilding", cost))
{
for (let q in queueManager.queues)
{
if (q === "economicBuilding")
continue;
queueManager.transferAccounts(cost, q, "economicBuilding");
if (queueManager.canAfford("economicBuilding", cost))
break;
}
}
}
return;
}
if (gameState.getPopulation() < this.Config.Economy.popForMarket)
return;
gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding);
let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market");
plan.queueToReset = "economicBuilding";
queues.economicBuilding.addPlan(plan);
};
/** Build a farmstead */
m.HQ.prototype.buildFarmstead = function(gameState, queues)
{
// Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs)
if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities())
return;
// Wait to have at least one dropsite and house before the farmstead
if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities())
return;
if (!gameState.getOwnEntitiesByClass("House", true).hasEntities())
return;
if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood"))
return;
if (!this.canBuild(gameState, "structures/{civ}_farmstead"))
return;
queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead"));
};
/**
* Try to build a wonder when required
* force = true when called from the gameTypeManager in case of Wonder mode
*/
m.HQ.prototype.buildWonder = function(gameState, queues, force = false)
{
if (queues.wonder && queues.wonder.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}_wonder"))
return;
if (!force)
{
let templateName = gameState.applyCiv("structures/{civ}_wonder");
if (gameState.isTemplateDisabled(templateName))
return;
let template = gameState.getTemplate(templateName);
if (!template)
return;
// Check that we have enough resources to start thinking to build a wonder
let cost = template.cost();
let resources = gameState.getResources();
let highLevel = 0;
let lowLevel = 0;
for (let res in cost)
{
if (resources[res] && resources[res] > 0.7 * cost[res])
++highLevel;
else if (!resources[res] || resources[res] < 0.3 * cost[res])
++lowLevel;
}
if (highLevel == 0 || lowLevel > 1)
return;
}
queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder"));
};
/** Build a corral, and train animals there */
m.HQ.prototype.manageCorral = function(gameState, queues)
{
if (queues.corral.hasQueuedUnits())
return;
let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length;
if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) &&
nCorral < this.currentPhase && gameState.getPopulation() > 30*nCorral)
{
if (this.canBuild(gameState, "structures/{civ}_corral"))
{
queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral"));
return;
}
if (!nCorral)
return;
}
// And train some animals
for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values())
{
if (corral.foundationProgress() !== undefined)
continue;
let trainables = corral.trainableEntities();
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.isHuntable())
continue;
let count = gameState.countEntitiesByType(trainable, true);
for (let item of corral.trainingQueue())
count += item.count;
if (count > nCorral)
continue;
queues.corral.addPlan(new m.TrainingPlan(gameState, trainable, { "trainer": corral.id() }));
return;
}
}
};
/**
* build more houses if needed.
* kinda ugly, lots of special cases to both build enough houses but not tooo many…
*/
m.HQ.prototype.buildMoreHouses = function(gameState, queues)
{
if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_house")) ||
gameState.getPopulationMax() <= gameState.getPopulationLimit())
return;
let numPlanned = queues.house.length();
if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80)
{
let plan = new m.ConstructionPlan(gameState, "structures/{civ}_house");
// change the starting condition according to the situation.
plan.goRequirement = "houseNeeded";
queues.house.addPlan(plan);
}
if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length)
{
let houseTemplateName = gameState.applyCiv("structures/{civ}_house");
let houseTemplate = gameState.getTemplate(houseTemplateName);
let needed = 0;
for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing))
{
if (!houseTemplate.hasClass(entityReq.class))
continue;
let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length;
if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName))
{
if (this.Config.debug > 1)
API3.warn("no room to place a house ... try to be less restrictive");
this.buildManager.setBuildable(houseTemplateName);
this.requireHouses = true;
}
needed = Math.max(needed, entityReq.count - count);
}
let houseQueue = queues.house.plans;
for (let i = 0; i < numPlanned; ++i)
if (houseQueue[i].isGo(gameState))
--needed;
else if (needed > 0)
{
houseQueue[i].goRequirement = undefined;
--needed;
}
}
if (this.requireHouses)
{
let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house"));
if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req =>
!houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count))
this.requireHouses = undefined;
}
// When population limit too tight
// - if no room to build, try to improve with technology
// - otherwise increase temporarily the priority of houses
let house = gameState.applyCiv("structures/{civ}_house");
let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length;
let popBonus = gameState.getTemplate(house).getPopulationBonus();
let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - gameState.getPopulation();
let priority;
if (freeSlots < 5)
{
if (this.buildManager.isUnbuildable(gameState, house))
{
if (this.Config.debug > 1)
API3.warn("no room to place a house ... try to improve with technology");
this.researchManager.researchPopulationBonus(gameState, queues);
}
else
priority = 2*this.Config.priorities.house;
}
else
priority = this.Config.priorities.house;
if (priority && priority != gameState.ai.queueManager.getPriority("house"))
gameState.ai.queueManager.changePriority("house", priority);
};
/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */
m.HQ.prototype.checkBaseExpansion = function(gameState, queues)
{
if (queues.civilCentre.hasQueuedUnits())
return;
// First build one cc if all have been destroyed
if (this.numPotentialBases() == 0)
{
this.buildFirstBase(gameState);
return;
}
// Then expand if we have not enough room available for buildings
if (this.buildManager.numberMissingRoom(gameState) > 1)
{
if (this.Config.debug > 2)
API3.warn("try to build a new base because not enough room to build ");
this.buildNewBase(gameState, queues);
return;
}
// If we've already planned to phase up, wait a bit before trying to expand
if (this.phasing)
return;
// Finally expand if we have lots of units (threshold depending on the aggressivity value)
let activeBases = this.numActiveBases();
let numUnits = gameState.getOwnUnits().length;
let numvar = 10 * (1 - this.Config.personality.aggressive);
if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50)
{
if (this.Config.debug > 2)
API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs");
this.buildNewBase(gameState, queues);
}
};
m.HQ.prototype.buildNewBase = function(gameState, queues, resource)
{
if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))
return false;
if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits())
return false;
let template;
// We require at least one of this civ civCentre as they may allow specific units or techs
let hasOwnCC = false;
for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values())
{
if (ent.owner() !== PlayerID || ent.templateName() !== gameState.applyCiv("structures/{civ}_civil_centre"))
continue;
hasOwnCC = true;
break;
}
if (hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony"))
template = "structures/{civ}_military_colony";
else if (this.canBuild(gameState, "structures/{civ}_civil_centre"))
template = "structures/{civ}_civil_centre";
else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony"))
template = "structures/{civ}_military_colony";
else
return false;
// base "-1" means new base.
if (this.Config.debug > 1)
API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource);
queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource }));
return true;
};
/** Deals with building fortresses and towers along our border with enemies. */
m.HQ.prototype.buildDefenses = function(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits())
return;
if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3))))
{
// try to build fortresses
if (this.canBuild(gameState, "structures/{civ}_fortress"))
{
let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length;
if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10*numFortresses)*this.fortressLapseTime + this.fortressStartTime) &&
numFortresses < this.numActiveBases() + 1 + this.extraFortresses &&
numFortresses < Math.floor(gameState.getPopulation() / 25) &&
gameState.getOwnFoundationsByClass("Fortress").length < 2)
{
this.fortressStartTime = gameState.ai.elapsedTime;
if (!numFortresses)
gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding);
let plan = new m.ConstructionPlan(gameState, "structures/{civ}_fortress");
plan.queueToReset = "defenseBuilding";
queues.defenseBuilding.addPlan(plan);
return;
}
}
}
if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower"))
{
let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers
let towerLapseTime = this.saveResource ? (1 + 0.5*numTowers) * this.towerLapseTime : this.towerLapseTime;
if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime)
{
this.fortStartTime = gameState.ai.elapsedTime;
queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_sentry_tower"));
}
return;
}
if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower"))
return;
let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length;
let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime;
if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1*numTowers)*towerLapseTime + this.towerStartTime) &&
numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers &&
numTowers < Math.floor(gameState.getPopulation() / 8) &&
gameState.getOwnFoundationsByClass("DefenseTower").length < 3)
{
this.towerStartTime = gameState.ai.elapsedTime;
if (numTowers > 2 * this.numActiveBases() + 3)
gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7*this.Config.priorities.defenseBuilding));
let plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower");
plan.queueToReset = "defenseBuilding";
queues.defenseBuilding.addPlan(plan);
}
};
m.HQ.prototype.buildBlacksmith = function(gameState, queues)
{
if (gameState.getPopulation() < this.Config.Military.popForBlacksmith ||
queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length)
return;
// build a market before the blacksmith
if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())
return;
if (this.canBuild(gameState, "structures/{civ}_blacksmith"))
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith"));
};
/**
* Deals with constructing military buildings (barracks, stables…)
* They are mostly defined by Config.js. This is unreliable since changes could be done easily.
*/
m.HQ.prototype.constructTrainingBuildings = function(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits())
return;
let numBarracks = this.canBuild(gameState, "structures/{civ}_barracks") ? gameState.getOwnEntitiesByClass("Barracks", true).length : -1;
let numStables = this.canBuild(gameState, "structures/{civ}_stables") ? gameState.getOwnEntitiesByClass("Stables", true).length : -1;
if (this.saveResources && numBarracks != 0)
return;
if (gameState.getPopulation() > this.Config.Military.popForBarracks1 ||
this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5)
{
// first barracks and stables.
if (numBarracks == 0)
{
gameState.ai.queueManager.changePriority("militaryBuilding", 2*this.Config.priorities.militaryBuilding);
let plan = new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "militaryBase": true });
plan.queueToReset = "militaryBuilding";
queues.militaryBuilding.addPlan(plan);
return;
}
if (numStables == 0)
{
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_stables", { "militaryBase": true }));
return;
}
// Second barracks and stables
if (numBarracks == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2)
{
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "militaryBase": true }));
return;
}
if (numStables == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2)
{
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_stables", { "militaryBase": true }));
return;
}
// Then 3rd barracks/stables if needed
if (numBarracks == 2 && numStables == -1 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 30)
{
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "militaryBase": true }));
return;
}
if (numBarracks == -1 && numStables == 2 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 30)
{
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_stables", { "militaryBase": true }));
return;
}
}
if (this.saveResources)
return;
if (this.currentPhase < 3)
return;
if (this.canBuild(gameState, "structures/{civ}_elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStables", true).hasEntities())
{
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_elephant_stables", { "militaryBase": true }));
return;
}
if (this.canBuild(gameState, "structures/{civ}_workshop") && !gameState.getOwnEntitiesByClass("Workshop", true).hasEntities())
{
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_workshop", { "militaryBase": true }));
return;
}
if (gameState.getPopulation() < 80 || !this.bAdvanced.length)
return;
//build advanced military buildings
let nAdvanced = 0;
for (let advanced of this.bAdvanced)
nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true);
if (!nAdvanced || nAdvanced < this.bAdvanced.length && gameState.getPopulation() > 110)
{
for (let advanced of this.bAdvanced)
{
if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced))
continue;
let template = gameState.getTemplate(advanced);
if (!template)
continue;
if (template.hasDefensiveFire() || template.trainableEntities())
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "militaryBase": true }));
else // not a military building, but still use this queue
queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced));
return;
}
}
};
/**
* Find base nearest to ennemies for military buildings.
*/
m.HQ.prototype.findBestBaseForMilitary = function(gameState)
{
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray();
let bestBase;
let enemyFound = false;
let distMin = Math.min();
for (let cce of ccEnts)
{
if (gameState.isPlayerAlly(cce.owner()))
continue;
if (enemyFound && !gameState.isPlayerEnemy(cce.owner()))
continue;
let access = m.getLandAccess(gameState, cce);
let isEnemy = gameState.isPlayerEnemy(cce.owner());
for (let cc of ccEnts)
{
if (cc.owner() != PlayerID)
continue;
if (m.getLandAccess(gameState, cc) != access)
continue;
let dist = API3.SquareVectorDistance(cc.position(), cce.position());
if (!enemyFound && isEnemy)
enemyFound = true;
else if (dist > distMin)
continue;
bestBase = cc.getMetadata(PlayerID, "base");
distMin = dist;
}
}
return bestBase;
};
/**
* train with highest priority ranged infantry in the nearest civil centre from a given set of positions
* and garrison them there for defense
*/
m.HQ.prototype.trainEmergencyUnits = function(gameState, positions)
{
if (gameState.ai.queues.emergency.hasQueuedUnits())
return false;
let civ = gameState.getPlayerCiv();
// find nearest base anchor
let distcut = 20000;
let nearestAnchor;
let distmin;
for (let pos of positions)
{
let access = gameState.ai.accessibility.getAccessValue(pos);
// check nearest base anchor
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.anchor.getMetadata(PlayerID, "access") !== access)
continue;
if (!base.anchor.trainableEntities(civ)) // base still in construction
continue;
let queue = base.anchor._entity.trainingQueue;
if (queue)
{
let time = 0;
for (let item of queue)
if (item.progress > 0 || item.metadata && item.metadata.garrisonType)
time += item.timeRemaining;
if (time/1000 > 5)
continue;
}
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (nearestAnchor && dist > distmin)
continue;
distmin = dist;
nearestAnchor = base.anchor;
}
}
if (!nearestAnchor || distmin > distcut)
return false;
// We will choose randomly ranged and melee units, except when garrisonHolder is full
// in which case we prefer melee units
let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor);
if (nearestAnchor._entity.trainingQueue)
{
for (let item of nearestAnchor._entity.trainingQueue)
{
if (item.metadata && item.metadata.garrisonType)
numGarrisoned += item.count;
else if (!item.progress && (!item.metadata || !item.metadata.trainer))
nearestAnchor.stopProduction(item.id);
}
}
let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() &&
nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints();
let rangedWanted = randBool() && autogarrison;
let total = gameState.getResources();
let templateFound;
let trainables = nearestAnchor.trainableEntities(civ);
let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses();
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier"))
continue;
if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses))
continue;
if (!total.canAfford(new API3.Resources(template.cost())))
continue;
templateFound = [trainable, template];
if (template.hasClass("Ranged") === rangedWanted)
break;
}
if (!templateFound)
return false;
// Check first if we can afford it without touching the other accounts
// and if not, take some of other accounted resources
// TODO sort the queues to be substracted
let queueManager = gameState.ai.queueManager;
let cost = new API3.Resources(templateFound[1].cost());
queueManager.setAccounts(gameState, cost, "emergency");
if (!queueManager.canAfford("emergency", cost))
{
for (let q in queueManager.queues)
{
if (q === "emergency")
continue;
queueManager.transferAccounts(cost, q, "emergency");
if (queueManager.canAfford("emergency", cost))
break;
}
}
let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() };
if (autogarrison)
metadata.garrisonType = "protection";
gameState.ai.queues.emergency.addPlan(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1));
return true;
};
m.HQ.prototype.canBuild = function(gameState, structure, debug = false)
{
let type = gameState.applyCiv(structure);
if (this.buildManager.isUnbuildable(gameState, type))
return false;
if (gameState.isTemplateDisabled(type))
{
this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled");
return false;
}
let template = gameState.getTemplate(type);
if (!template)
{
this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate");
return false;
}
if (!template.available(gameState))
{
this.buildManager.setUnbuildable(gameState, type, 30, "tech");
return false;
}
if (!this.buildManager.hasBuilder(type))
{
this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder");
return false;
}
if (this.numActiveBases() < 1)
{
// if no base, check that we can build outside our territory
let buildTerritories = template.buildTerritories();
if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own"))
{
this.buildManager.setUnbuildable(gameState, type, 180, "room");
return false;
}
}
// build limits
let limits = gameState.getEntityLimits();
let category = template.buildCategory();
if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category])
{
this.buildManager.setUnbuildable(gameState, type, 90, "limit");
return false;
}
return true;
};
m.HQ.prototype.updateTerritories = function(gameState)
{
const around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ];
let alliedVictory = gameState.getAlliedVictory();
let passabilityMap = gameState.getPassabilityMap();
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let insideSmall = Math.round(45 / cellSize);
let insideLarge = Math.round(80 / cellSize); // should be about the range of towers
let expansion = 0;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.borderMap.map[j] & m.outside_Mask)
continue;
if (this.borderMap.map[j] & m.fullFrontier_Mask)
this.borderMap.map[j] &= ~m.fullFrontier_Mask; // reset the frontier
if (this.territoryMap.getOwnerIndex(j) != PlayerID)
{
// If this tile was already accounted, remove it
if (this.basesMap.map[j] === 0)
continue;
let base = this.getBaseByID(this.basesMap.map[j]);
let index = base.territoryIndices.indexOf(j);
if (index == -1)
{
API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]);
continue;
}
base.territoryIndices.splice(index, 1);
this.basesMap.map[j] = 0;
}
else
{
// Update the frontier
let ix = j%width;
let iz = Math.floor(j/width);
let onFrontier = false;
for (let a of around)
{
let jx = ix + Math.round(insideSmall*a[0]);
if (jx < 0 || jx >= width)
continue;
let jz = iz + Math.round(insideSmall*a[1]);
if (jz < 0 || jz >= width)
continue;
if (this.borderMap.map[jx+width*jz] & m.outside_Mask)
continue;
let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
if (territoryOwner !== PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
{
this.borderMap.map[j] |= m.narrowFrontier_Mask;
break;
}
jx = ix + Math.round(insideLarge*a[0]);
if (jx < 0 || jx >= width)
continue;
jz = iz + Math.round(insideLarge*a[1]);
if (jz < 0 || jz >= width)
continue;
if (this.borderMap.map[jx+width*jz] & m.outside_Mask)
continue;
territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
if (territoryOwner !== PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
onFrontier = true;
}
if (onFrontier && !(this.borderMap.map[j] & m.narrowFrontier_Mask))
this.borderMap.map[j] |= m.largeFrontier_Mask;
// If this tile was not already accounted, add it
if (this.basesMap.map[j] !== 0)
continue;
let landPassable = false;
let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap);
let access;
for (let k of ind)
{
if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]])
continue;
landPassable = true;
access = gameState.ai.accessibility.landPassMap[k];
break;
}
if (!landPassable)
continue;
let distmin = Math.min();
let baseID;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex != access)
continue;
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (dist >= distmin)
continue;
distmin = dist;
baseID = base.ID;
}
if (!baseID)
continue;
this.getBaseByID(baseID).territoryIndices.push(j);
this.basesMap.map[j] = baseID;
expansion++;
}
}
if (!expansion)
return;
// We've increased our territory, so we may have some new room to build
this.buildManager.resetMissingRoom(gameState);
// And if sufficient expansion, check if building a new market would improve our present trade routes
let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize;
if (expansion * cellArea > 960)
this.tradeManager.routeProspection = true;
};
/**
* returns the base corresponding to baseID
*/
m.HQ.prototype.getBaseByID = function(baseID)
{
for (let base of this.baseManagers)
if (base.ID === baseID)
return base;
API3.warn("Petra error: no base found with ID " + baseID);
return undefined;
};
/**
* returns the number of bases with a cc
* ActiveBases includes only those with a built cc
* PotentialBases includes also those with a cc in construction
*/
m.HQ.prototype.numActiveBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.active;
};
m.HQ.prototype.numPotentialBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.potential;
};
m.HQ.prototype.updateBaseCache = function()
{
this.turnCache.base = { "active": 0, "potential": 0 };
for (let base of this.baseManagers)
{
if (!base.anchor)
continue;
++this.turnCache.base.potential;
if (base.anchor.foundationProgress() === undefined)
++this.turnCache.base.active;
}
};
m.HQ.prototype.resetBaseCache = function()
{
this.turnCache.base = undefined;
};
/**
* Count gatherers returning resources in the number of gatherers of resourceSupplies
* to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps).
*/
m.HQ.prototype.assignGatherers = function()
{
for (let base of this.baseManagers)
{
for (let worker of base.workers.values())
{
if (worker.unitAIState().split(".")[1] !== "RETURNRESOURCE")
continue;
let orders = worker.unitAIOrderData();
if (orders.length < 2 || !orders[1].target || orders[1].target !== worker.getMetadata(PlayerID, "supply"))
continue;
this.AddTCGatherer(orders[1].target);
}
}
};
m.HQ.prototype.isDangerousLocation = function(gameState, pos, radius)
{
return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius);
};
/** Check that the chosen position is not too near from an invading army */
m.HQ.prototype.isNearInvadingArmy = function(pos)
{
for (let army of this.defenseManager.armies)
if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000)
return true;
return false;
};
m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0)
{
if (!this.turnCache.firingStructures)
this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures());
for (let ent of this.turnCache.firingStructures.values())
{
let range = radius + ent.attackRange("Ranged").max;
if (API3.SquareVectorDistance(ent.position(), pos) < range*range)
return true;
}
return false;
};
/** Compute the capture strength of all units attacking a capturable target */
m.HQ.prototype.updateCaptureStrength = function(gameState)
{
this.capturableTargets.clear();
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.canCapture())
continue;
let state = ent.unitAIState();
if (!state || !state.split(".")[1] || state.split(".")[1] !== "COMBAT")
continue;
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].target)
continue;
let targetId = orderData[0].target;
let target = gameState.getEntityById(targetId);
if (!target || !target.isCapturable() || !ent.canCapture(target))
continue;
if (!this.capturableTargets.has(targetId))
this.capturableTargets.set(targetId, {
"strength": ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"),
"ents": new Set([ent.id()])
});
else
{
let capturableTarget = this.capturableTargets.get(target.id());
capturableTarget.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture");
capturableTarget.ents.add(ent.id());
}
}
for (let [targetId, capturableTarget] of this.capturableTargets)
{
let target = gameState.getEntityById(targetId);
let allowCapture;
for (let entId of capturableTarget.ents)
{
let ent = gameState.getEntityById(entId);
if (allowCapture === undefined)
allowCapture = m.allowCapture(gameState, ent, target);
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].attackType)
continue;
if ((orderData[0].attackType === "Capture") !== allowCapture)
ent.attack(targetId, allowCapture);
}
}
this.capturableTargetsTime = gameState.ai.elapsedTime;
};
/** Some functions that register that we assigned a gatherer to a resource this turn */
/** add a gatherer to the turn cache for this supply. */
m.HQ.prototype.AddTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined)
++this.turnCache.resourceGatherer[supplyID];
else
{
if (!this.turnCache.resourceGatherer)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = 1;
}
};
/** remove a gatherer to the turn cache for this supply. */
m.HQ.prototype.RemoveTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
--this.turnCache.resourceGatherer[supplyID];
else
{
if (!this.turnCache.resourceGatherer)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = -1;
}
};
m.HQ.prototype.GetTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
return this.turnCache.resourceGatherer[supplyID];
return 0;
};
/** The next two are to register that we assigned a gatherer to a resource this turn. */
m.HQ.prototype.AddTCResGatherer = function(resource)
{
if (this.turnCache["resourceGatherer-" + resource])
++this.turnCache["resourceGatherer-" + resource];
else
this.turnCache["resourceGatherer-" + resource] = 1;
this.turnCache.gatherRates = false;
};
m.HQ.prototype.GetTCResGatherer = function(resource)
{
if (this.turnCache["resourceGatherer-" + resource])
return this.turnCache["resourceGatherer-" + resource];
return 0;
};
/**
* Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around)
*/
m.HQ.prototype.isDefendable = function(ent)
{
if (!this.turnCache.numAround)
this.turnCache.numAround = {};
if (this.turnCache.numAround[ent.id()] === undefined)
this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130);
return +this.turnCache.numAround[ent.id()] > 8;
};
/**
* Some functions are run every turn
* Others once in a while
*/
m.HQ.prototype.update = function(gameState, queues, events)
{
Engine.ProfileStart("Headquarters update");
this.turnCache = {};
this.territoryMap = m.createTerritoryMap(gameState);
this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).hasEntities();
// TODO find a better way to update
if (this.currentPhase != gameState.currentPhase())
{
if (this.Config.debug > 0)
API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase +
" to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime +
" phasing " + this.phasing);
this.currentPhase = gameState.currentPhase();
// In principle, this.phasing should be already reset to 0 when starting the research
// but this does not work in case of an autoResearch tech
if (this.phasing)
this.phasing = 0;
}
/* if (this.Config.debug > 1)
{
gameState.getOwnUnits().forEach (function (ent) {
if (!ent.position())
return;
m.dumpEntity(ent);
});
} */
this.checkEvents(gameState, events);
if (this.phasing)
this.checkPhaseRequirements(gameState, queues);
else
this.researchManager.checkPhase(gameState, queues);
if (this.numActiveBases() > 0)
{
if (gameState.ai.playedTurn % 4 == 0)
this.trainMoreWorkers(gameState, queues);
if (gameState.ai.playedTurn % 4 == 1)
this.buildMoreHouses(gameState,queues);
if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2)
this.buildFarmstead(gameState, queues);
if (this.needCorral && gameState.ai.playedTurn % 4 == 3)
this.manageCorral(gameState, queues);
if (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1)
this.researchManager.update(gameState, queues);
}
if (this.numPotentialBases() < 1 ||
this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1)
this.checkBaseExpansion(gameState, queues);
if (this.currentPhase > 1)
{
if (!this.canBarter)
this.buildMarket(gameState, queues);
if (!this.saveResources)
{
this.buildBlacksmith(gameState, queues);
this.buildTemple(gameState, queues);
}
if (gameState.ai.playedTurn % 30 === 0 &&
gameState.getPopulation() > 0.9 * gameState.getPopulationMax())
this.buildWonder(gameState, queues, false);
}
this.tradeManager.update(gameState, events, queues);
this.garrisonManager.update(gameState, events);
this.defenseManager.update(gameState, events);
if (gameState.ai.playedTurn % 3 == 0)
this.constructTrainingBuildings(gameState, queues);
if (this.Config.difficulty > 0)
this.buildDefenses(gameState, queues);
this.assignGatherers();
for (let i = 0; i < this.baseManagers.length; ++i)
if ((i + gameState.ai.playedTurn)%this.baseManagers.length == 0)
this.baseManagers[i].update(gameState, queues, events);
this.navalManager.update(gameState, queues, events);
if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits))
this.attackManager.update(gameState, queues, events);
this.diplomacyManager.update(gameState, events);
this.gameTypeManager.update(gameState, events, queues);
// We update the capture strength at the end as it can change attack orders
if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3)
this.updateCaptureStrength(gameState);
Engine.ProfileStop();
};
m.HQ.prototype.Serialize = function()
{
let properties = {
"phasing": this.phasing,
"wantedRates": this.wantedRates,
"currentRates": this.currentRates,
"lastFailedGather": this.lastFailedGather,
"firstBaseConfig": this.firstBaseConfig,
"supportRatio": this.supportRatio,
"targetNumWorkers": this.targetNumWorkers,
"fortStartTime": this.fortStartTime,
"towerStartTime": this.towerStartTime,
"fortressStartTime": this.fortressStartTime,
"bAdvanced": this.bAdvanced,
"saveResources": this.saveResources,
"saveSpace": this.saveSpace,
"needCorral": this.needCorral,
"needFarm": this.needFarm,
"needFish": this.needFish,
"canExpand": this.canExpand,
"canBuildUnits": this.canBuildUnits,
"navalMap": this.navalMap,
"landRegions": this.landRegions,
"navalRegions": this.navalRegions,
"decayingStructures": this.decayingStructures,
"capturableTargets": this.capturableTargets,
"capturableTargetsTime": this.capturableTargetsTime
};
let baseManagers = [];
for (let base of this.baseManagers)
baseManagers.push(base.Serialize());
if (this.Config.debug == -100)
{
API3.warn(" HQ serialization ---------------------");
API3.warn(" properties " + uneval(properties));
API3.warn(" baseManagers " + uneval(baseManagers));
API3.warn(" attackManager " + uneval(this.attackManager.Serialize()));
API3.warn(" buildManager " + uneval(this.buildManager.Serialize()));
API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize()));
API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize()));
API3.warn(" navalManager " + uneval(this.navalManager.Serialize()));
API3.warn(" researchManager " + uneval(this.researchManager.Serialize()));
API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize()));
API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize()));
API3.warn(" gameTypeManager " + uneval(this.gameTypeManager.Serialize()));
}
return {
"properties": properties,
"baseManagers": baseManagers,
"attackManager": this.attackManager.Serialize(),
"buildManager": this.buildManager.Serialize(),
"defenseManager": this.defenseManager.Serialize(),
"tradeManager": this.tradeManager.Serialize(),
"navalManager": this.navalManager.Serialize(),
"researchManager": this.researchManager.Serialize(),
"diplomacyManager": this.diplomacyManager.Serialize(),
"garrisonManager": this.garrisonManager.Serialize(),
"gameTypeManager": this.gameTypeManager.Serialize(),
};
};
m.HQ.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
this.baseManagers = [];
for (let base of data.baseManagers)
{
// the first call to deserialize set the ID base needed by entitycollections
let newbase = new m.BaseManager(gameState, this.Config);
newbase.Deserialize(gameState, base);
newbase.init(gameState);
newbase.Deserialize(gameState, base);
this.baseManagers.push(newbase);
}
this.navalManager = new m.NavalManager(this.Config);
this.navalManager.init(gameState, true);
this.navalManager.Deserialize(gameState, data.navalManager);
this.attackManager = new m.AttackManager(this.Config);
this.attackManager.Deserialize(gameState, data.attackManager);
this.attackManager.init(gameState);
this.attackManager.Deserialize(gameState, data.attackManager);
this.buildManager = new m.BuildManager();
this.buildManager.Deserialize(data.buildManager);
this.defenseManager = new m.DefenseManager(this.Config);
this.defenseManager.Deserialize(gameState, data.defenseManager);
this.tradeManager = new m.TradeManager(this.Config);
this.tradeManager.init(gameState);
this.tradeManager.Deserialize(gameState, data.tradeManager);
this.researchManager = new m.ResearchManager(this.Config);
this.researchManager.Deserialize(data.researchManager);
this.diplomacyManager = new m.DiplomacyManager(this.Config);
this.diplomacyManager.Deserialize(data.diplomacyManager);
this.garrisonManager = new m.GarrisonManager(this.Config);
this.garrisonManager.Deserialize(data.garrisonManager);
this.gameTypeManager = new m.GameTypeManager(this.Config);
this.gameTypeManager.Deserialize(data.gameTypeManager);
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 20600)
@@ -1,599 +1,599 @@
var PETRA = function(m)
{
// This takes the input queues and picks which items to fund with resources until no more resources are left to distribute.
//
// Currently this manager keeps accounts for each queue, split between the 4 main resources
//
// Each time resources are available (ie not in any account), it is split between the different queues
// Mostly based on priority of the queue, and existing needs.
// Each turn, the queue Manager checks if a queue can afford its next item, then it does.
//
// A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it
// If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will
// be able to benefit form the 500 food (even if they only needed food).
// This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic.
//
// It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues
// get some part of the total, and if all queues have 70% of their needs, nothing gets done
// Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting.
//
// This system should be improved. It's probably not flexible enough.
m.QueueManager = function(Config, queues)
{
this.Config = Config;
this.queues = queues;
this.priorities = {};
for (let i in Config.priorities)
this.priorities[i] = Config.priorities[i];
this.accounts = {};
// the sorting is updated on priority change.
this.queueArrays = [];
for (let q in this.queues)
{
this.accounts[q] = new API3.Resources();
this.queueArrays.push([q, this.queues[q]]);
}
let priorities = this.priorities;
this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]);
};
m.QueueManager.prototype.getAvailableResources = function(gameState)
{
let resources = gameState.getResources();
for (let key in this.queues)
resources.subtract(this.accounts[key]);
return resources;
};
m.QueueManager.prototype.getTotalAccountedResources = function()
{
let resources = new API3.Resources();
for (let key in this.queues)
resources.add(this.accounts[key]);
return resources;
};
m.QueueManager.prototype.currentNeeds = function(gameState)
{
let needed = new API3.Resources();
//queueArrays because it's faster.
for (let q of this.queueArrays)
{
let queue = q[1];
if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState))
continue;
let costs = queue.plans[0].getCost();
needed.add(costs);
}
// get out current resources, not removing accounts.
let current = gameState.getResources();
- for (let res of needed.types)
+ for (let res of Resources.GetCodes())
needed[res] = Math.max(0, needed[res] - current[res]);
return needed;
};
// calculate the gather rates we'd want to be able to start all elements in our queues
// TODO: many things.
m.QueueManager.prototype.wantedGatherRates = function(gameState)
{
// default values for first turn when we have not yet set our queues.
if (gameState.ai.playedTurn === 0)
{
let ret = {};
- for (let res of gameState.sharedScript.resourceInfo.codes)
+ for (let res of Resources.GetCodes())
ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default;
return ret;
}
// get out current resources, not removing accounts.
let current = gameState.getResources();
// short queue is the first item of a queue, assumed to be ready in 30s
// medium queue is the second item of a queue, assumed to be ready in 60s
// long queue contains the isGo=false items, assumed to be ready in 300s
let totalShort = {};
let totalMedium = {};
let totalLong = {};
- for (let res of gameState.sharedScript.resourceInfo.codes)
+ for (let res of Resources.GetCodes())
{
totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default;
totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default;
totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default;
}
let total;
//queueArrays because it's faster.
for (let q of this.queueArrays)
{
let queue = q[1];
if (queue.paused)
continue;
for (let j = 0; j < queue.length(); ++j)
{
if (j > 1)
break;
let cost = queue.plans[j].getCost();
if (queue.plans[j].isGo(gameState))
{
if (j === 0)
total = totalShort;
else
total = totalMedium;
}
else
total = totalLong;
for (let type in total)
total[type] += cost[type];
if (!queue.plans[j].isGo(gameState))
break;
}
}
// global rates
let rates = {};
let diff;
- for (let res of gameState.sharedScript.resourceInfo.codes)
+ for (let res of Resources.GetCodes())
{
if (current[res] > 0)
{
diff = Math.min(current[res], totalShort[res]);
totalShort[res] -= diff;
current[res] -= diff;
if (current[res] > 0)
{
diff = Math.min(current[res], totalMedium[res]);
totalMedium[res] -= diff;
current[res] -= diff;
if (current[res] > 0)
totalLong[res] -= Math.min(current[res], totalLong[res]);
}
}
rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300;
}
return rates;
};
m.QueueManager.prototype.printQueues = function(gameState)
{
let numWorkers = 0;
gameState.getOwnUnits().forEach (function (ent) {
if (ent.getMetadata(PlayerID, "role") === "worker" && ent.getMetadata(PlayerID, "plan") === undefined)
numWorkers++;
});
API3.warn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + numWorkers);
for (let i in this.queues)
{
let q = this.queues[i];
if (q.hasQueuedUnits())
{
API3.warn(i + ": ( with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")");
API3.warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6)));
}
for (let plan of q.plans)
{
let qStr = " " + plan.type + " ";
if (plan.number)
qStr += "x" + plan.number;
qStr += " isGo " + plan.isGo(gameState);
API3.warn(qStr);
}
}
API3.warn("Accounts");
for (let p in this.accounts)
API3.warn(p + ": " + uneval(this.accounts[p]));
API3.warn("Current Resources: " + uneval(gameState.getResources()));
API3.warn("Available Resources: " + uneval(this.getAvailableResources(gameState)));
API3.warn("Wanted Gather Rates: " + uneval(this.wantedGatherRates(gameState)));
API3.warn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState)));
API3.warn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState)));
API3.warn("------------------------------------");
};
m.QueueManager.prototype.clear = function()
{
for (let i in this.queues)
this.queues[i].empty();
};
/**
* set accounts of queue i from the unaccounted resources
*/
m.QueueManager.prototype.setAccounts = function(gameState, cost, i)
{
let available = this.getAvailableResources(gameState);
- for (let res of this.accounts[i].types)
+ for (let res of Resources.GetCodes())
{
if (this.accounts[i][res] >= cost[res])
continue;
this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]);
}
};
/**
* transfer accounts from queue i to queue j
*/
m.QueueManager.prototype.transferAccounts = function(cost, i, j)
{
- for (let res of this.accounts[i].types)
+ for (let res of Resources.GetCodes())
{
if (this.accounts[j][res] >= cost[res])
continue;
let diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]);
this.accounts[i][res] -= diff;
this.accounts[j][res] += diff;
}
};
/**
* distribute the resources between the different queues according to their priorities
*/
m.QueueManager.prototype.distributeResources = function(gameState)
{
let availableRes = this.getAvailableResources(gameState);
- for (let res of availableRes.types)
+ for (let res of Resources.GetCodes())
{
if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering)
{
let total = gameState.getResources()[res];
let scale = total / (total - availableRes[res]);
availableRes[res] = total;
for (let j in this.queues)
{
this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]);
availableRes[res] -= this.accounts[j][res];
}
}
if (!availableRes[res])
{
this.switchResource(gameState, res);
continue;
}
let totalPriority = 0;
let tempPrio = {};
let maxNeed = {};
// Okay so this is where it gets complicated.
// If a queue requires "res" for the next elements (in the queue)
// And the account is not high enough for it.
// Then we add it to the total priority.
// To try and be clever, we don't want a long queue to hog all resources. So two things:
// -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2).
// -queues accounts are capped at "resources for the first + 60% of the next"
// This avoids getting a high priority queue with many elements hogging all of one resource
// uselessly while it awaits for other resources.
for (let j in this.queues)
{
// returns exactly the correct amount, ie 0 if we're not go.
let queueCost = this.queues[j].maxAccountWanted(gameState, 0.6);
if (this.queues[j].hasQueuedUnits() && this.accounts[j][res] < queueCost[res] && !this.queues[j].paused)
{
// adding us to the list of queues that need an update.
tempPrio[j] = this.priorities[j];
maxNeed[j] = queueCost[res] - this.accounts[j][res];
// if we have enough of that resource for our first item in the queue, diminish our priority.
if (this.accounts[j][res] >= this.queues[j].getNext().getCost()[res])
tempPrio[j] /= 2;
if (tempPrio[j])
totalPriority += tempPrio[j];
}
else if (this.accounts[j][res] > queueCost[res])
{
availableRes[res] += this.accounts[j][res] - queueCost[res];
this.accounts[j][res] = queueCost[res];
}
}
// Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available"
// But we'll sometimes allow less if that would overflow.
let available = availableRes[res];
let missing = false;
for (let j in tempPrio)
{
// we'll add at much what can be allowed to this queue.
let toAdd = Math.floor(availableRes[res] * tempPrio[j]/totalPriority);
if (toAdd >= maxNeed[j])
toAdd = maxNeed[j];
else
missing = true;
this.accounts[j][res] += toAdd;
maxNeed[j] -= toAdd;
available -= toAdd;
}
if (missing && available > 0) // distribute the rest (due to floor) in any queue
{
for (let j in tempPrio)
{
let toAdd = Math.min(maxNeed[j], available);
this.accounts[j][res] += toAdd;
available -= toAdd;
if (available <= 0)
break;
}
}
if (available < 0)
API3.warn("Petra: problem with remaining " + res + " in queueManager " + available);
}
};
m.QueueManager.prototype.switchResource = function(gameState, res)
{
// We have no available resources, see if we can't "compact" them in one queue.
// compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it.
// TODO: this isn't perfect compression.
for (let j in this.queues)
{
if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused)
continue;
let queue = this.queues[j];
let queueCost = queue.maxAccountWanted(gameState, 0);
if (this.accounts[j][res] >= queueCost[res])
continue;
for (let i in this.queues)
{
if (i === j)
continue;
let otherQueue = this.queues[i];
if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0)
continue;
if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res])
continue;
let diff = queueCost[res] - this.accounts[j][res];
this.accounts[j][res] += diff;
this.accounts[i][res] -= diff;
++otherQueue.switched;
if (this.Config.debug > 2)
API3.warn ("switching queue " + res + " from " + i + " to " + j + " in amount " + diff);
break;
}
}
};
// Start the next item in the queue if we can afford it.
m.QueueManager.prototype.startNextItems = function(gameState)
{
for (let q of this.queueArrays)
{
let name = q[0];
let queue = q[1];
if (queue.hasQueuedUnits() && !queue.paused)
{
let item = queue.getNext();
if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState))
{
// canStart may update the cost because of the costMultiplier so we must check it again
if (this.accounts[name].canAfford(item.getCost()))
{
this.finishingTime = gameState.ai.elapsedTime;
this.accounts[name].subtract(item.getCost());
queue.startNext(gameState);
queue.switched = 0;
}
}
}
else if (!queue.hasQueuedUnits())
{
this.accounts[name].reset();
queue.switched = 0;
}
}
};
m.QueueManager.prototype.update = function(gameState)
{
Engine.ProfileStart("Queue Manager");
for (let i in this.queues)
{
this.queues[i].check(gameState); // do basic sanity checks on the queue
if (this.priorities[i] > 0)
continue;
API3.warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities));
this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero.
}
// Pause or unpause queues depending on the situation
this.checkPausedQueues(gameState);
// Let's assign resources to plans that need them
this.distributeResources(gameState);
// Start the next item in the queue if we can afford it.
this.startNextItems(gameState);
if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0)
this.printQueues(gameState);
Engine.ProfileStop();
};
// Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training
m.QueueManager.prototype.checkPausedQueues = function(gameState)
{
let numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker");
let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2);
for (let q in this.queues)
{
let toBePaused = false;
if (gameState.ai.HQ.numPotentialBases() == 0)
toBePaused = q != "dock" && q != "civilCentre";
else if (numWorkers < workersMin / 3)
toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency";
else if (numWorkers < workersMin * 2 / 3)
toBePaused = q == "civilCentre" || q == "economicBuilding" ||
q == "militaryBuilding" || q == "defenseBuilding" || q == "healer" ||
q == "majorTech" || q == "minorTech" || q.indexOf("plan_") != -1;
else if (numWorkers < workersMin)
toBePaused = q == "civilCentre" || q == "defenseBuilding" ||
q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1;
if (toBePaused)
{
if (q == "field" && gameState.ai.HQ.needFarm &&
!gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities())
toBePaused = false;
if (q == "corral" && gameState.ai.HQ.needCorral &&
!gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities())
toBePaused = false;
if (q == "dock" && gameState.ai.HQ.needFish &&
!gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).hasEntities())
toBePaused = false;
if (q == "ships" && gameState.ai.HQ.needFish &&
!gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byClass("FishingBoat")).hasEntities())
toBePaused = false;
}
let queue = this.queues[q];
if (!queue.paused && toBePaused)
{
queue.paused = true;
this.accounts[q].reset();
}
else if (queue.paused && !toBePaused)
queue.paused = false;
// And reduce the batch sizes of attack queues
if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0])
{
queue.plans[0].number = 1;
if (queue.plans[1])
queue.plans[1].number = 1;
}
}
};
m.QueueManager.prototype.canAfford = function(queue, cost)
{
if (!this.accounts[queue])
return false;
return this.accounts[queue].canAfford(cost);
};
m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts)
{
if (!this.queues[queue])
return;
this.queues[queue].paused = true;
if (scrapAccounts)
this.accounts[queue].reset();
};
m.QueueManager.prototype.unpauseQueue = function(queue)
{
if (this.queues[queue])
this.queues[queue].paused = false;
};
m.QueueManager.prototype.pauseAll = function(scrapAccounts, but)
{
for (let q in this.queues)
{
if (q == but)
continue;
if (scrapAccounts)
this.accounts[q].reset();
this.queues[q].paused = true;
}
};
m.QueueManager.prototype.unpauseAll = function(but)
{
for (let q in this.queues)
if (q != but)
this.queues[q].paused = false;
};
m.QueueManager.prototype.addQueue = function(queueName, priority)
{
if (this.queues[queueName] !== undefined)
return;
this.queues[queueName] = new m.Queue();
this.priorities[queueName] = priority;
this.accounts[queueName] = new API3.Resources();
this.queueArrays = [];
for (let q in this.queues)
this.queueArrays.push([q, this.queues[q]]);
let priorities = this.priorities;
this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]);
};
m.QueueManager.prototype.removeQueue = function(queueName)
{
if (this.queues[queueName] === undefined)
return;
delete this.queues[queueName];
delete this.priorities[queueName];
delete this.accounts[queueName];
this.queueArrays = [];
for (let q in this.queues)
this.queueArrays.push([q, this.queues[q]]);
let priorities = this.priorities;
this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]);
};
m.QueueManager.prototype.getPriority = function(queueName)
{
return this.priorities[queueName];
};
m.QueueManager.prototype.changePriority = function(queueName, newPriority)
{
if (this.Config.debug > 1)
API3.warn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + " to " + newPriority);
if (this.queues[queueName] !== undefined)
this.priorities[queueName] = newPriority;
let priorities = this.priorities;
this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]);
};
m.QueueManager.prototype.Serialize = function()
{
let accounts = {};
let queues = {};
for (let q in this.queues)
{
queues[q] = this.queues[q].Serialize();
accounts[q] = this.accounts[q].Serialize();
if (this.Config.debug == -100)
API3.warn("queueManager serialization: queue " + q + " >>> " +
uneval(queues[q]) + " with accounts " + uneval(accounts[q]));
}
return {
"priorities": this.priorities,
"queues": queues,
"accounts": accounts
};
};
m.QueueManager.prototype.Deserialize = function(gameState, data)
{
this.priorities = data.priorities;
this.queues = {};
this.accounts = {};
// the sorting is updated on priority change.
this.queueArrays = [];
for (let q in data.queues)
{
this.queues[q] = new m.Queue();
this.queues[q].Deserialize(gameState, data.queues[q]);
this.accounts[q] = new API3.Resources();
this.accounts[q].Deserialize(data.accounts[q]);
this.queueArrays.push([q, this.queues[q]]);
}
this.queueArrays.sort((a,b) => data.priorities[b[0]] - data.priorities[a[0]]);
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 20600)
@@ -1,706 +1,706 @@
var PETRA = function(m)
{
/**
* Manage the trade
*/
m.TradeManager = function(Config)
{
this.Config = Config;
this.tradeRoute = undefined;
this.potentialTradeRoute = undefined;
this.routeProspection = false;
this.targetNumTraders = this.Config.Economy.targetNumTraders;
this.minimalGain = 3;
this.warnedAllies = {};
};
m.TradeManager.prototype.init = function(gameState)
{
this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader"));
this.traders.registerUpdates();
};
m.TradeManager.prototype.hasTradeRoute = function()
{
return this.tradeRoute !== undefined;
};
m.TradeManager.prototype.assignTrader = function(ent)
{
ent.setMetadata(PlayerID, "role", "trader");
this.traders.updateEnt(ent);
};
m.TradeManager.prototype.trainMoreTraders = function(gameState, queues)
{
if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits())
return;
let numTraders = this.traders.length;
let numSeaTraders = this.traders.filter(API3.Filters.byClass("Ship")).length;
let numLandTraders = numTraders - numSeaTraders;
// add traders already in training
gameState.getOwnTrainingFacilities().forEach(function(ent) {
for (let item of ent.trainingQueue())
{
if (!item.metadata || !item.metadata.role || item.metadata.role !== "trader")
continue;
numTraders += item.count;
if (item.metadata.sea !== undefined)
numSeaTraders += item.count;
else
numLandTraders += item.count;
}
});
if (numTraders >= this.targetNumTraders &&
(!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) ||
this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2)))
return;
let template;
let metadata = { "role": "trader" };
if (this.tradeRoute.sea)
{
// if we have some merchand ships affected to transport, try first to reaffect them
// May-be, there were produced at an early stage when no other ship were available
// and the naval manager will train now more appropriate ships.
let already = false;
let shipToSwitch;
gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) {
if (already || !ship.hasClass("Trader"))
return;
if (ship.getMetadata(PlayerID, "role") === "switchToTrader")
{
already = true;
return;
}
shipToSwitch = ship;
});
if (already)
return;
if (shipToSwitch)
{
if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined)
shipToSwitch.setMetadata(PlayerID, "role", "trader");
else
shipToSwitch.setMetadata(PlayerID, "role", "switchToTrader");
return;
}
template = gameState.applyCiv("units/{civ}_ship_merchant");
metadata.sea = this.tradeRoute.sea;
}
else
{
template = gameState.applyCiv("units/{civ}_support_trader");
if (!this.tradeRoute.source.hasClass("NavalMarket"))
metadata.base = this.tradeRoute.source.getMetadata(PlayerID, "base");
else
metadata.base = this.tradeRoute.target.getMetadata(PlayerID, "base");
}
if (!gameState.getTemplate(template))
{
if (this.Config.debug > 0)
API3.warn("Petra error: trying to train " + template + " for civ " +
gameState.getPlayerCiv() + " but no template found.");
return;
}
queues.trader.addPlan(new m.TrainingPlan(gameState, template, metadata, 1, 1));
};
m.TradeManager.prototype.updateTrader = function(gameState, ent)
{
if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 === 0 &&
!ent.unitAIState().startsWith("INDIVIDUAL.GATHER") &&
m.gatherTreasure(gameState, ent, true))
return;
if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position())
return;
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return;
// TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain
Engine.ProfileStart("Trade Manager");
let access = ent.hasClass("Ship") ? ent.getMetadata(PlayerID, "sea") : gameState.ai.accessibility.getAccessValue(ent.position());
let route = this.checkRoutes(gameState, access);
if (!route)
{
// TODO try to garrison land trader inside merchant ship when only sea routes available
if (this.Config.debug > 0)
API3.warn(" no available route for " + ent.genericName() + " " + ent.id());
Engine.ProfileStop();
return;
}
let nearerSource = true;
if (API3.SquareVectorDistance(route.target.position(), ent.position()) < API3.SquareVectorDistance(route.source.position(), ent.position()))
nearerSource = false;
if (!ent.hasClass("Ship") && route.land !== access)
{
if (nearerSource)
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.source.position());
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.target.position());
Engine.ProfileStop();
return;
}
if (nearerSource)
ent.tradeRoute(route.target, route.source);
else
ent.tradeRoute(route.source, route.target);
ent.setMetadata(PlayerID, "route", this.routeEntToId(route));
Engine.ProfileStop();
};
m.TradeManager.prototype.setTradingGoods = function(gameState)
{
let tradingGoods = {};
for (let res in gameState.ai.HQ.wantedRates)
tradingGoods[res] = 0;
// first, try to anticipate future needs
let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState);
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
let remaining = 100;
let targetNum = this.Config.Economy.targetNumTraders;
for (let res in stocks)
{
if (res === "food")
continue;
let wantedRate = gameState.ai.HQ.wantedRates[res];
if (stocks[res] < 200)
{
tradingGoods[res] = wantedRate > 0 ? 20 : 10;
targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30));
}
else if (stocks[res] < 500)
{
tradingGoods[res] = wantedRate > 0 ? 15 : 10;
targetNum += 2;
}
else if (stocks[res] < 1000)
{
tradingGoods[res] = 10;
targetNum += 1;
}
remaining -= tradingGoods[res];
}
this.targetNumTraders = Math.round(this.Config.popScaling * targetNum);
// then add what is needed now
let mainNeed = Math.floor(remaining * 70 / 100);
let nextNeed = remaining - mainNeed;
tradingGoods[mostNeeded[0].type] += mainNeed;
if (mostNeeded[1].wanted > 0)
tradingGoods[mostNeeded[1].type] += nextNeed;
else
tradingGoods[mostNeeded[0].type] += nextNeed;
Engine.PostCommand(PlayerID, {"type": "set-trading-goods", "tradingGoods": tradingGoods});
if (this.Config.debug > 2)
API3.warn(" trading goods set to " + uneval(tradingGoods));
};
/**
* Try to barter unneeded resources for needed resources.
* only once per turn because the info is not updated within a turn
*/
m.TradeManager.prototype.performBarter = function(gameState)
{
let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray();
if (barterers.length === 0)
return false;
// Available resources after account substraction
let available = gameState.ai.queueManager.getAvailableResources(gameState);
let needs = gameState.ai.queueManager.currentNeeds(gameState);
let rates = gameState.ai.HQ.GetCurrentGatherRates(gameState);
let barterPrices = gameState.getBarterPrices();
// calculates conversion rates
let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]);
// loop through each missing resource checking if we could barter and help finishing a queue quickly.
- for (let buy of needs.types)
+ for (let buy of Resources.GetCodes())
{
if (needs[buy] === 0 || needs[buy] < rates[buy]*30) // check if our rate allows to gather it fast enough
continue;
// pick the best resource to barter.
let bestToSell;
let bestRate = 0;
- for (let sell of needs.types)
+ for (let sell of Resources.GetCodes())
{
if (sell === buy)
continue;
if (needs[sell] > 0 || available[sell] < 500) // do not sell if we need it or do not have enough buffer
continue;
let barterRateMin;
if (sell === "food")
{
barterRateMin = 30;
if (available[sell] > 40000)
barterRateMin = 0;
else if (available[sell] > 15000)
barterRateMin = 5;
else if (available[sell] > 1000)
barterRateMin = 10;
}
else
{
barterRateMin = 70;
if (available[sell] > 1000)
barterRateMin = 50;
if (buy === "food")
barterRateMin += 20;
}
let barterRate = getBarterRate(barterPrices, buy, sell);
if (barterRate > bestRate && barterRate > barterRateMin)
{
bestRate = barterRate;
bestToSell = sell;
}
}
if (bestToSell !== undefined)
{
barterers[0].barter(buy, bestToSell, 100);
if (this.Config.debug > 2)
API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy + " >> need sell " + needs[bestToSell] +
" need buy " + needs[buy] + " rate buy " + rates[buy] + " available sell " + available[bestToSell] +
" available buy " + available[buy] + " barterRate " + bestRate);
return true;
}
}
// now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices)
if (available.food < 1000 || needs.food > 0)
return false;
let bestToBuy;
let bestChoice = 0;
- for (let buy of needs.types)
+ for (let buy of Resources.GetCodes())
{
if (buy === "food")
continue;
let barterRateMin = 80;
if (available[buy] < 5000 && available.food > 5000)
barterRateMin -= 20 - Math.floor(available[buy]/250);
let barterRate = getBarterRate(barterPrices, buy, "food");
if (barterRate < barterRateMin)
continue;
let choice = barterRate / (100 + available[buy]);
if (choice > bestChoice)
{
bestChoice = choice;
bestToBuy = buy;
}
}
if (bestToBuy !== undefined)
{
barterers[0].barter(bestToBuy, "food", 100);
if (this.Config.debug > 2)
API3.warn("Contingency bartering: sold food for " + bestToBuy + " available sell " + available.food +
" available buy " + available[bestToBuy] + " barterRate " + getBarterRate(barterPrices, bestToBuy, "food"));
return true;
}
return false;
};
m.TradeManager.prototype.checkEvents = function(gameState, events)
{
// check if one market from a traderoute is renamed, change the route accordingly
for (let evt of events.EntityRenamed)
{
let ent = gameState.getEntityById(evt.newentity);
if (!ent || !ent.hasClass("Market"))
continue;
for (let trader of this.traders.values())
{
let route = trader.getMetadata(PlayerID, "route");
if (!route)
continue;
if (route.source === evt.entity)
route.source = evt.newentity;
else if (route.target === evt.entity)
route.target = evt.newentity;
else
continue;
trader.setMetadata(PlayerID, "route", route);
}
}
// if one market (or market-foundation) is destroyed, we should look for a better route
for (let evt of events.Destroy)
{
if (!evt.entityObj)
continue;
let ent = evt.entityObj;
if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner()))
continue;
this.activateProspection(gameState);
return true;
}
// same thing if one market is built
for (let evt of events.Create)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner()))
continue;
this.activateProspection(gameState);
return true;
}
// and same thing for captured markets
for (let evt of events.OwnershipChanged)
{
if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to))
continue;
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market"))
continue;
this.activateProspection(gameState);
return true;
}
// or if diplomacy changed
if (events.DiplomacyChanged.length)
{
this.activateProspection(gameState);
return true;
}
return false;
};
m.TradeManager.prototype.activateProspection = function(gameState)
{
this.routeProspection = true;
gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market"));
gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock"));
};
/**
* fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute
* If an index is given, it returns the best route with this index or the best land route if index is a land index
*/
m.TradeManager.prototype.checkRoutes = function(gameState, accessIndex)
{
let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures());
let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities());
if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon
{
this.tradeRoute = undefined;
this.potentialTradeRoute = undefined;
return false;
}
let onlyOurs = !market2.hasEntities();
if (onlyOurs)
market2 = market1;
let candidate = { "gain": 0 };
let potential = { "gain": 0 };
let bestIndex = { "gain": 0 };
let bestLand = { "gain": 0 };
let mapSize = gameState.sharedScript.mapSize;
let traderTemplatesGains = gameState.getTraderTemplatesGains();
for (let m1 of market1.values())
{
if (!m1.position())
continue;
let access1 = m.getLandAccess(gameState, m1);
let sea1 = m1.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m1) : undefined;
for (let m2 of market2.values())
{
if (onlyOurs && m1.id() >= m2.id())
continue;
if (!m2.position())
continue;
let access2 = m.getLandAccess(gameState, m2);
let sea2 = m2.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m2) : undefined;
let land = access1 == access2 ? access1 : undefined;
let sea = sea1 && sea1 == sea2 ? sea1 : undefined;
if (!land && !sea)
continue;
if (land && m.isLineInsideEnemyTerritory(gameState, m1.position(), m2.position()))
continue;
let gainMultiplier;
if (land && traderTemplatesGains.landGainMultiplier)
gainMultiplier = traderTemplatesGains.landGainMultiplier;
else if (sea && traderTemplatesGains.navalGainMultiplier)
gainMultiplier = traderTemplatesGains.navalGainMultiplier;
else
continue;
let gain = Math.round(gainMultiplier * TradeGain(API3.SquareVectorDistance(m1.position(), m2.position()), mapSize));
if (gain < this.minimalGain)
continue;
if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined)
{
if (accessIndex)
{
if (gameState.ai.accessibility.regionType[accessIndex] === "water" && sea === accessIndex)
{
if (gain < bestIndex.gain)
continue;
bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
else if (gameState.ai.accessibility.regionType[accessIndex] === "land" && land === accessIndex)
{
if (gain < bestIndex.gain)
continue;
bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
else if (gameState.ai.accessibility.regionType[accessIndex] === "land")
{
if (gain < bestLand.gain)
continue;
bestLand = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
}
if (gain < candidate.gain)
continue;
candidate = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
if (gain < potential.gain)
continue;
potential = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
}
if (potential.gain < 1)
this.potentialTradeRoute = undefined;
else
this.potentialTradeRoute = potential;
if (candidate.gain < 1)
{
if (this.Config.debug > 2)
API3.warn("no better trade route possible");
this.tradeRoute = undefined;
return false;
}
if (this.Config.debug > 1 && this.tradeRoute)
{
if (candidate.gain > this.tradeRoute.gain)
API3.warn("one better trade route set with gain " + candidate.gain + " instead of " + this.tradeRoute.gain);
}
else if (this.Config.debug > 1)
API3.warn("one trade route set with gain " + candidate.gain);
this.tradeRoute = candidate;
if (this.Config.chat)
{
let owner = this.tradeRoute.source.owner();
if (owner === PlayerID)
owner = this.tradeRoute.target.owner();
if (owner !== PlayerID && !this.warnedAllies[owner])
{ // Warn an ally that we have a trade route with him
m.chatNewTradeRoute(gameState, owner);
this.warnedAllies[owner] = true;
}
}
if (accessIndex)
{
if (bestIndex.gain > 0)
return bestIndex;
else if (gameState.ai.accessibility.regionType[accessIndex] === "land" && bestLand.gain > 0)
return bestLand;
return false;
}
return true;
};
/** Called when a market was built or destroyed, and checks if trader orders should be changed */
m.TradeManager.prototype.checkTrader = function(gameState, ent)
{
let presentRoute = ent.getMetadata(PlayerID, "route");
if (!presentRoute)
return;
if (!ent.position())
{
// This trader is garrisoned, we will decide later (when ungarrisoning) what to do
ent.setMetadata(PlayerID, "route", undefined);
return;
}
let access = ent.hasClass("Ship") ? ent.getMetadata(PlayerID, "sea") : gameState.ai.accessibility.getAccessValue(ent.position());
let possibleRoute = this.checkRoutes(gameState, access);
// Warning: presentRoute is from metadata, so contains entity ids
if (!possibleRoute ||
possibleRoute.source.id() != presentRoute.source && possibleRoute.source.id() != presentRoute.target ||
possibleRoute.target.id() != presentRoute.source && possibleRoute.target.id() != presentRoute.target)
{
// Trader will be assigned in updateTrader
ent.setMetadata(PlayerID, "route", undefined);
if (!possibleRoute && !ent.hasClass("Ship"))
{
let closestBase = m.getBestBase(gameState, ent, true);
if (closestBase.accessIndex == access)
{
let closestBasePos = closestBase.anchor.position();
ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15);
return;
}
}
ent.stopMoving();
}
};
m.TradeManager.prototype.prospectForNewMarket = function(gameState, queues)
{
if (queues.economicBuilding.hasQueuedUnitsWithClass("Market") || queues.dock.hasQueuedUnitsWithClass("Market"))
return;
if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market"))
return;
if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).hasEntities() &&
!gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).hasEntities())
return;
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_market"));
if (!template)
return;
this.checkRoutes(gameState);
let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template);
if (!marketPos || marketPos[3] === 0) // marketPos[3] is the expected gain
{ // no position found
gameState.ai.HQ.buildManager.setUnbuildable(gameState, gameState.applyCiv("structures/{civ}_market"));
return;
}
this.routeProspection = false;
if (!this.isNewMarketWorth(marketPos[3]))
return; // position found, but not enough gain compared to our present route
if (this.Config.debug > 1)
{
if (this.potentialTradeRoute)
API3.warn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " +
marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain);
else
API3.warn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " +
marketPos[3]);
}
if (!this.tradeRoute)
gameState.ai.queueManager.changePriority("economicBuilding", 2*this.Config.priorities.economicBuilding);
let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market");
if (!this.tradeRoute)
plan.queueToReset = "economicBuilding";
queues.economicBuilding.addPlan(plan);
};
m.TradeManager.prototype.isNewMarketWorth = function(expectedGain)
{
if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain &&
expectedGain < this.potentialTradeRoute.gain + 20)
return false;
return true;
};
m.TradeManager.prototype.update = function(gameState, events, queues)
{
if (gameState.ai.HQ.canBarter)
this.performBarter(gameState);
if (this.Config.difficulty <= 1)
return;
if (this.checkEvents(gameState, events)) // true if one market was built or destroyed
{
this.traders.forEach(ent => { this.checkTrader(gameState, ent); });
this.checkRoutes(gameState);
}
if (this.tradeRoute)
{
this.traders.forEach(ent => { this.updateTrader(gameState, ent); });
if (gameState.ai.playedTurn % 5 == 0)
this.trainMoreTraders(gameState, queues);
if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2)
gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues);
if (gameState.ai.playedTurn % 60 == 0)
this.setTradingGoods(gameState);
}
if (this.routeProspection)
this.prospectForNewMarket(gameState, queues);
};
m.TradeManager.prototype.routeEntToId = function(route)
{
if (!route)
return undefined;
let ret = {};
for (let key in route)
{
if (key == "source" || key == "target")
{
if (!route[key])
return undefined;
ret[key] = route[key].id();
}
else
ret[key] = route[key];
}
return ret;
};
m.TradeManager.prototype.routeIdToEnt = function(gameState, route)
{
if (!route)
return undefined;
let ret = {};
for (let key in route)
{
if (key == "source" || key == "target")
{
ret[key] = gameState.getEntityById(route[key]);
if (!ret[key])
return undefined;
}
else
ret[key] = route[key];
}
return ret;
};
m.TradeManager.prototype.Serialize = function()
{
return {
"tradeRoute": this.routeEntToId(this.tradeRoute),
"potentialTradeRoute": this.routeEntToId(this.potentialTradeRoute),
"routeProspection": this.routeProspection,
"targetNumTraders": this.targetNumTraders,
"warnedAllies": this.warnedAllies
};
};
m.TradeManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
{
if (key == "tradeRoute" || key == "potentialTradeRoute")
this[key] = this.routeIdToEnt(gameState, data[key]);
else
this[key] = data[key];
}
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 20600)
@@ -1,1021 +1,1021 @@
var PETRA = function(m)
{
/**
* This class makes a worker do as instructed by the economy manager
*/
m.Worker = function(base)
{
this.ent = undefined;
this.base = base;
this.baseID = base.ID;
};
m.Worker.prototype.update = function(gameState, ent)
{
if (!ent.position() || ent.getMetadata(PlayerID, "plan") === -2 || ent.getMetadata(PlayerID, "plan") === -3)
return;
// If we are waiting for a transport or we are sailing, just wait
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return;
// base 0 for unassigned entities has no accessIndex, so take the one from the entity
if (this.baseID === gameState.ai.HQ.baseManagers[0].ID)
this.accessIndex = gameState.ai.accessibility.getAccessValue(ent.position());
else
this.accessIndex = this.base.accessIndex;
let subrole = ent.getMetadata(PlayerID, "subrole");
if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ...
{
ent.setMetadata(PlayerID, "subrole", "idle");
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
this.ent = ent;
let unitAIState = ent.unitAIState();
if ((subrole === "hunter" || subrole === "gatherer") &&
(unitAIState === "INDIVIDUAL.GATHER.GATHERING" || unitAIState === "INDIVIDUAL.GATHER.APPROACHING" ||
unitAIState === "INDIVIDUAL.COMBAT.APPROACHING"))
{
if (this.isInaccessibleSupply(gameState) && !this.retryGathering(gameState, subrole))
ent.stopMoving();
// Check that we have not drifted too far
if (unitAIState === "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length)
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target)
{
let supply = gameState.getEntityById(orderData.target);
if (supply && supply.resourceSupplyType() && supply.resourceSupplyType().generic === "food")
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position());
if (gameState.isPlayerEnemy(territoryOwner) && !this.retryGathering(gameState, subrole))
ent.stopMoving();
else if (!gameState.isPlayerAlly(territoryOwner))
{
let distanceSquare = ent.hasClass("Cavalry") ? 90000 : 30000;
let supplyAccess = gameState.ai.accessibility.getAccessValue(supply.position());
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = false;
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (supplyAccess !== m.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(supply.position(), dropsite.position()) < distanceSquare)
{
hasFoodDropsiteWithinDistance = true;
break;
}
}
if (!hasFoodDropsiteWithinDistance && !this.retryGathering(gameState, subrole))
ent.stopMoving();
}
}
}
}
}
else if (ent.getMetadata(PlayerID, "approachingTarget"))
{
ent.setMetadata(PlayerID, "approachingTarget", undefined);
ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let unitAIStateOrder = unitAIState.split(".")[1];
// If we're fighting or hunting, let's not start gathering
// but for fishers where UnitAI must have made us target a moving whale.
// Also, if we are attacking, do not capture
if (unitAIStateOrder === "COMBAT")
{
if (subrole === "fisher")
this.startFishing(gameState);
else if (unitAIState === "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length &&
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target && orderData.attackType && orderData.attackType === "Capture")
{
// If we are here, an enemy structure must have targeted one of our workers
// and UnitAI sent it fight back with allowCapture=true
let target = gameState.getEntityById(orderData.target);
if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner()))
ent.attack(orderData.target, m.allowCapture(gameState, ent, target));
}
}
return;
}
// Okay so we have a few tasks.
// If we're gathering, we'll check that we haven't run idle.
// And we'll also check that we're gathering a resource we want to gather.
if (subrole === "gatherer")
{
if (ent.isIdle())
{
// if we aren't storing resources or it's the same type as what we're about to gather,
// let's just pick a new resource.
// TODO if we already carry the max we can -> returnresources
if (!ent.resourceCarrying() || !ent.resourceCarrying().length ||
ent.resourceCarrying()[0].type === ent.getMetadata(PlayerID, "gather-type"))
{
this.startGathering(gameState);
}
else if (!m.returnResources(gameState, ent)) // try to deposit resources
{
// no dropsite, abandon old resources and start gathering new ones
this.startGathering(gameState);
}
}
else if (unitAIStateOrder === "GATHER")
{
// we're already gathering. But let's check if there is nothing better
// in case UnitAI did something bad
if (ent.unitAIOrderData().length)
{
let supplyId = ent.unitAIOrderData()[0].target;
let supply = gameState.getEntityById(supplyId);
if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") &&
supply.resourceSupplyType().generic !== "treasure" &&
supplyId !== ent.getMetadata(PlayerID, "supply"))
{
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId);
if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30)
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let gatherType = ent.getMetadata(PlayerID, "gather-type");
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (nearby.some(sup => sup.id == supplyId))
ent.setMetadata(PlayerID, "supply", supplyId);
else if (nearby.length)
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (medium.length && !medium.some(sup => sup.id == supplyId))
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
ent.setMetadata(PlayerID, "supply", supplyId);
}
}
}
}
}
else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
if (gameState.ai.playedTurn % 10 == 0)
{
// Check from time to time that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position())
{
let access = gameState.ai.accessibility.getAccessValue(ent.position());
let goalAccess = dropsite.getMetadata(PlayerID, "access");
if (!goalAccess || dropsite.hasClass("Elephant"))
{
goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position());
dropsite.setMetadata(PlayerID, "access", goalAccess);
}
if (access != goalAccess)
m.returnResources(gameState, this.ent);
}
}
// If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full.
// Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be
// reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one).
let gatherType = ent.getMetadata(PlayerID, "gather-type");
- let influenceGroup = gameState.sharedScript.resourceInfo.aiInfluenceGroups[gatherType];
+ let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup;
if (influenceGroup && influenceGroup == "sparse")
{
let supplyId = ent.getMetadata(PlayerID, "supply");
if (supplyId)
{
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (!nearby.some(sup => sup.id == supplyId))
{
if (nearby.length)
ent.setMetadata(PlayerID, "supply", undefined);
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (!medium.some(sup => sup.id == supplyId) && medium.length)
ent.setMetadata(PlayerID, "supply", undefined);
}
}
}
}
}
}
else if (subrole === "builder")
{
if (unitAIStateOrder === "REPAIR")
{
// Update our target in case UnitAI sent us to a different foundation because of autocontinue
// and abandon it if UnitAI has sent us to build a field (as we build them only when needed)
if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target &&
ent.getMetadata(PlayerID, "target-foundation") !== ent.unitAIOrderData()[0].target)
{
let targetId = ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (target && !target.hasClass("Field"))
{
ent.setMetadata(PlayerID, "target-foundation", targetId);
return;
}
ent.setMetadata(PlayerID, "target-foundation", undefined);
ent.setMetadata(PlayerID, "subrole", "idle");
ent.stopMoving();
if (this.baseID !== gameState.ai.HQ.baseManagers[0].ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
// Otherwise check that the target still exists (useful in REPAIR.APPROACHING)
let targetId = ent.getMetadata(PlayerID, "target-foundation");
if (targetId && gameState.getEntityById(targetId))
return;
ent.stopMoving();
}
// okay so apparently we aren't working.
// Unless we've been explicitely told to keep our role, make us idle.
let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() === false)
{
ent.setMetadata(PlayerID, "subrole", "idle");
ent.setMetadata(PlayerID, "target-foundation", undefined);
// If worker elephant, move away to avoid being trapped in between constructions
if (ent.hasClass("Elephant"))
this.moveAway(gameState);
else if (this.baseID !== gameState.ai.HQ.baseManagers[0].ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
else
{
let access = gameState.ai.accessibility.getAccessValue(ent.position());
let goalAccess = m.getLandAccess(gameState, target);
let queued = m.returnResources(gameState, ent);
if (access === goalAccess)
ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, goalAccess, target.position());
}
}
else if (subrole === "hunter")
{
let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch");
if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20))
{
if (!this.startHunting(gameState))
{
// nothing to hunt around. Try another region if any
let nowhereToHunt = true;
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
let basePos = base.anchor.position();
if (this.startHunting(gameState, basePos))
{
ent.setMetadata(PlayerID, "base", base.ID);
let access = gameState.ai.accessibility.getAccessValue(ent.position());
if (base.accessIndex === access)
ent.move(basePos[0], basePos[1]);
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, base.accessIndex, basePos);
nowhereToHunt = false;
break;
}
}
if (nowhereToHunt)
ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime);
}
}
else // Perform some sanity checks
{
if (unitAIStateOrder === "GATHER" || unitAIStateOrder === "RETURNRESOURCE")
{
// we may have drifted towards ennemy territory during the hunt, if yes go home
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startHunting(gameState);
else if (unitAIState === "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
// Check that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position())
{
let access = gameState.ai.accessibility.getAccessValue(ent.position());
let goalAccess = dropsite.getMetadata(PlayerID, "access");
if (!goalAccess || dropsite.hasClass("Elephant"))
{
goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position());
dropsite.setMetadata(PlayerID, "access", goalAccess);
}
if (access !== goalAccess)
m.returnResources(gameState, ent);
}
}
}
}
}
else if (subrole === "fisher")
{
if (ent.isIdle())
this.startFishing(gameState);
else // if we have drifted towards ennemy territory during the fishing, go home
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startFishing(gameState);
}
}
};
m.Worker.prototype.retryGathering = function(gameState, subrole)
{
switch (subrole)
{
case "gatherer":
return this.startGathering(gameState);
case "hunter":
return this.startHunting(gameState);
case "fisher":
return this.startFishing(gameState);
default:
return false;
}
};
m.Worker.prototype.startGathering = function(gameState)
{
let access = gameState.ai.accessibility.getAccessValue(this.ent.position());
// First look for possible treasure if any
if (m.gatherTreasure(gameState, this.ent))
return true;
let resource = this.ent.getMetadata(PlayerID, "gather-type");
// If we are gathering food, try to hunt first
if (resource === "food" && this.startHunting(gameState))
return true;
let findSupply = function(ent, supplies) {
let ret = false;
for (let i = 0; i < supplies.length; ++i)
{
// exhausted resource, remove it from this list
if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id))
{
supplies.splice(i--, 1);
continue;
}
if (m.IsSupplyFull(gameState, supplies[i].ent))
continue;
let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
continue;
// check if available resource is worth one additionnal gatherer (except for farms)
let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id);
if (supplies[i].ent.resourceSupplyType().specific !== "grain" &&
nbGatherers > 0 && supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30)
continue;
// not in ennemy territory
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position());
if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
continue;
gameState.ai.HQ.AddTCGatherer(supplies[i].id);
ent.setMetadata(PlayerID, "supply", supplies[i].id);
ret = supplies[i].ent;
break;
}
return ret;
};
let navalManager = gameState.ai.HQ.navalManager;
let supply;
// first look in our own base if accessible from our present position
if (this.accessIndex === access)
{
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.gather(supply);
return true;
}
// --> for food, try to gather from fields if any, otherwise build one if any
if (resource === "food")
{
supply = this.gatherNearestField(gameState, this.baseID);
if (supply)
{
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, this.baseID);
if (supply)
{
this.ent.repair(supply);
return true;
}
}
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
// So if we're here we have checked our whole base for a proper resource (or it was not accessible)
// --> check other bases directly accessible
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID === this.baseID)
continue;
if (base.accessIndex !== access)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
if (resource === "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID === this.baseID)
continue;
if (base.accessIndex !== access)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.repair(supply);
return true;
}
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID === this.baseID)
continue;
if (base.accessIndex !== access)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
// Okay may-be we haven't found any appropriate dropsite anywhere.
// Try to help building one if any accessible foundation available
let foundations = gameState.getOwnFoundations().toEntityArray();
let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || foundation.getMetadata(PlayerID, "access") !== access)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) !== -1)
{
if (foundation.getMetadata(PlayerID, "base") !== this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
this.ent.repair(foundation);
return true;
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing ... try bases which need a transport
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex === access)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby);
if (supply && navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()))
{
if (base.ID !== this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
if (resource === "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex === access)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()))
{
if (base.ID !== this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()))
{
if (base.ID !== this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex === access)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium);
if (supply && navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()))
{
if (base.ID !== this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
// Okay so we haven't found any appropriate dropsite anywhere.
// Try to help building one if any non-accessible foundation available
shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || foundation.getMetadata(PlayerID, "access") === access)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) !== -1)
{
let foundationAccess = m.getLandAccess(gameState, foundation);
if (navalManager.requireTransport(gameState, this.ent, access, foundationAccess, foundation.position()))
{
if (foundation.getMetadata(PlayerID, "base") !== this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
return true;
}
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing, we look now for faraway resources, first in the accessible ones, then in the others
// except for food when farms or corrals can be used
let allowDistant = true;
if (resource === "food")
{
if (gameState.ai.HQ.turnCache.allowDistantFood === undefined)
gameState.ai.HQ.turnCache.allowDistantFood =
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field") &&
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral");
allowDistant = gameState.ai.HQ.turnCache.allowDistantFood;
}
if (allowDistant)
{
if (this.accessIndex === access)
{
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID === this.baseID)
continue;
if (base.accessIndex !== access)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex === access)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway);
if (supply && navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()))
{
if (base.ID !== this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
// If we are here, we have nothing left to gather ... certainly no more resources of this type
gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime;
if (gameState.ai.Config.debug > 2)
warn(" >>>>> worker with gather-type " + resource + " with nothing to gather ");
this.ent.setMetadata(PlayerID, "subrole", "idle");
return false;
};
/**
* if position is given, we only check if we could hunt from this position but do nothing
* otherwise the position of the entity is taken, and if something is found, we directly start the hunt
*/
m.Worker.prototype.startHunting = function(gameState, position)
{
// First look for possible treasure if any
if (!position && m.gatherTreasure(gameState, this.ent))
return true;
let resources = gameState.getHuntableSupplies();
if (!resources.hasEntities())
return false;
let nearestSupplyDist = Math.min();
let nearestSupply;
let isCavalry = this.ent.hasClass("Cavalry");
let isRanged = this.ent.hasClass("Ranged");
let entPosition = position ? position : this.ent.position();
let access = gameState.ai.accessibility.getAccessValue(entPosition);
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare)
{
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (supplyAccess !== m.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare)
return true;
}
return false;
};
resources.forEach(function(supply)
{
if (!supply.position())
return;
let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
return;
if (m.IsSupplyFull(gameState, supply))
return;
// check if available resource is worth one additionnal gatherer (except for farms)
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
return;
let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1;
// Only cavalry and range units should hunt fleeing animals
if (canFlee && !isCavalry && !isRanged)
return;
let supplyAccess = gameState.ai.accessibility.getAccessValue(supply.position());
if (supplyAccess !== access)
return;
// measure the distance to the resource
let dist = API3.SquareVectorDistance(entPosition, supply.position());
if (dist > nearestSupplyDist)
return;
// Only cavalry should hunt faraway
if (!isCavalry && dist > 25000)
return;
// Avoid ennemy territory
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position());
if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
return;
// And if in ally territory, don't hunt this ally's cattle
if (territoryOwner !== 0 && territoryOwner !== PlayerID && supply.owner() === territoryOwner)
return;
// Only cavalry should hunt far from dropsite (specially for non domestic animals which flee)
if (!isCavalry && canFlee && territoryOwner === 0)
return;
let distanceSquare = isCavalry ? 35000 : ( canFlee ? 7000 : 12000);
if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare))
return;
nearestSupplyDist = dist;
nearestSupply = supply;
});
if (nearestSupply)
{
if (position)
return true;
gameState.ai.HQ.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
return false;
};
m.Worker.prototype.startFishing = function(gameState)
{
if (!this.ent.position())
return false;
let resources = gameState.getFishableSupplies();
if (!resources.hasEntities())
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState);
this.ent.destroy();
return false;
}
let nearestSupplyDist = Math.min();
let nearestSupply;
let fisherSea = this.ent.getMetadata(PlayerID, "sea");
let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")).filter(API3.Filters.byClass("Dock")).toEntityArray();
let nearestDropsiteDist = function(supply) {
let distMin = 1000000;
let pos = supply.position();
for (let dropsite of fishDropsites)
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (fisherSea !== m.getSeaAccess(gameState, dropsite))
continue;
distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position()));
}
return distMin;
};
let exhausted = true;
resources.forEach(function(supply)
{
if (!supply.position())
return;
// check that it is accessible
if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) !== fisherSea)
return;
exhausted = false;
if (m.IsSupplyFull(gameState, supply))
return;
// check if available resource is worth one additionnal gatherer (except for farms)
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
return;
// Avoid ennemy territory
if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply))
return;
// measure the distance from the resource to the nearest dropsite
let dist = nearestDropsiteDist(supply);
if (dist > nearestSupplyDist)
return;
nearestSupplyDist = dist;
nearestSupply = supply;
});
if (exhausted)
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea);
this.ent.destroy();
return false;
}
if (nearestSupply)
{
gameState.ai.HQ.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
if (this.ent.getMetadata(PlayerID,"subrole") === "fisher")
this.ent.setMetadata(PlayerID, "subrole", "idle");
return false;
};
m.Worker.prototype.gatherNearestField = function(gameState, baseID)
{
let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID));
let bestFarm;
for (let field of ownFields.values())
{
if (m.IsSupplyFull(gameState, field))
continue;
let rate = 1;
let diminishing = field.getDiminishingReturns();
if (diminishing < 1)
{
let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id());
if (num > 0)
rate = Math.pow(diminishing, num);
}
// Add a penalty distance depending on rate
let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000;
if (!bestFarm || dist < bestFarm.dist)
bestFarm = { "ent": field, "dist": dist, "rate": rate };
}
// If other field foundations available, better build them when rate becomes too small
if (!bestFarm || bestFarm.rate < 0.70 &&
gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities())
return false;
gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id());
this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id());
return bestFarm.ent;
};
/**
* WARNING with the present options of AI orders, the unit will not gather after building the farm.
* This is done by calling the gatherNearestField function when construction is completed.
*/
m.Worker.prototype.buildAnyField = function(gameState, baseID)
{
if (!this.ent.isBuilder())
return false;
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_field"));
if (!template)
return false;
let maxGatherers = template.maxGatherers();
let bestFarmEnt = false;
let bestFarmDist = 10000000;
let pos = this.ent.position();
for (let found of gameState.getOwnFoundations().values())
{
if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field"))
continue;
let current = found.getBuildersNb();
if (current === undefined || current >= maxGatherers)
continue;
let dist = API3.SquareVectorDistance(found.position(), pos);
if (dist > bestFarmDist)
continue;
bestFarmEnt = found;
bestFarmDist = dist;
}
return bestFarmEnt;
};
/**
* Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions
* For the time being, we move towards the nearest gatherer (providing him a dropsite)
*/
m.Worker.prototype.moveAway = function(gameState)
{
let gatherers = this.base.workersBySubrole(gameState, "gatherer");
let pos = this.ent.position();
let dist = Math.min();
let destination = pos;
for (let gatherer of gatherers.values())
{
if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined)
continue;
if (gatherer.isIdle())
continue;
let distance = API3.SquareVectorDistance(pos, gatherer.position());
if (distance > dist)
continue;
dist = distance;
destination = gatherer.position();
}
this.ent.move(destination[0], destination[1]);
};
/**
* Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes
* inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case
* it will be cleared later).
*/
m.Worker.prototype.isInaccessibleSupply = function(gameState)
{
if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target)
return false;
let targetId = this.ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (!target)
return true;
if (!target.resourceSupplyType())
return false;
let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget");
let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0;
if (!approachingTarget || approachingTarget !== targetId)
{
this.ent.setMetadata(PlayerID, "approachingTarget", targetId);
this.ent.setMetadata(PlayerID, "approachingTime", undefined);
this.ent.setMetadata(PlayerID, "approachingPos", undefined);
this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount);
let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried");
if (alreadyTried && alreadyTried !== targetId)
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore");
if (carriedBefore !== carriedAmount)
{
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
if (target.getMetadata(PlayerID, "inaccessibleTime"))
target.setMetadata(PlayerID, "inaccessibleTime", 0);
return false;
}
let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
return true;
let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime");
if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3)
{
let presentPos = this.ent.position();
let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos");
if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1])
{
this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime);
this.ent.setMetadata(PlayerID, "approachingPos", presentPos);
return false;
}
if (gameState.ai.elapsedTime - approachingTime > 10)
{
if (this.ent.getMetadata(PlayerID, "alreadyTried"))
{
target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600);
return true;
}
// let's try again to reach it
this.ent.setMetadata(PlayerID, "alreadyTried", targetId);
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.gather(target);
return false;
}
}
return false;
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20600)
@@ -1,2045 +1,2036 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised for the biggest part
// So most of the attributes shouldn't be serialized
// Return an object with a small selection of deterministic data
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
let cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
let phase = "";
let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
// store player ally/neutral/enemy data as arrays
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
// Add timeElapsed
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// Add ceasefire info
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
// Add cinema path info
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
// Add the game type and allied victory
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.gameType = cmpEndGameManager.GetGameType();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
- // Add Resource Codes, untranslated names and AI Analysis
- ret.resources = {
- "codes": Resources.GetCodes(),
- "names": Resources.GetNames(),
- "aiInfluenceGroups": {}
- };
- for (let res of ret.resources.codes)
- ret.resources.aiInfluenceGroups[res] = Resources.GetResource(res).aiAnalysisInfluenceGroup;
-
// Add basic statistics to each player
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
// Get basic simulation info
let ret = this.GetSimulationState();
// Add statistics to each player
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let n = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < n; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
else
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"template": template,
"alertRaiser": null,
"builder": null,
"canGarrison": null,
"identity": null,
"fogging": null,
"foundation": null,
"garrisonHolder": null,
"gate": null,
"guard": null,
"market": null,
"mirage": null,
"pack": null,
"upgrade" : null,
"player": -1,
"position": null,
"production": null,
"rallyPoint": null,
"resourceCarrying": null,
"rotation": null,
"trader": null,
"unitAI": null,
"visibility": null,
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
ret.rotation = cmpPosition.GetRotation();
}
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval"),
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress(),
};
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades" : cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
if (cmpFogging)
ret.fogging = {
"mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : null
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"possibleStances": cmpUnitAI.GetPossibleStances(),
"isIdle":cmpUnitAI.IsIdle(),
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked(),
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
return ret;
};
/**
* Get additionnal entity info, rarely used in the gui
*/
GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
{
let ret = {
"armour": null,
"attack": null,
"buildingAI": null,
"deathDamage": null,
"heal": null,
"isBarterMarket": null,
"loot": null,
"obstruction": null,
"turretParent":null,
"promotion": null,
"repairRate": null,
"buildRate": null,
"buildTime": null,
"resourceDropsite": null,
"resourceGatherRates": null,
"resourceSupply": null,
"resourceTrickle": null,
"speed": null,
};
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = cmpAttack.GetAttackStrengths(type);
ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// not a ranged attack, set some defaults
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the range in front of it, no spread. So angle = 0
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths();
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras)
ret.auras = cmpAuras.GetDescriptions();
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage);
if (cmpDeathDamage)
ret.deathDamage = cmpDeathDamage.GetDeathDamageStrengths();
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpRepairable = Engine.QueryInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairRate = cmpRepairable.GetRepairRate();
let cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
{
ret.buildRate = cmpFoundation.GetBuildRate();
ret.buildTime = cmpFoundation.GetBuildTime();
}
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"hp": cmpHeal.GetHP(),
"range": cmpHeal.GetRange().max,
"rate": cmpHeal.GetRate(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
let resources = cmpLoot.GetResources();
ret.loot = {
"xp": cmpLoot.GetXp()
};
for (let res of Resources.GetCodes())
ret.loot[res] = resources[res];
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
{
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetTimer(),
"rates": {}
};
let rates = cmpResourceTrickle.GetRates();
for (let res in rates)
ret.resourceTrickle.rates[res] = rates[res];
}
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetRunSpeed()
};
return ret;
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, name)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
// Add aura name and description loaded from JSON file
let auraNames = template.Auras._string.split(/\s+/);
let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
for (let name of auraNames)
aurasTemplate[name] = cmpDataTemplateManager.GetAuraTemplate(name);
return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
};
GuiInterface.prototype.GetTechnologyData = function(player, data)
{
let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
let template = cmpDataTemplateManager.GetTechnologyTemplate(data.name);
if (!template)
{
warn("Tried to get data for invalid technology: " + data.name);
return null;
}
return GetTechnologyDataHelper(template, data.civ, Resources);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
// Returns technologies that are being actively researched, along with
// which entity is researching them and how far along the research is.
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech in cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
else
ret[tech].progress = 0;
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default
if (notification.players == undefined)
{
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
notification.players = [-1];
for (let i = 1; i < numPlayers; ++i)
notification.players.push(i);
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// filter on players and time, since the delete timer might be executed with a delay
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
// GetLastFormationName is named in a strange way as it (also) is
// the value of the current formation (see Formation.js LoadFormation)
if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
return true;
}
return false;
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
let playerColors = {}; // cache of owner -> color map
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color:
let owner = -1;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r":1, "g":1, "b":1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeVisualization = Engine.QueryInterface(ent, IID_RangeVisualization);
if (!cmpRangeVisualization || player != owner && player != -1)
continue;
cmpRangeVisualization.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return [...this.entsWithAuraAndStatusBars];
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeVisualization = Engine.QueryInterface(ent, IID_RangeVisualization);
if (cmpRangeVisualization)
cmpRangeVisualization.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location)
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
};
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
// Move the preview into the right location
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
// Set it to a red shade if this is an invalid location
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// do some entity cache management and check for snapping
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// we're clearing the preview, clear the entity cache and bail
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
else
{
// Move all existing cached entities outside of the world and reset their use count
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before
for (let type in wallSet.templates)
{
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, tpl),
};
// ensure that the loaded template data contains a wallPiece component
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
}
// prevent division by zero errors further on if the start and end positions are the same
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// clear the single-building preview entity (we'll be rolling our own)
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// calculate wall placement and position preview entities
let result = {
"pieces": [],
"cost": { "population": 0, "populationBonus": 0, "time": 0 },
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true, // preview only, must not appear in the result
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []);
previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
// reuse an existing one
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// move piece to right location
// TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
// check whether this wall piece can be validly positioned here
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
let visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden");
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement
validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success);
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
// (TODO: break unlikely ties by choosing the lowest entity ID)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
var bucket = filtered.bucket;
if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if(!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
}
else if (!firstMarket)
{
result = { "type": "set first" };
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = { "type": "set first" };
}
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
return QueryPlayerIDInterface(player).GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetExtendedEntityState": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 20599)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 20600)
@@ -1,671 +1,641 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/RallyPoint.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
GetPrices: function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
},
PlayerHasMarket: function () { return false; }
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
GetGameType: function() { return "conquest"; },
GetAlliedVictory: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetNumPlayers: function() { return 2; },
GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; }
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
GetLosVisibility: function(ent, player) { return "visible"; },
GetLosCircular: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
GetCurrentTemplateName: function(ent) { return "example"; },
GetTemplate: function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
GetTime: function() { return 0; },
SetTimeout: function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
GetName: function() { return "Player 1"; },
GetCiv: function() { return "gaia"; },
GetColor: function() { return { r: 1, g: 1, b: 1, a: 1}; },
CanControlAllUnits: function() { return false; },
GetPopulationCount: function() { return 10; },
GetPopulationLimit: function() { return 20; },
GetMaxPopulation: function() { return 200; },
GetResourceCounts: function() { return { food: 100 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() { return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return false; },
IsMutualAlly: function() { return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return true; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(100, IID_EntityLimits, {
GetLimits: function() { return {"Foo": 10}; },
GetCounts: function() { return {"Foo": 5}; },
GetLimitChangers: function() {return {"Foo": {}}; }
});
AddMock(100, IID_TechnologyManager, {
IsTechnologyResearched: function(tech) { if (tech == "phase_village") return true; else return false; },
GetQueuedResearch: function() { return {}; },
GetStartedTechs: function() { return {}; },
GetResearchedTechs: function() { return {}; },
GetClassCounts: function() { return {}; },
GetTypeCountsByClass: function() { return {}; },
GetTechModifications: function() { return {}; }
});
AddMock(100, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
AddMock(101, IID_Player, {
GetName: function() { return "Player 2"; },
GetCiv: function() { return "mace"; },
GetColor: function() { return { r: 1, g: 0, b: 0, a: 1}; },
CanControlAllUnits: function() { return true; },
GetPopulationCount: function() { return 40; },
GetPopulationLimit: function() { return 30; },
GetMaxPopulation: function() { return 300; },
GetResourceCounts: function() { return { food: 200 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() {return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return true; },
IsMutualAlly: function() {return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return false; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(101, IID_EntityLimits, {
GetLimits: function() { return {"Bar": 20}; },
GetCounts: function() { return {"Bar": 0}; },
GetLimitChangers: function() {return {"Bar": {}}; }
});
AddMock(101, IID_TechnologyManager, {
IsTechnologyResearched: function(tech) { if (tech == "phase_village") return true; else return false; },
GetQueuedResearch: function() { return {}; },
GetStartedTechs: function() { return {}; },
GetResearchedTechs: function() { return {}; },
GetClassCounts: function() { return {}; },
GetTypeCountsByClass: function() { return {}; },
GetTechModifications: function() { return {}; }
});
AddMock(101, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
players: [
{
name: "Player 1",
civ: "gaia",
color: { r:1, g:1, b:1, a:1 },
controlsAll: false,
popCount: 10,
popLimit: 20,
popMax: 200,
panelEntities: [],
resourceCounts: { food: 100 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [false, false],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
entityLimitChangers: {"Foo": {}},
researchQueued: {},
researchStarted: {},
researchedTechs: {},
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
},
{
name: "Player 2",
civ: "mace",
color: { r:1, g:0, b:0, a:1 },
controlsAll: true,
popCount: 40,
popLimit: 30,
popMax: 300,
panelEntities: [],
resourceCounts: { food: 200 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [true, true],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [false, false],
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
entityLimitChangers: {"Bar": {}},
researchQueued: {},
researchStarted: {},
researchedTechs: {},
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
}
],
circularMap: false,
timeElapsed: 0,
gameType: "conquest",
- alliedVictory: false,
- "resources": {
- "codes": ["food", "metal", "stone", "wood"],
- "names": {
- "food": "Food",
- "metal": "Metal",
- "stone": "Stone",
- "wood": "Wood"
- },
- "aiInfluenceGroups": {
- "food": "ignore",
- "metal": "sparse",
- "stone": "sparse",
- "wood": "abundant"
- }
- }
+ alliedVictory: false
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r":1, "g":1, "b":1, "a":1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": {"Foo": 10},
"entityCounts": {"Foo": 5},
"entityLimitChangers": {"Foo": {}},
"researchQueued": {},
"researchStarted": {},
"researchedTechs": {},
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r":1, "g":0, "b":0, "a":1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": {"Bar": 20},
"entityCounts": {"Bar": 0},
"entityLimitChangers": {"Bar": {}},
"researchQueued": {},
"researchStarted": {},
"researchedTechs": {},
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
"gameType": "conquest",
- "alliedVictory": false,
- "resources": {
- "codes": ["food", "metal", "stone", "wood"],
- "names": {
- "food": "Food",
- "metal": "Metal",
- "stone": "Stone",
- "wood": "Wood"
- },
- "aiInfluenceGroups": {
- "food": "ignore",
- "metal": "sparse",
- "stone": "sparse",
- "wood": "abundant"
- }
- }
+ "alliedVictory": false
});
AddMock(10, IID_Builder, {
GetEntitiesList: function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
GetHitpoints: function() { return 50; },
GetMaxHitpoints: function() { return 60; },
IsRepairable: function() { return false; },
IsUnhealable: function() { return false; }
});
AddMock(10, IID_Identity, {
GetClassesList: function() { return ["class1", "class2"]; },
GetVisibleClassesList: function() { return ["class3", "class4"]; },
GetRank: function() { return "foo"; },
GetSelectionGroupName: function() { return "Selection Group Name"; },
HasClass: function() { return true; },
IsUndeletable: function() { return false; }
});
AddMock(10, IID_Position, {
GetTurretParent: function() {return INVALID_ENTITY;},
GetPosition: function() {
return {x:1, y:2, z:3};
},
GetRotation: function() {
return {x:4, y:5, z:6};
},
IsInWorld: function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetTimer": () => 1250,
"GetRates": () => ({
"food": 2,
"wood": 3,
"stone": 5,
"metal": 9
})
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
id: 10,
template: "example",
alertRaiser: null,
builder: true,
canGarrison: false,
identity: {
rank: "foo",
classes: ["class1", "class2"],
visibleClasses: ["class3", "class4"],
selectionGroupName: "Selection Group Name",
canDelete: true
},
fogging: null,
foundation: null,
garrisonHolder: null,
gate: null,
guard: null,
market: null,
mirage: null,
pack: null,
upgrade: null,
player: -1,
position: {x:1, y:2, z:3},
production: null,
rallyPoint: null,
resourceCarrying: null,
rotation: {x:4, y:5, z:6},
trader: null,
unitAI: null,
visibility: "visible",
hitpoints: 50,
maxHitpoints: 60,
needsRepair: false,
needsHeal: true
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedEntityState(-1, 10), {
armour: null,
attack: null,
buildingAI: null,
deathDamage:null,
heal: null,
isBarterMarket: true,
loot: null,
obstruction: null,
turretParent: null,
promotion: null,
repairRate: null,
buildRate: null,
buildTime: null,
resourceDropsite: null,
resourceGatherRates: null,
resourceSupply: null,
resourceTrickle: {
"interval": 1250,
"rates": {
"food": 2,
"wood": 3,
"stone": 5,
"metal": 9
}
},
speed: null
});
Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 20599)
+++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 20600)
@@ -1,1211 +1,1214 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpAIManager.h"
#include "simulation2/MessageTypes.h"
#include "graphics/Terrain.h"
#include "lib/timer.h"
#include "lib/tex/tex.h"
#include "lib/allocators/shared_ptr.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Profile.h"
+#include "ps/scripting/JSInterface_VFS.h"
#include "ps/TemplateLoader.h"
#include "ps/Util.h"
#include "simulation2/components/ICmpAIInterface.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpDataTemplateManager.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/helpers/LongPathfinder.h"
#include "simulation2/serialization/DebugSerializer.h"
#include "simulation2/serialization/StdDeserializer.h"
#include "simulation2/serialization/StdSerializer.h"
#include "simulation2/serialization/SerializeTemplates.h"
extern void kill_mainloop();
/**
* @file
* Player AI interface.
* AI is primarily scripted, and the CCmpAIManager component defined here
* takes care of managing all the scripts.
*
* To avoid slow AI scripts causing jerky rendering, they are run in a background
* thread (maintained by CAIWorker) so that it's okay if they take a whole simulation
* turn before returning their results (though preferably they shouldn't use nearly
* that much CPU).
*
* CCmpAIManager grabs the world state after each turn (making use of AIInterface.js
* and AIProxy.js to decide what data to include) then passes it to CAIWorker.
* The AI scripts will then run asynchronously and return a list of commands to execute.
* Any attempts to read the command list (including indirectly via serialization)
* will block until it's actually completed, so the rest of the engine should avoid
* reading it for as long as possible.
*
* JS::Values are passed between the game and AI threads using ScriptInterface::StructuredClone.
*
* TODO: actually the thread isn't implemented yet, because performance hasn't been
* sufficiently problematic to justify the complexity yet, but the CAIWorker interface
* is designed to hopefully support threading when we want it.
*/
/**
* Implements worker thread for CCmpAIManager.
*/
class CAIWorker
{
private:
class CAIPlayer
{
NONCOPYABLE(CAIPlayer);
public:
CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty,
shared_ptr scriptInterface) :
m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty),
m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetJSRuntime())
{
}
bool Initialise()
{
// LoadScripts will only load each script once even though we call it for each player
if (!m_Worker.LoadScripts(m_AIName))
return false;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
OsPath path = L"simulation/ai/" + m_AIName + L"/data.json";
JS::RootedValue metadata(cx);
m_Worker.LoadMetadata(path, &metadata);
if (metadata.isUndefined())
{
LOGERROR("Failed to create AI player: can't find %s", path.string8());
return false;
}
// Get the constructor name from the metadata
std::string moduleName;
std::string constructor;
JS::RootedValue objectWithConstructor(cx); // object that should contain the constructor function
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->HasProperty(metadata, "moduleName"))
{
LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8());
return false;
}
m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName);
if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor)
|| objectWithConstructor.isUndefined())
{
LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName);
return false;
}
if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor))
{
LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8());
return false;
}
// Get the constructor function from the loaded scripts
if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor)
|| ctor.isNull())
{
LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor);
return false;
}
m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent);
// Set up the data to pass as the constructor argument
JS::RootedValue settings(cx);
m_ScriptInterface->Eval(L"({})", &settings);
m_ScriptInterface->SetProperty(settings, "player", m_Player, false);
m_ScriptInterface->SetProperty(settings, "difficulty", m_Difficulty, false);
if (!m_UseSharedComponent)
{
ENSURE(m_Worker.m_HasLoadedEntityTemplates);
m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false);
}
JS::AutoValueVector argv(cx);
argv.append(settings.get());
m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj);
if (m_Obj.get().isNull())
{
LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor);
return false;
}
return true;
}
void Run(JS::HandleValue state, int playerID)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID);
}
// overloaded with a sharedAI part.
// javascript can handle both natively on the same function.
void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI);
}
void InitAI(JS::HandleValue state, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI);
}
CAIWorker& m_Worker;
std::wstring m_AIName;
player_id_t m_Player;
u8 m_Difficulty;
bool m_UseSharedComponent;
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptInterface;
JS::PersistentRootedValue m_Obj;
std::vector > m_Commands;
};
public:
struct SCommandSets
{
player_id_t player;
std::vector > commands;
};
CAIWorker() :
m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptRuntime)),
m_TurnNum(0),
m_CommandsComputed(true),
m_HasLoadedEntityTemplates(false),
m_HasSharedComponent(false),
m_SerializablePrototypes(new ObjectIdCache(g_ScriptRuntime)),
m_EntityTemplates(g_ScriptRuntime->m_rt),
m_TechTemplates(g_ScriptRuntime->m_rt),
m_SharedAIObj(g_ScriptRuntime->m_rt),
m_PassabilityMapVal(g_ScriptRuntime->m_rt),
m_TerritoryMapVal(g_ScriptRuntime->m_rt)
{
m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG);
m_ScriptInterface->LoadGlobalScripts();
m_ScriptInterface->SetCallbackData(static_cast (this));
m_SerializablePrototypes->init();
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
m_ScriptInterface->RegisterFunction("PostCommand");
m_ScriptInterface->RegisterFunction("IncludeModule");
m_ScriptInterface->RegisterFunction("Exit");
m_ScriptInterface->RegisterFunction("ComputePath");
m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage");
m_ScriptInterface->RegisterFunction("GetTemplate");
+
+ JSI_VFS::RegisterScriptFunctions_Simulation(*(m_ScriptInterface.get()));
}
~CAIWorker()
{
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
}
bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; }
bool LoadScripts(const std::wstring& moduleName)
{
// Ignore modules that are already loaded
if (m_LoadedModules.find(moduleName) != m_LoadedModules.end())
return true;
// Mark this as loaded, to prevent it recursively loading itself
m_LoadedModules.insert(moduleName);
// Load and execute *.js
VfsPaths pathnames;
if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0)
{
LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName));
return false;
}
for (const VfsPath& path : pathnames)
{
if (!m_ScriptInterface->LoadGlobalScriptFile(path))
{
LOGERROR("Failed to load script %s", path.string8());
return false;
}
}
return true;
}
static void IncludeModule(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->LoadScripts(name);
}
static void PostCommand(ScriptInterface::CxPrivate* pCxPrivate, int playerid, JS::HandleValue cmd)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->PostCommand(playerid, cmd);
}
void PostCommand(int playerid, JS::HandleValue cmd)
{
for (size_t i=0; im_Player == playerid)
{
m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd));
return;
}
}
LOGERROR("Invalid playerid in PostCommand!");
}
static JS::Value ComputePath(ScriptInterface::CxPrivate* pCxPrivate,
JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
JSContext* cx(self->m_ScriptInterface->GetContext());
JSAutoRequest rq(cx);
CFixedVector2D pos, goalPos;
std::vector waypoints;
JS::RootedValue retVal(cx);
self->m_ScriptInterface->FromJSVal(cx, position, pos);
self->m_ScriptInterface->FromJSVal(cx, goal, goalPos);
self->ComputePath(pos, goalPos, passClass, waypoints);
self->m_ScriptInterface->ToJSVal >(cx, &retVal, waypoints);
return retVal;
}
void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints)
{
WaypointPath ret;
PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y };
m_LongPathfinder.ComputePath(pos.X, pos.Y, pathGoal, passClass, ret);
for (Waypoint& wp : ret.m_Waypoints)
waypoints.emplace_back(wp.x, wp.z);
}
static CParamNode GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
return self->GetTemplate(name);
}
CParamNode GetTemplate(const std::string& name)
{
if (!m_TemplateLoader.TemplateExists(name))
return CParamNode(false);
return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity");
}
static void ExitProgram(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
kill_mainloop();
}
/**
* Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights).
*/
static void DumpImage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max)
{
// TODO: this is totally not threadsafe.
VfsPath filename = L"screenshots/aidump/" + name;
if (data.size() != w*h)
{
debug_warn(L"DumpImage: data size doesn't match w*h");
return;
}
if (max == 0)
{
debug_warn(L"DumpImage: max must not be 0");
return;
}
const size_t bpp = 8;
int flags = TEX_BOTTOM_UP|TEX_GREY;
const size_t img_size = w * h * bpp/8;
const size_t hdr_size = tex_hdr_size(filename);
shared_ptr buf;
AllocateAligned(buf, hdr_size+img_size, maxSectorSize);
Tex t;
if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0)
return;
u8* img = buf.get() + hdr_size;
for (size_t i = 0; i < data.size(); ++i)
img[i] = (u8)((data[i] * 255) / max);
tex_write(&t, filename);
}
void SetRNGSeed(u32 seed)
{
m_RNG.seed(seed);
}
bool TryLoadSharedComponent(bool hasTechs)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
// we don't need to load it.
if (!m_HasSharedComponent)
return false;
// reset the value so it can be used to determine if we actually initialized it.
m_HasSharedComponent = false;
if (LoadScripts(L"common-api"))
m_HasSharedComponent = true;
else
return false;
// mainly here for the error messages
OsPath path = L"simulation/ai/common-api/";
// Constructor name is SharedScript, it's in the module API3
// TODO: Hardcoding this is bad, we need a smarter way.
JS::RootedValue AIModule(cx);
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3");
return false;
}
if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor)
|| ctor.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript");
return false;
}
// Set up the data to pass as the constructor argument
JS::RootedValue settings(cx);
m_ScriptInterface->Eval(L"({})", &settings);
JS::RootedValue playersID(cx);
m_ScriptInterface->Eval(L"({})", &playersID);
for (size_t i = 0; i < m_Players.size(); ++i)
{
JS::RootedValue val(cx);
m_ScriptInterface->ToJSVal(cx, &val, m_Players[i]->m_Player);
m_ScriptInterface->SetPropertyInt(playersID, i, val, true);
}
m_ScriptInterface->SetProperty(settings, "players", playersID);
ENSURE(m_HasLoadedEntityTemplates);
m_ScriptInterface->SetProperty(settings, "templates", m_EntityTemplates, false);
if (hasTechs)
{
m_ScriptInterface->SetProperty(settings, "techTemplates", m_TechTemplates, false);
}
else
{
// won't get the tech templates directly.
JS::RootedValue fakeTech(cx);
m_ScriptInterface->Eval("({})", &fakeTech);
m_ScriptInterface->SetProperty(settings, "techTemplates", fakeTech, false);
}
JS::AutoValueVector argv(cx);
argv.append(settings);
m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj);
if (m_SharedAIObj.get().isNull())
{
LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript");
return false;
}
return true;
}
bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty)
{
shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, m_ScriptInterface));
if (!ai->Initialise())
return false;
// this will be set to true if we need to load the shared Component.
if (!m_HasSharedComponent)
m_HasSharedComponent = ai->m_UseSharedComponent;
m_Players.push_back(ai);
return true;
}
bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
// this will be run last by InitGame.Js, passing the full game representation.
// For now it will run for the shared Component.
// This is NOT run during deserialization.
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
m_ScriptInterface->ReadStructuredClone(gameState, &state);
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, passabilityMap);
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, territoryMap);
m_PassabilityMap = passabilityMap;
m_NonPathfindingPassClasses = nonPathfindingPassClassMasks;
m_PathfindingPassClasses = pathfindingPassClassMasks;
m_LongPathfinder.Reload(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
if (m_HasSharedComponent)
{
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state);
for (size_t i = 0; i < m_Players.size(); ++i)
{
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->InitAI(state, m_SharedAIObj);
}
}
return true;
}
void UpdateGameState(const shared_ptr& gameState)
{
ENSURE(m_CommandsComputed);
m_GameState = gameState;
}
void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
ENSURE(m_CommandsComputed);
bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H;
m_PassabilityMap = passabilityMap;
if (globallyDirty)
m_LongPathfinder.Reload(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
else
m_LongPathfinder.Update(&m_PassabilityMap, dirtinessGrid);
JSContext* cx = m_ScriptInterface->GetContext();
if (dimensionChange || justDeserialized)
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, m_PassabilityMap);
else
{
// Avoid a useless memory reallocation followed by a garbage collection.
JSAutoRequest rq(cx);
JS::RootedObject mapObj(cx, &m_PassabilityMapVal.toObject());
JS::RootedValue mapData(cx);
ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData));
JS::RootedObject dataObj(cx, &mapData.toObject());
u32 length = 0;
ENSURE(JS_GetArrayLength(cx, dataObj, &length));
u32 nbytes = (u32)(length * sizeof(NavcellData));
JS::AutoCheckCannotGC nogc;
memcpy((void*)JS_GetUint16ArrayData(dataObj, nogc), m_PassabilityMap.m_Data, nbytes);
}
}
void UpdateTerritoryMap(const Grid& territoryMap)
{
ENSURE(m_CommandsComputed);
bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H;
m_TerritoryMap = territoryMap;
JSContext* cx = m_ScriptInterface->GetContext();
if (dimensionChange)
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, m_TerritoryMap);
else
{
// Avoid a useless memory reallocation followed by a garbage collection.
JSAutoRequest rq(cx);
JS::RootedObject mapObj(cx, &m_TerritoryMapVal.toObject());
JS::RootedValue mapData(cx);
ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData));
JS::RootedObject dataObj(cx, &mapData.toObject());
u32 length = 0;
ENSURE(JS_GetArrayLength(cx, dataObj, &length));
u32 nbytes = (u32)(length * sizeof(u8));
JS::AutoCheckCannotGC nogc;
memcpy((void*)JS_GetUint8ArrayData(dataObj, nogc), m_TerritoryMap.m_Data, nbytes);
}
}
void StartComputation()
{
m_CommandsComputed = false;
}
void WaitToFinishComputation()
{
if (!m_CommandsComputed)
{
PerformComputation();
m_CommandsComputed = true;
}
}
void GetCommands(std::vector& commands)
{
WaitToFinishComputation();
commands.clear();
commands.resize(m_Players.size());
for (size_t i = 0; i < m_Players.size(); ++i)
{
commands[i].player = m_Players[i]->m_Player;
commands[i].commands = m_Players[i]->m_Commands;
}
}
void RegisterTechTemplates(const shared_ptr& techTemplates)
{
m_ScriptInterface->ReadStructuredClone(techTemplates, &m_TechTemplates);
}
void LoadEntityTemplates(const std::vector >& templates)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
m_HasLoadedEntityTemplates = true;
m_ScriptInterface->Eval("({})", &m_EntityTemplates);
JS::RootedValue val(cx);
for (size_t i = 0; i < templates.size(); ++i)
{
templates[i].second->ToJSVal(cx, false, &val);
m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true);
}
}
void Serialize(std::ostream& stream, bool isDebug)
{
WaitToFinishComputation();
if (isDebug)
{
CDebugSerializer serializer(*m_ScriptInterface, stream);
serializer.Indent(4);
SerializeState(serializer);
}
else
{
CStdSerializer serializer(*m_ScriptInterface, stream);
// TODO: see comment in Deserialize()
serializer.SetSerializablePrototypes(m_SerializablePrototypes);
SerializeState(serializer);
}
}
void SerializeState(ISerializer& serializer)
{
if (m_Players.empty())
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
std::stringstream rngStream;
rngStream << m_RNG;
serializer.StringASCII("rng", rngStream.str(), 0, 32);
serializer.NumberU32_Unbounded("turn", m_TurnNum);
serializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
JS::RootedValue sharedData(cx);
if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData))
LOGERROR("AI shared script Serialize call failed");
serializer.ScriptVal("sharedData", &sharedData);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
serializer.String("name", m_Players[i]->m_AIName, 1, 256);
serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player);
serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty);
serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size());
for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j)
{
JS::RootedValue val(cx);
m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val);
serializer.ScriptVal("command", &val);
}
bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize");
if (hasCustomSerialize)
{
JS::RootedValue scriptData(cx);
if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData))
LOGERROR("AI script Serialize call failed");
serializer.ScriptVal("data", &scriptData);
}
else
{
serializer.ScriptVal("data", &m_Players[i]->m_Obj);
}
}
// AI pathfinder
SerializeMap()(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(serializer, "pathfinding pass classes", m_PathfindingPassClasses);
serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W);
serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H);
serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data,
m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData));
}
void Deserialize(std::istream& stream, u32 numAis)
{
m_PlayerMetadata.clear();
m_Players.clear();
if (numAis == 0)
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad
CStdDeserializer deserializer(*m_ScriptInterface, stream);
std::string rngString;
std::stringstream rngStream;
deserializer.StringASCII("rng", rngString, 0, 32);
rngStream << rngString;
rngStream >> m_RNG;
deserializer.NumberU32_Unbounded("turn", m_TurnNum);
deserializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
TryLoadSharedComponent(false);
JS::RootedValue sharedData(cx);
deserializer.ScriptVal("sharedData", &sharedData);
if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData))
LOGERROR("AI shared script Deserialize call failed");
}
for (size_t i = 0; i < numAis; ++i)
{
std::wstring name;
player_id_t player;
u8 difficulty;
deserializer.String("name", name, 1, 256);
deserializer.NumberI32_Unbounded("player", player);
deserializer.NumberU8_Unbounded("difficulty",difficulty);
if (!AddPlayer(name, player, difficulty))
throw PSERROR_Deserialize_ScriptError();
u32 numCommands;
deserializer.NumberU32_Unbounded("num commands", numCommands);
m_Players.back()->m_Commands.reserve(numCommands);
for (size_t j = 0; j < numCommands; ++j)
{
JS::RootedValue val(cx);
deserializer.ScriptVal("command", &val);
m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val));
}
// TODO: this is yucky but necessary while the AIs are sharing data between contexts;
// ideally a new (de)serializer instance would be created for each player
// so they would have a single, consistent script context to use and serializable
// prototypes could be stored in their ScriptInterface
deserializer.SetSerializablePrototypes(m_DeserializablePrototypes);
bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize");
if (hasCustomDeserialize)
{
JS::RootedValue scriptData(cx);
deserializer.ScriptVal("data", &scriptData);
if (m_Players[i]->m_UseSharedComponent)
{
if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj))
LOGERROR("AI script Deserialize call failed");
}
else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData))
{
LOGERROR("AI script deserialize() call failed");
}
}
else
{
deserializer.ScriptVal("data", &m_Players.back()->m_Obj);
}
}
// AI pathfinder
SerializeMap()(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(deserializer, "pathfinding pass classes", m_PathfindingPassClasses);
u16 mapW, mapH;
deserializer.NumberU16_Unbounded("pathfinder grid w", mapW);
deserializer.NumberU16_Unbounded("pathfinder grid h", mapH);
m_PassabilityMap = Grid(mapW, mapH);
deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData));
m_LongPathfinder.Reload(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses);
}
int getPlayerSize()
{
return m_Players.size();
}
void RegisterSerializablePrototype(std::wstring name, JS::HandleValue proto)
{
// Require unique prototype and name (for reverse lookup)
// TODO: this is yucky - see comment in Deserialize()
ENSURE(proto.isObject() && "A serializable prototype has to be an object!");
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedObject obj(cx, &proto.toObject());
if (m_SerializablePrototypes->has(obj) || m_DeserializablePrototypes.find(name) != m_DeserializablePrototypes.end())
{
LOGERROR("RegisterSerializablePrototype called with same prototype multiple times: p=%p n='%s'", (void *)obj.get(), utf8_from_wstring(name));
return;
}
m_SerializablePrototypes->add(cx, obj, name);
m_DeserializablePrototypes[name] = JS::Heap(obj);
}
private:
static void Trace(JSTracer *trc, void *data)
{
reinterpret_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc)
{
for (std::pair>& prototype : m_DeserializablePrototypes)
JS_CallObjectTracer(trc, &prototype.second, "CAIWorker::m_DeserializablePrototypes");
for (std::pair>& metadata : m_PlayerMetadata)
JS_CallValueTracer(trc, &metadata.second, "CAIWorker::m_PlayerMetadata");
}
void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out)
{
if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end())
{
// Load and cache the AI player metadata
m_ScriptInterface->ReadJSONFile(path, out);
m_PlayerMetadata[path] = JS::Heap(out);
return;
}
out.set(m_PlayerMetadata[path].get());
}
void PerformComputation()
{
// Deserialize the game state, to pass to the AI's HandleMessage
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
{
PROFILE3("AI compute read state");
m_ScriptInterface->ReadStructuredClone(m_GameState, &state);
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
}
// It would be nice to do
// m_ScriptInterface->FreezeObject(state.get(), true);
// to prevent AI scripts accidentally modifying the state and
// affecting other AI scripts they share it with. But the performance
// cost is far too high, so we won't do that.
// If there is a shared component, run it
if (m_HasSharedComponent)
{
PROFILE3("AI run shared component");
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
PROFILE3("AI script");
PROFILE2_ATTR("player: %d", m_Players[i]->m_Player);
PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str());
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj);
else
m_Players[i]->Run(state, m_Players[i]->m_Player);
}
}
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptRuntime;
shared_ptr m_ScriptInterface;
boost::rand48 m_RNG;
u32 m_TurnNum;
JS::PersistentRootedValue m_EntityTemplates;
bool m_HasLoadedEntityTemplates;
JS::PersistentRootedValue m_TechTemplates;
std::map > m_PlayerMetadata;
std::vector > m_Players; // use shared_ptr just to avoid copying
bool m_HasSharedComponent;
JS::PersistentRootedValue m_SharedAIObj;
std::vector m_Commands;
std::set m_LoadedModules;
shared_ptr m_GameState;
Grid m_PassabilityMap;
JS::PersistentRootedValue m_PassabilityMapVal;
Grid m_TerritoryMap;
JS::PersistentRootedValue m_TerritoryMapVal;
std::map m_NonPathfindingPassClasses;
std::map m_PathfindingPassClasses;
LongPathfinder m_LongPathfinder;
bool m_CommandsComputed;
shared_ptr > m_SerializablePrototypes;
std::map > m_DeserializablePrototypes;
CTemplateLoader m_TemplateLoader;
};
/**
* Implementation of ICmpAIManager.
*/
class CCmpAIManager : public ICmpAIManager
{
public:
static void ClassInit(CComponentManager& UNUSED(componentManager))
{
}
DEFAULT_COMPONENT_ALLOCATOR(AIManager)
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_TerritoriesDirtyID = 0;
m_TerritoriesDirtyBlinkingID = 0;
m_JustDeserialized = false;
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& serialize)
{
serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize());
// Because the AI worker uses its own ScriptInterface, we can't use the
// ISerializer (which was initialised with the simulation ScriptInterface)
// directly. So we'll just grab the ISerializer's stream and write to it
// with an independent serializer.
m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug());
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
u32 numAis;
deserialize.NumberU32_Unbounded("num ais", numAis);
if (numAis > 0)
LoadUsedEntityTemplates();
m_Worker.Deserialize(deserialize.GetStream(), numAis);
m_JustDeserialized = true;
}
virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty)
{
LoadUsedEntityTemplates();
m_Worker.AddPlayer(id, player, difficulty);
// AI players can cheat and see through FoW/SoD, since that greatly simplifies
// their implementation.
// (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD)
CmpPtr cmpRangeManager(GetSystemEntity());
if (cmpRangeManager)
cmpRangeManager->SetLosRevealAll(player, true);
}
virtual void SetRNGSeed(u32 seed)
{
m_Worker.SetRNGSeed(seed);
}
virtual void TryLoadSharedComponent()
{
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
// load the technology templates
CmpPtr cmpDataTemplateManager(GetSystemEntity());
ENSURE(cmpDataTemplateManager);
JS::RootedValue techTemplates(cx);
cmpDataTemplateManager->GetAllTechs(&techTemplates);
m_Worker.RegisterTechTemplates(scriptInterface.WriteStructuredClone(techTemplates));
m_Worker.TryLoadSharedComponent(true);
}
virtual void RunGamestateInit()
{
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
// We flush events from the initialization so we get a clean state now.
JS::RootedValue state(cx);
cmpAIInterface->GetFullRepresentation(&state, true);
// Get the passability data
Grid dummyGrid;
const Grid* passabilityMap = &dummyGrid;
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
// Get the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
Grid dummyGrid2;
const Grid* territoryMap = &dummyGrid2;
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID))
territoryMap = &cmpTerritoryManager->GetTerritoryGrid();
LoadPathfinderClasses(state);
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
if (cmpPathfinder)
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
virtual void StartComputation()
{
PROFILE("AI setup");
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
if (m_Worker.getPlayerSize() == 0)
return;
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
JS::RootedValue state(cx);
if (m_JustDeserialized)
cmpAIInterface->GetFullRepresentation(&state, false);
else
cmpAIInterface->GetRepresentation(&state);
LoadPathfinderClasses(state); // add the pathfinding classes to it
// Update the game state
m_Worker.UpdateGameState(scriptInterface.WriteStructuredClone(state));
// Update the pathfinding data
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
{
const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation();
if (dirtinessInformations.dirty || m_JustDeserialized)
{
const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid();
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.UpdatePathfinder(passabilityMap,
dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized,
nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
cmpPathfinder->FlushAIPathfinderDirtinessInformation();
}
// Update the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized))
{
const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid();
m_Worker.UpdateTerritoryMap(territoryMap);
}
m_Worker.StartComputation();
m_JustDeserialized = false;
}
virtual void PushCommands()
{
std::vector commands;
m_Worker.GetCommands(commands);
CmpPtr cmpCommandQueue(GetSystemEntity());
if (!cmpCommandQueue)
return;
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue clonedCommandVal(cx);
for (size_t i = 0; i < commands.size(); ++i)
{
for (size_t j = 0; j < commands[i].commands.size(); ++j)
{
scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal);
cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal);
}
}
}
private:
size_t m_TerritoriesDirtyID;
size_t m_TerritoriesDirtyBlinkingID;
bool m_JustDeserialized;
/**
* Load the templates of all entities on the map (called when adding a new AI player for a new game
* or when deserializing)
*/
void LoadUsedEntityTemplates()
{
if (m_Worker.HasLoadedEntityTemplates())
return;
CmpPtr cmpTemplateManager(GetSystemEntity());
ENSURE(cmpTemplateManager);
std::vector templateNames = cmpTemplateManager->FindUsedTemplates();
std::vector > usedTemplates;
usedTemplates.reserve(templateNames.size());
for (const std::string& name : templateNames)
{
const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name);
if (node)
usedTemplates.emplace_back(name, node);
}
// Send the data to the worker
m_Worker.LoadEntityTemplates(usedTemplates);
}
void LoadPathfinderClasses(JS::HandleValue state)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue classesVal(cx);
scriptInterface.Eval("({})", &classesVal);
std::map classes;
cmpPathfinder->GetPassabilityClasses(classes);
for (std::map::iterator it = classes.begin(); it != classes.end(); ++it)
scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true);
scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true);
}
CAIWorker m_Worker;
};
REGISTER_COMPONENT_TYPE(AIManager)