Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 18429) @@ -1,857 +1,849 @@ var API3 = function(m) { /** * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ m.GameState = function() { this.ai = null; // must be updated by the AIs. }; m.GameState.prototype.init = function(SharedScript, state, player) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.timeElapsed = SharedScript.timeElapsed; this.circularMap = SharedScript.circularMap; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; this.entities = SharedScript.entities; this.player = player; this.playerData = SharedScript.playersData[this.player]; this.barterPrices = SharedScript.barterPrices; this.gameType = SharedScript.gameType; // get the list of possible phases for this civ: // we assume all of them are researchable from the civil centre this.phases = [ { name: "phase_village" }, { name: "phase_town" }, { name: "phase_city" } ]; let cctemplate = this.getTemplate(this.applyCiv("structures/{civ}_civil_centre")); if (!cctemplate) return; let techs = cctemplate.researchableTechs(this.civ()); for (let i = 0; i < this.phases.length; ++i) { let k = techs.indexOf(this.phases[i].name); if (k !== -1) { this.phases[i].requirements = (this.getTemplate(techs[k]))._template.requirements; continue; } for (let tech of techs) { let template = (this.getTemplate(tech))._template; if (template.replaces && template.replaces.indexOf(this.phases[i].name) != -1) { this.phases[i].name = tech; this.phases[i].requirements = template.requirements; break; } } } }; m.GameState.prototype.update = function(SharedScript) { this.timeElapsed = SharedScript.timeElapsed; this.playerData = SharedScript.playersData[this.player]; this.barterPrices = SharedScript.barterPrices; }; m.GameState.prototype.updatingCollection = function(id, filter, collection) { let gid = this.player + "-" + id; // automatically add the player ID return this.updatingGlobalCollection(gid, filter, collection); }; m.GameState.prototype.destroyCollection = function(id) { let gid = this.player + "-" + id; // automatically add the player ID this.destroyGlobalCollection(gid); }; m.GameState.prototype.getEC = function(id) { let gid = this.player + "-" + id; // automatically add the player ID return this.getGEC(gid); }; m.GameState.prototype.updatingGlobalCollection = function(id, filter, collection) { if (this.EntCollecNames.has(id)) return this.EntCollecNames.get(id); let newCollection = collection !== undefined ? collection.filter(filter) : this.entities.filter(filter); newCollection.registerUpdates(); this.EntCollecNames.set(id, newCollection); return newCollection; }; m.GameState.prototype.destroyGlobalCollection = function(id) { if (!this.EntCollecNames.has(id)) return; this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(id)); this.EntCollecNames.delete(id); }; m.GameState.prototype.getGEC = function(id) { if (!this.EntCollecNames.has(id)) return undefined; return this.EntCollecNames.get(id); }; m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; m.GameState.prototype.getBarterPrices = function() { return this.barterPrices; }; m.GameState.prototype.getGameType = function() { return this.gameType; }; m.GameState.prototype.getTemplate = function(type) { if (this.techTemplates[type] !== undefined) return new m.Technology(this.techTemplates, type); if (!this.templates[type]) return null; return new m.Template(this.templates[type]); }; m.GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; m.GameState.prototype.civ = function() { return this.playerData.civ; }; m.GameState.prototype.currentPhase = function() { for (let i = this.phases.length; i > 0; --i) if (this.isResearched(this.phases[i-1].name)) return i; return 0; }; m.GameState.prototype.townPhase = function() { return this.phases[1].name; }; m.GameState.prototype.cityPhase = function() { return this.phases[2].name; }; m.GameState.prototype.getPhaseRequirements = function(i) { if (!this.phases[i-1].requirements) return undefined; let requirements = this.phases[i-1].requirements; if (requirements.number) return requirements; else if (requirements.all) { for (let req of requirements.all) if (req.number) return req; } return undefined; }; m.GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs[template] !== undefined; }; /** true if started or queued */ m.GameState.prototype.isResearching = function(template) { return this.playerData.researchStarted[template] !== undefined || this.playerData.researchQueued[template] !== undefined; }; /** this is an "in-absolute" check that doesn't check if we have a building to research from. */ m.GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) { let template = this.getTemplate(techTemplateName); if (!template) return false; // researching or already researched: NOO. if (this.playerData.researchQueued[techTemplateName] || this.playerData.researchStarted[techTemplateName] || this.playerData.researchedTechs[techTemplateName]) return false; if (noRequirementCheck === true) return true; // not already researched, check if we can. // basically a copy of the function in technologyManager since we can't use it. // Checks the requirements for a technology to see if it can be researched at the current time // The technology which this technology supersedes is required if (template.supersedes() && !this.playerData.researchedTechs[template.supersedes()]) return false; // if this is a pair, we must check that the pair tech is not being researched if (template.pair()) { let other = template.pairedWith(); if (this.playerData.researchQueued[other] || this.playerData.researchStarted[other] || this.playerData.researchedTechs[other]) return false; } return this.checkTechRequirements(template.requirements()); }; /** * Private function for checking a set of requirements is met * basically copies TechnologyManager */ m.GameState.prototype.checkTechRequirements = function(reqs) { // If there are no requirements then all requirements are met if (!reqs) return true; if (reqs.all) return reqs.all.every(r => this.checkTechRequirements(r)); if (reqs.any) return reqs.any.some(r => this.checkTechRequirements(r)); if (reqs.civ) return this.playerData.civ == reqs.civ; if (reqs.notciv) return this.playerData.civ != reqs.notciv; if (reqs.tech) return this.playerData.researchedTechs[reqs.tech] !== undefined && this.playerData.researchedTechs[reqs.tech]; if (reqs.class && reqs.numberOfTypes) return this.playerData.typeCountsByClass[reqs.class] && Object.keys(this.playerData.typeCountsByClass[reqs.class]).length >= reqs.numberOfTypes; if (reqs.class && reqs.number) return this.playerData.classCounts[reqs.class] && this.playerData.classCounts[reqs.class] >= reqs.number; // The technologies requirements are not a recognised format error("Bad requirements " + uneval(reqs)); return false; }; m.GameState.prototype.getMap = function() { return this.sharedScript.passabilityMap; }; m.GameState.prototype.getPassabilityClassMask = function(name) { - if (!(name in this.sharedScript.passabilityClasses)) + if (!this.sharedScript.passabilityClasses[name]) error("Tried to use invalid passability class name '" + name + "'"); return this.sharedScript.passabilityClasses[name]; }; m.GameState.prototype.getResources = function() { return new m.Resources(this.playerData.resourceCounts); }; m.GameState.prototype.getPopulation = function() { return this.playerData.popCount; }; m.GameState.prototype.getPopulationLimit = function() { return this.playerData.popLimit; }; m.GameState.prototype.getPopulationMax = function() { return this.playerData.popMax; }; m.GameState.prototype.getPlayerID = function() { return this.player; }; m.GameState.prototype.hasAllies = function() { for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player) return true; return false; }; m.GameState.prototype.isPlayerAlly = function(id) { return this.playerData.isAlly[id]; }; m.GameState.prototype.isPlayerMutualAlly = function(id) { return this.playerData.isMutualAlly[id]; }; m.GameState.prototype.isPlayerEnemy = function(id) { return this.playerData.isEnemy[id]; }; m.GameState.prototype.getEnemies = function() { let ret = []; for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i]) ret.push(+i); return ret; }; m.GameState.prototype.getNeutrals = function() { let ret = []; for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i]) ret.push(+i); return ret; }; m.GameState.prototype.getAllies = function() { let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i]) ret.push(+i); return ret; }; m.GameState.prototype.getExclusiveAllies = function() { // Player is not included let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player) ret.push(+i); return ret; }; m.GameState.prototype.isEntityAlly = function(ent) { - if (!ent || !ent.owner) + if (!ent) return false; - if (typeof ent.owner === "function") - return this.playerData.isAlly[ent.owner()]; - return this.playerData.isAlly[ent.owner]; + return this.playerData.isAlly[ent.owner()]; }; m.GameState.prototype.isEntityExclusiveAlly = function(ent) { - if (!ent || !ent.owner) + if (!ent) return false; - if (typeof ent.owner === "function") - return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; - return this.playerData.isAlly[ent.owner] && ent.owner !== this.player; + return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; }; m.GameState.prototype.isEntityEnemy = function(ent) { - if (!ent || !ent.owner) + if (!ent) return false; - if (typeof ent.owner === "function") - return this.playerData.isEnemy[ent.owner()]; - return this.playerData.isEnemy[ent.owner]; + return this.playerData.isEnemy[ent.owner()]; }; m.GameState.prototype.isEntityOwn = function(ent) { - if (!ent || !ent.owner) + if (!ent) return false; - if (typeof ent.owner === "function") - return ent.owner() === this.player; - return ent.owner === this.player; + return ent.owner() === this.player; }; m.GameState.prototype.getEntityById = function(id) { if (this.entities._entities.has(+id)) return this.entities._entities.get(+id); return undefined; }; m.GameState.prototype.getEntities = function() { return this.entities; }; m.GameState.prototype.getStructures = function() { return this.updatingGlobalCollection("structures", m.Filters.byClass("Structure"), this.entities); }; m.GameState.prototype.getOwnEntities = function() { return this.updatingGlobalCollection("" + this.player + "-entities", m.Filters.byOwner(this.player)); }; m.GameState.prototype.getOwnStructures = function() { return this.updatingGlobalCollection("" + this.player + "-structures", m.Filters.byClass("Structure"), this.getOwnEntities()); }; m.GameState.prototype.getOwnUnits = function() { return this.updatingGlobalCollection("" + this.player + "-units", m.Filters.byClass("Unit"), this.getOwnEntities()); }; m.GameState.prototype.getAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getAllies())); }; m.GameState.prototype.getExclusiveAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getExclusiveAllies())); }; m.GameState.prototype.getAllyStructures = function() { return this.updatingCollection("ally-structures", m.Filters.byClass("Structure"), this.getAllyEntities()); }; m.GameState.prototype.resetAllyStructures = function() { this.destroyCollection("ally-structures"); }; m.GameState.prototype.getNeutralStructures = function() { return this.getStructures().filter(m.Filters.byOwners(this.getNeutrals())); }; m.GameState.prototype.getEnemyEntities = function(enemyID) { if (enemyID === undefined) return this.entities.filter(m.Filters.byOwners(this.getEnemies())); return this.updatingGlobalCollection("" + enemyID + "-entities", m.Filters.byOwner(enemyID)); }; m.GameState.prototype.getEnemyStructures = function(enemyID) { if (enemyID === undefined) return this.updatingCollection("enemy-structures", m.Filters.byClass("Structure"), this.getEnemyEntities()); return this.updatingGlobalCollection("" + enemyID + "-structures", m.Filters.byClass("Structure"), this.getEnemyEntities(enemyID)); }; m.GameState.prototype.resetEnemyStructures = function() { this.destroyCollection("enemy-structures"); }; m.GameState.prototype.getEnemyUnits = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Unit")); return this.updatingGlobalCollection("" + enemyID + "-units", m.Filters.byClass("Unit"), this.getEnemyEntities(enemyID)); }; /** if maintain is true, this will be stored. Otherwise it's one-shot. */ m.GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain) { if (maintain === true) return this.updatingCollection(key + "-" + value, m.Filters.byMetadata(this.player, key, value),this.getOwnEntities()); return this.getOwnEntities().filter(m.Filters.byMetadata(this.player, key, value)); }; m.GameState.prototype.getOwnEntitiesByRole = function(role, maintain) { return this.getOwnEntitiesByMetadata("role", role, maintain); }; m.GameState.prototype.getOwnEntitiesByType = function(type, maintain) { let filter = m.Filters.byType(type); if (maintain === true) return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnEntitiesByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("class-" + cls, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnFoundationsByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("foundations-class-" + cls, filter, this.getOwnFoundations()); return this.getOwnFoundations().filter(filter); }; m.GameState.prototype.getOwnTrainingFacilities = function() { return this.updatingGlobalCollection("" + this.player + "-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities()); }; m.GameState.prototype.getOwnResearchFacilities = function() { return this.updatingGlobalCollection("" + this.player + "-research-facilities", m.Filters.byResearchAvailable(this.playerData.civ), this.getOwnEntities()); }; m.GameState.prototype.countEntitiesByType = function(type, maintain) { return this.getOwnEntitiesByType(type, maintain).length; }; m.GameState.prototype.countEntitiesAndQueuedByType = function(type, maintain) { let template = this.getTemplate(type); if (!template) return 0; let count = this.countEntitiesByType(type, maintain); // Count building foundations if (template.hasClass("Structure") === true) count += this.countFoundationsByType(type, true); else if (template.resourceSupplyType() !== undefined) // animal resources count += this.countEntitiesByType("resource|" + type, true); else { // Count entities in building production queues // TODO: maybe this fails for corrals. this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.unitTemplate == type) count += item.count; }); } return count; }; m.GameState.prototype.countFoundationsByType = function(type, maintain) { let foundationType = "foundation|" + type; if (maintain === true) return this.updatingCollection("foundation-type-" + type, m.Filters.byType(foundationType), this.getOwnFoundations()).length; let count = 0; this.getOwnStructures().forEach(function(ent) { if (ent.templateName() == foundationType) ++count; }); return count; }; m.GameState.prototype.countOwnEntitiesByRole = function(role) { return this.getOwnEntitiesByRole(role, "true").length; }; m.GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { let count = this.countOwnEntitiesByRole(role); // Count entities in building production queues this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata.role && item.metadata.role == role) count += item.count; }); return count; }; m.GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { // Count entities in building production queues let count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); return count; }; m.GameState.prototype.getOwnFoundations = function() { return this.updatingGlobalCollection("" + this.player + "-foundations", m.Filters.isFoundation(), this.getOwnStructures()); }; m.GameState.prototype.getOwnDropsites = function(resource) { if (resource !== undefined) return this.updatingCollection("dropsite-" + resource, m.Filters.isDropsite(resource), this.getOwnEntities()); return this.updatingCollection("dropsite-all", m.Filters.isDropsite(), this.getOwnEntities()); }; m.GameState.prototype.getResourceSupplies = function(resource) { return this.updatingGlobalCollection("resource-" + resource, m.Filters.byResource(resource), this.getEntities()); }; m.GameState.prototype.getHuntableSupplies = function() { return this.updatingGlobalCollection("resource-hunt", m.Filters.isHuntable(), this.getEntities()); }; m.GameState.prototype.getFishableSupplies = function() { return this.updatingGlobalCollection("resource-fish", m.Filters.isFishable(), this.getEntities()); }; /** This returns only units from buildings. */ m.GameState.prototype.findTrainableUnits = function(classes, anticlasses) { let allTrainable = []; let civ = this.playerData.civ; this.getOwnStructures().forEach(function(ent) { let trainable = ent.trainableEntities(civ); if (!trainable) return; for (let unit of trainable) if (allTrainable.indexOf(unit) === -1) allTrainable.push(unit); }); let ret = []; let limits = this.getEntityLimits(); let current = this.getEntityCounts(); for (let trainable of allTrainable) { if (this.isTemplateDisabled(trainable)) continue; let template = this.getTemplate(trainable); if (!template || !template.available(this)) continue; let okay = true; for (let clas of classes) { if (template.hasClass(clas)) continue; okay = false; break; } if (!okay) continue; for (let clas of anticlasses) { if (!template.hasClass(clas)) continue; okay = false; break; } if (!okay) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; ret.push( [trainable, template] ); } return ret; }; /** * Return all techs which can currently be researched * Does not factor cost. * If there are pairs, both techs are returned. */ m.GameState.prototype.findAvailableTech = function() { let allResearchable = []; let civ = this.playerData.civ; this.getOwnEntities().forEach(function(ent) { let searchable = ent.researchableTechs(civ); if (!searchable) return; for (let tech of searchable) if (allResearchable.indexOf(tech) === -1) allResearchable.push(tech); }); let ret = []; for (let tech of allResearchable) { let template = this.getTemplate(tech); if (template.pairDef()) { let techs = template.getPairedTechs(); if (this.canResearch(techs[0]._templateName)) ret.push([techs[0]._templateName, techs[0]] ); if (this.canResearch(techs[1]._templateName)) ret.push([techs[1]._templateName, techs[1]] ); } else if (this.canResearch(tech) && template._templateName != this.townPhase() && template._templateName != this.cityPhase()) ret.push( [tech, template] ); } return ret; }; /** * Return true if we have a building able to train that template */ m.GameState.prototype.hasTrainer = function(template) { let civ = this.playerData.civ; for (let ent of this.getOwnTrainingFacilities().values()) { let trainable = ent.trainableEntities(civ); if (trainable && trainable.indexOf(template) !== -1) return true; } return false; }; /** * Find buildings able to train that template. */ m.GameState.prototype.findTrainers = function(template) { let civ = this.playerData.civ; return this.getOwnTrainingFacilities().filter(function(ent) { let trainable = ent.trainableEntities(civ); return trainable && trainable.indexOf(template) !== -1; }); }; /** * Get any unit that is capable of constructing the given building type. */ m.GameState.prototype.findBuilder = function(template) { for (let ent of this.getOwnUnits().values()) { let buildable = ent.buildableEntities(); if (buildable && buildable.indexOf(template) !== -1) return ent; } return undefined; }; /** Return true if one of our buildings is capable of researching the given tech */ m.GameState.prototype.hasResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return false; let civ = this.playerData.civ; for (let ent of this.getOwnResearchFacilities().values()) { let techs = ent.researchableTechs(civ); for (let tech of techs) { let temp = this.getTemplate(tech); if (temp.pairDef()) { let pairedTechs = temp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } } return false; }; /** Find buildings that are capable of researching the given tech */ m.GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return undefined; let self = this; let civ = this.playerData.civ; return this.getOwnResearchFacilities().filter(function(ent) { let techs = ent.researchableTechs(civ); for (let tech of techs) { let thisTemp = self.getTemplate(tech); if (thisTemp.pairDef()) { let pairedTechs = thisTemp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } return false; }); }; m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; m.GameState.prototype.isTemplateDisabled = function(template) { if (!this.playerData.disabledTemplates[template]) return false; return this.playerData.disabledTemplates[template]; }; /** Checks whether the maximum number of buildings have been constructed for a certain catergory */ m.GameState.prototype.isEntityLimitReached = function(category) { if (this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined) return false; return this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]; }; m.GameState.prototype.getTraderTemplatesGains = function() { let shipMechantTemplateName = this.applyCiv("units/{civ}_ship_merchant"); let supportTraderTemplateName = this.applyCiv("units/{civ}_support_trader"); let shipMerchantTemplate = !this.isTemplateDisabled(shipMechantTemplateName) && this.getTemplate(shipMechantTemplateName); let supportTraderTemplate = !this.isTemplateDisabled(supportTraderTemplateName) && this.getTemplate(supportTraderTemplateName); return { "navalGainMultiplier": shipMerchantTemplate && shipMerchantTemplate.gainMultiplier(), "landGainMultiplier": supportTraderTemplate && supportTraderTemplate.gainMultiplier() }; }; 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 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 18429) @@ -1,463 +1,463 @@ var API3 = function(m) { /** Shared script handling templates and basic terrain analysis */ m.SharedScript = function(settings) { if (!settings) return; this._players = settings.players; this._templates = settings.templates; this._derivedTemplates = {}; this._techTemplates = settings.techTemplates; this._entityMetadata = {}; for (let i in this._players) this._entityMetadata[this._players[i]] = {}; // 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; // A few notes about these maps. They're updated by checking for "create" and "destroy" events for all resources // TODO: change the map when the resource amounts change for at least stone and metal mines. this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement. // Resource maps data. // By how much to divide the resource amount when filling the map (ie a tree having 200 wood is "4"). this.decreaseFactor = {"wood": 50.0, "stone": 90.0, "metal": 90.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._derivedTemplates = {}; this.isDeserialized = true; }; /** * Components that will be disabled in foundation entity templates. * (This is a bit yucky and fragile since it's the inverse of * CCmpTemplateManager::CopyFoundationSubset and only includes components * that our Template class currently uses.) */ m.g_FoundationForbiddenComponents = { "ProductionQueue": 1, "ResourceSupply": 1, "ResourceDropsite": 1, "GarrisonHolder": 1, }; /** * Components that will be disabled in resource entity templates. * Roughly the inverse of CCmpTemplateManager::CopyResourceSubset. */ m.g_ResourceForbiddenComponents = { "Cost": 1, "Decay": 1, "Health": 1, "UnitAI": 1, "UnitMotion": 1, "Vision": 1 }; m.SharedScript.prototype.GetTemplate = function(name) { if (this._templates[name]) return this._templates[name]; if (this._derivedTemplates[name]) return this._derivedTemplates[name]; // If this is a foundation template, construct it automatically if (name.indexOf("foundation|") !== -1) { let base = this.GetTemplate(name.substr(11)); let foundation = {}; for (let key in base) if (!m.g_FoundationForbiddenComponents[key]) foundation[key] = base[key]; this._derivedTemplates[name] = foundation; return foundation; } else if (name.indexOf("resource|") !== -1) { let base = this.GetTemplate(name.substr(9)); let resource = {}; for (let key in base) if (!m.g_ResourceForbiddenComponents[key]) resource[key] = base[key]; this._derivedTemplates[name] = resource; return resource; } error("Tried to retrieve invalid template '"+name+"'"); return null; }; /** * 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(); for (let i = 0; i < state.players.length; ++i) this._templatesModifications[i] = {}; } this.ApplyTemplatesDelta(state); this.passabilityClasses = state.passabilityClasses; this.players = this._players; this.playersData = state.players; this.timeElapsed = state.timeElapsed; this.circularMap = state.circularMap; this.mapSize = state.mapSize; this.gameType = state.gameType; this.barterPrices = state.barterPrices; 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); + 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); + 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); // defined in TerrainAnalysis.js this.createResourceMaps(this); this.gameState = {}; for (let i in this._players) { this.gameState[this._players[i]] = new m.GameState(); this.gameState[this._players[i]].init(this,state, this._players[i]); } }; /** * 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.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, 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. let CreateEvents = state.events.Create; for (let i = 0; i < CreateEvents.length; ++i) { let evt = CreateEvents[i]; 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 p in this._players) { this._entityMetadata[this._players[p]][evt.newentity] = this._entityMetadata[this._players[p]][evt.entity]; this._entityMetadata[this._players[p]][evt.entity] = {}; } } for (let evt of state.events.TrainingFinished) { // Apply metadata stored in training queues for (let entId of evt.entities) 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) { // we'll move metadata. if (!this._entities.has(evt.entity)) continue; let ent = this._entities.get(evt.entity); let newEnt = this._entities.get(evt.newentity); if (this._entityMetadata[ent.owner()] && this._entityMetadata[ent.owner()][evt.entity] !== undefined) for (let key in this._entityMetadata[ent.owner()][evt.entity]) this.setMetadata(ent.owner(), newEnt, key, this._entityMetadata[ent.owner()][evt.entity][key]); 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]); } let DestroyEvents = state.events.Destroy; for (let i = 0; i < DestroyEvents.length; ++i) { let evt = DestroyEvents[i]; // A small warning: javascript "delete" does not actually delete, it only removes the reference in this object. // the "deleted" object remains in memory, and any older reference to it will still reference it as if it were not "deleted". // Worse, they might prevent it from being garbage collected, thus making it stay alive and consuming ram needlessly. // So take care, and if you encounter a weird bug with deletion not appearing to work correctly, this is probably why. 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 j in this._players) evt.metadata[this._players[j]] = this._entityMetadata[this._players[j]][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 j in this._players) delete this._entityMetadata[this._players[j]][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[player][template]) this._templatesModifications[player][template] = new Map(); let modif = this._templatesModifications[player][template]; 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) + 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 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js (revision 18429) @@ -1,506 +1,507 @@ 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(sharedScript) { for (let resource in this.decreaseFactor) { // if there is no resourceMap create one with an influence for everything with that resource if (!this.resourceMaps[resource]) { - // We're creting them 8-bit. Things could go above 255 if there are really tons of resources + // 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(sharedScript, "resource"); this.CCResourceMaps[resource] = new m.Map(sharedScript, "resource"); } } let cellSize = this.resourceMaps.wood.cellSize; for (let ent of sharedScript._entities.values()) { - if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { + if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") + { let resource = ent.resourceSupplyType().generic; if (!this.resourceMaps[resource]) continue; let x = Math.floor(ent.position()[0] / cellSize); let z = Math.floor(ent.position()[1] / cellSize); let strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); if (resource === "wood") { this.CCResourceMaps[resource].addInfluence(x, z, 60/cellSize, strength, "constant"); this.resourceMaps[resource].addInfluence(x, z, 36/cellSize, strength/2, "constant"); this.resourceMaps[resource].addInfluence(x, z, 36/cellSize, strength/2); } else if (resource === "stone" || resource === "metal") { this.CCResourceMaps[resource].addInfluence(x, z, 120/cellSize, strength, "constant"); this.resourceMaps[resource].addInfluence(x, z, 48/cellSize, strength/2, "constant"); this.resourceMaps[resource].addInfluence(x, z, 48/cellSize, strength/2); } } } }; /** * TODO: make it regularly update stone+metal mines and their resource levels. * 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(sharedScript, events) { for (let resource in this.decreaseFactor) { // if there is no resourceMap create one with an influence for everything with that resource if (!this.resourceMaps[resource]) { - // We're creting them 8-bit. Things could go above 255 if there are really tons of resources + // 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(sharedScript, "resource"); this.CCResourceMaps[resource] = new m.Map(sharedScript, "resource"); } } let cellSize = this.resourceMaps.wood.cellSize; // Look for destroy events and subtract the entities original influence from the resourceMap // TODO: perhaps do something when dropsites appear/disappear. let destEvents = events.Destroy; let createEvents = events.Create; for (let e of destEvents) { if (!e.entityObj) continue; let ent = e.entityObj; if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { let resource = ent.resourceSupplyType().generic; if (!this.resourceMaps[resource]) continue; let x = Math.floor(ent.position()[0] / cellSize); let z = Math.floor(ent.position()[1] / cellSize); let strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); if (resource === "wood") { this.CCResourceMaps[resource].addInfluence(x, z, 60/cellSize, -strength, "constant"); this.resourceMaps[resource].addInfluence(x, z, 36/cellSize, -strength/2, "constant"); this.resourceMaps[resource].addInfluence(x, z, 36/cellSize, -strength/2); } else if (resource === "stone" || resource === "metal") { this.CCResourceMaps[resource].addInfluence(x, z, 120/cellSize, -strength, "constant"); this.resourceMaps[resource].addInfluence(x, z, 48/cellSize, -strength/2, "constant"); this.resourceMaps[resource].addInfluence(x, z, 48/cellSize, -strength/2); } } } for (let e of createEvents) { if (!e.entity || !sharedScript._entities.has(e.entity)) continue; let ent = sharedScript._entities.get(e.entity); if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { let resource = ent.resourceSupplyType().generic; if (!this.resourceMaps[resource]) continue; let x = Math.floor(ent.position()[0] / cellSize); let z = Math.floor(ent.position()[1] / cellSize); let strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); if (resource === "wood") { this.CCResourceMaps[resource].addInfluence(x, z, 60/cellSize, strength, "constant"); this.resourceMaps[resource].addInfluence(x, z, 36/cellSize, strength/2, "constant"); this.resourceMaps[resource].addInfluence(x, z, 36/cellSize, strength/2); } else if (resource === "stone" || resource === "metal") { this.CCResourceMaps[resource].addInfluence(x, z, 120/cellSize, strength, "constant"); this.resourceMaps[resource].addInfluence(x, z, 48/cellSize, strength/2, "constant"); this.resourceMaps[resource].addInfluence(x, z, 48/cellSize, strength/2); } } } }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/utils.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/utils.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/utils.js (revision 18429) @@ -1,117 +1,117 @@ var API3 = function(m) { m.warn = function(output) { if (typeof output === "string") warn("PlayerID " + PlayerID + " | " + output); else warn("PlayerID " + PlayerID + " | " + uneval(output)); }; m.VectorDistance = function(a, b) { let dx = a[0] - b[0]; let dz = a[1] - b[1]; return Math.sqrt(dx*dx + dz*dz); }; m.SquareVectorDistance = function(a, b) { let dx = a[0] - b[0]; let dz = a[1] - b[1]; return dx*dx + dz*dz; }; /** * A is the reference, B must be in "range" of A * this supposes the range is already squared */ m.inRange = function(a, b, range)// checks for X distance { // will avoid unnecessary checking for position in some rare cases... I'm lazy if (a === undefined || b === undefined || range === undefined) return undefined; let dx = a[0] - b[0]; let dz = a[1] - b[1]; - return dx*dx + dz*dz < range; + return dx*dx + dz*dz < range; }; /** Slower than SquareVectorDistance, faster than VectorDistance but not exactly accurate. */ m.ManhattanDistance = function(a, b) { let dx = a[0] - b[0]; let dz = a[1] - b[1]; return Math.abs(dx) + Math.abs(dz); }; m.AssocArraytoArray = function(assocArray) { let endArray = []; for (let i in assocArray) endArray.push(assocArray[i]); return endArray; }; /** Picks a random element from an array */ m.PickRandom = function(list) { return list.length ? list[Math.floor(Math.random()*list.length)] : undefined; }; /** Utility functions for conversions of maps of different sizes */ /** * Returns the index of map2 with max content from indices contained inside the cell i of map1 * map1.cellSize must be a multiple of map2.cellSize */ m.getMaxMapIndex = function(i, map1, map2) { let ratio = map1.cellSize / map2.cellSize; let ix = (i % map1.width) * ratio; let iy = Math.floor(i / map1.width) * ratio; let index; for (let kx = 0; kx < ratio; ++kx) for (let ky = 0; ky < ratio; ++ky) if (!index || map2.map[ix+kx+(iy+ky)*map2.width] > map2.map[index]) index = ix+kx+(iy+ky)*map2.width; return index; }; /** * Returns the list of indices of map2 contained inside the cell i of map1 * map1.cellSize must be a multiple of map2.cellSize */ m.getMapIndices = function(i, map1, map2) { let ratio = map1.cellSize / map2.cellSize; // TODO check that this is integer >= 1 ? let ix = (i % map1.width) * ratio; let iy = Math.floor(i / map1.width) * ratio; let ret = []; for (let kx = 0; kx < ratio; ++kx) for (let ky = 0; ky < ratio; ++ky) ret.push(ix+kx+(iy+ky)*map2.width); return ret; }; /** * Returns the list of points of map2 contained inside the cell i of map1 * map1.cellSize must be a multiple of map2.cellSize */ m.getMapPoints = function(i, map1, map2) { let ratio = map1.cellSize / map2.cellSize; // TODO check that this is integer >= 1 ? let ix = (i % map1.width) * ratio; let iy = Math.floor(i / map1.width) * ratio; let ret = []; for (let kx = 0; kx < ratio; ++kx) for (let ky = 0; ky < ratio; ++ky) ret.push([ix+kx, iy+ky]); return ret; }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/army.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/army.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/army.js (revision 18429) @@ -1,434 +1,432 @@ var PETRA = function(m) { /** * Defines an army * An army is a collection of own entities and enemy entities. * This doesn't use entity collections are they aren't really useful * and it would probably slow the rest of the system down too much. * All entities are therefore lists of ID * Inherited by the defense manager and several of the attack manager's attack plan. */ m.Army = function(gameState, ownEntities, foeEntities) { this.ID = gameState.ai.uniqueIDs.armies++; this.Config = gameState.ai.Config; this.defenseRatio = this.Config.Defense.defenseRatio; this.compactSize = this.Config.Defense.armyCompactSize; this.breakawaySize = this.Config.Defense.armyBreakawaySize; // average this.foePosition = [0,0]; this.positionLastUpdate = gameState.ai.elapsedTime; // Some caching // A list of our defenders that were tasked with attacking a particular unit // This doesn't mean that they actually are since they could move on to something else on their own. this.assignedAgainst = {}; // who we assigned against, for quick removal. this.assignedTo = {}; this.foeEntities = []; this.foeStrength = 0; this.ownEntities = []; this.ownStrength = 0; // actually add units for (let id of foeEntities) this.addFoe(gameState, id, true); for (let id of ownEntities) this.addOwn(gameState, id); this.recalculatePosition(gameState, true); return true; }; /** if not forced, will only recalculate if on a different turn. */ m.Army.prototype.recalculatePosition = function(gameState, force) { if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) return; let npos = 0; let pos = [0, 0]; for (let id of this.foeEntities) { let ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; npos++; let epos = ent.position(); pos[0] += epos[0]; pos[1] += epos[1]; } // if npos = 0, the army must have been destroyed and will be removed next turn. keep previous position if (npos > 0) { this.foePosition[0] = pos[0]/npos; this.foePosition[1] = pos[1]/npos; } this.positionLastUpdate = gameState.ai.elapsedTime; }; m.Army.prototype.recalculateStrengths = function (gameState) { - this.ownStrength = 0; - this.foeStrength = 0; - - // todo: deal with specifics. + this.ownStrength = 0; + this.foeStrength = 0; for (let id of this.foeEntities) this.evaluateStrength(gameState.getEntityById(id)); for (let id of this.ownEntities) this.evaluateStrength(gameState.getEntityById(id), true); }; /** adds or remove the strength of the entity either to the enemy or to our units. */ m.Army.prototype.evaluateStrength = function (ent, isOwn, remove) { let entStrength; if (ent.hasClass("Structure")) { if (ent.owner() !== PlayerID) entStrength = ent.getDefaultArrow() ? 6*ent.getDefaultArrow() : 4; else // small strength used only when we try to recover capture points entStrength = 2; } else entStrength = m.getMaxStrength(ent); // TODO adapt the getMaxStrength function for animals. // For the time being, just increase it for elephants as the returned value is too small. if (ent.hasClass("Animal") && ent.hasClass("Elephant")) entStrength *= 3; if (remove) entStrength *= -1; if (isOwn) this.ownStrength += entStrength; else this.foeStrength += entStrength; }; /** * add an entity to the enemy army * Will return true if the entity was added and false otherwise. * won't recalculate our position but will dirty it. * force is true at army creation or when merging armies, so in this case we should add it even if far */ m.Army.prototype.addFoe = function (gameState, enemyId, force) { if (this.foeEntities.indexOf(enemyId) !== -1) return false; let ent = gameState.getEntityById(enemyId); if (!ent || !ent.position()) return false; // check distance if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) return false; this.foeEntities.push(enemyId); this.assignedAgainst[enemyId] = []; this.positionLastUpdate = 0; this.evaluateStrength(ent); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); return true; }; /** * returns true if the entity was removed and false otherwise. * TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. */ m.Army.prototype.removeFoe = function (gameState, enemyId, enemyEntity) { let idx = this.foeEntities.indexOf(enemyId); if (idx === -1) return false; this.foeEntities.splice(idx, 1); this.assignedAgainst[enemyId] = undefined; for (let to in this.assignedTo) if (this.assignedTo[to] == enemyId) this.assignedTo[to] = undefined; let ent = enemyEntity ? enemyEntity : gameState.getEntityById(enemyId); if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) { this.evaluateStrength(ent, false, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); } return true; }; /** * adds a defender but doesn't assign him yet. * force is true when merging armies, so in this case we should add it even if no position as it can be in a ship */ m.Army.prototype.addOwn = function (gameState, id, force) { if (this.ownEntities.indexOf(id) !== -1) return false; let ent = gameState.getEntityById(id); if (!ent || (!ent.position() && !force)) return false; this.ownEntities.push(id); this.evaluateStrength(ent, true); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); this.assignedTo[id] = 0; let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole === undefined || subrole !== "defender") ent.setMetadata(PlayerID, "formerSubrole", subrole); ent.setMetadata(PlayerID, "subrole", "defender"); return true; }; m.Army.prototype.removeOwn = function (gameState, id, Entity) { let idx = this.ownEntities.indexOf(id); if (idx === -1) return false; this.ownEntities.splice(idx, 1); if (this.assignedTo[id] !== 0) { let temp = this.assignedAgainst[this.assignedTo[id]]; if (temp) temp.splice(temp.indexOf(id), 1); } this.assignedTo[id] = undefined; let ent = Entity ? Entity : gameState.getEntityById(id); if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) { this.evaluateStrength(ent, true, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); let formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); if (formerSubrole !== undefined) ent.setMetadata(PlayerID, "subrole", formerSubrole); else ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "formerSubrole", undefined); // TODO be sure that all units in the transport need the cancelation /* if (!ent.position()) // this unit must still be in a transport plan ... try to cancel it { let planID = ent.getMetadata(PlayerID, "transport"); // no plans must mean that the unit was in a ship which was destroyed, so do nothing if (planID) { if (gameState.ai.Config.debug > 0) warn("ent from army still in transport plan: plan " + planID + " canceled"); let plan = gameState.ai.HQ.navalManager.getPlan(planID); if (plan && !plan.canceled) plan.cancelTransport(gameState); } } */ } return true; }; /** * Special army set to capture a gaia building. * It must only contain one foe (the building to capture) and never be merged */ m.Army.prototype.isCapturing = function (gameState) { if (this.foeEntities.length != 1) return false; let ent = gameState.getEntityById(this.foeEntities[0]); return ent && ent.hasClass("Structure"); }; /** * this one is "undefined entity" proof because it's called at odd times. * Orders a unit to attack an enemy. * overridden by specific army classes. */ m.Army.prototype.assignUnit = function (gameState, entID) { }; /** * resets the army properly. * assumes we already cleared dead units. */ m.Army.prototype.clear = function (gameState) { while (this.foeEntities.length > 0) this.removeFoe(gameState,this.foeEntities[0]); while (this.ownEntities.length > 0) this.removeOwn(gameState,this.ownEntities[0]); this.assignedAgainst = {}; this.assignedTo = {}; this.recalculateStrengths(gameState); this.recalculatePosition(gameState); }; /** * merge this army with another properly. * assumes units are in only one army. * also assumes that all have been properly cleaned up (no dead units). */ m.Army.prototype.merge = function (gameState, otherArmy) { // copy over all parameters. for (let i in otherArmy.assignedAgainst) { if (this.assignedAgainst[i] === undefined) this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; else this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); } for (let i in otherArmy.assignedTo) this.assignedTo[i] = otherArmy.assignedTo[i]; for (let id of otherArmy.foeEntities) this.addFoe(gameState, id, true); // TODO: reassign those ? for (let id of otherArmy.ownEntities) this.addOwn(gameState, id, true); this.recalculatePosition(gameState, true); this.recalculateStrengths(gameState); return true; }; m.Army.prototype.checkEvents = function (gameState, events) { // Warning the metadata is already cloned in shared.js. Futhermore, changes should be done before destroyEvents // otherwise it would remove the old entity from this army list // TODO we should may-be reevaluate the strength for (let evt of events.EntityRenamed) // take care of promoted and packed units { if (this.foeEntities.indexOf(evt.entity) !== -1) { let ent = gameState.getEntityById(evt.newentity); if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed continue; let idx = this.foeEntities.indexOf(evt.entity); this.foeEntities[idx] = evt.newentity; this.assignedAgainst[evt.newentity] = this.assignedAgainst[evt.entity]; this.assignedAgainst[evt.entity] = undefined; for (let to in this.assignedTo) if (this.assignedTo[to] === evt.entity) this.assignedTo[to] = evt.newentity; } else if (this.ownEntities.indexOf(evt.entity) !== -1) { let idx = this.ownEntities.indexOf(evt.entity); this.ownEntities[idx] = evt.newentity; this.assignedTo[evt.newentity] = this.assignedTo[evt.entity]; this.assignedTo[evt.entity] = undefined; for (let against in this.assignedAgainst) { if (!this.assignedAgainst[against]) continue; if (this.assignedAgainst[against].indexOf(evt.entity) !== -1) this.assignedAgainst[against][this.assignedAgainst[against].indexOf(evt.entity)] = evt.newentity; } } } for (let evt of events.Garrison) this.removeFoe(gameState, evt.entity); for (let evt of events.OwnershipChanged) // captured { if (!gameState.isPlayerEnemy(evt.to)) this.removeFoe(gameState, evt.entity); else if (evt.from === PlayerID) this.removeOwn(gameState, evt.entity); } for (let evt of events.Destroy) { let entityObj = evt.entityObj || undefined; // we may have capture+destroy, so do not trust owner and check all possibilities this.removeOwn(gameState, evt.entity, entityObj); this.removeFoe(gameState, evt.entity, entityObj); } }; /** assumes cleaned army. */ m.Army.prototype.onUpdate = function (gameState) { if (this.isCapturing(gameState)) { let done = true; let capture = gameState.getEntityById(this.foeEntities[0]).capturePoints(); if (capture !== undefined) { for (let j = 0; j < capture.length; ++j) { if (gameState.isPlayerEnemy(j) && capture[j] > 0) { done = false; break; } } } if (done) this.removeFoe(gameState, this.foeEntities[0]); return []; } let breakaways = []; // TODO: assign unassigned defenders, cleanup of a few things. // perhaps occasional strength recomputation // occasional update or breakaways, positions… if (gameState.ai.elapsedTime - this.positionLastUpdate > 5) { this.recalculatePosition(gameState); this.positionLastUpdate = gameState.ai.elapsedTime; // Check for breakaways. for (let i = 0; i < this.foeEntities.length; ++i) { let id = this.foeEntities[i]; let ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) { breakaways.push(id); if (this.removeFoe(gameState, id)) i--; } } this.recalculatePosition(gameState); } return breakaways; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 18429) @@ -1,518 +1,518 @@ var PETRA = function(m) { /** Attack Manager */ m.AttackManager = function(Config) { this.Config = Config; this.totalNumber = 0; this.attackNumber = 0; this.rushNumber = 0; this.raidNumber = 0; this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.debugTime = 0; this.maxRushes = 0; this.rushSize = []; this.currentEnemyPlayer = undefined; // enemy player we are currently targeting this.defeated = {}; }; /** More initialisation for stuff that needs the gameState */ m.AttackManager.prototype.init = function(gameState) { this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); this.outOfPlan.registerUpdates(); }; m.AttackManager.prototype.setRushes = function(allowed) { if (this.Config.personality.aggressive > 0.8 && allowed > 2) { this.maxRushes = 3; this.rushSize = [ 16, 20, 24 ]; } else if (this.Config.personality.aggressive > 0.6 && allowed > 1) { this.maxRushes = 2; this.rushSize = [ 18, 22 ]; } else if (this.Config.personality.aggressive > 0.3 && allowed > 0) { this.maxRushes = 1; this.rushSize = [ 20 ]; } }; m.AttackManager.prototype.checkEvents = function(gameState, events) { for (let evt of events.PlayerDefeated) this.defeated[evt.playerId] = true; let answer = false; let other; let targetPlayer; for (let evt of events.AttackRequest) { if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.target)) continue; targetPlayer = evt.target; let available = 0; for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing") { if (attack.targetPlayer === targetPlayer) available += attack.unitCollection.length; else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) other = attack.targetPlayer; continue; } attack.targetPlayer = targetPlayer; if (attack.unitCollection.length > 2) available += attack.unitCollection.length; } } if (available > 12) // launch the attack immediately { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing" || attack.targetPlayer !== targetPlayer || attack.unitCollection.length < 3) continue; attack.forceStart(); attack.requested = true; } } answer = true; } break; // take only the first attack request into account } if (targetPlayer !== undefined) m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); }; /** * Some functions are run every turn * Others once in a while */ m.AttackManager.prototype.update = function(gameState, queues, events) { if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) { this.debugTime = gameState.ai.elapsedTime; API3.warn(" upcoming attacks ================="); for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) - API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); + API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" started attacks =================="); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" =================================="); } this.checkEvents(gameState, events); let unexecutedAttacks = {"Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0}; for (let attackType in this.upcomingAttacks) { for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) { let attack = this.upcomingAttacks[attackType][i]; attack.checkEvents(gameState, events); if (attack.isStarted()) API3.warn("Petra problem in attackManager: attack in preparation has already started ???"); let updateStep = attack.updatePreparation(gameState); // now we're gonna check if the preparation time is over if (updateStep === 1 || attack.isPaused() ) { // just chillin' if (attack.state === "unexecuted") ++unexecutedAttacks[attackType]; } else if (updateStep === 0) { if (this.Config.debug > 1) API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--,1); } else if (updateStep === 2) { if (attack.StartAttack(gameState)) { if (this.Config.debug > 1) API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); if (this.Config.chat) m.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType()); this.startedAttacks[attackType].push(attack); } else attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--,1); } } } for (let attackType in this.startedAttacks) { for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { let attack = this.startedAttacks[attackType][i]; attack.checkEvents(gameState, events); // okay so then we'll update the attack. if (attack.isPaused()) continue; let remaining = attack.update(gameState, events); if (!remaining) { if (this.Config.debug > 1) API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--,1); } } } // creating plans after updating because an aborted plan might be reused in that case. let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length; if (this.rushNumber < this.maxRushes && barracksNb >= 1) { if (unexecutedAttacks.Rush === 0) { // we have a barracks and we want to rush, rush. let data = { "targetSize": this.rushSize[this.rushNumber] }; let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Headquarters: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Rush.push(attackPlan); } this.rushNumber++; } } else if (unexecutedAttacks.Attack === 0 && unexecutedAttacks.HugeAttack === 0 && this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100))) { if ((barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) || !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack { let type = (this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0) ? "Attack" : "HugeAttack"; let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, type); if (attackPlan.failed) this.attackPlansEncounteredWater = true; // hack else { if (this.Config.debug > 1) API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks[type].push(attackPlan); } this.attackNumber++; } } if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length) { let target; for (let targetId of gameState.ai.HQ.defenseManager.targetList) { target = gameState.getEntityById(targetId); if (target) break; } if (target) { // prepare a raid against this target let data = { "target": target }; let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Headquarters: Raiding plan " + this.totalNumber); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Raid.push(attackPlan); } this.raidNumber++; } } }; m.AttackManager.prototype.getPlan = function(planName) { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) if (attack.getName() == planName) return attack; } for (let attackType in this.startedAttacks) { for (let attack of this.startedAttacks[attackType]) if (attack.getName() == planName) return attack; } return undefined; }; m.AttackManager.prototype.pausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(true); }; m.AttackManager.prototype.unpausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(false); }; m.AttackManager.prototype.pauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(true); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(true); }; m.AttackManager.prototype.unpauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(false); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(false); }; m.AttackManager.prototype.getAttackInPreparation = function(type) { if (!this.upcomingAttacks[type].length) return undefined; return this.upcomingAttacks[type][0]; }; /** * determine which player should be attacked: when called when starting the attack, * attack.targetPlayer is undefined and in that case, we keep track of the chosen target * for future attacks. */ m.AttackManager.prototype.getEnemyPlayer = function(gameState, attack) { let enemyPlayer; // first check if there is a preferred enemy based on our victory conditions if (gameState.getGameType() === "wonder") { let moreAdvanced; let enemyWonder; let wonders = gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")); for (let wonder of wonders.values()) { if (wonder.owner() === 0) continue; let progress = wonder.foundationProgress(); if (progress === undefined) { enemyWonder = wonder; break; } if (enemyWonder && moreAdvanced > progress) continue; enemyWonder = wonder; moreAdvanced = progress; } if (enemyWonder) { enemyPlayer = enemyWonder.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; } } let veto = {}; for (let i in this.defeated) veto[i] = true; // No rush if enemy too well defended (i.e. iberians) if (attack.type === "Rush") { for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || veto[i]) continue; if (this.defeated[i]) continue; let enemyDefense = 0; for (let ent of gameState.getEnemyStructures(i).values()) if (ent.hasClass("Tower") || ent.hasClass("Fortress")) enemyDefense++; if (enemyDefense > 6) veto[i] = true; } } // then if not a huge attack, continue attacking our previous target as long as it has some entities, // otherwise target the most accessible one if (attack.type !== "HugeAttack") { if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && !this.defeated[this.currentEnemyPlayer] && gameState.getEnemyEntities(this.currentEnemyPlayer).hasEntities()) return this.currentEnemyPlayer; let distmin; let ccmin; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let ourcc of ccEnts.values()) { if (ourcc.owner() !== PlayerID) continue; let ourPos = ourcc.position(); let access = gameState.ai.accessibility.getAccessValue(ourPos); for (let enemycc of ccEnts.values()) { if (veto[enemycc.owner()]) continue; if (!gameState.isPlayerEnemy(enemycc.owner())) continue; let enemyPos = enemycc.position(); if (access !== gameState.ai.accessibility.getAccessValue(enemyPos)) continue; let dist = API3.SquareVectorDistance(ourPos, enemyPos); if (distmin && dist > distmin) continue; ccmin = enemycc; distmin = dist; } } if (ccmin) { enemyPlayer = ccmin.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; } } // then let's target our strongest enemy (basically counting enemies units) // with priority to enemies with civ center let max = 0; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (veto[i]) continue; if (!gameState.isPlayerEnemy(i)) continue; let enemyCount = 0; let enemyCivCentre = false; for (let ent of gameState.getEnemyEntities(i).values()) { enemyCount++; if (ent.hasClass("CivCentre")) enemyCivCentre = true; } if (enemyCivCentre) enemyCount += 500; if (enemyCount < max) continue; max = enemyCount; enemyPlayer = i; } if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; }; m.AttackManager.prototype.Serialize = function() { let properties = { "totalNumber": this.totalNumber, "attackNumber": this.attackNumber, "rushNumber": this.rushNumber, "raidNumber": this.raidNumber, "debugTime": this.debugTime, "maxRushes": this.maxRushes, "rushSize": this.rushSize, "currentEnemyPlayer": this.currentEnemyPlayer, "defeated": this.defeated }; let upcomingAttacks = {}; for (let key in this.upcomingAttacks) { upcomingAttacks[key] = []; for (let attack of this.upcomingAttacks[key]) upcomingAttacks[key].push(attack.Serialize()); } let startedAttacks = {}; for (let key in this.startedAttacks) { startedAttacks[key] = []; for (let attack of this.startedAttacks[key]) startedAttacks[key].push(attack.Serialize()); } return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; }; m.AttackManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.upcomingAttacks = {}; for (let key in data.upcomingAttacks) { this.upcomingAttacks[key] = []; for (let dataAttack of data.upcomingAttacks[key]) { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); + let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.upcomingAttacks[key].push(attack); } } this.startedAttacks = {}; for (let key in data.startedAttacks) { this.startedAttacks[key] = []; for (let dataAttack of data.startedAttacks[key]) { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); + let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.startedAttacks[key].push(attack); } } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 18429) @@ -1,205 +1,205 @@ var PETRA = function(m) { m.Config = function(difficulty) { // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. this.difficulty = difficulty !== undefined ? difficulty : 3; // debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug this.debug = 0; this.chat = true; // false to prevent AI's chats this.popScaling = 1; // scale factor depending on the max population this.Military = { "towerLapseTime" : 90, // Time to wait between building 2 towers "fortressLapseTime" : 390, // Time to wait between building 2 fortresses "popForBarracks1" : 25, "popForBarracks2" : 95, "popForBlacksmith" : 65, "numWoodenTowers" : 1 }; this.Economy = { "popForTown" : 40, // How many units we want before aging to town. "workForCity" : 80, // How many workers we want before aging to city. "cityPhase" : 840, // time to start trying to reach city phase "popForMarket" : 50, "popForDock" : 25, "targetNumWorkers" : 40, // dummy, will be changed later "targetNumTraders" : 5, // Target number of traders "targetNumFishers" : 1, // Target number of fishers per sea "supportRatio" : 0.3, // fraction of support workers among the workforce "provisionFields" : 2 }; // Note: attack settings are set directly in attack_plan.js // defense this.Defense = { "defenseRatio" : 2, // ratio of defenders/attackers. "armyCompactSize" : 2000, // squared. Half-diameter of an army. "armyBreakawaySize" : 3500, // squared. "armyMergeSize" : 1400 // squared. }; this.buildings = { "base": { "default": [ "structures/{civ}_civil_centre" ], "ptol": [ "structures/{civ}_military_colony" ], "sele": [ "structures/{civ}_military_colony" ] }, "advanced": { "default": [], "athen": [ "structures/{civ}_gymnasion", "structures/{civ}_prytaneion", "structures/{civ}_theatron" ], "brit": [ "structures/{civ}_rotarymill" ], "cart": [ "structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ], "gaul": [ "structures/{civ}_rotarymill", "structures/{civ}_tavern" ], "iber": [ "structures/{civ}_monument" ], "mace": [ "structures/{civ}_siege_workshop", "structures/{civ}_library", "structures/{civ}_theatron" ], "maur": [ "structures/{civ}_elephant_stables", "structures/{civ}_pillar_ashoka" ], "pers": [ "structures/{civ}_stables", "structures/{civ}_apadana", "structures/{civ}_hall"], "ptol": [ "structures/{civ}_library" ], "rome": [ "structures/{civ}_army_camp" ], "sele": [ "structures/{civ}_library" ], "spart": [ "structures/{civ}_syssiton", "structures/{civ}_theatron" ] }, "naval": { "default": [], // "brit": [ "structures/{civ}_crannog" ], "cart": [ "structures/{civ}_super_dock" ] } }; this.priorities = { "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food "citizenSoldier": 60, "trader": 50, "ships": 70, "house": 350, "dropsites": 200, "field": 400, "dock": 90, "corral": 60, "economicBuilding": 90, "militaryBuilding": 130, "defenseBuilding": 70, "civilCentre": 950, "majorTech": 700, "minorTech": 40, "emergency": 1000 // used only in emergency situations, should be the highest one }; this.personality = { "aggressive": 0.5, "cooperative": 0.5, "defensive": 0.5 }; this.resources = ["food", "wood", "stone", "metal"]; }; m.Config.prototype.setConfig = function(gameState) { // initialize personality traits if (this.difficulty > 1) { this.personality.aggressive = Math.random(); this.personality.cooperative = Math.random(); this.personality.defensive = Math.random(); } else { this.personality.aggressive = 0.1; this.personality.cooperative = 0.9; } // changing settings based on difficulty or personality if (this.difficulty < 2) { this.Economy.cityPhase = 240000; this.Economy.supportRatio = 0.5; this.Economy.provisionFields = 1; this.Military.numWoodenTowers = this.personality.defensive > 0.66 ? 1 : 0; } else if (this.difficulty < 3) { this.Economy.cityPhase = 1800; this.Economy.supportRatio = 0.4; this.Economy.provisionFields = 1; this.Military.numWoodenTowers = this.personality.defensive > 0.66 ? 1 : 0; } else { this.Military.towerLapseTime += Math.round(20*(this.personality.defensive - 0.5)); this.Military.fortressLapseTime += Math.round(60*(this.personality.defensive - 0.5)); if (this.difficulty == 3) this.Military.numWoodenTowers = 1; else this.Military.numWoodenTowers = 2; if (this.personality.defensive > 0.66) ++this.Military.numWoodenTowers; else if (this.personality.defensive < 0.33) --this.Military.numWoodenTowers; if (this.personality.aggressive > 0.7) { this.Military.popForBarracks1 = 12; this.Economy.popForTown = 55; this.Economy.popForMarket = 60; this.priorities.defenseBuilding = 60; } } let maxPop = gameState.getPopulationMax(); if (this.difficulty < 2) this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); else if (this.difficulty < 3) this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2))); else this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3))); this.Economy.targetNumTraders = 2 + this.difficulty; if (maxPop < 300) { this.popScaling = Math.sqrt(maxPop / 300); - this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5)); - this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3)); - this.Military.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * this.popScaling), 30), Math.floor(maxPop/2)); - this.Economy.popForTown = Math.min(Math.max(Math.floor(this.Economy.popForTown * this.popScaling), 25), Math.floor(maxPop/2)); - this.Economy.workForCity = Math.min(Math.max(Math.floor(this.Economy.workForCity * this.popScaling), 50), Math.floor(maxPop*2/3)); - this.Economy.popForMarket = Math.min(Math.max(Math.floor(this.Economy.popForMarket * this.popScaling), 25), Math.floor(maxPop/2)); + this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5)); + this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3)); + this.Military.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * this.popScaling), 30), Math.floor(maxPop/2)); + this.Economy.popForTown = Math.min(Math.max(Math.floor(this.Economy.popForTown * this.popScaling), 25), Math.floor(maxPop/2)); + this.Economy.workForCity = Math.min(Math.max(Math.floor(this.Economy.workForCity * this.popScaling), 50), Math.floor(maxPop*2/3)); + this.Economy.popForMarket = Math.min(Math.max(Math.floor(this.Economy.popForMarket * this.popScaling), 25), Math.floor(maxPop/2)); this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling); } this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popForTown); if (this.debug < 2) return; API3.warn(" >>> Petra bot: personality = " + uneval(this.personality)); }; m.Config.prototype.Serialize = function() { var data = {}; for (let key in this) if (this.hasOwnProperty(key) && key != "debug") data[key] = this[key]; return data; }; m.Config.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 18429) @@ -1,264 +1,264 @@ var PETRA = function(m) { /** * Manage the garrisonHolders * When a unit is ordered to garrison, it must be done through this.garrison() function so that * an object in this.holders is created. This object contains an array with the entities * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) */ m.GarrisonManager = function() { this.holders = new Map(); this.decayingStructures = new Map(); }; m.GarrisonManager.prototype.update = function(gameState, events) { for (let [id, list] of this.holders.entries()) { let holder = gameState.getEntityById(id); if (!holder || !gameState.isPlayerAlly(holder.owner())) { // this holder was certainly destroyed or captured. Let's remove it for (let entId of list) { let ent = gameState.getEntityById(entId); if (ent && ent.getMetadata(PlayerID, "garrisonHolder") == id) { this.leaveGarrison(ent); ent.stopMoving(); } } this.holders.delete(id); continue; } // Update the list of garrisoned units for (let j = 0; j < list.length; ++j) { for (let evt of events.EntityRenamed) if (evt.entity === list[j]) list[j] = evt.newentity; let ent = gameState.getEntityById(list[j]); if (!ent) // unit must have been killed while garrisoning list.splice(j--, 1); else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned { this.leaveGarrison(ent); list.splice(j--, 1); } else { let ok = false; for (let order of ent.unitAIOrderData()) { if (!order.target || order.target != id) continue; ok = true; break; } if (ok) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { // The garrison order must have failed this.leaveGarrison(ent); list.splice(j--, 1); } else { if (gameState.ai.Config.debug > 0) { API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() + ") is expected to garrison in " + id + " (" + holder.genericName() + "), but has no such garrison order " + uneval(ent.unitAIOrderData())); m.dumpEntity(ent); } list.splice(j--, 1); } } } if (!holder.position()) // could happen with siege unit inside a ship continue; if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) { let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; let enemiesAround = false; for (let ent of gameState.getEnemyEntities().values()) { if (!ent.position()) continue; if (ent.owner() === 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] !== "COMBAT")) continue; let dist = API3.SquareVectorDistance(ent.position(), holder.position()); if (dist > range*range) continue; enemiesAround = true; break; } for (let entId of holder.garrisoned()) { let ent = gameState.getEntityById(entId); if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, enemiesAround)) holder.unload(entId); } for (let j = 0; j < list.length; ++j) { let ent = gameState.getEntityById(list[j]); if (this.keepGarrisoned(ent, holder, enemiesAround)) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { this.leaveGarrison(ent); ent.stopMoving(); } list.splice(j--, 1); } if (this.numberOfGarrisonedUnits(holder) === 0) this.holders.delete(id); else holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); } } // Warning new garrison orders (as in the following lines) should be done after having updated the holders // (or TODO we should add a test that the garrison order is from a previous turn when updating) for (let [id, gmin] of this.decayingStructures.entries()) { let ent = gameState.getEntityById(id); if (!ent || ent.owner() !== PlayerID) this.decayingStructures.delete(id); else if (this.numberOfGarrisonedUnits(ent) < gmin) gameState.ai.HQ.defenseManager.garrisonRangedUnitsInside(gameState, ent, {"min": gmin, "type": "decay"}); } }; // TODO should add the units garrisoned inside garrisoned units m.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) { if (!this.holders.has(holder.id())) return holder.garrisoned().length; return holder.garrisoned().length + this.holders.get(holder.id()).length; }; // This is just a pre-garrison state, while the entity walk to the garrison holder m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) { if (this.numberOfGarrisonedUnits(holder) >= holder.garrisonMax()) return; this.registerHolder(gameState, holder); this.holders.get(holder.id()).push(ent.id()); if (gameState.ai.Config.debug > 2) { warn("garrison unit " + ent.genericName() + " in " + holder.genericName() + " with type " + type); warn(" we try to garrison a unit with plan " + ent.getMetadata(PlayerID, "plan") + " and role " + ent.getMetadata(PlayerID, "role") + - " and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport")); + " and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport")); } if (ent.getMetadata(PlayerID, "plan") !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); ent.setMetadata(PlayerID, "subrole", "garrisoning"); ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); ent.setMetadata(PlayerID, "garrisonType", type); ent.garrison(holder); }; // This is the end of the pre-garrison state, either because the entity is really garrisoned // or because it has changed its order (i.e. because the garrisonHolder was destroyed). m.GarrisonManager.prototype.leaveGarrison = function(ent) { ent.setMetadata(PlayerID, "subrole", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "garrisonHolder", undefined); }; m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, enemiesAround) { switch (ent.getMetadata(PlayerID, "garrisonType")) { case 'force': // force the ungarrisoning return false; case 'trade': // trader garrisoned in ship return true; case 'protection': // hurt unit for healing or infantry for defense if (ent.needsHeal() && holder.buffHeal()) return true; if (enemiesAround && (ent.hasClass("Support") || MatchesClassList(holder.getGarrisonArrowClasses(), ent.classes()))) return true; return false; case 'decay': return this.decayingStructures.has(holder.id()); default: if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager return true; API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + " for " + ent.id() + " inside " + holder.id()); ent.setMetadata(PlayerID, "garrisonType", "protection"); return true; } }; /** Add this holder in the list managed by the garrisonManager */ m.GarrisonManager.prototype.registerHolder = function(gameState, holder) { if (this.holders.has(holder.id())) // already registered return; this.holders.set(holder.id(), []); holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); }; /** * Garrison units in decaying structures to stop their decay * do it only for structures useful for defense, except if we are expanding (justCaptured=true) * in which case we also do it for structures useful for unit trainings (TODO only Barracks are done) */ m.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) { if (this.decayingStructures.has(entId)) return true; let ent = gameState.getEntityById(entId); if (!ent || (!(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire())) return false; if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) return false; let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); this.decayingStructures.set(entId, gmin); return true; }; m.GarrisonManager.prototype.removeDecayingStructure = function(entId) { if (!this.decayingStructures.has(entId)) return; this.decayingStructures.delete(entId); }; m.GarrisonManager.prototype.Serialize = function() { return { "holders": this.holders, "decayingStructures": this.decayingStructures }; }; m.GarrisonManager.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; 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 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 18429) @@ -1,2283 +1,2283 @@ 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.econState = "growth"; // existing values: growth, townPhasing. this.currentPhase = undefined; // cache the rates. this.turnCache = {}; this.wantedRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.currentRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.lastFailedGather = { "wood": undefined, "stone": undefined, "metal": undefined }; // workers configuration this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; this.stopBuilding = new Map(); // list of buildings to stop (temporarily) production because no room this.fortStartTime = 180; // wooden 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.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(); }; /** 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"); // area of n cells on the border of the map : 0=inside map, 1=border map, 2=border+inaccessible this.borderMap = m.createBorderMap(gameState); // initialize frontier map. Each cell is 2 if on the near frontier, 1 on the frontier and 0 otherwise this.frontierMap = m.createFrontierMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; 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; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); 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.getSeaIndex = 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, queues) { if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); if (events.DiplomacyChanged.length) { gameState.resetAllyStructures(); gameState.resetEnemyStructures(); } for (let evt of events.Create) { // Let's check if we have a building set to create a new base. let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) 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()); }); } } else if (ent.hasClass("Wonder") && gameState.getGameType() === "wonder") { // Let's get a few units from other bases there to build this. let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); let builders = this.bulkPickWorkers(gameState, base, 10); if (builders !== false) { builders.forEach(function (worker) { worker.setMetadata(PlayerID, "base", base.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } for (let evt of events.ConstructionFinished) { // Let's check if we have a building set to create a new base. // TODO: move to the base manager. if (evt.newentity) { if (evt.newentity === evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.isOwn(PlayerID)) continue; if (ent.getMetadata(PlayerID, "baseAnchor") === true) { let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (base.constructing) base.constructing = false; base.anchor = ent; base.anchorId = evt.newentity; base.buildings.updateEnt(ent); if (base.ID === this.baseManagers[1].ID) { // 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; } } } } for (let evt of events.OwnershipChanged) // capture events { 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) + 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"); 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); } // then deals with decaying structures 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; for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.90; break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; /** Called by the "town phase" research plan once it's started */ m.HQ.prototype.OnTownPhase = function(gameState) { let phaseName = gameState.getTemplate(gameState.townPhase()).name(); m.chatNewPhase(gameState, phaseName, true); }; /** Called by the "city phase" research plan once it's started */ m.HQ.prototype.OnCityPhase = function(gameState) { // increase the priority of defense buildings to free this queue for our first fortress gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); let phaseName = gameState.getTemplate(gameState.cityPhase()).name(); m.chatNewPhase(gameState, phaseName, true); }; /** 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 = [["cost", 1], ["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.popForTown + 10) return; if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popForTown && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase()))) return; if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) return; // Choose whether we want soldiers or support units. let supportRatio = gameState.isTemplateDisabled(gameState.applyCiv("structures/{civ}_field")) ? Math.min(this.supportRatio, 0.1) : this.supportRatio; let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * Math.atan(numberTotal/supportMax) / 1.570796; let template; if (numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["cost", 1], ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"]]; else requirements = [ ["strength", 1] ]; let classes = ["CitizenSoldier", "Infantry"]; let proba = Math.random(); // we require at least 30% ranged and 30% melee if ( proba < 0.3 ) classes.push("Ranged"); else if ( proba < 0.6 ) classes.push("Melee"); 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 aDivParam = 0; let bDivParam = 0; let aTopParam = 0; let bTopParam = 0; for (let param of parameters) { if (param[0] == "base") { aTopParam = param[1]; bTopParam = param[1]; } if (param[0] == "strength") { aTopParam += m.getMaxStrength(a[1]) * param[1]; bTopParam += m.getMaxStrength(b[1]) * param[1]; } if (param[0] == "siegeStrength") { aTopParam += m.getMaxStrength(a[1], "Structure") * param[1]; bTopParam += m.getMaxStrength(b[1], "Structure") * param[1]; } if (param[0] == "speed") { aTopParam += a[1].walkSpeed() * param[1]; bTopParam += b[1].walkSpeed() * param[1]; } if (param[0] == "cost") { aDivParam += a[1].costSum() * param[1]; bDivParam += b[1].costSum() * param[1]; } // requires a third parameter which is the resource if (param[0] == "costsResource") { if (a[1].cost()[param[2]]) aTopParam *= param[1]; if (b[1].cost()[param[2]]) bTopParam *= param[1]; } if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aTopParam *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bTopParam *= param[1]; } } return -aTopParam/(aDivParam+1) + bTopParam/(bDivParam+1); }); 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 = { "food": 0, "wood": 0, "stone": 0, "metal": 0 }; for (let base of this.baseManagers) for (let type in total) total[type] += base.getResourceLevel(gameState, type); 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 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. 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() / 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() + 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.getMap().cellSize * gameState.getMap().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) { norm = 0; continue; } else 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] > 0) // disfavor the borders of the map norm *= 0.5; let val = 2*gameState.sharedScript.CCResourceMaps[resource].map[j] + gameState.sharedScript.CCResourceMaps.wood.map[j] + gameState.sharedScript.CCResourceMaps.stone.map[j] + gameState.sharedScript.CCResourceMaps.metal.map[j]; val *= norm; if (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); 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.getSeaIndex(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() / obstructions.cellSize); + let radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let currentVal, delta; let distcc0, distcc1, distcc2; 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) - 300; // favor a distance of 300 currentVal = delta*delta; delta = Math.sqrt(distcc1) - 300; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - 300; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] > 0) 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.getSeaIndex(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 * TODO check that it is on same accessIndex */ m.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("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 radius = Math.ceil(template.obstructionRadius() / 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 border of our territory if (this.frontierMap.map[j] === 2) continue; if (this.basesMap.map[j] === 0) // only in our territory continue; // with enough room around to build the cc 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 gainMultiplier; for (let market of markets) { if (isNavalMarket && market.hasClass("NavalMarket")) { if (this.navalManager.getDockIndex(gameState, market, true) !== gameState.ai.accessibility.getAccessValue(pos, true)) continue; gainMultiplier = traderTemplatesGains.navalGainMultiplier; } else if (gameState.ai.accessibility.getAccessValue(market.position()) === index) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let val = API3.SquareVectorDistance(market.position(), pos) * gainMultiplier; if (val > maxVal) maxVal = val; } if (maxVal === 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; 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(bestVal / 10000); 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()) 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() + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); for (let j = 0; j < this.territoryMap.length; ++j) { if (!wonderMode) { // do not try if well inside or outside territory if (this.frontierMap.map[j] === 0) continue; if (this.frontierMap.map[j] === 1 && 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 (gameState.currentPhase() < 3 || queues.economicBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) 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.onStart = function(gameState) { gameState.ai.queueManager.changePriority("economicBuilding", gameState.ai.Config.priorities.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")); }; /** 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 === 0 || (gameState.isTemplateDisabled(gameState.applyCiv("structures/{civ}_field")) && nCorral < gameState.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 === 0) 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.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.isGo = function (gameState) { if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house")) return false; if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return false; let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); for (let ent of gameState.getOwnFoundations().values()) freeSlots += ent.getPopulationBonus(); if (gameState.ai.HQ.saveResources) return freeSlots <= 10; else if (gameState.getPopulation() > 55) return freeSlots <= 21; else if (gameState.getPopulation() > 30) return freeSlots <= 15; return freeSlots <= 10; }; queues.house.addPlan(plan); } if (numPlanned > 0 && this.econState == "townPhasing" && gameState.getPhaseRequirements(2)) { let requirements = gameState.getPhaseRequirements(2); let count = gameState.getOwnStructures().filter(API3.Filters.byClass(requirements["class"])).length; if (requirements && count < requirements.number && this.stopBuilding.has(gameState.applyCiv("structures/{civ}_house"))) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.stopBuilding.delete(gameState.applyCiv("structures/{civ}_house")); this.requireHouses = true; } let houseQueue = queues.house.plans; for (let i = 0; i < numPlanned; ++i) { if (houseQueue[i].isGo(gameState)) ++count; else if (count < requirements.number) { houseQueue[i].isGo = function () { return true; }; ++count; } } } if (this.requireHouses) { let requirements = gameState.getPhaseRequirements(2); if (gameState.getOwnStructures().filter(API3.Filters.byClass(requirements["class"])).length >= requirements.number) 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.stopBuilding.has(house)) { if (this.stopBuilding.get(house) > gameState.ai.elapsedTime) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to improve with technology"); this.researchManager.researchPopulationBonus(gameState, queues); } else { this.stopBuilding.delete(house); priority = 2*this.Config.priorities.house; } } 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 let activeBases = this.numActiveBase(); if (activeBases === 0) { this.buildFirstBase(gameState); return; } // then expand if we have not enough room available for buildings if (this.stopBuilding.size > 1) { if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build " + uneval(this.stopBuilding)); this.buildNewBase(gameState, queues); return; } // then expand if we have lots of units (threshold depending on the aggressivity value) 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.numActiveBase() > 0 && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase())) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) return false; let template = this.numActiveBase() > 0 ? this.bBase[0] : gameState.applyCiv("structures/{civ}_civil_centre"); if (!this.canBuild(gameState, template)) return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base 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 || queues.defenseBuilding.hasQueuedUnits()) return; if (gameState.currentPhase() > 2 || gameState.isResearching(gameState.cityPhase())) { // 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.numActiveBase() + 1 + this.extraFortresses && 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.onStart = function(gameState) { gameState.ai.queueManager.changePriority("defenseBuilding", gameState.ai.Config.priorities.defenseBuilding); }; queues.defenseBuilding.addPlan(plan); return; } } } if (this.Config.Military.numWoodenTowers && gameState.currentPhase() < 2 && this.canBuild(gameState, "structures/{civ}_wooden_tower")) { let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers if (numTowers < this.Config.Military.numWoodenTowers && gameState.ai.elapsedTime > this.towerLapseTime + this.fortStartTime) { this.fortStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wooden_tower")); } return; } if (gameState.currentPhase() < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("DefenseTower", true).filter(API3.Filters.byClass("Town")).length; if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.10*numTowers)*this.towerLapseTime + this.towerStartTime) && numTowers < 2 * this.numActiveBase() + 3 + this.extraTowers && gameState.getOwnFoundationsByClass("DefenseTower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower")); } }; 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")); }; m.HQ.prototype.buildWonder = function(gameState, queues) { if (queues.wonder && queues.wonder.hasQueuedUnits()) return; if (gameState.getOwnEntitiesByClass("Wonder", true).hasEntities()) return; if (!this.canBuild(gameState, "structures/{civ}_wonder")) return; if (!queues.wonder) gameState.ai.queueManager.addQueue("wonder", 1000); queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); }; /** * 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 (!queues.militaryBuilding.hasQueuedUnits() && this.canBuild(gameState, "structures/{civ}_barracks")) { let barrackNb = gameState.getOwnEntitiesByClass("Barracks", true).length; // first barracks. if (!barrackNb && (gameState.getPopulation() > this.Config.Military.popForBarracks1 || (this.econState == "townPhasing" && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5))) { gameState.ai.queueManager.changePriority("militaryBuilding", 2*this.Config.priorities.militaryBuilding); let preferredBase = this.findBestBaseForMilitary(gameState); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase }); plan.onStart = function(gameState) { gameState.ai.queueManager.changePriority("militaryBuilding", gameState.ai.Config.priorities.militaryBuilding); }; queues.militaryBuilding.addPlan(plan); } // second barracks, then 3rd barrack, and optional 4th for some civs as they rely on barracks more. else if (barrackNb == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); } else if (barrackNb == 2 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 20) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); } else if (barrackNb == 3 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 50 && (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber")) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); } } //build advanced military buildings if (gameState.currentPhase() > 2 && gameState.getPopulation() > 80 && !queues.militaryBuilding.hasQueuedUnits() && this.bAdvanced.length !== 0) { 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()) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "preferredBase": preferredBase })); } else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); break; } } } }; /** * Construct military building in bases nearest to the ennemies TODO revisit as the nearest one may not be accessible */ m.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase = 1; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); 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.civ(); // 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 = Math.random() > 0.5 && 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(garrisonArrowClasses, template.classes())) 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) { let type = gameState.applyCiv(structure); // available room to build it if (this.stopBuilding.has(type)) { if (this.stopBuilding.get(type) > gameState.ai.elapsedTime) return false; this.stopBuilding.delete(type); } if (gameState.isTemplateDisabled(type)) { this.stopBuilding.set(type, Infinity); return false; } let template = gameState.getTemplate(type); if (!template) { this.stopBuilding.set(type, Infinity); if (this.Config.debug > 0) API3.warn("Petra error: trying to build " + structure + " for civ " + gameState.civ() + " but no template found."); } if (!template || !template.available(gameState)) return false; if (!gameState.findBuilder(type)) { this.stopBuild(gameState, type, 120); return false; } if (this.numActiveBase() < 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.stopBuilding.set(type, gameState.ai.elapsedTime + 180); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) return false; return true; }; m.HQ.prototype.stopBuild = function(gameState, structure, time=180) { let type = gameState.applyCiv(structure); if (this.stopBuilding.has(type)) this.stopBuilding.set(type, Math.max(this.stopBuilding.get(type), gameState.ai.elapsedTime + time)); else this.stopBuilding.set(type, gameState.ai.elapsedTime + time); }; m.HQ.prototype.restartBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.stopBuilding.has(type)) this.stopBuilding.delete(type); }; m.HQ.prototype.updateTerritories = function(gameState) { let passabilityMap = gameState.getMap(); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let expansion = 0; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.borderMap.map[j] > 1) continue; if (this.territoryMap.getOwnerIndex(j) != PlayerID) { 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 if (this.basesMap.map[j] === 0) { 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++; } } - this.frontierMap = m.createFrontierMap(gameState); + this.frontierMap = m.createFrontierMap(gameState); if (!expansion) return; // We've increased our territory, so we may have some new room to build this.stopBuilding.clear(); // 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 active (i.e. with one cc) bases */ m.HQ.prototype.numActiveBase = function() { if (!this.turnCache.activeBase) { let num = 0; for (let base of this.baseManagers) if (base.anchor) ++num; this.turnCache.activeBase = num; } return this.turnCache.activeBase; }; m.HQ.prototype.resetActiveBase = function() { this.turnCache.activeBase = 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("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; }; /** 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; }; /** * 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); if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; m.dumpEntity(ent); }); } this.checkEvents(gameState, events, queues); this.researchManager.checkPhase(gameState, queues); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) { this.currentPhase = gameState.currentPhase(); let phaseName = "Unknown Phase"; if (this.currentPhase == 2) phaseName = gameState.getTemplate(gameState.townPhase()).name(); else if (this.currentPhase == 3) phaseName = gameState.getTemplate(gameState.cityPhase()).name(); m.chatNewPhase(gameState, phaseName, false); } if (gameState.getGameType() === "wonder") this.buildWonder(gameState, queues); if (this.numActiveBase() > 0) { this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 2 == 1) this.buildMoreHouses(gameState,queues); if (!this.saveResources && 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.numActiveBase() < 1 || (this.Config.difficulty > 0 && gameState.ai.playedTurn % 10 == 7 && gameState.currentPhase() > 1)) this.checkBaseExpansion(gameState, queues); if (gameState.currentPhase() > 1) { if (!this.saveResources) { this.buildMarket(gameState, queues); this.buildBlacksmith(gameState, queues); this.buildTemple(gameState, queues); } if (this.Config.difficulty > 1) this.tradeManager.update(gameState, events, queues); } this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); if (!this.saveResources) this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > 0) this.buildDefenses(gameState, queues); this.assignGatherers(); for (let i = 0; i < this.baseManagers.length; ++i) { this.baseManagers[i].checkEvents(gameState, events, queues); 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.numActiveBase() > 0 || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); Engine.ProfileStop(); }; m.HQ.prototype.Serialize = function() { let properties = { "econState": this.econState, "currentPhase": this.currentPhase, "wantedRates": this.wantedRates, "currentRates": this.currentRates, "lastFailedGather": this.lastFailedGather, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, "stopBuilding": this.stopBuilding, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "fortressStartTime": this.fortressStartTime, "bBase": this.bBase, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "needCorral": this.needCorral, "needFarm": this.needFarm, "needFish": this.needFish, "canBuildUnits": this.canBuildUnits, "navalMap": this.navalMap, "landRegions": this.landRegions, "navalRegions": this.navalRegions, "decayingStructures": this.decayingStructures }; 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(" 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())); } return { "properties": properties, "baseManagers": baseManagers, "attackManager": this.attackManager.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(), }; }; 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.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.garrisonManager.Deserialize(data.garrisonManager); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 18429) @@ -1,768 +1,768 @@ var PETRA = function(m) { /** * Naval Manager * Will deal with anything ships. * -Basically trade over water (with fleets and goals commissioned by the economy manager) * -Defense over water (commissioned by the defense manager) * -Transport of units over water (a few units). * -Scouting, ultimately. * Also deals with handling docks, making sure we have access and stuffs like that. */ m.NavalManager = function(Config) { this.Config = Config; // ship subCollections. Also exist for land zones, idem, not caring. this.seaShips = []; this.seaTransportShips = []; this.seaWarShips = []; this.seaFishShips = []; // wanted NB per zone. this.wantedTransportShips = []; this.wantedWarShips = []; this.wantedFishShips = []; // needed NB per zone. this.neededTransportShips = []; this.neededWarShips = []; this.transportPlans = []; // shore-line regions where we can load and unload units this.landingZones = {}; }; /** More initialisation for stuff that needs the gameState */ m.NavalManager.prototype.init = function(gameState, deserializing) { // finished docks this.docks = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClassesOr(["Dock", "Shipyard"]), API3.Filters.not(API3.Filters.isFoundation()))); this.docks.registerUpdates(); this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader")))); // note: those two can overlap (some transport ships are warships too and vice-versa). this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat")))); this.warShips = this.ships.filter(API3.Filters.byClass("Warship")); this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat")); this.ships.registerUpdates(); this.transportShips.registerUpdates(); this.warShips.registerUpdates(); this.fishShips.registerUpdates(); let fishes = gameState.getFishableSupplies(); let availableFishes = {}; for (let fish of fishes.values()) { let sea = gameState.ai.accessibility.getAccessValue(fish.position(), true); fish.setMetadata(PlayerID, "sea", sea); if (availableFishes[sea]) availableFishes[sea] += fish.resourceSupplyAmount(); else availableFishes[sea] = fish.resourceSupplyAmount(); } for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { if (!gameState.ai.HQ.navalRegions[i]) { // push dummies this.seaShips.push(undefined); this.seaTransportShips.push(undefined); this.seaWarShips.push(undefined); this.seaFishShips.push(undefined); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } else { let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaShips.push(collec); collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaTransportShips.push(collec); collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaWarShips.push(collec); collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaFishShips.push(collec); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); if (availableFishes[i] && availableFishes[i] > 1000) this.wantedFishShips.push(this.Config.Economy.targetNumFishers); else this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } } // load units and buildings from the config files let civ = gameState.civ(); if (civ in this.Config.buildings.naval) this.bNaval = this.Config.buildings.naval[civ]; else this.bNaval = this.Config.buildings.naval['default']; for (let i in this.bNaval) this.bNaval[i] = gameState.applyCiv(this.bNaval[i]); if (deserializing) return; // determination of the possible landing zones let width = gameState.getMap().width; let length = width * gameState.getMap().height; for (let i = 0; i < length; ++i) { let land = gameState.ai.accessibility.landPassMap[i]; if (land < 2) continue; let naval = gameState.ai.accessibility.navalPassMap[i]; if (naval < 2) continue; if (!this.landingZones[land]) this.landingZones[land] = {}; if (!this.landingZones[land][naval]) this.landingZones[land][naval] = new Set(); this.landingZones[land][naval].add(i); } // and keep only thoses with enough room around when possible for (let land in this.landingZones) { for (let sea in this.landingZones[land]) { let landing = this.landingZones[land][sea]; let nbaround = {}; let nbcut = 0; for (let i of landing) { let nb = 0; if (landing.has(i-1)) nb++; if (landing.has(i+1)) nb++; if (landing.has(i+width)) nb++; if (landing.has(i-width)) nb++; nbaround[i] = nb; nbcut = Math.max(nb, nbcut); } nbcut = Math.min(2, nbcut); for (let i of landing) { if (nbaround[i] < nbcut) landing.delete(i); } } } // Assign our initial docks and ships for (let ship of this.ships.values()) this.setShipIndex(gameState, ship); for (let dock of this.docks.values()) this.setDockIndex(gameState, dock); }; m.NavalManager.prototype.updateFishingBoats = function(sea, num) { if (this.wantedFishShips[sea]) this.wantedFishShips[sea] = num; }; m.NavalManager.prototype.resetFishingBoats = function(gameState, sea) { if (sea !== undefined) this.wantedFishShips[sea] = 0; else for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) this.wantedFishShips[i] = 0; }; m.NavalManager.prototype.setShipIndex = function(gameState, ship) { let sea = gameState.ai.accessibility.getAccessValue(ship.position(), true); ship.setMetadata(PlayerID, "sea", sea); }; m.NavalManager.prototype.setDockIndex = function(gameState, dock) { let land = dock.getMetadata(PlayerID, "access"); if (land === undefined) { land = this.getDockIndex(gameState, dock, false); dock.setMetadata(PlayerID, "access", land); } let sea = dock.getMetadata(PlayerID, "sea"); if (sea === undefined) { sea = this.getDockIndex(gameState, dock, true); dock.setMetadata(PlayerID, "sea", sea); } }; /** * get the indices for our starting docks and those of our allies * land index when onWater=false, sea indes when true */ m.NavalManager.prototype.getDockIndex = function(gameState, dock, onWater) { let index = gameState.ai.accessibility.getAccessValue(dock.position(), onWater); if (index < 2) { // pre-positioned docks are sometimes not well positionned let dockPos = dock.position(); let radius = dock.footprintRadius(); for (let i = 0; i < 16; i++) { let pos = [ dockPos[0] + radius*Math.cos(i*Math.PI/8), dockPos[1] + radius*Math.sin(i*Math.PI/8)]; index = gameState.ai.accessibility.getAccessValue(pos, onWater); if (index >= 2) break; } } if (index < 2) API3.warn("ERROR in Petra navalManager because of dock position (onWater=" + onWater + ") index " + index); return index; }; /** get the list of seas (or lands) around this region not connected by a dock */ m.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) { let seas = gameState.ai.accessibility.regionLinks[region].slice(); let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); docks.forEach(function (dock) { if (dock.getMetadata(PlayerID, "access") !== region) return; let i = seas.indexOf(dock.getMetadata(PlayerID, "sea")); if (i !== -1) seas.splice(i--,1); }); return seas; }; m.NavalManager.prototype.checkEvents = function(gameState, queues, events) { for (let evt of events.ConstructionFinished) { if (!evt || !evt.newentity) continue; let entity = gameState.getEntityById(evt.newentity); if (entity && entity.hasClass("Dock") && entity.isOwn(PlayerID)) this.setDockIndex(gameState, entity); } for (let evt of events.TrainingFinished) { if (!evt || !evt.entities) continue; for (let entId of evt.entities) { let entity = gameState.getEntityById(entId); if (!entity || !entity.hasClass("Ship") || !entity.isOwn(PlayerID)) continue; this.setShipIndex(gameState, entity); } } for (let evt of events.Destroy) { if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID]) continue; if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter) continue; let plan = this.getPlan(evt.metadata[PlayerID].transporter); if (!plan) continue; let shipId = evt.entityObj.id(); if (this.Config.debug > 1) API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state); if (plan.state === "boarding") { // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan plan.units.forEach(function (ent) { if ((ent.getMetadata(PlayerID, "onBoard") === "onBoard" && ent.position()) || ent.getMetadata(PlayerID, "onBoard") === shipId) ent.setMetadata(PlayerID, "onBoard", undefined); }); plan.needTransportShips = !plan.transportShips.hasEntities(); } else if (plan.state === "sailing") { let endIndex = plan.endIndex; let self = this; plan.units.forEach(function (ent) { if (!ent.position()) // unit from another ship of this plan ... do nothing return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); let endPos = ent.getMetadata(PlayerID, "endPos"); ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); // nothing else to do if access = endIndex as already at destination // otherwise, we should require another transport // TODO if attacking and no more ships available, remove the units from the attack // to avoid delaying it too much if (access !== endIndex) self.requireTransport(gameState, ent, access, endIndex, endPos); }); } } for (let evt of events.OwnershipChanged) // capture events { if (evt.to === PlayerID) { let ent = gameState.getEntityById(evt.entity); if (ent && ent.hasClass("Dock")) this.setDockIndex(gameState, ent); } } }; m.NavalManager.prototype.getPlan = function(ID) { for (let plan of this.transportPlans) if (plan.ID === ID) return plan; return undefined; }; m.NavalManager.prototype.addPlan = function(plan) { this.transportPlans.push(plan); }; /** * complete already existing plan or create a new one for this requirement * (many units can then call this separately and end up in the same plan) * TODO check garrison classes */ m.NavalManager.prototype.requireTransport = function(gameState, entity, startIndex, endIndex, endPos) { if (entity.getMetadata(PlayerID, "transport") !== undefined) { if (this.Config.debug > 0) - API3.warn("Petra naval manager error: unit " + entity.id() + " has already required a transport"); + API3.warn("Petra naval manager error: unit " + entity.id() + " has already required a transport"); return false; } for (let plan of this.transportPlans) { if (plan.startIndex !== startIndex || plan.endIndex !== endIndex) continue; if (plan.state !== "boarding") continue; plan.addUnit(entity, endPos); return true; } let plan = new m.TransportPlan(gameState, [entity], startIndex, endIndex, endPos); if (plan.failed) { if (this.Config.debug > 1) API3.warn(">>>> transport plan aborted <<<<"); return false; } plan.init(gameState); this.transportPlans.push(plan); return true; }; /** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */ m.NavalManager.prototype.splitTransport = function(gameState, plan) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan started <<<<"); let newplan = new m.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); if (newplan.failed) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan aborted <<<<"); return false; } newplan.init(gameState); let nbUnits = 0; plan.units.forEach(function (ent) { if (ent.getMetadata(PlayerID, "onBoard")) return; ++nbUnits; newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos")); }); if (this.Config.debug > 1) API3.warn(">>>> previous plan left with units " + plan.units.length); if (nbUnits) this.transportPlans.push(newplan); return nbUnits !== 0; }; /** * create a transport from a garrisoned ship to a land location * needed at start game when starting with a garrisoned ship */ m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) { let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); if (fromAccess !== 1) return; if (toAccess < 2) return; for (let ship of this.ships.values()) { if (!ship.isGarrisonHolder() || !ship.garrisoned().length) continue; if (ship.getMetadata(PlayerID, "transporter") !== undefined) continue; let units = []; for (let entId of ship.garrisoned()) units.push(gameState.getEntityById(entId)); // TODO check that the garrisoned units have not another purpose let plan = new m.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); if (plan.failed) continue; plan.init(gameState); this.transportPlans.push(plan); } }; // set minimal number of needed ships when a new event (new base or new attack plan) m.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) { if (!sea) return; if (this.wantedTransportShips[sea] < number ) this.wantedTransportShips[sea] = number; }; // bumps up the number of ships we want if we need more. m.NavalManager.prototype.checkLevels = function(gameState, queues) { if (queues.ships.hasQueuedUnits()) return; for (let sea = 0; sea < this.neededTransportShips.length; sea++) this.neededTransportShips[sea] = 0; for (let plan of this.transportPlans) { if (!plan.needTransportShips || plan.units.length < 2) continue; let sea = plan.sea; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 || this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) continue; ++this.neededTransportShips[sea]; if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2) { ++this.wantedTransportShips[sea]; return; } } for (let sea = 0; sea < this.neededTransportShips.length; sea++) if (this.neededTransportShips[sea] > 2) ++this.wantedTransportShips[sea]; }; m.NavalManager.prototype.maintainFleet = function(gameState, queues) { if (queues.ships.hasQueuedUnits()) return; if (!gameState.getOwnEntitiesByClass("Dock", true).filter(API3.Filters.isBuilt()).hasEntities() && !gameState.getOwnEntitiesByClass("Shipyard", true).filter(API3.Filters.isBuilt()).hasEntities()) return; // check if we have enough transport ships per region. for (let sea = 0; sea < this.seaShips.length; ++sea) { if (this.seaShips[sea] === undefined) continue; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) continue; if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) { let template = this.getBestShip(gameState, sea, "transport"); if (template) { queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); continue; } } if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) { let template = this.getBestShip(gameState, sea, "fishing"); if (template) { queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); continue; } } } }; /** assigns free ships to plans that need some */ m.NavalManager.prototype.assignShipsToPlans = function(gameState) { for (let plan of this.transportPlans) if (plan.needTransportShips) plan.assignShip(gameState); }; /** let blocking ships move apart from active ships (waiting for a better pathfinder) */ m.NavalManager.prototype.moveApart = function(gameState) { let self = this; this.ships.forEach(function(ship) { if (ship.hasClass("FishingBoat")) // small ships should not be a problem return; let sea = ship.getMetadata(PlayerID, "sea"); if (ship.getMetadata(PlayerID, "transporter") === undefined) { if (ship.isIdle()) // do not stay idle near a dock to not disturb other ships { gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).forEach(function(dock) { if (dock.getMetadata(PlayerID, "sea") !== sea) return; if (API3.SquareVectorDistance(ship.position(), dock.position()) > 2500) return; ship.moveApart(dock.position(), 50); }); } return; } // if transporter ship not idle, move away other ships which could block it self.seaShips[sea].forEach(function(blockingShip) { if (blockingShip === ship || !blockingShip.isIdle()) return; if (API3.SquareVectorDistance(ship.position(), blockingShip.position()) > 900) return; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(ship.position(), 12); else blockingShip.moveApart(ship.position(), 6); }); }); gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).forEach(function(ship) { if (ship.getMetadata(PlayerID, "route") === undefined) return; let sea = ship.getMetadata(PlayerID, "sea"); self.seaShips[sea].forEach(function(blockingShip) { if (blockingShip === ship || !blockingShip.isIdle()) return; if (API3.SquareVectorDistance(ship.position(), blockingShip.position()) > 900) return; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(ship.position(), 12); else blockingShip.moveApart(ship.position(), 6); }); }); }; m.NavalManager.prototype.buildNavalStructures = function(gameState, queues) { if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) return; if (gameState.getPopulation() > this.Config.Economy.popForDock) { if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 && !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), API3.Filters.isFoundation())).hasEntities() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_dock")) { let dockStarted = false; for (let base of gameState.ai.HQ.baseManagers) { if (dockStarted) break; if (!base.anchor || base.constructing) continue; let remaining = this.getUnconnectedSeas(gameState, base.accessIndex); for (let sea of remaining) { if (!gameState.ai.HQ.navalRegions[sea]) continue; let wantedLand = {}; wantedLand[base.accessIndex] = true; queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": wantedLand, "sea": sea })); dockStarted = true; break; } } } } if (gameState.currentPhase() < 2 || gameState.getPopulation() < this.Config.Economy.popForTown + 15 || queues.militaryBuilding.hasQueuedUnits() || this.bNaval.length === 0) return; let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); if (!docks.hasEntities()) return; let nNaval = 0; for (let naval of this.bNaval) nNaval += gameState.countEntitiesAndQueuedByType(naval, true); if (nNaval === 0 || (nNaval < this.bNaval.length && gameState.getPopulation() > 120)) { for (let naval of this.bNaval) { if (gameState.countEntitiesAndQueuedByType(naval, true) < 1 && gameState.ai.HQ.canBuild(gameState, naval)) { let wantedLand = {}; for (let base of gameState.ai.HQ.baseManagers) if (base.anchor) wantedLand[base.accessIndex] = true; let sea = docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, naval, { "land": wantedLand, "sea": sea })); break; } } } }; /** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ m.NavalManager.prototype.getBestShip = function(gameState, sea, goal) { let civ = gameState.civ(); let trainableShips = []; gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { let trainables = ent.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) trainableShips.push(trainable); } }); let best = 0; let bestShip; let limits = gameState.getEntityLimits(); let current = gameState.getEntityCounts(); for (let trainable of trainableShips) { let template = gameState.getTemplate(trainable); if (!template.available(gameState)) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; let arrows = +(template.getDefaultArrow() || 0); if (goal === "attack") // choose the maximum default arrows { if (best > arrows) continue; best = arrows; } else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport { let capacity = +(template.garrisonMax() || 0); if (capacity < 2) continue; capacity += 10*arrows; if (MatchesClassList(template.garrisonableClasses(), "Siege")) capacity += 50; if (best > capacity) continue; best = capacity; } else if (goal === "fishing") if (!template.hasClass("FishingBoat")) continue; bestShip = trainable; } return bestShip; }; m.NavalManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Naval Manager update"); this.checkEvents(gameState, queues, events); // close previous transport plans if finished for (let i = 0; i < this.transportPlans.length; ++i) { let remaining = this.transportPlans[i].update(gameState); if (remaining) continue; if (this.Config.debug > 1) API3.warn("no more units on transport plan " + this.transportPlans[i].ID); this.transportPlans[i].releaseAll(); this.transportPlans.splice(i--, 1); } // assign free ships to plans which need them this.assignShipsToPlans(gameState); // and require for more ships/structures if needed if (gameState.ai.playedTurn % 3 === 0) { this.checkLevels(gameState, queues); this.maintainFleet(gameState, queues); this.buildNavalStructures(gameState, queues); } // let inactive ships move apart from active ones (waiting for a better pathfinder) this.moveApart(gameState); Engine.ProfileStop(); }; m.NavalManager.prototype.Serialize = function() { let properties = { "wantedTransportShips": this.wantedTransportShips, "wantedWarShips": this.wantedWarShips, "wantedFishShips": this.wantedFishShips, "neededTransportShips": this.neededTransportShips, "neededWarShips": this.neededWarShips, "landingZones": this.landingZones }; let transports = {}; for (let plan in this.transportPlans) transports[plan] = this.transportPlans[plan].Serialize(); return { "properties": properties, "transports": transports }; }; m.NavalManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.transportPlans = []; for (let i in data.transports) { let dataPlan = data.transports[i]; let plan = new m.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); plan.Deserialize(dataPlan); plan.init(gameState); this.transportPlans.push(plan); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 18429) @@ -1,798 +1,798 @@ var PETRA = function(m) { /** * Defines a construction plan, ie a building. * We'll try to fing a good position if non has been provided */ m.ConstructionPlan = function(gameState, type, metadata, position) { if (!m.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); m.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn return false; if (!this.isGo(gameState)) return false; if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) return false; return gameState.findBuilder(this.type) !== undefined; }; m.ConstructionPlan.prototype.start = function(gameState) { Engine.ProfileStart("Building construction start"); // We don't care which builder we assign, since they won't actually // do the building themselves - all we care about is that there is // at least one unit that can start the foundation let builder = gameState.findBuilder(this.type); let pos = this.findGoodPosition(gameState); if (!pos) { gameState.ai.HQ.stopBuild(gameState, this.type); Engine.ProfileStop(); return; } if (this.metadata && this.metadata.expectedGain) { // Check if this market is still worth building (others may have been built making it useless) let tradeManager = gameState.ai.HQ.tradeManager; tradeManager.checkRoutes(gameState); if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { Engine.ProfileStop(); return; } } gameState.ai.HQ.turnCache.buildingBuilt = true; if (this.metadata === undefined) this.metadata = { "base": pos.base }; else if (this.metadata.base === undefined) this.metadata.base = pos.base; if (pos.access) this.metadata.access = pos.access; // needed for Docks whose position is on water else this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); if (this.template.buildCategory() === "Dock") { // adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } else if (pos.xx === undefined || (pos.x == pos.xx && pos.z == pos.zz)) builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); else // try with the lowest, move towards us unless we're same { for (let step = 0; step <= 1; step += 0.2) builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, pos.angle, this.metadata); } this.onStart(gameState); Engine.ProfileStop(); if (this.metadata && this.metadata.proximity) gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); }; m.ConstructionPlan.prototype.findGoodPosition = function(gameState) { let template = this.template; if (template.buildCategory() === "Dock") return this.findDockPosition(gameState); if (template.hasClass("Storehouse") && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = gameState.ai.HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; let newpos = base.findBestDropsiteLocation(gameState, type); if (newpos && newpos.quality > 0) { let pos = newpos.pos; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; } } if (!this.position) { if (template.hasClass("CivCentre")) { let pos; if (this.metadata && this.metadata.resource) { let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; pos = gameState.ai.HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = gameState.ai.HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; return false; } else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) { let pos = gameState.ai.HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; if (template.hasClass("DefenseTower") || gameState.civ() === "mace" || gameState.civ() === "maur" || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_fortress"), true) > 0 || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_army_camp"), true) > 0) return false; // if this fortress is our first siege unit builder, just try the standard placement as we want siege units } else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before { let pos = gameState.ai.HQ.findMarketLocation(gameState, template); if (pos && pos[2] > 0) { if (!this.metadata) this.metadata = {}; this.metadata.expectedGain = pos[3]; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; } else if (!pos) return false; } } // Compute each tile's closeness to friendly structures: let placement = new API3.Map(gameState.sharedScript, "territory"); let cellSize = placement.cellSize; // size of each tile let alreadyHasHouses = false; if (this.position) // If a position was specified then place the building as close to it as possible { let x = Math.floor(this.position[0] / cellSize); let z = Math.floor(this.position[1] / cellSize); placement.addInfluence(x, z, 255); } else // No position was specified so try and find a sensible place to build { // give a small > 0 level as the result of addInfluence is constrained to be > 0 // if we really need houses (i.e. townPhasing without enough village building), do not apply these constraints if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] == base) placement.map[j] = 45; } else { for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] !== 0) placement.map[j] = 45; } if (!gameState.ai.HQ.requireHouses || !template.hasClass("House")) { gameState.getOwnStructures().forEach(function(ent) { let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1) { if (template.hasClass("Field") || template.hasClass("Corral")) placement.addInfluence(x, z, 80/cellSize, 50); else // If this is not a field add a negative influence because we want to leave this area for fields placement.addInfluence(x, z, 80/cellSize, -20); } else if (template.hasClass("House")) { if (ent.hasClass("House")) { placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses alreadyHasHouses = true; } else if (!ent.hasClass("StoneWall") || ent.hasClass("Gates")) placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs } else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && !ent.hasClass("Corral") && (!ent.hasClass("StoneWall") || ent.hasClass("Gates")))) placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.genericName() == "House") placement.addInfluence(x, z, 120/cellSize, -50); else if (template.hasClass("Military")) placement.addInfluence(x, z, 40/cellSize, -40); else if (template.genericName() === "Rotary Mill" && ent.hasClass("Field")) placement.addInfluence(x, z, 60/cellSize, 40); }); } if (template.hasClass("Farmstead")) { for (let j = 0; j < placement.map.length; ++j) { let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; placement.map[j] = value >= 0 ? value : 0; if (gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] /= 2; // we need space around farmstead, so disfavor map border } } } // Requires to be inside our territory, and inside our base territory if required // and if our first market, put it on border if possible to maximize distance with next market let favorBorder = template.hasClass("BarterMarket"); let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); let preferredBase = this.metadata && this.metadata.preferredBase; if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] != base) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] += 50; else if (disfavorBorder && gameState.ai.HQ.borderMap.map[j] === 0 && placement.map[j] > 0) placement.map[j] += 10; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] === 0) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] += 50; else if (disfavorBorder && gameState.ai.HQ.borderMap.map[j] === 0 && placement.map[j] > 0) placement.map[j] += 10; if (preferredBase && gameState.ai.HQ.basesMap.map[j] == this.metadata.preferredBase) placement.map[j] += 200; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } // Find the best non-obstructed: // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, // this allows room for units to walk between buildings. // note: not for houses and dropsites who ought to be closer to either each other or a resource. // also not for fields who can be stacked quite a bit let obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); let radius = 0; if (template.hasClass("Fortress") || this.type === gameState.applyCiv("structures/{civ}_siege_workshop") || this.type === gameState.applyCiv("structures/{civ}_elephant_stables")) radius = Math.floor((template.obstructionRadius() + 12) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && !template.hasClass("Field")) radius = Math.ceil((template.obstructionRadius() + 4) / obstructions.cellSize); else radius = Math.ceil((template.obstructionRadius() + 0.5) / obstructions.cellSize); let bestTile; let bestVal; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); bestVal = bestTile[1]; } if (bestVal === undefined || bestVal === -1) { bestTile = placement.findBestTile(radius, obstructions); bestVal = bestTile[1]; } let bestIdx = bestTile[0]; if (bestVal <= 0) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined) { let secondBest = obstructions.findNearestObstructed(bestIdx, radius); if (secondBest >= 0) { x = (secondBest % obstructions.width + 0.5) * obstructions.cellSize; z = (Math.floor(secondBest / obstructions.width) + 0.5) * obstructions.cellSize; } } let territorypos = placement.gamePosToMapPos([x, z]); let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": gameState.ai.HQ.basesMap.map[territoryIndex] }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory */ m.ConstructionPlan.prototype.findDockPosition = function(gameState) { let template = this.template; let territoryMap = gameState.ai.HQ.territoryMap; let obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); let bestIdx; let bestJdx; let bestAngle; let bestLand; let bestWater; let bestVal = -1; let navalPassMap = gameState.ai.accessibility.navalPassMap; let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let nbShips = gameState.ai.HQ.navalManager.transportShips.length; let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; if (nbShips === 0 && proxyAccess && proxyAccess > 1) { wantedLand = {}; wantedLand[proxyAccess] = true; } let dropsiteTypes = template.resourceDropsiteTypes(); let radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); let halfSize = 0; // used for dock angle let halfDepth = 0; // used by checkPlacement let halfWidth = 0; // used by checkPlacement if (template.get("Footprint/Square")) { halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; halfDepth = +template.get("Footprint/Square/@depth") / 2; halfWidth = +template.get("Footprint/Square/@width") / 2; } else if (template.get("Footprint/Circle")) { halfSize = +template.get("Footprint/Circle/@radius"); halfDepth = halfSize; halfWidth = halfSize; } // res is a measure of the amount of resources around, and maxRes is the max value taken into account // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement const maxRes = 10; const maxWater = 16; for (let j = 0; j < territoryMap.length; ++j) { if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) continue; let dist; if (!proxyAccess) { // if not in our (or allied) territory, we do not want it too far to be able to defend it dist = this.getFrontierProximity(gameState, j); if (dist > 4) continue; } let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; if (wantedSea && navalPassMap[i] !== wantedSea) continue; let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; if (proxyAccess) { // if proximity is given, we look for the nearest point dist = API3.SquareVectorDistance(this.metadata.proximity, pos); dist = Math.sqrt(dist) + 20 * (maxRes - res); } else dist += 0.6 * (maxRes - res); // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] > 0) dist += 2; // do a pre-selection, supposing we will have the best possible water if (bestIdx !== undefined && dist > bestVal + maxWater) continue; let x = (i % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; let angle = this.getDockAngle(gameState, x, z, halfSize); if (angle === false) continue; let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); if (!ret || !gameState.ai.HQ.landRegions[ret.land]) continue; // final selection now that the checkDockPlacement water is known if (bestIdx !== undefined && dist + maxWater - ret.water > bestVal) continue; if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = dist + maxWater - ret.water; bestIdx = i; bestJdx = j; bestAngle = angle; bestLand = ret.land; bestWater = ret.water; } if (bestVal < 0) return false; // if no good place with enough water around and still in first phase, wait for expansion at the next phase if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Assign this dock to a base let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; if (!baseIndex) { for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex !== bestLand) continue; baseIndex = base.ID; break; } if (!baseIndex) { if (gameState.ai.HQ.numActiveBase() > 0) API3.warn("Petra: dock constructed without base index " + baseIndex); else baseIndex = gameState.ai.HQ.baseManagers[0].ID; } } return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let seaRef = gameState.ai.accessibility.navalPassMap[k]; if (seaRef < 2) return false; const numPoints = 16; for (let dist = 0; dist < 4; ++dist) { let waterPoints = []; for (let i = 0; i < numPoints; ++i) { let angle = (i/numPoints)*2*Math.PI; pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; pos = gameState.ai.accessibility.gamePosToMapPos(pos); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.navalPassMap[j] === seaRef) waterPoints.push(i); } let length = waterPoints.length; if (!length) continue; let consec = []; for (let i = 0; i < length; ++i) { let count = 0; for (let j = 0; j < length-1; ++j) { if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } let start = 0; let count = 0; for (let c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; } return false; }; /** * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js * to determine the special dock requirements * returns {"land": land index for this dock, "water": amount of water around this spot} */ m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) { let sz = halfDepth * Math.sin(angle); let cz = halfDepth * Math.cos(angle); // center back position let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); let j = pos[0] + pos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[j]; if (land < 2) return null; // center front position pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) return null; // additional constraints compared to BuildRestriction.js to assure we have enough place to build let sw = halfWidth * Math.cos(angle) * 3 / 4; let cw = halfWidth * Math.sin(angle) * 3 / 4; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; let water = 0; let sp = 15 * Math.sin(angle); let cp = 15 * Math.cos(angle); for (let i = 1; i < 5; ++i) { pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; water += 4; } return {"land": land, "water": water}; }; /** * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension * if the (object) wantedLand is given, this nearest land should have one of these accessibility * if wantedSea is given, this tile should be inside this sea */ const around = [ [ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], [-1.0, 0.0], [-0.87,-0.50], [-0.50,-0.87], [ 0.0,-1.0], [ 0.50,-0.87], [ 0.87,-0.50] ]; m.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) { let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let dist = dimension + 2*cellSize; let x = (j%width + 0.5) * cellSize; let z = (Math.floor(j/width) + 0.5) * cellSize; for (let a of around) { let pos = gameState.ai.accessibility.gamePosToMapPos([x + dist*a[0], z + dist*a[1]]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) continue; if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let landPass = gameState.ai.accessibility.landPassMap[k]; if (landPass < 2 || (wantedLand && !wantedLand[landPass])) continue; pos = gameState.ai.accessibility.gamePosToMapPos([x - dist*a[0], z - dist*a[1]]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) continue; if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; k = pos[0] + pos[1]*gameState.ai.accessibility.width; if (wantedSea && gameState.ai.accessibility.navalPassMap[k] !== wantedSea) continue; else if (!wantedSea && gameState.ai.accessibility.navalPassMap[k] < 2) continue; return true; } return false; }; /** * return a measure of the proximity to our frontier (including our allies) * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m */ m.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) { let territoryMap = gameState.ai.HQ.territoryMap; if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(j))) return 0; let borderMap = gameState.ai.HQ.borderMap; let width = territoryMap.width; let step = Math.round(24 / territoryMap.cellSize); let ix = j % width; let iz = Math.floor(j / width); let best = 5; for (let a of around) { for (let i = 1; i < 5; ++i) { let jx = ix + Math.round(i*step*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(i*step*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] > 1) continue; if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) { best = Math.min(best, i); break; } } if (best === 1) break; } return best; }; /** * get the sum of the resources (except food) around, inside a given radius * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood */ m.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) { let resourceMaps = gameState.sharedScript.resourceMaps; let w = resourceMaps.wood.width; let cellSize = resourceMaps.wood.cellSize; let size = Math.floor(radius / cellSize); let ix = i % w; let iy = Math.floor(i / w); let total = 0; let nbcell = 0; for (let k of types) { if (k === "food" || !resourceMaps[k]) continue; let weigh0 = k === "wood" ? 2 : 1; for (let dy = 0; dy <= size; ++dy) { let dxmax = size - dy; let ky = iy + dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } if (dy === 0) continue; ky = iy - dy; - if (ky >= 0 && ky < w) + if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } } } return nbcell ? total / nbcell : 0; }; m.ConstructionPlan.prototype.Serialize = function() { let prop = { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position }; let func = { "isGo": uneval(this.isGo), "onStart": uneval(this.onStart) }; return { "prop": prop, "func": func }; }; m.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.prop) this[key] = data.prop[key]; let cost = new API3.Resources(); cost.Deserialize(data.prop.cost); this.cost = cost; for (let fun in data.func) this[fun] = eval(data.func[fun]); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 18428) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 18429) @@ -1,630 +1,630 @@ var PETRA = function(m) { /** * Describes a transport plan * Constructor assign units (units is an ID array), a destination (position). * The naval manager will try to deal with it accordingly. * * By this I mean that the naval manager will find how to go from access point 1 to access point 2 * and then carry units from there. * * Note: only assign it units currently over land, or it won't work. * Also: destination should probably be land, otherwise the units will be lost at sea. * * metadata for units: * transport = this.ID * onBoard = ship.id() when affected to a ship but not yet garrisoned * = "onBoard" when garrisoned in a ship * = undefined otherwise * endPos = position of destination * * metadata for ships * transporter = this.ID */ m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos, ship) { this.ID = gameState.ai.uniqueIDs.transports++; this.debug = gameState.ai.Config.debug; this.flotilla = false; // when false, only one ship per transport ... not yet tested when true this.endPos = endPos; this.endIndex = endIndex; this.startIndex = startIndex; // TODO only cases with land-sea-land are allowed for the moment // we could also have land-sea-land-sea-land if (startIndex === 1) { // special transport from already garrisoned ship if (!ship) { this.failed = true; return false; } this.sea = ship.getMetadata(PlayerID, "sea"); ship.setMetadata(PlayerID, "transporter", this.ID); for (let ent of units) ent.setMetadata(PlayerID, "onBoard", "onBoard"); } else { this.sea = gameState.ai.HQ.getSeaIndex(gameState, startIndex, endIndex); if (!this.sea) { this.failed = true; if (this.debug > 1) API3.warn("transport plan with bad path: startIndex " + startIndex + " endIndex " + endIndex); return false; } } for (let ent of units) { ent.setMetadata(PlayerID, "transport", this.ID); ent.setMetadata(PlayerID, "endPos", endPos); } if (this.debug > 1) - API3.warn("Starting a new transport plan with ID " + this.ID + + API3.warn("Starting a new transport plan with ID " + this.ID + " to index " + endIndex + " with units length " + units.length); this.state = "boarding"; this.boardingPos = {}; this.needTransportShips = ship === undefined; this.nTry = {}; return true; }; m.TransportPlan.prototype.init = function(gameState) { this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "transport", this.ID)); this.ships = gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); this.transportShips = gameState.ai.HQ.navalManager.transportShips.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); this.units.registerUpdates(); this.ships.registerUpdates(); this.transportShips.registerUpdates(); this.boardingRange = 18*18; // TODO compute it from the ship clearance and garrison range }; /** count available slots */ m.TransportPlan.prototype.countFreeSlots = function() { let self = this; let slots = 0; this.transportShips.forEach(function (ship) { slots += self.countFreeSlotsOnShip(ship); }); return slots; }; m.TransportPlan.prototype.countFreeSlotsOnShip = function(ship) { if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints()) return 0; let occupied = ship.garrisoned().length + this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", ship.id())).length; return Math.max(ship.garrisonMax() - occupied, 0); }; m.TransportPlan.prototype.assignUnitToShip = function(gameState, ent) { if (this.needTransportShips) return; for (let ship of this.transportShips.values()) { if (this.countFreeSlotsOnShip(ship) === 0) continue; ent.setMetadata(PlayerID, "onBoard", ship.id()); if (this.debug > 1) { if (ent.getMetadata(PlayerID, "role") === "attack") Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); else Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); } return; } if (this.flotilla) this.needTransportShips = true; else gameState.ai.HQ.navalManager.splitTransport(gameState, this); }; m.TransportPlan.prototype.assignShip = function(gameState) { let pos; // choose a unit of this plan not yet assigned to a ship for (let ent of this.units.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "onBoard") !== undefined) continue; pos = ent.position(); break; } // and choose the nearest available ship from this unit let distmin = Math.min(); let nearest; gameState.ai.HQ.navalManager.seaTransportShips[this.sea].forEach(function (ship) { if (ship.getMetadata(PlayerID, "transporter")) return; if (pos) { let dist = API3.SquareVectorDistance(pos, ship.position()); if (dist > distmin) return; distmin = dist; nearest = ship; } else if (!nearest) nearest = ship; }); if (!nearest) return false; nearest.setMetadata(PlayerID, "transporter", this.ID); this.ships.updateEnt(nearest); this.transportShips.updateEnt(nearest); this.needTransportShips = false; return true; }; /** add a unit to this plan */ m.TransportPlan.prototype.addUnit = function(unit, endPos) { unit.setMetadata(PlayerID, "transport", this.ID); unit.setMetadata(PlayerID, "endPos", endPos); this.units.updateEnt(unit); }; m.TransportPlan.prototype.releaseAll = function() { this.ships.forEach(function (ship) { ship.setMetadata(PlayerID, "transporter", undefined); if (ship.getMetadata(PlayerID, "role") === "switchToTrader") ship.setMetadata(PlayerID, "role", "trader"); }); this.units.forEach(function (ent) { ent.setMetadata(PlayerID, "endPos", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "transport", undefined); // TODO if the index of the endPos of the entity is !== , require again another transport (we could need land-sea-land-sea-land) }); this.transportShips.unregister(); this.ships.unregister(); this.units.unregister(); }; m.TransportPlan.prototype.cancelTransport = function(gameState) { let ent = this.units.toEntityArray()[0]; let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); if (!base.anchor || !base.anchor.position()) { for (let newbase of gameState.ai.HQ.baseManagers) { if (!newbase.anchor || !newbase.anchor.position()) continue; ent.setMetadata(PlayerID, "base", newbase.ID); base = newbase; break; } if (!base.anchor || !base.anchor.position()) return false; this.units.forEach(function (ent) { ent.setMetadata(PlayerID, "base", base.ID); }); } this.endIndex = this.startIndex; this.endPos = base.anchor.position(); this.canceled = true; return true; }; /** * try to move on. There are two states: * - "boarding" means we're trying to board units onto our ships * - "sailing" means we're moving ships and eventually unload units * - then the plan is cleared */ m.TransportPlan.prototype.update = function(gameState) { if (this.state === "boarding") this.onBoarding(gameState); else if (this.state === "sailing") this.onSailing(gameState); return this.units.length; }; m.TransportPlan.prototype.onBoarding = function(gameState) { let ready = true; let self = this; let time = gameState.ai.elapsedTime; this.units.forEach(function (ent) { if (!ent.getMetadata(PlayerID, "onBoard")) { ready = false; self.assignUnitToShip(gameState, ent); if (ent.getMetadata(PlayerID, "onBoard")) { let shipId = ent.getMetadata(PlayerID, "onBoard"); let ship = gameState.getEntityById(shipId); if (!self.boardingPos[shipId]) { self.boardingPos[shipId] = self.getBoardingPos(gameState, ship, self.startIndex, self.sea, ent.position(), false); ship.move(self.boardingPos[shipId][0], self.boardingPos[shipId][1]); ship.setMetadata(PlayerID, "timeGarrison", time); } ent.garrison(ship); ent.setMetadata(PlayerID, "timeGarrison", time); ent.setMetadata(PlayerID, "posGarrison", ent.position()); } } else if (ent.getMetadata(PlayerID, "onBoard") !== "onBoard" && !self.isOnBoard(ent)) { ready = false; let shipId = ent.getMetadata(PlayerID, "onBoard"); let ship = gameState.getEntityById(shipId); if (!ship) // the ship must have been destroyed ent.setMetadata(PlayerID, "onBoard", undefined); else { let distShip = API3.SquareVectorDistance(self.boardingPos[shipId], ship.position()); if (time - ship.getMetadata(PlayerID, "timeGarrison") > 8 && distShip > self.boardingRange) { if (!self.nTry[shipId]) self.nTry[shipId] = 1; else ++self.nTry[shipId]; if (self.nTry[shipId] > 1) // we must have been blocked by something ... try with another boarding point { self.nTry[shipId] = 0; if (self.debug > 1) API3.warn("ship " + shipId + " new attempt for a landing point "); self.boardingPos[shipId] = self.getBoardingPos(gameState, ship, self.startIndex, self.sea, undefined, false); } ship.move(self.boardingPos[shipId][0], self.boardingPos[shipId][1]); ship.setMetadata(PlayerID, "timeGarrison", time); } else if (time - ent.getMetadata(PlayerID, "timeGarrison") > 2) { let oldPos = ent.getMetadata(PlayerID, "posGarrison"); let newPos = ent.position(); if (oldPos[0] === newPos[0] && oldPos[1] === newPos[1]) { if (distShip < self.boardingRange) // looks like we are blocked ... try to go out of this trap { if (!self.nTry[ent.id()]) self.nTry[ent.id()] = 1; else ++self.nTry[ent.id()]; if (self.nTry[ent.id()] > 5) { if (self.debug > 1) API3.warn("unit blocked, but no ways out of the trap ... destroy it"); self.resetUnit(gameState, ent); ent.destroy(); return; } if (self.nTry[ent.id()] > 1) ent.moveToRange(newPos[0], newPos[1], 30, 30); ent.garrison(ship, true); } else // wait for the ship ent.move(self.boardingPos[shipId][0], self.boardingPos[shipId][1]); } else self.nTry[ent.id()] = 0; ent.setMetadata(PlayerID, "timeGarrison", time); ent.setMetadata(PlayerID, "posGarrison", ent.position()); } } } }); if (!ready) return; this.ships.forEach(function (ship) { self.boardingPos[ship.id()] = undefined; self.boardingPos[ship.id()] = self.getBoardingPos(gameState, ship, self.endIndex, self.sea, self.endPos, true); ship.move(self.boardingPos[ship.id()][0], self.boardingPos[ship.id()][1]); }); this.state = "sailing"; this.nTry = {}; this.unloaded = []; this.recovered = []; }; /** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */ m.TransportPlan.prototype.isOnBoard = function(ent) { for (let ship of this.transportShips.values()) { if (ship.garrisoned().indexOf(ent.id()) === -1) continue; ent.setMetadata(PlayerID, "onBoard", "onBoard"); return true; } return false; }; /** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */ m.TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, seaIndex, destination, avoidEnnemy) { if (!gameState.ai.HQ.navalManager.landingZones[landIndex]) { API3.warn(" >>> no landing zone for land " + landIndex); return destination; } else if (!gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) { API3.warn(" >>> no landing zone for land " + landIndex + " and sea " + seaIndex); return destination; } let startPos = ship.position(); let distmin = Math.min(); let posmin = destination; let width = gameState.getMap().width; let cell = gameState.getMap().cellSize; let alliedDocks = gameState.getAllyStructures().filter(API3.Filters.and( API3.Filters.byClass("Dock"), API3.Filters.byMetadata(PlayerID, "sea", seaIndex))).toEntityArray(); for (let i of gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) { let pos = [i%width+0.5, Math.floor(i/width)+0.5]; pos = [cell*pos[0], cell*pos[1]]; let dist = API3.SquareVectorDistance(startPos, pos); if (destination) dist += API3.SquareVectorDistance(pos, destination); if (avoidEnnemy) { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) dist += 100000000; } // require a small distance between all ships of the transport plan to avoid path finder problems // this is also used when the ship is blocked and we want to find a new boarding point for (let shipId in this.boardingPos) if (this.boardingPos[shipId] !== undefined && API3.SquareVectorDistance(this.boardingPos[shipId], pos) < this.boardingRange) dist += 1000000; // and not too near our allied docks to not disturb naval traffic for (let dock of alliedDocks) { let dockDist = API3.SquareVectorDistance(dock.position(), pos); if (dockDist < 4900) dist += 100000 * (4900 - dockDist) / 4900; } if (dist > distmin) continue; distmin = dist; posmin = pos; } // We should always have either destination or the previous boardingPos defined // so let's return this value if everything failed if (!posmin && this.boardingPos[ship.id()]) posmin = this.boardingPos[ship.id()]; return posmin; }; m.TransportPlan.prototype.onSailing = function(gameState) { // Check that the units recovered on the previous turn have been reloaded for (let recov of this.recovered) { let ent = gameState.getEntityById(recov.entId); if (!ent) // entity destroyed continue; if (!ent.position()) // reloading succeeded ... move a bit the ship before trying again { let ship = gameState.getEntityById(recov.shipId); if (ship) ship.moveApart(recov.entPos, 15); continue; } if (this.debug > 1) API3.warn(">>> transport " + this.ID + " reloading failed ... <<<"); // destroy the unit if inaccessible otherwise leave it there let index = gameState.ai.accessibility.getAccessValue(ent.position()); if (gameState.ai.HQ.landRegions[index]) { if (this.debug > 1) API3.warn(" recovered entity kept " + ent.id()); this.resetUnit(gameState, ent); // TODO we should not destroy it, but now the unit could still be reloaded on the next turn // and mess everything ent.destroy(); } else { if (this.debug > 1) API3.warn("recovered entity destroyed " + ent.id()); this.resetUnit(gameState, ent); ent.destroy(); } } this.recovered = []; // Check that the units unloaded on the previous turn have been really unloaded and in the right position let shipsToMove = {}; for (let entId of this.unloaded) { let ent = gameState.getEntityById(entId); if (!ent) // entity destroyed continue; else if (!ent.position()) // unloading failed { let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); if (ship) { if (ship.garrisoned().indexOf(entId) !== -1) ent.setMetadata(PlayerID, "onBoard", "onBoard"); else { API3.warn("Petra transportPlan problem: unit not on ship without position ???"); this.resetUnit(gameState, ent); ent.destroy(); } } else { API3.warn("Petra transportPlan problem: unit on ship, but no ship ???"); this.resetUnit(gameState, ent); ent.destroy(); } } else if (gameState.ai.accessibility.getAccessValue(ent.position()) !== this.endIndex) { // unit unloaded on a wrong region - try to regarrison it and move a bit the ship if (this.debug > 1) API3.warn(">>> unit unloaded on a wrong region ! try to garrison it again <<<"); let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); if (ship && !this.canceled) { shipsToMove[ship.id()] = ship; this.recovered.push( {"entId": ent.id(), "entPos": ent.position(), "shipId": ship.id()} ); ent.garrison(ship); ent.setMetadata(PlayerID, "onBoard", "onBoard"); } else { if (this.debug > 1) API3.warn("no way ... we destroy it"); this.resetUnit(gameState, ent); ent.destroy(); } } else { ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); } } for (let shipId in shipsToMove) { this.boardingPos[shipId] = this.getBoardingPos(gameState, shipsToMove[shipId], this.endIndex, this.sea, this.endPos, true); shipsToMove[shipId].move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); } this.unloaded = []; if (this.canceled) { for (let ship of this.ships.values()) { this.boardingPos[ship.id()] = undefined; this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); } this.canceled = undefined; } for (let ship of this.transportShips.values()) { if (ship.unitAIState() === "INDIVIDUAL.WALKING") continue; let shipId = ship.id(); let dist = API3.SquareVectorDistance(ship.position(), this.boardingPos[shipId]); let remaining = 0; for (let entId of ship.garrisoned()) { let ent = gameState.getEntityById(entId); if (!ent.getMetadata(PlayerID, "transport")) continue; remaining++; if (dist < 625) { ship.unload(entId); this.unloaded.push(entId); ent.setMetadata(PlayerID, "onBoard", shipId); } } let recovering = 0; for (let recov of this.recovered) if (recov.shipId === shipId) recovering++; if (!remaining && !recovering) // when empty, release the ship and move apart to leave room for other ships. TODO fight { ship.moveApart(this.boardingPos[shipId], 15); ship.setMetadata(PlayerID, "transporter", undefined); if (ship.getMetadata(PlayerID, "role") === "switchToTrader") ship.setMetadata(PlayerID, "role", "trader"); continue; } if (dist > this.boardingRange) { if (!this.nTry[shipId]) this.nTry[shipId] = 1; else ++this.nTry[shipId]; if (this.nTry[shipId] > 2) // we must have been blocked by something ... try with another boarding point { this.nTry[shipId] = 0; if (this.debug > 1) API3.warn(shipId + " new attempt for a landing point "); this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, undefined, true); } ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); } } }; m.TransportPlan.prototype.resetUnit = function(gameState, ent) { ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); // if from an army or attack, remove it if (ent.getMetadata(PlayerID, "plan") >= 0) { let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); if (attackPlan) attackPlan.removeUnit(ent, true); } if (ent.getMetadata(PlayerID, "PartOfArmy")) { let army = gameState.ai.HQ.defenseManager.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")); if (army) army.removeOwn(gameState, ent.id()); } }; m.TransportPlan.prototype.Serialize = function() { return { "ID": this.ID, "flotilla": this.flotilla, "endPos": this.endPos, "endIndex": this.endIndex, "startIndex": this.startIndex, "sea": this.sea, "state": this.state, "boardingPos": this.boardingPos, "needTransportShips": this.needTransportShips, "nTry": this.nTry, "canceled": this.canceled, "unloaded": this.unloaded, "recovered": this.recovered }; }; m.TransportPlan.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; this.failed = false; }; return m; }(PETRA);