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)