From 8b1e78ae1ef482e26447faa55bdddce5aeab0b44 Mon Sep 17 00:00:00 2001 From: phosit Date: Mon, 13 Jul 2026 19:47:21 +0200 Subject: [PATCH] Use JS classes for the AI Fixes: #6285 --- .../public/simulation/ai/common-api/baseAI.js | 113 +- .../ai/common-api/entitycollection.js | 675 +-- .../simulation/ai/common-api/gamestate.js | 1834 +++---- .../simulation/ai/common-api/resources.js | 113 +- .../public/simulation/ai/common-api/shared.js | 805 +-- .../simulation/ai/common-api/technology.js | 235 +- .../public/simulation/ai/petra/_petrabot.js | 308 +- .../simulation/ai/petra/attackManager.js | 1481 +++--- .../public/simulation/ai/petra/attackPlan.js | 4137 +++++++-------- .../public/simulation/ai/petra/baseManager.js | 2208 ++++---- .../simulation/ai/petra/basesManager.js | 1456 +++--- .../simulation/ai/petra/buildManager.js | 299 +- .../mods/public/simulation/ai/petra/config.js | 329 +- .../public/simulation/ai/petra/defenseArmy.js | 1251 ++--- .../simulation/ai/petra/defenseManager.js | 1781 +++---- .../simulation/ai/petra/diplomacyManager.js | 1017 ++-- .../simulation/ai/petra/emergencyManager.js | 182 +- .../simulation/ai/petra/garrisonManager.js | 656 +-- .../simulation/ai/petra/headquarters.js | 4555 +++++++++-------- .../simulation/ai/petra/navalManager.js | 1619 +++--- .../mods/public/simulation/ai/petra/queue.js | 310 +- .../simulation/ai/petra/queueManager.js | 1083 ++-- .../simulation/ai/petra/researchManager.js | 477 +- .../simulation/ai/petra/tradeManager.js | 1273 ++--- .../simulation/ai/petra/transportPlan.js | 1321 ++--- .../simulation/ai/petra/victoryManager.js | 1339 ++--- .../mods/public/simulation/ai/petra/worker.js | 1973 +++---- 27 files changed, 16452 insertions(+), 16378 deletions(-) diff --git a/binaries/data/mods/public/simulation/ai/common-api/baseAI.js b/binaries/data/mods/public/simulation/ai/common-api/baseAI.js index fd149f08cf..ce36a46b3d 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/baseAI.js +++ b/binaries/data/mods/public/simulation/ai/common-api/baseAI.js @@ -1,60 +1,63 @@ globalThis.PlayerID = -1; -export function BaseAI(settings) +export class BaseAI { - if (!settings) - return; + constructor(settings) + { + if (!settings) + return; - this.player = settings.player; + this.player = settings.player; + } + + /** Return a simple object (using no classes etc) that will be serialized into saved games */ + Serialize() + { + return {}; + } + + /** + * Called after the constructor when loading a saved game, with 'data' being + * whatever Serialize() returned + */ + Deserialize(data, sharedScript) + { + } + + Init(playerID, sharedAI) + { + PlayerID = playerID; + + this.territoryMap = sharedAI.territoryMap; + this.accessibility = sharedAI.accessibility; + + this.gameState = sharedAI.gameState[this.player]; + this.gameState.ai = this; + + this.timeElapsed = sharedAI.timeElapsed; + + this.CustomInit(this.gameState); + } + + /** AIs override this function */ + CustomInit() + { + } + + HandleMessage(state, playerID, sharedAI) + { + PlayerID = playerID; + this.territoryMap = sharedAI.territoryMap; + this.OnUpdate(sharedAI); + } + + /** AIs override this function */ + OnUpdate() + { + } + + chat(message) + { + Engine.PostCommand(PlayerID, { "type": "aichat", "message": message }); + } } - -/** Return a simple object (using no classes etc) that will be serialized into saved games */ -BaseAI.prototype.Serialize = function() -{ - return {}; -}; - -/** - * Called after the constructor when loading a saved game, with 'data' being - * whatever Serialize() returned - */ -BaseAI.prototype.Deserialize = function(data, sharedScript) -{ -}; - -BaseAI.prototype.Init = function(playerID, sharedAI) -{ - PlayerID = playerID; - - this.territoryMap = sharedAI.territoryMap; - this.accessibility = sharedAI.accessibility; - - this.gameState = sharedAI.gameState[this.player]; - this.gameState.ai = this; - - this.timeElapsed = sharedAI.timeElapsed; - - this.CustomInit(this.gameState); -}; - -/** AIs override this function */ -BaseAI.prototype.CustomInit = function() -{ -}; - -BaseAI.prototype.HandleMessage = function(state, playerID, sharedAI) -{ - PlayerID = playerID; - this.territoryMap = sharedAI.territoryMap; - this.OnUpdate(sharedAI); -}; - -/** AIs override this function */ -BaseAI.prototype.OnUpdate = function() -{ -}; - -BaseAI.prototype.chat = function(message) -{ - Engine.PostCommand(PlayerID, { "type": "aichat", "message": message }); -}; diff --git a/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js b/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js index cd14c55d35..60b75da0c7 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js +++ b/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js @@ -1,368 +1,371 @@ import { SquareVectorDistance } from "simulation/ai/common-api/utils.js"; -export function EntityCollection(sharedAI, entities = new Map(), filters = []) +export class EntityCollection { - this._ai = sharedAI; - this._entities = entities; - this._filters = filters; - this.dynamicProp = []; - for (const filter of this._filters) - if (filter.dynamicProperties.length) - this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties); - - Object.defineProperty(this, "length", { "get": () => this._entities.size }); - this.frozen = false; -} - -EntityCollection.prototype.Serialize = function() -{ - const filters = []; - for (const f of this._filters) - filters.push(uneval(f)); - return { - "ents": this.toIdArray(), - "frozen": this.frozen, - "filters": filters - }; -}; - -EntityCollection.prototype.Deserialize = function(data, sharedAI) -{ - this._ai = sharedAI; - for (const id of data.ents) - this._entities.set(id, sharedAI._entities.get(id)); - - for (const f of data.filters) - this._filters.push(eval(f)); - - if (data.frozen) - this.freeze(); - else - this.defreeze(); -}; - -/** - * If an entitycollection is frozen, it will never automatically add a unit. - * But can remove one. - * this makes it easy to create entity collection that will auto-remove dead units - * but never add new ones. - */ -EntityCollection.prototype.freeze = function() -{ - this.frozen = true; -}; - -EntityCollection.prototype.defreeze = function() -{ - this.frozen = false; -}; - -EntityCollection.prototype.toIdArray = function() -{ - return Array.from(this._entities.keys()); -}; - -EntityCollection.prototype.toEntityArray = function() -{ - return Array.from(this._entities.values()); -}; - -EntityCollection.prototype.values = function() -{ - return this._entities.values(); -}; - -EntityCollection.prototype.toString = function() -{ - return "[EntityCollection " + this.toEntityArray().join(" ") + "]"; -}; - -EntityCollection.prototype.filter = function(filter, thisp) -{ - if (typeof filter === "function") - filter = { "func": filter, "dynamicProperties": [] }; - - const ret = new Map(); - for (const [id, ent] of this._entities) - if (filter.func.call(thisp, ent, id, this)) - ret.set(id, ent); - - return new EntityCollection(this._ai, ret, this._filters.concat([filter])); -}; - -/** - * Returns the (at most) n entities nearest to targetPos. - */ -EntityCollection.prototype.filterNearest = function(targetPos, n) -{ - // Compute the distance of each entity - const data = []; // [ [id, ent, distance], ... ] - for (const [id, ent] of this._entities) - if (ent.position()) - data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]); - - // Sort by increasing distance - data.sort((a, b) => a[2] - b[2]); - - if (n === undefined) - n = data.length; - else - n = Math.min(n, data.length); - - // Extract the first n - const ret = new Map(); - for (let i = 0; i < n; ++i) - ret.set(data[i][0], data[i][1]); - - return new EntityCollection(this._ai, ret); -}; - -EntityCollection.prototype.filter_raw = function(callback, thisp) -{ - const ret = new Map(); - for (const [id, ent] of this._entities) + constructor(sharedAI, entities = new Map(), filters = []) { - const val = ent._entity; - if (callback.call(thisp, val, id, this)) - ret.set(id, ent); + this._ai = sharedAI; + this._entities = entities; + this._filters = filters; + this.dynamicProp = []; + for (const filter of this._filters) + if (filter.dynamicProperties.length) + this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties); + + Object.defineProperty(this, "length", { "get": () => this._entities.size }); + this.frozen = false; } - return new EntityCollection(this._ai, ret); -}; -EntityCollection.prototype.forEach = function(callback) -{ - for (const ent of this._entities.values()) - callback(ent); - return this; -}; + Serialize() + { + const filters = []; + for (const f of this._filters) + filters.push(uneval(f)); + return { + "ents": this.toIdArray(), + "frozen": this.frozen, + "filters": filters + }; + } -EntityCollection.prototype.hasEntities = function() -{ - return this._entities.size !== 0; -}; + Deserialize(data, sharedAI) + { + this._ai = sharedAI; + for (const id of data.ents) + this._entities.set(id, sharedAI._entities.get(id)); -EntityCollection.prototype.move = function(x, z, queued = false, pushFront = false) -{ - Engine.PostCommand(PlayerID, { - "type": "walk", - "entities": this.toIdArray(), - "x": x, - "z": z, - "queued": queued, - "pushFront": pushFront - }); - return this; -}; + for (const f of data.filters) + this._filters.push(eval(f)); -EntityCollection.prototype.moveToRange = function(x, z, min, max, queued = false, pushFront = false) -{ - Engine.PostCommand(PlayerID, { - "type": "walk-to-range", - "entities": this.toIdArray(), - "x": x, - "z": z, - "min": min, - "max": max, - "queued": queued, - "pushFront": pushFront - }); - return this; -}; + if (data.frozen) + this.freeze(); + else + this.defreeze(); + } -EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) -{ - Engine.PostCommand(PlayerID, { - "type": "attack-walk", - "entities": this.toIdArray(), - "x": x, - "z": z, - "targetClasses": targetClasses, - "allowCapture": allowCapture, - "queued": queued, - "pushFront": pushFront - }); - return this; -}; + /** + * If an entitycollection is frozen, it will never automatically add a unit. + * But can remove one. + * this makes it easy to create entity collection that will auto-remove dead units + * but never add new ones. + */ + freeze() + { + this.frozen = true; + } -EntityCollection.prototype.moveIndiv = function(x, z, queued = false, pushFront = false) -{ - for (const id of this._entities.keys()) + defreeze() + { + this.frozen = false; + } + + toIdArray() + { + return Array.from(this._entities.keys()); + } + + toEntityArray() + { + return Array.from(this._entities.values()); + } + + values() + { + return this._entities.values(); + } + + toString() + { + return "[EntityCollection " + this.toEntityArray().join(" ") + "]"; + } + + filter(filter, thisp) + { + if (typeof filter === "function") + filter = { "func": filter, "dynamicProperties": [] }; + + const ret = new Map(); + for (const [id, ent] of this._entities) + if (filter.func.call(thisp, ent, id, this)) + ret.set(id, ent); + + return new EntityCollection(this._ai, ret, this._filters.concat([filter])); + } + + /** + * Returns the (at most) n entities nearest to targetPos. + */ + filterNearest(targetPos, n) + { + // Compute the distance of each entity + const data = []; // [ [id, ent, distance], ... ] + for (const [id, ent] of this._entities) + if (ent.position()) + data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]); + + // Sort by increasing distance + data.sort((a, b) => a[2] - b[2]); + + if (n === undefined) + n = data.length; + else + n = Math.min(n, data.length); + + // Extract the first n + const ret = new Map(); + for (let i = 0; i < n; ++i) + ret.set(data[i][0], data[i][1]); + + return new EntityCollection(this._ai, ret); + } + + filter_raw(callback, thisp) + { + const ret = new Map(); + for (const [id, ent] of this._entities) + { + const val = ent._entity; + if (callback.call(thisp, val, id, this)) + ret.set(id, ent); + } + return new EntityCollection(this._ai, ret); + } + + forEach(callback) + { + for (const ent of this._entities.values()) + callback(ent); + return this; + } + + hasEntities() + { + return this._entities.size !== 0; + } + + move(x, z, queued = false, pushFront = false) + { Engine.PostCommand(PlayerID, { "type": "walk", - "entities": [id], + "entities": this.toIdArray(), "x": x, "z": z, "queued": queued, "pushFront": pushFront }); - return this; -}; - -EntityCollection.prototype.garrison = function(target, queued = false, pushFront = false) -{ - Engine.PostCommand(PlayerID, { - "type": "garrison", - "entities": this.toIdArray(), - "target": target.id(), - "queued": queued, - "pushFront": pushFront - }); - return this; -}; - -EntityCollection.prototype.occupyTurret = function(target, queued = false, pushFront = false) -{ - Engine.PostCommand(PlayerID, { - "type": "occupy-turret", - "entities": this.toIdArray(), - "target": target.id(), - "queued": queued, - "pushFront": pushFront - }); - return this; -}; - -EntityCollection.prototype.destroy = function() -{ - Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() }); - return this; -}; - -EntityCollection.prototype.attack = function(unitId, queued = false, pushFront = false) -{ - Engine.PostCommand(PlayerID, { - "type": "attack", - "entities": this.toIdArray(), - "target": unitId, - "queued": queued, - "pushFront": pushFront - }); - return this; -}; - -/** violent, aggressive, defensive, passive, standground */ -EntityCollection.prototype.setStance = function(stance) -{ - Engine.PostCommand(PlayerID, { - "type": "stance", - "entities": this.toIdArray(), - "name": stance - }); - return this; -}; - -/** Returns the average position of all units */ -EntityCollection.prototype.getCentrePosition = function() -{ - const sumPos = [0, 0]; - let count = 0; - for (const ent of this._entities.values()) - { - if (!ent.position()) - continue; - sumPos[0] += ent.position()[0]; - sumPos[1] += ent.position()[1]; - count++; + return this; } - return count ? [sumPos[0]/count, sumPos[1]/count] : undefined; -}; - -/** - * returns the average position from the sample first units. - * This might be faster for huge collections, but there's - * always a risk that it'll be unprecise. - */ -EntityCollection.prototype.getApproximatePosition = function(sample) -{ - const sumPos = [0, 0]; - let i = 0; - for (const ent of this._entities.values()) + moveToRange(x, z, min, max, queued = false, pushFront = false) { - if (!ent.position()) - continue; - sumPos[0] += ent.position()[0]; - sumPos[1] += ent.position()[1]; - i++; - if (i === sample) - break; + Engine.PostCommand(PlayerID, { + "type": "walk-to-range", + "entities": this.toIdArray(), + "x": x, + "z": z, + "min": min, + "max": max, + "queued": queued, + "pushFront": pushFront + }); + return this; } - return i ? [sumPos[0]/i, sumPos[1]/i] : undefined; -}; - -EntityCollection.prototype.hasEntId = function(id) -{ - return this._entities.has(id); -}; - -/** Removes an entity from the collection, returns true if the entity was a member, false otherwise */ -EntityCollection.prototype.removeEnt = function(ent) -{ - if (!this._entities.has(ent.id())) - return false; - this._entities.delete(ent.id()); - return true; -}; - -/** Adds an entity to the collection, returns true if the entity was not member, false otherwise */ -EntityCollection.prototype.addEnt = function(ent) -{ - if (this._entities.has(ent.id())) - return false; - this._entities.set(ent.id(), ent); - const temp = this.toEntityArray(); - temp.sort((a, b) => a.id() - b.id()); - this._entities.clear(); - for (const e of temp) - this._entities.set(e.id(), e); - return true; -}; - -/** - * Checks the entity against the filters, and adds or removes it appropriately, returns true if the - * entity collection was modified. - * Force can add a unit despite a freezing. - * If an entitycollection is frozen, it will never automatically add a unit. - * But can remove one. - */ -EntityCollection.prototype.updateEnt = function(ent, force) -{ - let passesFilters = true; - for (const filter of this._filters) - passesFilters = passesFilters && filter.func(ent); - - if (passesFilters) + attackMove(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) { - if (!force && this.frozen) + Engine.PostCommand(PlayerID, { + "type": "attack-walk", + "entities": this.toIdArray(), + "x": x, + "z": z, + "targetClasses": targetClasses, + "allowCapture": allowCapture, + "queued": queued, + "pushFront": pushFront + }); + return this; + } + + moveIndiv(x, z, queued = false, pushFront = false) + { + for (const id of this._entities.keys()) + Engine.PostCommand(PlayerID, { + "type": "walk", + "entities": [id], + "x": x, + "z": z, + "queued": queued, + "pushFront": pushFront + }); + return this; + } + + garrison(target, queued = false, pushFront = false) + { + Engine.PostCommand(PlayerID, { + "type": "garrison", + "entities": this.toIdArray(), + "target": target.id(), + "queued": queued, + "pushFront": pushFront + }); + return this; + } + + occupyTurret(target, queued = false, pushFront = false) + { + Engine.PostCommand(PlayerID, { + "type": "occupy-turret", + "entities": this.toIdArray(), + "target": target.id(), + "queued": queued, + "pushFront": pushFront + }); + return this; + } + + destroy() + { + Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() }); + return this; + } + + attack(unitId, queued = false, pushFront = false) + { + Engine.PostCommand(PlayerID, { + "type": "attack", + "entities": this.toIdArray(), + "target": unitId, + "queued": queued, + "pushFront": pushFront + }); + return this; + } + + /** violent, aggressive, defensive, passive, standground */ + setStance(stance) + { + Engine.PostCommand(PlayerID, { + "type": "stance", + "entities": this.toIdArray(), + "name": stance + }); + return this; + } + + /** Returns the average position of all units */ + getCentrePosition() + { + const sumPos = [0, 0]; + let count = 0; + for (const ent of this._entities.values()) + { + if (!ent.position()) + continue; + sumPos[0] += ent.position()[0]; + sumPos[1] += ent.position()[1]; + count++; + } + + return count ? [sumPos[0]/count, sumPos[1]/count] : undefined; + } + + /** + * returns the average position from the sample first units. + * This might be faster for huge collections, but there's + * always a risk that it'll be unprecise. + */ + getApproximatePosition(sample) + { + const sumPos = [0, 0]; + let i = 0; + for (const ent of this._entities.values()) + { + if (!ent.position()) + continue; + sumPos[0] += ent.position()[0]; + sumPos[1] += ent.position()[1]; + i++; + if (i === sample) + break; + } + + return i ? [sumPos[0]/i, sumPos[1]/i] : undefined; + } + + hasEntId(id) + { + return this._entities.has(id); + } + + /** Removes an entity from the collection, returns true if the entity was a member, false otherwise */ + removeEnt(ent) + { + if (!this._entities.has(ent.id())) return false; - return this.addEnt(ent); + this._entities.delete(ent.id()); + return true; } - return this.removeEnt(ent); -}; + /** Adds an entity to the collection, returns true if the entity was not member, false otherwise */ + addEnt(ent) + { + if (this._entities.has(ent.id())) + return false; + this._entities.set(ent.id(), ent); + const temp = this.toEntityArray(); + temp.sort((a, b) => a.id() - b.id()); + this._entities.clear(); + for (const e of temp) + this._entities.set(e.id(), e); + return true; + } -EntityCollection.prototype.registerUpdates = function() -{ - this._ai.registerUpdatingEntityCollection(this); -}; + /** + * Checks the entity against the filters, and adds or removes it appropriately, returns true if the + * entity collection was modified. + * Force can add a unit despite a freezing. + * If an entitycollection is frozen, it will never automatically add a unit. + * But can remove one. + */ + updateEnt(ent, force) + { + let passesFilters = true; + for (const filter of this._filters) + passesFilters = passesFilters && filter.func(ent); -EntityCollection.prototype.unregister = function() -{ - this._ai.removeUpdatingEntityCollection(this); -}; + if (passesFilters) + { + if (!force && this.frozen) + return false; + return this.addEnt(ent); + } -EntityCollection.prototype.dynamicProperties = function() -{ - return this.dynamicProp; -}; + return this.removeEnt(ent); + } -EntityCollection.prototype.setUID = function(id) -{ - this._UID = id; -}; + registerUpdates() + { + this._ai.registerUpdatingEntityCollection(this); + } -EntityCollection.prototype.getUID = function() -{ - return this._UID; -}; + unregister() + { + this._ai.removeUpdatingEntityCollection(this); + } + + dynamicProperties() + { + return this.dynamicProp; + } + + setUID(id) + { + this._UID = id; + } + + getUID() + { + return this._UID; + } +} diff --git a/binaries/data/mods/public/simulation/ai/common-api/gamestate.js b/binaries/data/mods/public/simulation/ai/common-api/gamestate.js index dad41f0d84..176833c64d 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/gamestate.js +++ b/binaries/data/mods/public/simulation/ai/common-api/gamestate.js @@ -7,967 +7,967 @@ import { Technology } from "simulation/ai/common-api/technology.js"; * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ -export function GameState() +export class GameState { - this.ai = null; // must be updated by the AIs. -} + ai = null; // must be updated by the AIs. -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.entities = SharedScript.entities; - this.player = player; - this.playerData = SharedScript.playersData[this.player]; - this.victoryConditions = SharedScript.victoryConditions; - this.alliedVictory = SharedScript.alliedVictory; - this.ceasefireActive = SharedScript.ceasefireActive; - this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; - - // get the list of possible phases for this civ: - // we assume all of them are researchable from the civil center - this.phases = []; - const cctemplate = this.getTemplate(this.applyCiv("structures/{civ}/civil_centre")); - if (!cctemplate) - return; - const civ = this.getPlayerCiv(); - const techs = cctemplate.researchableTechs(this, civ); - - const phaseData = {}; - const phaseMap = {}; - for (let techName of techs) + init(SharedScript, state, player) { - if (!techName.startsWith("phase")) - continue; - let techData = this.getTemplate(techName); + this.sharedScript = SharedScript; + this.EntCollecNames = SharedScript._entityCollectionsName; + this.timeElapsed = SharedScript.timeElapsed; + this.circularMap = SharedScript.circularMap; + this.templates = SharedScript._templates; + this.entities = SharedScript.entities; + this.player = player; + this.playerData = SharedScript.playersData[this.player]; + this.victoryConditions = SharedScript.victoryConditions; + this.alliedVictory = SharedScript.alliedVictory; + this.ceasefireActive = SharedScript.ceasefireActive; + this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; - if (techData._definesPair) + // get the list of possible phases for this civ: + // we assume all of them are researchable from the civil center + this.phases = []; + const cctemplate = this.getTemplate(this.applyCiv("structures/{civ}/civil_centre")); + if (!cctemplate) + return; + const civ = this.getPlayerCiv(); + const techs = cctemplate.researchableTechs(this, civ); + + const phaseData = {}; + const phaseMap = {}; + for (let techName of techs) { - // Cannot call pickrandom because this function is called on rejoin and that causes oos. - // (reverting rP20750) - techName = this.playerData.disabledTechnologies[techData._template.pair[0]] ? - techData._template.pair[1] : techData._template.pair[0]; + if (!techName.startsWith("phase")) + continue; + let techData = this.getTemplate(techName); - const supersedes = techData._template.supersedes; - techData = clone(this.getTemplate(techName)); - if (supersedes) - techData._template.supersedes = supersedes; - } - - phaseData[techName] = GetTechnologyBasicDataHelper(techData._template, civ); - if (phaseData[techName].replaces) - phaseMap[phaseData[techName].replaces[0]] = techName; - } - - this.phases = UnravelPhases(phaseData).map(phaseName => ({ - "name": phaseMap[phaseName] || phaseName, - "requirements": phaseMap[phaseName] ? phaseData[phaseMap[phaseName]].reqs : [] - })); -}; - -GameState.prototype.update = function(SharedScript) -{ - this.timeElapsed = SharedScript.timeElapsed; - this.playerData = SharedScript.playersData[this.player]; - this.ceasefireActive = SharedScript.ceasefireActive; - this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; -}; - -GameState.prototype.updatingCollection = function(id, filter, parentCollection) -{ - const gid = "player-" + this.player + "-" + id; // automatically add the player ID - return this.updatingGlobalCollection(gid, filter, parentCollection); -}; - -GameState.prototype.destroyCollection = function(id) -{ - const gid = "player-" + this.player + "-" + id; // automatically add the player ID - this.destroyGlobalCollection(gid); -}; - -GameState.prototype.updatingGlobalCollection = function(gid, filter, parentCollection) -{ - if (this.EntCollecNames.has(gid)) - return this.EntCollecNames.get(gid); - - const collection = parentCollection ? parentCollection.filter(filter) : this.entities.filter(filter); - collection.registerUpdates(); - this.EntCollecNames.set(gid, collection); - return collection; -}; - -GameState.prototype.destroyGlobalCollection = function(gid) -{ - if (!this.EntCollecNames.has(gid)) - return; - - this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(gid)); - this.EntCollecNames.delete(gid); -}; - -/** - * Reset the entities collections which depend on diplomacy - */ -GameState.prototype.resetOnDiplomacyChanged = function() -{ - for (const name of this.EntCollecNames.keys()) - if (name.startsWith("player-" + this.player + "-diplo")) - this.destroyGlobalCollection(name); -}; - -GameState.prototype.getTimeElapsed = function() -{ - return this.timeElapsed; -}; - -GameState.prototype.getBarterPrices = function() -{ - return this.playerData.barterPrices; -}; - -GameState.prototype.getVictoryConditions = function() -{ - return this.victoryConditions; -}; - -GameState.prototype.getAlliedVictory = function() -{ - return this.alliedVictory; -}; - -GameState.prototype.isCeasefireActive = function() -{ - return this.ceasefireActive; -}; - -GameState.prototype.getTemplate = function(type) -{ - if (TechnologyTemplates.Has(type)) - return new Technology(type); - - if (this.templates[type] === undefined) - this.sharedScript.GetTemplate(type); - - return this.templates[type] ? new Template(this.sharedScript, type, this.templates[type]) : null; -}; - -/** Return the template of the structure built from this foundation */ -GameState.prototype.getBuiltTemplate = function(foundationName) -{ - if (!foundationName.startsWith("foundation|")) - { - warn("Foundation " + foundationName + " not recognized as a foundation."); - return null; - } - return this.getTemplate(foundationName.substr(11)); -}; - -GameState.prototype.applyCiv = function(str) -{ - return str.replace(/\{civ\}/g, this.playerData.civ); -}; - -GameState.prototype.getPlayerCiv = function(player) -{ - return player !== undefined ? this.sharedScript.playersData[player].civ : this.playerData.civ; -}; - -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; -}; - -GameState.prototype.getNumberOfPhases = function() -{ - return this.phases.length; -}; - -GameState.prototype.getPhaseName = function(i) -{ - return this.phases[i-1] ? this.phases[i-1].name : undefined; -}; - -GameState.prototype.getPhaseEntityRequirements = function(i) -{ - const entityReqs = []; - - for (const requirement of this.phases[i-1].requirements) - { - if (!requirement.entities) - continue; - for (const entity of requirement.entities) - if (entity.check == "count") - entityReqs.push({ - "class": entity.class, - "count": entity.number - }); - } - - return entityReqs; -}; - -GameState.prototype.isResearched = function(template) -{ - return this.playerData.researchedTechs.has(template); -}; - -GameState.prototype.isResearching = function(template) -{ - return this.playerData.researchQueued.has(template); -}; - -/** this is an "in-absolute" check that doesn't check if we have a building to research from. */ -GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) -{ - if (this.playerData.disabledTechnologies[techTemplateName]) - return false; - - const template = this.getTemplate(techTemplateName); - if (!template) - return false; - - if (this.playerData.researchQueued.has(techTemplateName) || - this.playerData.researchedTechs.has(techTemplateName)) - return false; - - if (noRequirementCheck) - return true; - - // if this is a pair, we must check that the pair tech is not being researched - if (template.pair()) - { - const other = template.pairedWith(); - if (this.playerData.researchQueued.has(other) || - this.playerData.researchedTechs.has(other)) - return false; - } - - return this.checkTechRequirements(template.requirements(this.playerData.civ)); -}; - -/** - * Private function for checking a set of requirements is met. - * Basically copies TechnologyManager, but compares against - * variables only available within the AI - */ -GameState.prototype.checkTechRequirements = function(reqs) -{ - if (!reqs) - return false; - - if (!reqs.length) - return true; - - const doesEntitySpecPass = entity => - { - switch (entity.check) - { - case "count": - return this.playerData.classCounts[entity.class] && - this.playerData.classCounts[entity.class] >= entity.number; - - case "variants": - return this.playerData.typeCountsByClass[entity.class] && - Object.keys(this.playerData.typeCountsByClass[entity.class]).length >= entity.number; - - default: - return true; - } - }; - - return reqs.some(req => - { - return Object.keys(req).every(type => - { - switch (type) + if (techData._definesPair) { - case "techs": - return req[type].every(tech => this.playerData.researchedTechs.has(tech)); + // Cannot call pickrandom because this function is called on rejoin and that causes oos. + // (reverting rP20750) + techName = this.playerData.disabledTechnologies[techData._template.pair[0]] ? + techData._template.pair[1] : techData._template.pair[0]; - case "entities": - return req[type].every(doesEntitySpecPass); - default: - return false; + const supersedes = techData._template.supersedes; + techData = clone(this.getTemplate(techName)); + if (supersedes) + techData._template.supersedes = supersedes; } + + phaseData[techName] = GetTechnologyBasicDataHelper(techData._template, civ); + if (phaseData[techName].replaces) + phaseMap[phaseData[techName].replaces[0]] = techName; + } + + this.phases = UnravelPhases(phaseData).map(phaseName => ({ + "name": phaseMap[phaseName] || phaseName, + "requirements": phaseMap[phaseName] ? phaseData[phaseMap[phaseName]].reqs : [] + })); + } + + update(SharedScript) + { + this.timeElapsed = SharedScript.timeElapsed; + this.playerData = SharedScript.playersData[this.player]; + this.ceasefireActive = SharedScript.ceasefireActive; + this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; + } + + updatingCollection(id, filter, parentCollection) + { + const gid = "player-" + this.player + "-" + id; // automatically add the player ID + return this.updatingGlobalCollection(gid, filter, parentCollection); + } + + destroyCollection(id) + { + const gid = "player-" + this.player + "-" + id; // automatically add the player ID + this.destroyGlobalCollection(gid); + } + + updatingGlobalCollection(gid, filter, parentCollection) + { + if (this.EntCollecNames.has(gid)) + return this.EntCollecNames.get(gid); + + const collection = parentCollection ? parentCollection.filter(filter) : this.entities.filter(filter); + collection.registerUpdates(); + this.EntCollecNames.set(gid, collection); + return collection; + } + + destroyGlobalCollection(gid) + { + if (!this.EntCollecNames.has(gid)) + return; + + this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(gid)); + this.EntCollecNames.delete(gid); + } + + /** + * Reset the entities collections which depend on diplomacy + */ + resetOnDiplomacyChanged() + { + for (const name of this.EntCollecNames.keys()) + if (name.startsWith("player-" + this.player + "-diplo")) + this.destroyGlobalCollection(name); + } + + getTimeElapsed() + { + return this.timeElapsed; + } + + getBarterPrices() + { + return this.playerData.barterPrices; + } + + getVictoryConditions() + { + return this.victoryConditions; + } + + getAlliedVictory() + { + return this.alliedVictory; + } + + isCeasefireActive() + { + return this.ceasefireActive; + } + + getTemplate(type) + { + if (TechnologyTemplates.Has(type)) + return new Technology(type); + + if (this.templates[type] === undefined) + this.sharedScript.GetTemplate(type); + + return this.templates[type] ? new Template(this.sharedScript, type, this.templates[type]) : null; + } + + /** Return the template of the structure built from this foundation */ + getBuiltTemplate(foundationName) + { + if (!foundationName.startsWith("foundation|")) + { + warn("Foundation " + foundationName + " not recognized as a foundation."); + return null; + } + return this.getTemplate(foundationName.substr(11)); + } + + applyCiv(str) + { + return str.replace(/\{civ\}/g, this.playerData.civ); + } + + getPlayerCiv(player) + { + return player !== undefined ? this.sharedScript.playersData[player].civ : this.playerData.civ; + } + + currentPhase() + { + for (let i = this.phases.length; i > 0; --i) + if (this.isResearched(this.phases[i-1].name)) + return i; + return 0; + } + + getNumberOfPhases() + { + return this.phases.length; + } + + getPhaseName(i) + { + return this.phases[i-1] ? this.phases[i-1].name : undefined; + } + + getPhaseEntityRequirements(i) + { + const entityReqs = []; + + for (const requirement of this.phases[i-1].requirements) + { + if (!requirement.entities) + continue; + for (const entity of requirement.entities) + if (entity.check == "count") + entityReqs.push({ + "class": entity.class, + "count": entity.number + }); + } + + return entityReqs; + } + + isResearched(template) + { + return this.playerData.researchedTechs.has(template); + } + + isResearching(template) + { + return this.playerData.researchQueued.has(template); + } + + /** this is an "in-absolute" check that doesn't check if we have a building to research from. */ + canResearch(techTemplateName, noRequirementCheck) + { + if (this.playerData.disabledTechnologies[techTemplateName]) + return false; + + const template = this.getTemplate(techTemplateName); + if (!template) + return false; + + if (this.playerData.researchQueued.has(techTemplateName) || + this.playerData.researchedTechs.has(techTemplateName)) + return false; + + if (noRequirementCheck) + return true; + + // if this is a pair, we must check that the pair tech is not being researched + if (template.pair()) + { + const other = template.pairedWith(); + if (this.playerData.researchQueued.has(other) || + this.playerData.researchedTechs.has(other)) + return false; + } + + return this.checkTechRequirements(template.requirements(this.playerData.civ)); + } + + /** + * Private function for checking a set of requirements is met. + * Basically copies TechnologyManager, but compares against + * variables only available within the AI + */ + checkTechRequirements(reqs) + { + if (!reqs) + return false; + + if (!reqs.length) + return true; + + const doesEntitySpecPass = entity => + { + switch (entity.check) + { + case "count": + return this.playerData.classCounts[entity.class] && + this.playerData.classCounts[entity.class] >= entity.number; + + case "variants": + return this.playerData.typeCountsByClass[entity.class] && + Object.keys(this.playerData.typeCountsByClass[entity.class]).length >= entity.number; + + default: + return true; + } + }; + + return reqs.some(req => + { + return Object.keys(req).every(type => + { + switch (type) + { + case "techs": + return req[type].every(tech => this.playerData.researchedTechs.has(tech)); + + case "entities": + return req[type].every(doesEntitySpecPass); + default: + return false; + } + }); }); - }); -}; - -GameState.prototype.getPassabilityMap = function() -{ - return this.sharedScript.passabilityMap; -}; - -GameState.prototype.getPassabilityClassMask = function(name) -{ - if (!this.sharedScript.passabilityClasses[name]) - error("Tried to use invalid passability class name '" + name + "'"); - return this.sharedScript.passabilityClasses[name]; -}; - -GameState.prototype.getResources = function() -{ - return new ResourcesManager(this.playerData.resourceCounts); -}; - -GameState.prototype.getPopulation = function() -{ - return this.playerData.popCount; -}; - -GameState.prototype.getPopulationLimit = function() -{ - return this.playerData.popLimit; -}; - -GameState.prototype.getPopulationMax = function() -{ - return this.playerData.popMax; -}; - -GameState.prototype.getPlayerID = function() -{ - return this.player; -}; - -GameState.prototype.hasAllies = function() -{ - for (const i in this.playerData.isAlly) - if (this.playerData.isAlly[i] && +i !== this.player && - this.sharedScript.playersData[i].state !== "defeated") - return true; - return false; -}; - -GameState.prototype.hasEnemies = function() -{ - for (const i in this.playerData.isEnemy) - if (this.playerData.isEnemy[i] && +i !== 0 && - this.sharedScript.playersData[i].state !== "defeated") - return true; - return false; -}; - -GameState.prototype.hasNeutrals = function() -{ - for (const i in this.playerData.isNeutral) - if (this.playerData.isNeutral[i] && - this.sharedScript.playersData[i].state !== "defeated") - return true; - return false; -}; - -GameState.prototype.isPlayerNeutral = function(id) -{ - return this.playerData.isNeutral[id]; -}; - -GameState.prototype.isPlayerAlly = function(id) -{ - return this.playerData.isAlly[id]; -}; - -GameState.prototype.isPlayerMutualAlly = function(id) -{ - return this.playerData.isMutualAlly[id]; -}; - -GameState.prototype.isPlayerEnemy = function(id) -{ - return this.playerData.isEnemy[id]; -}; - -/** Return the number of players currently enemies, not including gaia */ -GameState.prototype.getNumPlayerEnemies = function() -{ - let num = 0; - for (let i = 1; i < this.playerData.isEnemy.length; ++i) - if (this.playerData.isEnemy[i] && - this.sharedScript.playersData[i].state != "defeated") - ++num; - return num; -}; - -GameState.prototype.getEnemies = function() -{ - const ret = []; - for (const i in this.playerData.isEnemy) - if (this.playerData.isEnemy[i]) - ret.push(+i); - return ret; -}; - -GameState.prototype.getNeutrals = function() -{ - const ret = []; - for (const i in this.playerData.isNeutral) - if (this.playerData.isNeutral[i]) - ret.push(+i); - return ret; -}; - -GameState.prototype.getAllies = function() -{ - const ret = []; - for (const i in this.playerData.isAlly) - if (this.playerData.isAlly[i]) - ret.push(+i); - return ret; -}; - -GameState.prototype.getExclusiveAllies = function() -{ // Player is not included - const ret = []; - for (const i in this.playerData.isAlly) - if (this.playerData.isAlly[i] && +i !== this.player) - ret.push(+i); - return ret; -}; - -GameState.prototype.getMutualAllies = function() -{ - const ret = []; - for (const i in this.playerData.isMutualAlly) - if (this.playerData.isMutualAlly[i] && - this.sharedScript.playersData[i].isMutualAlly[this.player]) - ret.push(+i); - return ret; -}; - -GameState.prototype.isEntityAlly = function(ent) -{ - if (!ent) - return false; - return this.playerData.isAlly[ent.owner()]; -}; - -GameState.prototype.isEntityExclusiveAlly = function(ent) -{ - if (!ent) - return false; - return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; -}; - -GameState.prototype.isEntityEnemy = function(ent) -{ - if (!ent) - return false; - return this.playerData.isEnemy[ent.owner()]; -}; - -GameState.prototype.isEntityOwn = function(ent) -{ - if (!ent) - return false; - return ent.owner() === this.player; -}; - -GameState.prototype.getEntityById = function(id) -{ - return this.entities._entities.get(+id); -}; - -GameState.prototype.getEntities = function(id) -{ - if (id === undefined) - return this.entities; - - return this.updatingGlobalCollection("player-" + id + "-entities", filters.byOwner(id)); -}; - -GameState.prototype.getStructures = function() -{ - return this.updatingGlobalCollection("structures", filters.byClass("Structure"), this.entities); -}; - -GameState.prototype.getOwnEntities = function() -{ - return this.updatingGlobalCollection("player-" + this.player + "-entities", - filters.byOwner(this.player)); -}; - -GameState.prototype.getOwnStructures = function() -{ - return this.updatingGlobalCollection("player-" + this.player + "-structures", - filters.byClass("Structure"), this.getOwnEntities()); -}; - -GameState.prototype.getOwnUnits = function() -{ - return this.updatingGlobalCollection("player-" + this.player + "-units", filters.byClass("Unit"), - this.getOwnEntities()); -}; - -GameState.prototype.getAllyEntities = function() -{ - return this.entities.filter(filters.byOwners(this.getAllies())); -}; - -GameState.prototype.getExclusiveAllyEntities = function() -{ - return this.entities.filter(filters.byOwners(this.getExclusiveAllies())); -}; - -GameState.prototype.getAllyStructures = function(allyID) -{ - if (allyID == undefined) - { - return this.updatingCollection("diplo-ally-structures", filters.byOwners(this.getAllies()), - this.getStructures()); } - return this.updatingGlobalCollection("player-" + allyID + "-structures", filters.byOwner(allyID), - this.getStructures()); -}; - -GameState.prototype.getNeutralStructures = function() -{ - return this.getStructures().filter(filters.byOwners(this.getNeutrals())); -}; - -GameState.prototype.getEnemyEntities = function() -{ - return this.entities.filter(filters.byOwners(this.getEnemies())); -}; - -GameState.prototype.getEnemyStructures = function(enemyID) -{ - if (enemyID === undefined) + getPassabilityMap() { - return this.updatingCollection("diplo-enemy-structures", filters.byOwners(this.getEnemies()), - this.getStructures()); + return this.sharedScript.passabilityMap; } - return this.updatingGlobalCollection("player-" + enemyID + "-structures", filters.byOwner(enemyID), - this.getStructures()); -}; - -GameState.prototype.getEnemyUnits = function(enemyID) -{ - if (enemyID === undefined) - return this.getEnemyEntities().filter(filters.byClass("Unit")); - - return this.updatingGlobalCollection("player-" + enemyID + "-units", filters.byClass("Unit"), - this.getEntities(enemyID)); -}; - -/** if maintain is true, this will be stored. Otherwise it's one-shot. */ -GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain) -{ - if (maintain) + getPassabilityClassMask(name) { - return this.updatingCollection(key + "-" + value, filters.byMetadata(this.player, key, value), + if (!this.sharedScript.passabilityClasses[name]) + error("Tried to use invalid passability class name '" + name + "'"); + return this.sharedScript.passabilityClasses[name]; + } + + getResources() + { + return new ResourcesManager(this.playerData.resourceCounts); + } + + getPopulation() + { + return this.playerData.popCount; + } + + getPopulationLimit() + { + return this.playerData.popLimit; + } + + getPopulationMax() + { + return this.playerData.popMax; + } + + getPlayerID() + { + return this.player; + } + + hasAllies() + { + for (const i in this.playerData.isAlly) + if (this.playerData.isAlly[i] && +i !== this.player && + this.sharedScript.playersData[i].state !== "defeated") + return true; + return false; + } + + hasEnemies() + { + for (const i in this.playerData.isEnemy) + if (this.playerData.isEnemy[i] && +i !== 0 && + this.sharedScript.playersData[i].state !== "defeated") + return true; + return false; + } + + hasNeutrals() + { + for (const i in this.playerData.isNeutral) + if (this.playerData.isNeutral[i] && + this.sharedScript.playersData[i].state !== "defeated") + return true; + return false; + } + + isPlayerNeutral(id) + { + return this.playerData.isNeutral[id]; + } + + isPlayerAlly(id) + { + return this.playerData.isAlly[id]; + } + + isPlayerMutualAlly(id) + { + return this.playerData.isMutualAlly[id]; + } + + isPlayerEnemy(id) + { + return this.playerData.isEnemy[id]; + } + + /** Return the number of players currently enemies, not including gaia */ + getNumPlayerEnemies() + { + let num = 0; + for (let i = 1; i < this.playerData.isEnemy.length; ++i) + if (this.playerData.isEnemy[i] && + this.sharedScript.playersData[i].state != "defeated") + ++num; + return num; + } + + getEnemies() + { + const ret = []; + for (const i in this.playerData.isEnemy) + if (this.playerData.isEnemy[i]) + ret.push(+i); + return ret; + } + + getNeutrals() + { + const ret = []; + for (const i in this.playerData.isNeutral) + if (this.playerData.isNeutral[i]) + ret.push(+i); + return ret; + } + + getAllies() + { + const ret = []; + for (const i in this.playerData.isAlly) + if (this.playerData.isAlly[i]) + ret.push(+i); + return ret; + } + + getExclusiveAllies() + { // Player is not included + const ret = []; + for (const i in this.playerData.isAlly) + if (this.playerData.isAlly[i] && +i !== this.player) + ret.push(+i); + return ret; + } + + getMutualAllies() + { + const ret = []; + for (const i in this.playerData.isMutualAlly) + if (this.playerData.isMutualAlly[i] && + this.sharedScript.playersData[i].isMutualAlly[this.player]) + ret.push(+i); + return ret; + } + + isEntityAlly(ent) + { + if (!ent) + return false; + return this.playerData.isAlly[ent.owner()]; + } + + isEntityExclusiveAlly(ent) + { + if (!ent) + return false; + return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; + } + + isEntityEnemy(ent) + { + if (!ent) + return false; + return this.playerData.isEnemy[ent.owner()]; + } + + isEntityOwn(ent) + { + if (!ent) + return false; + return ent.owner() === this.player; + } + + getEntityById(id) + { + return this.entities._entities.get(+id); + } + + getEntities(id) + { + if (id === undefined) + return this.entities; + + return this.updatingGlobalCollection("player-" + id + "-entities", filters.byOwner(id)); + } + + getStructures() + { + return this.updatingGlobalCollection("structures", filters.byClass("Structure"), this.entities); + } + + getOwnEntities() + { + return this.updatingGlobalCollection("player-" + this.player + "-entities", + filters.byOwner(this.player)); + } + + getOwnStructures() + { + return this.updatingGlobalCollection("player-" + this.player + "-structures", + filters.byClass("Structure"), this.getOwnEntities()); + } + + getOwnUnits() + { + return this.updatingGlobalCollection("player-" + this.player + "-units", filters.byClass("Unit"), this.getOwnEntities()); } - return this.getOwnEntities().filter(filters.byMetadata(this.player, key, value)); -}; -GameState.prototype.getOwnEntitiesByRole = function(role, maintain) -{ - return this.getOwnEntitiesByMetadata("role", role, maintain); -}; - -GameState.prototype.getOwnEntitiesByType = function(type, maintain) -{ - const filter = filters.byType(type); - if (maintain) - return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); - return this.getOwnEntities().filter(filter); -}; - -GameState.prototype.getOwnEntitiesByClass = function(cls, maintain) -{ - const filter = filters.byClass(cls); - if (maintain) - return this.updatingCollection("class-" + cls, filter, this.getOwnEntities()); - return this.getOwnEntities().filter(filter); -}; - -GameState.prototype.getOwnFoundationsByClass = function(cls, maintain) -{ - const filter = filters.byClass(cls); - if (maintain) - return this.updatingCollection("foundations-class-" + cls, filter, this.getOwnFoundations()); - return this.getOwnFoundations().filter(filter); -}; - -GameState.prototype.getOwnTrainingFacilities = function() -{ - return this.updatingGlobalCollection("player-" + this.player + "-training-facilities", - filters.byTrainingQueue(), this.getOwnEntities()); -}; - -GameState.prototype.getOwnResearchFacilities = function() -{ - return this.updatingGlobalCollection("player-" + this.player + "-research-facilities", - filters.byResearchAvailable(this, this.playerData.civ), this.getOwnEntities()); -}; - - -GameState.prototype.countEntitiesByType = function(type, maintain) -{ - return this.getOwnEntitiesByType(type, maintain).length; -}; - -GameState.prototype.countEntitiesAndQueuedByType = function(type, maintain) -{ - const 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 + getAllyEntities() { + return this.entities.filter(filters.byOwners(this.getAllies())); + } + + getExclusiveAllyEntities() + { + return this.entities.filter(filters.byOwners(this.getExclusiveAllies())); + } + + getAllyStructures(allyID) + { + if (allyID == undefined) + { + return this.updatingCollection("diplo-ally-structures", filters.byOwners(this.getAllies()), + this.getStructures()); + } + + return this.updatingGlobalCollection("player-" + allyID + "-structures", filters.byOwner(allyID), + this.getStructures()); + } + + getNeutralStructures() + { + return this.getStructures().filter(filters.byOwners(this.getNeutrals())); + } + + getEnemyEntities() + { + return this.entities.filter(filters.byOwners(this.getEnemies())); + } + + getEnemyStructures(enemyID) + { + if (enemyID === undefined) + { + return this.updatingCollection("diplo-enemy-structures", filters.byOwners(this.getEnemies()), + this.getStructures()); + } + + return this.updatingGlobalCollection("player-" + enemyID + "-structures", filters.byOwner(enemyID), + this.getStructures()); + } + + getEnemyUnits(enemyID) + { + if (enemyID === undefined) + return this.getEnemyEntities().filter(filters.byClass("Unit")); + + return this.updatingGlobalCollection("player-" + enemyID + "-units", filters.byClass("Unit"), + this.getEntities(enemyID)); + } + + /** if maintain is true, this will be stored. Otherwise it's one-shot. */ + getOwnEntitiesByMetadata(key, value, maintain) + { + if (maintain) + { + return this.updatingCollection(key + "-" + value, filters.byMetadata(this.player, key, value), + this.getOwnEntities()); + } + return this.getOwnEntities().filter(filters.byMetadata(this.player, key, value)); + } + + getOwnEntitiesByRole(role, maintain) + { + return this.getOwnEntitiesByMetadata("role", role, maintain); + } + + getOwnEntitiesByType(type, maintain) + { + const filter = filters.byType(type); + if (maintain) + return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); + return this.getOwnEntities().filter(filter); + } + + getOwnEntitiesByClass(cls, maintain) + { + const filter = filters.byClass(cls); + if (maintain) + return this.updatingCollection("class-" + cls, filter, this.getOwnEntities()); + return this.getOwnEntities().filter(filter); + } + + getOwnFoundationsByClass(cls, maintain) + { + const filter = filters.byClass(cls); + if (maintain) + return this.updatingCollection("foundations-class-" + cls, filter, this.getOwnFoundations()); + return this.getOwnFoundations().filter(filter); + } + + getOwnTrainingFacilities() + { + return this.updatingGlobalCollection("player-" + this.player + "-training-facilities", + filters.byTrainingQueue(), this.getOwnEntities()); + } + + getOwnResearchFacilities() + { + return this.updatingGlobalCollection("player-" + this.player + "-research-facilities", + filters.byResearchAvailable(this, this.playerData.civ), this.getOwnEntities()); + } + + + countEntitiesByType(type, maintain) + { + return this.getOwnEntitiesByType(type, maintain).length; + } + + countEntitiesAndQueuedByType(type, maintain) + { + const 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 (const item of ent.trainingQueue()) + if (item.unitTemplate == type) + count += item.count; + }); + } + + return count; + } + + countFoundationsByType(type, maintain) + { + const foundationType = "foundation|" + type; + + if (maintain) + { + return this.updatingCollection("foundation-type-" + type, filters.byType(foundationType), + this.getOwnFoundations()).length; + } + + let count = 0; + this.getOwnStructures().forEach(function(ent) + { + if (ent.templateName() == foundationType) + ++count; + }); + return count; + } + + countOwnEntitiesByRole(role) + { + return this.getOwnEntitiesByRole(role, "true").length; + } + + countOwnEntitiesAndQueuedWithRole(role) + { + let count = this.countOwnEntitiesByRole(role); + // Count entities in building production queues - // TODO: maybe this fails for corrals. this.getOwnTrainingFacilities().forEach(function(ent) { for (const item of ent.trainingQueue()) - if (item.unitTemplate == type) + if (item.metadata && item.metadata.role && item.metadata.role == role) count += item.count; }); + return count; + } + + countOwnQueuedEntitiesWithMetadata(data, value) + { + // Count entities in building production queues + let count = 0; + this.getOwnTrainingFacilities().forEach(function(ent) + { + for (const item of ent.trainingQueue()) + if (item.metadata && item.metadata[data] && item.metadata[data] == value) + count += item.count; + }); + return count; + } + + getOwnFoundations() + { + return this.updatingGlobalCollection("player-" + this.player + "-foundations", + filters.isFoundation(), this.getOwnStructures()); + } + + getOwnDropsites(resource) + { + if (resource) + { + return this.updatingCollection("ownDropsite-" + resource, filters.isDropsite(resource), + this.getOwnEntities()); + } + return this.updatingCollection("ownDropsite-all", filters.isDropsite(), this.getOwnEntities()); + } + + getAnyDropsites(resource) + { + if (resource) + return this.updatingGlobalCollection("anyDropsite-" + resource, filters.isDropsite(resource), this.getEntities()); + return this.updatingGlobalCollection("anyDropsite-all", filters.isDropsite(), this.getEntities()); + } + + getResourceSupplies(resource) + { + return this.updatingGlobalCollection("resource-" + resource, filters.byResource(resource), + this.getEntities()); + } + + getHuntableSupplies() + { + return this.updatingGlobalCollection("resource-hunt", filters.isHuntable(), this.getEntities()); + } + + getFishableSupplies() + { + return this.updatingGlobalCollection("resource-fish", filters.isFishable(), this.getEntities()); + } + + /** This returns only units from buildings. */ + findTrainableUnits(classes, anticlasses) + { + const allTrainable = []; + const civ = this.playerData.civ; + this.getOwnTrainingFacilities().forEach(function(ent) + { + const trainable = ent.trainableEntities(civ); + if (!trainable) + return; + for (const unit of trainable) + if (allTrainable.indexOf(unit) === -1) + allTrainable.push(unit); + }); + const ret = []; + const limits = this.getEntityLimits(); + const current = this.getEntityCounts(); + const matchCounts = this.getEntityMatchCounts(); + for (const trainable of allTrainable) + { + if (this.isTemplateDisabled(trainable)) + continue; + const template = this.getTemplate(trainable); + if (!template || !template.available(this)) + continue; + const limit = template.matchLimit(); + if (matchCounts && limit && matchCounts[trainable] >= limit) + continue; + if (!template.hasClasses(classes) || template.hasClasses(anticlasses)) + continue; + const 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. + */ + findAvailableTech() + { + const allResearchable = []; + const civ = this.playerData.civ; + for (const ent of this.getOwnEntities().values()) + { + const searchable = ent.researchableTechs(this, civ); + if (!searchable) + continue; + for (const tech of searchable) + if (!this.playerData.disabledTechnologies[tech] && allResearchable.indexOf(tech) === -1) + allResearchable.push(tech); + } + + const ret = []; + for (const tech of allResearchable) + { + const template = this.getTemplate(tech); + if (template.pairDef()) + { + const 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)) + { + // Phases are treated separately + if (this.phases.every(phase => template._templateName != phase.name)) + ret.push([tech, template]); + } + } + return ret; + } + + /** + * Return true if we have a building able to train that template + */ + hasTrainer(template) + { + const civ = this.playerData.civ; + for (const ent of this.getOwnTrainingFacilities().values()) + { + const trainable = ent.trainableEntities(civ); + if (trainable && trainable.indexOf(template) !== -1) + return true; + } + return false; + } + + /** + * Find buildings able to train that template. + */ + findTrainers(template) + { + const civ = this.playerData.civ; + return this.getOwnTrainingFacilities().filter(function(ent) + { + const trainable = ent.trainableEntities(civ); + return trainable && trainable.indexOf(template) !== -1; + }); } - return count; -}; - -GameState.prototype.countFoundationsByType = function(type, maintain) -{ - const foundationType = "foundation|" + type; - - if (maintain) + /** + * Get any unit that is capable of constructing the given building type. + */ + findBuilder(template) { - return this.updatingCollection("foundation-type-" + type, filters.byType(foundationType), - this.getOwnFoundations()).length; - } - - let count = 0; - this.getOwnStructures().forEach(function(ent) - { - if (ent.templateName() == foundationType) - ++count; - }); - return count; -}; - -GameState.prototype.countOwnEntitiesByRole = function(role) -{ - return this.getOwnEntitiesByRole(role, "true").length; -}; - -GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) -{ - let count = this.countOwnEntitiesByRole(role); - - // Count entities in building production queues - this.getOwnTrainingFacilities().forEach(function(ent) - { - for (const item of ent.trainingQueue()) - if (item.metadata && item.metadata.role && item.metadata.role == role) - count += item.count; - }); - return count; -}; - -GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) -{ - // Count entities in building production queues - let count = 0; - this.getOwnTrainingFacilities().forEach(function(ent) - { - for (const item of ent.trainingQueue()) - if (item.metadata && item.metadata[data] && item.metadata[data] == value) - count += item.count; - }); - return count; -}; - -GameState.prototype.getOwnFoundations = function() -{ - return this.updatingGlobalCollection("player-" + this.player + "-foundations", - filters.isFoundation(), this.getOwnStructures()); -}; - -GameState.prototype.getOwnDropsites = function(resource) -{ - if (resource) - { - return this.updatingCollection("ownDropsite-" + resource, filters.isDropsite(resource), - this.getOwnEntities()); - } - return this.updatingCollection("ownDropsite-all", filters.isDropsite(), this.getOwnEntities()); -}; - -GameState.prototype.getAnyDropsites = function(resource) -{ - if (resource) - return this.updatingGlobalCollection("anyDropsite-" + resource, filters.isDropsite(resource), this.getEntities()); - return this.updatingGlobalCollection("anyDropsite-all", filters.isDropsite(), this.getEntities()); -}; - -GameState.prototype.getResourceSupplies = function(resource) -{ - return this.updatingGlobalCollection("resource-" + resource, filters.byResource(resource), - this.getEntities()); -}; - -GameState.prototype.getHuntableSupplies = function() -{ - return this.updatingGlobalCollection("resource-hunt", filters.isHuntable(), this.getEntities()); -}; - -GameState.prototype.getFishableSupplies = function() -{ - return this.updatingGlobalCollection("resource-fish", filters.isFishable(), this.getEntities()); -}; - -/** This returns only units from buildings. */ -GameState.prototype.findTrainableUnits = function(classes, anticlasses) -{ - const allTrainable = []; - const civ = this.playerData.civ; - this.getOwnTrainingFacilities().forEach(function(ent) - { - const trainable = ent.trainableEntities(civ); - if (!trainable) - return; - for (const unit of trainable) - if (allTrainable.indexOf(unit) === -1) - allTrainable.push(unit); - }); - const ret = []; - const limits = this.getEntityLimits(); - const current = this.getEntityCounts(); - const matchCounts = this.getEntityMatchCounts(); - for (const trainable of allTrainable) - { - if (this.isTemplateDisabled(trainable)) - continue; - const template = this.getTemplate(trainable); - if (!template || !template.available(this)) - continue; - const limit = template.matchLimit(); - if (matchCounts && limit && matchCounts[trainable] >= limit) - continue; - if (!template.hasClasses(classes) || template.hasClasses(anticlasses)) - continue; - const 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. - */ -GameState.prototype.findAvailableTech = function() -{ - const allResearchable = []; - const civ = this.playerData.civ; - for (const ent of this.getOwnEntities().values()) - { - const searchable = ent.researchableTechs(this, civ); - if (!searchable) - continue; - for (const tech of searchable) - if (!this.playerData.disabledTechnologies[tech] && allResearchable.indexOf(tech) === -1) - allResearchable.push(tech); - } - - const ret = []; - for (const tech of allResearchable) - { - const template = this.getTemplate(tech); - if (template.pairDef()) + const civ = this.getPlayerCiv(); + for (const ent of this.getOwnUnits().values()) { - const 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]]); + const buildable = ent.buildableEntities(civ); + if (buildable && buildable.indexOf(template) !== -1) + return ent; } - else if (this.canResearch(tech)) - { - // Phases are treated separately - if (this.phases.every(phase => template._templateName != phase.name)) - ret.push([tech, template]); - } - } - return ret; -}; - -/** - * Return true if we have a building able to train that template - */ -GameState.prototype.hasTrainer = function(template) -{ - const civ = this.playerData.civ; - for (const ent of this.getOwnTrainingFacilities().values()) - { - const trainable = ent.trainableEntities(civ); - if (trainable && trainable.indexOf(template) !== -1) - return true; - } - return false; -}; - -/** - * Find buildings able to train that template. - */ -GameState.prototype.findTrainers = function(template) -{ - const civ = this.playerData.civ; - return this.getOwnTrainingFacilities().filter(function(ent) - { - const trainable = ent.trainableEntities(civ); - return trainable && trainable.indexOf(template) !== -1; - }); -}; - -/** - * Get any unit that is capable of constructing the given building type. - */ -GameState.prototype.findBuilder = function(template) -{ - const civ = this.getPlayerCiv(); - for (const ent of this.getOwnUnits().values()) - { - const buildable = ent.buildableEntities(civ); - if (buildable && buildable.indexOf(template) !== -1) - return ent; - } - return undefined; -}; - -/** Return true if one of our buildings is capable of researching the given tech */ -GameState.prototype.hasResearchers = function(templateName, noRequirementCheck) -{ - // let's check we can research the tech. - if (!this.canResearch(templateName, noRequirementCheck)) - return false; - - const template = this.getTemplate(templateName); - if (template.autoResearch()) - return true; - - const civ = this.playerData.civ; - - for (const ent of this.getOwnResearchFacilities().values()) - { - const techs = ent.researchableTechs(this, civ); - for (const tech of techs) - { - const temp = this.getTemplate(tech); - if (temp.pairDef()) - { - const 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 */ -GameState.prototype.findResearchers = function(templateName, noRequirementCheck) -{ - // let's check we can research the tech. - if (!this.canResearch(templateName, noRequirementCheck)) return undefined; + } - const self = this; - const civ = this.playerData.civ; - - return this.getOwnResearchFacilities().filter(function(ent) + /** Return true if one of our buildings is capable of researching the given tech */ + hasResearchers(templateName, noRequirementCheck) { - const techs = ent.researchableTechs(self, civ); - for (const tech of techs) + // let's check we can research the tech. + if (!this.canResearch(templateName, noRequirementCheck)) + return false; + + const template = this.getTemplate(templateName); + if (template.autoResearch()) + return true; + + const civ = this.playerData.civ; + + for (const ent of this.getOwnResearchFacilities().values()) { - const thisTemp = self.getTemplate(tech); - if (thisTemp.pairDef()) + const techs = ent.researchableTechs(this, civ); + for (const tech of techs) { - const pairedTechs = thisTemp.getPairedTechs(); - if (pairedTechs[0]._templateName == templateName || - pairedTechs[1]._templateName == templateName) + const temp = this.getTemplate(tech); + if (temp.pairDef()) + { + const pairedTechs = temp.getPairedTechs(); + if (pairedTechs[0]._templateName == templateName || + pairedTechs[1]._templateName == templateName) + return true; + } + else if (tech == templateName) return true; } - else if (tech == templateName) - return true; } return false; - }); -}; + } -GameState.prototype.getEntityLimits = function() -{ - return this.playerData.entityLimits; -}; + /** Find buildings that are capable of researching the given tech */ + findResearchers(templateName, noRequirementCheck) + { + // let's check we can research the tech. + if (!this.canResearch(templateName, noRequirementCheck)) + return undefined; -GameState.prototype.getEntityMatchCounts = function() -{ - return this.playerData.matchEntityCounts; -}; + const self = this; + const civ = this.playerData.civ; -GameState.prototype.getEntityCounts = function() -{ - return this.playerData.entityCounts; -}; + return this.getOwnResearchFacilities().filter(function(ent) + { + const techs = ent.researchableTechs(self, civ); + for (const tech of techs) + { + const thisTemp = self.getTemplate(tech); + if (thisTemp.pairDef()) + { + const pairedTechs = thisTemp.getPairedTechs(); + if (pairedTechs[0]._templateName == templateName || + pairedTechs[1]._templateName == templateName) + return true; + } + else if (tech == templateName) + return true; + } + return false; + }); + } -GameState.prototype.isTemplateAvailable = function(templateName) -{ - if (this.templates[templateName] === undefined) - this.sharedScript.GetTemplate(templateName); - return this.templates[templateName] && !this.isTemplateDisabled(templateName); -}; + getEntityLimits() + { + return this.playerData.entityLimits; + } -GameState.prototype.isTemplateDisabled = function(templateName) -{ - if (!this.playerData.disabledTemplates[templateName]) - return false; - return this.playerData.disabledTemplates[templateName]; -}; + getEntityMatchCounts() + { + return this.playerData.matchEntityCounts; + } -/** Checks whether the maximum number of buildings have been constructed for a certain catergory */ -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]; -}; + getEntityCounts() + { + return this.playerData.entityCounts; + } -GameState.prototype.getTraderTemplatesGains = function() -{ - const shipMechantTemplateName = this.applyCiv("units/{civ}/ship_merchant"); - const supportTraderTemplateName = this.applyCiv("units/{civ}/support_trader"); - const shipMerchantTemplate = !this.isTemplateDisabled(shipMechantTemplateName) && this.getTemplate(shipMechantTemplateName); - const supportTraderTemplate = !this.isTemplateDisabled(supportTraderTemplateName) && this.getTemplate(supportTraderTemplateName); - const norm = TradeGainNormalization(this.sharedScript.mapSize); - const ret = {}; - if (supportTraderTemplate) - ret.landGainMultiplier = norm * supportTraderTemplate.gainMultiplier(); - if (shipMerchantTemplate) - ret.navalGainMultiplier = norm * shipMerchantTemplate.gainMultiplier(); - return ret; -}; + isTemplateAvailable(templateName) + { + if (this.templates[templateName] === undefined) + this.sharedScript.GetTemplate(templateName); + return this.templates[templateName] && !this.isTemplateDisabled(templateName); + } + + isTemplateDisabled(templateName) + { + if (!this.playerData.disabledTemplates[templateName]) + return false; + return this.playerData.disabledTemplates[templateName]; + } + + /** Checks whether the maximum number of buildings have been constructed for a certain catergory */ + isEntityLimitReached(category) + { + if (this.playerData.entityLimits[category] === undefined || + this.playerData.entityCounts[category] === undefined) + return false; + return this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]; + } + + getTraderTemplatesGains() + { + const shipMechantTemplateName = this.applyCiv("units/{civ}/ship_merchant"); + const supportTraderTemplateName = this.applyCiv("units/{civ}/support_trader"); + const shipMerchantTemplate = !this.isTemplateDisabled(shipMechantTemplateName) && this.getTemplate(shipMechantTemplateName); + const supportTraderTemplate = !this.isTemplateDisabled(supportTraderTemplateName) && this.getTemplate(supportTraderTemplateName); + const norm = TradeGainNormalization(this.sharedScript.mapSize); + const ret = {}; + if (supportTraderTemplate) + ret.landGainMultiplier = norm * supportTraderTemplate.gainMultiplier(); + if (shipMerchantTemplate) + ret.navalGainMultiplier = norm * shipMerchantTemplate.gainMultiplier(); + return ret; + } +} diff --git a/binaries/data/mods/public/simulation/ai/common-api/resources.js b/binaries/data/mods/public/simulation/ai/common-api/resources.js index c61e8a6d3f..b9bd00ed16 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/resources.js +++ b/binaries/data/mods/public/simulation/ai/common-api/resources.js @@ -1,60 +1,63 @@ Resources = new Resources(); -export function ResourcesManager(amounts = {}, population = 0) +export class ResourcesManager { - for (const key of Resources.GetCodes()) - this[key] = amounts[key] || 0; + constructor(amounts = {}, population = 0) + { + for (const key of Resources.GetCodes()) + this[key] = amounts[key] || 0; - this.population = population > 0 ? population : 0; + this.population = population > 0 ? population : 0; + } + + reset() + { + for (const key of Resources.GetCodes()) + this[key] = 0; + this.population = 0; + } + + canAfford(that) + { + for (const key of Resources.GetCodes()) + if (this[key] < that[key]) + return false; + return true; + } + + add(that) + { + for (const key of Resources.GetCodes()) + this[key] += that[key]; + this.population += that.population; + } + + subtract(that) + { + for (const key of Resources.GetCodes()) + this[key] -= that[key]; + this.population += that.population; + } + + multiply(n) + { + for (const key of Resources.GetCodes()) + this[key] *= n; + this.population *= n; + } + + Serialize() + { + const amounts = {}; + for (const key of Resources.GetCodes()) + amounts[key] = this[key]; + return { "amounts": amounts, "population": this.population }; + } + + Deserialize(data) + { + for (const key in data.amounts) + this[key] = data.amounts[key]; + this.population = data.population; + } } - -ResourcesManager.prototype.reset = function() -{ - for (const key of Resources.GetCodes()) - this[key] = 0; - this.population = 0; -}; - -ResourcesManager.prototype.canAfford = function(that) -{ - for (const key of Resources.GetCodes()) - if (this[key] < that[key]) - return false; - return true; -}; - -ResourcesManager.prototype.add = function(that) -{ - for (const key of Resources.GetCodes()) - this[key] += that[key]; - this.population += that.population; -}; - -ResourcesManager.prototype.subtract = function(that) -{ - for (const key of Resources.GetCodes()) - this[key] -= that[key]; - this.population += that.population; -}; - -ResourcesManager.prototype.multiply = function(n) -{ - for (const key of Resources.GetCodes()) - this[key] *= n; - this.population *= n; -}; - -ResourcesManager.prototype.Serialize = function() -{ - const amounts = {}; - for (const key of Resources.GetCodes()) - amounts[key] = this[key]; - return { "amounts": amounts, "population": this.population }; -}; - -ResourcesManager.prototype.Deserialize = function(data) -{ - for (const key in data.amounts) - this[key] = data.amounts[key]; - this.population = data.population; -}; diff --git a/binaries/data/mods/public/simulation/ai/common-api/shared.js b/binaries/data/mods/public/simulation/ai/common-api/shared.js index e68a2084be..09d71d8fbd 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/shared.js +++ b/binaries/data/mods/public/simulation/ai/common-api/shared.js @@ -5,448 +5,451 @@ import { InfoMap } from "simulation/ai/common-api/map-module.js"; import { Accessibility, TerrainAnalysis } from "simulation/ai/common-api/terrain-analysis.js"; /** Shared script handling templates and basic terrain analysis */ -export function SharedScript(settings) +export class SharedScript { - if (!settings) - return; - - this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players) - this._templates = settings.templates; - - this._entityMetadata = {}; - for (const player of this._players) - this._entityMetadata[player] = {}; - - // array of entity collections - this._entityCollections = new Map(); - this._entitiesModifications = new Map(); // entities modifications - this._templatesModifications = {}; // template modifications - // each name is a reference to the actual one. - this._entityCollectionsName = new Map(); - this._entityCollectionsByDynProp = {}; - this._entityCollectionsUID = 0; -} - -/** Return a simple object (using no classes etc) that will be serialized into saved games */ -SharedScript.prototype.Serialize = function() -{ - return { - "players": this._players, - "templatesModifications": this._templatesModifications, - "entitiesModifications": this._entitiesModifications, - "metadata": this._entityMetadata - }; -}; - -/** - * Called after the constructor when loading a saved game, with 'data' being - * whatever Serialize() returned - */ -SharedScript.prototype.Deserialize = function(data) -{ - this._players = data.players; - this._templatesModifications = data.templatesModifications; - this._entitiesModifications = data.entitiesModifications; - this._entityMetadata = data.metadata; - - this.isDeserialized = true; -}; - -SharedScript.prototype.GetTemplate = function(name) -{ - if (this._templates[name] === undefined) - this._templates[name] = Engine.GetTemplate(name) || null; - - return this._templates[name]; -}; - -/** - * Initialize the shared component. - * We need to know the initial state of the game for this, as we will use it. - * This is called right at the end of the map generation. - */ -SharedScript.prototype.init = function(state, deserialization) -{ - if (!deserialization) - this._entitiesModifications = new Map(); - - this.ApplyTemplatesDelta(state); - - this.passabilityClasses = state.passabilityClasses; - this.playersData = state.players; - this.timeElapsed = state.timeElapsed; - this.circularMap = state.circularMap; - this.mapSize = state.mapSize; - this.victoryConditions = new Set(state.victoryConditions); - this.alliedVictory = state.alliedVictory; - this.ceasefireActive = state.ceasefireActive; - this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000; - - this.passabilityMap = state.passabilityMap; - if (this.mapSize % this.passabilityMap.width !== 0) - error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width); - this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width; - this.territoryMap = state.territoryMap; - if (this.mapSize % this.territoryMap.width !== 0) - error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width); - this.territoryMap.cellSize = this.mapSize / this.territoryMap.width; - - /* - let landPassMap = new Uint8Array(this.passabilityMap.data.length); - let waterPassMap = new Uint8Array(this.passabilityMap.data.length); - let obstructionMaskLand = this.passabilityClasses["default-terrain-only"]; - let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"]; - for (let i = 0; i < this.passabilityMap.data.length; ++i) + constructor(settings) { - 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); -*/ + if (!settings) + return; - this._entities = new Map(); - if (state.entities) - for (const id in state.entities) - this._entities.set(+id, new Entity(this, state.entities[id])); - // entity collection updated on create/destroy event. - this.entities = new EntityCollection(this, this._entities); + this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players) + this._templates = settings.templates; - // create the terrain analyzer - this.terrainAnalyzer = new TerrainAnalysis(this, state); - this.accessibility = new Accessibility(state, this.terrainAnalyzer); + this._entityMetadata = {}; + for (const player of this._players) + this._entityMetadata[player] = {}; - // Resource types: ignore = not used for resource maps - // abundant = abundant resource with small amount each - // sparse = sparse resource, but huge amount each - // The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites) - // They are updated by checking for create and destroy events for all resources - this.normalizationFactor = { "abundant": 50, "sparse": 90 }; - this.influenceRadius = { "abundant": 36, "sparse": 48 }; - this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 }; - - this.resources = []; // Contains entityIds of all resources in the maps. - this.resourceMaps = {}; // Contains maps showing the density of resources - this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement. - this.createResourceMaps(); - - this.gameState = {}; - for (const player of this._players) - { - this.gameState[player] = new GameState(); - this.gameState[player].init(this, state, player); - } -}; - -/** - * General update of the shared script, before each AI's update - * applies entity deltas, and each gamestate. - */ -SharedScript.prototype.onUpdate = function(state) -{ - if (this.isDeserialized) - { - this.init(state, true); - this.isDeserialized = false; + // 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; } - // deals with updating based on create and destroy messages. - this.ApplyEntitiesDelta(state); - this.ApplyTemplatesDelta(state); - - Engine.ProfileStart("onUpdate"); - - // those are dynamic and need to be reset as the "state" object moves in memory. - this.events = state.events; - this.passabilityClasses = state.passabilityClasses; - this.playersData = state.players; - this.timeElapsed = state.timeElapsed; - this.barterPrices = state.barterPrices; - this.ceasefireActive = state.ceasefireActive; - this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000; - - this.passabilityMap = state.passabilityMap; - this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width; - this.territoryMap = state.territoryMap; - this.territoryMap.cellSize = this.mapSize / this.territoryMap.width; - - for (const i in this.gameState) - this.gameState[i].update(this); - - // TODO: merge this with "ApplyEntitiesDelta" since after all they do the same. - this.updateResourceMaps(this.events); - - Engine.ProfileStop(); -}; - -SharedScript.prototype.ApplyEntitiesDelta = function(state) -{ - Engine.ProfileStart("Shared ApplyEntitiesDelta"); - - const foundationFinished = {}; - - // by order of updating: - // we "Destroy" last because we want to be able to switch Metadata first. - - for (const evt of state.events.Create) + /** Return a simple object (using no classes etc) that will be serialized into saved games */ + Serialize() { - if (!state.entities[evt.entity]) - continue; // Sometimes there are things like foundations which get destroyed too fast - - const entity = new 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 (const entCol of this._entityCollections.values()) - entCol.updateEnt(entity); + return { + "players": this._players, + "templatesModifications": this._templatesModifications, + "entitiesModifications": this._entitiesModifications, + "metadata": this._entityMetadata + }; } - for (const evt of state.events.EntityRenamed) - { // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly + /** + * Called after the constructor when loading a saved game, with 'data' being + * whatever Serialize() returned + */ + Deserialize(data) + { + this._players = data.players; + this._templatesModifications = data.templatesModifications; + this._entitiesModifications = data.entitiesModifications; + this._entityMetadata = data.metadata; + + this.isDeserialized = true; + } + + GetTemplate(name) + { + if (this._templates[name] === undefined) + this._templates[name] = Engine.GetTemplate(name) || null; + + return this._templates[name]; + } + + /** + * Initialize the shared component. + * We need to know the initial state of the game for this, as we will use it. + * This is called right at the end of the map generation. + */ + init(state, deserialization) + { + if (!deserialization) + this._entitiesModifications = new Map(); + + this.ApplyTemplatesDelta(state); + + this.passabilityClasses = state.passabilityClasses; + this.playersData = state.players; + this.timeElapsed = state.timeElapsed; + this.circularMap = state.circularMap; + this.mapSize = state.mapSize; + this.victoryConditions = new Set(state.victoryConditions); + this.alliedVictory = state.alliedVictory; + this.ceasefireActive = state.ceasefireActive; + this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000; + + this.passabilityMap = state.passabilityMap; + if (this.mapSize % this.passabilityMap.width !== 0) + error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width); + this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width; + this.territoryMap = state.territoryMap; + if (this.mapSize % this.territoryMap.width !== 0) + error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width); + this.territoryMap.cellSize = this.mapSize / this.territoryMap.width; + + /* + let landPassMap = new Uint8Array(this.passabilityMap.data.length); + let waterPassMap = new Uint8Array(this.passabilityMap.data.length); + let obstructionMaskLand = this.passabilityClasses["default-terrain-only"]; + let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"]; + for (let i = 0; i < this.passabilityMap.data.length; ++i) + { + landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255; + waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255; + } + Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255); + Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255); + */ + + this._entities = new Map(); + if (state.entities) + for (const id in state.entities) + this._entities.set(+id, new Entity(this, state.entities[id])); + // entity collection updated on create/destroy event. + this.entities = new EntityCollection(this, this._entities); + + // create the terrain analyzer + this.terrainAnalyzer = new TerrainAnalysis(this, state); + this.accessibility = new Accessibility(state, this.terrainAnalyzer); + + // Resource types: ignore = not used for resource maps + // abundant = abundant resource with small amount each + // sparse = sparse resource, but huge amount each + // The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites) + // They are updated by checking for create and destroy events for all resources + this.normalizationFactor = { "abundant": 50, "sparse": 90 }; + this.influenceRadius = { "abundant": 36, "sparse": 48 }; + this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 }; + + this.resources = []; // Contains entityIds of all resources in the maps. + this.resourceMaps = {}; // Contains maps showing the density of resources + this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement. + this.createResourceMaps(); + + this.gameState = {}; for (const player of this._players) { - this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity]; - this._entityMetadata[player][evt.entity] = {}; + this.gameState[player] = new GameState(); + this.gameState[player].init(this, state, player); } } - for (const evt of state.events.TrainingFinished) - { // Apply metadata stored in training queues - for (const entId of evt.entities) - if (this._entities.has(entId)) - for (const key in evt.metadata) - this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]); - } - - for (const evt of state.events.ConstructionFinished) + /** + * General update of the shared script, before each AI's update + * applies entity deltas, and each gamestate. + */ + onUpdate(state) { - // metada are already moved by EntityRenamed when needed (i.e. construction, not repair) - if (evt.entity != evt.newentity) - foundationFinished[evt.entity] = true; - } - - for (const 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 (const key in evt.metadata) - this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]); - } - - for (const evt of state.events.Destroy) - { - if (foundationFinished[evt.entity]) - evt.SuccessfulFoundation = true; - - // The entity was destroyed but its data may still be useful, so - // remember the AI's metadata concerning it - evt.metadata = {}; - for (const player of this._players) - evt.metadata[player] = this._entityMetadata[player][evt.entity]; - - const entity = this._entities.get(evt.entity); - if (entity) + if (this.isDeserialized) { + this.init(state, true); + this.isDeserialized = false; + } + + // deals with updating based on create and destroy messages. + this.ApplyEntitiesDelta(state); + this.ApplyTemplatesDelta(state); + + Engine.ProfileStart("onUpdate"); + + // those are dynamic and need to be reset as the "state" object moves in memory. + this.events = state.events; + this.passabilityClasses = state.passabilityClasses; + this.playersData = state.players; + this.timeElapsed = state.timeElapsed; + this.barterPrices = state.barterPrices; + this.ceasefireActive = state.ceasefireActive; + this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000; + + this.passabilityMap = state.passabilityMap; + this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width; + this.territoryMap = state.territoryMap; + this.territoryMap.cellSize = this.mapSize / this.territoryMap.width; + + for (const i in this.gameState) + this.gameState[i].update(this); + + // TODO: merge this with "ApplyEntitiesDelta" since after all they do the same. + this.updateResourceMaps(this.events); + + Engine.ProfileStop(); + } + + ApplyEntitiesDelta(state) + { + Engine.ProfileStart("Shared ApplyEntitiesDelta"); + + const foundationFinished = {}; + + // by order of updating: + // we "Destroy" last because we want to be able to switch Metadata first. + + for (const evt of state.events.Create) + { + if (!state.entities[evt.entity]) + continue; // Sometimes there are things like foundations which get destroyed too fast + + const entity = new 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 (const entCol of this._entityCollections.values()) - entCol.removeEnt(entity); - this.entities.removeEnt(entity); + entCol.updateEnt(entity); } - this._entities.delete(evt.entity); - this._entitiesModifications.delete(evt.entity); - for (const player of this._players) - delete this._entityMetadata[player][evt.entity]; - } - - for (const id in state.entities) - { - const changes = state.entities[id]; - const entity = this._entities.get(+id); - for (const prop in changes) - { - entity._entity[prop] = changes[prop]; - this.updateEntityCollections(prop, entity); + for (const evt of state.events.EntityRenamed) + { // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly + for (const player of this._players) + { + this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity]; + this._entityMetadata[player][evt.entity] = {}; + } } - } - // apply per-entity aura-related changes. - // this supersedes tech-related changes. - for (const id in state.changedEntityTemplateInfo) - { - if (!this._entities.has(+id)) - continue; // dead, presumably. - const changes = state.changedEntityTemplateInfo[id]; - if (!this._entitiesModifications.has(+id)) - this._entitiesModifications.set(+id, new Map()); - const modif = this._entitiesModifications.get(+id); - for (const change of changes) - modif.set(change.variable, change.value); - } - Engine.ProfileStop(); -}; + for (const evt of state.events.TrainingFinished) + { // Apply metadata stored in training queues + for (const entId of evt.entities) + if (this._entities.has(entId)) + for (const key in evt.metadata) + this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]); + } -SharedScript.prototype.ApplyTemplatesDelta = function(state) -{ - Engine.ProfileStart("Shared ApplyTemplatesDelta"); - - for (const player in state.changedTemplateInfo) - { - const playerDiff = state.changedTemplateInfo[player]; - for (const template in playerDiff) + for (const evt of state.events.ConstructionFinished) { - const changes = playerDiff[template]; - if (!this._templatesModifications[template]) - this._templatesModifications[template] = {}; - if (!this._templatesModifications[template][player]) - this._templatesModifications[template][player] = new Map(); - const modif = this._templatesModifications[template][player]; + // metada are already moved by EntityRenamed when needed (i.e. construction, not repair) + if (evt.entity != evt.newentity) + foundationFinished[evt.entity] = true; + } + + for (const 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 (const key in evt.metadata) + this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]); + } + + for (const evt of state.events.Destroy) + { + if (foundationFinished[evt.entity]) + evt.SuccessfulFoundation = true; + + // The entity was destroyed but its data may still be useful, so + // remember the AI's metadata concerning it + evt.metadata = {}; + for (const player of this._players) + evt.metadata[player] = this._entityMetadata[player][evt.entity]; + + const entity = this._entities.get(evt.entity); + if (entity) + { + for (const entCol of this._entityCollections.values()) + entCol.removeEnt(entity); + this.entities.removeEnt(entity); + } + + this._entities.delete(evt.entity); + this._entitiesModifications.delete(evt.entity); + for (const player of this._players) + delete this._entityMetadata[player][evt.entity]; + } + + for (const id in state.entities) + { + const changes = state.entities[id]; + const entity = this._entities.get(+id); + for (const prop in changes) + { + entity._entity[prop] = changes[prop]; + this.updateEntityCollections(prop, entity); + } + } + + // apply per-entity aura-related changes. + // this supersedes tech-related changes. + for (const id in state.changedEntityTemplateInfo) + { + if (!this._entities.has(+id)) + continue; // dead, presumably. + const changes = state.changedEntityTemplateInfo[id]; + if (!this._entitiesModifications.has(+id)) + this._entitiesModifications.set(+id, new Map()); + const modif = this._entitiesModifications.get(+id); for (const change of changes) modif.set(change.variable, change.value); } + Engine.ProfileStop(); } - this._templatesModifications = - Object.fromEntries(Object.entries(this._templatesModifications).sort()); - Engine.ProfileStop(); -}; -SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection) -{ - entCollection.setUID(this._entityCollectionsUID); - this._entityCollections.set(this._entityCollectionsUID, entCollection); - for (const prop of entCollection.dynamicProperties()) + ApplyTemplatesDelta(state) { - if (!this._entityCollectionsByDynProp[prop]) - this._entityCollectionsByDynProp[prop] = new Map(); - this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection); + Engine.ProfileStart("Shared ApplyTemplatesDelta"); + + for (const player in state.changedTemplateInfo) + { + const playerDiff = state.changedTemplateInfo[player]; + for (const template in playerDiff) + { + const changes = playerDiff[template]; + if (!this._templatesModifications[template]) + this._templatesModifications[template] = {}; + if (!this._templatesModifications[template][player]) + this._templatesModifications[template][player] = new Map(); + const modif = this._templatesModifications[template][player]; + for (const change of changes) + modif.set(change.variable, change.value); + } + } + this._templatesModifications = + Object.fromEntries(Object.entries(this._templatesModifications).sort()); + Engine.ProfileStop(); } - this._entityCollectionsUID++; -}; -SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection) -{ - const uid = entCollection.getUID(); - - if (this._entityCollections.has(uid)) - this._entityCollections.delete(uid); - - for (const prop of entCollection.dynamicProperties()) - if (this._entityCollectionsByDynProp[prop].has(uid)) - this._entityCollectionsByDynProp[prop].delete(uid); -}; - -SharedScript.prototype.updateEntityCollections = function(property, ent) -{ - if (this._entityCollectionsByDynProp[property] === undefined) - return; - - for (const entCol of this._entityCollectionsByDynProp[property].values()) - entCol.updateEnt(ent); -}; - -SharedScript.prototype.setMetadata = function(player, ent, key, value) -{ - let metadata = this._entityMetadata[player][ent.id()]; - if (!metadata) + registerUpdatingEntityCollection(entCollection) { - this._entityMetadata[player][ent.id()] = {}; - metadata = this._entityMetadata[player][ent.id()]; + entCollection.setUID(this._entityCollectionsUID); + this._entityCollections.set(this._entityCollectionsUID, entCollection); + for (const prop of entCollection.dynamicProperties()) + { + if (!this._entityCollectionsByDynProp[prop]) + this._entityCollectionsByDynProp[prop] = new Map(); + this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection); + } + this._entityCollectionsUID++; } - metadata[key] = value; - this.updateEntityCollections('metadata', ent); - this.updateEntityCollections('metadata.' + key, ent); -}; + removeUpdatingEntityCollection(entCollection) + { + const uid = entCollection.getUID(); -SharedScript.prototype.getMetadata = function(player, ent, key) -{ - return this._entityMetadata[player][ent.id()]?.[key]; -}; + if (this._entityCollections.has(uid)) + this._entityCollections.delete(uid); -SharedScript.prototype.deleteMetadata = function(player, ent, key) -{ - const metadata = this._entityMetadata[player][ent.id()]; + for (const prop of entCollection.dynamicProperties()) + if (this._entityCollectionsByDynProp[prop].has(uid)) + this._entityCollectionsByDynProp[prop].delete(uid); + } - if (!metadata || !(key in metadata)) + updateEntityCollections(property, ent) + { + if (this._entityCollectionsByDynProp[property] === undefined) + return; + + for (const entCol of this._entityCollectionsByDynProp[property].values()) + entCol.updateEnt(ent); + } + + setMetadata(player, ent, key, value) + { + let metadata = this._entityMetadata[player][ent.id()]; + if (!metadata) + { + this._entityMetadata[player][ent.id()] = {}; + metadata = this._entityMetadata[player][ent.id()]; + } + metadata[key] = value; + + this.updateEntityCollections('metadata', ent); + this.updateEntityCollections('metadata.' + key, ent); + } + + getMetadata(player, ent, key) + { + return this._entityMetadata[player][ent.id()]?.[key]; + } + + deleteMetadata(player, ent, key) + { + const 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; - metadata[key] = undefined; - delete metadata[key]; - this.updateEntityCollections('metadata', ent); - this.updateEntityCollections('metadata.' + key, ent); - return true; -}; - -/** creates a map of resource density */ -SharedScript.prototype.createResourceMaps = function() -{ - for (const resource of Resources.GetCodes()) - { - if (this.resourceMaps[resource] || - !(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor)) - continue; - // We're creating them 8-bit. Things could go above 255 if there are really tons of resources - // But at that point the precision is not really important anyway. And it saves memory. - this.resourceMaps[resource] = new InfoMap(this, "resource"); - this.ccResourceMaps[resource] = new InfoMap(this, "resource"); - } - for (const ent of this._entities.values()) - this.addEntityToResourceMap(ent); -}; - -/** - * @param {Object} events - The events from a turn. - */ -SharedScript.prototype.updateResourceMaps = function(events) -{ - if (events.Destroy.some(e => this.resources.includes(e.entity))) - { - this.resources = []; - this.resourceMaps = {}; - this.createResourceMaps(); } - for (const e of events.Create) - if (e.entity && this._entities.has(e.entity)) - this.addEntityToResourceMap(this._entities.get(e.entity)); -}; + /** creates a map of resource density */ + createResourceMaps() + { + for (const resource of Resources.GetCodes()) + { + if (this.resourceMaps[resource] || + !(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor)) + continue; + // We're creating them 8-bit. Things could go above 255 if there are really tons of resources + // But at that point the precision is not really important anyway. And it saves memory. + this.resourceMaps[resource] = new InfoMap(this, "resource"); + this.ccResourceMaps[resource] = new InfoMap(this, "resource"); + } + for (const ent of this._entities.values()) + this.addEntityToResourceMap(ent); + } -/** - * @param {entity} entity - The entity to add to the resource map. - */ -SharedScript.prototype.addEntityToResourceMap = function(entity) -{ - this.changeEntityInResourceMapHelper(entity, 1); - this.resources.push(entity.id()); -}; + /** + * @param {Object} events - The events from a turn. + */ + updateResourceMaps(events) + { + if (events.Destroy.some(e => this.resources.includes(e.entity))) + { + this.resources = []; + this.resourceMaps = {}; + this.createResourceMaps(); + } -/** - * @param {entity} entity - The entity to remove from the resource map. - */ -SharedScript.prototype.removeEntityFromResourceMap = function(entity) -{ - this.changeEntityInResourceMapHelper(entity, -1); -}; + for (const e of events.Create) + if (e.entity && this._entities.has(e.entity)) + this.addEntityToResourceMap(this._entities.get(e.entity)); + } -/** - * @param {entity} ent - The entity to add to the resource map. - */ -SharedScript.prototype.changeEntityInResourceMapHelper = function(ent, multiplication = 1) -{ - if (!ent) - return; - const entPos = ent.position(); - if (!entPos) - return; - const resource = ent.resourceSupplyType()?.generic; - if (!resource || !this.resourceMaps[resource]) - return; - const cellSize = this.resourceMaps[resource].cellSize; - const x = Math.floor(entPos[0] / cellSize); - const y = Math.floor(entPos[1] / cellSize); - const grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup; - const strength = multiplication * ent.resourceSupplyMax() / this.normalizationFactor[grp]; - this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2, "constant"); - this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2); - this.ccResourceMaps[resource].addInfluence(x, y, this.ccInfluenceRadius[grp] / cellSize, strength, "constant"); -}; + /** + * @param {entity} entity - The entity to add to the resource map. + */ + addEntityToResourceMap(entity) + { + this.changeEntityInResourceMapHelper(entity, 1); + this.resources.push(entity.id()); + } + + /** + * @param {entity} entity - The entity to remove from the resource map. + */ + removeEntityFromResourceMap(entity) + { + this.changeEntityInResourceMapHelper(entity, -1); + } + + /** + * @param {entity} ent - The entity to add to the resource map. + */ + changeEntityInResourceMapHelper(ent, multiplication = 1) + { + if (!ent) + return; + const entPos = ent.position(); + if (!entPos) + return; + const resource = ent.resourceSupplyType()?.generic; + if (!resource || !this.resourceMaps[resource]) + return; + const cellSize = this.resourceMaps[resource].cellSize; + const x = Math.floor(entPos[0] / cellSize); + const y = Math.floor(entPos[1] / cellSize); + const grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup; + const strength = multiplication * ent.resourceSupplyMax() / this.normalizationFactor[grp]; + this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2, "constant"); + this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2); + this.ccResourceMaps[resource].addInfluence(x, y, this.ccInfluenceRadius[grp] / cellSize, strength, "constant"); + } +} diff --git a/binaries/data/mods/public/simulation/ai/common-api/technology.js b/binaries/data/mods/public/simulation/ai/common-api/technology.js index cd8783cb80..59661acc56 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/technology.js +++ b/binaries/data/mods/public/simulation/ai/common-api/technology.js @@ -1,124 +1,127 @@ LoadModificationTemplates(); /** Wrapper around a technology template */ -export function Technology(templateName) +export class Technology { - this._templateName = templateName; - const template = TechnologyTemplates.Get(templateName); - - // check if this is one of two paired technologies. - if (template.partOfPair) + constructor(templateName) { - const parentTech = TechnologyTemplates.Get(template.partOfPair); - this._pairedWith = parentTech.pair[0] == templateName ? parentTech.pair[1] : parentTech.pair[0]; + this._templateName = templateName; + const template = TechnologyTemplates.Get(templateName); + + // check if this is one of two paired technologies. + if (template.partOfPair) + { + const parentTech = TechnologyTemplates.Get(template.partOfPair); + this._pairedWith = parentTech.pair[0] == templateName ? parentTech.pair[1] : parentTech.pair[0]; + } + + // check if it only defines a pair: + this._definesPair = !!template.pair; + this._template = template; } - // check if it only defines a pair: - this._definesPair = !!template.pair; - this._template = template; + /** returns generic, or specific if civ provided. */ + name(civ) + { + if (civ === undefined) + return this._template.genericName; + + if (this._template.specificName === undefined || this._template.specificName[civ] === undefined) + return undefined; + return this._template.specificName[civ]; + } + + pairDef() + { + return this._definesPair; + } + + /** in case this defines a pair only, returns the two paired technologies. */ + getPairedTechs() + { + if (!this._definesPair) + return undefined; + + return this._template.pair.map(name => new Technology(name)); + } + + pair() + { + return this._template.partOfPair; + } + + pairedWith() + { + if (!this._template.partOfPair) + return undefined; + return this._pairedWith; + } + + cost(researcher) + { + if (!this._template.cost) + return undefined; + const cost = {}; + for (const type in this._template.cost) + { + cost[type] = +this._template.cost[type]; + if (researcher) + cost[type] *= researcher.techCostMultiplier(type); + } + return cost; + } + + costSum(researcher) + { + const cost = this.cost(researcher); + if (!cost) + return 0; + let ret = 0; + for (const type in cost) + ret += cost[type]; + return ret; + } + + researchTime() + { + return this._template.researchTime || 0; + } + + requirements(civ) + { + return DeriveTechnologyRequirements(this._template, civ); + } + + autoResearch() + { + if (!this._template.autoResearch) + return undefined; + return this._template.autoResearch; + } + + supersedes() + { + if (!this._template.supersedes) + return undefined; + return this._template.supersedes; + } + + modifications() + { + if (!this._template.modifications) + return undefined; + return this._template.modifications; + } + + affects() + { + if (!this._template.affects) + return undefined; + return this._template.affects; + } + + isAffected(classes) + { + return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect)); + } } - -/** returns generic, or specific if civ provided. */ -Technology.prototype.name = function(civ) -{ - if (civ === undefined) - return this._template.genericName; - - if (this._template.specificName === undefined || this._template.specificName[civ] === undefined) - return undefined; - return this._template.specificName[civ]; -}; - -Technology.prototype.pairDef = function() -{ - return this._definesPair; -}; - -/** in case this defines a pair only, returns the two paired technologies. */ -Technology.prototype.getPairedTechs = function() -{ - if (!this._definesPair) - return undefined; - - return this._template.pair.map(name => new Technology(name)); -}; - -Technology.prototype.pair = function() -{ - return this._template.partOfPair; -}; - -Technology.prototype.pairedWith = function() -{ - if (!this._template.partOfPair) - return undefined; - return this._pairedWith; -}; - -Technology.prototype.cost = function(researcher) -{ - if (!this._template.cost) - return undefined; - const cost = {}; - for (const type in this._template.cost) - { - cost[type] = +this._template.cost[type]; - if (researcher) - cost[type] *= researcher.techCostMultiplier(type); - } - return cost; -}; - -Technology.prototype.costSum = function(researcher) -{ - const cost = this.cost(researcher); - if (!cost) - return 0; - let ret = 0; - for (const type in cost) - ret += cost[type]; - return ret; -}; - -Technology.prototype.researchTime = function() -{ - return this._template.researchTime || 0; -}; - -Technology.prototype.requirements = function(civ) -{ - return DeriveTechnologyRequirements(this._template, civ); -}; - -Technology.prototype.autoResearch = function() -{ - if (!this._template.autoResearch) - return undefined; - return this._template.autoResearch; -}; - -Technology.prototype.supersedes = function() -{ - if (!this._template.supersedes) - return undefined; - return this._template.supersedes; -}; - -Technology.prototype.modifications = function() -{ - if (!this._template.modifications) - return undefined; - return this._template.modifications; -}; - -Technology.prototype.affects = function() -{ - if (!this._template.affects) - return undefined; - return this._template.affects; -}; - -Technology.prototype.isAffected = function(classes) -{ - return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect)); -}; diff --git a/binaries/data/mods/public/simulation/ai/petra/_petrabot.js b/binaries/data/mods/public/simulation/ai/petra/_petrabot.js index ae04fae2c6..fe3564219d 100644 --- a/binaries/data/mods/public/simulation/ai/petra/_petrabot.js +++ b/binaries/data/mods/public/simulation/ai/petra/_petrabot.js @@ -6,171 +6,173 @@ import { Queue } from "simulation/ai/petra/queue.js"; import { QueueManager } from "simulation/ai/petra/queueManager.js"; import { gameAnalysis } from "simulation/ai/petra/startingStrategy.js"; -export function PetraBot(settings) +export class PetraBot extends BaseAI { - BaseAI.call(this, settings); - - // played turn, because Petra doesn't play every turn. - this.turn = 0; - this.playedTurn = 0; - this.elapsedTime = 0; - - this.uniqueIDs = { - "armies": 1, // starts at 1 to allow easier tests on armies ID existence - "bases": 1, // base manager ID starts at one because "0" means "no base" on the map - "plans": 0, // training/building/research plans - "transports": 1 // transport plans start at 1 because 0 might be used as none - }; - - this.Config = new Config(settings.difficulty, settings.behavior); - - this.savedEvents = {}; -} - -PetraBot.prototype = Object.create(BaseAI.prototype); - -PetraBot.prototype.CustomInit = function(gameState) -{ - if (this.isDeserialized) + constructor(settings) { - // WARNING: the deserializations should not modify the metadatas infos inside their init functions - this.canPlay = this.data.canPlay; - this.turn = this.data.turn; - this.playedTurn = this.data.playedTurn; - this.elapsedTime = this.data.elapsedTime; - this.savedEvents = this.data.savedEvents; + super(settings); + + // played turn, because Petra doesn't play every turn. + this.turn = 0; + this.playedTurn = 0; + this.elapsedTime = 0; + + this.uniqueIDs = { + "armies": 1, // starts at 1 to allow easier tests on armies ID existence + "bases": 1, // base manager ID starts at one because "0" means "no base" on the map + "plans": 0, // training/building/research plans + "transports": 1 // transport plans start at 1 because 0 might be used as none + }; + + this.Config = new Config(settings.difficulty, settings.behavior); + + this.savedEvents = {}; + } + + CustomInit(gameState) + { + if (this.isDeserialized) + { + // WARNING: the deserializations should not modify the metadatas infos inside their init functions + this.canPlay = this.data.canPlay; + this.turn = this.data.turn; + this.playedTurn = this.data.playedTurn; + this.elapsedTime = this.data.elapsedTime; + this.savedEvents = this.data.savedEvents; + for (const key in this.savedEvents) + { + for (const i in this.savedEvents[key]) + { + const evt = this.savedEvents[key][i]; + const evtmod = {}; + for (const keyevt in evt) + { + evtmod[keyevt] = evt[keyevt]; + this.savedEvents[key][i] = evtmod; + } + } + } + + this.Config.Deserialize(this.data.config); + + this.queueManager = new QueueManager(this.Config, {}); + this.queueManager.Deserialize(gameState, this.data.queueManager); + this.queues = this.queueManager.queues; + + this.HQ = new Headquarters(this.Config, true); + this.HQ.init(gameState, this.queues); + this.HQ.Deserialize(gameState, this.data.HQ); + + this.uniqueIDs = this.data.uniqueIDs; + this.isDeserialized = false; + this.data = undefined; + + // initialisation needed after the completion of the deserialization + this.HQ.postinit(gameState); + } + else + { + this.Config.setConfig(gameState); + + // this.queues can only be modified by the queue manager or things will go awry. + this.queues = {}; + for (const i in this.Config.priorities) + this.queues[i] = new Queue(); + + this.queueManager = new QueueManager(this.Config, this.queues); + + this.HQ = new Headquarters(this.Config, false); + + this.HQ.init(gameState, this.queues); + + // Try to analyze our starting position and set a strategy. + this.canPlay = gameAnalysis(this.HQ, gameState); + } + } + + OnUpdate(sharedScript) + { + if (this.isDeserialized) + this.Init(PlayerID, sharedScript); + + if (this.gameFinished || this.gameState.playerData.state == "defeated") + return; + + for (const i in sharedScript.events) + { + if (i == "AIMetadata") // not used inside petra + continue; + if (this.savedEvents[i] !== undefined) + this.savedEvents[i] = this.savedEvents[i].concat(sharedScript.events[i]); + else + this.savedEvents[i] = sharedScript.events[i]; + } + + // Run the update every n turns, offset depending on player ID to balance the load + this.elapsedTime = this.gameState.getTimeElapsed() / 1000; + if (!this.playedTurn || (this.turn + this.player) % 8 == 5) + { + Engine.ProfileStart("PetraBot bot (player " + this.player +")"); + + this.playedTurn++; + + if (!this.canPlay) + { + Engine.ProfileStop(); + return; + } + + this.HQ.update(this.gameState, this.queues, this.savedEvents); + + this.queueManager.update(this.gameState); + + for (const i in this.savedEvents) + this.savedEvents[i] = []; + + Engine.ProfileStop(); + } + + this.turn++; + } + + Serialize() + { + if (this.isDeserialized) + return this.data; + + const savedEvents = {}; for (const key in this.savedEvents) { - for (const i in this.savedEvents[key]) + savedEvents[key] = this.savedEvents[key].slice(); + for (const i in savedEvents[key]) { - const evt = this.savedEvents[key][i]; + if (!savedEvents[key][i]) + continue; + const evt = savedEvents[key][i]; const evtmod = {}; for (const keyevt in evt) - { evtmod[keyevt] = evt[keyevt]; - this.savedEvents[key][i] = evtmod; - } + savedEvents[key][i] = evtmod; } } - this.Config.Deserialize(this.data.config); - - this.queueManager = new QueueManager(this.Config, {}); - this.queueManager.Deserialize(gameState, this.data.queueManager); - this.queues = this.queueManager.queues; - - this.HQ = new Headquarters(this.Config, true); - this.HQ.init(gameState, this.queues); - this.HQ.Deserialize(gameState, this.data.HQ); - - this.uniqueIDs = this.data.uniqueIDs; - this.isDeserialized = false; - this.data = undefined; - - // initialisation needed after the completion of the deserialization - this.HQ.postinit(gameState); + return { + "canPlay": this.canPlay, + "uniqueIDs": this.uniqueIDs, + "turn": this.turn, + "playedTurn": this.playedTurn, + "elapsedTime": this.elapsedTime, + "savedEvents": savedEvents, + "config": this.Config.Serialize(), + "queueManager": this.queueManager.Serialize(), + "HQ": this.HQ.Serialize() + }; } - else + + Deserialize(data, sharedScript) { - this.Config.setConfig(gameState); - - // this.queues can only be modified by the queue manager or things will go awry. - this.queues = {}; - for (const i in this.Config.priorities) - this.queues[i] = new Queue(); - - this.queueManager = new QueueManager(this.Config, this.queues); - - this.HQ = new Headquarters(this.Config, false); - - this.HQ.init(gameState, this.queues); - - // Try to analyze our starting position and set a strategy. - this.canPlay = gameAnalysis(this.HQ, gameState); + this.isDeserialized = true; + this.data = data; } -}; +} -PetraBot.prototype.OnUpdate = function(sharedScript) -{ - if (this.isDeserialized) - this.Init(PlayerID, sharedScript); - - if (this.gameFinished || this.gameState.playerData.state == "defeated") - return; - - for (const i in sharedScript.events) - { - if (i == "AIMetadata") // not used inside petra - continue; - if (this.savedEvents[i] !== undefined) - this.savedEvents[i] = this.savedEvents[i].concat(sharedScript.events[i]); - else - this.savedEvents[i] = sharedScript.events[i]; - } - - // Run the update every n turns, offset depending on player ID to balance the load - this.elapsedTime = this.gameState.getTimeElapsed() / 1000; - if (!this.playedTurn || (this.turn + this.player) % 8 == 5) - { - Engine.ProfileStart("PetraBot bot (player " + this.player +")"); - - this.playedTurn++; - - if (!this.canPlay) - { - Engine.ProfileStop(); - return; - } - - this.HQ.update(this.gameState, this.queues, this.savedEvents); - - this.queueManager.update(this.gameState); - - for (const i in this.savedEvents) - this.savedEvents[i] = []; - - Engine.ProfileStop(); - } - - this.turn++; -}; - -PetraBot.prototype.Serialize = function() -{ - if (this.isDeserialized) - return this.data; - - const savedEvents = {}; - for (const key in this.savedEvents) - { - savedEvents[key] = this.savedEvents[key].slice(); - for (const i in savedEvents[key]) - { - if (!savedEvents[key][i]) - continue; - const evt = savedEvents[key][i]; - const evtmod = {}; - for (const keyevt in evt) - evtmod[keyevt] = evt[keyevt]; - savedEvents[key][i] = evtmod; - } - } - - return { - "canPlay": this.canPlay, - "uniqueIDs": this.uniqueIDs, - "turn": this.turn, - "playedTurn": this.playedTurn, - "elapsedTime": this.elapsedTime, - "savedEvents": savedEvents, - "config": this.Config.Serialize(), - "queueManager": this.queueManager.Serialize(), - "HQ": this.HQ.Serialize() - }; -}; - -PetraBot.prototype.Deserialize = function(data, sharedScript) -{ - this.isDeserialized = true; - this.data = data; -}; diff --git a/binaries/data/mods/public/simulation/ai/petra/attackManager.js b/binaries/data/mods/public/simulation/ai/petra/attackManager.js index b86ad0f9a9..933c4b33c2 100644 --- a/binaries/data/mods/public/simulation/ai/petra/attackManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/attackManager.js @@ -7,861 +7,864 @@ import * as difficulty from "simulation/ai/petra/difficultyLevel.js"; import { allowCapture, getLandAccess } from "simulation/ai/petra/entityExtend.js"; import { Worker } from "simulation/ai/petra/worker.js"; -export function AttackManager(config) +export class AttackManager { - this.Config = config; - - this.totalNumber = 0; - this.attackNumber = 0; - this.rushNumber = 0; - this.raidNumber = 0; - this.upcomingAttacks = { + totalNumber = 0; + attackNumber = 0; + rushNumber = 0; + raidNumber = 0; + upcomingAttacks = { [AttackPlan.TYPE_RUSH]: [], [AttackPlan.TYPE_RAID]: [], [AttackPlan.TYPE_DEFAULT]: [], [AttackPlan.TYPE_HUGE_ATTACK]: [] }; - this.startedAttacks = { + startedAttacks = { [AttackPlan.TYPE_RUSH]: [], [AttackPlan.TYPE_RAID]: [], [AttackPlan.TYPE_DEFAULT]: [], [AttackPlan.TYPE_HUGE_ATTACK]: [] }; - this.bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start - this.debugTime = 0; - this.maxRushes = 0; - this.rushSize = []; - this.currentEnemyPlayer = undefined; // enemy player we are currently targeting - this.defeated = {}; -} + bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start + debugTime = 0; + maxRushes = 0; + rushSize = []; + currentEnemyPlayer = undefined; // enemy player we are currently targeting + defeated = {}; -/** More initialisation for stuff that needs the gameState */ -AttackManager.prototype.init = function(gameState) -{ - this.outOfPlan = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "plan", -1)); - this.outOfPlan.registerUpdates(); -}; - -AttackManager.prototype.setRushes = function(allowed) -{ - if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) + constructor(config) { - this.maxRushes = 3; - this.rushSize = [ 16, 20, 24 ]; + this.Config = config; } - else if (this.Config.personality.aggressive > this.Config.personalityCut.medium && allowed > 1) - { - this.maxRushes = 2; - this.rushSize = [ 18, 22 ]; - } - else if (this.Config.personality.aggressive > this.Config.personalityCut.weak && allowed > 0) - { - this.maxRushes = 1; - this.rushSize = [ 20 ]; - } -}; -AttackManager.prototype.checkEvents = function(gameState, events) -{ - for (const evt of events.PlayerDefeated) - this.defeated[evt.playerId] = true; - - let answer = "decline"; - let other; - let targetPlayer; - for (const evt of events.AttackRequest) + /** More initialisation for stuff that needs the gameState */ + init(gameState) { - if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) - continue; - targetPlayer = evt.player; - let available = 0; - for (const attackType in this.upcomingAttacks) + this.outOfPlan = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "plan", -1)); + this.outOfPlan.registerUpdates(); + } + + setRushes(allowed) + { + if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) { - for (const attack of this.upcomingAttacks[attackType]) - { - if (attack.state === AttackPlan.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; - } + this.maxRushes = 3; + this.rushSize = [ 16, 20, 24 ]; } - - if (available > 12) // launch the attack immediately + else if (this.Config.personality.aggressive > this.Config.personalityCut.medium && allowed > 1) { + this.maxRushes = 2; + this.rushSize = [ 18, 22 ]; + } + else if (this.Config.personality.aggressive > this.Config.personalityCut.weak && allowed > 0) + { + this.maxRushes = 1; + this.rushSize = [ 20 ]; + } + } + + checkEvents(gameState, events) + { + for (const evt of events.PlayerDefeated) + this.defeated[evt.playerId] = true; + + let answer = "decline"; + let other; + let targetPlayer; + for (const evt of events.AttackRequest) + { + if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) + continue; + targetPlayer = evt.player; + let available = 0; for (const attackType in this.upcomingAttacks) { for (const attack of this.upcomingAttacks[attackType]) { - if (attack.state === AttackPlan.STATE_COMPLETING || - attack.targetPlayer !== targetPlayer || - attack.unitCollection.length < 3) + if (attack.state === AttackPlan.STATE_COMPLETING) + { + if (attack.targetPlayer === targetPlayer) + available += attack.unitCollection.length; + else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) + other = attack.targetPlayer; continue; - attack.forceStart(); - attack.requested = true; + } + + attack.targetPlayer = targetPlayer; + + if (attack.unitCollection.length > 2) + available += attack.unitCollection.length; } } - answer = "join"; - } - else if (other !== undefined) - answer = "other"; - break; // take only the first attack request into account - } - if (targetPlayer !== undefined) - chat.answerRequestAttack(gameState, targetPlayer, answer, other); - for (const evt of events.EntityRenamed) // take care of packing units in bombing attacks - { - for (const [targetId, unitIds] of this.bombingAttacks) - { - if (targetId == evt.entity) + if (available > 12) // launch the attack immediately { - this.bombingAttacks.set(evt.newentity, unitIds); - this.bombingAttacks.delete(evt.entity); - } - else if (unitIds.has(evt.entity)) - { - unitIds.add(evt.newentity); - unitIds.delete(evt.entity); - } - } - } -}; - -/** - * Check for any structure in range from within our territory, and bomb it - */ -AttackManager.prototype.assignBombers = function(gameState) -{ - // First some cleaning of current bombing attacks - for (const [targetId, unitIds] of this.bombingAttacks) - { - const target = gameState.getEntityById(targetId); - if (!target || !gameState.isPlayerEnemy(target.owner())) - this.bombingAttacks.delete(targetId); - else - { - for (const entId of unitIds.values()) - { - const ent = gameState.getEntityById(entId); - if (ent && ent.owner() == PlayerID) + for (const attackType in this.upcomingAttacks) { - const plan = ent.getMetadata(PlayerID, "plan"); - const orders = ent.unitAIOrderData(); - const lastOrder = orders && orders.length ? orders[orders.length-1] : null; - if (lastOrder && lastOrder.target && lastOrder.target == targetId && plan != -2 && plan != -3) - continue; + for (const attack of this.upcomingAttacks[attackType]) + { + if (attack.state === AttackPlan.STATE_COMPLETING || + attack.targetPlayer !== targetPlayer || + attack.unitCollection.length < 3) + continue; + attack.forceStart(); + attack.requested = true; + } } - unitIds.delete(entId); + answer = "join"; } - if (!unitIds.size) - this.bombingAttacks.delete(targetId); + else if (other !== undefined) + answer = "other"; + break; // take only the first attack request into account } - } + if (targetPlayer !== undefined) + chat.answerRequestAttack(gameState, targetPlayer, answer, other); - const bombers = gameState.updatingCollection("bombers", - filters.byClasses(["BoltShooter", "StoneThrower"]), gameState.getOwnUnits()); - for (const ent of bombers.values()) - { - if (!ent.position() || !ent.isIdle() || !ent.attackRange("Ranged")) - continue; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - continue; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) + for (const evt of events.EntityRenamed) // take care of packing units in bombing attacks { - const subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole === Worker.SUBROLE_COMPLETING || - subrole === Worker.SUBROLE_WALKING || subrole === Worker.SUBROLE_ATTACKING)) - continue; - } - let alreadyBombing = false; - for (const unitIds of this.bombingAttacks.values()) - { - if (!unitIds.has(ent.id())) - continue; - alreadyBombing = true; - break; - } - if (alreadyBombing) - break; - - const range = ent.attackRange("Ranged").max; - const entPos = ent.position(); - const access = getLandAccess(gameState, ent); - for (const struct of gameState.getEnemyStructures().values()) - { - if (!ent.canAttackTarget(struct, allowCapture(gameState, ent, struct))) - continue; - - const structPos = struct.position(); - let x; - let z; - if (struct.hasClass("Field")) - { - if (!struct.resourceSupplyNumGatherers() || - !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos))) - continue; - } - const dist = VectorDistance(entPos, structPos); - if (dist > range) - { - const safety = struct.footprintRadius() + 30; - x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist; - z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist; - const owner = gameState.ai.HQ.territoryMap.getOwner([x, z]); - if (owner != 0 && gameState.isPlayerEnemy(owner)) - continue; - x = structPos[0] + (entPos[0] - structPos[0]) * range / dist; - z = structPos[1] + (entPos[1] - structPos[1]) * range / dist; - if (gameState.ai.HQ.territoryMap.getOwner([x, z]) != PlayerID || - gameState.ai.accessibility.getAccessValue([x, z]) != access) - continue; - } - let attackingUnits; for (const [targetId, unitIds] of this.bombingAttacks) { - if (targetId != struct.id()) + if (targetId == evt.entity) + { + this.bombingAttacks.set(evt.newentity, unitIds); + this.bombingAttacks.delete(evt.entity); + } + else if (unitIds.has(evt.entity)) + { + unitIds.add(evt.newentity); + unitIds.delete(evt.entity); + } + } + } + } + + /** + * Check for any structure in range from within our territory, and bomb it + */ + assignBombers(gameState) + { + // First some cleaning of current bombing attacks + for (const [targetId, unitIds] of this.bombingAttacks) + { + const target = gameState.getEntityById(targetId); + if (!target || !gameState.isPlayerEnemy(target.owner())) + this.bombingAttacks.delete(targetId); + else + { + for (const entId of unitIds.values()) + { + const ent = gameState.getEntityById(entId); + if (ent && ent.owner() == PlayerID) + { + const plan = ent.getMetadata(PlayerID, "plan"); + const orders = ent.unitAIOrderData(); + const lastOrder = orders && orders.length ? orders[orders.length-1] : null; + if (lastOrder && lastOrder.target && lastOrder.target == targetId && plan != -2 && plan != -3) + continue; + } + unitIds.delete(entId); + } + if (!unitIds.size) + this.bombingAttacks.delete(targetId); + } + } + + const bombers = gameState.updatingCollection("bombers", + filters.byClasses(["BoltShooter", "StoneThrower"]), gameState.getOwnUnits()); + for (const ent of bombers.values()) + { + if (!ent.position() || !ent.isIdle() || !ent.attackRange("Ranged")) + continue; + if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) + continue; + if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) + { + const subrole = ent.getMetadata(PlayerID, "subrole"); + if (subrole && (subrole === Worker.SUBROLE_COMPLETING || + subrole === Worker.SUBROLE_WALKING || subrole === Worker.SUBROLE_ATTACKING)) continue; - attackingUnits = unitIds; + } + let alreadyBombing = false; + for (const unitIds of this.bombingAttacks.values()) + { + if (!unitIds.has(ent.id())) + continue; + alreadyBombing = true; break; } - if (attackingUnits && attackingUnits.size > 4) - continue; // already enough units against that target - if (!attackingUnits) + if (alreadyBombing) + break; + + const range = ent.attackRange("Ranged").max; + const entPos = ent.position(); + const access = getLandAccess(gameState, ent); + for (const struct of gameState.getEnemyStructures().values()) { - attackingUnits = new Set(); - this.bombingAttacks.set(struct.id(), attackingUnits); + if (!ent.canAttackTarget(struct, allowCapture(gameState, ent, struct))) + continue; + + const structPos = struct.position(); + let x; + let z; + if (struct.hasClass("Field")) + { + if (!struct.resourceSupplyNumGatherers() || + !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos))) + continue; + } + const dist = VectorDistance(entPos, structPos); + if (dist > range) + { + const safety = struct.footprintRadius() + 30; + x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist; + z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist; + const owner = gameState.ai.HQ.territoryMap.getOwner([x, z]); + if (owner != 0 && gameState.isPlayerEnemy(owner)) + continue; + x = structPos[0] + (entPos[0] - structPos[0]) * range / dist; + z = structPos[1] + (entPos[1] - structPos[1]) * range / dist; + if (gameState.ai.HQ.territoryMap.getOwner([x, z]) != PlayerID || + gameState.ai.accessibility.getAccessValue([x, z]) != access) + continue; + } + let attackingUnits; + for (const [targetId, unitIds] of this.bombingAttacks) + { + if (targetId != struct.id()) + continue; + attackingUnits = unitIds; + break; + } + if (attackingUnits && attackingUnits.size > 4) + continue; // already enough units against that target + if (!attackingUnits) + { + attackingUnits = new Set(); + this.bombingAttacks.set(struct.id(), attackingUnits); + } + attackingUnits.add(ent.id()); + if (dist > range) + ent.move(x, z); + ent.attack(struct.id(), false, dist > range); + break; } - attackingUnits.add(ent.id()); - if (dist > range) - ent.move(x, z); - ent.attack(struct.id(), false, dist > range); - break; } } -}; -/** - * Some functions are run every turn - * Others once in a while - */ -AttackManager.prototype.update = function(gameState, queues, events) -{ - if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) + /** + * Some functions are run every turn + * Others once in a while + */ + update(gameState, queues, events) { - this.debugTime = gameState.ai.elapsedTime; - aiWarn(" upcoming attacks ================="); + if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) + { + this.debugTime = gameState.ai.elapsedTime; + aiWarn(" upcoming attacks ================="); + for (const attackType in this.upcomingAttacks) + { + for (const attack of this.upcomingAttacks[attackType]) + { + aiWarn(" plan " + attack.name + " type " + attackType + " state " + attack.state + + " units " + attack.unitCollection.length); + } + } + aiWarn(" started attacks =================="); + for (const attackType in this.startedAttacks) + { + for (const attack of this.startedAttacks[attackType]) + { + aiWarn(" plan " + attack.name + " type " + attackType + " state " + attack.state + + " units " + attack.unitCollection.length); + } + } + aiWarn(" =================================="); + } + + this.checkEvents(gameState, events); + const unexecutedAttacks = { + [AttackPlan.TYPE_RUSH]: 0, + [AttackPlan.TYPE_RAID]: 0, + [AttackPlan.TYPE_DEFAULT]: 0, + [AttackPlan.TYPE_HUGE_ATTACK]: 0 + }; for (const attackType in this.upcomingAttacks) { - for (const attack of this.upcomingAttacks[attackType]) + for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) { - aiWarn(" plan " + attack.name + " type " + attackType + " state " + attack.state + - " units " + attack.unitCollection.length); - } - } - aiWarn(" started attacks =================="); - for (const attackType in this.startedAttacks) - { - for (const attack of this.startedAttacks[attackType]) - { - aiWarn(" plan " + attack.name + " type " + attackType + " state " + attack.state + - " units " + attack.unitCollection.length); - } - } - aiWarn(" =================================="); - } + const attack = this.upcomingAttacks[attackType][i]; + attack.checkEvents(gameState, events); - this.checkEvents(gameState, events); - const unexecutedAttacks = { - [AttackPlan.TYPE_RUSH]: 0, - [AttackPlan.TYPE_RAID]: 0, - [AttackPlan.TYPE_DEFAULT]: 0, - [AttackPlan.TYPE_HUGE_ATTACK]: 0 - }; - for (const attackType in this.upcomingAttacks) - { - for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) - { - const attack = this.upcomingAttacks[attackType][i]; - attack.checkEvents(gameState, events); - - if (attack.isStarted()) - { - aiWarn("Petra problem in attackManager: attack in preparation has already " + - "started ???"); - } - - const updateStep = attack.updatePreparation(gameState); - // now we're gonna check if the preparation time is over - if (updateStep === AttackPlan.PREPARATION_KEEP_GOING || attack.isPaused()) - { - // just chillin' - if (attack.state === AttackPlan.STATE_UNEXECUTED) - ++unexecutedAttacks[attackType]; - } - else if (updateStep === AttackPlan.PREPARATION_FAILED) - { - if (this.Config.debug > 1) + if (attack.isStarted()) { - aiWarn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + - " aborted."); + aiWarn("Petra problem in attackManager: attack in preparation has already " + + "started ???"); } - attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - else if (updateStep === AttackPlan.PREPARATION_START) - { - if (attack.StartAttack(gameState)) + + const updateStep = attack.updatePreparation(gameState); + // now we're gonna check if the preparation time is over + if (updateStep === AttackPlan.PREPARATION_KEEP_GOING || attack.isPaused()) + { + // just chillin' + if (attack.state === AttackPlan.STATE_UNEXECUTED) + ++unexecutedAttacks[attackType]; + } + else if (updateStep === AttackPlan.PREPARATION_FAILED) { if (this.Config.debug > 1) { - aiWarn("Attack Manager: Starting " + attack.getType() + " plan " + - attack.getName()); + aiWarn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + + " aborted."); } - if (this.Config.chat) - chat.launchAttack(gameState, attack.targetPlayer, attack.getType()); - this.startedAttacks[attackType].push(attack); - } - else attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - } - } - - for (const attackType in this.startedAttacks) - { - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - const attack = this.startedAttacks[attackType][i]; - attack.checkEvents(gameState, events); - // okay so then we'll update the attack. - if (attack.isPaused()) - continue; - const remaining = attack.update(gameState, events); - if (!remaining) - { - if (this.Config.debug > 1) - { - aiWarn("Military Manager: " + attack.getType() + " plan " + - attack.getName() + " is finished with remaining " + remaining); + this.upcomingAttacks[attackType].splice(i--, 1); } - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); - } - } - } - - // creating plans after updating because an aborted plan might be reused in that case. - - const barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(filters.isBuilt()).length; - if (this.rushNumber < this.maxRushes && barracksNb >= 1) - { - if (unexecutedAttacks[AttackPlan.TYPE_RUSH] === 0) - { - // we have a barracks and we want to rush, rush. - const data = { "targetSize": this.rushSize[this.rushNumber] }; - const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, - AttackPlan.TYPE_RUSH, data, false); - if (!attackPlan.failed) - { - if (this.Config.debug > 1) + else if (updateStep === AttackPlan.PREPARATION_START) { - aiWarn("Military Manager: Rushing plan " + this.totalNumber + - " with maxRushes " + this.maxRushes); + if (attack.StartAttack(gameState)) + { + if (this.Config.debug > 1) + { + aiWarn("Attack Manager: Starting " + attack.getType() + " plan " + + attack.getName()); + } + if (this.Config.chat) + chat.launchAttack(gameState, attack.targetPlayer, attack.getType()); + this.startedAttacks[attackType].push(attack); + } + else + attack.Abort(gameState); + this.upcomingAttacks[attackType].splice(i--, 1); } - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks[AttackPlan.TYPE_RUSH].push(attackPlan); } - this.rushNumber++; } - } - else if (unexecutedAttacks[AttackPlan.TYPE_DEFAULT] == 0 && - unexecutedAttacks[AttackPlan.TYPE_HUGE_ATTACK] == 0 && - this.startedAttacks[AttackPlan.TYPE_DEFAULT].length + - this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length < - Math.min(2, 1 + Math.round(gameState.getPopulationMax() / 100)) && - (this.startedAttacks[AttackPlan.TYPE_DEFAULT].length + - this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length == 0 || - gameState.getPopulationMax() - gameState.getPopulation() > 12)) - { - if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || - !gameState.ai.HQ.hasPotentialBase()) // if we have no base ... nothing else to do than attack + + for (const attackType in this.startedAttacks) { - const type = this.attackNumber < 2 || - this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length > 0 ? - AttackPlan.TYPE_DEFAULT : AttackPlan.TYPE_HUGE_ATTACK; - const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, type, - undefined, false); - if (attackPlan.failed) - this.attackPlansEncounteredWater = true; // hack - else + for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { - if (this.Config.debug > 1) + const attack = this.startedAttacks[attackType][i]; + attack.checkEvents(gameState, events); + // okay so then we'll update the attack. + if (attack.isPaused()) + continue; + const remaining = attack.update(gameState, events); + if (!remaining) { - aiWarn("Military Manager: Creating the plan " + type + " " + - this.totalNumber); + if (this.Config.debug > 1) + { + aiWarn("Military Manager: " + attack.getType() + " plan " + + attack.getName() + " is finished with remaining " + remaining); + } + attack.Abort(gameState); + this.startedAttacks[attackType].splice(i--, 1); } - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks[type].push(attackPlan); } - this.attackNumber++; } - } - if (unexecutedAttacks[AttackPlan.TYPE_RAID] === 0 && - gameState.ai.HQ.defenseManager.targetList.length) - { - let target; - for (const targetId of gameState.ai.HQ.defenseManager.targetList) + // creating plans after updating because an aborted plan might be reused in that case. + + const barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(filters.isBuilt()).length; + if (this.rushNumber < this.maxRushes && barracksNb >= 1) { - target = gameState.getEntityById(targetId); - if (!target) - continue; - if (gameState.isPlayerEnemy(target.owner())) - break; - target = undefined; + if (unexecutedAttacks[AttackPlan.TYPE_RUSH] === 0) + { + // we have a barracks and we want to rush, rush. + const data = { "targetSize": this.rushSize[this.rushNumber] }; + const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, + AttackPlan.TYPE_RUSH, data, false); + if (!attackPlan.failed) + { + if (this.Config.debug > 1) + { + aiWarn("Military Manager: Rushing plan " + this.totalNumber + + " with maxRushes " + this.maxRushes); + } + this.totalNumber++; + attackPlan.init(gameState); + this.upcomingAttacks[AttackPlan.TYPE_RUSH].push(attackPlan); + } + this.rushNumber++; + } } - if (target) // prepare a raid against this target - this.raidTargetEntity(gameState, target); + else if (unexecutedAttacks[AttackPlan.TYPE_DEFAULT] == 0 && + unexecutedAttacks[AttackPlan.TYPE_HUGE_ATTACK] == 0 && + this.startedAttacks[AttackPlan.TYPE_DEFAULT].length + + this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length < + Math.min(2, 1 + Math.round(gameState.getPopulationMax() / 100)) && + (this.startedAttacks[AttackPlan.TYPE_DEFAULT].length + + this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length == 0 || + gameState.getPopulationMax() - gameState.getPopulation() > 12)) + { + if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || + !gameState.ai.HQ.hasPotentialBase()) // if we have no base ... nothing else to do than attack + { + const type = this.attackNumber < 2 || + this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length > 0 ? + AttackPlan.TYPE_DEFAULT : AttackPlan.TYPE_HUGE_ATTACK; + const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, type, + undefined, false); + if (attackPlan.failed) + this.attackPlansEncounteredWater = true; // hack + else + { + if (this.Config.debug > 1) + { + aiWarn("Military Manager: Creating the plan " + type + " " + + this.totalNumber); + } + this.totalNumber++; + attackPlan.init(gameState); + this.upcomingAttacks[type].push(attackPlan); + } + this.attackNumber++; + } + } + + if (unexecutedAttacks[AttackPlan.TYPE_RAID] === 0 && + gameState.ai.HQ.defenseManager.targetList.length) + { + let target; + for (const targetId of gameState.ai.HQ.defenseManager.targetList) + { + target = gameState.getEntityById(targetId); + if (!target) + continue; + if (gameState.isPlayerEnemy(target.owner())) + break; + target = undefined; + } + if (target) // prepare a raid against this target + this.raidTargetEntity(gameState, target); + } + + // Check if we have some unused ranged siege unit which could do something useful while waiting + if (this.Config.difficulty > difficulty.VERY_EASY && gameState.ai.playedTurn % 5 == 0) + this.assignBombers(gameState); } - // Check if we have some unused ranged siege unit which could do something useful while waiting - if (this.Config.difficulty > difficulty.VERY_EASY && gameState.ai.playedTurn % 5 == 0) - this.assignBombers(gameState); -}; - -AttackManager.prototype.getPlan = function(planName) -{ - for (const attackType in this.upcomingAttacks) + getPlan(planName) { - for (const attack of this.upcomingAttacks[attackType]) - if (attack.getName() == planName) - return attack; + for (const attackType in this.upcomingAttacks) + { + for (const attack of this.upcomingAttacks[attackType]) + if (attack.getName() == planName) + return attack; + } + for (const attackType in this.startedAttacks) + { + for (const attack of this.startedAttacks[attackType]) + if (attack.getName() == planName) + return attack; + } + return undefined; } - for (const attackType in this.startedAttacks) + + pausePlan(planName) { - for (const attack of this.startedAttacks[attackType]) - if (attack.getName() == planName) - return attack; - } - return undefined; -}; - -AttackManager.prototype.pausePlan = function(planName) -{ - const attack = this.getPlan(planName); - if (attack) - attack.setPaused(true); -}; - -AttackManager.prototype.unpausePlan = function(planName) -{ - const attack = this.getPlan(planName); - if (attack) - attack.setPaused(false); -}; - -AttackManager.prototype.pauseAllPlans = function() -{ - for (const attackType in this.upcomingAttacks) - for (const attack of this.upcomingAttacks[attackType]) + const attack = this.getPlan(planName); + if (attack) attack.setPaused(true); + } - for (const attackType in this.startedAttacks) - for (const attack of this.startedAttacks[attackType]) - attack.setPaused(true); -}; - -AttackManager.prototype.unpauseAllPlans = function() -{ - for (const attackType in this.upcomingAttacks) - for (const attack of this.upcomingAttacks[attackType]) - attack.setPaused(false); - - for (const attackType in this.startedAttacks) - for (const attack of this.startedAttacks[attackType]) - attack.setPaused(false); -}; - -AttackManager.prototype.getAttackInPreparation = function(type) -{ - return this.upcomingAttacks[type].length ? this.upcomingAttacks[type][0] : undefined; -}; - -/** - * 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. - */ -AttackManager.prototype.getEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - - // First check if there is a preferred enemy based on our victory conditions. - // If both wonder and relic, choose randomly between them TODO should combine decisions - - if (gameState.getVictoryConditions().has("wonder")) - enemyPlayer = this.getWonderEnemyPlayer(gameState, attack); - - if (gameState.getVictoryConditions().has("capture_the_relic")) - if (!enemyPlayer || randBool()) - enemyPlayer = this.getRelicEnemyPlayer(gameState, attack) || enemyPlayer; - - if (enemyPlayer) - return enemyPlayer; - - const veto = {}; - for (const i in this.defeated) - veto[i] = true; - // No rush if enemy too well defended (i.e. iberians) - if (attack.type === AttackPlan.TYPE_RUSH) + unpausePlan(planName) { + const attack = this.getPlan(planName); + if (attack) + attack.setPaused(false); + } + + pauseAllPlans() + { + for (const attackType in this.upcomingAttacks) + for (const attack of this.upcomingAttacks[attackType]) + attack.setPaused(true); + + for (const attackType in this.startedAttacks) + for (const attack of this.startedAttacks[attackType]) + attack.setPaused(true); + } + + unpauseAllPlans() + { + for (const attackType in this.upcomingAttacks) + for (const attack of this.upcomingAttacks[attackType]) + attack.setPaused(false); + + for (const attackType in this.startedAttacks) + for (const attack of this.startedAttacks[attackType]) + attack.setPaused(false); + } + + getAttackInPreparation(type) + { + return this.upcomingAttacks[type].length ? this.upcomingAttacks[type][0] : undefined; + } + + /** + * 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. + */ + getEnemyPlayer(gameState, attack) + { + let enemyPlayer; + + // First check if there is a preferred enemy based on our victory conditions. + // If both wonder and relic, choose randomly between them TODO should combine decisions + + if (gameState.getVictoryConditions().has("wonder")) + enemyPlayer = this.getWonderEnemyPlayer(gameState, attack); + + if (gameState.getVictoryConditions().has("capture_the_relic")) + if (!enemyPlayer || randBool()) + enemyPlayer = this.getRelicEnemyPlayer(gameState, attack) || enemyPlayer; + + if (enemyPlayer) + return enemyPlayer; + + const veto = {}; + for (const i in this.defeated) + veto[i] = true; + // No rush if enemy too well defended (i.e. iberians) + if (attack.type === AttackPlan.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 (const ent of gameState.getEnemyStructures(i).values()) + if (ent.hasClasses(["Tower", "WallTower", "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 !== AttackPlan.TYPE_HUGE_ATTACK) + { + if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && + !this.defeated[this.currentEnemyPlayer] && + gameState.isPlayerEnemy(this.currentEnemyPlayer) && + gameState.getEntities(this.currentEnemyPlayer).hasEntities()) + return this.currentEnemyPlayer; + + let distmin; + let ccmin; + const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); + for (const ourcc of ccEnts.values()) + { + if (ourcc.owner() != PlayerID) + continue; + const ourPos = ourcc.position(); + const access = getLandAccess(gameState, ourcc); + for (const enemycc of ccEnts.values()) + { + if (veto[enemycc.owner()]) + continue; + if (!gameState.isPlayerEnemy(enemycc.owner())) + continue; + if (access !== getLandAccess(gameState, enemycc)) + continue; + const dist = SquareVectorDistance(ourPos, enemycc.position()); + 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 (!gameState.isPlayerEnemy(i) || veto[i]) + if (veto[i]) continue; - if (this.defeated[i]) + if (!gameState.isPlayerEnemy(i)) continue; - let enemyDefense = 0; - for (const ent of gameState.getEnemyStructures(i).values()) - if (ent.hasClasses(["Tower", "WallTower", "Fortress"])) - enemyDefense++; - if (enemyDefense > 6) - veto[i] = true; + let enemyCount = 0; + let enemyCivCentre = false; + for (const ent of gameState.getEntities(i).values()) + { + enemyCount++; + if (ent.hasClass("CivCentre")) + enemyCivCentre = true; + } + if (enemyCivCentre) + enemyCount += 500; + if (!enemyCount || enemyCount < max) + continue; + max = enemyCount; + enemyPlayer = i; } + if (attack.targetPlayer === undefined) + this.currentEnemyPlayer = enemyPlayer; + return enemyPlayer; } - // 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 !== AttackPlan.TYPE_HUGE_ATTACK) + /** + * Target the player with the most advanced wonder. + * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. + */ + getWonderEnemyPlayer(gameState, attack) { - if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && - !this.defeated[this.currentEnemyPlayer] && - gameState.isPlayerEnemy(this.currentEnemyPlayer) && - gameState.getEntities(this.currentEnemyPlayer).hasEntities()) - return this.currentEnemyPlayer; - - let distmin; - let ccmin; - const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); - for (const ourcc of ccEnts.values()) + let enemyPlayer; + let enemyWonder; + let moreAdvanced; + for (const wonder of gameState.getEnemyStructures().filter(filters.byClass("Wonder")).values()) { - if (ourcc.owner() != PlayerID) + if (wonder.owner() == 0) continue; - const ourPos = ourcc.position(); - const access = getLandAccess(gameState, ourcc); - for (const enemycc of ccEnts.values()) + const progress = wonder.foundationProgress(); + if (progress === undefined) { - if (veto[enemycc.owner()]) - continue; - if (!gameState.isPlayerEnemy(enemycc.owner())) - continue; - if (access !== getLandAccess(gameState, enemycc)) - continue; - const dist = SquareVectorDistance(ourPos, enemycc.position()); - if (distmin && dist > distmin) - continue; - ccmin = enemycc; - distmin = dist; + enemyWonder = wonder; + break; } + if (enemyWonder && moreAdvanced > progress) + continue; + enemyWonder = wonder; + moreAdvanced = progress; } - if (ccmin) + if (enemyWonder) { - enemyPlayer = ccmin.owner(); + enemyPlayer = enemyWonder.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; - return 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) + /** + * Target the player with the most relics (including gaia). + */ + getRelicEnemyPlayer(gameState, attack) { - if (veto[i]) - continue; - if (!gameState.isPlayerEnemy(i)) - continue; - let enemyCount = 0; - let enemyCivCentre = false; - for (const ent of gameState.getEntities(i).values()) + let enemyPlayer; + const allRelics = gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic")); + let maxRelicsOwned = 0; + for (let i = 0; i < gameState.sharedScript.playersData.length; ++i) { - enemyCount++; - if (ent.hasClass("CivCentre")) - enemyCivCentre = true; + if (!gameState.isPlayerEnemy(i) || this.defeated[i] || + i == 0 && !gameState.ai.HQ.victoryManager.tryCaptureGaiaRelic) + continue; + + const relicsCount = allRelics.filter(relic => relic.owner() == i).length; + if (relicsCount <= maxRelicsOwned) + continue; + maxRelicsOwned = relicsCount; + enemyPlayer = i; } - if (enemyCivCentre) - enemyCount += 500; - if (!enemyCount || enemyCount < max) - continue; - max = enemyCount; - enemyPlayer = i; - } - if (attack.targetPlayer === undefined) - this.currentEnemyPlayer = enemyPlayer; - return enemyPlayer; -}; - -/** - * Target the player with the most advanced wonder. - * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. - */ -AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - let enemyWonder; - let moreAdvanced; - for (const wonder of gameState.getEnemyStructures().filter(filters.byClass("Wonder")).values()) - { - if (wonder.owner() == 0) - continue; - const progress = wonder.foundationProgress(); - if (progress === undefined) + if (enemyPlayer !== undefined) { - enemyWonder = wonder; - break; + if (attack.targetPlayer === undefined) + this.currentEnemyPlayer = enemyPlayer; + if (enemyPlayer == 0) + gameState.ai.HQ.victoryManager.resetCaptureGaiaRelic(gameState); } - if (enemyWonder && moreAdvanced > progress) - continue; - enemyWonder = wonder; - moreAdvanced = progress; + return enemyPlayer; } - if (enemyWonder) + + /** f.e. if we have changed diplomacy with another player. */ + cancelAttacksAgainstPlayer(gameState, player) { - enemyPlayer = enemyWonder.owner(); - if (attack.targetPlayer === undefined) - this.currentEnemyPlayer = enemyPlayer; - } - return enemyPlayer; -}; + for (const attackType in this.upcomingAttacks) + for (const attack of this.upcomingAttacks[attackType]) + if (attack.targetPlayer === player) + attack.targetPlayer = undefined; -/** - * Target the player with the most relics (including gaia). - */ -AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - const allRelics = gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic")); - let maxRelicsOwned = 0; - for (let i = 0; i < gameState.sharedScript.playersData.length; ++i) - { - if (!gameState.isPlayerEnemy(i) || this.defeated[i] || - i == 0 && !gameState.ai.HQ.victoryManager.tryCaptureGaiaRelic) - continue; - - const relicsCount = allRelics.filter(relic => relic.owner() == i).length; - if (relicsCount <= maxRelicsOwned) - continue; - maxRelicsOwned = relicsCount; - enemyPlayer = i; - } - if (enemyPlayer !== undefined) - { - if (attack.targetPlayer === undefined) - this.currentEnemyPlayer = enemyPlayer; - if (enemyPlayer == 0) - gameState.ai.HQ.victoryManager.resetCaptureGaiaRelic(gameState); - } - return enemyPlayer; -}; - -/** f.e. if we have changed diplomacy with another player. */ -AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player) -{ - for (const attackType in this.upcomingAttacks) - for (const attack of this.upcomingAttacks[attackType]) - if (attack.targetPlayer === player) - attack.targetPlayer = undefined; - - for (const attackType in this.startedAttacks) - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - const attack = this.startedAttacks[attackType][i]; - if (attack.targetPlayer === player) + for (const attackType in this.startedAttacks) + for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); + const attack = this.startedAttacks[attackType][i]; + if (attack.targetPlayer === player) + { + attack.Abort(gameState); + this.startedAttacks[attackType].splice(i--, 1); + } + } + } + + raidTargetEntity(gameState, ent) + { + const data = { "target": ent }; + const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, + AttackPlan.TYPE_RAID, data, false); + if (attackPlan.failed) + return null; + if (this.Config.debug > 1) + aiWarn("Military Manager: Raiding plan " + this.totalNumber); + this.raidNumber++; + this.totalNumber++; + attackPlan.init(gameState); + this.upcomingAttacks[AttackPlan.TYPE_RAID].push(attackPlan); + return attackPlan; + } + + /** + * Return the number of units from any of our attacking armies around this position + */ + numAttackingUnitsAround(pos, dist) + { + let num = 0; + for (const attackType in this.startedAttacks) + for (const attack of this.startedAttacks[attackType]) + { + if (!attack.position) // this attack may be inside a transport + continue; + if (SquareVectorDistance(pos, attack.position) < dist*dist) + num += attack.unitCollection.length; + } + return num; + } + + /** + * Switch defense armies into an attack one against the given target + * data.range: transform all defense armies inside range of the target into a new attack + * data.armyID: transform only the defense army ID into a new attack + * data.uniqueTarget: the attack will stop when the target is destroyed or captured + */ + switchDefenseToAttack(gameState, target, data) + { + if (!target || !target.position()) + return false; + if (!data.range && !data.armyID) + { + aiWarn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data)); + return false; + } + const attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined; + const pos = target.position(); + const attackType = AttackPlan.TYPE_DEFAULT; + const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData, + false); + if (attackPlan.failed) + return false; + this.totalNumber++; + attackPlan.init(gameState); + this.startedAttacks[attackType].push(attackPlan); + + const targetAccess = getLandAccess(gameState, target); + for (const army of gameState.ai.HQ.defenseManager.armies) + { + if (data.range) + { + army.recalculatePosition(gameState); + if (SquareVectorDistance(pos, army.foePosition) > data.range * data.range) + continue; + } + else if (army.ID != +data.armyID) + continue; + + while (army.foeEntities.length > 0) + army.removeFoe(gameState, army.foeEntities[0]); + while (army.ownEntities.length > 0) + { + const unitId = army.ownEntities[0]; + army.removeOwn(gameState, unitId); + const unit = gameState.getEntityById(unitId); + const accessOk = unit.getMetadata(PlayerID, "transport") !== undefined || + unit.position() && getLandAccess(gameState, unit) == targetAccess; + if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit)) + { + unit.setMetadata(PlayerID, "plan", attackPlan.name); + unit.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK); + attackPlan.unitCollection.updateEnt(unit); + } } } -}; - -AttackManager.prototype.raidTargetEntity = function(gameState, ent) -{ - const data = { "target": ent }; - const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, - AttackPlan.TYPE_RAID, data, false); - if (attackPlan.failed) - return null; - if (this.Config.debug > 1) - aiWarn("Military Manager: Raiding plan " + this.totalNumber); - this.raidNumber++; - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks[AttackPlan.TYPE_RAID].push(attackPlan); - return attackPlan; -}; - -/** - * Return the number of units from any of our attacking armies around this position - */ -AttackManager.prototype.numAttackingUnitsAround = function(pos, dist) -{ - let num = 0; - for (const attackType in this.startedAttacks) - for (const attack of this.startedAttacks[attackType]) + if (!attackPlan.unitCollection.hasEntities()) { - if (!attack.position) // this attack may be inside a transport - continue; - if (SquareVectorDistance(pos, attack.position) < dist*dist) - num += attack.unitCollection.length; + attackPlan.Abort(gameState); + return false; } - return num; -}; - -/** - * Switch defense armies into an attack one against the given target - * data.range: transform all defense armies inside range of the target into a new attack - * data.armyID: transform only the defense army ID into a new attack - * data.uniqueTarget: the attack will stop when the target is destroyed or captured - */ -AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data) -{ - if (!target || !target.position()) - return false; - if (!data.range && !data.armyID) - { - aiWarn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data)); - return false; + for (const unit of attackPlan.unitCollection.values()) + unit.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK); + attackPlan.targetPlayer = target.owner(); + attackPlan.targetPos = pos; + attackPlan.target = target; + attackPlan.state = AttackPlan.STATE_ARRIVED; + return true; } - const attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined; - const pos = target.position(); - const attackType = AttackPlan.TYPE_DEFAULT; - const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData, - false); - if (attackPlan.failed) - return false; - this.totalNumber++; - attackPlan.init(gameState); - this.startedAttacks[attackType].push(attackPlan); - const targetAccess = getLandAccess(gameState, target); - for (const army of gameState.ai.HQ.defenseManager.armies) + Serialize() { - if (data.range) - { - army.recalculatePosition(gameState); - if (SquareVectorDistance(pos, army.foePosition) > data.range * data.range) - continue; - } - else if (army.ID != +data.armyID) - continue; + const 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 + }; - while (army.foeEntities.length > 0) - army.removeFoe(gameState, army.foeEntities[0]); - while (army.ownEntities.length > 0) + const upcomingAttacks = {}; + for (const key in this.upcomingAttacks) { - const unitId = army.ownEntities[0]; - army.removeOwn(gameState, unitId); - const unit = gameState.getEntityById(unitId); - const accessOk = unit.getMetadata(PlayerID, "transport") !== undefined || - unit.position() && getLandAccess(gameState, unit) == targetAccess; - if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit)) + upcomingAttacks[key] = []; + for (const attack of this.upcomingAttacks[key]) + upcomingAttacks[key].push(attack.Serialize()); + } + + const startedAttacks = {}; + for (const key in this.startedAttacks) + { + startedAttacks[key] = []; + for (const attack of this.startedAttacks[key]) + startedAttacks[key].push(attack.Serialize()); + } + + return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; + } + + Deserialize(gameState, data) + { + for (const key in data.properties) + this[key] = data.properties[key]; + + this.upcomingAttacks = {}; + for (const key in data.upcomingAttacks) + { + this.upcomingAttacks[key] = []; + for (const dataAttack of data.upcomingAttacks[key]) { - unit.setMetadata(PlayerID, "plan", attackPlan.name); - unit.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK); - attackPlan.unitCollection.updateEnt(unit); + const attack = new AttackPlan(gameState, this.Config, dataAttack.properties.name, + undefined, undefined, true); + attack.Deserialize(gameState, dataAttack); + attack.init(gameState); + this.upcomingAttacks[key].push(attack); + } + } + + this.startedAttacks = {}; + for (const key in data.startedAttacks) + { + this.startedAttacks[key] = []; + for (const dataAttack of data.startedAttacks[key]) + { + const attack = new AttackPlan(gameState, this.Config, dataAttack.properties.name, + undefined, undefined, true); + attack.Deserialize(gameState, dataAttack); + attack.init(gameState); + this.startedAttacks[key].push(attack); } } } - if (!attackPlan.unitCollection.hasEntities()) - { - attackPlan.Abort(gameState); - return false; - } - for (const unit of attackPlan.unitCollection.values()) - unit.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK); - attackPlan.targetPlayer = target.owner(); - attackPlan.targetPos = pos; - attackPlan.target = target; - attackPlan.state = AttackPlan.STATE_ARRIVED; - return true; -}; - -AttackManager.prototype.Serialize = function() -{ - const 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 - }; - - const upcomingAttacks = {}; - for (const key in this.upcomingAttacks) - { - upcomingAttacks[key] = []; - for (const attack of this.upcomingAttacks[key]) - upcomingAttacks[key].push(attack.Serialize()); - } - - const startedAttacks = {}; - for (const key in this.startedAttacks) - { - startedAttacks[key] = []; - for (const attack of this.startedAttacks[key]) - startedAttacks[key].push(attack.Serialize()); - } - - return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; -}; - -AttackManager.prototype.Deserialize = function(gameState, data) -{ - for (const key in data.properties) - this[key] = data.properties[key]; - - this.upcomingAttacks = {}; - for (const key in data.upcomingAttacks) - { - this.upcomingAttacks[key] = []; - for (const dataAttack of data.upcomingAttacks[key]) - { - const attack = new AttackPlan(gameState, this.Config, dataAttack.properties.name, - undefined, undefined, true); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.upcomingAttacks[key].push(attack); - } - } - - this.startedAttacks = {}; - for (const key in data.startedAttacks) - { - this.startedAttacks[key] = []; - for (const dataAttack of data.startedAttacks[key]) - { - const attack = new AttackPlan(gameState, this.Config, dataAttack.properties.name, - undefined, undefined, true); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.startedAttacks[key].push(attack); - } - } -}; +} diff --git a/binaries/data/mods/public/simulation/ai/petra/attackPlan.js b/binaries/data/mods/public/simulation/ai/petra/attackPlan.js index bb6b808d78..9ce6a50dfb 100644 --- a/binaries/data/mods/public/simulation/ai/petra/attackPlan.js +++ b/binaries/data/mods/public/simulation/ai/petra/attackPlan.js @@ -16,1768 +16,1648 @@ import { Worker } from "simulation/ai/petra/worker.js"; * When @c deserialized is true, do not call any random function inside constructor * as that would cause oos. */ -export function AttackPlan(gameState, config, uniqueID, type = AttackPlan.TYPE_DEFAULT, data, deserialized) +export class AttackPlan { - this.Config = config; - this.name = uniqueID; - this.type = type; - this.state = AttackPlan.STATE_UNEXECUTED; - this.forced = false; // true when this attacked has been forced to help an ally - - if (data && data.target) + constructor(gameState, config, uniqueID, type = AttackPlan.TYPE_DEFAULT, data, deserialized) { - this.target = data.target; - this.targetPos = this.target.position(); - this.targetPlayer = this.target.owner(); - } - else - { - this.target = undefined; - this.targetPos = undefined; - this.targetPlayer = undefined; - } + this.Config = config; + this.name = uniqueID; + this.type = type; + this.state = AttackPlan.STATE_UNEXECUTED; + this.forced = false; // true when this attacked has been forced to help an ally - this.uniqueTargetId = data && data.uniqueTargetId || undefined; - - // get a starting rallyPoint ... will be improved later - let rallyPoint; - let rallyAccess; - const allAccesses = {}; - for (const base of gameState.ai.HQ.baseManagers()) - { - if (!base.anchor || !base.anchor.position()) - continue; - const access = getLandAccess(gameState, base.anchor); - if (!rallyPoint) + if (data && data.target) { - rallyPoint = base.anchor.position(); - rallyAccess = access; + this.target = data.target; + this.targetPos = this.target.position(); + this.targetPlayer = this.target.owner(); } - if (!allAccesses[access]) - allAccesses[access] = base.anchor.position(); - } - if (!rallyPoint) // no base ? take the position of any of our entities - { - for (const ent of gameState.getOwnEntities().values()) + else { - if (!ent.position()) - continue; - const access = getLandAccess(gameState, ent); - rallyPoint = ent.position(); - rallyAccess = access; - allAccesses[access] = rallyPoint; - break; - } - if (!rallyPoint) - { - this.failed = true; - return false; - } - } - this.rallyPoint = rallyPoint; - this.overseas = 0; - if (gameState.ai.HQ.navalMap) - { - for (const structure of gameState.getEnemyStructures().values()) - { - if (this.target && structure.id() != this.target.id()) - continue; - if (!structure.position()) - continue; - const access = getLandAccess(gameState, structure); - if (access in allAccesses) - { - this.overseas = 0; - this.rallyPoint = allAccesses[access]; - break; - } - else if (!this.overseas) - { - const sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); - if (!sea) - { - if (this.target) - { - aiWarn("Petra: " + this.type + " " + this.name + " has an inaccessible target " + - this.target.templateName() + " indices " + rallyAccess + " " + access); - this.failed = true; - return false; - } - continue; - } - this.overseas = sea; - gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - } - } - this.paused = false; - this.maxCompletingTime = 0; - - // priority of the queues we'll create. - let priority; - - // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". - // if not, this is a "bonus". The higher the priority, the faster this unit will get built. - // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) - // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built - // only once every other category is at least 50% of its target size. - // note: siege build order is currently added by the military manager if a fortress is there. - this.unitStat = {}; - - // neededShips is the minimal number of ships which should be available for transport - if (type === AttackPlan.TYPE_RUSH) - { - priority = 250; - this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"], - "interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] }; - this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], - "interests": [["strength", 1]] }; - if (data && data.targetSize) - this.unitStat.Infantry.targetSize = data.targetSize; - this.neededShips = 1; - } - else if (type === AttackPlan.TYPE_RAID) - { - priority = 150; - this.unitStat.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], - "interests": [ ["strength", 1] ] }; - this.neededShips = 1; - } - else if (type === AttackPlan.TYPE_HUGE_ATTACK) - { - priority = 90; - // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. - this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Ranged+CitizenSoldier"], - "interests": [["strength", 3]] }; - this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Melee+CitizenSoldier"], - "interests": [["strength", 3]] }; - this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Ranged+Champion"], - "interests": [["strength", 3]] }; - this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Melee+Champion"], - "interests": [["strength", 3]] }; - this.unitStat.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Ranged+CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Melee+CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Ranged+Champion"], - "interests": [["strength", 3]] }; - this.unitStat.ChampMeleeFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Melee+Champion"], - "interests": [["strength", 2]] }; - this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], - "interests": [["strength", 2]] }; - this.neededShips = 5; - } - else - { - priority = 70; - this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Ranged"], - "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; - this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Melee"], - "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; - this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], - "interests": [["strength", 1]] }; - this.neededShips = 3; - } - - // Put some randomness on the attack size - let variation = deserialized ? 1 : randFloat(0.8, 1.2); - // and lower priority and smaller sizes for easier difficulty levels - if (this.Config.difficulty < difficulty.EASY) - { - priority *= 0.4; - variation *= 0.2; - } - else if (this.Config.difficulty < difficulty.MEDIUM) - { - priority *= 0.8; - variation *= 0.6; - } - - if (this.Config.difficulty < difficulty.EASY) - { - for (const cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize); - this.unitStat[cat].minSize = Math.min(this.unitStat[cat].targetSize, Math.min(this.unitStat[cat].minSize, 2)); - this.unitStat[cat].batchSize = this.unitStat[cat].minSize; - } - } - else - { - for (const cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize); - this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize); - } - } - - // change the sizes according to max population - this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips); - for (const cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].targetSize); - this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize); - } - - if (!deserialized) - { - // TODO: there should probably be one queue per type of training building - gameState.ai.queueManager.addQueue("plan_" + this.name, priority); - gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); - gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority); - } - - // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] - this.buildOrders = []; - this.canBuildUnits = gameState.ai.HQ.canBuildUnits; - this.siegeState = AttackPlan.SIEGE_NOT_TESTED; - - // some variables used during the attack - this.position5TurnsAgo = [0, 0]; - this.lastPosition = [0, 0]; - this.position = [0, 0]; - this.isBlocked = false; // true when this attack faces walls - - return true; -} - -AttackPlan.PREPARATION_FAILED = 0; -AttackPlan.PREPARATION_KEEP_GOING = 1; -AttackPlan.PREPARATION_START = 2; - -AttackPlan.SIEGE_NOT_TESTED = 0; -AttackPlan.SIEGE_NO_TRAINER = 1; - -/** - * Siege added in build orders - */ -AttackPlan.SIEGE_ADDED = 2; - -AttackPlan.STATE_UNEXECUTED = "unexecuted"; -AttackPlan.STATE_COMPLETING = "completing"; -AttackPlan.STATE_ARRIVED = "arrived"; - -AttackPlan.TYPE_DEFAULT = "Attack"; -AttackPlan.TYPE_HUGE_ATTACK = "HugeAttack"; -AttackPlan.TYPE_RAID = "Raid"; -AttackPlan.TYPE_RUSH = "Rush"; - -AttackPlan.prototype.init = function(gameState) -{ - this.queue = gameState.ai.queues["plan_" + this.name]; - this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; - this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"]; - - this.unitCollection = gameState.getOwnUnits().filter( - filters.byMetadata(PlayerID, "plan", this.name)); - this.unitCollection.registerUpdates(); - - this.unit = {}; - - // defining the entity collections. Will look for units I own, that are part of this plan. - // Also defining the buildOrders. - for (const cat in this.unitStat) - { - const Unit = this.unitStat[cat]; - this.unit[cat] = this.unitCollection.filter(filters.byClasses(Unit.classes)); - this.unit[cat].registerUpdates(); - if (this.canBuildUnits) - this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); - } -}; - -AttackPlan.prototype.getName = function() -{ - return this.name; -}; - -AttackPlan.prototype.getType = function() -{ - return this.type; -}; - -AttackPlan.prototype.isStarted = function() -{ - return this.state !== AttackPlan.STATE_UNEXECUTED && this.state !== AttackPlan.STATE_COMPLETING; -}; - -AttackPlan.prototype.isPaused = function() -{ - return this.paused; -}; - -AttackPlan.prototype.setPaused = function(boolValue) -{ - this.paused = boolValue; -}; - -/** - * Returns true if the attack can be executed at the current time - * Basically it checks we have enough units. - */ -AttackPlan.prototype.canStart = function() -{ - if (!this.canBuildUnits) - return true; - - for (const unitCat in this.unitStat) - if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) - return false; - - return true; -}; - -AttackPlan.prototype.mustStart = function() -{ - if (this.isPaused()) - return false; - - if (!this.canBuildUnits) - return this.unitCollection.hasEntities(); - - let MaxReachedEverywhere = true; - let MinReachedEverywhere = true; - for (const unitCat in this.unitStat) - { - const Unit = this.unitStat[unitCat]; - if (this.unit[unitCat].length < Unit.targetSize) - MaxReachedEverywhere = false; - if (this.unit[unitCat].length < Unit.minSize) - { - MinReachedEverywhere = false; - break; - } - } - - if (MaxReachedEverywhere) - return true; - if (MinReachedEverywhere) - return this.type === AttackPlan.TYPE_RAID && this.target && this.target.foundationProgress() && - this.target.foundationProgress() > 50; - return false; -}; - -AttackPlan.prototype.forceStart = function() -{ - for (const unitCat in this.unitStat) - { - const Unit = this.unitStat[unitCat]; - Unit.targetSize = 0; - Unit.minSize = 0; - } - this.forced = true; -}; - -AttackPlan.prototype.emptyQueues = function() -{ - this.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); -}; - -AttackPlan.prototype.removeQueues = function(gameState) -{ - gameState.ai.queueManager.removeQueue("plan_" + this.name); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); -}; - -/** Adds a build order. If resetQueue is true, this will reset the queue. */ -AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) -{ - if (!this.isStarted()) - { - // no minsize as we don't want the plan to fail at the last minute though. - this.unitStat[name] = unitStats; - const Unit = this.unitStat[name]; - this.unit[name] = this.unitCollection.filter(filters.byClasses(Unit.classes)); - this.unit[name].registerUpdates(); - this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); - if (resetQueue) - this.emptyQueues(); - } -}; - -AttackPlan.prototype.addSiegeUnits = function(gameState) -{ - if (this.siegeState === AttackPlan.SIEGE_ADDED || this.state !== AttackPlan.STATE_UNEXECUTED) - return false; - - const civ = gameState.getPlayerCiv(); - const classes = [["Siege+Melee"], ["Siege+Ranged"], ["Elephant+Melee"]]; - const hasTrainer = [false, false, false]; - for (const ent of gameState.getOwnTrainingFacilities().values()) - { - const trainables = ent.trainableEntities(civ); - if (!trainables) - continue; - for (const trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - const template = gameState.getTemplate(trainable); - if (!template || !template.available(gameState)) - continue; - for (let i = 0; i < classes.length; ++i) - if (template.hasClasses(classes[i])) - hasTrainer[i] = true; - } - } - if (hasTrainer.every(e => !e)) - { - this.siegeState = AttackPlan.SIEGE_NO_TRAINER; - return false; - } - let i = this.name % classes.length; - for (let k = 0; k < classes.length; ++k) - { - if (hasTrainer[i]) - break; - i = (i + 1) % classes.length; - } - - this.siegeState = AttackPlan.SIEGE_ADDED; - let targetSize; - if (this.Config.difficulty < difficulty.MEDIUM) - targetSize = this.type === AttackPlan.TYPE_HUGE_ATTACK ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); - else - targetSize = this.type === AttackPlan.TYPE_HUGE_ATTACK ? this.Config.difficulty + 1 : this.Config.difficulty - 1; - targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type === AttackPlan.TYPE_HUGE_ATTACK ? 1 : 0); - if (!targetSize) - return true; - // no minsize as we don't want the plan to fail at the last minute though. - const stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2), - "classes": classes[i], "interests": [ ["siegeStrength", 3] ] }; - this.addBuildOrder(gameState, "Siege", stat, true); - return true; -}; - -/** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */ -AttackPlan.prototype.updatePreparation = function(gameState) -{ - // the completing step is used to return resources and regroup the units - // so we check that we have no more forced order before starting the attack - if (this.state === AttackPlan.STATE_COMPLETING) - { - // if our target was destroyed, go back to "unexecuted" state - if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) - { - this.state = AttackPlan.STATE_UNEXECUTED; this.target = undefined; + this.targetPos = undefined; + this.targetPlayer = undefined; } - else - { - // check that all units have finished with their transport if needed - if (this.waitingForTransport()) - return AttackPlan.PREPARATION_KEEP_GOING; - // bloqued units which cannot finish their order should not stop the attack - if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) - return AttackPlan.PREPARATION_KEEP_GOING; - return AttackPlan.PREPARATION_START; - } - } - if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0) - this.debugAttack(); + this.uniqueTargetId = data && data.uniqueTargetId || undefined; - // if we need a transport, wait for some transport ships - if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length) - return AttackPlan.PREPARATION_KEEP_GOING; - - if (this.type !== AttackPlan.TYPE_RAID || !this.forced) // Forced Raids have special purposes (as relic capture) - this.assignUnits(gameState); - if (this.type !== AttackPlan.TYPE_RAID && gameState.ai.HQ.attackManager.getAttackInPreparation(AttackPlan.TYPE_RAID) !== undefined) - this.reassignFastUnit(gameState); // reassign some fast units (if any) to fasten raid preparations - - // Fasten the end game. - if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits()) - { - let totEnemies = 0; - let hasEnemies = false; - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i]) - continue; - hasEnemies = true; - totEnemies += gameState.getEnemyUnits(i).length; - } - if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies) - this.forceStart(); - } - - // special case: if we've reached max pop, and we can start the plan, start it. - if (gameState.getPopulationMax() - gameState.getPopulation() < 5) - { - let lengthMin = 16; - if (gameState.getPopulationMax() < 300) - lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300); - if (this.canStart() || this.unitCollection.length > lengthMin) - { - this.emptyQueues(); - } - else // Abort the plan so that its units will be reassigned to other plans. - { - if (this.Config.debug > 1) - { - const am = gameState.ai.HQ.attackManager; - aiWarn(" attacks upcoming: raid " + - am.upcomingAttacks[AttackPlan.TYPE_RAID].length + - " rush " + am.upcomingAttacks[AttackPlan.TYPE_RUSH].length + - " attack " + am.upcomingAttacks[AttackPlan.TYPE_DEFAULT].length + - " huge " + am.upcomingAttacks[AttackPlan.TYPE_HUGE_ATTACK].length); - aiWarn(" attacks started: raid " + am.startedAttacks[AttackPlan.TYPE_RAID].length + - " rush " + am.startedAttacks[AttackPlan.TYPE_RUSH].length + - " attack " + am.startedAttacks[AttackPlan.TYPE_DEFAULT].length + - " huge " + am.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length); - } - return AttackPlan.PREPARATION_FAILED; - } - } - else if (this.mustStart()) - { - if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) - { - // keep on while the units finish being trained, then we'll start - this.emptyQueues(); - return AttackPlan.PREPARATION_KEEP_GOING; - } - } - else - { - if (this.canBuildUnits) - { - // We still have time left to recruit units and do stuffs. - if (this.siegeState === AttackPlan.SIEGE_NOT_TESTED || - this.siegeState === AttackPlan.SIEGE_NO_TRAINER && gameState.ai.playedTurn % 5 == 0) - this.addSiegeUnits(gameState); - this.trainMoreUnits(gameState); - // may happen if we have no more training facilities and build orders are canceled - if (!this.buildOrders.length) - return AttackPlan.PREPARATION_FAILED; // will abort the plan - } - return AttackPlan.PREPARATION_KEEP_GOING; - } - - // if we're here, it means we must start - this.state = AttackPlan.STATE_COMPLETING; - - // Raids have their predefined target - if (!this.target && !this.chooseTarget(gameState)) - return AttackPlan.PREPARATION_FAILED; - if (!this.overseas) - this.getPathToTarget(gameState); - - if (this.type === AttackPlan.TYPE_RAID) - this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; - else - { - if (this.type === AttackPlan.TYPE_RUSH || this.forced) - this.maxCompletingTime = gameState.ai.elapsedTime + 40; - else - this.maxCompletingTime = gameState.ai.elapsedTime + 60; - // warn our allies so that they can help if possible - if (!this.requested) - Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer }); - } - - // Remove those units which were in a temporary bombing attack - for (const unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) - { - for (const entId of unitIds.values()) - { - const ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) - continue; - unitIds.delete(entId); - ent.stopMoving(); - } - } - - const rallyPoint = this.rallyPoint; - const rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); - for (const ent of this.unitCollection.values()) - { - // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that - if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) - { - ent.setMetadata(PlayerID, "plan", -1); - continue; - } - ent.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK); - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_COMPLETING); - let queued = false; - if (ent.resourceCarrying() && ent.resourceCarrying().length) - queued = returnResources(gameState, ent); - const index = getLandAccess(gameState, ent); - if (index == rallyIndex) - ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint); - } - - // reset all queued units - this.removeQueues(gameState); - return AttackPlan.PREPARATION_KEEP_GOING; -}; - -AttackPlan.prototype.trainMoreUnits = function(gameState) -{ - // let's sort by training advancement, ie 'current size / target size' - // count the number of queued units too. - // substract priority. - for (const order of this.buildOrders) - { - const special = "Plan_" + this.name + "_" + order[4]; - let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); - aQueued += this.queue.countQueuedUnitsWithMetadata("special", special); - aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special); - aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special); - order[0] = order[2].length + aQueued; - } - this.buildOrders.sort((a, b) => - { - let va = a[0]/a[3].targetSize - a[3].priority; - if (a[0] >= a[3].targetSize) - va += 1000; - let vb = b[0]/b[3].targetSize - b[3].priority; - if (b[0] >= b[3].targetSize) - vb += 1000; - - const calcResult = va - vb; - if (calcResult !== 0) - return calcResult; - - if (a[4] < b[4]) - return 1; - if (a[4] > b[4]) - return -1; - return 0; - }); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - { - aiWarn("===================================="); - aiWarn("======== build order for plan " + this.name); - for (const order of this.buildOrders) - { - const specialData = "Plan_"+this.name+"_"+order[4]; - const inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); - const queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); - const queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); - const queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); - aiWarn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + - " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); - } - aiWarn("===================================="); - } - - const firstOrder = this.buildOrders[0]; - if (firstOrder[0] < firstOrder[3].targetSize) - { - // find the actual queue we want - let queue = this.queue; - if (firstOrder[4] == "Siege") - queue = this.queueSiege; - else if (firstOrder[3].classes.indexOf("Hero") != -1) - queue = this.queueSiege; - else if (firstOrder[3].classes.indexOf("Champion") != -1) - queue = this.queueChamp; - - if (queue.length() <= 5) - { - const template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests); - // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, - // effectively removing the unit from the plan. - if (template === undefined) - { - if (this.Config.debug > 1) - aiWarn("attack no template found " + firstOrder[1]); - delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat. - this.buildOrders.splice(0, 1); - } - else - { - if (this.Config.debug > 2) - aiWarn("attack template " + template + " added for plan " + this.name); - const max = firstOrder[3].batchSize; - const specialData = "Plan_" + this.name + "_" + firstOrder[4]; - const data = { "plan": this.name, "special": specialData, "base": 0 }; - data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? - Worker.ROLE_WORKER : Worker.ROLE_ATTACK; - const trainingPlan = new TrainingPlan(gameState, template, data, max, max); - if (trainingPlan.template) - queue.addPlan(trainingPlan); - else if (this.Config.debug > 1) - { - aiWarn("training plan canceled because no template for " + template + - " build1 " + uneval(firstOrder[1]) + - " build3 " + uneval(firstOrder[3].interests)); - } - } - } - } -}; - -AttackPlan.prototype.assignUnits = function(gameState) -{ - const plan = this.name; - let added = false; - // If we can not build units, assign all available except those affected to allied defense to the current attack. - if (!this.canBuildUnits) - { - for (const ent of gameState.getOwnUnits().values()) - { - if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent)) - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - return added; - } - - if (this.type === AttackPlan.TYPE_RAID) - { - // Raids are quick attacks: assign all FastMoving soldiers except some for hunting. - let num = 0; - for (const ent of gameState.getOwnUnits().values()) - { - if (!ent.hasClass("FastMoving") || !this.isAvailableUnit(gameState, ent)) - continue; - if (num++ < 2) - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - return added; - } - - // Assign all units without specific role. - for (const ent of gameState.getOwnEntitiesByRole(undefined, true).values()) - { - if (ent.hasClasses(["!Unit", "Ship", "Support"]) || - !this.isAvailableUnit(gameState, ent) || - ent.attackTypes() === undefined) - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - // Add units previously in a plan, but which left it because needed for defense or attack finished. - for (const ent of gameState.ai.HQ.attackManager.outOfPlan.values()) - { - if (!this.isAvailableUnit(gameState, ent)) - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - - // Finally add also some workers for the higher difficulties, - // If Rush, assign all kind of workers, keeping only a minimum number of defenders - // Otherwise, assign only some idle workers if too much of them - if (this.Config.difficulty <= difficulty.EASY) - return added; - - let num = 0; - const numbase = {}; - let keep = this.type !== AttackPlan.TYPE_RUSH ? - 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; - keep = Math.round(this.Config.popScaling * keep); - for (const ent of gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).values()) - { - if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) - continue; - const baseID = ent.getMetadata(PlayerID, "base"); - if (baseID) - numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; - else - { - aiWarn("Petra problem ent without base "); - dumpEntity(ent); - continue; - } - if (num++ < keep || numbase[baseID] < 5) - continue; - if (this.type !== AttackPlan.TYPE_RUSH && ent.getMetadata(PlayerID, "subrole") !== Worker.SUBROLE_IDLE) - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - return added; -}; - -AttackPlan.prototype.isAvailableUnit = function(gameState, ent) -{ - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 || - ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) - return false; - if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8)) - return false; - return true; -}; - -/** Reassign one (at each turn) FastMoving unit to fasten raid preparation. */ -AttackPlan.prototype.reassignFastUnit = function(gameState) -{ - for (const ent of this.unitCollection.values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - if (!ent.hasClasses(["FastMoving", "CitizenSoldier"])) - continue; - const raid = gameState.ai.HQ.attackManager.getAttackInPreparation(AttackPlan.TYPE_RAID); - ent.setMetadata(PlayerID, "plan", raid.name); - this.unitCollection.updateEnt(ent); - raid.unitCollection.updateEnt(ent); - return; - } -}; - -AttackPlan.prototype.chooseTarget = function(gameState) -{ - if (this.targetPlayer === undefined) - { - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - if (this.targetPlayer === undefined) - return false; - } - - this.target = this.getNearestTarget(gameState, this.rallyPoint); - if (!this.target) - { - if (this.uniqueTargetId) - return false; - - // may-be all our previous enemey target (if not recomputed here) have been destroyed ? - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - if (this.targetPlayer !== undefined) - this.target = this.getNearestTarget(gameState, this.rallyPoint); - if (!this.target) - return false; - } - this.targetPos = this.target.position(); - // redefine a new rally point for this target if we have a base on the same land - // find a new one on the pseudo-nearest base (dist weighted by the size of the island) - const targetIndex = getLandAccess(gameState, this.target); - let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - if (targetIndex != rallyIndex) - { - let distminSame = Math.min(); - let rallySame; - let distminDiff = Math.min(); - let rallyDiff; + // get a starting rallyPoint ... will be improved later + let rallyPoint; + let rallyAccess; + const allAccesses = {}; for (const base of gameState.ai.HQ.baseManagers()) { - const anchor = base.anchor; - if (!anchor || !anchor.position()) + if (!base.anchor || !base.anchor.position()) continue; - let dist = SquareVectorDistance(anchor.position(), this.targetPos); - if (base.accessIndex == targetIndex) + const access = getLandAccess(gameState, base.anchor); + if (!rallyPoint) { - if (dist >= distminSame) - continue; - distminSame = dist; - rallySame = anchor.position(); - } - else - { - dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]); - if (dist >= distminDiff) - continue; - distminDiff = dist; - rallyDiff = anchor.position(); + rallyPoint = base.anchor.position(); + rallyAccess = access; } + if (!allAccesses[access]) + allAccesses[access] = base.anchor.position(); } - - if (rallySame) + if (!rallyPoint) // no base ? take the position of any of our entities { - this.rallyPoint = rallySame; - this.overseas = 0; - } - else if (rallyDiff) - { - rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff); - this.rallyPoint = rallyDiff; - const sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); - if (sea) + for (const ent of gameState.getOwnEntities().values()) { - this.overseas = sea; - gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); + if (!ent.position()) + continue; + const access = getLandAccess(gameState, ent); + rallyPoint = ent.position(); + rallyAccess = access; + allAccesses[access] = rallyPoint; + break; } - else + if (!rallyPoint) { - aiWarn("Petra: " + this.type + " " + this.name + " has an inaccessible target" + - " with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName()); + this.failed = true; return false; } } - } - else if (this.overseas) + this.rallyPoint = rallyPoint; this.overseas = 0; - - return true; -}; -/** - * sameLand true means that we look for a target for which we do not need to take a transport - */ -AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand) -{ - this.isBlocked = false; - // Temporary variables needed by isValidTarget - this.gameState = gameState; - this.sameLand = sameLand && sameLand > 1 ? sameLand : false; - - let targets; - if (this.uniqueTargetId) - { - targets = new EntityCollection(gameState.sharedScript); - const ent = gameState.getEntityById(this.uniqueTargetId); - if (ent) - targets.addEnt(ent); - } - else - { - if (this.type === AttackPlan.TYPE_RAID) - targets = this.raidTargetFinder(gameState); - else if (this.type === AttackPlan.TYPE_RUSH || this.type === AttackPlan.TYPE_DEFAULT) + if (gameState.ai.HQ.navalMap) { - targets = this.rushTargetFinder(gameState, this.targetPlayer); - if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced)) - targets = this.defaultTargetFinder(gameState, this.targetPlayer); + for (const structure of gameState.getEnemyStructures().values()) + { + if (this.target && structure.id() != this.target.id()) + continue; + if (!structure.position()) + continue; + const access = getLandAccess(gameState, structure); + if (access in allAccesses) + { + this.overseas = 0; + this.rallyPoint = allAccesses[access]; + break; + } + else if (!this.overseas) + { + const sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); + if (!sea) + { + if (this.target) + { + aiWarn("Petra: " + this.type + " " + this.name + " has an inaccessible target " + + this.target.templateName() + " indices " + rallyAccess + " " + access); + this.failed = true; + return false; + } + continue; + } + this.overseas = sea; + gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1); + } + } + } + this.paused = false; + this.maxCompletingTime = 0; + + // priority of the queues we'll create. + let priority; + + // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". + // if not, this is a "bonus". The higher the priority, the faster this unit will get built. + // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) + // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built + // only once every other category is at least 50% of its target size. + // note: siege build order is currently added by the military manager if a fortress is there. + this.unitStat = {}; + + // neededShips is the minimal number of ships which should be available for transport + if (type === AttackPlan.TYPE_RUSH) + { + priority = 250; + this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"], + "interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] }; + this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], + "interests": [["strength", 1]] }; + if (data && data.targetSize) + this.unitStat.Infantry.targetSize = data.targetSize; + this.neededShips = 1; + } + else if (type === AttackPlan.TYPE_RAID) + { + priority = 150; + this.unitStat.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], + "interests": [ ["strength", 1] ] }; + this.neededShips = 1; + } + else if (type === AttackPlan.TYPE_HUGE_ATTACK) + { + priority = 90; + // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. + this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Ranged+CitizenSoldier"], + "interests": [["strength", 3]] }; + this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Melee+CitizenSoldier"], + "interests": [["strength", 3]] }; + this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Ranged+Champion"], + "interests": [["strength", 3]] }; + this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Melee+Champion"], + "interests": [["strength", 3]] }; + this.unitStat.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Ranged+CitizenSoldier"], + "interests": [["strength", 2]] }; + this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Melee+CitizenSoldier"], + "interests": [["strength", 2]] }; + this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Ranged+Champion"], + "interests": [["strength", 3]] }; + this.unitStat.ChampMeleeFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Melee+Champion"], + "interests": [["strength", 2]] }; + this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], + "interests": [["strength", 2]] }; + this.neededShips = 5; } else - targets = this.defaultTargetFinder(gameState, this.targetPlayer); - } - if (!targets.hasEntities()) - return undefined; - - // picking the nearest target - let target; - let minDist = Math.min(); - for (const ent of targets.values()) - { - if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && - (!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id()))) - continue; - // Do not bother with some pointless targets - if (!this.isValidTarget(ent)) - continue; - let dist = SquareVectorDistance(ent.position(), position); - // In normal attacks, disfavor fields - if (this.type !== AttackPlan.TYPE_RUSH && this.type !== AttackPlan.TYPE_RAID && ent.hasClass("Field")) - dist += 100000; - if (dist < minDist) { - minDist = dist; - target = ent; + priority = 70; + this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Ranged"], + "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; + this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Melee"], + "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; + this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], + "interests": [["strength", 1]] }; + this.neededShips = 3; + } + + // Put some randomness on the attack size + let variation = deserialized ? 1 : randFloat(0.8, 1.2); + // and lower priority and smaller sizes for easier difficulty levels + if (this.Config.difficulty < difficulty.EASY) + { + priority *= 0.4; + variation *= 0.2; + } + else if (this.Config.difficulty < difficulty.MEDIUM) + { + priority *= 0.8; + variation *= 0.6; + } + + if (this.Config.difficulty < difficulty.EASY) + { + for (const cat in this.unitStat) + { + this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize); + this.unitStat[cat].minSize = Math.min(this.unitStat[cat].targetSize, Math.min(this.unitStat[cat].minSize, 2)); + this.unitStat[cat].batchSize = this.unitStat[cat].minSize; + } + } + else + { + for (const cat in this.unitStat) + { + this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize); + this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize); + } + } + + // change the sizes according to max population + this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips); + for (const cat in this.unitStat) + { + this.unitStat[cat].targetSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].targetSize); + this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize); + } + + if (!deserialized) + { + // TODO: there should probably be one queue per type of training building + gameState.ai.queueManager.addQueue("plan_" + this.name, priority); + gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); + gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority); + } + + // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] + this.buildOrders = []; + this.canBuildUnits = gameState.ai.HQ.canBuildUnits; + this.siegeState = AttackPlan.SIEGE_NOT_TESTED; + + // some variables used during the attack + this.position5TurnsAgo = [0, 0]; + this.lastPosition = [0, 0]; + this.position = [0, 0]; + this.isBlocked = false; // true when this attack faces walls + + return true; + } + + static PREPARATION_FAILED = 0; + static PREPARATION_KEEP_GOING = 1; + static PREPARATION_START = 2; + + static SIEGE_NOT_TESTED = 0; + static SIEGE_NO_TRAINER = 1; + + /** + * Siege added in build orders + */ + static SIEGE_ADDED = 2; + + static STATE_UNEXECUTED = "unexecuted"; + static STATE_COMPLETING = "completing"; + static STATE_ARRIVED = "arrived"; + + static TYPE_DEFAULT = "Attack"; + static TYPE_HUGE_ATTACK = "HugeAttack"; + static TYPE_RAID = "Raid"; + static TYPE_RUSH = "Rush"; + + init(gameState) + { + this.queue = gameState.ai.queues["plan_" + this.name]; + this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; + this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"]; + + this.unitCollection = gameState.getOwnUnits().filter( + filters.byMetadata(PlayerID, "plan", this.name)); + this.unitCollection.registerUpdates(); + + this.unit = {}; + + // defining the entity collections. Will look for units I own, that are part of this plan. + // Also defining the buildOrders. + for (const cat in this.unitStat) + { + const Unit = this.unitStat[cat]; + this.unit[cat] = this.unitCollection.filter(filters.byClasses(Unit.classes)); + this.unit[cat].registerUpdates(); + if (this.canBuildUnits) + this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); } } - if (!target) - return undefined; - // Check that we can reach this target - target = this.checkTargetObstruction(gameState, target, position); - - if (!target) - return undefined; - if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic")) - gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]); - // Rushes can change their enemy target if nothing found with the preferred enemy - // Obstruction also can change the enemy target - this.targetPlayer = target.owner(); - return target; -}; - -/** - * Default target finder aims for conquest critical targets - * We must apply the *same* selection (isValidTarget) as done in getNearestTarget - */ -AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) -{ - let targets = new EntityCollection(gameState.sharedScript); - if (gameState.getVictoryConditions().has("wonder")) + getName() { - for (const ent of - gameState.getEnemyStructures(playerEnemy).filter(filters.byClass("Wonder")).values()) + return this.name; + } + + getType() + { + return this.type; + } + + isStarted() + { + return this.state !== AttackPlan.STATE_UNEXECUTED && this.state !== AttackPlan.STATE_COMPLETING; + } + + isPaused() + { + return this.paused; + } + + setPaused(boolValue) + { + this.paused = boolValue; + } + + /** + * Returns true if the attack can be executed at the current time + * Basically it checks we have enough units. + */ + canStart() + { + if (!this.canBuildUnits) + return true; + + for (const unitCat in this.unitStat) + if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) + return false; + + return true; + } + + mustStart() + { + if (this.isPaused()) + return false; + + if (!this.canBuildUnits) + return this.unitCollection.hasEntities(); + + let MaxReachedEverywhere = true; + let MinReachedEverywhere = true; + for (const unitCat in this.unitStat) { - targets.addEnt(ent); + const Unit = this.unitStat[unitCat]; + if (this.unit[unitCat].length < Unit.targetSize) + MaxReachedEverywhere = false; + if (this.unit[unitCat].length < Unit.minSize) + { + MinReachedEverywhere = false; + break; + } } - } - if (gameState.getVictoryConditions().has("regicide")) - { - for (const ent of gameState.getEnemyUnits(playerEnemy).filter(filters.byClass("Hero")) - .values()) - { - targets.addEnt(ent); - } - } - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - for (const ent of gameState.updatingGlobalCollection("allRelics", - filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) - { - targets.addEnt(ent); - } - } - targets = targets.filter(this.isValidTarget, this); - if (targets.hasEntities()) - return targets; - const validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); - targets = validTargets.filter(filters.byClass("CivCentre")); - if (!targets.hasEntities()) - targets = validTargets.filter(filters.byClass("ConquestCritical")); - // If there's nothing, attack anything else that's less critical - if (!targets.hasEntities()) - targets = validTargets.filter(filters.byClass("Town")); - if (!targets.hasEntities()) - targets = validTargets.filter(filters.byClass("Village")); - // No buildings, attack anything conquest critical, units included. - // TODO Should add naval attacks against the last remaining ships. - if (!targets.hasEntities()) - { - targets = gameState.getEntities(playerEnemy).filter(filters.byClass("ConquestCritical")) - .filter(filters.not(filters.byClass("Ship"))); - } - return targets; -}; - -AttackPlan.prototype.isValidTarget = function(ent) -{ - if (!ent.position()) + if (MaxReachedEverywhere) + return true; + if (MinReachedEverywhere) + return this.type === AttackPlan.TYPE_RAID && this.target && this.target.foundationProgress() && + this.target.foundationProgress() > 50; return false; - if (this.sameLand && getLandAccess(this.gameState, ent) != this.sameLand) - return false; - return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length; -}; + } -/** Rush target finder aims at isolated non-defended buildings */ -AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) -{ - let targets = new EntityCollection(gameState.sharedScript); - let buildings; - if (playerEnemy !== undefined) - buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray(); - else - buildings = gameState.getEnemyStructures().toEntityArray(); - if (!buildings.length) - return targets; - - this.position = this.unitCollection.getCentrePosition(); - if (!this.position) - this.position = this.rallyPoint; - - let target; - let minDist = Math.min(); - for (const building of buildings) + forceStart() { - if (building.owner() == 0) - continue; - if (building.hasDefensiveFire()) - continue; - if (!this.isValidTarget(building)) - continue; - const pos = building.position(); - let defended = false; - for (const defense of buildings) + for (const unitCat in this.unitStat) { - if (!defense.hasDefensiveFire()) + const Unit = this.unitStat[unitCat]; + Unit.targetSize = 0; + Unit.minSize = 0; + } + this.forced = true; + } + + emptyQueues() + { + this.queue.empty(); + this.queueChamp.empty(); + this.queueSiege.empty(); + } + + removeQueues(gameState) + { + gameState.ai.queueManager.removeQueue("plan_" + this.name); + gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); + gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); + } + + /** Adds a build order. If resetQueue is true, this will reset the queue. */ + addBuildOrder(gameState, name, unitStats, resetQueue) + { + if (!this.isStarted()) + { + // no minsize as we don't want the plan to fail at the last minute though. + this.unitStat[name] = unitStats; + const Unit = this.unitStat[name]; + this.unit[name] = this.unitCollection.filter(filters.byClasses(Unit.classes)); + this.unit[name].registerUpdates(); + this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); + if (resetQueue) + this.emptyQueues(); + } + } + + addSiegeUnits(gameState) + { + if (this.siegeState === AttackPlan.SIEGE_ADDED || this.state !== AttackPlan.STATE_UNEXECUTED) + return false; + + const civ = gameState.getPlayerCiv(); + const classes = [["Siege+Melee"], ["Siege+Ranged"], ["Elephant+Melee"]]; + const hasTrainer = [false, false, false]; + for (const ent of gameState.getOwnTrainingFacilities().values()) + { + const trainables = ent.trainableEntities(civ); + if (!trainables) continue; - const dist = SquareVectorDistance(pos, defense.position()); - if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 + for (const trainable of trainables) { - defended = true; - break; + if (gameState.isTemplateDisabled(trainable)) + continue; + const template = gameState.getTemplate(trainable); + if (!template || !template.available(gameState)) + continue; + for (let i = 0; i < classes.length; ++i) + if (template.hasClasses(classes[i])) + hasTrainer[i] = true; } } - if (defended) - continue; - const dist = SquareVectorDistance(pos, this.position); - if (dist > minDist) - continue; - minDist = dist; - target = building; - } - if (target) - targets.addEnt(target); - - if (!targets.hasEntities() && this.type === AttackPlan.TYPE_RUSH && playerEnemy) - targets = this.rushTargetFinder(gameState); - - return targets; -}; - -/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ -AttackPlan.prototype.raidTargetFinder = function(gameState) -{ - const targets = new EntityCollection(gameState.sharedScript); - for (const targetId of gameState.ai.HQ.defenseManager.targetList) - { - const target = gameState.getEntityById(targetId); - if (target && target.position()) - targets.addEnt(target); - } - return targets; -}; - -/** - * Check that we can have a path to this target - * otherwise we may be blocked by walls and try to react accordingly - * This is done only when attacker and target are on the same land - */ -AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) -{ - if (getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) - return target; - - const targetPos = target.position(); - const startPos = { "x": position[0], "y": position[1] }; - const endPos = { "x": targetPos[0], "y": targetPos[1] }; - let blocker; - const path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); - if (!path.length) - return undefined; - - const pathPos = [path[0].x, path[0].y]; - const dist = VectorDistance(pathPos, targetPos); - const radius = target.obstructionRadius().max; - for (const struct of gameState.getEnemyStructures().values()) - { - if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field")) - continue; - // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate - if (dist < radius + 10 && !struct.hasClass("Gate")) - continue; - // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m - // in the target direction would bring us inside its obstruction. - const structPos = struct.position(); - const x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; - const y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; - - if (struct.get("Obstruction/Static")) + if (hasTrainer.every(e => !e)) { - if (!struct.angle()) - continue; - const angle = struct.angle(); - const width = +struct.get("Obstruction/Static/@width"); - const depth = +struct.get("Obstruction/Static/@depth"); - const cosa = Math.cos(angle); - const sina = Math.sin(angle); - const u = x * cosa - y * sina; - const v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) - { - blocker = struct; + this.siegeState = AttackPlan.SIEGE_NO_TRAINER; + return false; + } + let i = this.name % classes.length; + for (let k = 0; k < classes.length; ++k) + { + if (hasTrainer[i]) break; + i = (i + 1) % classes.length; + } + + this.siegeState = AttackPlan.SIEGE_ADDED; + let targetSize; + if (this.Config.difficulty < difficulty.MEDIUM) + targetSize = this.type === AttackPlan.TYPE_HUGE_ATTACK ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); + else + targetSize = this.type === AttackPlan.TYPE_HUGE_ATTACK ? this.Config.difficulty + 1 : this.Config.difficulty - 1; + targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type === AttackPlan.TYPE_HUGE_ATTACK ? 1 : 0); + if (!targetSize) + return true; + // no minsize as we don't want the plan to fail at the last minute though. + const stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2), + "classes": classes[i], "interests": [ ["siegeStrength", 3] ] }; + this.addBuildOrder(gameState, "Siege", stat, true); + return true; + } + + /** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */ + updatePreparation(gameState) + { + // the completing step is used to return resources and regroup the units + // so we check that we have no more forced order before starting the attack + if (this.state === AttackPlan.STATE_COMPLETING) + { + // if our target was destroyed, go back to "unexecuted" state + if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) + { + this.state = AttackPlan.STATE_UNEXECUTED; + this.target = undefined; + } + else + { + // check that all units have finished with their transport if needed + if (this.waitingForTransport()) + return AttackPlan.PREPARATION_KEEP_GOING; + // bloqued units which cannot finish their order should not stop the attack + if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) + return AttackPlan.PREPARATION_KEEP_GOING; + return AttackPlan.PREPARATION_START; } } - else if (struct.get("Obstruction/Obstructions")) + + if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0) + this.debugAttack(); + + // if we need a transport, wait for some transport ships + if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length) + return AttackPlan.PREPARATION_KEEP_GOING; + + if (this.type !== AttackPlan.TYPE_RAID || !this.forced) // Forced Raids have special purposes (as relic capture) + this.assignUnits(gameState); + if (this.type !== AttackPlan.TYPE_RAID && gameState.ai.HQ.attackManager.getAttackInPreparation(AttackPlan.TYPE_RAID) !== undefined) + this.reassignFastUnit(gameState); // reassign some fast units (if any) to fasten raid preparations + + // Fasten the end game. + if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits()) { - if (!struct.angle()) - continue; - const angle = struct.angle(); - let width = +struct.get("Obstruction/Obstructions/Door/@width"); - let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); - const doorHalfWidth = width / 2; - width += +struct.get("Obstruction/Obstructions/Left/@width"); - depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth")); - width += +struct.get("Obstruction/Obstructions/Right/@width"); - depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth")); - const cosa = Math.cos(angle); - const sina = Math.sin(angle); - const u = x * cosa - y * sina; - const v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) + let totEnemies = 0; + let hasEnemies = false; + for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { - blocker = struct; - break; + if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i]) + continue; + hasEnemies = true; + totEnemies += gameState.getEnemyUnits(i).length; } - // check that the path does not cross this gate (could happen if not locked) - for (let i = 1; i < path.length; ++i) + if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies) + this.forceStart(); + } + + // special case: if we've reached max pop, and we can start the plan, start it. + if (gameState.getPopulationMax() - gameState.getPopulation() < 5) + { + let lengthMin = 16; + if (gameState.getPopulationMax() < 300) + lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300); + if (this.canStart() || this.unitCollection.length > lengthMin) { - const u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; - const v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; - const u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; - const v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; - if (v1 * v2 < 0) + this.emptyQueues(); + } + else // Abort the plan so that its units will be reassigned to other plans. + { + if (this.Config.debug > 1) { - const u0 = (u1*v2 - u2*v1) / (v2-v1); - if (Math.abs(u0) > doorHalfWidth) + const am = gameState.ai.HQ.attackManager; + aiWarn(" attacks upcoming: raid " + + am.upcomingAttacks[AttackPlan.TYPE_RAID].length + + " rush " + am.upcomingAttacks[AttackPlan.TYPE_RUSH].length + + " attack " + am.upcomingAttacks[AttackPlan.TYPE_DEFAULT].length + + " huge " + am.upcomingAttacks[AttackPlan.TYPE_HUGE_ATTACK].length); + aiWarn(" attacks started: raid " + am.startedAttacks[AttackPlan.TYPE_RAID].length + + " rush " + am.startedAttacks[AttackPlan.TYPE_RUSH].length + + " attack " + am.startedAttacks[AttackPlan.TYPE_DEFAULT].length + + " huge " + am.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length); + } + return AttackPlan.PREPARATION_FAILED; + } + } + else if (this.mustStart()) + { + if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) + { + // keep on while the units finish being trained, then we'll start + this.emptyQueues(); + return AttackPlan.PREPARATION_KEEP_GOING; + } + } + else + { + if (this.canBuildUnits) + { + // We still have time left to recruit units and do stuffs. + if (this.siegeState === AttackPlan.SIEGE_NOT_TESTED || + this.siegeState === AttackPlan.SIEGE_NO_TRAINER && gameState.ai.playedTurn % 5 == 0) + this.addSiegeUnits(gameState); + this.trainMoreUnits(gameState); + // may happen if we have no more training facilities and build orders are canceled + if (!this.buildOrders.length) + return AttackPlan.PREPARATION_FAILED; // will abort the plan + } + return AttackPlan.PREPARATION_KEEP_GOING; + } + + // if we're here, it means we must start + this.state = AttackPlan.STATE_COMPLETING; + + // Raids have their predefined target + if (!this.target && !this.chooseTarget(gameState)) + return AttackPlan.PREPARATION_FAILED; + if (!this.overseas) + this.getPathToTarget(gameState); + + if (this.type === AttackPlan.TYPE_RAID) + this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; + else + { + if (this.type === AttackPlan.TYPE_RUSH || this.forced) + this.maxCompletingTime = gameState.ai.elapsedTime + 40; + else + this.maxCompletingTime = gameState.ai.elapsedTime + 60; + // warn our allies so that they can help if possible + if (!this.requested) + Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer }); + } + + // Remove those units which were in a temporary bombing attack + for (const unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) + { + for (const entId of unitIds.values()) + { + const ent = gameState.getEntityById(entId); + if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) + continue; + unitIds.delete(entId); + ent.stopMoving(); + } + } + + const rallyPoint = this.rallyPoint; + const rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); + for (const ent of this.unitCollection.values()) + { + // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that + if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) + { + ent.setMetadata(PlayerID, "plan", -1); + continue; + } + ent.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK); + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_COMPLETING); + let queued = false; + if (ent.resourceCarrying() && ent.resourceCarrying().length) + queued = returnResources(gameState, ent); + const index = getLandAccess(gameState, ent); + if (index == rallyIndex) + ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); + else + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint); + } + + // reset all queued units + this.removeQueues(gameState); + return AttackPlan.PREPARATION_KEEP_GOING; + } + + trainMoreUnits(gameState) + { + // let's sort by training advancement, ie 'current size / target size' + // count the number of queued units too. + // substract priority. + for (const order of this.buildOrders) + { + const special = "Plan_" + this.name + "_" + order[4]; + let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); + aQueued += this.queue.countQueuedUnitsWithMetadata("special", special); + aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special); + aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special); + order[0] = order[2].length + aQueued; + } + this.buildOrders.sort((a, b) => + { + let va = a[0]/a[3].targetSize - a[3].priority; + if (a[0] >= a[3].targetSize) + va += 1000; + let vb = b[0]/b[3].targetSize - b[3].priority; + if (b[0] >= b[3].targetSize) + vb += 1000; + + const calcResult = va - vb; + if (calcResult !== 0) + return calcResult; + + if (a[4] < b[4]) + return 1; + if (a[4] > b[4]) + return -1; + return 0; + }); + + if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) + { + aiWarn("===================================="); + aiWarn("======== build order for plan " + this.name); + for (const order of this.buildOrders) + { + const specialData = "Plan_"+this.name+"_"+order[4]; + const inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); + const queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); + const queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); + const queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); + aiWarn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + + " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); + } + aiWarn("===================================="); + } + + const firstOrder = this.buildOrders[0]; + if (firstOrder[0] < firstOrder[3].targetSize) + { + // find the actual queue we want + let queue = this.queue; + if (firstOrder[4] == "Siege") + queue = this.queueSiege; + else if (firstOrder[3].classes.indexOf("Hero") != -1) + queue = this.queueSiege; + else if (firstOrder[3].classes.indexOf("Champion") != -1) + queue = this.queueChamp; + + if (queue.length() <= 5) + { + const template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests); + // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, + // effectively removing the unit from the plan. + if (template === undefined) + { + if (this.Config.debug > 1) + aiWarn("attack no template found " + firstOrder[1]); + delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat. + this.buildOrders.splice(0, 1); + } + else + { + if (this.Config.debug > 2) + aiWarn("attack template " + template + " added for plan " + this.name); + const max = firstOrder[3].batchSize; + const specialData = "Plan_" + this.name + "_" + firstOrder[4]; + const data = { "plan": this.name, "special": specialData, "base": 0 }; + data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? + Worker.ROLE_WORKER : Worker.ROLE_ATTACK; + const trainingPlan = new TrainingPlan(gameState, template, data, max, max); + if (trainingPlan.template) + queue.addPlan(trainingPlan); + else if (this.Config.debug > 1) + { + aiWarn("training plan canceled because no template for " + template + + " build1 " + uneval(firstOrder[1]) + + " build3 " + uneval(firstOrder[3].interests)); + } + } + } + } + } + + assignUnits(gameState) + { + const plan = this.name; + let added = false; + // If we can not build units, assign all available except those affected to allied defense to the current attack. + if (!this.canBuildUnits) + { + for (const ent of gameState.getOwnUnits().values()) + { + if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent)) + continue; + ent.setMetadata(PlayerID, "plan", plan); + this.unitCollection.updateEnt(ent); + added = true; + } + return added; + } + + if (this.type === AttackPlan.TYPE_RAID) + { + // Raids are quick attacks: assign all FastMoving soldiers except some for hunting. + let num = 0; + for (const ent of gameState.getOwnUnits().values()) + { + if (!ent.hasClass("FastMoving") || !this.isAvailableUnit(gameState, ent)) + continue; + if (num++ < 2) + continue; + ent.setMetadata(PlayerID, "plan", plan); + this.unitCollection.updateEnt(ent); + added = true; + } + return added; + } + + // Assign all units without specific role. + for (const ent of gameState.getOwnEntitiesByRole(undefined, true).values()) + { + if (ent.hasClasses(["!Unit", "Ship", "Support"]) || + !this.isAvailableUnit(gameState, ent) || + ent.attackTypes() === undefined) + continue; + ent.setMetadata(PlayerID, "plan", plan); + this.unitCollection.updateEnt(ent); + added = true; + } + // Add units previously in a plan, but which left it because needed for defense or attack finished. + for (const ent of gameState.ai.HQ.attackManager.outOfPlan.values()) + { + if (!this.isAvailableUnit(gameState, ent)) + continue; + ent.setMetadata(PlayerID, "plan", plan); + this.unitCollection.updateEnt(ent); + added = true; + } + + // Finally add also some workers for the higher difficulties, + // If Rush, assign all kind of workers, keeping only a minimum number of defenders + // Otherwise, assign only some idle workers if too much of them + if (this.Config.difficulty <= difficulty.EASY) + return added; + + let num = 0; + const numbase = {}; + let keep = this.type !== AttackPlan.TYPE_RUSH ? + 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; + keep = Math.round(this.Config.popScaling * keep); + for (const ent of gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).values()) + { + if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) + continue; + const baseID = ent.getMetadata(PlayerID, "base"); + if (baseID) + numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; + else + { + aiWarn("Petra problem ent without base "); + dumpEntity(ent); + continue; + } + if (num++ < keep || numbase[baseID] < 5) + continue; + if (this.type !== AttackPlan.TYPE_RUSH && ent.getMetadata(PlayerID, "subrole") !== Worker.SUBROLE_IDLE) + continue; + ent.setMetadata(PlayerID, "plan", plan); + this.unitCollection.updateEnt(ent); + added = true; + } + return added; + } + + isAvailableUnit(gameState, ent) + { + if (!ent.position()) + return false; + if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 || + ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) + return false; + if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8)) + return false; + return true; + } + + /** Reassign one (at each turn) FastMoving unit to fasten raid preparation. */ + reassignFastUnit(gameState) + { + for (const ent of this.unitCollection.values()) + { + if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) + continue; + if (!ent.hasClasses(["FastMoving", "CitizenSoldier"])) + continue; + const raid = gameState.ai.HQ.attackManager.getAttackInPreparation(AttackPlan.TYPE_RAID); + ent.setMetadata(PlayerID, "plan", raid.name); + this.unitCollection.updateEnt(ent); + raid.unitCollection.updateEnt(ent); + return; + } + } + + chooseTarget(gameState) + { + if (this.targetPlayer === undefined) + { + this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); + if (this.targetPlayer === undefined) + return false; + } + + this.target = this.getNearestTarget(gameState, this.rallyPoint); + if (!this.target) + { + if (this.uniqueTargetId) + return false; + + // may-be all our previous enemey target (if not recomputed here) have been destroyed ? + this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); + if (this.targetPlayer !== undefined) + this.target = this.getNearestTarget(gameState, this.rallyPoint); + if (!this.target) + return false; + } + this.targetPos = this.target.position(); + // redefine a new rally point for this target if we have a base on the same land + // find a new one on the pseudo-nearest base (dist weighted by the size of the island) + const targetIndex = getLandAccess(gameState, this.target); + let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); + if (targetIndex != rallyIndex) + { + let distminSame = Math.min(); + let rallySame; + let distminDiff = Math.min(); + let rallyDiff; + for (const base of gameState.ai.HQ.baseManagers()) + { + const anchor = base.anchor; + if (!anchor || !anchor.position()) + continue; + let dist = SquareVectorDistance(anchor.position(), this.targetPos); + if (base.accessIndex == targetIndex) + { + if (dist >= distminSame) continue; + distminSame = dist; + rallySame = anchor.position(); + } + else + { + dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]); + if (dist >= distminDiff) + continue; + distminDiff = dist; + rallyDiff = anchor.position(); + } + } + + if (rallySame) + { + this.rallyPoint = rallySame; + this.overseas = 0; + } + else if (rallyDiff) + { + rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff); + this.rallyPoint = rallyDiff; + const sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); + if (sea) + { + this.overseas = sea; + gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); + } + else + { + aiWarn("Petra: " + this.type + " " + this.name + " has an inaccessible target" + + " with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName()); + return false; + } + } + } + else if (this.overseas) + this.overseas = 0; + + return true; + } + /** + * sameLand true means that we look for a target for which we do not need to take a transport + */ + getNearestTarget(gameState, position, sameLand) + { + this.isBlocked = false; + // Temporary variables needed by isValidTarget + this.gameState = gameState; + this.sameLand = sameLand && sameLand > 1 ? sameLand : false; + + let targets; + if (this.uniqueTargetId) + { + targets = new EntityCollection(gameState.sharedScript); + const ent = gameState.getEntityById(this.uniqueTargetId); + if (ent) + targets.addEnt(ent); + } + else + { + if (this.type === AttackPlan.TYPE_RAID) + targets = this.raidTargetFinder(gameState); + else if (this.type === AttackPlan.TYPE_RUSH || this.type === AttackPlan.TYPE_DEFAULT) + { + targets = this.rushTargetFinder(gameState, this.targetPlayer); + if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced)) + targets = this.defaultTargetFinder(gameState, this.targetPlayer); + } + else + targets = this.defaultTargetFinder(gameState, this.targetPlayer); + } + if (!targets.hasEntities()) + return undefined; + + // picking the nearest target + let target; + let minDist = Math.min(); + for (const ent of targets.values()) + { + if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && + (!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id()))) + continue; + // Do not bother with some pointless targets + if (!this.isValidTarget(ent)) + continue; + let dist = SquareVectorDistance(ent.position(), position); + // In normal attacks, disfavor fields + if (this.type !== AttackPlan.TYPE_RUSH && this.type !== AttackPlan.TYPE_RAID && ent.hasClass("Field")) + dist += 100000; + if (dist < minDist) + { + minDist = dist; + target = ent; + } + } + if (!target) + return undefined; + + // Check that we can reach this target + target = this.checkTargetObstruction(gameState, target, position); + + if (!target) + return undefined; + if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic")) + gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]); + // Rushes can change their enemy target if nothing found with the preferred enemy + // Obstruction also can change the enemy target + this.targetPlayer = target.owner(); + return target; + } + + /** + * Default target finder aims for conquest critical targets + * We must apply the *same* selection (isValidTarget) as done in getNearestTarget + */ + defaultTargetFinder(gameState, playerEnemy) + { + let targets = new EntityCollection(gameState.sharedScript); + if (gameState.getVictoryConditions().has("wonder")) + { + for (const ent of + gameState.getEnemyStructures(playerEnemy).filter(filters.byClass("Wonder")).values()) + { + targets.addEnt(ent); + } + } + if (gameState.getVictoryConditions().has("regicide")) + { + for (const ent of gameState.getEnemyUnits(playerEnemy).filter(filters.byClass("Hero")) + .values()) + { + targets.addEnt(ent); + } + } + if (gameState.getVictoryConditions().has("capture_the_relic")) + { + for (const ent of gameState.updatingGlobalCollection("allRelics", + filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) + { + targets.addEnt(ent); + } + } + targets = targets.filter(this.isValidTarget, this); + if (targets.hasEntities()) + return targets; + + const validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); + targets = validTargets.filter(filters.byClass("CivCentre")); + if (!targets.hasEntities()) + targets = validTargets.filter(filters.byClass("ConquestCritical")); + // If there's nothing, attack anything else that's less critical + if (!targets.hasEntities()) + targets = validTargets.filter(filters.byClass("Town")); + if (!targets.hasEntities()) + targets = validTargets.filter(filters.byClass("Village")); + // No buildings, attack anything conquest critical, units included. + // TODO Should add naval attacks against the last remaining ships. + if (!targets.hasEntities()) + { + targets = gameState.getEntities(playerEnemy).filter(filters.byClass("ConquestCritical")) + .filter(filters.not(filters.byClass("Ship"))); + } + return targets; + } + + isValidTarget(ent) + { + if (!ent.position()) + return false; + if (this.sameLand && getLandAccess(this.gameState, ent) != this.sameLand) + return false; + return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length; + } + + /** Rush target finder aims at isolated non-defended buildings */ + rushTargetFinder(gameState, playerEnemy) + { + let targets = new EntityCollection(gameState.sharedScript); + let buildings; + if (playerEnemy !== undefined) + buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray(); + else + buildings = gameState.getEnemyStructures().toEntityArray(); + if (!buildings.length) + return targets; + + this.position = this.unitCollection.getCentrePosition(); + if (!this.position) + this.position = this.rallyPoint; + + let target; + let minDist = Math.min(); + for (const building of buildings) + { + if (building.owner() == 0) + continue; + if (building.hasDefensiveFire()) + continue; + if (!this.isValidTarget(building)) + continue; + const pos = building.position(); + let defended = false; + for (const defense of buildings) + { + if (!defense.hasDefensiveFire()) + continue; + const dist = SquareVectorDistance(pos, defense.position()); + if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 + { + defended = true; + break; + } + } + if (defended) + continue; + const dist = SquareVectorDistance(pos, this.position); + if (dist > minDist) + continue; + minDist = dist; + target = building; + } + if (target) + targets.addEnt(target); + + if (!targets.hasEntities() && this.type === AttackPlan.TYPE_RUSH && playerEnemy) + targets = this.rushTargetFinder(gameState); + + return targets; + } + + /** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ + raidTargetFinder(gameState) + { + const targets = new EntityCollection(gameState.sharedScript); + for (const targetId of gameState.ai.HQ.defenseManager.targetList) + { + const target = gameState.getEntityById(targetId); + if (target && target.position()) + targets.addEnt(target); + } + return targets; + } + + /** + * Check that we can have a path to this target + * otherwise we may be blocked by walls and try to react accordingly + * This is done only when attacker and target are on the same land + */ + checkTargetObstruction(gameState, target, position) + { + if (getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) + return target; + + const targetPos = target.position(); + const startPos = { "x": position[0], "y": position[1] }; + const endPos = { "x": targetPos[0], "y": targetPos[1] }; + let blocker; + const path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); + if (!path.length) + return undefined; + + const pathPos = [path[0].x, path[0].y]; + const dist = VectorDistance(pathPos, targetPos); + const radius = target.obstructionRadius().max; + for (const struct of gameState.getEnemyStructures().values()) + { + if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field")) + continue; + // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate + if (dist < radius + 10 && !struct.hasClass("Gate")) + continue; + // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m + // in the target direction would bring us inside its obstruction. + const structPos = struct.position(); + const x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; + const y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; + + if (struct.get("Obstruction/Static")) + { + if (!struct.angle()) + continue; + const angle = struct.angle(); + const width = +struct.get("Obstruction/Static/@width"); + const depth = +struct.get("Obstruction/Static/@depth"); + const cosa = Math.cos(angle); + const sina = Math.sin(angle); + const u = x * cosa - y * sina; + const v = x * sina + y * cosa; + if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) + { blocker = struct; break; } } - if (blocker) - break; - } - else if (struct.get("Obstruction/Unit")) - { - const r = +struct.get("Obstruction/Unit/@radius"); - if (x*x + y*y < r*r) + else if (struct.get("Obstruction/Obstructions")) { - blocker = struct; - break; - } - } - } - - if (blocker) - { - this.isBlocked = true; - return blocker; - } - - return target; -}; - -AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false) -{ - const startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - const endAccess = getLandAccess(gameState, this.target); - if (startAccess != endAccess) - return false; - - Engine.ProfileStart("AI Compute path"); - const startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; - const endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; - const path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); - this.path = []; - this.path.push(clone(this.targetPos)); - for (const p in path) - this.path.push([path[p].x, path[p].y]); - this.path.push(clone(this.rallyPoint)); - this.path.reverse(); - // Change the rally point to something useful - if (!fixedRallyPoint) - this.setRallyPoint(gameState); - Engine.ProfileStop(); - - return true; -}; - -/** Set rally point at the border of our territory */ -AttackPlan.prototype.setRallyPoint = function(gameState) -{ - for (let i = 0; i < this.path.length; ++i) - { - if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID) - continue; - - if (i === 0) - this.rallyPoint = this.path[0]; - else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20)) - { - this.rallyPoint = this.path[i-2]; - this.path.splice(0, i-2); - } - else - { - this.rallyPoint = this.path[i-1]; - this.path.splice(0, i-1); - } - break; - } -}; - -/** - * Executes the attack plan, after this is executed the update function will be run every turn - * If we're here, it's because we have enough units. - */ -AttackPlan.prototype.StartAttack = function(gameState) -{ - if (this.Config.debug > 1) - aiWarn("start attack " + this.name + " with type " + this.type); - - // if our target was destroyed during preparation, choose a new one - if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) && - !this.chooseTarget(gameState)) - return false; - - // erase our queue. This will stop any leftover unit from being trained. - this.removeQueues(gameState); - - for (const ent of this.unitCollection.values()) - { - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_WALKING); - const stance = ent.isPackable() ? "standground" : "aggressive"; - if (ent.getStance() != stance) - ent.setStance(stance); - } - - const rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - const targetAccess = getLandAccess(gameState, this.target); - if (rallyAccess == targetAccess) - { - if (!this.path) - this.getPathToTarget(gameState, true); - if (!this.path || !this.path[0][0] || !this.path[0][1]) - return false; - this.overseas = 0; - this.state = "walking"; - this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); - } - else - { - this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess); - if (!this.overseas) - return false; - this.state = "transporting"; - // TODO require a global transport for the collection, - // and put back its state to "walking" when the transport is finished - for (const ent of this.unitCollection.values()) - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos); - } - return true; -}; - -/** Runs every turn after the attack is executed */ -AttackPlan.prototype.update = function(gameState, events) -{ - if (!this.unitCollection.hasEntities()) - return 0; - - Engine.ProfileStart("Update Attack"); - - this.position = this.unitCollection.getCentrePosition(); - - // we are transporting our units, let's wait - // TODO instead of state "arrived", made a state "walking" with a new path - if (this.state == "transporting") - this.UpdateTransporting(gameState, events); - - if (!this.position) - { - Engine.ProfileStop(); - return this.unitCollection.length; - } - - if (this.state == "walking" && !this.UpdateWalking(gameState, events)) - { - Engine.ProfileStop(); - return 0; - } - - if (this.state === AttackPlan.STATE_ARRIVED) - { - // let's proceed on with whatever happens now. - this.state = ""; - this.startingAttack = true; - this.unitCollection.forEach(ent => - { - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_ATTACKING); - }); - if (this.type === AttackPlan.TYPE_RUSH) // try to find a better target for rush - { - const newtarget = this.getNearestTarget(gameState, this.position); - if (newtarget) - { - this.target = newtarget; - this.targetPos = this.target.position(); - } - } - } - - // basic state of attacking. - if (this.state == "") - { - // First update the target and/or its position if needed - if (!this.UpdateTarget(gameState)) - { - Engine.ProfileStop(); - return false; - } - - const time = gameState.ai.elapsedTime; - const attackedByStructure = {}; - for (const evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - const attacker = gameState.getEntityById(evt.attacker); - const ourUnit = gameState.getEntityById(evt.target); - if (!ourUnit || !ourUnit.position() || !attacker || !attacker.position()) - continue; - if (!attacker.hasClass("Unit")) - { - attackedByStructure[evt.target] = true; - continue; - } - if (isSiegeUnit(ourUnit)) - { // if our siege units are attacked, we'll send some units to deal with enemies. - const collec = this.unitCollection.filter(filters.not(filters.byClass("Siege"))) - .filterNearest(ourUnit.position(), 5); - for (const ent of collec.values()) + if (!struct.angle()) + continue; + const angle = struct.angle(); + let width = +struct.get("Obstruction/Obstructions/Door/@width"); + let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); + const doorHalfWidth = width / 2; + width += +struct.get("Obstruction/Obstructions/Left/@width"); + depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth")); + width += +struct.get("Obstruction/Obstructions/Right/@width"); + depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth")); + const cosa = Math.cos(angle); + const sina = Math.sin(angle); + const u = x * cosa - y * sina; + const v = x * sina + y * cosa; + if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { - if (isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out - continue; - - const shouldCapture = allowCapture(gameState, ent, attacker); - if (!ent.canAttackTarget(attacker, shouldCapture)) - continue; - ent.attack(attacker.id(), shouldCapture); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + blocker = struct; + break; } - // And if this attacker is a non-ranged siege unit and our unit also, attack it - if (isSiegeUnit(attacker) && attacker.hasClass("Melee") && - ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, - allowCapture(gameState, ourUnit, attacker))) + // check that the path does not cross this gate (could happen if not locked) + for (let i = 1; i < path.length; ++i) { - ourUnit.attack(attacker.id(), allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + const u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; + const v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; + const u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; + const v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; + if (v1 * v2 < 0) + { + const u0 = (u1*v2 - u2*v1) / (v2-v1); + if (Math.abs(u0) > doorHalfWidth) + continue; + blocker = struct; + break; + } } + if (blocker) + break; + } + else if (struct.get("Obstruction/Unit")) + { + const r = +struct.get("Obstruction/Unit/@radius"); + if (x*x + y*y < r*r) + { + blocker = struct; + break; + } + } + } + + if (blocker) + { + this.isBlocked = true; + return blocker; + } + + return target; + } + + getPathToTarget(gameState, fixedRallyPoint = false) + { + const startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); + const endAccess = getLandAccess(gameState, this.target); + if (startAccess != endAccess) + return false; + + Engine.ProfileStart("AI Compute path"); + const startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; + const endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; + const path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); + this.path = []; + this.path.push(clone(this.targetPos)); + for (const p in path) + this.path.push([path[p].x, path[p].y]); + this.path.push(clone(this.rallyPoint)); + this.path.reverse(); + // Change the rally point to something useful + if (!fixedRallyPoint) + this.setRallyPoint(gameState); + Engine.ProfileStop(); + + return true; + } + + /** Set rally point at the border of our territory */ + setRallyPoint(gameState) + { + for (let i = 0; i < this.path.length; ++i) + { + if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID) + continue; + + if (i === 0) + this.rallyPoint = this.path[0]; + else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20)) + { + this.rallyPoint = this.path[i-2]; + this.path.splice(0, i-2); } else { - if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) + this.rallyPoint = this.path[i-1]; + this.path.splice(0, i-1); + } + break; + } + } + + /** + * Executes the attack plan, after this is executed the update function will be run every turn + * If we're here, it's because we have enough units. + */ + StartAttack(gameState) + { + if (this.Config.debug > 1) + aiWarn("start attack " + this.name + " with type " + this.type); + + // if our target was destroyed during preparation, choose a new one + if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) && + !this.chooseTarget(gameState)) + return false; + + // erase our queue. This will stop any leftover unit from being trained. + this.removeQueues(gameState); + + for (const ent of this.unitCollection.values()) + { + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_WALKING); + const stance = ent.isPackable() ? "standground" : "aggressive"; + if (ent.getStance() != stance) + ent.setStance(stance); + } + + const rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); + const targetAccess = getLandAccess(gameState, this.target); + if (rallyAccess == targetAccess) + { + if (!this.path) + this.getPathToTarget(gameState, true); + if (!this.path || !this.path[0][0] || !this.path[0][1]) + return false; + this.overseas = 0; + this.state = "walking"; + this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); + } + else + { + this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess); + if (!this.overseas) + return false; + this.state = "transporting"; + // TODO require a global transport for the collection, + // and put back its state to "walking" when the transport is finished + for (const ent of this.unitCollection.values()) + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos); + } + return true; + } + + /** Runs every turn after the attack is executed */ + update(gameState, events) + { + if (!this.unitCollection.hasEntities()) + return 0; + + Engine.ProfileStart("Update Attack"); + + this.position = this.unitCollection.getCentrePosition(); + + // we are transporting our units, let's wait + // TODO instead of state "arrived", made a state "walking" with a new path + if (this.state == "transporting") + this.UpdateTransporting(gameState, events); + + if (!this.position) + { + Engine.ProfileStop(); + return this.unitCollection.length; + } + + if (this.state == "walking" && !this.UpdateWalking(gameState, events)) + { + Engine.ProfileStop(); + return 0; + } + + if (this.state === AttackPlan.STATE_ARRIVED) + { + // let's proceed on with whatever happens now. + this.state = ""; + this.startingAttack = true; + this.unitCollection.forEach(ent => + { + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_ATTACKING); + }); + if (this.type === AttackPlan.TYPE_RUSH) // try to find a better target for rush + { + const newtarget = this.getNearestTarget(gameState, this.position); + if (newtarget) { - // do not react if our melee units are attacked by ranged one and we are blocked by walls - // TODO check that the attacker is from behind the wall + this.target = newtarget; + this.targetPos = this.target.position(); + } + } + } + + // basic state of attacking. + if (this.state == "") + { + // First update the target and/or its position if needed + if (!this.UpdateTarget(gameState)) + { + Engine.ProfileStop(); + return false; + } + + const time = gameState.ai.elapsedTime; + const attackedByStructure = {}; + for (const evt of events.Attacked) + { + if (!this.unitCollection.hasEntId(evt.target)) + continue; + const attacker = gameState.getEntityById(evt.attacker); + const ourUnit = gameState.getEntityById(evt.target); + if (!ourUnit || !ourUnit.position() || !attacker || !attacker.position()) + continue; + if (!attacker.hasClass("Unit")) + { + attackedByStructure[evt.target] = true; continue; } - else if (isSiegeUnit(attacker)) - { // if our unit is attacked by a siege unit, we'll send some melee units to help it. - const collec = this.unitCollection.filter(filters.byClass("Melee")) + if (isSiegeUnit(ourUnit)) + { // if our siege units are attacked, we'll send some units to deal with enemies. + const collec = this.unitCollection.filter(filters.not(filters.byClass("Siege"))) .filterNearest(ourUnit.position(), 5); for (const ent of collec.values()) { + if (isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out + continue; + const shouldCapture = allowCapture(gameState, ent, attacker); if (!ent.canAttackTarget(attacker, shouldCapture)) continue; ent.attack(attacker.id(), shouldCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } + // And if this attacker is a non-ranged siege unit and our unit also, attack it + if (isSiegeUnit(attacker) && attacker.hasClass("Melee") && + ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, + allowCapture(gameState, ourUnit, attacker))) + { + ourUnit.attack(attacker.id(), allowCapture(gameState, ourUnit, attacker)); + ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + } } else { - // Look first for nearby units to help us if possible - const collec = this.unitCollection.filterNearest(ourUnit.position(), 2); - for (const ent of collec.values()) + if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) { - const shouldCapture = allowCapture(gameState, ent, attacker); - if (isSiegeUnit(ent) || !ent.canAttackTarget(attacker, shouldCapture)) - continue; - const orderData = ent.unitAIOrderData(); + // do not react if our melee units are attacked by ranged one and we are blocked by walls + // TODO check that the attacker is from behind the wall + continue; + } + else if (isSiegeUnit(attacker)) + { // if our unit is attacked by a siege unit, we'll send some melee units to help it. + const collec = this.unitCollection.filter(filters.byClass("Melee")) + .filterNearest(ourUnit.position(), 5); + for (const ent of collec.values()) + { + const shouldCapture = allowCapture(gameState, ent, attacker); + if (!ent.canAttackTarget(attacker, shouldCapture)) + continue; + ent.attack(attacker.id(), shouldCapture); + ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + } + } + else + { + // Look first for nearby units to help us if possible + const collec = this.unitCollection.filterNearest(ourUnit.position(), 2); + for (const ent of collec.values()) + { + const shouldCapture = allowCapture(gameState, ent, attacker); + if (isSiegeUnit(ent) || !ent.canAttackTarget(attacker, shouldCapture)) + continue; + const orderData = ent.unitAIOrderData(); + if (orderData && orderData.length && orderData[0].target) + { + if (orderData[0].target === attacker.id()) + continue; + const target = gameState.getEntityById(orderData[0].target); + if (target && !target.hasClasses(["Structure", "Support"])) + continue; + } + ent.attack(attacker.id(), shouldCapture); + ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + } + // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate + // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate + const orderData = ourUnit.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; const target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClasses(["Structure", "Support"])) - continue; + { + if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) + continue; + } } - ent.attack(attacker.id(), shouldCapture); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate - // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate - const orderData = ourUnit.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - { - if (orderData[0].target === attacker.id()) - continue; - const target = gameState.getEntityById(orderData[0].target); - if (target && !target.hasClasses(["Structure", "Support"])) + const shouldCapture = allowCapture(gameState, ourUnit, attacker); + if (ourUnit.canAttackTarget(attacker, shouldCapture)) { - if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) - continue; + ourUnit.attack(attacker.id(), shouldCapture); + ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } - const shouldCapture = allowCapture(gameState, ourUnit, attacker); - if (ourUnit.canAttackTarget(attacker, shouldCapture)) - { - ourUnit.attack(attacker.id(), shouldCapture); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } } } - } - const enemyUnits = gameState.getEnemyUnits(this.targetPlayer); - const enemyStructures = gameState.getEnemyStructures(this.targetPlayer); + const enemyUnits = gameState.getEnemyUnits(this.targetPlayer); + const enemyStructures = gameState.getEnemyStructures(this.targetPlayer); - // Count the number of times an enemy is targeted, to prevent all units to follow the same target - const unitTargets = {}; - for (const ent of this.unitCollection.values()) - { - if (ent.hasClass("Ship")) // TODO What to do with ships - continue; - const orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - const targetId = orderData[0].target; - const target = gameState.getEntityById(targetId); - if (!target || target.hasClass("Structure")) - continue; - if (!(targetId in unitTargets)) + // Count the number of times an enemy is targeted, to prevent all units to follow the same target + const unitTargets = {}; + for (const ent of this.unitCollection.values()) { - if (isSiegeUnit(target) || target.hasClass("Hero")) - unitTargets[targetId] = -8; - else if (target.hasClasses(["Champion", "Ship"])) - unitTargets[targetId] = -5; - else - unitTargets[targetId] = -3; - } - ++unitTargets[targetId]; - } - const veto = {}; - for (const target in unitTargets) - if (unitTargets[target] > 0) - veto[target] = true; - - let targetClassesUnit; - let targetClassesSiege; - if (this.type === AttackPlan.TYPE_RUSH) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Tower", "Fortress"], "vetoEntities": veto }; - else - { - if (this.target.hasClass("Fortress")) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto }; - else if (this.target.hasClasses(["Palisade", "Wall"])) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; - else - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Fortress"], "vetoEntities": veto }; - } - if (this.target.hasClass("Structure")) - targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto }; - else - targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto }; - - // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later - if (this.target.hasDefensiveFire()) - { - targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge"); - targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge"); - } - - if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length) - this.unitCollUpdateArray = this.unitCollection.toIdArray(); - - // Let's check a few units each time we update (currently 10) except when attack starts - const lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; - for (let check = 0; check < lgth; check++) - { - const ent = gameState.getEntityById(this.unitCollUpdateArray[check]); - if (!ent || !ent.position()) - continue; - // Do not reassign units which have reacted to an attack in that same turn - if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) - continue; - - let targetId; - const orderData = ent.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - targetId = orderData[0].target; - - // update the order if needed - let needsUpdate = false; - let maybeUpdate = false; - const siegeUnit = isSiegeUnit(ent); - if (ent.isIdle()) - needsUpdate = true; - else if (siegeUnit && targetId) - { - const target = gameState.getEntityById(targetId); - if (!target || gameState.isPlayerAlly(target.owner())) - needsUpdate = true; - else if (unitTargets[targetId] && unitTargets[targetId] > 0) - { - needsUpdate = true; - --unitTargets[targetId]; - } - else if (!target.hasClass("Structure")) - maybeUpdate = true; - } - else if (targetId) - { - const target = gameState.getEntityById(targetId); - if (!target || gameState.isPlayerAlly(target.owner())) - needsUpdate = true; - else if (unitTargets[targetId] && unitTargets[targetId] > 0) - { - needsUpdate = true; - --unitTargets[targetId]; - } - else if (target.hasClass("Ship") && !ent.hasClass("Ship")) - maybeUpdate = true; - else if (attackedByStructure[ent.id()] && target.hasClass("Field")) - maybeUpdate = true; - else if (!ent.hasClass("FastMoving") && !ent.hasClass("Ranged") && - target.hasClass("Civilian") && target.unitAIState().split(".")[1] == "FLEEING") - maybeUpdate = true; - } - - // don't update too soon if not necessary - if (!needsUpdate) - { - if (!maybeUpdate) + if (ent.hasClass("Ship")) // TODO What to do with ships continue; - const deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; - const lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); - if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) + const orderData = ent.unitAIOrderData(); + if (!orderData || !orderData.length || !orderData[0].target) continue; - } - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - let range = 60; - const attackTypes = ent.attackTypes(); - if (this.isBlocked) - { - if (attackTypes && attackTypes.indexOf("Ranged") !== -1) - range = ent.attackRange("Ranged").max; - else if (attackTypes && attackTypes.indexOf("Melee") !== -1) - range = ent.attackRange("Melee").max; - else - range = 10; - } - else if (attackTypes && attackTypes.indexOf("Ranged") !== -1) - range = 30 + ent.attackRange("Ranged").max; - else if (ent.hasClass("FastMoving")) - range += 30; - range *= range; - const entAccess = getLandAccess(gameState, ent); - // Checking for gates if we're a siege unit. - if (siegeUnit) - { - const mStruct = enemyStructures.filter(enemy => + const targetId = orderData[0].target; + const target = gameState.getEntityById(targetId); + if (!target || target.hasClass("Structure")) + continue; + if (!(targetId in unitTargets)) { - if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy))) - return false; - if (SquareVectorDistance(enemy.position(), ent.position()) > range) - return false; - if (enemy.foundationProgress() == 0) - return false; - if (getLandAccess(gameState, enemy) != entAccess) - return false; - return true; - }).toEntityArray(); - if (mStruct.length) - { - mStruct.sort((structa, structb) => - { - let vala = structa.costSum(); - if (structa.hasClass("Gate") && ent.canAttackClass("Wall")) - vala += 10000; - else if (structa.hasDefensiveFire()) - vala += 1000; - else if (structa.hasClass("ConquestCritical")) - vala += 200; - let valb = structb.costSum(); - if (structb.hasClass("Gate") && ent.canAttackClass("Wall")) - valb += 10000; - else if (structb.hasDefensiveFire()) - valb += 1000; - else if (structb.hasClass("ConquestCritical")) - valb += 200; - return valb - vala; - }); - if (mStruct[0].hasClass("Gate")) - ent.attack(mStruct[0].id(), allowCapture(gameState, ent, mStruct[0])); + if (isSiegeUnit(target) || target.hasClass("Hero")) + unitTargets[targetId] = -8; + else if (target.hasClasses(["Champion", "Ship"])) + unitTargets[targetId] = -5; else - { - const rand = randIntExclusive(0, mStruct.length * 0.2); - ent.attack(mStruct[rand].id(), allowCapture(gameState, ent, mStruct[rand])); - } - } - else - { - if (!ent.hasClass("Ranged")) - { - const targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto }; - ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); - } - else - ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege); + unitTargets[targetId] = -3; } + ++unitTargets[targetId]; } + const veto = {}; + for (const target in unitTargets) + if (unitTargets[target] > 0) + veto[target] = true; + + let targetClassesUnit; + let targetClassesSiege; + if (this.type === AttackPlan.TYPE_RUSH) + targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Tower", "Fortress"], "vetoEntities": veto }; else { - const nearby = !ent.hasClasses(["FastMoving", "Ranged"]); - const mUnit = enemyUnits.filter(enemy => - { - if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy))) - return false; - if (enemy.hasClass("Animal")) - return false; - if (nearby && enemy.hasClass("Civilian") && enemy.unitAIState().split(".")[1] == "FLEEING") - return false; - const dist = SquareVectorDistance(enemy.position(), ent.position()); - if (dist > range) - return false; - if (getLandAccess(gameState, enemy) != entAccess) - return false; - // if already too much units targeting this enemy, let's continue towards our main target - if (veto[enemy.id()] && - SquareVectorDistance(this.targetPos, ent.position()) > 2500) - { - return false; - } - enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist)); - return true; - }, this).toEntityArray(); - if (mUnit.length) - { - mUnit.sort((unitA, unitB) => - { - let vala = unitA.hasClass("Support") ? 50 : 0; - if (ent.counters(unitA)) - vala += 100; - let valb = unitB.hasClass("Support") ? 50 : 0; - if (ent.counters(unitB)) - valb += 100; - const distA = unitA.getMetadata(PlayerID, "distance"); - const distB = unitB.getMetadata(PlayerID, "distance"); - if (distA && distB) - { - vala -= distA; - valb -= distB; - } - if (veto[unitA.id()]) - vala -= 20000; - if (veto[unitB.id()]) - valb -= 20000; - return valb - vala; - }); - const rand = randIntExclusive(0, mUnit.length * 0.1); - ent.attack(mUnit[rand].id(), allowCapture(gameState, ent, mUnit[rand])); - } - // This may prove dangerous as we may be blocked by something we - // cannot attack. See similar behaviour at #5741. - else if (this.isBlocked && ent.canAttackTarget(this.target, false)) - ent.attack(this.target.id(), false); - else if (SquareVectorDistance(this.targetPos, ent.position()) > 2500) - { - let targetClasses = targetClassesUnit; - if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything - { - if (!ent.hasClasses(["Ranged", "Ship"])) - targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; - else - targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; - } - else if (!ent.hasClasses(["Ranged", "Ship"])) - targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; - ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); - } + if (this.target.hasClass("Fortress")) + targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto }; + else if (this.target.hasClasses(["Palisade", "Wall"])) + targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; else + targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Fortress"], "vetoEntities": veto }; + } + if (this.target.hasClass("Structure")) + targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto }; + else + targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto }; + + // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later + if (this.target.hasDefensiveFire()) + { + targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge"); + targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge"); + } + + if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length) + this.unitCollUpdateArray = this.unitCollection.toIdArray(); + + // Let's check a few units each time we update (currently 10) except when attack starts + const lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; + for (let check = 0; check < lgth; check++) + { + const ent = gameState.getEntityById(this.unitCollUpdateArray[check]); + if (!ent || !ent.position()) + continue; + // Do not reassign units which have reacted to an attack in that same turn + if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) + continue; + + let targetId; + const orderData = ent.unitAIOrderData(); + if (orderData && orderData.length && orderData[0].target) + targetId = orderData[0].target; + + // update the order if needed + let needsUpdate = false; + let maybeUpdate = false; + const siegeUnit = isSiegeUnit(ent); + if (ent.isIdle()) + needsUpdate = true; + else if (siegeUnit && targetId) + { + const target = gameState.getEntityById(targetId); + if (!target || gameState.isPlayerAlly(target.owner())) + needsUpdate = true; + else if (unitTargets[targetId] && unitTargets[targetId] > 0) + { + needsUpdate = true; + --unitTargets[targetId]; + } + else if (!target.hasClass("Structure")) + maybeUpdate = true; + } + else if (targetId) + { + const target = gameState.getEntityById(targetId); + if (!target || gameState.isPlayerAlly(target.owner())) + needsUpdate = true; + else if (unitTargets[targetId] && unitTargets[targetId] > 0) + { + needsUpdate = true; + --unitTargets[targetId]; + } + else if (target.hasClass("Ship") && !ent.hasClass("Ship")) + maybeUpdate = true; + else if (attackedByStructure[ent.id()] && target.hasClass("Field")) + maybeUpdate = true; + else if (!ent.hasClass("FastMoving") && !ent.hasClass("Ranged") && + target.hasClass("Civilian") && target.unitAIState().split(".")[1] == "FLEEING") + maybeUpdate = true; + } + + // don't update too soon if not necessary + if (!needsUpdate) + { + if (!maybeUpdate) + continue; + const deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; + const lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); + if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) + continue; + } + ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + let range = 60; + const attackTypes = ent.attackTypes(); + if (this.isBlocked) + { + if (attackTypes && attackTypes.indexOf("Ranged") !== -1) + range = ent.attackRange("Ranged").max; + else if (attackTypes && attackTypes.indexOf("Melee") !== -1) + range = ent.attackRange("Melee").max; + else + range = 10; + } + else if (attackTypes && attackTypes.indexOf("Ranged") !== -1) + range = 30 + ent.attackRange("Ranged").max; + else if (ent.hasClass("FastMoving")) + range += 30; + range *= range; + const entAccess = getLandAccess(gameState, ent); + // Checking for gates if we're a siege unit. + if (siegeUnit) { const mStruct = enemyStructures.filter(enemy => { - if (this.isBlocked && enemy.id() != this.target.id()) - return false; if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy))) return false; if (SquareVectorDistance(enemy.position(), ent.position()) > range) return false; + if (enemy.foundationProgress() == 0) + return false; if (getLandAccess(gameState, enemy) != entAccess) return false; return true; - }, this).toEntityArray(); + }).toEntityArray(); if (mStruct.length) { mStruct.sort((structa, structb) => @@ -1785,521 +1665,644 @@ AttackPlan.prototype.update = function(gameState, events) let vala = structa.costSum(); if (structa.hasClass("Gate") && ent.canAttackClass("Wall")) vala += 10000; + else if (structa.hasDefensiveFire()) + vala += 1000; else if (structa.hasClass("ConquestCritical")) - vala += 100; + vala += 200; let valb = structb.costSum(); if (structb.hasClass("Gate") && ent.canAttackClass("Wall")) valb += 10000; + else if (structb.hasDefensiveFire()) + valb += 1000; else if (structb.hasClass("ConquestCritical")) - valb += 100; + valb += 200; return valb - vala; }); if (mStruct[0].hasClass("Gate")) - ent.attack(mStruct[0].id(), false); + ent.attack(mStruct[0].id(), allowCapture(gameState, ent, mStruct[0])); else { const rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), allowCapture(gameState, ent, mStruct[rand])); } } - else if (needsUpdate) // really nothing let's try to help our nearest unit + else { - let distmin = Math.min(); - let attacker; - this.unitCollection.forEach(unit => + if (!ent.hasClass("Ranged")) { - if (!unit.position()) - return; - if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length || - !unit.unitAIOrderData()[0].target) - return; - const target = gameState.getEntityById(unit.unitAIOrderData()[0].target); - if (!target) - return; - const dist = SquareVectorDistance(unit.position(), ent.position()); - if (dist > distmin) - return; - distmin = dist; - if (!ent.canAttackTarget(target, allowCapture(gameState, ent, target))) - return; - attacker = target; + const targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto }; + ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); + } + else + ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege); + } + } + else + { + const nearby = !ent.hasClasses(["FastMoving", "Ranged"]); + const mUnit = enemyUnits.filter(enemy => + { + if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy))) + return false; + if (enemy.hasClass("Animal")) + return false; + if (nearby && enemy.hasClass("Civilian") && enemy.unitAIState().split(".")[1] == "FLEEING") + return false; + const dist = SquareVectorDistance(enemy.position(), ent.position()); + if (dist > range) + return false; + if (getLandAccess(gameState, enemy) != entAccess) + return false; + // if already too much units targeting this enemy, let's continue towards our main target + if (veto[enemy.id()] && + SquareVectorDistance(this.targetPos, ent.position()) > 2500) + { + return false; + } + enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist)); + return true; + }, this).toEntityArray(); + if (mUnit.length) + { + mUnit.sort((unitA, unitB) => + { + let vala = unitA.hasClass("Support") ? 50 : 0; + if (ent.counters(unitA)) + vala += 100; + let valb = unitB.hasClass("Support") ? 50 : 0; + if (ent.counters(unitB)) + valb += 100; + const distA = unitA.getMetadata(PlayerID, "distance"); + const distB = unitB.getMetadata(PlayerID, "distance"); + if (distA && distB) + { + vala -= distA; + valb -= distB; + } + if (veto[unitA.id()]) + vala -= 20000; + if (veto[unitB.id()]) + valb -= 20000; + return valb - vala; }); - if (attacker) - ent.attack(attacker.id(), allowCapture(gameState, ent, attacker)); + const rand = randIntExclusive(0, mUnit.length * 0.1); + ent.attack(mUnit[rand].id(), allowCapture(gameState, ent, mUnit[rand])); + } + // This may prove dangerous as we may be blocked by something we + // cannot attack. See similar behaviour at #5741. + else if (this.isBlocked && ent.canAttackTarget(this.target, false)) + ent.attack(this.target.id(), false); + else if (SquareVectorDistance(this.targetPos, ent.position()) > 2500) + { + let targetClasses = targetClassesUnit; + if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything + { + if (!ent.hasClasses(["Ranged", "Ship"])) + targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; + else + targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; + } + else if (!ent.hasClasses(["Ranged", "Ship"])) + targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; + ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); + } + else + { + const mStruct = enemyStructures.filter(enemy => + { + if (this.isBlocked && enemy.id() != this.target.id()) + return false; + if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy))) + return false; + if (SquareVectorDistance(enemy.position(), ent.position()) > range) + return false; + if (getLandAccess(gameState, enemy) != entAccess) + return false; + return true; + }, this).toEntityArray(); + if (mStruct.length) + { + mStruct.sort((structa, structb) => + { + let vala = structa.costSum(); + if (structa.hasClass("Gate") && ent.canAttackClass("Wall")) + vala += 10000; + else if (structa.hasClass("ConquestCritical")) + vala += 100; + let valb = structb.costSum(); + if (structb.hasClass("Gate") && ent.canAttackClass("Wall")) + valb += 10000; + else if (structb.hasClass("ConquestCritical")) + valb += 100; + return valb - vala; + }); + if (mStruct[0].hasClass("Gate")) + ent.attack(mStruct[0].id(), false); + else + { + const rand = randIntExclusive(0, mStruct.length * 0.2); + ent.attack(mStruct[rand].id(), allowCapture(gameState, ent, mStruct[rand])); + } + } + else if (needsUpdate) // really nothing let's try to help our nearest unit + { + let distmin = Math.min(); + let attacker; + this.unitCollection.forEach(unit => + { + if (!unit.position()) + return; + if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length || + !unit.unitAIOrderData()[0].target) + return; + const target = gameState.getEntityById(unit.unitAIOrderData()[0].target); + if (!target) + return; + const dist = SquareVectorDistance(unit.position(), ent.position()); + if (dist > distmin) + return; + distmin = dist; + if (!ent.canAttackTarget(target, allowCapture(gameState, ent, target))) + return; + attacker = target; + }); + if (attacker) + ent.attack(attacker.id(), allowCapture(gameState, ent, attacker)); + } } } } + this.unitCollUpdateArray.splice(0, lgth); + this.startingAttack = false; + + // check if this enemy has resigned + if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) + this.target = undefined; } - this.unitCollUpdateArray.splice(0, lgth); - this.startingAttack = false; + this.lastPosition = clone(this.position); + Engine.ProfileStop(); - // check if this enemy has resigned - if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) - this.target = undefined; - } - this.lastPosition = clone(this.position); - Engine.ProfileStop(); - - return this.unitCollection.length; -}; - -AttackPlan.prototype.UpdateTransporting = function(gameState, events) -{ - let done = true; - for (const ent of this.unitCollection.values()) - { - if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] }); - else if (this.Config.debug > 1) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] }); - if (!done) - continue; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - done = false; + return this.unitCollection.length; } - if (done) + UpdateTransporting(gameState, events) { - this.state = AttackPlan.STATE_ARRIVED; - return; - } - - // if we are attacked while waiting the rest of the army, retaliate - for (const evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - const attacker = gameState.getEntityById(evt.attacker); - if (!attacker || !gameState.getEntityById(evt.target)) - continue; + let done = true; for (const ent of this.unitCollection.values()) { + if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined) + Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] }); + else if (this.Config.debug > 1) + Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] }); + if (!done) + continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - - const shouldCapture = allowCapture(gameState, ent, attacker); - if (!ent.isIdle() || !ent.canAttackTarget(attacker, shouldCapture)) - continue; - ent.attack(attacker.id(), shouldCapture); + done = false; } - break; - } -}; -AttackPlan.prototype.UpdateWalking = function(gameState, events) -{ - // we're marching towards the target - // Let's check if any of our unit has been attacked. - // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building - // or if we reached the enemy base. Different plans may react differently. - let attackedNB = 0; - let attackedUnitNB = 0; - for (const evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - const attacker = gameState.getEntityById(evt.attacker); - if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0)) + if (done) { - attackedNB++; - if (attacker.hasClass("Unit")) - attackedUnitNB++; + this.state = AttackPlan.STATE_ARRIVED; + return; + } + + // if we are attacked while waiting the rest of the army, retaliate + for (const evt of events.Attacked) + { + if (!this.unitCollection.hasEntId(evt.target)) + continue; + const attacker = gameState.getEntityById(evt.attacker); + if (!attacker || !gameState.getEntityById(evt.target)) + continue; + for (const ent of this.unitCollection.values()) + { + if (ent.getMetadata(PlayerID, "transport") !== undefined) + continue; + + const shouldCapture = allowCapture(gameState, ent, attacker); + if (!ent.isIdle() || !ent.canAttackTarget(attacker, shouldCapture)) + continue; + ent.attack(attacker.id(), shouldCapture); + } + break; } } - // Are we arrived at destination ? - if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits())) + + UpdateWalking(gameState, events) { - if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3) + // we're marching towards the target + // Let's check if any of our unit has been attacked. + // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building + // or if we reached the enemy base. Different plans may react differently. + let attackedNB = 0; + let attackedUnitNB = 0; + for (const evt of events.Attacked) { + if (!this.unitCollection.hasEntId(evt.target)) + continue; + const attacker = gameState.getEntityById(evt.attacker); + if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0)) + { + attackedNB++; + if (attacker.hasClass("Unit")) + attackedUnitNB++; + } + } + // Are we arrived at destination ? + if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits())) + { + if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3) + { + this.state = AttackPlan.STATE_ARRIVED; + return true; + } + } + + // basically haven't moved an inch: very likely stuck) + if (SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && + this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) + { + // check for stuck siege units + let farthest = 0; + let farthestEnt; + for (const ent of this.unitCollection.filter(filters.byClass("Siege")).values()) + { + const dist = SquareVectorDistance(ent.position(), this.position); + if (dist < farthest) + continue; + farthest = dist; + farthestEnt = ent; + } + if (farthestEnt) + farthestEnt.destroy(); + } + if (gameState.ai.playedTurn % 5 === 0) + this.position5TurnsAgo = clone(this.position); + + if (this.lastPosition && SquareVectorDistance(this.position, this.lastPosition) < 16 && + this.path.length > 0) + { + if (!this.path[0][0] || !this.path[0][1]) + aiWarn("Start: Problem with path " + uneval(this.path)); + // We're stuck, presumably. Check if there are no walls just close to us. + for (const ent of gameState.getEnemyStructures().filter(filters.byClass(["Palisade", "Wall"])) + .values()) + { + if (SquareVectorDistance(this.position, ent.position()) > 800) + continue; + const enemyClass = ent.hasClass("Wall") ? "Wall" : "Palisade"; + // there are walls, so check if we can attack + if (this.unitCollection.filter(filters.byCanAttackClass(enemyClass)).hasEntities()) + { + if (this.Config.debug > 1) + { + aiWarn("Attack Plan " + this.type + " " + this.name + + " has met walls and is not happy."); + } + this.state = AttackPlan.STATE_ARRIVED; + return true; + } + // abort plan + if (this.Config.debug > 1) + { + aiWarn("Attack Plan " + this.type + " " + this.name + + " has met walls and gives up."); + } + return false; + } + + // this.unitCollection.move(this.path[0][0], this.path[0][1]); + this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]); + } + + // check if our units are close enough from the next waypoint. + if (SquareVectorDistance(this.position, this.targetPos) < 10000) + { + if (this.Config.debug > 1) + aiWarn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = AttackPlan.STATE_ARRIVED; return true; } - } - - // basically haven't moved an inch: very likely stuck) - if (SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && - this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) - { - // check for stuck siege units - let farthest = 0; - let farthestEnt; - for (const ent of this.unitCollection.filter(filters.byClass("Siege")).values()) + else if (this.path.length && SquareVectorDistance(this.position, this.path[0]) < 1600) { - const dist = SquareVectorDistance(ent.position(), this.position); - if (dist < farthest) - continue; - farthest = dist; - farthestEnt = ent; - } - if (farthestEnt) - farthestEnt.destroy(); - } - if (gameState.ai.playedTurn % 5 === 0) - this.position5TurnsAgo = clone(this.position); - - if (this.lastPosition && SquareVectorDistance(this.position, this.lastPosition) < 16 && - this.path.length > 0) - { - if (!this.path[0][0] || !this.path[0][1]) - aiWarn("Start: Problem with path " + uneval(this.path)); - // We're stuck, presumably. Check if there are no walls just close to us. - for (const ent of gameState.getEnemyStructures().filter(filters.byClass(["Palisade", "Wall"])) - .values()) - { - if (SquareVectorDistance(this.position, ent.position()) > 800) - continue; - const enemyClass = ent.hasClass("Wall") ? "Wall" : "Palisade"; - // there are walls, so check if we can attack - if (this.unitCollection.filter(filters.byCanAttackClass(enemyClass)).hasEntities()) + this.path.shift(); + if (this.path.length) + this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); + else { if (this.Config.debug > 1) { aiWarn("Attack Plan " + this.type + " " + this.name + - " has met walls and is not happy."); + " has arrived to destination."); } this.state = AttackPlan.STATE_ARRIVED; return true; } - // abort plan - if (this.Config.debug > 1) - { - aiWarn("Attack Plan " + this.type + " " + this.name + - " has met walls and gives up."); - } - return false; } - // this.unitCollection.move(this.path[0][0], this.path[0][1]); - this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]); - } - - // check if our units are close enough from the next waypoint. - if (SquareVectorDistance(this.position, this.targetPos) < 10000) - { - if (this.Config.debug > 1) - aiWarn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); - this.state = AttackPlan.STATE_ARRIVED; return true; } - else if (this.path.length && SquareVectorDistance(this.position, this.path[0]) < 1600) + + UpdateTarget(gameState) { - this.path.shift(); - if (this.path.length) - this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); - else + // First update the target position in case it's a unit (and check if it has garrisoned) + if (this.target && this.target.hasClass("Unit")) { - if (this.Config.debug > 1) + this.targetPos = this.target.position(); + if (!this.targetPos) { - aiWarn("Attack Plan " + this.type + " " + this.name + - " has arrived to destination."); + const holder = getHolder(gameState, this.target); + if (holder && gameState.isPlayerEnemy(holder.owner())) + { + this.target = holder; + this.targetPos = holder.position(); + } + else + this.target = undefined; } - this.state = AttackPlan.STATE_ARRIVED; - return true; } - } - - return true; -}; - -AttackPlan.prototype.UpdateTarget = function(gameState) -{ - // First update the target position in case it's a unit (and check if it has garrisoned) - if (this.target && this.target.hasClass("Unit")) - { - this.targetPos = this.target.position(); - if (!this.targetPos) + // Then update the target if needed: + if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer)) { - const holder = getHolder(gameState, this.target); - if (holder && gameState.isPlayerEnemy(holder.owner())) - { - this.target = holder; - this.targetPos = holder.position(); - } - else - this.target = undefined; - } - } - // Then update the target if needed: - if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer)) - { - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - if (this.targetPlayer === undefined) - return false; - - if (this.target && this.target.owner() !== this.targetPlayer) - this.target = undefined; - } - if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned - this.target = undefined; - - if (!this.target || !gameState.getEntityById(this.target.id())) - { - if (this.Config.debug > 1) - { - aiWarn("Seems like our target for plan " + this.name + - " has been destroyed or captured. Switching."); - } - const accessIndex = this.getAttackAccess(gameState); - this.target = this.getNearestTarget(gameState, this.position, accessIndex); - if (!this.target) - { - if (this.uniqueTargetId) + this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); + if (this.targetPlayer === undefined) return false; - // Check if we could help any current attack - const attackManager = gameState.ai.HQ.attackManager; - for (const attackType in attackManager.startedAttacks) - { - for (const attack of attackManager.startedAttacks[attackType]) - { - if (attack.name == this.name) - continue; - if (!attack.target || !gameState.getEntityById(attack.target.id()) || - !gameState.isPlayerEnemy(attack.target.owner())) - continue; - if (accessIndex != getLandAccess(gameState, attack.target)) - continue; - if (attack.target.owner() == 0 && attack.targetPlayer != 0) // looks like it has resigned - continue; - if (!gameState.isPlayerEnemy(attack.targetPlayer)) - continue; - this.target = attack.target; - this.targetPlayer = attack.targetPlayer; - this.targetPos = this.target.position(); - return true; - } - } + if (this.target && this.target.owner() !== this.targetPlayer) + this.target = undefined; + } + if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned + this.target = undefined; - // If not, let's look for another enemy + if (!this.target || !gameState.getEntityById(this.target.id())) + { + if (this.Config.debug > 1) + { + aiWarn("Seems like our target for plan " + this.name + + " has been destroyed or captured. Switching."); + } + const accessIndex = this.getAttackAccess(gameState); + this.target = this.getNearestTarget(gameState, this.position, accessIndex); if (!this.target) { - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - if (this.targetPlayer !== undefined) - this.target = this.getNearestTarget(gameState, this.position, accessIndex); + if (this.uniqueTargetId) + return false; + + // Check if we could help any current attack + const attackManager = gameState.ai.HQ.attackManager; + for (const attackType in attackManager.startedAttacks) + { + for (const attack of attackManager.startedAttacks[attackType]) + { + if (attack.name == this.name) + continue; + if (!attack.target || !gameState.getEntityById(attack.target.id()) || + !gameState.isPlayerEnemy(attack.target.owner())) + continue; + if (accessIndex != getLandAccess(gameState, attack.target)) + continue; + if (attack.target.owner() == 0 && attack.targetPlayer != 0) // looks like it has resigned + continue; + if (!gameState.isPlayerEnemy(attack.targetPlayer)) + continue; + this.target = attack.target; + this.targetPlayer = attack.targetPlayer; + this.targetPos = this.target.position(); + return true; + } + } + + // If not, let's look for another enemy if (!this.target) { - if (this.Config.debug > 1) + this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); + if (this.targetPlayer !== undefined) + this.target = this.getNearestTarget(gameState, this.position, accessIndex); + if (!this.target) { - aiWarn("No new target found. Remaining units " + - this.unitCollection.length); + if (this.Config.debug > 1) + { + aiWarn("No new target found. Remaining units " + + this.unitCollection.length); + } + return false; } - return false; + } + if (this.Config.debug > 1) + aiWarn("We will help one of our other attacks"); + } + this.targetPos = this.target.position(); + } + return true; + } + + /** reset any units */ + Abort(gameState) + { + this.unitCollection.unregister(); + if (this.unitCollection.hasEntities()) + { + // If the attack was started, look for a good rallyPoint to withdraw + let rallyPoint; + if (this.isStarted()) + { + const access = this.getAttackAccess(gameState); + let dist = Math.min(); + if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) + { + rallyPoint = this.rallyPoint; + dist = SquareVectorDistance(this.position, rallyPoint); + } + // Then check if we have a nearer base (in case this attack has captured one) + for (const base of gameState.ai.HQ.baseManagers()) + { + if (!base.anchor || !base.anchor.position()) + continue; + if (getLandAccess(gameState, base.anchor) != access) + continue; + const newdist = SquareVectorDistance(this.position, base.anchor.position()); + if (newdist > dist) + continue; + dist = newdist; + rallyPoint = base.anchor.position(); } } - if (this.Config.debug > 1) - aiWarn("We will help one of our other attacks"); - } - this.targetPos = this.target.position(); - } - return true; -}; -/** reset any units */ -AttackPlan.prototype.Abort = function(gameState) -{ - this.unitCollection.unregister(); - if (this.unitCollection.hasEntities()) - { - // If the attack was started, look for a good rallyPoint to withdraw - let rallyPoint; - if (this.isStarted()) - { - const access = this.getAttackAccess(gameState); - let dist = Math.min(); - if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) + for (const ent of this.unitCollection.values()) { - rallyPoint = this.rallyPoint; - dist = SquareVectorDistance(this.position, rallyPoint); + if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_ATTACK) + ent.stopMoving(); + if (rallyPoint) + ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); + this.removeUnit(ent); } - // Then check if we have a nearer base (in case this attack has captured one) + } + + for (const unitCat in this.unitStat) + this.unit[unitCat].unregister(); + + this.removeQueues(gameState); + } + + removeUnit(ent, update) + { + if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_ATTACK) + { + if (ent.hasClass("CitizenSoldier")) + ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); + else + ent.setMetadata(PlayerID, "role", undefined); + ent.setMetadata(PlayerID, "subrole", undefined); + } + ent.setMetadata(PlayerID, "plan", -1); + if (update) + this.unitCollection.updateEnt(ent); + } + + checkEvents(gameState, events) + { + for (const evt of events.EntityRenamed) + { + if (!this.target || this.target.id() != evt.entity) + continue; + if (this.type === AttackPlan.TYPE_RAID && !this.isStarted()) + this.target = undefined; + else + this.target = gameState.getEntityById(evt.newentity); + if (this.target) + this.targetPos = this.target.position(); + } + + for (const evt of events.OwnershipChanged) // capture event + if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) + this.target = undefined; + + for (const evt of events.PlayerDefeated) + { + if (this.targetPlayer !== evt.playerId) + continue; + this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); + this.target = undefined; + } + + this.unitCollection = gameState.getOwnUnits().filter( + filters.byMetadata(PlayerID, "plan", this.name)); + this.unitCollection.registerUpdates(); + + if (!this.overseas || this.state !== AttackPlan.STATE_UNEXECUTED) + return; + // let's check if an enemy has built a structure at our access + for (const evt of events.Create) + { + const ent = gameState.getEntityById(evt.entity); + if (!ent || !ent.position() || !ent.hasClass("Structure")) + continue; + if (!gameState.isPlayerEnemy(ent.owner())) + continue; + const access = getLandAccess(gameState, ent); for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; - if (getLandAccess(gameState, base.anchor) != access) + if (base.accessIndex != access) continue; - const newdist = SquareVectorDistance(this.position, base.anchor.position()); - if (newdist > dist) - continue; - dist = newdist; - rallyPoint = base.anchor.position(); + this.overseas = 0; + this.rallyPoint = base.anchor.position(); } } + } + waitingForTransport() + { + for (const ent of this.unitCollection.values()) + if (ent.getMetadata(PlayerID, "transport") !== undefined) + return true; + return false; + } + + hasSiegeUnits() + { + for (const ent of this.unitCollection.values()) + if (isSiegeUnit(ent)) + return true; + return false; + } + + hasForceOrder(data, value) + { for (const ent of this.unitCollection.values()) { - if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_ATTACK) - ent.stopMoving(); - if (rallyPoint) - ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); - this.removeUnit(ent); + if (data && +ent.getMetadata(PlayerID, data) !== value) + continue; + const orders = ent.unitAIOrderData(); + for (const order of orders) + if (order.force) + return true; } + return false; } - for (const unitCat in this.unitStat) - this.unit[unitCat].unregister(); - - this.removeQueues(gameState); -}; - -AttackPlan.prototype.removeUnit = function(ent, update) -{ - if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_ATTACK) + /** + * The center position of this attack may be in an inaccessible area. So we use the access + * of the unit nearest to this center position. + */ + getAttackAccess(gameState) { - if (ent.hasClass("CitizenSoldier")) - ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); - else - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - } - ent.setMetadata(PlayerID, "plan", -1); - if (update) - this.unitCollection.updateEnt(ent); -}; + for (const ent of this.unitCollection.filterNearest(this.position, 1).values()) + return getLandAccess(gameState, ent); -AttackPlan.prototype.checkEvents = function(gameState, events) -{ - for (const evt of events.EntityRenamed) - { - if (!this.target || this.target.id() != evt.entity) - continue; - if (this.type === AttackPlan.TYPE_RAID && !this.isStarted()) - this.target = undefined; - else - this.target = gameState.getEntityById(evt.newentity); - if (this.target) - this.targetPos = this.target.position(); + return 0; } - for (const evt of events.OwnershipChanged) // capture event - if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) - this.target = undefined; - - for (const evt of events.PlayerDefeated) + debugAttack() { - if (this.targetPlayer !== evt.playerId) - continue; - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - this.target = undefined; - } - - this.unitCollection = gameState.getOwnUnits().filter( - filters.byMetadata(PlayerID, "plan", this.name)); - this.unitCollection.registerUpdates(); - - if (!this.overseas || this.state !== AttackPlan.STATE_UNEXECUTED) - return; - // let's check if an enemy has built a structure at our access - for (const evt of events.Create) - { - const ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.position() || !ent.hasClass("Structure")) - continue; - if (!gameState.isPlayerEnemy(ent.owner())) - continue; - const access = getLandAccess(gameState, ent); - for (const base of gameState.ai.HQ.baseManagers()) + aiWarn("---------- attack " + this.name); + for (const unitCat in this.unitStat) { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - this.overseas = 0; - this.rallyPoint = base.anchor.position(); + const Unit = this.unitStat[unitCat]; + aiWarn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + + Unit.targetSize); } + aiWarn("------------------------------"); } -}; -AttackPlan.prototype.waitingForTransport = function() -{ - for (const ent of this.unitCollection.values()) - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return true; - return false; -}; - -AttackPlan.prototype.hasSiegeUnits = function() -{ - for (const ent of this.unitCollection.values()) - if (isSiegeUnit(ent)) - return true; - return false; -}; - -AttackPlan.prototype.hasForceOrder = function(data, value) -{ - for (const ent of this.unitCollection.values()) + Serialize() { - if (data && +ent.getMetadata(PlayerID, data) !== value) - continue; - const orders = ent.unitAIOrderData(); - for (const order of orders) - if (order.force) - return true; + const properties = { + "name": this.name, + "type": this.type, + "state": this.state, + "forced": this.forced, + "rallyPoint": clone(this.rallyPoint), + "overseas": this.overseas, + "paused": this.paused, + "maxCompletingTime": this.maxCompletingTime, + "neededShips": this.neededShips, + "unitStat": this.unitStat, + "siegeState": this.siegeState, + "position5TurnsAgo": this.position5TurnsAgo, + "lastPosition": this.lastPosition, + "position": this.position !== undefined ? clone(this.position) : undefined, + "isBlocked": this.isBlocked, + "targetPlayer": this.targetPlayer, + "target": this.target !== undefined ? this.target.id() : undefined, + "targetPos": this.targetPos !== undefined ? clone(this.targetPos) : undefined, + "uniqueTargetId": this.uniqueTargetId, + "path": this.path, + "unitCollUpdateArray": this.unitCollUpdateArray + }; + + return { "properties": properties }; } - return false; -}; -/** - * The center position of this attack may be in an inaccessible area. So we use the access - * of the unit nearest to this center position. - */ -AttackPlan.prototype.getAttackAccess = function(gameState) -{ - for (const ent of this.unitCollection.filterNearest(this.position, 1).values()) - return getLandAccess(gameState, ent); - - return 0; -}; - -AttackPlan.prototype.debugAttack = function() -{ - aiWarn("---------- attack " + this.name); - for (const unitCat in this.unitStat) + Deserialize(gameState, data) { - const Unit = this.unitStat[unitCat]; - aiWarn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + - Unit.targetSize); + for (const key in data.properties) + this[key] = data.properties[key]; + + if (this.target) + this.target = gameState.getEntityById(this.target); + + this.failed = undefined; } - aiWarn("------------------------------"); -}; - -AttackPlan.prototype.Serialize = function() -{ - const properties = { - "name": this.name, - "type": this.type, - "state": this.state, - "forced": this.forced, - "rallyPoint": clone(this.rallyPoint), - "overseas": this.overseas, - "paused": this.paused, - "maxCompletingTime": this.maxCompletingTime, - "neededShips": this.neededShips, - "unitStat": this.unitStat, - "siegeState": this.siegeState, - "position5TurnsAgo": this.position5TurnsAgo, - "lastPosition": this.lastPosition, - "position": this.position !== undefined ? clone(this.position) : undefined, - "isBlocked": this.isBlocked, - "targetPlayer": this.targetPlayer, - "target": this.target !== undefined ? this.target.id() : undefined, - "targetPos": this.targetPos !== undefined ? clone(this.targetPos) : undefined, - "uniqueTargetId": this.uniqueTargetId, - "path": this.path, - "unitCollUpdateArray": this.unitCollUpdateArray - }; - - return { "properties": properties }; -}; - -AttackPlan.prototype.Deserialize = function(gameState, data) -{ - for (const key in data.properties) - this[key] = data.properties[key]; - - if (this.target) - this.target = gameState.getEntityById(this.target); - - this.failed = undefined; -}; +} diff --git a/binaries/data/mods/public/simulation/ai/petra/baseManager.js b/binaries/data/mods/public/simulation/ai/petra/baseManager.js index 1819962bbf..59b7b2cce6 100644 --- a/binaries/data/mods/public/simulation/ai/petra/baseManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/baseManager.js @@ -20,926 +20,757 @@ import { Worker } from "simulation/ai/petra/worker.js"; * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) */ -export function BaseManager(gameState, basesManager) +export class BaseManager { - this.Config = basesManager.Config; - this.ID = gameState.ai.uniqueIDs.bases++; - this.basesManager = basesManager; - - // anchor building: seen as the main building of the base. Needs to have territorial influence - this.anchor = undefined; - this.anchorId = undefined; - this.accessIndex = undefined; - - // Maximum distance (from any dropsite) to look for resources - // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max - this.maxDistResourceSquare = 360*360; - - this.constructing = false; - // Defenders to train in this cc when its construction is finished - this.neededDefenders = this.Config.difficulty > difficulty.EASY ? 3 + 2*(this.Config.difficulty - 3) : 0; - - // vector for iterating, to check one use the HQ map. - this.territoryIndices = []; - - this.timeNextIdleCheck = 0; -} - - -BaseManager.STATE_WITH_ANCHOR = "anchored"; - -/** - * New base with a foundation anchor. - */ -BaseManager.STATE_UNCONSTRUCTED = "unconstructed"; - -/** - * Captured base with an anchor. - */ -BaseManager.STATE_CAPTURED = "captured"; - -/** - * Anchorless base, currently with dock. - */ -BaseManager.STATE_ANCHORLESS = "anchorless"; - -BaseManager.prototype.init = function(gameState, state) -{ - if (state === BaseManager.STATE_UNCONSTRUCTED) - this.constructing = true; - else if (state !== BaseManager.STATE_CAPTURED) - this.neededDefenders = 0; - this.workerObject = new Worker(this); - // entitycollections - this.units = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "base", this.ID)); - this.workers = this.units.filter(filters.byMetadata(PlayerID, "role", Worker.ROLE_WORKER)); - this.buildings = gameState.getOwnStructures().filter(filters.byMetadata(PlayerID, "base", this.ID)); - this.mobileDropsites = this.units.filter(filters.isDropsite()); - - this.units.registerUpdates(); - this.workers.registerUpdates(); - this.buildings.registerUpdates(); - this.mobileDropsites.registerUpdates(); - - // array of entity IDs, with each being - this.dropsites = {}; - this.dropsiteSupplies = {}; - this.gatherers = {}; - for (const res of Resources.GetCodes()) + constructor(gameState, basesManager) { - this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; - this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; - } -}; + this.Config = basesManager.Config; + this.ID = gameState.ai.uniqueIDs.bases++; + this.basesManager = basesManager; + + // anchor building: seen as the main building of the base. Needs to have territorial influence + this.anchor = undefined; + this.anchorId = undefined; + this.accessIndex = undefined; + + // Maximum distance (from any dropsite) to look for resources + // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max + this.maxDistResourceSquare = 360*360; -BaseManager.prototype.reset = function(gameState, state) -{ - if (state === BaseManager.STATE_UNCONSTRUCTED) - this.constructing = true; - else this.constructing = false; - if (state !== BaseManager.STATE_CAPTURED || this.Config.difficulty < difficulty.MEDIUM) - this.neededDefenders = 0; - else - this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); -}; + // Defenders to train in this cc when its construction is finished + this.neededDefenders = this.Config.difficulty > difficulty.EASY ? 3 + 2*(this.Config.difficulty - 3) : 0; -BaseManager.prototype.assignEntity = function(gameState, ent) -{ - ent.setMetadata(PlayerID, "base", this.ID); - this.units.updateEnt(ent); - this.workers.updateEnt(ent); - this.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit")) - this.assignResourceToDropsite(gameState, ent, false); -}; + // vector for iterating, to check one use the HQ map. + this.territoryIndices = []; -BaseManager.prototype.setAnchor = function(gameState, anchorEntity, deserialize = false) -{ - if (!anchorEntity.hasClass("CivCentre")) - { - aiWarn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + - " as anchor."); + this.timeNextIdleCheck = 0; } - else + + + static STATE_WITH_ANCHOR = "anchored"; + + /** + * New base with a foundation anchor. + */ + static STATE_UNCONSTRUCTED = "unconstructed"; + + /** + * Captured base with an anchor. + */ + static STATE_CAPTURED = "captured"; + + /** + * Anchorless base, currently with dock. + */ + static STATE_ANCHORLESS = "anchorless"; + + init(gameState, state) { - this.anchor = anchorEntity; - this.anchorId = anchorEntity.id(); - if (!deserialize) - this.anchor.setMetadata(PlayerID, "baseAnchor", true); - this.basesManager.resetBaseCache(); + if (state === BaseManager.STATE_UNCONSTRUCTED) + this.constructing = true; + else if (state !== BaseManager.STATE_CAPTURED) + this.neededDefenders = 0; + this.workerObject = new Worker(this); + // entitycollections + this.units = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "base", this.ID)); + this.workers = this.units.filter(filters.byMetadata(PlayerID, "role", Worker.ROLE_WORKER)); + this.buildings = gameState.getOwnStructures().filter(filters.byMetadata(PlayerID, "base", this.ID)); + this.mobileDropsites = this.units.filter(filters.isDropsite()); + + this.units.registerUpdates(); + this.workers.registerUpdates(); + this.buildings.registerUpdates(); + this.mobileDropsites.registerUpdates(); + + // array of entity IDs, with each being + this.dropsites = {}; + this.dropsiteSupplies = {}; + this.gatherers = {}; + for (const res of Resources.GetCodes()) + { + this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; + this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; + } } - if (!deserialize) - anchorEntity.setMetadata(PlayerID, "base", this.ID); - this.buildings.updateEnt(anchorEntity); - this.accessIndex = getLandAccess(gameState, anchorEntity); - return true; -}; -/* we lost our anchor. Let's reassign our units and buildings */ -BaseManager.prototype.anchorLost = function(gameState) -{ - this.anchor = undefined; - this.anchorId = undefined; - this.neededDefenders = 0; - this.basesManager.resetBaseCache(); -}; - -/** Set a building of an anchorless base */ -BaseManager.prototype.setAnchorlessEntity = function(gameState, ent, deserialize = false) -{ - if (!this.buildings.hasEntities()) + reset(gameState, state) { - if (!getBuiltEntity(gameState, ent).resourceDropsiteTypes()) + if (state === BaseManager.STATE_UNCONSTRUCTED) + this.constructing = true; + else + this.constructing = false; + if (state !== BaseManager.STATE_CAPTURED || this.Config.difficulty < difficulty.MEDIUM) + this.neededDefenders = 0; + else + this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); + } + + assignEntity(gameState, ent) + { + ent.setMetadata(PlayerID, "base", this.ID); + this.units.updateEnt(ent); + this.workers.updateEnt(ent); + this.buildings.updateEnt(ent); + if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit")) + this.assignResourceToDropsite(gameState, ent, false); + } + + setAnchor(gameState, anchorEntity, deserialize = false) + { + if (!anchorEntity.hasClass("CivCentre")) { aiWarn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + - " as origin."); + " as anchor."); } - this.accessIndex = getLandAccess(gameState, ent); - } - else if (this.accessIndex !== getLandAccess(gameState, ent)) - { - aiWarn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + - " has been assigned " + ent.templateName() + " with access" + - getLandAccess(gameState, ent)); - } - - if (!deserialize) - ent.setMetadata(PlayerID, "base", this.ID); - this.buildings.updateEnt(ent); - return true; -}; - -/** - * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area. - * Moving resources (animals) and buildable resources (fields) are treated elsewhere. - */ -BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite, deserialized) -{ - if (this.dropsites[dropsite.id()]) - { - if (this.Config.debug > 0) - warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); - return; - } - - let accessIndex = this.accessIndex; - const dropsitePos = dropsite.position(); - const dropsiteId = dropsite.id(); - this.dropsites[dropsiteId] = true; - - if (this.ID == this.basesManager.baselessBase().ID) - accessIndex = getLandAccess(gameState, dropsite); - - const maxDistResourceSquare = this.maxDistResourceSquare; - for (const type of dropsite.resourceDropsiteTypes()) - { - const resources = gameState.getResourceSupplies(type); - if (!resources.length) - continue; - - const nearby = this.dropsiteSupplies[type].nearby; - const medium = this.dropsiteSupplies[type].medium; - const faraway = this.dropsiteSupplies[type].faraway; - - resources.forEach(function(supply) + else { - if (!supply.position()) - return; - // Moving resources and fields are treated differently. - if (supply.hasClasses(["Animal", "Field"])) - return; - // quick accessibility check - if (getLandAccess(gameState, supply) != accessIndex) - return; + this.anchor = anchorEntity; + this.anchorId = anchorEntity.id(); + if (!deserialize) + this.anchor.setMetadata(PlayerID, "baseAnchor", true); + this.basesManager.resetBaseCache(); + } + if (!deserialize) + anchorEntity.setMetadata(PlayerID, "base", this.ID); + this.buildings.updateEnt(anchorEntity); + this.accessIndex = getLandAccess(gameState, anchorEntity); + return true; + } - const dist = SquareVectorDistance(supply.position(), dropsitePos); - if (dist < maxDistResourceSquare) + /* we lost our anchor. Let's reassign our units and buildings */ + anchorLost(gameState) + { + this.anchor = undefined; + this.anchorId = undefined; + this.neededDefenders = 0; + this.basesManager.resetBaseCache(); + } + + /** Set a building of an anchorless base */ + setAnchorlessEntity(gameState, ent, deserialize = false) + { + if (!this.buildings.hasEntities()) + { + if (!getBuiltEntity(gameState, ent).resourceDropsiteTypes()) { - if (dist < maxDistResourceSquare/16) // distmax/4 - nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "dist": dist }); - else if (dist < maxDistResourceSquare/4) // distmax/2 - medium.push({ "dropsite": dropsiteId, "id": supply.id(), "dist": dist }); - else - faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "dist": dist }); + aiWarn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + + " as origin."); } - }); - - nearby.sort((r1, r2) => r1.dist - r2.dist); - medium.sort((r1, r2) => r1.dist - r2.dist); - faraway.sort((r1, r2) => r1.dist - r2.dist); - - /* - let debug = false; - if (debug) - { - faraway.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.id], "rgb": [2,0,0]}); - }); - medium.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.id], "rgb": [0,2,0]}); - }); - nearby.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.id], "rgb": [0,0,2]}); - }); + this.accessIndex = getLandAccess(gameState, ent); } - */ + else if (this.accessIndex !== getLandAccess(gameState, ent)) + { + aiWarn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + + " has been assigned " + ent.templateName() + " with access" + + getLandAccess(gameState, ent)); + } + + if (!deserialize) + ent.setMetadata(PlayerID, "base", this.ID); + this.buildings.updateEnt(ent); + return true; } - if (deserialized) - return; - - // Allows all allies to use this dropsite except if base anchor to be sure to keep - // a minimum of resources for this base - Engine.PostCommand(PlayerID, { - "type": "set-dropsite-sharing", - "entities": [dropsiteId], - "shared": dropsiteId != this.anchorId - }); -}; - -BaseManager.prototype.removeFromAssignedDropsite = function(entityID) -{ - for (const type in this.dropsiteSupplies) - for (const proxim in this.dropsiteSupplies[type]) + /** + * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area. + * Moving resources (animals) and buildable resources (fields) are treated elsewhere. + */ + assignResourceToDropsite(gameState, dropsite, deserialized) + { + if (this.dropsites[dropsite.id()]) { - const resourcesList = this.dropsiteSupplies[type][proxim]; - for (let i = 0; i < resourcesList.length; ++i) - { - if (resourcesList[i].id === entityID) - resourcesList.splice(i--, 1); - } + if (this.Config.debug > 0) + warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); + return; } -}; -// completely remove the dropsite resources from our list. -BaseManager.prototype.removeDropsite = function(gameState, entityID) -{ - if (!entityID) - return; + let accessIndex = this.accessIndex; + const dropsitePos = dropsite.position(); + const dropsiteId = dropsite.id(); + this.dropsites[dropsiteId] = true; - const removeSupply = function(supply) - { - for (let i = 0; i < supply.length; ++i) + if (this.ID == this.basesManager.baselessBase().ID) + accessIndex = getLandAccess(gameState, dropsite); + + const maxDistResourceSquare = this.maxDistResourceSquare; + for (const type of dropsite.resourceDropsiteTypes()) { - // exhausted resource, remove it from this list - if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) - supply.splice(i--, 1); - // resource assigned to the removed dropsite, remove it - else if (supply[i].dropsite == entityID) - supply.splice(i--, 1); - } - }; - - for (const type in this.dropsiteSupplies) - { - removeSupply(this.dropsiteSupplies[type].nearby); - removeSupply(this.dropsiteSupplies[type].medium); - removeSupply(this.dropsiteSupplies[type].faraway); - } - - this.dropsites[entityID] = undefined; -}; - -/** - * @return {Object} - The position of the best place to build a new dropsite for the specified resource, - * its quality and its template name. - */ -BaseManager.prototype.findBestDropsiteAndLocation = function(gameState, resource) -{ - let bestResult = { - "quality": 0, - "pos": [0, 0] - }; - for (const templateName of gameState.ai.HQ.buildManager.findStructuresByFilter(gameState, - filters.isDropsite(resource))) - { - const dp = this.findBestDropsiteLocation(gameState, resource, templateName); - if (dp.quality < bestResult.quality) - continue; - bestResult = dp; - bestResult.templateName = templateName; - } - return bestResult; -}; - -/** - * Returns the position of the best place to build a new dropsite for the specified resource and dropsite template. - */ -BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource, templateName) -{ - const template = gameState.getTemplate(gameState.applyCiv(templateName)); - - // CCs and Docks are handled elsewhere. - if (template.hasClasses(["CivCentre", "Dock"])) - return { "quality": 0, "pos": [0, 0] }; - - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then checks for a good spot in the territory. If none, and town/city phase, checks outside - // The AI will currently not build a CC if it wouldn't connect with an existing CC. - - const obstructions = createObstructionMap(gameState, this.accessIndex, template); - - const dpEnts = gameState.getOwnStructures().filter(filters.isDropsite(resource)).toEntityArray(); - - // Foundations don't have the dropsite properties yet, so treat them separately. - for (const foundation of gameState.getOwnFoundations().toEntityArray()) - if (getBuiltEntity(gameState, foundation).isResourceDropsite(resource)) - dpEnts.push(foundation); - - let bestIdx; - let bestVal = 0; - const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - const territoryMap = gameState.ai.HQ.territoryMap; - const width = territoryMap.width; - const cellSize = territoryMap.cellSize; - - const droppableResources = template.resourceDropsiteTypes(); - - for (const j of this.territoryIndices) - { - const i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) // no room around - continue; - - // We add 3 times the needed resource and once others that can be dropped here. - let total = 2 * gameState.sharedScript.resourceMaps[resource].map[j]; - for (const res in gameState.sharedScript.resourceMaps) - if (droppableResources.indexOf(res) != -1) - total += gameState.sharedScript.resourceMaps[res].map[j]; - - total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 - if (total <= bestVal) - continue; - - const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - for (const dp of dpEnts) - { - const dpPos = dp.position(); - if (!dpPos) + const resources = gameState.getResourceSupplies(type); + if (!resources.length) continue; - const dist = SquareVectorDistance(dpPos, pos); - if (dist < 3600) + + const nearby = this.dropsiteSupplies[type].nearby; + const medium = this.dropsiteSupplies[type].medium; + const faraway = this.dropsiteSupplies[type].faraway; + + resources.forEach(function(supply) { - total = 0; - break; - } - else if (dist < 6400) - total *= (Math.sqrt(dist)-60)/20; - } - if (total <= bestVal) - continue; - - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = total; - bestIdx = i; - } - - if (this.Config.debug > 2) - warn(" for dropsite best is " + bestVal); - - if (bestVal <= 0) - return { "quality": bestVal, "pos": [0, 0] }; - - const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return { "quality": bestVal, "pos": [x, z] }; -}; - -BaseManager.prototype.getResourceLevel = function(gameState, type, distances = ["nearby", "medium", "faraway"]) -{ - let count = 0; - const check = {}; - for (const proxim of distances) - for (const supply of this.dropsiteSupplies[type][proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - const supplyEntity = gameState.getEntityById(supply.id); - if (supplyEntity) - count += supplyEntity.resourceSupplyAmount(); - } - return count; -}; - -/** check our resource levels and react accordingly */ -BaseManager.prototype.checkResourceLevels = function(gameState, queues) -{ - for (const type of Resources.GetCodes()) - { - if (type == "food") - { - const prox = ["nearby"]; - if (gameState.currentPhase() < 2) - prox.push("medium"); - if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms. - { - const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted - const numFarms = gameState.getOwnStructures().filter(filters.byClass("Field")) - .length; // including foundations - const numQueue = queues.field.countQueuedUnits(); - - // TODO if not yet farms, add a check on time used/lost and build farmstead if needed - if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them - { - if (count < 600) - { - queues.field.addPlan(new ConstructionPlan(gameState, - "structures/{civ}/field", { "favoredBase": this.ID })); - gameState.ai.HQ.needFarm = true; - } - } - else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) - { - const numFound = gameState.getOwnFoundations().filter(filters.byClass("Field")) - .length; - let goal = this.Config.Economy.provisionFields; - if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) - goal = Math.max(goal-1, 1); - if (numFound + numQueue < goal) - { - queues.field.addPlan(new ConstructionPlan(gameState, - "structures/{civ}/field", { "favoredBase": this.ID })); - } - } - else if (gameState.ai.HQ.needCorral && - !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && - !queues.corral.hasQueuedUnits() && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) - { - queues.corral.addPlan(new ConstructionPlan(gameState, - "structures/{civ}/corral", { "favoredBase": this.ID })); - } - continue; - } - if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && - !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) - { - const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted - if (count < 900) - { - queues.corral.addPlan(new ConstructionPlan(gameState, - "structures/{civ}/corral", { "favoredBase": this.ID })); - gameState.ai.HQ.needCorral = true; - } - } - continue; - } - // Non food stuff - if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() || - gameState.getOwnFoundations().filter(filters.byClass("Storehouse")).hasEntities()) - { - this.gatherers[type].nextCheck = gameState.ai.playedTurn; - this.gatherers[type].used = 0; - this.gatherers[type].lost = 0; - continue; - } - if (gameState.ai.playedTurn < this.gatherers[type].nextCheck) - continue; - for (const ent of this.gatherersByType(gameState, type).values()) - { - if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") - ++this.gatherers[type].used; - else if (ent.unitAIState() == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") - ++this.gatherers[type].lost; - } - // TODO add also a test on remaining resources. - const total = this.gatherers[type].used + this.gatherers[type].lost; - if (total > 150 || total > 60 && type != "wood") - { - const ratio = this.gatherers[type].lost / total; - if (ratio > 0.15) - { - const newDP = this.findBestDropsiteAndLocation(gameState, type); - if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) - { - queues.dropsites.addPlan(new ConstructionPlan(gameState, newDP.templateName, - { "base": this.ID, "type": type }, newDP.pos)); - } - else if (!gameState.getOwnFoundations().filter(filters.byClass("CivCentre")) - .hasEntities() && - !queues.civilCentre.hasQueuedUnits()) - { - // No good dropsite, try to build a new base if no base already planned, - // and if not possible, be less strict on dropsite quality. - if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && - newDP.quality > Math.min(25, 50*0.15/ratio) && - gameState.ai.HQ.canBuild(gameState, newDP.templateName)) - { - queues.dropsites.addPlan(new ConstructionPlan(gameState, - newDP.templateName, { "base": this.ID, "type": type }, - newDP.pos)); - } - } - } - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; - this.gatherers[type].used = 0; - this.gatherers[type].lost = 0; - } - else if (total == 0) - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; - } - -}; - -/** Adds the estimated gather rates from this base to the currentRates */ -BaseManager.prototype.addGatherRates = function(gameState, currentRates) -{ - for (const res in currentRates) - { - // I calculate the exact gathering rate for each unit. - // I must then lower that to account for travel time. - // Given that the faster you gather, the more travel time matters, - // I use some logarithms. - // TODO: this should take into account for unit speed and/or distance to target - - this.gatherersByType(gameState, res).forEach(ent => - { - if (ent.isIdle() || !ent.position()) - return; - const gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - if (res == "food") - { - this.workersBySubrole(gameState, Worker.SUBROLE_HUNTER).forEach(ent => - { - if (ent.isIdle() || !ent.position()) + if (!supply.position()) return; - const gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - this.workersBySubrole(gameState, Worker.SUBROLE_FISHER).forEach(ent => - { - if (ent.isIdle() || !ent.position()) + // Moving resources and fields are treated differently. + if (supply.hasClasses(["Animal", "Field"])) return; - const gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; + // quick accessibility check + if (getLandAccess(gameState, supply) != accessIndex) + return; + + const dist = SquareVectorDistance(supply.position(), dropsitePos); + if (dist < maxDistResourceSquare) + { + if (dist < maxDistResourceSquare/16) // distmax/4 + nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "dist": dist }); + else if (dist < maxDistResourceSquare/4) // distmax/2 + medium.push({ "dropsite": dropsiteId, "id": supply.id(), "dist": dist }); + else + faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "dist": dist }); + } }); - } - } -}; -BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) -{ - if (!roleless) - roleless = this.units.filter(filters.not(filters.byHasMetadata(PlayerID, "role"))).values(); + nearby.sort((r1, r2) => r1.dist - r2.dist); + medium.sort((r1, r2) => r1.dist - r2.dist); + faraway.sort((r1, r2) => r1.dist - r2.dist); - for (const ent of roleless) - { - if (ent.hasClasses(["Worker", "CitizenSoldier", "FishingBoat"])) - ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); - else if (ent.hasClass("Support") && ent.hasClass("Elephant")) - ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); - } -}; - -/** - * If the numbers of workers on the resources is unbalanced then set some of workers to idle so - * they can be reassigned by reassignIdleWorkers. - * TODO: actually this probably should be in the HQ. - */ -BaseManager.prototype.setWorkersIdleByPriority = function(gameState) -{ - this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; - // change resource only towards one which is more needed, and if changing will not change this order - let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) - const mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - let sumWanted = 0; - let sumCurrent = 0; - for (const need of mostNeeded) - { - sumWanted += need.wanted; - sumCurrent += need.current; - } - let scale = 1; - if (sumWanted > 0) - scale = sumCurrent / sumWanted; - - for (let i = mostNeeded.length-1; i > 0; --i) - { - const lessNeed = mostNeeded[i]; - for (let j = 0; j < i; ++j) - { - const moreNeed = mostNeeded[j]; - const lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - // Ensure that the most wanted resource is not exhausted - if (moreNeed.type != "food" && this.basesManager.isResourceExhausted(moreNeed.type)) + /* + let debug = false; + if (debug) { - if (lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) + faraway.forEach(function(res){ + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.id], "rgb": [2,0,0]}); + }); + medium.forEach(function(res){ + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.id], "rgb": [0,2,0]}); + }); + nearby.forEach(function(res){ + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.id], "rgb": [0,0,2]}); + }); + } + */ + } + + if (deserialized) + return; + + // Allows all allies to use this dropsite except if base anchor to be sure to keep + // a minimum of resources for this base + Engine.PostCommand(PlayerID, { + "type": "set-dropsite-sharing", + "entities": [dropsiteId], + "shared": dropsiteId != this.anchorId + }); + } + + removeFromAssignedDropsite(entityID) + { + for (const type in this.dropsiteSupplies) + for (const proxim in this.dropsiteSupplies[type]) + { + const resourcesList = this.dropsiteSupplies[type][proxim]; + for (let i = 0; i < resourcesList.length; ++i) + { + if (resourcesList[i].id === entityID) + resourcesList.splice(i--, 1); + } + } + } + + // completely remove the dropsite resources from our list. + removeDropsite(gameState, entityID) + { + if (!entityID) + return; + + const removeSupply = function(supply) + { + for (let i = 0; i < supply.length; ++i) + { + // exhausted resource, remove it from this list + if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) + supply.splice(i--, 1); + // resource assigned to the removed dropsite, remove it + else if (supply[i].dropsite == entityID) + supply.splice(i--, 1); + } + }; + + for (const type in this.dropsiteSupplies) + { + removeSupply(this.dropsiteSupplies[type].nearby); + removeSupply(this.dropsiteSupplies[type].medium); + removeSupply(this.dropsiteSupplies[type].faraway); + } + + this.dropsites[entityID] = undefined; + } + + /** + * @return {Object} - The position of the best place to build a new dropsite for the specified resource, + * its quality and its template name. + */ + findBestDropsiteAndLocation(gameState, resource) + { + let bestResult = { + "quality": 0, + "pos": [0, 0] + }; + for (const templateName of gameState.ai.HQ.buildManager.findStructuresByFilter(gameState, + filters.isDropsite(resource))) + { + const dp = this.findBestDropsiteLocation(gameState, resource, templateName); + if (dp.quality < bestResult.quality) + continue; + bestResult = dp; + bestResult.templateName = templateName; + } + return bestResult; + } + + /** + * Returns the position of the best place to build a new dropsite for the specified resource and dropsite template. + */ + findBestDropsiteLocation(gameState, resource, templateName) + { + const template = gameState.getTemplate(gameState.applyCiv(templateName)); + + // CCs and Docks are handled elsewhere. + if (template.hasClasses(["CivCentre", "Dock"])) + return { "quality": 0, "pos": [0, 0] }; + + let halfSize = 0; + if (template.get("Footprint/Square")) + halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; + else if (template.get("Footprint/Circle")) + halfSize = +template.get("Footprint/Circle/@radius"); + + // This builds a map. The procedure is fairly simple. It adds the resource maps + // (which are dynamically updated and are made so that they will facilitate DP placement) + // Then checks for a good spot in the territory. If none, and town/city phase, checks outside + // The AI will currently not build a CC if it wouldn't connect with an existing CC. + + const obstructions = createObstructionMap(gameState, this.accessIndex, template); + + const dpEnts = gameState.getOwnStructures().filter(filters.isDropsite(resource)).toEntityArray(); + + // Foundations don't have the dropsite properties yet, so treat them separately. + for (const foundation of gameState.getOwnFoundations().toEntityArray()) + if (getBuiltEntity(gameState, foundation).isResourceDropsite(resource)) + dpEnts.push(foundation); + + let bestIdx; + let bestVal = 0; + const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); + + const territoryMap = gameState.ai.HQ.territoryMap; + const width = territoryMap.width; + const cellSize = territoryMap.cellSize; + + const droppableResources = template.resourceDropsiteTypes(); + + for (const j of this.territoryIndices) + { + const i = territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) // no room around + continue; + + // We add 3 times the needed resource and once others that can be dropped here. + let total = 2 * gameState.sharedScript.resourceMaps[resource].map[j]; + for (const res in gameState.sharedScript.resourceMaps) + if (droppableResources.indexOf(res) != -1) + total += gameState.sharedScript.resourceMaps[res].map[j]; + + total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 + if (total <= bestVal) + continue; + + const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + + for (const dp of dpEnts) + { + const dpPos = dp.position(); + if (!dpPos) continue; - - // And if so, move the gatherer to the less wanted one. - nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb); - if (nb == 0) - return; - } - - // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1 - // but we require a bit more to avoid too frequent changes - if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 || - lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) - { - nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb); - if (nb == 0) - return; - } - } - } -}; - -/** - * Switch some gatherers (limited to number) from resource "from" to resource "to" - * and return remaining number of possible switches. - * Prefer Civilian for food and CitizenSoldier for other resources. - */ -BaseManager.prototype.switchGatherer = function(gameState, from, to, number) -{ - let num = number; - let only; - const gatherers = this.gatherersByType(gameState, from); - if (from == "food" && gatherers.filter(filters.byClass("CitizenSoldier")).hasEntities()) - only = "CitizenSoldier"; - else if (to == "food" && gatherers.filter(filters.byClass("Civilian")).hasEntities()) - only = "Civilian"; - - for (const ent of gatherers.values()) - { - if (num == 0) - return num; - if (!ent.canGather(to)) - continue; - if (only && !ent.hasClass(only)) - continue; - --num; - ent.stopMoving(); - ent.setMetadata(PlayerID, "gather-type", to); - this.basesManager.AddTCResGatherer(to); - } - return num; -}; - -BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) -{ - // Search for idle workers, and tell them to gather resources based on demand - if (!idleWorkers) - { - const filter = filters.byMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); - } - - for (const ent of idleWorkers) - { - // Check that the worker isn't garrisoned - if (!ent.position()) - continue; - // Support elephant can only be builders - if (ent.hasClass("Support") && ent.hasClass("Elephant")) - { - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - continue; - } - - if (ent.hasClass("Worker")) - { - // Just emergency repairing here. It is better managed in assignToFoundations - if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && - gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) - ent.repair(this.anchor); - else if (ent.isGatherer()) - { - const mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - for (const needed of mostNeeded) + const dist = SquareVectorDistance(dpPos, pos); + if (dist < 3600) { - if (!ent.canGather(needed.type)) - continue; - const lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - if (needed.type != "food" && this.basesManager.isResourceExhausted(needed.type)) - continue; - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_GATHERER); - ent.setMetadata(PlayerID, "gather-type", needed.type); - this.basesManager.AddTCResGatherer(needed.type); + total = 0; break; } + else if (dist < 6400) + total *= (Math.sqrt(dist)-60)/20; } - } - else if (isFastMoving(ent) && ent.canGather("food") && ent.canAttackClass("Animal")) - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_HUNTER); - else if (ent.hasClass("FishingBoat")) - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_FISHER); - } -}; - -BaseManager.prototype.workersBySubrole = function(gameState, subrole) -{ - return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, - filters.byMetadata(PlayerID, "subrole", subrole), this.workers); -}; - -BaseManager.prototype.gatherersByType = function(gameState, type) -{ - return gameState.updatingCollection("workers-gathering-" + type +"-base-" + - this.ID, filters.byMetadata(PlayerID, "gather-type", type), - this.workersBySubrole(gameState, Worker.SUBROLE_GATHERER)); -}; - -/** - * returns an entity collection of workers. - * They are idled immediatly and their subrole set to idle. - */ -BaseManager.prototype.pickBuilders = function(gameState, workers, number) -{ - const availableWorkers = this.workers.filter(ent => - { - if (!ent.position() || !ent.isBuilder()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }).toEntityArray(); - availableWorkers.sort((a, b) => - { - let vala = 0; - let valb = 0; - if (a.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) - vala = 100; - if (b.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) - valb = 100; - if (a.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_IDLE) - vala = -50; - if (b.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_IDLE) - valb = -50; - if (a.getMetadata(PlayerID, "plan") === undefined) - vala = -20; - if (b.getMetadata(PlayerID, "plan") === undefined) - valb = -20; - return vala - valb; - }); - const needed = Math.min(number, availableWorkers.length - 3); - for (let i = 0; i < needed; ++i) - { - availableWorkers[i].stopMoving(); - availableWorkers[i].setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - workers.addEnt(availableWorkers[i]); - } - return; -}; - -/** - * If we have some foundations, and we don't have enough builder-workers, - * try reassigning some other workers who are nearby - * AI tries to use builders sensibly, not completely stopping its econ. - */ -BaseManager.prototype.assignToFoundations = function(gameState, noRepair) -{ - let foundations = this.buildings.filter(filters.and(filters.isFoundation(), - filters.not(filters.byClass("Field")))); - - const damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); - - // Check if nothing to build - if (!foundations.length && !damagedBuildings.length) - return; - - const workers = this.workers.filter(ent => ent.isBuilder()); - const builderWorkers = this.workersBySubrole(gameState, Worker.SUBROLE_BUILDER); - const idleBuilderWorkers = builderWorkers.filter(filters.isIdle()); - - // if we're constructing and we have the foundations to our base anchor, only try building that. - if (this.constructing && foundations.filter(filters.byMetadata(PlayerID, "baseAnchor", true)) - .hasEntities()) - { - foundations = foundations.filter(filters.byMetadata(PlayerID, "baseAnchor", true)); - const tID = foundations.toEntityArray()[0].id(); - workers.forEach(ent => - { - const target = ent.getMetadata(PlayerID, "target-foundation"); - if (target && target != tID) - { - ent.stopMoving(); - ent.setMetadata(PlayerID, "target-foundation", tID); - } - }); - } - - if (workers.length < 3) - { - const fromOtherBase = this.basesManager.bulkPickWorkers(gameState, this, 2); - if (fromOtherBase) - { - const baseID = this.ID; - fromOtherBase.forEach(worker => - { - worker.setMetadata(PlayerID, "base", baseID); - worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); - workers.updateEnt(worker); - builderWorkers.updateEnt(worker); - idleBuilderWorkers.updateEnt(worker); - }); - } - } - - let builderTot = builderWorkers.length - idleBuilderWorkers.length; - - // Make the limit on number of builders depends on the available resources - const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); - let builderRatio = 1; - for (const res of Resources.GetCodes()) - { - if (availableResources[res] < 200) - { - builderRatio = 0.2; - break; - } - else if (availableResources[res] < 1000) - builderRatio = Math.min(builderRatio, availableResources[res] / 1000); - } - - for (const target of foundations.values()) - { - if (target.hasClass("Field")) - continue; // we do not build fields - - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - if (!target.hasClasses(["CivCentre", "Wall"]) && - (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) + if (total <= bestVal) continue; - // if our territory has shrinked since this foundation was positioned, do not build it - if (isNotWorthBuilding(gameState, target)) - continue; - - let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; - let maxTotalBuilders = Math.ceil(workers.length * builderRatio); - if (maxTotalBuilders < 2 && workers.length > 1) - maxTotalBuilders = 2; - if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 && - gameState.getPopulationLimit() < gameState.getPopulationMax()) - maxTotalBuilders += 2; - let targetNB = 2; - if (target.hasClasses(["Fortress", "Wonder"]) || - target.getMetadata(PlayerID, "phaseUp") == true) - targetNB = 7; - else if (target.hasClasses(["Barracks", "Range", "Stable", "Tower", "Market"])) - targetNB = 4; - else if (target.hasClasses(["House", "DropsiteWood"])) - targetNB = 3; - - if (target.getMetadata(PlayerID, "baseAnchor") == true || - target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) - { - targetNB = 15; - maxTotalBuilders = Math.max(maxTotalBuilders, 15); + if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) + continue; + bestVal = total; + bestIdx = i; } - if (!this.basesManager.hasActiveBase()) - { - targetNB = workers.length; - maxTotalBuilders = targetNB; - } + if (this.Config.debug > 2) + warn(" for dropsite best is " + bestVal); - if (assigned >= targetNB) - continue; - idleBuilderWorkers.forEach(function(ent) - { - if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (assigned >= targetNB || !ent.position() || - SquareVectorDistance(ent.position(), target.position()) > 40000) + if (bestVal <= 0) + return { "quality": bestVal, "pos": [0, 0] }; + + const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + return { "quality": bestVal, "pos": [x, z] }; + } + + getResourceLevel(gameState, type, distances = ["nearby", "medium", "faraway"]) + { + let count = 0; + const check = {}; + for (const proxim of distances) + for (const supply of this.dropsiteSupplies[type][proxim]) { - return; + if (check[supply.id]) // avoid double counting as same resource can appear several time + continue; + check[supply.id] = true; + const supplyEntity = gameState.getEntityById(supply.id); + if (supplyEntity) + count += supplyEntity.resourceSupplyAmount(); } - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - const nonBuilderWorkers = workers.filter(function(ent) + return count; + } + + /** check our resource levels and react accordingly */ + checkResourceLevels(gameState, queues) + { + for (const type of Resources.GetCodes()) { - if (ent.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) - return false; + if (type == "food") + { + const prox = ["nearby"]; + if (gameState.currentPhase() < 2) + prox.push("medium"); + if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms. + { + const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted + const numFarms = gameState.getOwnStructures().filter(filters.byClass("Field")) + .length; // including foundations + const numQueue = queues.field.countQueuedUnits(); + + // TODO if not yet farms, add a check on time used/lost and build farmstead if needed + if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them + { + if (count < 600) + { + queues.field.addPlan(new ConstructionPlan(gameState, + "structures/{civ}/field", { "favoredBase": this.ID })); + gameState.ai.HQ.needFarm = true; + } + } + else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) + { + const numFound = gameState.getOwnFoundations().filter(filters.byClass("Field")) + .length; + let goal = this.Config.Economy.provisionFields; + if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) + goal = Math.max(goal-1, 1); + if (numFound + numQueue < goal) + { + queues.field.addPlan(new ConstructionPlan(gameState, + "structures/{civ}/field", { "favoredBase": this.ID })); + } + } + else if (gameState.ai.HQ.needCorral && + !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && + !queues.corral.hasQueuedUnits() && + gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) + { + queues.corral.addPlan(new ConstructionPlan(gameState, + "structures/{civ}/corral", { "favoredBase": this.ID })); + } + continue; + } + if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && + !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) + { + const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted + if (count < 900) + { + queues.corral.addPlan(new ConstructionPlan(gameState, + "structures/{civ}/corral", { "favoredBase": this.ID })); + gameState.ai.HQ.needCorral = true; + } + } + continue; + } + // Non food stuff + if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() || + gameState.getOwnFoundations().filter(filters.byClass("Storehouse")).hasEntities()) + { + this.gatherers[type].nextCheck = gameState.ai.playedTurn; + this.gatherers[type].used = 0; + this.gatherers[type].lost = 0; + continue; + } + if (gameState.ai.playedTurn < this.gatherers[type].nextCheck) + continue; + for (const ent of this.gatherersByType(gameState, type).values()) + { + if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") + ++this.gatherers[type].used; + else if (ent.unitAIState() == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") + ++this.gatherers[type].lost; + } + // TODO add also a test on remaining resources. + const total = this.gatherers[type].used + this.gatherers[type].lost; + if (total > 150 || total > 60 && type != "wood") + { + const ratio = this.gatherers[type].lost / total; + if (ratio > 0.15) + { + const newDP = this.findBestDropsiteAndLocation(gameState, type); + if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) + { + queues.dropsites.addPlan(new ConstructionPlan(gameState, newDP.templateName, + { "base": this.ID, "type": type }, newDP.pos)); + } + else if (!gameState.getOwnFoundations().filter(filters.byClass("CivCentre")) + .hasEntities() && + !queues.civilCentre.hasQueuedUnits()) + { + // No good dropsite, try to build a new base if no base already planned, + // and if not possible, be less strict on dropsite quality. + if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && + newDP.quality > Math.min(25, 50*0.15/ratio) && + gameState.ai.HQ.canBuild(gameState, newDP.templateName)) + { + queues.dropsites.addPlan(new ConstructionPlan(gameState, + newDP.templateName, { "base": this.ID, "type": type }, + newDP.pos)); + } + } + } + this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; + this.gatherers[type].used = 0; + this.gatherers[type].lost = 0; + } + else if (total == 0) + this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; + } + } + + /** Adds the estimated gather rates from this base to the currentRates */ + addGatherRates(gameState, currentRates) + { + for (const res in currentRates) + { + // I calculate the exact gathering rate for each unit. + // I must then lower that to account for travel time. + // Given that the faster you gather, the more travel time matters, + // I use some logarithms. + // TODO: this should take into account for unit speed and/or distance to target + + this.gatherersByType(gameState, res).forEach(ent => + { + if (ent.isIdle() || !ent.position()) + return; + const gRate = ent.currentGatherRate(); + if (gRate) + currentRates[res] += Math.log(1+gRate)/1.1; + }); + if (res == "food") + { + this.workersBySubrole(gameState, Worker.SUBROLE_HUNTER).forEach(ent => + { + if (ent.isIdle() || !ent.position()) + return; + const gRate = ent.currentGatherRate(); + if (gRate) + currentRates[res] += Math.log(1+gRate)/1.1; + }); + this.workersBySubrole(gameState, Worker.SUBROLE_FISHER).forEach(ent => + { + if (ent.isIdle() || !ent.position()) + return; + const gRate = ent.currentGatherRate(); + if (gRate) + currentRates[res] += Math.log(1+gRate)/1.1; + }); + } + } + } + + assignRolelessUnits(gameState, roleless) + { + if (!roleless) + roleless = this.units.filter(filters.not(filters.byHasMetadata(PlayerID, "role"))).values(); + + for (const ent of roleless) + { + if (ent.hasClasses(["Worker", "CitizenSoldier", "FishingBoat"])) + ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); + else if (ent.hasClass("Support") && ent.hasClass("Elephant")) + ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); + } + } + + /** + * If the numbers of workers on the resources is unbalanced then set some of workers to idle so + * they can be reassigned by reassignIdleWorkers. + * TODO: actually this probably should be in the HQ. + */ + setWorkersIdleByPriority(gameState) + { + this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; + // change resource only towards one which is more needed, and if changing will not change this order + let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) + const mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); + let sumWanted = 0; + let sumCurrent = 0; + for (const need of mostNeeded) + { + sumWanted += need.wanted; + sumCurrent += need.current; + } + let scale = 1; + if (sumWanted > 0) + scale = sumCurrent / sumWanted; + + for (let i = mostNeeded.length-1; i > 0; --i) + { + const lessNeed = mostNeeded[i]; + for (let j = 0; j < i; ++j) + { + const moreNeed = mostNeeded[j]; + const lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; + if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) + continue; + // Ensure that the most wanted resource is not exhausted + if (moreNeed.type != "food" && this.basesManager.isResourceExhausted(moreNeed.type)) + { + if (lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) + continue; + + // And if so, move the gatherer to the less wanted one. + nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb); + if (nb == 0) + return; + } + + // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1 + // but we require a bit more to avoid too frequent changes + if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 || + lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) + { + nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb); + if (nb == 0) + return; + } + } + } + } + + /** + * Switch some gatherers (limited to number) from resource "from" to resource "to" + * and return remaining number of possible switches. + * Prefer Civilian for food and CitizenSoldier for other resources. + */ + switchGatherer(gameState, from, to, number) + { + let num = number; + let only; + const gatherers = this.gatherersByType(gameState, from); + if (from == "food" && gatherers.filter(filters.byClass("CitizenSoldier")).hasEntities()) + only = "CitizenSoldier"; + else if (to == "food" && gatherers.filter(filters.byClass("Civilian")).hasEntities()) + only = "Civilian"; + + for (const ent of gatherers.values()) + { + if (num == 0) + return num; + if (!ent.canGather(to)) + continue; + if (only && !ent.hasClass(only)) + continue; + --num; + ent.stopMoving(); + ent.setMetadata(PlayerID, "gather-type", to); + this.basesManager.AddTCResGatherer(to); + } + return num; + } + + reassignIdleWorkers(gameState, idleWorkers) + { + // Search for idle workers, and tell them to gather resources based on demand + if (!idleWorkers) + { + const filter = filters.byMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); + } + + for (const ent of idleWorkers) + { + // Check that the worker isn't garrisoned if (!ent.position()) + continue; + // Support elephant can only be builders + if (ent.hasClass("Support") && ent.hasClass("Elephant")) + { + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + continue; + } + + if (ent.hasClass("Worker")) + { + // Just emergency repairing here. It is better managed in assignToFoundations + if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && + gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) + ent.repair(this.anchor); + else if (ent.isGatherer()) + { + const mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); + for (const needed of mostNeeded) + { + if (!ent.canGather(needed.type)) + continue; + const lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; + if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) + continue; + if (needed.type != "food" && this.basesManager.isResourceExhausted(needed.type)) + continue; + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_GATHERER); + ent.setMetadata(PlayerID, "gather-type", needed.type); + this.basesManager.AddTCResGatherer(needed.type); + break; + } + } + } + else if (isFastMoving(ent) && ent.canGather("food") && ent.canAttackClass("Animal")) + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_HUNTER); + else if (ent.hasClass("FishingBoat")) + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_FISHER); + } + } + + workersBySubrole(gameState, subrole) + { + return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, + filters.byMetadata(PlayerID, "subrole", subrole), this.workers); + } + + gatherersByType(gameState, type) + { + return gameState.updatingCollection("workers-gathering-" + type +"-base-" + + this.ID, filters.byMetadata(PlayerID, "gather-type", type), + this.workersBySubrole(gameState, Worker.SUBROLE_GATHERER)); + } + + /** + * returns an entity collection of workers. + * They are idled immediatly and their subrole set to idle. + */ + pickBuilders(gameState, workers, number) + { + const availableWorkers = this.workers.filter(ent => + { + if (!ent.position() || !ent.isBuilder()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; @@ -947,145 +778,365 @@ BaseManager.prototype.assignToFoundations = function(gameState, noRepair) return false; return true; }).toEntityArray(); - const time = target.buildTime(); - nonBuilderWorkers.sort((workerA, workerB) => + availableWorkers.sort((a, b) => { - let coeffA = SquareVectorDistance(target.position(), workerA.position()); - // elephant moves slowly, so when far away they are only useful if build time is long - if (workerA.hasClass("Elephant")) - coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time); - else if (workerA.getMetadata(PlayerID, "gather-type") == "food") - coeffA *= 3; - let coeffB = SquareVectorDistance(target.position(), workerB.position()); - if (workerB.hasClass("Elephant")) - coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time); - else if (workerB.getMetadata(PlayerID, "gather-type") == "food") - coeffB *= 3; - return coeffA - coeffB; + let vala = 0; + let valb = 0; + if (a.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) + vala = 100; + if (b.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) + valb = 100; + if (a.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_IDLE) + vala = -50; + if (b.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_IDLE) + valb = -50; + if (a.getMetadata(PlayerID, "plan") === undefined) + vala = -20; + if (b.getMetadata(PlayerID, "plan") === undefined) + valb = -20; + return vala - valb; }); - let current = 0; - const nonBuilderTot = nonBuilderWorkers.length; - while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) + const needed = Math.min(number, availableWorkers.length - 3); + for (let i = 0; i < needed; ++i) { - ++assigned; - ++builderTot; - const ent = nonBuilderWorkers[current++]; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); - ent.setMetadata(PlayerID, "target-foundation", target.id()); + availableWorkers[i].stopMoving(); + availableWorkers[i].setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + workers.addEnt(availableWorkers[i]); } + return; } - for (const target of damagedBuildings.values()) + /** + * If we have some foundations, and we don't have enough builder-workers, + * try reassigning some other workers who are nearby + * AI tries to use builders sensibly, not completely stopping its econ. + */ + assignToFoundations(gameState, noRepair) { - // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building - // that's being destroyed. - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) + let foundations = this.buildings.filter(filters.and(filters.isFoundation(), + filters.not(filters.byClass("Field")))); + + const damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); + + // Check if nothing to build + if (!foundations.length && !damagedBuildings.length) + return; + + const workers = this.workers.filter(ent => ent.isBuilder()); + const builderWorkers = this.workersBySubrole(gameState, Worker.SUBROLE_BUILDER); + const idleBuilderWorkers = builderWorkers.filter(filters.isIdle()); + + // if we're constructing and we have the foundations to our base anchor, only try building that. + if (this.constructing && foundations.filter(filters.byMetadata(PlayerID, "baseAnchor", true)) + .hasEntities()) { - if (target.healthLevel() > 0.5 || - !target.hasClasses(["CivCentre", "Wall"]) && - (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) + foundations = foundations.filter(filters.byMetadata(PlayerID, "baseAnchor", true)); + const tID = foundations.toEntityArray()[0].id(); + workers.forEach(ent => + { + const target = ent.getMetadata(PlayerID, "target-foundation"); + if (target && target != tID) + { + ent.stopMoving(); + ent.setMetadata(PlayerID, "target-foundation", tID); + } + }); + } + + if (workers.length < 3) + { + const fromOtherBase = this.basesManager.bulkPickWorkers(gameState, this, 2); + if (fromOtherBase) + { + const baseID = this.ID; + fromOtherBase.forEach(worker => + { + worker.setMetadata(PlayerID, "base", baseID); + worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + workers.updateEnt(worker); + builderWorkers.updateEnt(worker); + idleBuilderWorkers.updateEnt(worker); + }); + } + } + + let builderTot = builderWorkers.length - idleBuilderWorkers.length; + + // Make the limit on number of builders depends on the available resources + const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); + let builderRatio = 1; + for (const res of Resources.GetCodes()) + { + if (availableResources[res] < 200) + { + builderRatio = 0.2; + break; + } + else if (availableResources[res] < 1000) + builderRatio = Math.min(builderRatio, availableResources[res] / 1000); + } + + for (const target of foundations.values()) + { + if (target.hasClass("Field")) + continue; // we do not build fields + + if (gameState.ai.HQ.isNearInvadingArmy(target.position())) + if (!target.hasClasses(["CivCentre", "Wall"]) && + (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) + continue; + + // if our territory has shrinked since this foundation was positioned, do not build it + if (isNotWorthBuilding(gameState, target)) continue; - } - else if (noRepair && !target.hasClass("CivCentre")) - continue; - if (target.decaying()) - continue; - - let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; - let maxTotalBuilders = Math.ceil(workers.length * builderRatio); - let targetNB = 1; - if (target.hasClasses(["Fortress", "Wonder"])) - targetNB = 3; - if (target.getMetadata(PlayerID, "baseAnchor") == true || - target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) - { - maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio)); - targetNB = 5; - if (target.healthLevel() < 0.3) - { - maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio)); + let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; + let maxTotalBuilders = Math.ceil(workers.length * builderRatio); + if (maxTotalBuilders < 2 && workers.length > 1) + maxTotalBuilders = 2; + if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 && + gameState.getPopulationLimit() < gameState.getPopulationMax()) + maxTotalBuilders += 2; + let targetNB = 2; + if (target.hasClasses(["Fortress", "Wonder"]) || + target.getMetadata(PlayerID, "phaseUp") == true) targetNB = 7; + else if (target.hasClasses(["Barracks", "Range", "Stable", "Tower", "Market"])) + targetNB = 4; + else if (target.hasClasses(["House", "DropsiteWood"])) + targetNB = 3; + + if (target.getMetadata(PlayerID, "baseAnchor") == true || + target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) + { + targetNB = 15; + maxTotalBuilders = Math.max(maxTotalBuilders, 15); } + if (!this.basesManager.hasActiveBase()) + { + targetNB = workers.length; + maxTotalBuilders = targetNB; + } + + if (assigned >= targetNB) + continue; + idleBuilderWorkers.forEach(function(ent) + { + if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) + return; + if (assigned >= targetNB || !ent.position() || + SquareVectorDistance(ent.position(), target.position()) > 40000) + { + return; + } + ++assigned; + ++builderTot; + ent.setMetadata(PlayerID, "target-foundation", target.id()); + }); + if (assigned >= targetNB || builderTot >= maxTotalBuilders) + continue; + const nonBuilderWorkers = workers.filter(function(ent) + { + if (ent.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) + return false; + if (!ent.position()) + return false; + if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) + return false; + if (ent.getMetadata(PlayerID, "transport")) + return false; + return true; + }).toEntityArray(); + const time = target.buildTime(); + nonBuilderWorkers.sort((workerA, workerB) => + { + let coeffA = SquareVectorDistance(target.position(), workerA.position()); + // elephant moves slowly, so when far away they are only useful if build time is long + if (workerA.hasClass("Elephant")) + coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time); + else if (workerA.getMetadata(PlayerID, "gather-type") == "food") + coeffA *= 3; + let coeffB = SquareVectorDistance(target.position(), workerB.position()); + if (workerB.hasClass("Elephant")) + coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time); + else if (workerB.getMetadata(PlayerID, "gather-type") == "food") + coeffB *= 3; + return coeffA - coeffB; + }); + let current = 0; + const nonBuilderTot = nonBuilderWorkers.length; + while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) + { + ++assigned; + ++builderTot; + const ent = nonBuilderWorkers[current++]; + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + ent.setMetadata(PlayerID, "target-foundation", target.id()); + } } - if (assigned >= targetNB) - continue; - idleBuilderWorkers.forEach(function(ent) + for (const target of damagedBuildings.values()) { - if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (assigned >= targetNB || !ent.position() || - SquareVectorDistance(ent.position(), target.position()) > 40000) - return; - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - const nonBuilderWorkers = workers.filter(function(ent) - { - if (ent.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) - return false; - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }); - const num = Math.min(nonBuilderWorkers.length, targetNB-assigned); - const nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); - - nearestNonBuilders.forEach(function(ent) - { - ++assigned; - ++builderTot; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - } -}; - -/** Return false when the base is not active (no workers on it) */ -BaseManager.prototype.update = function(gameState, queues, events) -{ - if (this.ID == this.basesManager.baselessBase().ID) - { - // if some active base, reassigns the workers/buildings - // otherwise look for anything useful to do, i.e. treasures to gather - if (this.basesManager.hasActiveBase()) - { - for (const ent of this.units.values()) + // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building + // that's being destroyed. + if (gameState.ai.HQ.isNearInvadingArmy(target.position())) { - const bestBase = getBestBase(gameState, ent); - if (bestBase.ID != this.ID) - bestBase.assignEntity(gameState, ent); + if (target.healthLevel() > 0.5 || + !target.hasClasses(["CivCentre", "Wall"]) && + (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) + continue; } + else if (noRepair && !target.hasClass("CivCentre")) + continue; + + if (target.decaying()) + continue; + + let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; + let maxTotalBuilders = Math.ceil(workers.length * builderRatio); + let targetNB = 1; + if (target.hasClasses(["Fortress", "Wonder"])) + targetNB = 3; + if (target.getMetadata(PlayerID, "baseAnchor") == true || + target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) + { + maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio)); + targetNB = 5; + if (target.healthLevel() < 0.3) + { + maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio)); + targetNB = 7; + } + + } + + if (assigned >= targetNB) + continue; + idleBuilderWorkers.forEach(function(ent) + { + if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) + return; + if (assigned >= targetNB || !ent.position() || + SquareVectorDistance(ent.position(), target.position()) > 40000) + return; + ++assigned; + ++builderTot; + ent.setMetadata(PlayerID, "target-foundation", target.id()); + }); + if (assigned >= targetNB || builderTot >= maxTotalBuilders) + continue; + const nonBuilderWorkers = workers.filter(function(ent) + { + if (ent.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_BUILDER) + return false; + if (!ent.position()) + return false; + if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) + return false; + if (ent.getMetadata(PlayerID, "transport")) + return false; + return true; + }); + const num = Math.min(nonBuilderWorkers.length, targetNB-assigned); + const nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); + + nearestNonBuilders.forEach(function(ent) + { + ++assigned; + ++builderTot; + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + ent.setMetadata(PlayerID, "target-foundation", target.id()); + }); + } + } + + /** Return false when the base is not active (no workers on it) */ + update(gameState, queues, events) + { + if (this.ID == this.basesManager.baselessBase().ID) + { + // if some active base, reassigns the workers/buildings + // otherwise look for anything useful to do, i.e. treasures to gather + if (this.basesManager.hasActiveBase()) + { + for (const ent of this.units.values()) + { + const bestBase = getBestBase(gameState, ent); + if (bestBase.ID != this.ID) + bestBase.assignEntity(gameState, ent); + } + for (const ent of this.buildings.values()) + { + const bestBase = getBestBase(gameState, ent); + if (!bestBase) + { + if (ent.hasClass("Dock")) + { + aiWarn("Petra: dock in 'noBase' baseManager. It may be useful to " + + "do an anchorless base for " + ent.templateName()); + } + continue; + } + if (ent.resourceDropsiteTypes()) + this.removeDropsite(gameState, ent.id()); + bestBase.assignEntity(gameState, ent); + } + } + else if (gameState.ai.HQ.canBuildUnits) + { + this.assignToFoundations(gameState); + if (gameState.ai.elapsedTime > this.timeNextIdleCheck) + this.setWorkersIdleByPriority(gameState); + this.assignRolelessUnits(gameState); + this.reassignIdleWorkers(gameState); + for (const ent of this.workers.values()) + this.workerObject.update(gameState, ent); + for (const ent of this.mobileDropsites.values()) + this.workerObject.moveToGatherer(gameState, ent, false); + } + return false; + } + + if (!this.anchor) // This anchor has been destroyed, but the base may still be usable + { + if (!this.buildings.hasEntities()) + { + // Reassign all remaining entities to its nearest base + for (const ent of this.units.values()) + { + const base = getBestBase(gameState, ent, false, this.ID); + base.assignEntity(gameState, ent); + } + return false; + } + // If we have a base with anchor on the same land, reassign everything to it + let reassignedBase; for (const ent of this.buildings.values()) { - const bestBase = getBestBase(gameState, ent); - if (!bestBase) - { - if (ent.hasClass("Dock")) - { - aiWarn("Petra: dock in 'noBase' baseManager. It may be useful to " + - "do an anchorless base for " + ent.templateName()); - } + if (!ent.position()) continue; - } - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent.id()); - bestBase.assignEntity(gameState, ent); + const base = getBestBase(gameState, ent); + if (base.anchor) + reassignedBase = base; + break; } - } - else if (gameState.ai.HQ.canBuildUnits) - { + + if (reassignedBase) + { + for (const ent of this.units.values()) + reassignedBase.assignEntity(gameState, ent); + for (const ent of this.buildings.values()) + { + if (ent.resourceDropsiteTypes()) + this.removeDropsite(gameState, ent.id()); + reassignedBase.assignEntity(gameState, ent); + } + return false; + } + this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); @@ -1095,138 +1146,89 @@ BaseManager.prototype.update = function(gameState, queues, events) this.workerObject.update(gameState, ent); for (const ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); - } - return false; - } - - if (!this.anchor) // This anchor has been destroyed, but the base may still be usable - { - if (!this.buildings.hasEntities()) - { - // Reassign all remaining entities to its nearest base - for (const ent of this.units.values()) - { - const base = getBestBase(gameState, ent, false, this.ID); - base.assignEntity(gameState, ent); - } - return false; - } - // If we have a base with anchor on the same land, reassign everything to it - let reassignedBase; - for (const ent of this.buildings.values()) - { - if (!ent.position()) - continue; - const base = getBestBase(gameState, ent); - if (base.anchor) - reassignedBase = base; - break; + return true; } - if (reassignedBase) - { - for (const ent of this.units.values()) - reassignedBase.assignEntity(gameState, ent); - for (const ent of this.buildings.values()) - { - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent.id()); - reassignedBase.assignEntity(gameState, ent); - } - return false; - } + Engine.ProfileStart("Base update - base " + this.ID); + this.checkResourceLevels(gameState, queues); this.assignToFoundations(gameState); - if (gameState.ai.elapsedTime > this.timeNextIdleCheck) + + if (this.constructing) + { + const owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); + if (owner != 0 && !gameState.isPlayerAlly(owner)) + { + // we're in enemy territory. If we're too close from the enemy, destroy us. + const ccEnts = gameState.updatingGlobalCollection("allCCs", + filters.byClass("CivCentre")); + for (const cc of ccEnts.values()) + { + if (cc.owner() != owner) + continue; + if (SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) + continue; + this.anchor.destroy(); + this.basesManager.resetBaseCache(); + break; + } + } + } + else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) + --this.neededDefenders; + + if (gameState.ai.elapsedTime > this.timeNextIdleCheck && + (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) this.setWorkersIdleByPriority(gameState); + this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); + // check if workers can find something useful to do for (const ent of this.workers.values()) this.workerObject.update(gameState, ent); for (const ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); + + Engine.ProfileStop(); return true; } - Engine.ProfileStart("Base update - base " + this.ID); - - this.checkResourceLevels(gameState, queues); - this.assignToFoundations(gameState); - - if (this.constructing) + AddTCGatherer(supplyID) { - const owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); - if (owner != 0 && !gameState.isPlayerAlly(owner)) - { - // we're in enemy territory. If we're too close from the enemy, destroy us. - const ccEnts = gameState.updatingGlobalCollection("allCCs", - filters.byClass("CivCentre")); - for (const cc of ccEnts.values()) - { - if (cc.owner() != owner) - continue; - if (SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) - continue; - this.anchor.destroy(); - this.basesManager.resetBaseCache(); - break; - } - } + return this.basesManager.AddTCGatherer(supplyID); } - else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) - --this.neededDefenders; - if (gameState.ai.elapsedTime > this.timeNextIdleCheck && - (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) - this.setWorkersIdleByPriority(gameState); + RemoveTCGatherer(supplyID) + { + this.basesManager.RemoveTCGatherer(supplyID); + } - this.assignRolelessUnits(gameState); - this.reassignIdleWorkers(gameState); - // check if workers can find something useful to do - for (const ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (const ent of this.mobileDropsites.values()) - this.workerObject.moveToGatherer(gameState, ent, false); + GetTCGatherer(supplyID) + { + return this.basesManager.GetTCGatherer(supplyID); + } - Engine.ProfileStop(); - return true; -}; + Serialize() + { + return { + "ID": this.ID, + "anchorId": this.anchorId, + "accessIndex": this.accessIndex, + "maxDistResourceSquare": this.maxDistResourceSquare, + "constructing": this.constructing, + "gatherers": this.gatherers, + "neededDefenders": this.neededDefenders, + "territoryIndices": this.territoryIndices, + "timeNextIdleCheck": this.timeNextIdleCheck, + "dropsiteSupplies": this.dropsiteSupplies + }; + } -BaseManager.prototype.AddTCGatherer = function(supplyID) -{ - return this.basesManager.AddTCGatherer(supplyID); -}; + Deserialize(gameState, data) + { + for (const key in data) + this[key] = data[key]; -BaseManager.prototype.RemoveTCGatherer = function(supplyID) -{ - this.basesManager.RemoveTCGatherer(supplyID); -}; - -BaseManager.prototype.GetTCGatherer = function(supplyID) -{ - return this.basesManager.GetTCGatherer(supplyID); -}; - -BaseManager.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "anchorId": this.anchorId, - "accessIndex": this.accessIndex, - "maxDistResourceSquare": this.maxDistResourceSquare, - "constructing": this.constructing, - "gatherers": this.gatherers, - "neededDefenders": this.neededDefenders, - "territoryIndices": this.territoryIndices, - "timeNextIdleCheck": this.timeNextIdleCheck, - "dropsiteSupplies": this.dropsiteSupplies - }; -}; - -BaseManager.prototype.Deserialize = function(gameState, data) -{ - for (const key in data) - this[key] = data[key]; - - this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; -}; + this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/basesManager.js b/binaries/data/mods/public/simulation/ai/petra/basesManager.js index 51cc63dd09..c2daa2f77e 100644 --- a/binaries/data/mods/public/simulation/ai/petra/basesManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/basesManager.js @@ -12,720 +12,683 @@ import { Worker } from "simulation/ai/petra/worker.js"; * Only one base is run every turn. */ -export function BasesManager(Config) +export class BasesManager { - this.Config = Config; - - this.currentBase = 0; + currentBase = 0; // Cache some quantities for performance. - this.turnCache = {}; + turnCache = {}; // Deals with unit/structure without base. - this.noBase = undefined; + noBase = undefined; - this.baseManagers = []; -} + baseManagers = []; -BasesManager.prototype.init = function(gameState, deserialize = false) -{ - // Initialize base map. Each pixel is a base ID, or 0 if not or not accessible. - this.basesMap = new InfoMap(gameState.sharedScript, "territory"); - - this.noBase = new BaseManager(gameState, this); - this.noBase.init(gameState, BaseManager.STATE_WITH_ANCHOR); - this.noBase.accessIndex = 0; - - for (const cc of gameState.getOwnStructures().filter(filters.byClass("CivCentre")).values()) - if (cc.foundationProgress() === undefined) - this.createBase(gameState, cc, BaseManager.STATE_WITH_ANCHOR, deserialize); - else - this.createBase(gameState, cc, BaseManager.STATE_UNCONSTRUCTED, deserialize); -}; - -/** - * Initialization needed after deserialization (only called when deserialising). - */ -BasesManager.prototype.postinit = function(gameState) -{ - // Rebuild the base maps from the territory indices of each base. - this.basesMap = new InfoMap(gameState.sharedScript, "territory"); - for (const base of this.baseManagers) - for (const j of base.territoryIndices) - this.basesMap.map[j] = base.ID; - - for (const ent of gameState.getOwnEntities().values()) + constructor(Config) { - if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) - continue; - // Entities which have been built or have changed ownership after the last AI turn have no base. - // they will be dealt with in the next checkEvents - const baseID = ent.getMetadata(PlayerID, "base"); - if (baseID === undefined) - continue; - const base = this.getBaseByID(baseID); + this.Config = Config; } -}; -/** - * Create a new base in the baseManager: - * If an existing one without anchor already exist, use it. - * Otherwise create a new one. - * TODO when buildings, criteria should depend on distance - */ -BasesManager.prototype.createBase = function(gameState, ent, type = BaseManager.STATE_WITH_ANCHOR, - deserialize = false) -{ - const access = getLandAccess(gameState, ent); - let newbase; - for (const base of this.baseManagers) + init(gameState, deserialize = false) { - if (base.accessIndex != access) - continue; - if (type !== BaseManager.STATE_ANCHORLESS && base.anchor) - continue; - if (type !== BaseManager.STATE_ANCHORLESS) + // Initialize base map. Each pixel is a base ID, or 0 if not or not accessible. + this.basesMap = new InfoMap(gameState.sharedScript, "territory"); + + this.noBase = new BaseManager(gameState, this); + this.noBase.init(gameState, BaseManager.STATE_WITH_ANCHOR); + this.noBase.accessIndex = 0; + + for (const cc of gameState.getOwnStructures().filter(filters.byClass("CivCentre")).values()) + if (cc.foundationProgress() === undefined) + this.createBase(gameState, cc, BaseManager.STATE_WITH_ANCHOR, deserialize); + else + this.createBase(gameState, cc, BaseManager.STATE_UNCONSTRUCTED, deserialize); + } + + /** + * Initialization needed after deserialization (only called when deserialising). + */ + postinit(gameState) + { + // Rebuild the base maps from the territory indices of each base. + this.basesMap = new InfoMap(gameState.sharedScript, "territory"); + for (const base of this.baseManagers) + for (const j of base.territoryIndices) + this.basesMap.map[j] = base.ID; + + for (const ent of gameState.getOwnEntities().values()) { - // TODO we keep the first one, we should rather use the nearest if buildings - // and possibly also cut on distance - newbase = base; - break; - } - else - { - // TODO here also test on distance instead of first - if (newbase && !base.anchor) + if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) continue; - newbase = base; - if (newbase.anchor) + // Entities which have been built or have changed ownership after the last AI turn have no base. + // they will be dealt with in the next checkEvents + const baseID = ent.getMetadata(PlayerID, "base"); + if (baseID === undefined) + continue; + const base = this.getBaseByID(baseID); + } + } + + /** + * Create a new base in the baseManager: + * If an existing one without anchor already exist, use it. + * Otherwise create a new one. + * TODO when buildings, criteria should depend on distance + */ + createBase(gameState, ent, type = BaseManager.STATE_WITH_ANCHOR, deserialize = false) + { + const access = getLandAccess(gameState, ent); + let newbase; + for (const base of this.baseManagers) + { + if (base.accessIndex != access) + continue; + if (type !== BaseManager.STATE_ANCHORLESS && base.anchor) + continue; + if (type !== BaseManager.STATE_ANCHORLESS) + { + // TODO we keep the first one, we should rather use the nearest if buildings + // and possibly also cut on distance + newbase = base; break; - } - } - - if (this.Config.debug > 0) - { - aiWarn(" ----------------------------------------------------------"); - aiWarn(" BasesManager createBase entrance avec access " + access + " and type " + type); - aiWarn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + - " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + - " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); - } - - if (!newbase) - { - newbase = new BaseManager(gameState, this); - newbase.init(gameState, type); - this.baseManagers.push(newbase); - } - else - newbase.reset(type); - - if (type !== BaseManager.STATE_ANCHORLESS) - newbase.setAnchor(gameState, ent, deserialize); - else - newbase.setAnchorlessEntity(gameState, ent, deserialize); - - return newbase; -}; - -/** TODO check if the new anchorless bases should be added to addBase */ -BasesManager.prototype.checkEvents = function(gameState, events) -{ - this.turnCache = {}; - let addBase = false; - - for (const evt of events.Destroy) - { - // Let's check we haven't lost an important building here. - if (evt && !evt.SuccessfulFoundation && evt.metadata?.[PlayerID]?.base) - { - if (evt?.metadata?.[PlayerID]?.assignedResource) - { - this.getBaseByID(evt.metadata[PlayerID].base) - .removeFromAssignedDropsite(evt.entity); } - // A new base foundation was created and destroyed on the same (AI) turn - if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) - continue; - const base = this.getBaseByID(evt.metadata[PlayerID].base); - base.removeDropsite(gameState, evt.entity); - if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) - base.anchorLost(gameState); - } - } - - for (const evt of events.EntityRenamed) - { - const ent = gameState.getEntityById(evt.newentity); - const baseID = ent?.getMetadata(PlayerID, "base"); - if (!ent || ent.owner() != PlayerID || baseID === undefined) - continue; - - const base = this.getBaseByID(baseID); - - // Promoted workers should be reset - their new gathering rates/capabilities might differ meaningfully. - if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER) - { - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - if (!ent.isGatherer()) - { - // TODO: Find a way to properly reset the metadata, as this is currently done selectively - ent.setMetadata(PlayerID, "gather-type", undefined); - ent.setMetadata(PlayerID, "supply", undefined); - ent.setMetadata(PlayerID, "role", undefined); - base.assignRolelessUnits(gameState, [ent]); - } - continue; - } - - // Handle cases where a base anchor changes - if (!base.anchorId || base.anchorId != evt.entity) - continue; - base.anchorId = evt.newentity; - base.anchor = ent; - } - - for (const evt of events.Create) - { - // Let's check if we have a valuable foundation needing builders quickly - // (normal foundations are taken care in baseManager.assignToFoundations) - const ent = gameState.getEntityById(evt.entity); - if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) - continue; - - if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc - { - // Okay so let's try to create a new base around this. - const newbase = this.createBase(gameState, ent, BaseManager.STATE_UNCONSTRUCTED); - // Let's get a few units from other bases there to build this. - const builders = this.bulkPickWorkers(gameState, newbase, 10); - if (builders !== false) - { - builders.forEach(worker => - { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock - { - const newbase = this.createBase(gameState, ent, BaseManager.STATE_ANCHORLESS); - // Let's get a few units from other bases there to build this. - const builders = this.bulkPickWorkers(gameState, newbase, 4); - if (builders != false) - { - builders.forEach(worker => - { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - } - - for (const evt of events.ConstructionFinished) - { - if (evt.newentity == evt.entity) // repaired building - continue; - const ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID) - continue; - if (ent.getMetadata(PlayerID, "base") === undefined) - continue; - const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes()) - base.assignResourceToDropsite(gameState, ent, false); - - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - { - if (base.constructing) - base.constructing = false; - addBase = true; - } - } - - for (const evt of events.OwnershipChanged) - { - if (evt.from == PlayerID) - { - const ent = gameState.getEntityById(evt.entity); - if (!ent || ent.getMetadata(PlayerID, "base") === undefined) - continue; - const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent.id()); - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - base.anchorLost(gameState); - } - - if (evt.to != PlayerID) - continue; - const ent = gameState.getEntityById(evt.entity); - if (!ent) - continue; - if (ent.hasClass("Unit")) - { - getBestBase(gameState, ent).assignEntity(gameState, ent); - continue; - } - if (ent.hasClass("CivCentre")) // build a new base around it - { - let newbase; - if (ent.foundationProgress() !== undefined) - newbase = this.createBase(gameState, ent, BaseManager.STATE_UNCONSTRUCTED); else { - newbase = this.createBase(gameState, ent, BaseManager.STATE_CAPTURED); - addBase = true; + // TODO here also test on distance instead of first + if (newbase && !base.anchor) + continue; + newbase = base; + if (newbase.anchor) + break; } - newbase.assignEntity(gameState, ent); + } + + if (this.Config.debug > 0) + { + aiWarn(" ----------------------------------------------------------"); + aiWarn(" BasesManager createBase entrance avec access " + access + " and type " + type); + aiWarn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + + " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + + " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); + } + + if (!newbase) + { + newbase = new BaseManager(gameState, this); + newbase.init(gameState, type); + this.baseManagers.push(newbase); } else - { - let base; - // If dropsite on new island, create a base around it - if (!ent.decaying() && ent.resourceDropsiteTypes()) - base = this.createBase(gameState, ent, BaseManager.STATE_ANCHORLESS); - else - base = getBestBase(gameState, ent) || this.noBase; - base.assignEntity(gameState, ent); - } + newbase.reset(type); + + if (type !== BaseManager.STATE_ANCHORLESS) + newbase.setAnchor(gameState, ent, deserialize); + else + newbase.setAnchorlessEntity(gameState, ent, deserialize); + + return newbase; } - for (const evt of events.TrainingFinished) + /** TODO check if the new anchorless bases should be added to addBase */ + checkEvents(gameState, events) { - for (const entId of evt.entities) + this.turnCache = {}; + let addBase = false; + + for (const evt of events.Destroy) { - const ent = gameState.getEntityById(entId); - if (!ent || !ent.isOwn(PlayerID)) + // Let's check we haven't lost an important building here. + if (evt && !evt.SuccessfulFoundation && evt.metadata?.[PlayerID]?.base) + { + if (evt?.metadata?.[PlayerID]?.assignedResource) + { + this.getBaseByID(evt.metadata[PlayerID].base) + .removeFromAssignedDropsite(evt.entity); + } + // A new base foundation was created and destroyed on the same (AI) turn + if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) + continue; + const base = this.getBaseByID(evt.metadata[PlayerID].base); + base.removeDropsite(gameState, evt.entity); + if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) + base.anchorLost(gameState); + } + } + + for (const evt of events.EntityRenamed) + { + const ent = gameState.getEntityById(evt.newentity); + const baseID = ent?.getMetadata(PlayerID, "base"); + if (!ent || ent.owner() != PlayerID || baseID === undefined) continue; - // Assign it immediately to something useful to do. + const base = this.getBaseByID(baseID); + + // Promoted workers should be reset - their new gathering rates/capabilities might differ meaningfully. if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER) { - let base; - if (ent.getMetadata(PlayerID, "base") === undefined) + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + if (!ent.isGatherer()) { - base = getBestBase(gameState, ent); - base.assignEntity(gameState, ent); + // TODO: Find a way to properly reset the metadata, as this is currently done selectively + ent.setMetadata(PlayerID, "gather-type", undefined); + ent.setMetadata(PlayerID, "supply", undefined); + ent.setMetadata(PlayerID, "role", undefined); + base.assignRolelessUnits(gameState, [ent]); } - else - base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.reassignIdleWorkers(gameState, [ent]); - base.workerObject.update(gameState, ent); + continue; } - else if (ent.resourceSupplyType() && ent.position()) + + // Handle cases where a base anchor changes + if (!base.anchorId || base.anchorId != evt.entity) + continue; + base.anchorId = evt.newentity; + base.anchor = ent; + } + + for (const evt of events.Create) + { + // Let's check if we have a valuable foundation needing builders quickly + // (normal foundations are taken care in baseManager.assignToFoundations) + const ent = gameState.getEntityById(evt.entity); + if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) + continue; + + if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc { - const type = ent.resourceSupplyType(); - if (!type.generic) - continue; - const dropsites = gameState.getOwnDropsites(type.generic); - const pos = ent.position(); - const access = getLandAccess(gameState, ent); - let distmin = Math.min(); - let goal; - for (const dropsite of dropsites.values()) + // Okay so let's try to create a new base around this. + const newbase = this.createBase(gameState, ent, BaseManager.STATE_UNCONSTRUCTED); + // Let's get a few units from other bases there to build this. + const builders = this.bulkPickWorkers(gameState, newbase, 10); + if (builders !== false) { - if (!dropsite.position() || getLandAccess(gameState, dropsite) != access) - continue; - const dist = SquareVectorDistance(pos, dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - goal = dropsite.position(); + builders.forEach(worker => + { + worker.setMetadata(PlayerID, "base", newbase.ID); + worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + worker.setMetadata(PlayerID, "target-foundation", ent.id()); + }); } - if (goal) - ent.moveToRange(goal[0], goal[1]); + } + else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock + { + const newbase = this.createBase(gameState, ent, BaseManager.STATE_ANCHORLESS); + // Let's get a few units from other bases there to build this. + const builders = this.bulkPickWorkers(gameState, newbase, 4); + if (builders != false) + { + builders.forEach(worker => + { + worker.setMetadata(PlayerID, "base", newbase.ID); + worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + worker.setMetadata(PlayerID, "target-foundation", ent.id()); + }); + } + } + } + + for (const evt of events.ConstructionFinished) + { + if (evt.newentity == evt.entity) // repaired building + continue; + const ent = gameState.getEntityById(evt.newentity); + if (!ent || ent.owner() != PlayerID) + continue; + if (ent.getMetadata(PlayerID, "base") === undefined) + continue; + const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); + base.buildings.updateEnt(ent); + if (ent.resourceDropsiteTypes()) + base.assignResourceToDropsite(gameState, ent, false); + + if (ent.getMetadata(PlayerID, "baseAnchor") === true) + { + if (base.constructing) + base.constructing = false; + addBase = true; + } + } + + for (const evt of events.OwnershipChanged) + { + if (evt.from == PlayerID) + { + const ent = gameState.getEntityById(evt.entity); + if (!ent || ent.getMetadata(PlayerID, "base") === undefined) + continue; + const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); + if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) + base.removeDropsite(gameState, ent.id()); + if (ent.getMetadata(PlayerID, "baseAnchor") === true) + base.anchorLost(gameState); + } + + if (evt.to != PlayerID) + continue; + const ent = gameState.getEntityById(evt.entity); + if (!ent) + continue; + if (ent.hasClass("Unit")) + { + getBestBase(gameState, ent).assignEntity(gameState, ent); + continue; + } + if (ent.hasClass("CivCentre")) // build a new base around it + { + let newbase; + if (ent.foundationProgress() !== undefined) + newbase = this.createBase(gameState, ent, BaseManager.STATE_UNCONSTRUCTED); + else + { + newbase = this.createBase(gameState, ent, BaseManager.STATE_CAPTURED); + addBase = true; + } + newbase.assignEntity(gameState, ent); + } + else + { + let base; + // If dropsite on new island, create a base around it + if (!ent.decaying() && ent.resourceDropsiteTypes()) + base = this.createBase(gameState, ent, BaseManager.STATE_ANCHORLESS); + else + base = getBestBase(gameState, ent) || this.noBase; + base.assignEntity(gameState, ent); + } + } + + for (const evt of events.TrainingFinished) + { + for (const entId of evt.entities) + { + const ent = gameState.getEntityById(entId); + if (!ent || !ent.isOwn(PlayerID)) + continue; + + // Assign it immediately to something useful to do. + if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER) + { + let base; + if (ent.getMetadata(PlayerID, "base") === undefined) + { + base = 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()) + { + const type = ent.resourceSupplyType(); + if (!type.generic) + continue; + const dropsites = gameState.getOwnDropsites(type.generic); + const pos = ent.position(); + const access = getLandAccess(gameState, ent); + let distmin = Math.min(); + let goal; + for (const dropsite of dropsites.values()) + { + if (!dropsite.position() || getLandAccess(gameState, dropsite) != access) + continue; + const dist = SquareVectorDistance(pos, dropsite.position()); + if (dist > distmin) + continue; + distmin = dist; + goal = dropsite.position(); + } + if (goal) + ent.moveToRange(goal[0], goal[1]); + } + } + } + + if (addBase) + gameState.ai.HQ.handleNewBase(gameState); + } + + /** + * returns an entity collection of workers through BaseManager.pickBuilders + * TODO: when same accessIndex, sort by distance + */ + bulkPickWorkers(gameState, baseRef, number) + { + const accessIndex = baseRef.accessIndex; + if (!accessIndex) + return false; + const baseBest = this.baseManagers.slice(); + // We can also use workers without a base. + baseBest.push(this.noBase); + baseBest.sort((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; + const workers = new EntityCollection(gameState.sharedScript); + for (const base of baseBest) + { + if (base.ID == baseRef.ID) + continue; + base.pickBuilders(gameState, workers, needed); + if (workers.length >= number) + break; + needed = number - workers.length; + } + if (!workers.length) + return false; + return workers; + } + + /** + * @return {Object} - Resources (estimation) still gatherable in our territory. + */ + getTotalResourceLevel(gameState, resources = Resources.GetCodes(), proximity = ["nearby", "medium"]) + { + const total = {}; + for (const res of resources) + total[res] = 0; + for (const base of this.baseManagers) + for (const res in total) + total[res] += base.getResourceLevel(gameState, res, proximity); + + 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. + */ + GetCurrentGatherRates(gameState) + { + if (!this.turnCache.currentRates) + { + const currentRates = {}; + for (const res of Resources.GetCodes()) + currentRates[res] = 0.5 * this.GetTCResGatherer(res); + + this.addGatherRates(gameState, currentRates); + + for (const res of Resources.GetCodes()) + currentRates[res] = Math.max(currentRates[res], 0); + + this.turnCache.currentRates = currentRates; + } + + return this.turnCache.currentRates; + } + + /** Some functions that register that we assigned a gatherer to a resource this turn */ + + /** Add a gatherer to the turn cache for this supply. */ + AddTCGatherer(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 from the turn cache for this supply. */ + RemoveTCGatherer(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; + } + } + + GetTCGatherer(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. */ + AddTCResGatherer(resource) + { + const check = "resourceGatherer-" + resource; + if (this.turnCache[check]) + ++this.turnCache[check]; + else + this.turnCache[check] = 1; + + if (this.turnCache.currentRates) + this.turnCache.currentRates[resource] += 0.5; + } + + GetTCResGatherer(resource) + { + const check = "resourceGatherer-" + resource; + if (this.turnCache[check]) + return this.turnCache[check]; + + return 0; + } + + /** + * returns the number of bases with a cc + * ActiveBases includes only those with a built cc + * PotentialBases includes also those with a cc in construction + */ + numActiveBases() + { + if (!this.turnCache.base) + this.updateBaseCache(); + return this.turnCache.base.active; + } + + hasActiveBase() + { + return !!this.numActiveBases(); + } + + numPotentialBases() + { + if (!this.turnCache.base) + this.updateBaseCache(); + return this.turnCache.base.potential; + } + + hasPotentialBase() + { + return !!this.numPotentialBases(); + } + + /** + * Updates the number of active and potential bases. + * .potential {number} - Bases that may or may not still be a foundation. + * .active {number} - Usable bases. + */ + updateBaseCache() + { + this.turnCache.base = { "active": 0, "potential": 0 }; + for (const base of this.baseManagers) + { + if (!base.anchor) + continue; + ++this.turnCache.base.potential; + if (base.anchor.foundationProgress() === undefined) + ++this.turnCache.base.active; + } + } + + resetBaseCache() + { + this.turnCache.base = undefined; + } + + baselessBase() + { + return this.noBase; + } + + /** + * @param {number} baseID + * @return {Object} - The base corresponding to baseID. + */ + getBaseByID(baseID) + { + if (this.noBase.ID === baseID) + return this.noBase; + return this.baseManagers.find(base => base.ID === baseID); + } + + /** + * flag a resource as exhausted + */ + isResourceExhausted(resource) + { + return this.baseManagers.every(base => + !base.dropsiteSupplies[resource].nearby.length && + !base.dropsiteSupplies[resource].medium.length && + !base.dropsiteSupplies[resource].faraway.length); + } + + /** + * Count gatherers returning resources in the number of gatherers of resourceSupplies + * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). + */ + assignGatherers() + { + for (const base of this.baseManagers) + for (const worker of base.workers.values()) + { + if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) + continue; + const orders = worker.unitAIOrderData(); + if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) + continue; + this.AddTCGatherer(orders[1].target); + } + } + + /** + * Assign an entity to the closest base. + * Used by the starting strategy. + */ + assignEntity(gameState, ent, territoryIndex) + { + let bestbase; + for (const base of this.baseManagers) + { + if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) && + base.territoryIndices.indexOf(territoryIndex) == -1) + continue; + base.assignEntity(gameState, ent); + bestbase = base; + break; + } + if (!bestbase) // entity outside our territory + { + if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes()) + bestbase = this.createBase(gameState, ent, BaseManager.STATE_ANCHORLESS); + else + bestbase = getBestBase(gameState, ent) || this.noBase; + bestbase.assignEntity(gameState, ent); + } + // now assign entities garrisoned inside this entity + if (ent.isGarrisonHolder() && ent.garrisoned().length) + for (const id of ent.garrisoned()) + bestbase.assignEntity(gameState, gameState.getEntityById(id)); + // and find something useful to do if we already have a base + if (ent.position() && bestbase.ID !== this.noBase.ID) + { + bestbase.assignRolelessUnits(gameState, [ent]); + if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER) + { + bestbase.reassignIdleWorkers(gameState, [ent]); + bestbase.workerObject.update(gameState, ent); } } } - if (addBase) - gameState.ai.HQ.handleNewBase(gameState); -}; - -/** - * returns an entity collection of workers through BaseManager.pickBuilders - * TODO: when same accessIndex, sort by distance - */ -BasesManager.prototype.bulkPickWorkers = function(gameState, baseRef, number) -{ - const accessIndex = baseRef.accessIndex; - if (!accessIndex) - return false; - const baseBest = this.baseManagers.slice(); - // We can also use workers without a base. - baseBest.push(this.noBase); - baseBest.sort((a, b) => + /** + * Adds the gather rates of individual bases to a shared object. + * @param {Object} gameState + * @param {Object} rates - The rates to add the gather rates to. + */ + addGatherRates(gameState, rates) { - if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) - return -1; - else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) - return 1; - return 0; - }); + for (const base of this.baseManagers) + base.addGatherRates(gameState, rates); + } - let needed = number; - const workers = new EntityCollection(gameState.sharedScript); - for (const base of baseBest) + /** + * @param {number} territoryIndex + * @return {number} - The ID of the base at the given territory index. + */ + baseAtIndex(territoryIndex) { - if (base.ID == baseRef.ID) - continue; - base.pickBuilders(gameState, workers, needed); - if (workers.length >= number) + return this.basesMap.map[territoryIndex]; + } + + /** + * @param {number} territoryIndex + */ + removeBaseFromTerritoryIndex(territoryIndex) + { + const baseID = this.basesMap.map[territoryIndex]; + if (baseID == 0) + return; + const base = this.getBaseByID(baseID); + if (base) + { + const index = base.territoryIndices.indexOf(territoryIndex); + if (index != -1) + base.territoryIndices.splice(index, 1); + else + aiWarn(" problem in headquarters::updateTerritories for base " + baseID); + } + else + aiWarn(" problem in headquarters::updateTerritories without base " + baseID); + this.basesMap.map[territoryIndex] = 0; + } + + /** + * @return {boolean} - Whether the index was added to a base. + */ + addTerritoryIndexToBase(gameState, territoryIndex, passabilityMap) + { + if (this.baseAtIndex(territoryIndex) != 0) + return false; + let landPassable = false; + const ind = getMapIndices(territoryIndex, gameState.ai.HQ.territoryMap, passabilityMap); + let access; + for (const k of ind) + { + if (!gameState.ai.HQ.landRegions[gameState.ai.accessibility.landPassMap[k]]) + continue; + landPassable = true; + access = gameState.ai.accessibility.landPassMap[k]; break; - needed = number - workers.length; - } - if (!workers.length) - return false; - return workers; -}; - -/** - * @return {Object} - Resources (estimation) still gatherable in our territory. - */ -BasesManager.prototype.getTotalResourceLevel = function(gameState, resources = Resources.GetCodes(), proximity = ["nearby", "medium"]) -{ - const total = {}; - for (const res of resources) - total[res] = 0; - for (const base of this.baseManagers) - for (const res in total) - total[res] += base.getResourceLevel(gameState, res, proximity); - - 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. - */ -BasesManager.prototype.GetCurrentGatherRates = function(gameState) -{ - if (!this.turnCache.currentRates) - { - const currentRates = {}; - for (const res of Resources.GetCodes()) - currentRates[res] = 0.5 * this.GetTCResGatherer(res); - - this.addGatherRates(gameState, currentRates); - - for (const res of Resources.GetCodes()) - currentRates[res] = Math.max(currentRates[res], 0); - - this.turnCache.currentRates = currentRates; - } - - return this.turnCache.currentRates; -}; - -/** Some functions that register that we assigned a gatherer to a resource this turn */ - -/** Add a gatherer to the turn cache for this supply. */ -BasesManager.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 from the turn cache for this supply. */ -BasesManager.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; - } -}; - -BasesManager.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. */ -BasesManager.prototype.AddTCResGatherer = function(resource) -{ - const check = "resourceGatherer-" + resource; - if (this.turnCache[check]) - ++this.turnCache[check]; - else - this.turnCache[check] = 1; - - if (this.turnCache.currentRates) - this.turnCache.currentRates[resource] += 0.5; -}; - -BasesManager.prototype.GetTCResGatherer = function(resource) -{ - const check = "resourceGatherer-" + resource; - if (this.turnCache[check]) - return this.turnCache[check]; - - return 0; -}; - -/** - * returns the number of bases with a cc - * ActiveBases includes only those with a built cc - * PotentialBases includes also those with a cc in construction - */ -BasesManager.prototype.numActiveBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.active; -}; - -BasesManager.prototype.hasActiveBase = function() -{ - return !!this.numActiveBases(); -}; - -BasesManager.prototype.numPotentialBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.potential; -}; - -BasesManager.prototype.hasPotentialBase = function() -{ - return !!this.numPotentialBases(); -}; - -/** - * Updates the number of active and potential bases. - * .potential {number} - Bases that may or may not still be a foundation. - * .active {number} - Usable bases. - */ -BasesManager.prototype.updateBaseCache = function() -{ - this.turnCache.base = { "active": 0, "potential": 0 }; - for (const base of this.baseManagers) - { - if (!base.anchor) - continue; - ++this.turnCache.base.potential; - if (base.anchor.foundationProgress() === undefined) - ++this.turnCache.base.active; - } -}; - -BasesManager.prototype.resetBaseCache = function() -{ - this.turnCache.base = undefined; -}; - -BasesManager.prototype.baselessBase = function() -{ - return this.noBase; -}; - -/** - * @param {number} baseID - * @return {Object} - The base corresponding to baseID. - */ -BasesManager.prototype.getBaseByID = function(baseID) -{ - if (this.noBase.ID === baseID) - return this.noBase; - return this.baseManagers.find(base => base.ID === baseID); -}; - -/** - * flag a resource as exhausted - */ -BasesManager.prototype.isResourceExhausted = function(resource) -{ - return this.baseManagers.every(base => - !base.dropsiteSupplies[resource].nearby.length && - !base.dropsiteSupplies[resource].medium.length && - !base.dropsiteSupplies[resource].faraway.length); -}; - -/** - * Count gatherers returning resources in the number of gatherers of resourceSupplies - * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). - */ -BasesManager.prototype.assignGatherers = function() -{ - for (const base of this.baseManagers) - for (const worker of base.workers.values()) - { - if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) - continue; - const orders = worker.unitAIOrderData(); - if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) - continue; - this.AddTCGatherer(orders[1].target); } -}; - -/** - * Assign an entity to the closest base. - * Used by the starting strategy. - */ -BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex) -{ - let bestbase; - for (const base of this.baseManagers) - { - if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) && - base.territoryIndices.indexOf(territoryIndex) == -1) - continue; - base.assignEntity(gameState, ent); - bestbase = base; - break; - } - if (!bestbase) // entity outside our territory - { - if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes()) - bestbase = this.createBase(gameState, ent, BaseManager.STATE_ANCHORLESS); - else - bestbase = getBestBase(gameState, ent) || this.noBase; - bestbase.assignEntity(gameState, ent); - } - // now assign entities garrisoned inside this entity - if (ent.isGarrisonHolder() && ent.garrisoned().length) - for (const id of ent.garrisoned()) - bestbase.assignEntity(gameState, gameState.getEntityById(id)); - // and find something useful to do if we already have a base - if (ent.position() && bestbase.ID !== this.noBase.ID) - { - bestbase.assignRolelessUnits(gameState, [ent]); - if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER) - { - bestbase.reassignIdleWorkers(gameState, [ent]); - bestbase.workerObject.update(gameState, ent); - } - } -}; - -/** - * Adds the gather rates of individual bases to a shared object. - * @param {Object} gameState - * @param {Object} rates - The rates to add the gather rates to. - */ -BasesManager.prototype.addGatherRates = function(gameState, rates) -{ - for (const base of this.baseManagers) - base.addGatherRates(gameState, rates); -}; - -/** - * @param {number} territoryIndex - * @return {number} - The ID of the base at the given territory index. - */ -BasesManager.prototype.baseAtIndex = function(territoryIndex) -{ - return this.basesMap.map[territoryIndex]; -}; - -/** - * @param {number} territoryIndex - */ -BasesManager.prototype.removeBaseFromTerritoryIndex = function(territoryIndex) -{ - const baseID = this.basesMap.map[territoryIndex]; - if (baseID == 0) - return; - const base = this.getBaseByID(baseID); - if (base) - { - const index = base.territoryIndices.indexOf(territoryIndex); - if (index != -1) - base.territoryIndices.splice(index, 1); - else - aiWarn(" problem in headquarters::updateTerritories for base " + baseID); - } - else - aiWarn(" problem in headquarters::updateTerritories without base " + baseID); - this.basesMap.map[territoryIndex] = 0; -}; - -/** - * @return {boolean} - Whether the index was added to a base. - */ -BasesManager.prototype.addTerritoryIndexToBase = function(gameState, territoryIndex, passabilityMap) -{ - if (this.baseAtIndex(territoryIndex) != 0) - return false; - let landPassable = false; - const ind = getMapIndices(territoryIndex, gameState.ai.HQ.territoryMap, passabilityMap); - let access; - for (const k of ind) - { - if (!gameState.ai.HQ.landRegions[gameState.ai.accessibility.landPassMap[k]]) - continue; - landPassable = true; - access = gameState.ai.accessibility.landPassMap[k]; - break; - } - if (!landPassable) - return false; - let distmin = Math.min(); - let baseID; - const pos = [gameState.ai.HQ.territoryMap.cellSize * (territoryIndex % gameState.ai.HQ.territoryMap.width + 0.5), gameState.ai.HQ.territoryMap.cellSize * (Math.floor(territoryIndex / gameState.ai.HQ.territoryMap.width) + 0.5)]; - for (const base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - const dist = SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (!baseID) - return false; - this.getBaseByID(baseID).territoryIndices.push(territoryIndex); - this.basesMap.map[territoryIndex] = baseID; - return true; -}; - -/** Reassign territories when a base is going to be deleted */ -BasesManager.prototype.reassignTerritories = function(deletedBase, territoryMap) -{ - const cellSize = territoryMap.cellSize; - const width = territoryMap.width; - for (let j = 0; j < territoryMap.length; ++j) - { - if (this.basesMap.map[j] != deletedBase.ID) - continue; - if (territoryMap.getOwnerIndex(j) != PlayerID) - { - aiWarn("Petra reassignTerritories: should never happen"); - this.basesMap.map[j] = 0; - continue; - } - + if (!landPassable) + return false; let distmin = Math.min(); let baseID; - const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + const pos = [gameState.ai.HQ.territoryMap.cellSize * (territoryIndex % gameState.ai.HQ.territoryMap.width + 0.5), gameState.ai.HQ.territoryMap.cellSize * (Math.floor(territoryIndex / gameState.ai.HQ.territoryMap.width) + 0.5)]; for (const base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; - if (base.accessIndex != deletedBase.accessIndex) + if (base.accessIndex != access) continue; const dist = SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) @@ -733,77 +696,116 @@ BasesManager.prototype.reassignTerritories = function(deletedBase, territoryMap) distmin = dist; baseID = base.ID; } - if (baseID) + if (!baseID) + return false; + this.getBaseByID(baseID).territoryIndices.push(territoryIndex); + this.basesMap.map[territoryIndex] = baseID; + return true; + } + + /** Reassign territories when a base is going to be deleted */ + reassignTerritories(deletedBase, territoryMap) + { + const cellSize = territoryMap.cellSize; + const width = territoryMap.width; + for (let j = 0; j < territoryMap.length; ++j) { - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; + if (this.basesMap.map[j] != deletedBase.ID) + continue; + if (territoryMap.getOwnerIndex(j) != PlayerID) + { + aiWarn("Petra reassignTerritories: should never happen"); + this.basesMap.map[j] = 0; + continue; + } + + let distmin = Math.min(); + let baseID; + const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + for (const base of this.baseManagers) + { + if (!base.anchor || !base.anchor.position()) + continue; + if (base.accessIndex != deletedBase.accessIndex) + continue; + const dist = SquareVectorDistance(base.anchor.position(), pos); + if (dist >= distmin) + continue; + distmin = dist; + baseID = base.ID; + } + if (baseID) + { + this.getBaseByID(baseID).territoryIndices.push(j); + this.basesMap.map[j] = baseID; + } + else + this.basesMap.map[j] = 0; } - else - this.basesMap.map[j] = 0; } -}; -/** - * We will loop only on one active base per turn. - */ -BasesManager.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("BasesManager update"); - - this.assignGatherers(); - let nbBases = this.baseManagers.length; - let activeBase = false; - this.noBase.update(gameState, queues, events); - while (!activeBase && nbBases != 0) + /** + * We will loop only on one active base per turn. + */ + update(gameState, queues, events) { - this.currentBase %= this.baseManagers.length; - activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); - --nbBases; - // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); + Engine.ProfileStart("BasesManager update"); + + this.assignGatherers(); + let nbBases = this.baseManagers.length; + let activeBase = false; + this.noBase.update(gameState, queues, events); + while (!activeBase && nbBases != 0) + { + this.currentBase %= this.baseManagers.length; + activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); + --nbBases; + // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); + } + + Engine.ProfileStop(); } - Engine.ProfileStop(); -}; - -BasesManager.prototype.Serialize = function() -{ - const properties = { - "currentBase": this.currentBase - }; - - const baseManagers = []; - for (const base of this.baseManagers) - baseManagers.push(base.Serialize()); - - return { - "properties": properties, - // noBase can be undefined if the region analysis failed and the managers haven't been initialised. - "noBase": this.noBase?.Serialize(), - "baseManagers": baseManagers - }; -}; - -BasesManager.prototype.Deserialize = function(gameState, data) -{ - for (const key in data.properties) - this[key] = data.properties[key]; - - if (data.noBase) + Serialize() { - this.noBase = new BaseManager(gameState, this); - this.noBase.Deserialize(gameState, data.noBase); - this.noBase.init(gameState, BaseManager.STATE_WITH_ANCHOR); - this.noBase.Deserialize(gameState, data.noBase); + const properties = { + "currentBase": this.currentBase + }; + + const baseManagers = []; + for (const base of this.baseManagers) + baseManagers.push(base.Serialize()); + + return { + "properties": properties, + // noBase can be undefined if the region analysis failed and the managers haven't been initialised. + "noBase": this.noBase?.Serialize(), + "baseManagers": baseManagers + }; } - this.baseManagers = []; - for (const basedata of data.baseManagers) + Deserialize(gameState, data) { - // The first call to deserialize set the ID base needed by entitycollections. - const newbase = new BaseManager(gameState, this); - newbase.Deserialize(gameState, basedata); - newbase.init(gameState, BaseManager.STATE_WITH_ANCHOR); - newbase.Deserialize(gameState, basedata); - this.baseManagers.push(newbase); + for (const key in data.properties) + this[key] = data.properties[key]; + + if (data.noBase) + { + this.noBase = new BaseManager(gameState, this); + this.noBase.Deserialize(gameState, data.noBase); + this.noBase.init(gameState, BaseManager.STATE_WITH_ANCHOR); + this.noBase.Deserialize(gameState, data.noBase); + } + + this.baseManagers = []; + for (const basedata of data.baseManagers) + { + // The first call to deserialize set the ID base needed by entitycollections. + const newbase = new BaseManager(gameState, this); + newbase.Deserialize(gameState, basedata); + newbase.init(gameState, BaseManager.STATE_WITH_ANCHOR); + newbase.Deserialize(gameState, basedata); + this.baseManagers.push(newbase); + } } -}; +} diff --git a/binaries/data/mods/public/simulation/ai/petra/buildManager.js b/binaries/data/mods/public/simulation/ai/petra/buildManager.js index 9e87bcf199..55c0c7c2ab 100644 --- a/binaries/data/mods/public/simulation/ai/petra/buildManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/buildManager.js @@ -1,21 +1,6 @@ import * as filters from "simulation/ai/common-api/filters.js"; import { aiWarn } from "simulation/ai/common-api/utils.js"; -/** - * One task of this manager is to cache the list of structures we have builders for, - * to avoid having to loop on all entities each time. - * It also takes care of the structures we can't currently build and should not try to build endlessly. - */ - -export function BuildManager() -{ - // List of buildings we have builders for, with number of possible builders. - this.builders = new Map(); - // List of buildings we can't currently build (because no room, no builder or whatever), - // with time we should wait before trying again to build it. - this.unbuildables = new Map(); -} - function addBuilder(builders, civ, entity) { for (const buildable of entity.buildableEntities(civ)) @@ -36,153 +21,171 @@ function removeBuilder(builders, entityId) } } -/** Initialization at start of game */ -BuildManager.prototype.init = function(gameState) +/** + * One task of this manager is to cache the list of structures we have builders for, + * to avoid having to loop on all entities each time. + * It also takes care of the structures we can't currently build and should not try to build endlessly. + */ + +export class BuildManager { - const civ = gameState.getPlayerCiv(); - for (const ent of gameState.getOwnUnits().values()) - addBuilder(this.builders, civ, ent); -}; + // List of buildings we have builders for, with number of possible builders. + builders = new Map(); + // List of buildings we can't currently build (because no room, no builder or whatever), + // with time we should wait before trying again to build it. + unbuildables = new Map(); -/** Update the builders counters */ -BuildManager.prototype.checkEvents = function(gameState, events) -{ - this.elapsedTime = gameState.ai.elapsedTime; - const civ = gameState.getPlayerCiv(); - - for (const evt of events.Create) + /** Initialization at start of game */ + init(gameState) { - if (events.Destroy.some(e => e.entity == evt.entity)) - continue; - const ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) - addBuilder(this.builders, civ, ent); - } - - for (const evt of events.Destroy) - removeBuilder(this.builders, evt.entity); - - for (const evt of events.OwnershipChanged) // capture events - { - if (evt.from == PlayerID) - { - removeBuilder(this.builders, evt.entity); - continue; - } - else if (evt.to != PlayerID) - continue; - - const ent = gameState.getEntityById(evt.entity); - if (ent?.hasClass("Unit")) - addBuilder(this.builders, civ, ent); - } - - for (const evt of events.ValueModification) - { - if (evt.component != "Builder" || - !evt.valueNames.some(val => val.startsWith("Builder/Entities/"))) - continue; - - // Unfortunately there really is not an easy way to determine the changes - // at this stage, so we simply have to dump the cache. - this.builders = new Map(); - + const civ = gameState.getPlayerCiv(); for (const ent of gameState.getOwnUnits().values()) addBuilder(this.builders, civ, ent); } -}; - -/** - * Get the buildable structures passing a filter. - */ -BuildManager.prototype.findStructuresByFilter = function(gameState, filter) -{ - const result = []; - for (const [templateName, entities] of this.builders) + /** Update the builders counters */ + checkEvents(gameState, events) { - if (!entities.length || gameState.isTemplateDisabled(templateName)) - continue; - const template = gameState.getTemplate(templateName); - if (!template || !template.available(gameState)) - continue; - if (filter.func(template)) - result.push(templateName); - } - return result; -}; + this.elapsedTime = gameState.ai.elapsedTime; + const civ = gameState.getPlayerCiv(); -/** - * Get the first buildable structure with a given class - * TODO when several available, choose the best one - */ -BuildManager.prototype.findStructureWithClass = function(gameState, classes) -{ - return this.findStructuresByFilter(gameState, filters.byClasses(classes))[0]; -}; - -BuildManager.prototype.hasBuilder = function(template) -{ - const numBuilders = this.builders.get(template); - return numBuilders && numBuilders.length > 0; -}; - -BuildManager.prototype.isUnbuildable = function(gameState, template) -{ - return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime; -}; - -BuildManager.prototype.setBuildable = function(template) -{ - if (this.unbuildables.has(template)) - this.unbuildables.delete(template); -}; - -/** Time is the duration in second that we will wait before checking again if it is buildable */ -BuildManager.prototype.setUnbuildable = function(gameState, template, time = 90, reason = "room") -{ - if (!this.unbuildables.has(template)) - this.unbuildables.set(template, { "reason": reason, "time": gameState.ai.elapsedTime + time }); - else - { - const unbuildable = this.unbuildables.get(template); - if (unbuildable.time < gameState.ai.elapsedTime + time) + for (const evt of events.Create) { - unbuildable.reason = reason; - unbuildable.time = gameState.ai.elapsedTime + time; + if (events.Destroy.some(e => e.entity == evt.entity)) + continue; + const ent = gameState.getEntityById(evt.entity); + if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) + addBuilder(this.builders, civ, ent); + } + + for (const evt of events.Destroy) + removeBuilder(this.builders, evt.entity); + + for (const evt of events.OwnershipChanged) // capture events + { + if (evt.from == PlayerID) + { + removeBuilder(this.builders, evt.entity); + continue; + } + else if (evt.to != PlayerID) + continue; + + const ent = gameState.getEntityById(evt.entity); + if (ent?.hasClass("Unit")) + addBuilder(this.builders, civ, ent); + } + + for (const evt of events.ValueModification) + { + if (evt.component != "Builder" || + !evt.valueNames.some(val => val.startsWith("Builder/Entities/"))) + continue; + + // Unfortunately there really is not an easy way to determine the changes + // at this stage, so we simply have to dump the cache. + this.builders = new Map(); + + for (const ent of gameState.getOwnUnits().values()) + addBuilder(this.builders, civ, ent); } } -}; -/** Return the number of unbuildables due to missing room */ -BuildManager.prototype.numberMissingRoom = function(gameState) -{ - let num = 0; - for (const unbuildable of this.unbuildables.values()) - if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) - ++num; - return num; -}; -/** Reset the unbuildables due to missing room */ -BuildManager.prototype.resetMissingRoom = function(gameState) -{ - for (const [key, unbuildable] of this.unbuildables) - if (unbuildable.reason == "room") - this.unbuildables.delete(key); -}; + /** + * Get the buildable structures passing a filter. + */ + findStructuresByFilter(gameState, filter) + { + const result = []; + for (const [templateName, entities] of this.builders) + { + if (!entities.length || gameState.isTemplateDisabled(templateName)) + continue; + const template = gameState.getTemplate(templateName); + if (!template || !template.available(gameState)) + continue; + if (filter.func(template)) + result.push(templateName); + } + return result; + } -BuildManager.prototype.Serialize = function() -{ - return { - "builders": this.builders, - "unbuildables": this.unbuildables - }; -}; + /** + * Get the first buildable structure with a given class + * TODO when several available, choose the best one + */ + findStructureWithClass(gameState, classes) + { + return this.findStructuresByFilter(gameState, filters.byClasses(classes))[0]; + } -BuildManager.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; -}; + hasBuilder(template) + { + const numBuilders = this.builders.get(template); + return numBuilders && numBuilders.length > 0; + } + isUnbuildable(gameState, template) + { + return this.unbuildables.has(template) && + this.unbuildables.get(template).time > gameState.ai.elapsedTime; + } + + setBuildable(template) + { + if (this.unbuildables.has(template)) + this.unbuildables.delete(template); + } + + /** Time is the duration in second that we will wait before checking again if it is buildable */ + setUnbuildable(gameState, template, time = 90, reason = "room") + { + if (!this.unbuildables.has(template)) + { + this.unbuildables.set(template, + { "reason": reason, "time": gameState.ai.elapsedTime + time }); + } + else + { + const unbuildable = this.unbuildables.get(template); + if (unbuildable.time < gameState.ai.elapsedTime + time) + { + unbuildable.reason = reason; + unbuildable.time = gameState.ai.elapsedTime + time; + } + } + } + + /** Return the number of unbuildables due to missing room */ + numberMissingRoom(gameState) + { + let num = 0; + for (const unbuildable of this.unbuildables.values()) + if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) + ++num; + return num; + } + + /** Reset the unbuildables due to missing room */ + resetMissingRoom(gameState) + { + for (const [key, unbuildable] of this.unbuildables) + if (unbuildable.reason == "room") + this.unbuildables.delete(key); + } + + Serialize() + { + return { + "builders": this.builders, + "unbuildables": this.unbuildables + }; + } + + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/config.js b/binaries/data/mods/public/simulation/ai/petra/config.js index 7c650e3e8a..b01889712d 100644 --- a/binaries/data/mods/public/simulation/ai/petra/config.js +++ b/binaries/data/mods/public/simulation/ai/petra/config.js @@ -1,21 +1,16 @@ import { aiWarn } from "simulation/ai/common-api/utils.js"; import * as difficultyLevel from "simulation/ai/petra/difficultyLevel.js"; -export function Config(difficulty = difficultyLevel.MEDIUM, behavior) +export class Config { - this.difficulty = difficulty; - - // for instance "balanced", "aggressive" or "defensive" - this.behavior = behavior || "random"; - // debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug - this.debug = 0; + debug = 0; - this.chat = true; // false to prevent AI's chats + chat = true; // false to prevent AI's chats - this.popScaling = 1; // scale factor depending on the max population + popScaling = 1; // scale factor depending on the max population - this.Military = { + Military = { "towerLapseTime": 360, // Time to wait between building 2 towers "fortressLapseTime": 390, // Time to wait between building 2 fortresses "popForBarracks1": 25, @@ -24,14 +19,14 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) "numSentryTowers": 1 }; - this.DamageTypeImportance = { + DamageTypeImportance = { "Hack": 0.075, "Pierce": 0.085, "Crush": 0.045, "Fire": 0.001 }; - this.Economy = { + Economy = { "popPhase2": 150, // How many units we want before aging to phase2. "workPhase3": 180, // How many workers we want before aging to phase3. "workPhase4": 200, // How many workers we want before aging to phase4 or higher. @@ -45,7 +40,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) // Note: attack settings are set directly in attack_plan.js // defense - this.Defense = + Defense = { "defenseRatio": { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers. "armyCompactSize": 2000, // squared. Half-diameter of an army. @@ -55,7 +50,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) // Additional buildings that the AI does not yet know when to build // and that it will try to build on phase 3 when enough resources. - this.buildings = + buildings = { "default": [], "achae": [ @@ -112,7 +107,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) ] }; - this.priorities = + priorities = { "villager": 300, // should be slightly lower than the citizen soldier one to not get all the food "citizenSoldier": 600, @@ -135,7 +130,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) }; // Default personality (will be updated in setConfig) - this.personality = + personality = { "aggressive": 0.5, "cooperative": 0.5, @@ -143,7 +138,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) }; // See QueueManager.prototype.wantedGatherRates() - this.queues = + queues = { "firstTurn": { "food": 10, @@ -163,15 +158,15 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) } }; - this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; + garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; - this.unusedNoAllyTechs = [ + unusedNoAllyTechs = [ "Player/sharedLos", "Market/InternationalBonus", "Player/sharedDropsites" ]; - this.criticalPopulationFactors = [ + criticalPopulationFactors = [ 0.8, 0.8, 0.7, @@ -180,7 +175,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) 0.35 ]; - this.criticalStructureFactors = [ + criticalStructureFactors = [ 0.8, 0.8, 0.7, @@ -189,7 +184,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) 0.35 ]; - this.criticalRootFactors = [ + criticalRootFactors = [ 0.8, 0.8, 0.67, @@ -197,157 +192,165 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior) 0.35, 0.2 ]; -} -Config.prototype.setConfig = function(gameState) -{ - if (this.difficulty > difficultyLevel.SANDBOX) + constructor(difficulty = difficultyLevel.MEDIUM, behavior) { - // Setup personality traits according to the user choice: - // The parameter used to define the personality is basically the aggressivity or (1-defensiveness) - // as they are anticorrelated, although some small smearing to decorelate them will be added. - // And for each user choice, this parameter can vary between min and max - const personalityList = { - "random": { "min": 0, "max": 1 }, - "defensive": { "min": 0, "max": 0.27 }, - "balanced": { "min": 0.37, "max": 0.63 }, - "aggressive": { "min": 0.73, "max": 1 } - }; - const behavior = randFloat(-0.5, 0.5); - // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety - const variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); - const aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; - const defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; - const min = personalityList[this.behavior].min; - const max = personalityList[this.behavior].max; - this.personality = { - "aggressive": min + aggressive * (max - min), - "defensive": 1 - max + defensive * (max - min), - "cooperative": randFloat(0, 1) - }; + this.difficulty = difficulty; + + // for instance "balanced", "aggressive" or "defensive" + this.behavior = behavior || "random"; } - // Petra usually uses the continuous values of personality.aggressive and personality.defensive - // to define its behavior according to personality. But when discontinuous behavior is needed, - // it uses the following personalityCut which should be set such that: - // behavior="aggressive" => personality.aggressive > personalityCut.strong && - // personality.defensive < personalityCut.weak - // and inversely for behavior="defensive" - this.personalityCut = { "weak": 0.3, "medium": 0.5, "strong": 0.7 }; - if (gameState.playerData.teamsLocked) - this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30); - else if (gameState.getAlliedVictory()) - this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15); - - // changing settings based on difficulty or personality - this.Military.towerLapseTime = Math.round(this.Military.towerLapseTime * (1.1 - 0.2 * this.personality.defensive)); - this.Military.fortressLapseTime = Math.round(this.Military.fortressLapseTime * (1.1 - 0.2 * this.personality.defensive)); - this.priorities.defenseBuilding = Math.round(this.priorities.defenseBuilding * (0.9 + 0.2 * this.personality.defensive)); - - if (this.difficulty < difficultyLevel.EASY) + setConfig(gameState) { - this.popScaling = 0.5; - this.Economy.supportRatio = 0.5; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else if (this.difficulty < difficultyLevel.MEDIUM) - { - this.popScaling = 0.7; - this.Economy.supportRatio = 0.4; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else - { - if (this.difficulty == difficultyLevel.MEDIUM) - this.Military.numSentryTowers = 1; - else - this.Military.numSentryTowers = 2; - if (this.personality.defensive > this.personalityCut.strong) - ++this.Military.numSentryTowers; - else if (this.personality.defensive < this.personalityCut.weak) - --this.Military.numSentryTowers; - - if (this.personality.aggressive > this.personalityCut.strong) + if (this.difficulty > difficultyLevel.SANDBOX) { - this.Military.popForBarracks1 = 12; - this.Economy.popPhase2 = 50; - this.priorities.healer = 10; + // Setup personality traits according to the user choice: + // The parameter used to define the personality is basically the aggressivity or (1-defensiveness) + // as they are anticorrelated, although some small smearing to decorelate them will be added. + // And for each user choice, this parameter can vary between min and max + const personalityList = { + "random": { "min": 0, "max": 1 }, + "defensive": { "min": 0, "max": 0.27 }, + "balanced": { "min": 0.37, "max": 0.63 }, + "aggressive": { "min": 0.73, "max": 1 } + }; + const behavior = randFloat(-0.5, 0.5); + // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety + const variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); + const aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; + const defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; + const min = personalityList[this.behavior].min; + const max = personalityList[this.behavior].max; + this.personality = { + "aggressive": min + aggressive * (max - min), + "defensive": 1 - max + defensive * (max - min), + "cooperative": randFloat(0, 1) + }; } + // Petra usually uses the continuous values of personality.aggressive and personality.defensive + // to define its behavior according to personality. But when discontinuous behavior is needed, + // it uses the following personalityCut which should be set such that: + // behavior="aggressive" => personality.aggressive > personalityCut.strong && + // personality.defensive < personalityCut.weak + // and inversely for behavior="defensive" + this.personalityCut = { "weak": 0.3, "medium": 0.5, "strong": 0.7 }; + + if (gameState.playerData.teamsLocked) + this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30); + else if (gameState.getAlliedVictory()) + this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15); + + // changing settings based on difficulty or personality + this.Military.towerLapseTime = Math.round(this.Military.towerLapseTime * (1.1 - 0.2 * this.personality.defensive)); + this.Military.fortressLapseTime = Math.round(this.Military.fortressLapseTime * (1.1 - 0.2 * this.personality.defensive)); + this.priorities.defenseBuilding = Math.round(this.priorities.defenseBuilding * (0.9 + 0.2 * this.personality.defensive)); + + if (this.difficulty < difficultyLevel.EASY) + { + this.popScaling = 0.5; + this.Economy.supportRatio = 0.5; + this.Economy.provisionFields = 1; + this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; + } + else if (this.difficulty < difficultyLevel.MEDIUM) + { + this.popScaling = 0.7; + this.Economy.supportRatio = 0.4; + this.Economy.provisionFields = 1; + this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; + } + else + { + if (this.difficulty == difficultyLevel.MEDIUM) + this.Military.numSentryTowers = 1; + else + this.Military.numSentryTowers = 2; + if (this.personality.defensive > this.personalityCut.strong) + ++this.Military.numSentryTowers; + else if (this.personality.defensive < this.personalityCut.weak) + --this.Military.numSentryTowers; + + if (this.personality.aggressive > this.personalityCut.strong) + { + this.Military.popForBarracks1 = 12; + this.Economy.popPhase2 = 50; + this.priorities.healer = 10; + } + } + + const maxPop = gameState.getPopulationMax(); + if (this.difficulty < difficultyLevel.EASY) + this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); + else if (this.difficulty < difficultyLevel.MEDIUM) + 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 (gameState.getVictoryConditions().has("wonder")) + { + this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3); + this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4); + } + + 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.popForForge = Math.min(Math.max(Math.floor(this.Military.popForForge * this.popScaling), 30), Math.floor(maxPop/2)); + this.Economy.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2)); + this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3)); + this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), Math.floor(maxPop*2/3)); + this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling); + this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popPhase2); + this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers); + this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers); + if (this.difficulty < difficultyLevel.EASY) + this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase + + this.emergencyValues = { + "population": this.criticalPopulationFactors[this.difficulty], + "structures": this.criticalStructureFactors[this.difficulty], + "roots": this.criticalRootFactors[this.difficulty], + }; + + this.Cheat(gameState); + + if (this.debug < 2) + return; + aiWarn(" >>> Petra bot: personality = " + uneval(this.personality)); } - const maxPop = gameState.getPopulationMax(); - if (this.difficulty < difficultyLevel.EASY) - this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); - else if (this.difficulty < difficultyLevel.MEDIUM) - 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 (gameState.getVictoryConditions().has("wonder")) + Cheat(gameState) { - this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3); - this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4); + // Sandbox, Very Easy, Easy, Medium, Hard, Very Hard + // rate apply on resource stockpiling as gathering and trading + // time apply on building, upgrading, packing, training and technologies + const rate = [ 0.42, 0.56, 0.75, 1.00, 1.25, 1.56 ]; + const time = [ 1.40, 1.25, 1.10, 1.00, 1.00, 1.00 ]; + const AIDiff = Math.min(this.difficulty, rate.length - 1); + SimEngine.QueryInterface(Sim.SYSTEM_ENTITY, Sim.IID_ModifiersManager).AddModifiers("AI Bonus", { + "ResourceGatherer/BaseSpeed": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }], + "Trader/GainMultiplier": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }], + "Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }], + }, gameState.playerData.entity); } - if (maxPop < 300) - this.popScaling *= Math.sqrt(maxPop / 300); + Serialize() + { + var data = {}; + for (const key in this) + if (Object.hasOwn(this, key) && key != "debug") + data[key] = this[key]; + return data; + } - 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.popForForge = Math.min(Math.max(Math.floor(this.Military.popForForge * this.popScaling), 30), Math.floor(maxPop/2)); - this.Economy.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2)); - this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3)); - this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), Math.floor(maxPop*2/3)); - this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling); - this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popPhase2); - this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers); - this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers); - if (this.difficulty < difficultyLevel.EASY) - this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase - - this.emergencyValues = { - "population": this.criticalPopulationFactors[this.difficulty], - "structures": this.criticalStructureFactors[this.difficulty], - "roots": this.criticalRootFactors[this.difficulty], - }; - - this.Cheat(gameState); - - if (this.debug < 2) - return; - aiWarn(" >>> Petra bot: personality = " + uneval(this.personality)); -}; - -Config.prototype.Cheat = function(gameState) -{ - // Sandbox, Very Easy, Easy, Medium, Hard, Very Hard - // rate apply on resource stockpiling as gathering and trading - // time apply on building, upgrading, packing, training and technologies - const rate = [ 0.42, 0.56, 0.75, 1.00, 1.25, 1.56 ]; - const time = [ 1.40, 1.25, 1.10, 1.00, 1.00, 1.00 ]; - const AIDiff = Math.min(this.difficulty, rate.length - 1); - SimEngine.QueryInterface(Sim.SYSTEM_ENTITY, Sim.IID_ModifiersManager).AddModifiers("AI Bonus", { - "ResourceGatherer/BaseSpeed": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }], - "Trader/GainMultiplier": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }], - "Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }], - }, gameState.playerData.entity); -}; - -Config.prototype.Serialize = function() -{ - var data = {}; - for (const key in this) - if (Object.hasOwn(this, key) && key != "debug") - data[key] = this[key]; - return data; -}; - -Config.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; -}; + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js b/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js index c5b7fbfd2e..f25e058834 100644 --- a/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js +++ b/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js @@ -13,645 +13,648 @@ import { Worker } from "simulation/ai/petra/worker.js"; * "capturing": army set to capture a gaia building or recover capture points to one of its own structures * It must contain only one foe (the building to capture) and never be merged */ -export function DefenseArmy(gameState, foeEntities, type) +export class DefenseArmy { - this.ID = gameState.ai.uniqueIDs.armies++; - this.type = type || "default"; - - this.Config = gameState.ai.Config; - 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 (const id of foeEntities) - this.addFoe(gameState, id, true); - - this.recalculatePosition(gameState, true); - - return true; -} - -/** - * 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 - */ -DefenseArmy.prototype.addFoe = function(gameState, enemyId, force) -{ - if (this.foeEntities.indexOf(enemyId) !== -1) - return false; - const ent = gameState.getEntityById(enemyId); - if (!ent || !ent.position()) - return false; - - // check distance - if (!force && 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. - */ -DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity) -{ - const idx = this.foeEntities.indexOf(enemyId); - if (idx === -1) - return false; - - this.foeEntities.splice(idx, 1); - - this.assignedAgainst[enemyId] = undefined; - for (const to in this.assignedTo) - if (this.assignedTo[to] == enemyId) - this.assignedTo[to] = undefined; - - const ent = enemyEntity ? enemyEntity : gameState.getEntityById(enemyId); - if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) + constructor(gameState, foeEntities, type) { - this.evaluateStrength(ent, false, true); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - } + this.ID = gameState.ai.uniqueIDs.armies++; + this.type = type || "default"; - return true; -}; + this.Config = gameState.ai.Config; + this.compactSize = this.Config.Defense.armyCompactSize; + this.breakawaySize = this.Config.Defense.armyBreakawaySize; -/** - * 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 - */ -DefenseArmy.prototype.addOwn = function(gameState, id, force) -{ - if (this.ownEntities.indexOf(id) !== -1) - return false; - const 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; - - const plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - const subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole === undefined || subrole !== Worker.SUBROLE_DEFENDER) - ent.setMetadata(PlayerID, "formerSubrole", subrole); - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_DEFENDER); - return true; -}; - -DefenseArmy.prototype.removeOwn = function(gameState, id, Entity) -{ - const idx = this.ownEntities.indexOf(id); - if (idx === -1) - return false; - - this.ownEntities.splice(idx, 1); - - if (this.assignedTo[id] !== 0) - { - const temp = this.assignedAgainst[this.assignedTo[id]]; - if (temp) - temp.splice(temp.indexOf(id), 1); - } - this.assignedTo[id] = undefined; - - const ent = Entity ? Entity : gameState.getEntityById(id); - if (!ent) - return true; - - 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); - - const formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); - if (formerSubrole !== undefined) - ent.setMetadata(PlayerID, "subrole", formerSubrole); - else - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "formerSubrole", undefined); - - // Remove from transport plan if not yet on Board - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - const plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan && plan.state === TransportPlan.BOARDING && ent.position()) - plan.removeUnit(gameState, ent); - } - - /* - // 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; -}; - -/** - * resets the army properly. - * assumes we already cleared dead units. - */ -DefenseArmy.prototype.clear = function(gameState) -{ - while (this.foeEntities.length > 0) - this.removeFoe(gameState, this.foeEntities[0]); - - // Go back to our or allied territory if needed - const posOwn = [0, 0]; - let nOwn = 0; - const posAlly = [0, 0]; - let nAlly = 0; - const posOther = [0, 0]; - let nOther = 0; - for (const entId of this.ownEntities) - { - const ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - const pos = ent.position(); - const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); - if (territoryOwner === PlayerID) - { - posOwn[0] += pos[0]; - posOwn[1] += pos[1]; - ++nOwn; - } - else if (gameState.isPlayerMutualAlly(territoryOwner)) - { - posAlly[0] += pos[0]; - posAlly[1] += pos[1]; - ++nAlly; - } - else - { - posOther[0] += pos[0]; - posOther[1] += pos[1]; - ++nOther; - } - } - let destination; - let defensiveFound; - let distmin; - let radius = 0; - if (nOwn > 0) - destination = [posOwn[0]/nOwn, posOwn[1]/nOwn]; - else if (nAlly > 0) - destination = [posAlly[0]/nAlly, posAlly[1]/nAlly]; - else - { - posOther[0] /= nOther; - posOther[1] /= nOther; - const armyAccess = gameState.ai.accessibility.getAccessValue(posOther); - for (const struct of gameState.getAllyStructures().values()) - { - const pos = struct.position(); - if (!pos || !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(pos))) - continue; - if (getLandAccess(gameState, struct) !== armyAccess) - continue; - const defensiveStruct = struct.hasDefensiveFire(); - if (defensiveFound && !defensiveStruct) - continue; - const dist = SquareVectorDistance(posOther, pos); - if (distmin && dist > distmin && (defensiveFound || !defensiveStruct)) - continue; - if (defensiveStruct) - defensiveFound = true; - distmin = dist; - destination = pos; - radius = struct.obstructionRadius().max; - } - } - while (this.ownEntities.length > 0) - { - const entId = this.ownEntities[0]; - this.removeOwn(gameState, entId); - const ent = gameState.getEntityById(entId); - if (ent) - { - if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined || - ent.getMetadata(PlayerID, "transporter") !== undefined) - continue; - if (ent.healthLevel() < this.Config.garrisonHealthLevel.low && - gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, ent)) - continue; - - if (destination && !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(ent.position()))) - ent.moveToRange(destination[0], destination[1], radius, radius + 5); - else - ent.stopMoving(); - } - } - - this.assignedAgainst = {}; - this.assignedTo = {}; - - this.recalculateStrengths(gameState); - this.recalculatePosition(gameState); -}; - -DefenseArmy.prototype.assignUnit = function(gameState, entID) -{ - // we'll assume this defender is ours already. - // we'll also override any previous assignment - - const ent = gameState.getEntityById(entID); - if (!ent || !ent.position()) - return false; - - // try to return its resources, and if any, the attack order will be queued - const queued = returnResources(gameState, ent); - - let idMin; - let distMin; - let idMinAll; - let distMinAll; - for (const id of this.foeEntities) - { - const eEnt = gameState.getEntityById(id); - if (!eEnt || !eEnt.position()) // probably can't happen. - continue; - - if (!ent.canAttackTarget(eEnt, allowCapture(gameState, ent, eEnt))) - continue; - - if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length && - eEnt.unitAIOrderData()[0].target && eEnt.unitAIOrderData()[0].target == entID) - { // being attacked >>> target the unit - idMin = id; - break; - } - - // already enough units against it - if (this.assignedAgainst[id].length > 8 || - this.assignedAgainst[id].length > 5 && !eEnt.hasClass("Hero") && !isSiegeUnit(eEnt)) - continue; - - const dist = SquareVectorDistance(ent.position(), eEnt.position()); - if (idMinAll === undefined || dist < distMinAll) - { - idMinAll = id; - distMinAll = dist; - } - if (this.assignedAgainst[id].length > 2) - continue; - if (idMin === undefined || dist < distMin) - { - idMin = id; - distMin = dist; - } - } - - let idFoe; - if (idMin !== undefined) - idFoe = idMin; - else if (idMinAll !== undefined) - idFoe = idMinAll; - else - return false; - - const ownIndex = getLandAccess(gameState, ent); - const foeEnt = gameState.getEntityById(idFoe); - const foePosition = foeEnt.position(); - const foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); - if (ownIndex == foeIndex || ent.hasClass("Ship")) - { - this.assignedTo[entID] = idFoe; - this.assignedAgainst[idFoe].push(entID); - ent.attack(idFoe, allowCapture(gameState, ent, foeEnt), queued); - } - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); - return true; -}; - -DefenseArmy.prototype.getType = function() -{ - return this.type; -}; - -DefenseArmy.prototype.getState = function() -{ - if (!this.foeEntities.length) - return 0; - return 1; -}; - -/** - * 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). - */ -DefenseArmy.prototype.merge = function(gameState, otherArmy) -{ - // copy over all parameters. - for (const 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 (const i in otherArmy.assignedTo) - this.assignedTo[i] = otherArmy.assignedTo[i]; - - for (const id of otherArmy.foeEntities) - this.addFoe(gameState, id, true); - // TODO: reassign those ? - for (const id of otherArmy.ownEntities) - this.addOwn(gameState, id, true); - - this.recalculatePosition(gameState, true); - this.recalculateStrengths(gameState); - - return true; -}; - -DefenseArmy.prototype.needsDefenders = function(gameState) -{ - let defenseRatio; - const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(this.foePosition); - if (territoryOwner == PlayerID) - defenseRatio = this.Config.Defense.defenseRatio.own; - else if (gameState.isPlayerAlly(territoryOwner)) - { - defenseRatio = this.Config.Defense.defenseRatio.ally; - let numExclusiveAllies = 0; - for (let p = 1; p < gameState.sharedScript.playersData.length; ++p) - if (p != territoryOwner && gameState.sharedScript.playersData[p].isAlly[territoryOwner]) - ++numExclusiveAllies; - defenseRatio /= 1 + 0.5*Math.max(0, numExclusiveAllies-1); - } - else - defenseRatio = this.Config.Defense.defenseRatio.neutral; - - // some preliminary checks because we don't update for tech so entStrength removed can be > entStrength added - if (this.foeStrength <= 0 || this.ownStrength <= 0) - this.recalculateStrengths(gameState); - - if (this.foeStrength * defenseRatio <= this.ownStrength) - return false; - return this.foeStrength * defenseRatio - this.ownStrength; -}; - - -/** if not forced, will only recalculate if on a different turn. */ -DefenseArmy.prototype.recalculatePosition = function(gameState, force) -{ - if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) - return; - - let npos = 0; - const pos = [0, 0]; - for (const id of this.foeEntities) - { - const ent = gameState.getEntityById(id); - if (!ent || !ent.position()) - continue; - npos++; - const 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; -}; - -DefenseArmy.prototype.recalculateStrengths = function(gameState) -{ - this.ownStrength = 0; - this.foeStrength = 0; - - for (const id of this.foeEntities) - this.evaluateStrength(gameState.getEntityById(id)); - for (const 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. */ -DefenseArmy.prototype.evaluateStrength = function(ent, isOwn, remove) -{ - if (!ent) - return; - - 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 = getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); - - // 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.hasClasses(["Animal+Elephant"])) - entStrength *= 3; - - if (remove) - entStrength *= -1; - - if (isOwn) - this.ownStrength += entStrength; - else - this.foeStrength += entStrength; -}; - -DefenseArmy.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 (const evt of events.EntityRenamed) // take care of promoted and packed units - { - if (this.foeEntities.indexOf(evt.entity) !== -1) - { - const ent = gameState.getEntityById(evt.newentity); - if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed - continue; - const 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 (const to in this.assignedTo) - if (this.assignedTo[to] === evt.entity) - this.assignedTo[to] = evt.newentity; - } - else if (this.ownEntities.indexOf(evt.entity) !== -1) - { - const newEnt = gameState.getEntityById(evt.newentity); - if (newEnt && (!newEnt.hasUnitAI() || !newEnt.attackTypes())) - continue; - const 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 (const 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 (const evt of events.Garrison) - this.removeFoe(gameState, evt.entity); - - for (const 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 (const evt of events.Destroy) - { - // we may have capture+destroy, so do not trust owner and check all possibilities - this.removeOwn(gameState, evt.entity); - this.removeFoe(gameState, evt.entity); - } -}; - -DefenseArmy.prototype.update = function(gameState) -{ - for (const entId of this.ownEntities) - { - const ent = gameState.getEntityById(entId); - if (!ent) - continue; - const orderData = ent.unitAIOrderData(); - if (!orderData.length && !ent.getMetadata(PlayerID, "transport")) - this.assignUnit(gameState, entId); - else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") - { - const target = gameState.getEntityById(orderData[0].target); - if (target && !allowCapture(gameState, ent, target)) - ent.attack(orderData[0].target, false); - } - } - - if (this.type == "capturing") - { - if (this.foeEntities.length && gameState.getEntityById(this.foeEntities[0])) - { - // Check if we still still some capturePoints to recover - // and if not, remove this foe from the list (capture army have only one foe) - const capture = gameState.getEntityById(this.foeEntities[0]).capturePoints(); - if (capture) - for (let j = 0; j < capture.length; ++j) - if (gameState.isPlayerEnemy(j) && capture[j] > 0) - return []; - this.removeFoe(gameState, this.foeEntities[0]); - } - return []; - } - - const 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); + // average + this.foePosition = [0, 0]; this.positionLastUpdate = gameState.ai.elapsedTime; - // Check for breakaways. - for (let i = 0; i < this.foeEntities.length; ++i) + // 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 (const id of foeEntities) + this.addFoe(gameState, id, true); + + this.recalculatePosition(gameState, true); + + return true; + } + + /** + * 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 + */ + addFoe(gameState, enemyId, force) + { + if (this.foeEntities.indexOf(enemyId) !== -1) + return false; + const ent = gameState.getEntityById(enemyId); + if (!ent || !ent.position()) + return false; + + // check distance + if (!force && 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. + */ + removeFoe(gameState, enemyId, enemyEntity) + { + const idx = this.foeEntities.indexOf(enemyId); + if (idx === -1) + return false; + + this.foeEntities.splice(idx, 1); + + this.assignedAgainst[enemyId] = undefined; + for (const to in this.assignedTo) + if (this.assignedTo[to] == enemyId) + this.assignedTo[to] = undefined; + + const 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 + */ + addOwn(gameState, id, force) + { + if (this.ownEntities.indexOf(id) !== -1) + return false; + const 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; + + const plan = ent.getMetadata(PlayerID, "plan"); + if (plan !== undefined) + ent.setMetadata(PlayerID, "plan", -2); + else + ent.setMetadata(PlayerID, "plan", -3); + const subrole = ent.getMetadata(PlayerID, "subrole"); + if (subrole === undefined || subrole !== Worker.SUBROLE_DEFENDER) + ent.setMetadata(PlayerID, "formerSubrole", subrole); + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_DEFENDER); + return true; + } + + removeOwn(gameState, id, Entity) + { + const idx = this.ownEntities.indexOf(id); + if (idx === -1) + return false; + + this.ownEntities.splice(idx, 1); + + if (this.assignedTo[id] !== 0) + { + const temp = this.assignedAgainst[this.assignedTo[id]]; + if (temp) + temp.splice(temp.indexOf(id), 1); + } + this.assignedTo[id] = undefined; + + const ent = Entity ? Entity : gameState.getEntityById(id); + if (!ent) + return true; + + 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); + + const formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); + if (formerSubrole !== undefined) + ent.setMetadata(PlayerID, "subrole", formerSubrole); + else + ent.setMetadata(PlayerID, "subrole", undefined); + ent.setMetadata(PlayerID, "formerSubrole", undefined); + + // Remove from transport plan if not yet on Board + if (ent.getMetadata(PlayerID, "transport") !== undefined) + { + const plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); + if (plan && plan.state === TransportPlan.BOARDING && ent.position()) + plan.removeUnit(gameState, ent); + } + + /* + // 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; + } + + /** + * resets the army properly. + * assumes we already cleared dead units. + */ + clear(gameState) + { + while (this.foeEntities.length > 0) + this.removeFoe(gameState, this.foeEntities[0]); + + // Go back to our or allied territory if needed + const posOwn = [0, 0]; + let nOwn = 0; + const posAlly = [0, 0]; + let nAlly = 0; + const posOther = [0, 0]; + let nOther = 0; + for (const entId of this.ownEntities) + { + const ent = gameState.getEntityById(entId); + if (!ent || !ent.position()) + continue; + const pos = ent.position(); + const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); + if (territoryOwner === PlayerID) + { + posOwn[0] += pos[0]; + posOwn[1] += pos[1]; + ++nOwn; + } + else if (gameState.isPlayerMutualAlly(territoryOwner)) + { + posAlly[0] += pos[0]; + posAlly[1] += pos[1]; + ++nAlly; + } + else + { + posOther[0] += pos[0]; + posOther[1] += pos[1]; + ++nOther; + } + } + let destination; + let defensiveFound; + let distmin; + let radius = 0; + if (nOwn > 0) + destination = [posOwn[0]/nOwn, posOwn[1]/nOwn]; + else if (nAlly > 0) + destination = [posAlly[0]/nAlly, posAlly[1]/nAlly]; + else + { + posOther[0] /= nOther; + posOther[1] /= nOther; + const armyAccess = gameState.ai.accessibility.getAccessValue(posOther); + for (const struct of gameState.getAllyStructures().values()) + { + const pos = struct.position(); + if (!pos || !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(pos))) + continue; + if (getLandAccess(gameState, struct) !== armyAccess) + continue; + const defensiveStruct = struct.hasDefensiveFire(); + if (defensiveFound && !defensiveStruct) + continue; + const dist = SquareVectorDistance(posOther, pos); + if (distmin && dist > distmin && (defensiveFound || !defensiveStruct)) + continue; + if (defensiveStruct) + defensiveFound = true; + distmin = dist; + destination = pos; + radius = struct.obstructionRadius().max; + } + } + while (this.ownEntities.length > 0) + { + const entId = this.ownEntities[0]; + this.removeOwn(gameState, entId); + const ent = gameState.getEntityById(entId); + if (ent) + { + if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined || + ent.getMetadata(PlayerID, "transporter") !== undefined) + continue; + if (ent.healthLevel() < this.Config.garrisonHealthLevel.low && + gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, ent)) + continue; + + if (destination && !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(ent.position()))) + ent.moveToRange(destination[0], destination[1], radius, radius + 5); + else + ent.stopMoving(); + } + } + + this.assignedAgainst = {}; + this.assignedTo = {}; + + this.recalculateStrengths(gameState); + this.recalculatePosition(gameState); + } + + assignUnit(gameState, entID) + { + // we'll assume this defender is ours already. + // we'll also override any previous assignment + + const ent = gameState.getEntityById(entID); + if (!ent || !ent.position()) + return false; + + // try to return its resources, and if any, the attack order will be queued + const queued = returnResources(gameState, ent); + + let idMin; + let distMin; + let idMinAll; + let distMinAll; + for (const id of this.foeEntities) + { + const eEnt = gameState.getEntityById(id); + if (!eEnt || !eEnt.position()) // probably can't happen. + continue; + + if (!ent.canAttackTarget(eEnt, allowCapture(gameState, ent, eEnt))) + continue; + + if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length && + eEnt.unitAIOrderData()[0].target && eEnt.unitAIOrderData()[0].target == entID) + { // being attacked >>> target the unit + idMin = id; + break; + } + + // already enough units against it + if (this.assignedAgainst[id].length > 8 || + this.assignedAgainst[id].length > 5 && !eEnt.hasClass("Hero") && !isSiegeUnit(eEnt)) + continue; + + const dist = SquareVectorDistance(ent.position(), eEnt.position()); + if (idMinAll === undefined || dist < distMinAll) + { + idMinAll = id; + distMinAll = dist; + } + if (this.assignedAgainst[id].length > 2) + continue; + if (idMin === undefined || dist < distMin) + { + idMin = id; + distMin = dist; + } + } + + let idFoe; + if (idMin !== undefined) + idFoe = idMin; + else if (idMinAll !== undefined) + idFoe = idMinAll; + else + return false; + + const ownIndex = getLandAccess(gameState, ent); + const foeEnt = gameState.getEntityById(idFoe); + const foePosition = foeEnt.position(); + const foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); + if (ownIndex == foeIndex || ent.hasClass("Ship")) + { + this.assignedTo[entID] = idFoe; + this.assignedAgainst[idFoe].push(entID); + ent.attack(idFoe, allowCapture(gameState, ent, foeEnt), queued); + } + else + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); + return true; + } + + getType() + { + return this.type; + } + + getState() + { + if (!this.foeEntities.length) + return 0; + return 1; + } + + /** + * 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). + */ + merge(gameState, otherArmy) + { + // copy over all parameters. + for (const 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 (const i in otherArmy.assignedTo) + this.assignedTo[i] = otherArmy.assignedTo[i]; + + for (const id of otherArmy.foeEntities) + this.addFoe(gameState, id, true); + // TODO: reassign those ? + for (const id of otherArmy.ownEntities) + this.addOwn(gameState, id, true); + + this.recalculatePosition(gameState, true); + this.recalculateStrengths(gameState); + + return true; + } + + needsDefenders(gameState) + { + let defenseRatio; + const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(this.foePosition); + if (territoryOwner == PlayerID) + defenseRatio = this.Config.Defense.defenseRatio.own; + else if (gameState.isPlayerAlly(territoryOwner)) + { + defenseRatio = this.Config.Defense.defenseRatio.ally; + let numExclusiveAllies = 0; + for (let p = 1; p < gameState.sharedScript.playersData.length; ++p) + if (p != territoryOwner && gameState.sharedScript.playersData[p].isAlly[territoryOwner]) + ++numExclusiveAllies; + defenseRatio /= 1 + 0.5*Math.max(0, numExclusiveAllies-1); + } + else + defenseRatio = this.Config.Defense.defenseRatio.neutral; + + // some preliminary checks because we don't update for tech so entStrength removed can be > entStrength added + if (this.foeStrength <= 0 || this.ownStrength <= 0) + this.recalculateStrengths(gameState); + + if (this.foeStrength * defenseRatio <= this.ownStrength) + return false; + return this.foeStrength * defenseRatio - this.ownStrength; + } + + + /** if not forced, will only recalculate if on a different turn. */ + recalculatePosition(gameState, force) + { + if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) + return; + + let npos = 0; + const pos = [0, 0]; + for (const id of this.foeEntities) { - const id = this.foeEntities[i]; const ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; - if (SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) + npos++; + const 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; + } + + recalculateStrengths(gameState) + { + this.ownStrength = 0; + this.foeStrength = 0; + + for (const id of this.foeEntities) + this.evaluateStrength(gameState.getEntityById(id)); + for (const 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. */ + evaluateStrength(ent, isOwn, remove) + { + if (!ent) + return; + + 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 = getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); + + // 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.hasClasses(["Animal+Elephant"])) + entStrength *= 3; + + if (remove) + entStrength *= -1; + + if (isOwn) + this.ownStrength += entStrength; + else + this.foeStrength += entStrength; + } + + checkEvents(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 (const evt of events.EntityRenamed) // take care of promoted and packed units + { + if (this.foeEntities.indexOf(evt.entity) !== -1) { - breakaways.push(id); - if (this.removeFoe(gameState, id)) - i--; + const ent = gameState.getEntityById(evt.newentity); + if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed + continue; + const 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 (const to in this.assignedTo) + if (this.assignedTo[to] === evt.entity) + this.assignedTo[to] = evt.newentity; + } + else if (this.ownEntities.indexOf(evt.entity) !== -1) + { + const newEnt = gameState.getEntityById(evt.newentity); + if (newEnt && (!newEnt.hasUnitAI() || !newEnt.attackTypes())) + continue; + const 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 (const 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; + } } } - this.recalculatePosition(gameState); + for (const evt of events.Garrison) + this.removeFoe(gameState, evt.entity); + + for (const 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 (const evt of events.Destroy) + { + // we may have capture+destroy, so do not trust owner and check all possibilities + this.removeOwn(gameState, evt.entity); + this.removeFoe(gameState, evt.entity); + } } - return breakaways; -}; + update(gameState) + { + for (const entId of this.ownEntities) + { + const ent = gameState.getEntityById(entId); + if (!ent) + continue; + const orderData = ent.unitAIOrderData(); + if (!orderData.length && !ent.getMetadata(PlayerID, "transport")) + this.assignUnit(gameState, entId); + else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") + { + const target = gameState.getEntityById(orderData[0].target); + if (target && !allowCapture(gameState, ent, target)) + ent.attack(orderData[0].target, false); + } + } -DefenseArmy.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "type": this.type, - "foePosition": this.foePosition, - "positionLastUpdate": this.positionLastUpdate, - "assignedAgainst": this.assignedAgainst, - "assignedTo": this.assignedTo, - "foeEntities": this.foeEntities, - "foeStrength": this.foeStrength, - "ownEntities": this.ownEntities, - "ownStrength": this.ownStrength - }; -}; + if (this.type == "capturing") + { + if (this.foeEntities.length && gameState.getEntityById(this.foeEntities[0])) + { + // Check if we still still some capturePoints to recover + // and if not, remove this foe from the list (capture army have only one foe) + const capture = gameState.getEntityById(this.foeEntities[0]).capturePoints(); + if (capture) + for (let j = 0; j < capture.length; ++j) + if (gameState.isPlayerEnemy(j) && capture[j] > 0) + return []; + this.removeFoe(gameState, this.foeEntities[0]); + } + return []; + } -DefenseArmy.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; -}; + const 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) + { + const id = this.foeEntities[i]; + const ent = gameState.getEntityById(id); + if (!ent || !ent.position()) + continue; + if (SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) + { + breakaways.push(id); + if (this.removeFoe(gameState, id)) + i--; + } + } + + this.recalculatePosition(gameState); + } + + return breakaways; + } + + Serialize() + { + return { + "ID": this.ID, + "type": this.type, + "foePosition": this.foePosition, + "positionLastUpdate": this.positionLastUpdate, + "assignedAgainst": this.assignedAgainst, + "assignedTo": this.assignedTo, + "foeEntities": this.foeEntities, + "foeStrength": this.foeStrength, + "ownEntities": this.ownEntities, + "ownStrength": this.ownStrength + }; + } + + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/defenseManager.js b/binaries/data/mods/public/simulation/ai/petra/defenseManager.js index 5d0e444a45..eee41f68c7 100644 --- a/binaries/data/mods/public/simulation/ai/petra/defenseManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/defenseManager.js @@ -7,982 +7,985 @@ import { allowCapture, getLandAccess, getMaxStrength, isSiegeUnit } from import { GarrisonManager } from "simulation/ai/petra/garrisonManager.js"; import { Worker } from "simulation/ai/petra/worker.js"; -export function DefenseManager(Config) +export class DefenseManager { - // Array of "army" Objects. - this.armies = []; - this.Config = Config; - this.targetList = []; - this.armyMergeSize = this.Config.Defense.armyMergeSize; - // Stats on how many enemies are currently attacking our allies - // this.attackingArmies[enemy][ally] = number of enemy armies inside allied territory - // this.attackingUnits[enemy][ally] = number of enemy units not in armies inside allied territory - // this.attackedAllies[ally] = number of enemies attacking the ally - this.attackingArmies = {}; - this.attackingUnits = {}; - this.attackedAllies = {}; -} - -DefenseManager.prototype.update = function(gameState, events) -{ - Engine.ProfileStart("Defense Manager"); - - this.territoryMap = gameState.ai.HQ.territoryMap; - - this.checkEvents(gameState, events); - - // Check if our potential targets are still valid. - for (let i = 0; i < this.targetList.length; ++i) + constructor(Config) { - const target = gameState.getEntityById(this.targetList[i]); - if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) - this.targetList.splice(i--, 1); + // Array of "army" Objects. + this.armies = []; + this.Config = Config; + this.targetList = []; + this.armyMergeSize = this.Config.Defense.armyMergeSize; + // Stats on how many enemies are currently attacking our allies + // this.attackingArmies[enemy][ally] = number of enemy armies inside allied territory + // this.attackingUnits[enemy][ally] = number of enemy units not in armies inside allied territory + // this.attackedAllies[ally] = number of enemies attacking the ally + this.attackingArmies = {}; + this.attackingUnits = {}; + this.attackedAllies = {}; } - // Count the number of enemies attacking our allies in the previous turn. - // We'll be more cooperative if several enemies are attacking him simultaneously. - this.attackedAllies = {}; - const attackingArmies = clone(this.attackingArmies); - for (const enemy in this.attackingUnits) + update(gameState, events) { - if (!this.attackingUnits[enemy]) - continue; - for (const ally in this.attackingUnits[enemy]) + Engine.ProfileStart("Defense Manager"); + + this.territoryMap = gameState.ai.HQ.territoryMap; + + this.checkEvents(gameState, events); + + // Check if our potential targets are still valid. + for (let i = 0; i < this.targetList.length; ++i) { - if (this.attackingUnits[enemy][ally] < 8) + const target = gameState.getEntityById(this.targetList[i]); + if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) + this.targetList.splice(i--, 1); + } + + // Count the number of enemies attacking our allies in the previous turn. + // We'll be more cooperative if several enemies are attacking him simultaneously. + this.attackedAllies = {}; + const attackingArmies = clone(this.attackingArmies); + for (const enemy in this.attackingUnits) + { + if (!this.attackingUnits[enemy]) continue; - if (attackingArmies[enemy] === undefined) - attackingArmies[enemy] = {}; - if (attackingArmies[enemy][ally] === undefined) - attackingArmies[enemy][ally] = 0; - attackingArmies[enemy][ally] += 1; - } - } - for (const enemy in attackingArmies) - { - for (const ally in attackingArmies[enemy]) - { - if (this.attackedAllies[ally] === undefined) - this.attackedAllies[ally] = 0; - this.attackedAllies[ally] += 1; - } - } - this.checkEnemyArmies(gameState); - this.checkEnemyUnits(gameState); - this.assignDefenders(gameState); - - Engine.ProfileStop(); -}; - -DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default") -{ - if (type == "default") - { - for (const army of this.armies) - if (army.getType() == type && army.addFoe(gameState, entityID)) - return; - } - - this.armies.push(new DefenseArmy(gameState, [entityID], type)); -}; - -DefenseManager.prototype.getArmy = function(partOfArmy) -{ - return this.armies.find(army => army.ID == partOfArmy); -}; - -DefenseManager.prototype.isDangerous = function(gameState, entity) -{ - if (!entity.position()) - return false; - - const territoryOwner = this.territoryMap.getOwner(entity.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) - return false; - // Check if the entity is trying to build a new base near our buildings, - // and if yes, add this base in our target list. - if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING") - { - const targetId = entity.unitAIOrderData()[0].target; - if (this.targetList.indexOf(targetId) != -1) - return true; - const target = gameState.getEntityById(targetId); - if (target) - { - const isTargetEnemy = gameState.isPlayerEnemy(target.owner()); - if (isTargetEnemy && territoryOwner == PlayerID) + for (const ally in this.attackingUnits[enemy]) { - if (target.hasClass("Structure")) - this.targetList.push(targetId); + if (this.attackingUnits[enemy][ally] < 8) + continue; + if (attackingArmies[enemy] === undefined) + attackingArmies[enemy] = {}; + if (attackingArmies[enemy][ally] === undefined) + attackingArmies[enemy][ally] = 0; + attackingArmies[enemy][ally] += 1; + } + } + for (const enemy in attackingArmies) + { + for (const ally in attackingArmies[enemy]) + { + if (this.attackedAllies[ally] === undefined) + this.attackedAllies[ally] = 0; + this.attackedAllies[ally] += 1; + } + } + this.checkEnemyArmies(gameState); + this.checkEnemyUnits(gameState); + this.assignDefenders(gameState); + + Engine.ProfileStop(); + } + + makeIntoArmy(gameState, entityID, type = "default") + { + if (type == "default") + { + for (const army of this.armies) + if (army.getType() == type && army.addFoe(gameState, entityID)) + return; + } + + this.armies.push(new DefenseArmy(gameState, [entityID], type)); + } + + getArmy(partOfArmy) + { + return this.armies.find(army => army.ID == partOfArmy); + } + + isDangerous(gameState, entity) + { + if (!entity.position()) + return false; + + const territoryOwner = this.territoryMap.getOwner(entity.position()); + if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) + return false; + // Check if the entity is trying to build a new base near our buildings, + // and if yes, add this base in our target list. + if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING") + { + const targetId = entity.unitAIOrderData()[0].target; + if (this.targetList.indexOf(targetId) != -1) return true; - } - else if (isTargetEnemy && target.hasClass("CivCentre")) + const target = gameState.getEntityById(targetId); + if (target) { - const myBuildings = gameState.getOwnStructures(); - for (const building of myBuildings.values()) + const isTargetEnemy = gameState.isPlayerEnemy(target.owner()); + if (isTargetEnemy && territoryOwner == PlayerID) { - if (building.foundationProgress() == 0) - continue; - if (SquareVectorDistance(building.position(), entity.position()) > 30000) - continue; - this.targetList.push(targetId); + if (target.hasClass("Structure")) + this.targetList.push(targetId); return true; } + else if (isTargetEnemy && target.hasClass("CivCentre")) + { + const myBuildings = gameState.getOwnStructures(); + for (const building of myBuildings.values()) + { + if (building.foundationProgress() == 0) + continue; + if (SquareVectorDistance(building.position(), entity.position()) > 30000) + continue; + this.targetList.push(targetId); + return true; + } + } } } - } - if (entity.attackTypes() === undefined || entity.hasClass("Support")) + if (entity.attackTypes() === undefined || entity.hasClass("Support")) + return false; + let dist2Min = 6000; + // TODO the 30 is to take roughly into account the structure size in following checks. Can be improved. + if (entity.attackTypes().indexOf("Ranged") != -1) + dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30); + + for (const targetId of this.targetList) + { + const target = gameState.getEntityById(targetId); + // The enemy base is either destroyed or built. + if (!target || !target.position()) + continue; + if (SquareVectorDistance(target.position(), entity.position()) < dist2Min) + return true; + } + + const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); + for (const cc of ccEnts.values()) + { + if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) + continue; + const cooperation = this.GetCooperationLevel(cc.owner()); + if (cooperation < 0.3 || cooperation < 0.6 && !!cc.foundationProgress()) + continue; + if (SquareVectorDistance(cc.position(), entity.position()) < dist2Min) + return true; + } + + for (const building of gameState.getOwnStructures().values()) + { + if (building.foundationProgress() == 0 || + SquareVectorDistance(building.position(), entity.position()) > dist2Min) + { + continue; + } + if (!this.territoryMap.isBlinking(building.position()) || gameState.ai.HQ.isDefendable(building)) + return true; + } + + if (gameState.isPlayerMutualAlly(territoryOwner)) + { + // If ally attacked by more than 2 enemies, help him not only for cc but also for structures. + if (territoryOwner != PlayerID && this.attackedAllies[territoryOwner] && + this.attackedAllies[territoryOwner] > 1 && + this.GetCooperationLevel(territoryOwner) > 0.7) + { + for (const building of gameState.getAllyStructures(territoryOwner).values()) + { + if (building.foundationProgress() == 0 || + SquareVectorDistance(building.position(), entity.position()) > dist2Min) + { + continue; + } + if (!this.territoryMap.isBlinking(building.position())) + return true; + } + } + + // Update the number of enemies attacking this ally. + const enemy = entity.owner(); + if (this.attackingUnits[enemy] === undefined) + this.attackingUnits[enemy] = {}; + if (this.attackingUnits[enemy][territoryOwner] === undefined) + this.attackingUnits[enemy][territoryOwner] = 0; + this.attackingUnits[enemy][territoryOwner] += 1; + } + return false; - let dist2Min = 6000; - // TODO the 30 is to take roughly into account the structure size in following checks. Can be improved. - if (entity.attackTypes().indexOf("Ranged") != -1) - dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30); - - for (const targetId of this.targetList) - { - const target = gameState.getEntityById(targetId); - // The enemy base is either destroyed or built. - if (!target || !target.position()) - continue; - if (SquareVectorDistance(target.position(), entity.position()) < dist2Min) - return true; } - const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); - for (const cc of ccEnts.values()) + checkEnemyUnits(gameState) { - if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) - continue; - const cooperation = this.GetCooperationLevel(cc.owner()); - if (cooperation < 0.3 || cooperation < 0.6 && !!cc.foundationProgress()) - continue; - if (SquareVectorDistance(cc.position(), entity.position()) < dist2Min) - return true; - } + const nbPlayers = gameState.sharedScript.playersData.length; + const i = gameState.ai.playedTurn % nbPlayers; + this.attackingUnits[i] = undefined; - for (const building of gameState.getOwnStructures().values()) - { - if (building.foundationProgress() == 0 || - SquareVectorDistance(building.position(), entity.position()) > dist2Min) + if (i == PlayerID) { - continue; - } - if (!this.territoryMap.isBlinking(building.position()) || gameState.ai.HQ.isDefendable(building)) - return true; - } - - if (gameState.isPlayerMutualAlly(territoryOwner)) - { - // If ally attacked by more than 2 enemies, help him not only for cc but also for structures. - if (territoryOwner != PlayerID && this.attackedAllies[territoryOwner] && - this.attackedAllies[territoryOwner] > 1 && - this.GetCooperationLevel(territoryOwner) > 0.7) - { - for (const building of gameState.getAllyStructures(territoryOwner).values()) + if (!this.armies.length) { - if (building.foundationProgress() == 0 || - SquareVectorDistance(building.position(), entity.position()) > dist2Min) + // Check if we can recover capture points from any of our notdecaying structures. + for (const ent of gameState.getOwnStructures().values()) + { + if (ent.decaying()) + continue; + const capture = ent.capturePoints(); + if (capture === undefined) + continue; + let lost = 0; + for (let j = 0; j < capture.length; ++j) + if (gameState.isPlayerEnemy(j)) + lost += capture[j]; + if (lost < Math.ceil(0.25 * capture[i])) + continue; + this.makeIntoArmy(gameState, ent.id(), "capturing"); + break; + } + } + return; + } + else if (!gameState.isPlayerEnemy(i)) + return; + + for (const ent of gameState.getEnemyUnits(i).values()) + { + if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) + continue; + + // Keep animals attacking us or our allies. + if (ent.hasClass("Animal")) + { + if (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT") + continue; + const orders = ent.unitAIOrderData(); + if (!orders || !orders.length || !orders[0].target) + continue; + const target = gameState.getEntityById(orders[0].target); + if (!target || !gameState.isPlayerAlly(target.owner())) + continue; + } + + // TODO what to do for ships ? + if (ent.hasClasses(["Ship", "Trader"])) + continue; + + // Check if unit is dangerous "a priori". + if (this.isDangerous(gameState, ent)) + this.makeIntoArmy(gameState, ent.id()); + } + + if (i != 0 || this.armies.length > 1 || !gameState.ai.HQ.hasActiveBase()) + return; + // Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay) + // and attack it only if useful (and capturable) or dangereous. + for (const ent of gameState.getEnemyStructures(i).values()) + { + if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) + continue; + if (!ent.capturePoints() && !ent.hasDefensiveFire()) + continue; + const owner = this.territoryMap.getOwner(ent.position()); + if (owner == PlayerID) + this.makeIntoArmy(gameState, ent.id(), "capturing"); + } + } + + checkEnemyArmies(gameState) + { + for (let i = 0; i < this.armies.length; ++i) + { + const army = this.armies[i]; + // This returns a list of IDs: the units that broke away from the army for being too far. + const breakaways = army.update(gameState); + // Assume dangerosity. + for (const breaker of breakaways) + this.makeIntoArmy(gameState, breaker); + + if (army.getState() == 0) + { + if (army.getType() == "default") + this.switchToAttack(gameState, army); + army.clear(gameState); + this.armies.splice(i--, 1); + } + } + // Check if we can't merge it with another. + for (let i = 0; i < this.armies.length - 1; ++i) + { + const army = this.armies[i]; + if (army.getType() != "default") + continue; + for (let j = i+1; j < this.armies.length; ++j) + { + const otherArmy = this.armies[j]; + if (otherArmy.getType() != "default" || + SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize) { continue; } - if (!this.territoryMap.isBlinking(building.position())) - return true; + // No need to clear here. + army.merge(gameState, otherArmy); + this.armies.splice(j--, 1); } } - // Update the number of enemies attacking this ally. - const enemy = entity.owner(); - if (this.attackingUnits[enemy] === undefined) - this.attackingUnits[enemy] = {}; - if (this.attackingUnits[enemy][territoryOwner] === undefined) - this.attackingUnits[enemy][territoryOwner] = 0; - this.attackingUnits[enemy][territoryOwner] += 1; - } - - return false; -}; - -DefenseManager.prototype.checkEnemyUnits = function(gameState) -{ - const nbPlayers = gameState.sharedScript.playersData.length; - const i = gameState.ai.playedTurn % nbPlayers; - this.attackingUnits[i] = undefined; - - if (i == PlayerID) - { - if (!this.armies.length) + if (gameState.ai.playedTurn % 5 != 0) + return; + // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base). + this.attackingArmies = {}; + for (let i = 0; i < this.armies.length; ++i) { - // Check if we can recover capture points from any of our notdecaying structures. - for (const ent of gameState.getOwnStructures().values()) + const army = this.armies[i]; + army.recalculatePosition(gameState); + const owner = this.territoryMap.getOwner(army.foePosition); + if (!gameState.isPlayerEnemy(owner)) { - if (ent.decaying()) + if (gameState.isPlayerMutualAlly(owner)) + { + // Update the number of enemies attacking this ally. + for (const id of army.foeEntities) + { + const ent = gameState.getEntityById(id); + if (!ent) + continue; + const enemy = ent.owner(); + if (this.attackingArmies[enemy] === undefined) + this.attackingArmies[enemy] = {}; + if (this.attackingArmies[enemy][owner] === undefined) + this.attackingArmies[enemy][owner] = 0; + this.attackingArmies[enemy][owner] += 1; + break; + } + } + continue; + } + // Enemy army back in its territory. + else if (owner != 0) + { + army.clear(gameState); + this.armies.splice(i--, 1); + continue; + } + + // Army in neutral territory. + // TODO check smaller distance with all our buildings instead of only ccs with big distance. + let stillDangerous = false; + const bases = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); + for (const base of bases.values()) + { + if (!gameState.isEntityAlly(base)) continue; - const capture = ent.capturePoints(); - if (capture === undefined) + const cooperation = this.GetCooperationLevel(base.owner()); + if (cooperation < 0.3 && !gameState.isEntityOwn(base)) continue; - let lost = 0; - for (let j = 0; j < capture.length; ++j) - if (gameState.isPlayerEnemy(j)) - lost += capture[j]; - if (lost < Math.ceil(0.25 * capture[i])) + if (SquareVectorDistance(base.position(), army.foePosition) > 40000) continue; - this.makeIntoArmy(gameState, ent.id(), "capturing"); + if (this.Config.debug > 1) + aiWarn("army in neutral territory, but still near one of our CC"); + stillDangerous = true; break; } - } - return; - } - else if (!gameState.isPlayerEnemy(i)) - return; - - for (const ent of gameState.getEnemyUnits(i).values()) - { - if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) - continue; - - // Keep animals attacking us or our allies. - if (ent.hasClass("Animal")) - { - if (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT") + if (stillDangerous) continue; - const orders = ent.unitAIOrderData(); - if (!orders || !orders.length || !orders[0].target) + // Need to also check docks because of oversea bases. + for (const dock of gameState.getOwnStructures().filter(filters.byClass("Dock")).values()) + { + if (SquareVectorDistance(dock.position(), army.foePosition) > 10000) + continue; + stillDangerous = true; + break; + } + if (stillDangerous) continue; - const target = gameState.getEntityById(orders[0].target); - if (!target || !gameState.isPlayerAlly(target.owner())) - continue; - } - // TODO what to do for ships ? - if (ent.hasClasses(["Ship", "Trader"])) - continue; - - // Check if unit is dangerous "a priori". - if (this.isDangerous(gameState, ent)) - this.makeIntoArmy(gameState, ent.id()); - } - - if (i != 0 || this.armies.length > 1 || !gameState.ai.HQ.hasActiveBase()) - return; - // Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay) - // and attack it only if useful (and capturable) or dangereous. - for (const ent of gameState.getEnemyStructures(i).values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) - continue; - if (!ent.capturePoints() && !ent.hasDefensiveFire()) - continue; - const owner = this.territoryMap.getOwner(ent.position()); - if (owner == PlayerID) - this.makeIntoArmy(gameState, ent.id(), "capturing"); - } -}; - -DefenseManager.prototype.checkEnemyArmies = function(gameState) -{ - for (let i = 0; i < this.armies.length; ++i) - { - const army = this.armies[i]; - // This returns a list of IDs: the units that broke away from the army for being too far. - const breakaways = army.update(gameState); - // Assume dangerosity. - for (const breaker of breakaways) - this.makeIntoArmy(gameState, breaker); - - if (army.getState() == 0) - { if (army.getType() == "default") this.switchToAttack(gameState, army); army.clear(gameState); this.armies.splice(i--, 1); } } - // Check if we can't merge it with another. - for (let i = 0; i < this.armies.length - 1; ++i) - { - const army = this.armies[i]; - if (army.getType() != "default") - continue; - for (let j = i+1; j < this.armies.length; ++j) - { - const otherArmy = this.armies[j]; - if (otherArmy.getType() != "default" || - SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize) - { - continue; - } - // No need to clear here. - army.merge(gameState, otherArmy); - this.armies.splice(j--, 1); - } - } - if (gameState.ai.playedTurn % 5 != 0) - return; - // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base). - this.attackingArmies = {}; - for (let i = 0; i < this.armies.length; ++i) + assignDefenders(gameState) { - const army = this.armies[i]; - army.recalculatePosition(gameState); - const owner = this.territoryMap.getOwner(army.foePosition); - if (!gameState.isPlayerEnemy(owner)) + if (!this.armies.length) + return; + + const armiesNeeding = []; + // Let's add defenders. + for (const army of this.armies) { - if (gameState.isPlayerMutualAlly(owner)) + const needsDef = army.needsDefenders(gameState); + if (needsDef === false) + continue; + + let armyAccess; + for (const entId of army.foeEntities) { - // Update the number of enemies attacking this ally. - for (const id of army.foeEntities) + const ent = gameState.getEntityById(entId); + if (!ent || !ent.position()) + continue; + armyAccess = getLandAccess(gameState, ent); + break; + } + if (!armyAccess) + aiWarn(" Petra error: attacking army " + army.ID + " without access"); + army.recalculatePosition(gameState); + armiesNeeding.push({ "army": army, "access": armyAccess, "need": needsDef }); + } + + if (!armiesNeeding.length) + return; + + // Let's get our potential units. + const potentialDefenders = []; + gameState.getOwnUnits().forEach(function(ent) + { + if (!ent.position()) + return; + if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) + return; + if (ent.hasClass("Support") || ent.attackTypes() === undefined) + return; + if (ent.hasClasses(["StoneThrower", "Support", "FishingBoat"])) + return; + if (ent.getMetadata(PlayerID, "transport") !== undefined || + ent.getMetadata(PlayerID, "transporter") !== undefined) + return; + if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id())) + return; + if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) + { + const subrole = ent.getMetadata(PlayerID, "subrole"); + if (subrole && + (subrole === Worker.SUBROLE_COMPLETING || subrole === Worker.SUBROLE_WALKING || + subrole === Worker.SUBROLE_ATTACKING)) { - const ent = gameState.getEntityById(id); - if (!ent) + return; + } + } + potentialDefenders.push(ent.id()); + }); + + for (let ipass = 0; ipass < 2; ++ipass) + { + // First pass only assign defenders with the right access. + // Second pass assign all defenders. + // TODO could sort them by distance. + let backup = 0; + for (let i = 0; i < potentialDefenders.length; ++i) + { + const ent = gameState.getEntityById(potentialDefenders[i]); + if (!ent || !ent.position()) + continue; + let aMin; + let distMin; + const access = ipass == 0 ? getLandAccess(gameState, ent) : undefined; + for (let a = 0; a < armiesNeeding.length; ++a) + { + if (access && armiesNeeding[a].access != access) continue; - const enemy = ent.owner(); - if (this.attackingArmies[enemy] === undefined) - this.attackingArmies[enemy] = {}; - if (this.attackingArmies[enemy][owner] === undefined) - this.attackingArmies[enemy][owner] = 0; - this.attackingArmies[enemy][owner] += 1; - break; - } - } - continue; - } - // Enemy army back in its territory. - else if (owner != 0) - { - army.clear(gameState); - this.armies.splice(i--, 1); - continue; - } - // Army in neutral territory. - // TODO check smaller distance with all our buildings instead of only ccs with big distance. - let stillDangerous = false; - const bases = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); - for (const base of bases.values()) - { - if (!gameState.isEntityAlly(base)) - continue; - const cooperation = this.GetCooperationLevel(base.owner()); - if (cooperation < 0.3 && !gameState.isEntityOwn(base)) - continue; - if (SquareVectorDistance(base.position(), army.foePosition) > 40000) - continue; - if (this.Config.debug > 1) - aiWarn("army in neutral territory, but still near one of our CC"); - stillDangerous = true; - break; - } - if (stillDangerous) - continue; - // Need to also check docks because of oversea bases. - for (const dock of gameState.getOwnStructures().filter(filters.byClass("Dock")).values()) - { - if (SquareVectorDistance(dock.position(), army.foePosition) > 10000) - continue; - stillDangerous = true; - break; - } - if (stillDangerous) - continue; - - if (army.getType() == "default") - this.switchToAttack(gameState, army); - army.clear(gameState); - this.armies.splice(i--, 1); - } -}; - -DefenseManager.prototype.assignDefenders = function(gameState) -{ - if (!this.armies.length) - return; - - const armiesNeeding = []; - // Let's add defenders. - for (const army of this.armies) - { - const needsDef = army.needsDefenders(gameState); - if (needsDef === false) - continue; - - let armyAccess; - for (const entId of army.foeEntities) - { - const ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - armyAccess = getLandAccess(gameState, ent); - break; - } - if (!armyAccess) - aiWarn(" Petra error: attacking army " + army.ID + " without access"); - army.recalculatePosition(gameState); - armiesNeeding.push({ "army": army, "access": armyAccess, "need": needsDef }); - } - - if (!armiesNeeding.length) - return; - - // Let's get our potential units. - const potentialDefenders = []; - gameState.getOwnUnits().forEach(function(ent) - { - if (!ent.position()) - return; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return; - if (ent.hasClass("Support") || ent.attackTypes() === undefined) - return; - if (ent.hasClasses(["StoneThrower", "Support", "FishingBoat"])) - return; - if (ent.getMetadata(PlayerID, "transport") !== undefined || - ent.getMetadata(PlayerID, "transporter") !== undefined) - return; - if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id())) - return; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) - { - const subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && - (subrole === Worker.SUBROLE_COMPLETING || subrole === Worker.SUBROLE_WALKING || - subrole === Worker.SUBROLE_ATTACKING)) - { - return; - } - } - potentialDefenders.push(ent.id()); - }); - - for (let ipass = 0; ipass < 2; ++ipass) - { - // First pass only assign defenders with the right access. - // Second pass assign all defenders. - // TODO could sort them by distance. - let backup = 0; - for (let i = 0; i < potentialDefenders.length; ++i) - { - const ent = gameState.getEntityById(potentialDefenders[i]); - if (!ent || !ent.position()) - continue; - let aMin; - let distMin; - const access = ipass == 0 ? getLandAccess(gameState, ent) : undefined; - for (let a = 0; a < armiesNeeding.length; ++a) - { - if (access && armiesNeeding[a].access != access) - continue; - - // Do not assign defender if it cannot attack at least part of the attacking army. - if (!armiesNeeding[a].army.foeEntities.some(eEnt => - { - const eEntID = gameState.getEntityById(eEnt); - return ent.canAttackTarget(eEntID, allowCapture(gameState, ent, eEntID)); - })) - continue; - - const dist = SquareVectorDistance(ent.position(), - armiesNeeding[a].army.foePosition); - if (aMin !== undefined && dist > distMin) - continue; - aMin = a; - distMin = dist; - } - - // If outside our territory (helping an ally or attacking a cc foundation) - // or if in another access, keep some troops in backup. - if (backup < 12 && (aMin == undefined || distMin > 40000 && - this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) != PlayerID)) - { - ++backup; - potentialDefenders[i] = undefined; - continue; - } - else if (aMin === undefined) - continue; - - armiesNeeding[aMin].need -= - getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); - armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]); - armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]); - potentialDefenders[i] = undefined; - - if (armiesNeeding[aMin].need <= 0) - armiesNeeding.splice(aMin, 1); - if (!armiesNeeding.length) - return; - } - } - - // If shortage of defenders, produce infantry garrisoned in nearest civil center. - const armiesPos = []; - for (let a = 0; a < armiesNeeding.length; ++a) - armiesPos.push(armiesNeeding[a].army.foePosition); - gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); -}; - -DefenseManager.prototype.abortArmy = function(gameState, army) -{ - army.clear(gameState); - for (let i = 0; i < this.armies.length; ++i) - { - if (this.armies[i].ID != army.ID) - continue; - this.armies.splice(i, 1); - break; - } -}; - -/** - * If our defense structures are attacked, garrison soldiers inside when possible - * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure - * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress. - * If our hero is attacked with regicide victory condition, the victoryManager will handle it. - */ -DefenseManager.prototype.checkEvents = function(gameState, events) -{ - // Must be called every turn for all armies. - for (const army of this.armies) - army.checkEvents(gameState, events); - - // Capture events. - for (const evt of events.OwnershipChanged) - { - if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) - { - const ent = gameState.getEntityById(evt.entity); - // One of our cc has been captured. - if (ent && ent.hasClass("CivCentre")) - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); - } - } - - const allAttacked = {}; - for (const evt of events.Attacked) - allAttacked[evt.target] = evt.attacker; - - for (const evt of events.Attacked) - { - const target = gameState.getEntityById(evt.target); - if (!target || !target.position()) - continue; - - const attacker = gameState.getEntityById(evt.attacker); - if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship") && - (!target.hasClass("Structure") || target.attackRange("Ranged"))) - { - // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier - // (enemy non-defensive structure are not considered to stay in sync with garrisonManager). - if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier() && - (target.owner() != 0 || !target.hasClass("Unit") || - target.unitAIState() && target.unitAIState().split(".")[1] == "COMBAT")) - this.garrisonUnitsInside(gameState, attacker, { "attacker": target }); - } - - if (!gameState.isEntityOwn(target)) - continue; - - // If attacked by one of our allies (he must trying to recover capture points), do not react. - if (attacker && gameState.isEntityAlly(attacker)) - continue; - - if (attacker && attacker.position() && target.hasClass("FishingBoat")) - { - const unitAIState = target.unitAIState(); - const unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (target.isIdle() || unitAIStateOrder == "GATHER") - { - const pos = attacker.position(); - const range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; - if (range * range > SquareVectorDistance(pos, target.position())) - target.moveToRange(pos[0], pos[1], range, range + 5); - } - continue; - } - - // TODO integrate other ships later, need to be sure it is accessible. - if (target.hasClass("Ship")) - continue; - - // If a building on a blinking tile is attacked, check if it can be defended. - // Same thing for a building in an isolated base (not connected to a base with anchor). - if (target.hasClass("Structure")) - { - const base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base")); - if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) || - !base || gameState.ai.HQ.baseManagers().every(b => !b.anchor || b.accessIndex != base.accessIndex)) - { - const capture = target.capturePoints(); - if (!capture) - continue; - const captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); - if (captureRatio > 0.50 && captureRatio < 0.70) - target.destroy(); - continue; - } - } - - - // If inside a started attack plan, let the plan deal with this unit. - const plan = target.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - const attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && attack.state != AttackPlan.STATE_UNEXECUTED) - continue; - } - - // Signal this attacker to our defense manager, except if we are in enemy territory. - // TODO treat ship attack. - if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined && - !attacker.hasClasses(["Structure", "Ship"])) - { - const territoryOwner = this.territoryMap.getOwner(attacker.position()); - if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) - this.makeIntoArmy(gameState, attacker.id()); - } - - if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) - { - const army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); - if (army.getType() == "capturing") - { - let abort = false; - // If one of the units trying to capture a structure is attacked, - // abort the army so that the unit can defend itself - if (army.ownEntities.indexOf(target.id()) != -1) - abort = true; - else if (army.foeEntities[0] == target.id() && target.owner() == PlayerID) - { - // else we may be trying to regain some capture point from one of our structure. - abort = true; - const capture = target.capturePoints(); - for (let j = 0; j < capture.length; ++j) + // Do not assign defender if it cannot attack at least part of the attacking army. + if (!armiesNeeding[a].army.foeEntities.some(eEnt => { - if (!gameState.isPlayerEnemy(j) || capture[j] == 0) - continue; - abort = false; - break; - } + const eEntID = gameState.getEntityById(eEnt); + return ent.canAttackTarget(eEntID, allowCapture(gameState, ent, eEntID)); + })) + continue; + + const dist = SquareVectorDistance(ent.position(), + armiesNeeding[a].army.foePosition); + if (aMin !== undefined && dist > distMin) + continue; + aMin = a; + distMin = dist; } - if (abort) - this.abortArmy(gameState, army); + + // If outside our territory (helping an ally or attacking a cc foundation) + // or if in another access, keep some troops in backup. + if (backup < 12 && (aMin == undefined || distMin > 40000 && + this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) != PlayerID)) + { + ++backup; + potentialDefenders[i] = undefined; + continue; + } + else if (aMin === undefined) + continue; + + armiesNeeding[aMin].need -= + getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); + armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]); + armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]); + potentialDefenders[i] = undefined; + + if (armiesNeeding[aMin].need <= 0) + armiesNeeding.splice(aMin, 1); + if (!armiesNeeding.length) + return; } - continue; } - // Try to garrison any attacked support unit if low health. - if (target.hasClass("Support") && target.healthLevel() < this.Config.garrisonHealthLevel.medium && - !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) + // If shortage of defenders, produce infantry garrisoned in nearest civil center. + const armiesPos = []; + for (let a = 0; a < armiesNeeding.length; ++a) + armiesPos.push(armiesNeeding[a].army.foePosition); + gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); + } + + abortArmy(gameState, army) + { + army.clear(gameState); + for (let i = 0; i < this.armies.length; ++i) { - this.garrisonAttackedUnit(gameState, target); - continue; - } - - // Try to garrison any attacked stone thrower. - if (target.hasClass("StoneThrower") && - !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) - { - this.garrisonSiegeUnit(gameState, target); - continue; - } - - if (!attacker || !attacker.position()) - continue; - - if (target.isGarrisonHolder() && target.getArrowMultiplier()) - this.garrisonUnitsInside(gameState, target, { "attacker": attacker }); - - if (target.hasClass("Unit") && attacker.hasClass("Unit")) - { - // Consider whether we should retaliate or continue our task. - if (target.hasClass("Support") || target.attackTypes() === undefined) + if (this.armies[i].ID != army.ID) continue; - const orderData = target.unitAIOrderData(); - const currentTarget = orderData && orderData.length && orderData[0].target ? - gameState.getEntityById(orderData[0].target) : undefined; - if (currentTarget) + this.armies.splice(i, 1); + break; + } + } + + /** + * If our defense structures are attacked, garrison soldiers inside when possible + * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure + * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress. + * If our hero is attacked with regicide victory condition, the victoryManager will handle it. + */ + checkEvents(gameState, events) + { + // Must be called every turn for all armies. + for (const army of this.armies) + army.checkEvents(gameState, events); + + // Capture events. + for (const evt of events.OwnershipChanged) + { + if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) + { + const ent = gameState.getEntityById(evt.entity); + // One of our cc has been captured. + if (ent && ent.hasClass("CivCentre")) + gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); + } + } + + const allAttacked = {}; + for (const evt of events.Attacked) + allAttacked[evt.target] = evt.attacker; + + for (const evt of events.Attacked) + { + const target = gameState.getEntityById(evt.target); + if (!target || !target.position()) + continue; + + const attacker = gameState.getEntityById(evt.attacker); + if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship") && + (!target.hasClass("Structure") || target.attackRange("Ranged"))) + { + // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier + // (enemy non-defensive structure are not considered to stay in sync with garrisonManager). + if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier() && + (target.owner() != 0 || !target.hasClass("Unit") || + target.unitAIState() && target.unitAIState().split(".")[1] == "COMBAT")) + this.garrisonUnitsInside(gameState, attacker, { "attacker": target }); + } + + if (!gameState.isEntityOwn(target)) + continue; + + // If attacked by one of our allies (he must trying to recover capture points), do not react. + if (attacker && gameState.isEntityAlly(attacker)) + continue; + + if (attacker && attacker.position() && target.hasClass("FishingBoat")) { const unitAIState = target.unitAIState(); const unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (unitAIStateOrder === "COMBAT" && (currentTarget.id() === attacker.id() || - !currentTarget.hasClasses(["Structure", "Support"]))) - continue; - if (unitAIStateOrder === "REPAIR" && currentTarget.hasDefensiveFire()) - continue; - if (unitAIStateOrder === "COMBAT" && !isSiegeUnit(currentTarget) && - gameState.ai.HQ.capturableTargets.has(orderData[0].target)) + if (target.isIdle() || unitAIStateOrder == "GATHER") { - // Take the nearest unit also attacking this structure to help us. - const capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); - let minDist; - let minEnt; const pos = attacker.position(); - capturableTarget.ents.delete(target.id()); - for (const entId of capturableTarget.ents) - { - if (allAttacked[entId]) - continue; - const ent = gameState.getEntityById(entId); - if (!ent || !ent.position() || !ent.canAttackTarget(attacker, - allowCapture(gameState, ent, attacker))) - { - continue; - } - // Check that the unit is still attacking the structure (since the last played turn). - const state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - const entOrderData = ent.unitAIOrderData(); - if (!entOrderData || !entOrderData.length || !entOrderData[0].target || - entOrderData[0].target != orderData[0].target) - continue; - const dist = SquareVectorDistance(pos, ent.position()); - if (minEnt && dist > minDist) - continue; - minDist = dist; - minEnt = ent; - } - if (minEnt) - { - capturableTarget.ents.delete(minEnt.id()); - minEnt.attack(attacker.id(), allowCapture(gameState, minEnt, attacker)); - } + const range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; + if (range * range > SquareVectorDistance(pos, target.position())) + target.moveToRange(pos[0], pos[1], range, range + 5); + } + continue; + } + + // TODO integrate other ships later, need to be sure it is accessible. + if (target.hasClass("Ship")) + continue; + + // If a building on a blinking tile is attacked, check if it can be defended. + // Same thing for a building in an isolated base (not connected to a base with anchor). + if (target.hasClass("Structure")) + { + const base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base")); + if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) || + !base || gameState.ai.HQ.baseManagers().every(b => !b.anchor || b.accessIndex != base.accessIndex)) + { + const capture = target.capturePoints(); + if (!capture) + continue; + const captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); + if (captureRatio > 0.50 && captureRatio < 0.70) + target.destroy(); + continue; } } - const shouldCapture = allowCapture(gameState, target, attacker); - if (target.canAttackTarget(attacker, shouldCapture)) - target.attack(attacker.id(), shouldCapture); - } - } -}; -DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) -{ - if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) - return false; - const minGarrison = data.min || target.garrisonMax(); - if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) - return false; - if (data.attacker) - { - const attackTypes = target.attackTypes(); - if (!attackTypes || attackTypes.indexOf("Ranged") == -1) - return false; - const dist = SquareVectorDistance(data.attacker.position(), target.position()); - const range = target.attackRange("Ranged").max; - if (dist >= range*range) - return false; - } - const access = getLandAccess(gameState, target); - const garrisonManager = gameState.ai.HQ.garrisonManager; - const garrisonArrowClasses = target.getGarrisonArrowClasses(); - const typeGarrison = data.type || GarrisonManager.TYPE_PROTECTION; - let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target); - if (allowMelee === undefined) - { - // Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units. - if (data.attacker) - { - allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : - !isSiegeUnit(data.attacker); - } - else - allowMelee = true; - } - const units = gameState.getOwnUnits().filter(ent => - { - if (!ent.position()) - return false; - if (!ent.hasClasses(garrisonArrowClasses)) - return false; - if (typeGarrison !== GarrisonManager.TYPE_DECAY && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) - return false; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return false; - const army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; - if (!army && (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)) - return false; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) - { - const subrole = ent.getMetadata(PlayerID, "subrole"); - // When structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan. - if (typeGarrison !== GarrisonManager.TYPE_DECAY && subrole && - (subrole === Worker.SUBROLE_COMPLETING || subrole === Worker.SUBROLE_WALKING || - subrole === Worker.SUBROLE_ATTACKING)) + + // If inside a started attack plan, let the plan deal with this unit. + const plan = target.getMetadata(PlayerID, "plan"); + if (plan !== undefined && plan >= 0) { - return false; + const attack = gameState.ai.HQ.attackManager.getPlan(plan); + if (attack && attack.state != AttackPlan.STATE_UNEXECUTED) + continue; + } + + // Signal this attacker to our defense manager, except if we are in enemy territory. + // TODO treat ship attack. + if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined && + !attacker.hasClasses(["Structure", "Ship"])) + { + const territoryOwner = this.territoryMap.getOwner(attacker.position()); + if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) + this.makeIntoArmy(gameState, attacker.id()); + } + + if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) + { + const army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); + if (army.getType() == "capturing") + { + let abort = false; + // If one of the units trying to capture a structure is attacked, + // abort the army so that the unit can defend itself + if (army.ownEntities.indexOf(target.id()) != -1) + abort = true; + else if (army.foeEntities[0] == target.id() && target.owner() == PlayerID) + { + // else we may be trying to regain some capture point from one of our structure. + abort = true; + const capture = target.capturePoints(); + for (let j = 0; j < capture.length; ++j) + { + if (!gameState.isPlayerEnemy(j) || capture[j] == 0) + continue; + abort = false; + break; + } + } + if (abort) + this.abortArmy(gameState, army); + } + continue; + } + + // Try to garrison any attacked support unit if low health. + if (target.hasClass("Support") && target.healthLevel() < this.Config.garrisonHealthLevel.medium && + !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) + { + this.garrisonAttackedUnit(gameState, target); + continue; + } + + // Try to garrison any attacked stone thrower. + if (target.hasClass("StoneThrower") && + !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) + { + this.garrisonSiegeUnit(gameState, target); + continue; + } + + if (!attacker || !attacker.position()) + continue; + + if (target.isGarrisonHolder() && target.getArrowMultiplier()) + this.garrisonUnitsInside(gameState, target, { "attacker": attacker }); + + if (target.hasClass("Unit") && attacker.hasClass("Unit")) + { + // Consider whether we should retaliate or continue our task. + if (target.hasClass("Support") || target.attackTypes() === undefined) + continue; + const orderData = target.unitAIOrderData(); + const currentTarget = orderData && orderData.length && orderData[0].target ? + gameState.getEntityById(orderData[0].target) : undefined; + if (currentTarget) + { + const unitAIState = target.unitAIState(); + const unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; + if (unitAIStateOrder === "COMBAT" && (currentTarget.id() === attacker.id() || + !currentTarget.hasClasses(["Structure", "Support"]))) + continue; + if (unitAIStateOrder === "REPAIR" && currentTarget.hasDefensiveFire()) + continue; + if (unitAIStateOrder === "COMBAT" && !isSiegeUnit(currentTarget) && + gameState.ai.HQ.capturableTargets.has(orderData[0].target)) + { + // Take the nearest unit also attacking this structure to help us. + const capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); + let minDist; + let minEnt; + const pos = attacker.position(); + capturableTarget.ents.delete(target.id()); + for (const entId of capturableTarget.ents) + { + if (allAttacked[entId]) + continue; + const ent = gameState.getEntityById(entId); + if (!ent || !ent.position() || !ent.canAttackTarget(attacker, + allowCapture(gameState, ent, attacker))) + { + continue; + } + // Check that the unit is still attacking the structure (since the last played turn). + const state = ent.unitAIState(); + if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") + continue; + const entOrderData = ent.unitAIOrderData(); + if (!entOrderData || !entOrderData.length || !entOrderData[0].target || + entOrderData[0].target != orderData[0].target) + continue; + const dist = SquareVectorDistance(pos, ent.position()); + if (minEnt && dist > minDist) + continue; + minDist = dist; + minEnt = ent; + } + if (minEnt) + { + capturableTarget.ents.delete(minEnt.id()); + minEnt.attack(attacker.id(), allowCapture(gameState, minEnt, attacker)); + } + } + } + const shouldCapture = allowCapture(gameState, target, attacker); + if (target.canAttackTarget(attacker, shouldCapture)) + target.attack(attacker.id(), shouldCapture); } } - if (getLandAccess(gameState, ent) != access) + } + + garrisonUnitsInside(gameState, target, data) + { + if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) return false; - return true; - }).filterNearest(target.position()); - - let ret = false; - for (const ent of units.values()) - { - if (garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) - break; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) + const minGarrison = data.min || target.garrisonMax(); + if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) + return false; + if (data.attacker) { - const attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); + const attackTypes = target.attackTypes(); + if (!attackTypes || attackTypes.indexOf("Ranged") == -1) + return false; + const dist = SquareVectorDistance(data.attacker.position(), target.position()); + const range = target.attackRange("Ranged").max; + if (dist >= range*range) + return false; } - const army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; - if (army) - army.removeOwn(gameState, ent.id()); - garrisonManager.garrison(gameState, ent, target, typeGarrison); - ret = true; - } - return ret; -}; + const access = getLandAccess(gameState, target); + const garrisonManager = gameState.ai.HQ.garrisonManager; + const garrisonArrowClasses = target.getGarrisonArrowClasses(); + const typeGarrison = data.type || GarrisonManager.TYPE_PROTECTION; + let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target); + if (allowMelee === undefined) + { + // Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units. + if (data.attacker) + { + allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : + !isSiegeUnit(data.attacker); + } + else + allowMelee = true; + } + const units = gameState.getOwnUnits().filter(ent => + { + if (!ent.position()) + return false; + if (!ent.hasClasses(garrisonArrowClasses)) + return false; + if (typeGarrison !== GarrisonManager.TYPE_DECAY && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) + return false; + if (ent.getMetadata(PlayerID, "transport") !== undefined) + return false; + const army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; + if (!army && (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)) + return false; + if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) + { + const subrole = ent.getMetadata(PlayerID, "subrole"); + // When structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan. + if (typeGarrison !== GarrisonManager.TYPE_DECAY && subrole && + (subrole === Worker.SUBROLE_COMPLETING || subrole === Worker.SUBROLE_WALKING || + subrole === Worker.SUBROLE_ATTACKING)) + { + return false; + } + } + if (getLandAccess(gameState, ent) != access) + return false; + return true; + }).filterNearest(target.position()); -/** Garrison a attacked siege ranged unit inside the nearest fortress. */ -DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) -{ - let distmin = Math.min(); - let nearest; - const unitAccess = getLandAccess(gameState, unit); - const garrisonManager = gameState.ai.HQ.garrisonManager; - for (const ent of gameState.getAllyStructures().values()) - { - if (!ent.isGarrisonHolder()) - continue; - if (!unit.hasClasses(ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax()) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (getLandAccess(gameState, ent) != unitAccess) - continue; - const dist = SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; + let ret = false; + for (const ent of units.values()) + { + if (garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) + break; + if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) + { + const attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); + if (attackPlan) + attackPlan.removeUnit(ent, true); + } + const army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; + if (army) + army.removeOwn(gameState, ent.id()); + garrisonManager.garrison(gameState, ent, target, typeGarrison); + ret = true; + } + return ret; } - if (nearest) - garrisonManager.garrison(gameState, unit, nearest, GarrisonManager.TYPE_PROTECTION); - return nearest !== undefined; -}; -/** - * Garrison a hurt unit inside a player-owned or allied structure. - * If emergency is true, the unit will be garrisoned in the closest possible structure. - * Otherwise, it will garrison in the closest healing structure. - */ -DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) -{ - let distmin = Math.min(); - let nearest; - const unitAccess = getLandAccess(gameState, unit); - const garrisonManager = gameState.ai.HQ.garrisonManager; - for (const ent of gameState.getAllyStructures().values()) + /** Garrison a attacked siege ranged unit inside the nearest fortress. */ + garrisonSiegeUnit(gameState, unit) { - if (!ent.isGarrisonHolder()) - continue; - if (!emergency && !ent.buffHeal()) - continue; - if (!unit.hasClasses(ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax() && - (!emergency || !ent.garrisoned().length)) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (getLandAccess(gameState, ent) != unitAccess) - continue; - const dist = SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; + let distmin = Math.min(); + let nearest; + const unitAccess = getLandAccess(gameState, unit); + const garrisonManager = gameState.ai.HQ.garrisonManager; + for (const ent of gameState.getAllyStructures().values()) + { + if (!ent.isGarrisonHolder()) + continue; + if (!unit.hasClasses(ent.garrisonableClasses())) + continue; + if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax()) + continue; + if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) + continue; + if (getLandAccess(gameState, ent) != unitAccess) + continue; + const dist = SquareVectorDistance(ent.position(), unit.position()); + if (dist > distmin) + continue; + distmin = dist; + nearest = ent; + } + if (nearest) + garrisonManager.garrison(gameState, unit, nearest, GarrisonManager.TYPE_PROTECTION); + return nearest !== undefined; } - if (!nearest) - return false; - if (!emergency) + /** + * Garrison a hurt unit inside a player-owned or allied structure. + * If emergency is true, the unit will be garrisoned in the closest possible structure. + * Otherwise, it will garrison in the closest healing structure. + */ + garrisonAttackedUnit(gameState, unit, emergency = false) { - garrisonManager.garrison(gameState, unit, nearest, GarrisonManager.TYPE_PROTECTION); + let distmin = Math.min(); + let nearest; + const unitAccess = getLandAccess(gameState, unit); + const garrisonManager = gameState.ai.HQ.garrisonManager; + for (const ent of gameState.getAllyStructures().values()) + { + if (!ent.isGarrisonHolder()) + continue; + if (!emergency && !ent.buffHeal()) + continue; + if (!unit.hasClasses(ent.garrisonableClasses())) + continue; + if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax() && + (!emergency || !ent.garrisoned().length)) + continue; + if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) + continue; + if (getLandAccess(gameState, ent) != unitAccess) + continue; + const dist = SquareVectorDistance(ent.position(), unit.position()); + if (dist > distmin) + continue; + distmin = dist; + nearest = ent; + } + if (!nearest) + return false; + + if (!emergency) + { + garrisonManager.garrison(gameState, unit, nearest, GarrisonManager.TYPE_PROTECTION); + return true; + } + if (garrisonManager.numberOfGarrisonedSlots(nearest) >= nearest.garrisonMax()) // make room for this ent + nearest.unload(nearest.garrisoned()[0]); + + garrisonManager.garrison(gameState, unit, nearest, + nearest.buffHeal() ? GarrisonManager.TYPE_PROTECTION : GarrisonManager.TYPE_EMERGENCY); return true; } - if (garrisonManager.numberOfGarrisonedSlots(nearest) >= nearest.garrisonMax()) // make room for this ent - nearest.unload(nearest.garrisoned()[0]); - garrisonManager.garrison(gameState, unit, nearest, - nearest.buffHeal() ? GarrisonManager.TYPE_PROTECTION : GarrisonManager.TYPE_EMERGENCY); - return true; -}; - -/** - * Be more inclined to help an ally attacked by several enemies. - */ -DefenseManager.prototype.GetCooperationLevel = function(ally) -{ - let cooperation = this.Config.personality.cooperative; - if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1) - cooperation += 0.2 * (this.attackedAllies[ally] - 1); - return cooperation; -}; - -/** - * Switch a defense army into an attack if needed. - */ -DefenseManager.prototype.switchToAttack = function(gameState, army) -{ - if (!army) - return; - for (const targetId of this.targetList) + /** + * Be more inclined to help an ally attacked by several enemies. + */ + GetCooperationLevel(ally) { - const target = gameState.getEntityById(targetId); - if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) - continue; - const targetAccess = getLandAccess(gameState, target); - const targetPos = target.position(); - for (const entId of army.ownEntities) - { - const ent = gameState.getEntityById(entId); - if (!ent || !ent.position() || getLandAccess(gameState, ent) != targetAccess) - continue; - if (SquareVectorDistance(targetPos, ent.position()) > 14400) - continue; - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); + let cooperation = this.Config.personality.cooperative; + if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1) + cooperation += 0.2 * (this.attackedAllies[ally] - 1); + return cooperation; + } + + /** + * Switch a defense army into an attack if needed. + */ + switchToAttack(gameState, army) + { + if (!army) return; + for (const targetId of this.targetList) + { + const target = gameState.getEntityById(targetId); + if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) + continue; + const targetAccess = getLandAccess(gameState, target); + const targetPos = target.position(); + for (const entId of army.ownEntities) + { + const ent = gameState.getEntityById(entId); + if (!ent || !ent.position() || getLandAccess(gameState, ent) != targetAccess) + continue; + if (SquareVectorDistance(targetPos, ent.position()) > 14400) + continue; + gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); + return; + } } } -}; -DefenseManager.prototype.Serialize = function() -{ - const properties = { - "targetList": this.targetList, - "armyMergeSize": this.armyMergeSize, - "attackingUnits": this.attackingUnits, - "attackingArmies": this.attackingArmies, - "attackedAllies": this.attackedAllies - }; - - const armies = []; - for (const army of this.armies) - armies.push(army.Serialize()); - - return { "properties": properties, "armies": armies }; -}; - -DefenseManager.prototype.Deserialize = function(gameState, data) -{ - for (const key in data.properties) - this[key] = data.properties[key]; - - this.armies = []; - for (const dataArmy of data.armies) + Serialize() { - const army = new DefenseArmy(gameState, []); - army.Deserialize(dataArmy); - this.armies.push(army); + const properties = { + "targetList": this.targetList, + "armyMergeSize": this.armyMergeSize, + "attackingUnits": this.attackingUnits, + "attackingArmies": this.attackingArmies, + "attackedAllies": this.attackedAllies + }; + + const armies = []; + for (const army of this.armies) + armies.push(army.Serialize()); + + return { "properties": properties, "armies": armies }; } -}; + + Deserialize(gameState, data) + { + for (const key in data.properties) + this[key] = data.properties[key]; + + this.armies = []; + for (const dataArmy of data.armies) + { + const army = new DefenseArmy(gameState, []); + army.Deserialize(dataArmy); + this.armies.push(army); + } + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js b/binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js index cd90959419..c70b19d110 100644 --- a/binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js @@ -24,563 +24,566 @@ import * as chat from "simulation/ai/petra/chatHelper.js"; * that we suggested within a period of time, or else the request will be deleted from this.sentDiplomacyRequests. * When @c deserialized is true, do not call any random function inside constructor as that would cause oos. */ -export function DiplomacyManager(Config, deserialized) +export class DiplomacyManager { - this.Config = Config; - this.nextTributeUpdate = 90; - this.nextTributeRequest = new Map(); - this.nextTributeRequest.set("all", 240); - this.betrayLapseTime = -1; - this.waitingToBetray = false; - this.betrayWeighting = 150; - this.receivedDiplomacyRequests = new Map(); - this.sentDiplomacyRequests = new Map(); - this.sentDiplomacyRequestLapseTime = deserialized ? 175 : randFloat(130, 220); -} - -/** - * If there are any players that are allied/neutral with us but we are not allied/neutral with them, - * treat this situation like an ally/neutral request. - */ -DiplomacyManager.prototype.init = function(gameState) -{ - this.lastManStandingCheck(gameState); - - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) + constructor(Config, deserialized) { - if (i === PlayerID) - continue; - - if (gameState.isPlayerMutualAlly(i)) - this.receivedDiplomacyRequests.set(i, { "requestType": "ally", "status": "accepted" }); - else if (gameState.sharedScript.playersData[i].isAlly[PlayerID]) - this.handleDiplomacyRequest(gameState, i, "ally"); - else if (gameState.sharedScript.playersData[i].isNeutral[PlayerID] && gameState.isPlayerEnemy(i)) - this.handleDiplomacyRequest(gameState, i, "neutral"); - } -}; - -/** - * Check if any allied needs help (tribute) and sent it if we have enough resource - * or ask for a tribute if we are in need and one ally can help - */ -DiplomacyManager.prototype.tributes = function(gameState) -{ - this.nextTributeUpdate = gameState.ai.elapsedTime + 30; - const resTribCodes = Resources.GetTributableCodes(); - if (!resTribCodes.length) - return; - const totalResources = gameState.getResources(); - const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); - let mostNeeded; - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - if (i === PlayerID || !gameState.isPlayerAlly(i) || gameState.ai.HQ.attackManager.defeated[i]) - continue; - const donor = gameState.getAlliedVictory() || gameState.getEntities(i).length < gameState.getOwnEntities().length; - const allyResources = gameState.sharedScript.playersData[i].resourceCounts; - const allyPop = gameState.sharedScript.playersData[i].popCount; - const tribute = {}; - let toSend = false; - for (const res of resTribCodes) - { - if (donor && availableResources[res] > 200 && allyResources[res] < 0.2 * availableResources[res]) - { - tribute[res] = Math.floor(0.3*availableResources[res] - allyResources[res]); - toSend = true; - } - else if (donor && allyPop < Math.min(30, 0.5*gameState.getPopulation()) && totalResources[res] > 500 && allyResources[res] < 100) - { - tribute[res] = 100; - toSend = true; - } - else if (this.Config.chat && availableResources[res] === 0 && allyResources[res] > totalResources[res] + 600) - { - if (gameState.ai.elapsedTime < this.nextTributeRequest.get("all")) - continue; - if (this.nextTributeRequest.has(res) && gameState.ai.elapsedTime < this.nextTributeRequest.get(res)) - continue; - if (!mostNeeded) - mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState, resTribCodes); - for (let k = 0; k < mostNeeded.length; ++k) - { - if (mostNeeded[k].type == res && mostNeeded[k].wanted > 0) - { - this.nextTributeRequest.set("all", gameState.ai.elapsedTime + 90); - this.nextTributeRequest.set(res, gameState.ai.elapsedTime + 240); - chat.requestTribute(gameState, res); - if (this.Config.debug > 1) - aiWarn("Tribute on " + res + " requested to player " + i); - break; - } - } - } - } - if (!toSend) - continue; - if (this.Config.debug > 1) - aiWarn("Tribute " + uneval(tribute) + " sent to player " + i); - if (this.Config.chat) - chat.sentTribute(gameState, i); - Engine.PostCommand(PlayerID, { "type": "tribute", "player": i, "amounts": tribute }); - } -}; - -DiplomacyManager.prototype.checkEvents = function(gameState, events) -{ - // Increase slowly the cooperative personality trait either when we receive tribute from our allies - // or if our allies attack enemies inside our territory - for (const evt of events.TributeExchanged) - { - if (evt.to === PlayerID && !gameState.isPlayerAlly(evt.from) && this.receivedDiplomacyRequests.has(evt.from)) - { - const request = this.receivedDiplomacyRequests.get(evt.from); - if (request.status === "waitingForTribute" && request.type in evt.amounts) - { - request.wanted -= evt.amounts[request.type]; - - if (request.wanted <= 0) - { - if (this.Config.debug > 1) - { - aiWarn("Player " + uneval(evt.from) + - " has sent the required tribute amount"); - } - - this.changePlayerDiplomacy(gameState, evt.from, request.requestType); - request.status = "accepted"; - } - else if (evt.amounts[request.type] > 0) - { - // Reset the warning sent to the player that reminds them to speed up the tributes - request.warnTime = gameState.ai.elapsedTime + 60; - request.sentWarning = false; - } - } - } - - if (evt.to !== PlayerID || !gameState.isPlayerAlly(evt.from)) - continue; - let tributes = 0; - for (const key in evt.amounts) - { - if (key === "food") - tributes += evt.amounts[key]; - else - tributes += 2*evt.amounts[key]; - } - this.Config.personality.cooperative = Math.min(1, this.Config.personality.cooperative + 0.0001 * tributes); + this.Config = Config; + this.nextTributeUpdate = 90; + this.nextTributeRequest = new Map(); + this.nextTributeRequest.set("all", 240); + this.betrayLapseTime = -1; + this.waitingToBetray = false; + this.betrayWeighting = 150; + this.receivedDiplomacyRequests = new Map(); + this.sentDiplomacyRequests = new Map(); + this.sentDiplomacyRequestLapseTime = deserialized ? 175 : randFloat(130, 220); } - for (const evt of events.Attacked) + /** + * If there are any players that are allied/neutral with us but we are not allied/neutral with them, + * treat this situation like an ally/neutral request. + */ + init(gameState) { - const target = gameState.getEntityById(evt.target); - if (!target || !target.position() || - gameState.ai.HQ.territoryMap.getOwner(target.position()) !== PlayerID || - !gameState.isPlayerEnemy(target.owner())) - continue; - const attacker = gameState.getEntityById(evt.attacker); - if (!attacker || attacker.owner() === PlayerID || !gameState.isPlayerAlly(attacker.owner())) - continue; - this.Config.personality.cooperative = Math.min(1, this.Config.personality.cooperative + 0.003); - } - - if (events.DiplomacyChanged.length || events.PlayerDefeated.length || events.CeasefireEnded.length) this.lastManStandingCheck(gameState); - for (const evt of events.PlayerDefeated) - { - this.receivedDiplomacyRequests.delete(evt.playerId); - this.sentDiplomacyRequests.delete(evt.playerId); - } - - for (const evt of events.DiplomacyChanged) - { - if (evt.otherPlayer !== PlayerID) - continue; - - if (this.sentDiplomacyRequests.has(evt.player)) // If another player has accepted a diplomacy request we sent + for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { - const sentRequest = this.sentDiplomacyRequests.get(evt.player); - if (gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && sentRequest.requestType === "ally" || - gameState.sharedScript.playersData[evt.player].isNeutral[PlayerID] && sentRequest.requestType === "neutral") - this.changePlayerDiplomacy(gameState, evt.player, sentRequest.requestType); + if (i === PlayerID) + continue; - // Just remove the request if the other player switched their stance to a different and/or more negative state - // TODO: Keep this send request and take it into account for later diplomacy changes (maybe be less inclined to offer to this player) - this.sentDiplomacyRequests.delete(evt.player); - continue; - } - - const request = this.receivedDiplomacyRequests.get(evt.player); - if (request !== undefined && - (!gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && request.requestType === "ally" || - gameState.sharedScript.playersData[evt.player].isEnemy[PlayerID] && request.requestType === "neutral")) - { - // a player that had requested to be allies changed their stance with us - if (request.status === "accepted") - request.status = "allianceBroken"; - else if (request.status !== "allianceBroken") - request.status = "declinedRequest"; - } - else if (gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && gameState.isPlayerEnemy(evt.player)) - { - const response = request !== undefined && (request.status === "declinedRequest" || request.status === "allianceBroken") ? - "decline" : "declineSuggestNeutral"; - chat.answerRequestDiplomacy(gameState, evt.player, "ally", response); - } - else if (gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && gameState.isPlayerNeutral(evt.player)) - this.handleDiplomacyRequest(gameState, evt.player, "ally"); - else if (gameState.sharedScript.playersData[evt.player].isNeutral[PlayerID] && gameState.isPlayerEnemy(evt.player)) - this.handleDiplomacyRequest(gameState, evt.player, "neutral"); - } - - // These events will only be sent by other AI players - for (const evt of events.DiplomacyRequest) - { - if (evt.player !== PlayerID) - continue; - - this.handleDiplomacyRequest(gameState, evt.source, evt.to); - const request = this.receivedDiplomacyRequests.get(evt.source); - if (this.Config.debug > 0) - { - aiWarn("Responding to diplomacy request from AI player " + evt.source + " with " + - uneval(request)); - } - - // Our diplomacy will have changed already if the response was "accept" - if (request.status === "waitingForTribute") - { - Engine.PostCommand(PlayerID, { - "type": "tribute-request", - "source": PlayerID, - "player": evt.source, - "resourceWanted": request.wanted, - "resourceType": request.type - }); + if (gameState.isPlayerMutualAlly(i)) + this.receivedDiplomacyRequests.set(i, { "requestType": "ally", "status": "accepted" }); + else if (gameState.sharedScript.playersData[i].isAlly[PlayerID]) + this.handleDiplomacyRequest(gameState, i, "ally"); + else if (gameState.sharedScript.playersData[i].isNeutral[PlayerID] && gameState.isPlayerEnemy(i)) + this.handleDiplomacyRequest(gameState, i, "neutral"); } } - // An AI player we sent a diplomacy request to demanded we send them a tribute - for (const evt of events.TributeRequest) + /** + * Check if any allied needs help (tribute) and sent it if we have enough resource + * or ask for a tribute if we are in need and one ally can help + */ + tributes(gameState) { - if (evt.player !== PlayerID) - continue; - + this.nextTributeUpdate = gameState.ai.elapsedTime + 30; + const resTribCodes = Resources.GetTributableCodes(); + if (!resTribCodes.length) + return; + const totalResources = gameState.getResources(); const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); - // TODO: Save this event and wait until we get more resources if we don't have enough - if (evt.resourceWanted < availableResources[evt.resourceType]) + let mostNeeded; + for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { - const responseTribute = {}; - responseTribute[evt.resourceType] = evt.resourceWanted; + if (i === PlayerID || !gameState.isPlayerAlly(i) || gameState.ai.HQ.attackManager.defeated[i]) + continue; + const donor = gameState.getAlliedVictory() || gameState.getEntities(i).length < gameState.getOwnEntities().length; + const allyResources = gameState.sharedScript.playersData[i].resourceCounts; + const allyPop = gameState.sharedScript.playersData[i].popCount; + const tribute = {}; + let toSend = false; + for (const res of resTribCodes) + { + if (donor && availableResources[res] > 200 && allyResources[res] < 0.2 * availableResources[res]) + { + tribute[res] = Math.floor(0.3*availableResources[res] - allyResources[res]); + toSend = true; + } + else if (donor && allyPop < Math.min(30, 0.5*gameState.getPopulation()) && totalResources[res] > 500 && allyResources[res] < 100) + { + tribute[res] = 100; + toSend = true; + } + else if (this.Config.chat && availableResources[res] === 0 && allyResources[res] > totalResources[res] + 600) + { + if (gameState.ai.elapsedTime < this.nextTributeRequest.get("all")) + continue; + if (this.nextTributeRequest.has(res) && gameState.ai.elapsedTime < this.nextTributeRequest.get(res)) + continue; + if (!mostNeeded) + mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState, resTribCodes); + for (let k = 0; k < mostNeeded.length; ++k) + { + if (mostNeeded[k].type == res && mostNeeded[k].wanted > 0) + { + this.nextTributeRequest.set("all", gameState.ai.elapsedTime + 90); + this.nextTributeRequest.set(res, gameState.ai.elapsedTime + 240); + chat.requestTribute(gameState, res); + if (this.Config.debug > 1) + aiWarn("Tribute on " + res + " requested to player " + i); + break; + } + } + } + } + if (!toSend) + continue; + if (this.Config.debug > 1) + aiWarn("Tribute " + uneval(tribute) + " sent to player " + i); + if (this.Config.chat) + chat.sentTribute(gameState, i); + Engine.PostCommand(PlayerID, { "type": "tribute", "player": i, "amounts": tribute }); + } + } + + checkEvents(gameState, events) + { + // Increase slowly the cooperative personality trait either when we receive tribute from our allies + // or if our allies attack enemies inside our territory + for (const evt of events.TributeExchanged) + { + if (evt.to === PlayerID && !gameState.isPlayerAlly(evt.from) && this.receivedDiplomacyRequests.has(evt.from)) + { + const request = this.receivedDiplomacyRequests.get(evt.from); + if (request.status === "waitingForTribute" && request.type in evt.amounts) + { + request.wanted -= evt.amounts[request.type]; + + if (request.wanted <= 0) + { + if (this.Config.debug > 1) + { + aiWarn("Player " + uneval(evt.from) + + " has sent the required tribute amount"); + } + + this.changePlayerDiplomacy(gameState, evt.from, request.requestType); + request.status = "accepted"; + } + else if (evt.amounts[request.type] > 0) + { + // Reset the warning sent to the player that reminds them to speed up the tributes + request.warnTime = gameState.ai.elapsedTime + 60; + request.sentWarning = false; + } + } + } + + if (evt.to !== PlayerID || !gameState.isPlayerAlly(evt.from)) + continue; + let tributes = 0; + for (const key in evt.amounts) + { + if (key === "food") + tributes += evt.amounts[key]; + else + tributes += 2*evt.amounts[key]; + } + this.Config.personality.cooperative = Math.min(1, this.Config.personality.cooperative + 0.0001 * tributes); + } + + for (const evt of events.Attacked) + { + const target = gameState.getEntityById(evt.target); + if (!target || !target.position() || + gameState.ai.HQ.territoryMap.getOwner(target.position()) !== PlayerID || + !gameState.isPlayerEnemy(target.owner())) + continue; + const attacker = gameState.getEntityById(evt.attacker); + if (!attacker || attacker.owner() === PlayerID || !gameState.isPlayerAlly(attacker.owner())) + continue; + this.Config.personality.cooperative = Math.min(1, this.Config.personality.cooperative + 0.003); + } + + if (events.DiplomacyChanged.length || events.PlayerDefeated.length || events.CeasefireEnded.length) + this.lastManStandingCheck(gameState); + + for (const evt of events.PlayerDefeated) + { + this.receivedDiplomacyRequests.delete(evt.playerId); + this.sentDiplomacyRequests.delete(evt.playerId); + } + + for (const evt of events.DiplomacyChanged) + { + if (evt.otherPlayer !== PlayerID) + continue; + + if (this.sentDiplomacyRequests.has(evt.player)) // If another player has accepted a diplomacy request we sent + { + const sentRequest = this.sentDiplomacyRequests.get(evt.player); + if (gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && sentRequest.requestType === "ally" || + gameState.sharedScript.playersData[evt.player].isNeutral[PlayerID] && sentRequest.requestType === "neutral") + this.changePlayerDiplomacy(gameState, evt.player, sentRequest.requestType); + + // Just remove the request if the other player switched their stance to a different and/or more negative state + // TODO: Keep this send request and take it into account for later diplomacy changes (maybe be less inclined to offer to this player) + this.sentDiplomacyRequests.delete(evt.player); + continue; + } + + const request = this.receivedDiplomacyRequests.get(evt.player); + if (request !== undefined && + (!gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && request.requestType === "ally" || + gameState.sharedScript.playersData[evt.player].isEnemy[PlayerID] && request.requestType === "neutral")) + { + // a player that had requested to be allies changed their stance with us + if (request.status === "accepted") + request.status = "allianceBroken"; + else if (request.status !== "allianceBroken") + request.status = "declinedRequest"; + } + else if (gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && gameState.isPlayerEnemy(evt.player)) + { + const response = request !== undefined && (request.status === "declinedRequest" || request.status === "allianceBroken") ? + "decline" : "declineSuggestNeutral"; + chat.answerRequestDiplomacy(gameState, evt.player, "ally", response); + } + else if (gameState.sharedScript.playersData[evt.player].isAlly[PlayerID] && gameState.isPlayerNeutral(evt.player)) + this.handleDiplomacyRequest(gameState, evt.player, "ally"); + else if (gameState.sharedScript.playersData[evt.player].isNeutral[PlayerID] && gameState.isPlayerEnemy(evt.player)) + this.handleDiplomacyRequest(gameState, evt.player, "neutral"); + } + + // These events will only be sent by other AI players + for (const evt of events.DiplomacyRequest) + { + if (evt.player !== PlayerID) + continue; + + this.handleDiplomacyRequest(gameState, evt.source, evt.to); + const request = this.receivedDiplomacyRequests.get(evt.source); if (this.Config.debug > 0) { - aiWarn("Responding to tribute request from AI player " + evt.source + " with " + - uneval(responseTribute)); + aiWarn("Responding to diplomacy request from AI player " + evt.source + " with " + + uneval(request)); } - Engine.PostCommand(PlayerID, { "type": "tribute", "player": evt.source, "amounts": responseTribute }); - this.nextTributeUpdate = gameState.ai.elapsedTime + 15; - } - } -}; -/** - * If the "Last Man Standing" option is enabled, check if the only remaining players are allies or neutral. - * If so, turn against the strongest first, but be more likely to first turn against neutral players, if there are any. - */ -DiplomacyManager.prototype.lastManStandingCheck = function(gameState) -{ - if (gameState.sharedScript.playersData[PlayerID].teamsLocked || gameState.isCeasefireActive() || - gameState.getAlliedVictory() && gameState.hasAllies()) - return; - - if (gameState.hasEnemies()) - { - this.waitingToBetray = false; - return; - } - - if (!gameState.hasAllies() && !gameState.hasNeutrals()) - return; - - // wait a bit before turning - if (!this.waitingToBetray) - { - this.betrayLapseTime = gameState.ai.elapsedTime + randFloat(10, 110); - this.waitingToBetray = true; - return; - } - - // do not turn against a player yet if we are not strong enough - if (gameState.getOwnUnits().length < 50) - { - this.betrayLapseTime += 60; - return; - } - - let playerToTurnAgainst; - let max = 0; - - // count the amount of entities remaining players have - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - if (i === PlayerID || gameState.ai.HQ.attackManager.defeated[i]) - continue; - - let turnFactor = gameState.getEntities(i).length; - - if (gameState.isPlayerNeutral(i)) // be more inclined to turn against neutral players - turnFactor += this.betrayWeighting; - - if (gameState.getVictoryConditions().has("wonder")) - { - const wonder = gameState.getEnemyStructures(i).filter(filters.byClass("Wonder"))[0]; - if (wonder) - { - const wonderProgess = wonder.foundationProgress(); - if (wonderProgess === undefined) - { - playerToTurnAgainst = i; - break; - } - turnFactor += wonderProgess * 2.5 + this.betrayWeighting; - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - const relicsCount = gameState.updatingGlobalCollection("allRelics", - filters.byClass("Relic")).filter(relic => relic.owner() === i).length; - turnFactor += relicsCount * this.betrayWeighting; - } - - if (turnFactor < max) - continue; - - max = turnFactor; - playerToTurnAgainst = i; - } - - if (playerToTurnAgainst) - { - this.changePlayerDiplomacy(gameState, playerToTurnAgainst, "enemy"); - const request = this.receivedDiplomacyRequests.get(playerToTurnAgainst); - if (request && request.status !== "allianceBroken") - { + // Our diplomacy will have changed already if the response was "accept" if (request.status === "waitingForTribute") - chat.answerRequestDiplomacy(gameState, playerToTurnAgainst, request.requestType, "decline"); - request.status = request.status === "accepted" ? "allianceBroken" : "declinedRequest"; + { + Engine.PostCommand(PlayerID, { + "type": "tribute-request", + "source": PlayerID, + "player": evt.source, + "resourceWanted": request.wanted, + "resourceType": request.type + }); + } } - // If we had sent this player a diplomacy request, just rescind it - this.sentDiplomacyRequests.delete(playerToTurnAgainst); - } - this.betrayLapseTime = -1; - this.waitingToBetray = false; -}; -/** - * Do not become allies with a player if the game would be over. - * Overall, be reluctant to become allies with any one player, but be more likely to accept neutral requests. - */ -DiplomacyManager.prototype.handleDiplomacyRequest = function(gameState, player, requestType) -{ - if (gameState.sharedScript.playersData[PlayerID].teamsLocked) - return; - let response; - let requiredTribute; - const request = this.receivedDiplomacyRequests.get(player); - const moreEnemiesThanAllies = gameState.getEnemies().length > gameState.getMutualAllies().length; + // An AI player we sent a diplomacy request to demanded we send them a tribute + for (const evt of events.TributeRequest) + { + if (evt.player !== PlayerID) + continue; - // For any given diplomacy request be likely to permanently decline - if (!request && gameState.getPlayerCiv() !== gameState.getPlayerCiv(player) && randBool(0.6) || - !moreEnemiesThanAllies || gameState.ai.HQ.attackManager.currentEnemyPlayer === player) - { - this.receivedDiplomacyRequests.set(player, { "requestType": requestType, "status": "declinedRequest" }); - response = "decline"; - } - else if (request && request.status !== "accepted" && request.requestType !== "ally") - { - if (request.status === "declinedRequest") - response = "decline"; - else if (request.status === "allianceBroken") // Previous alliance was broken, so decline - response = "declineRepeatedOffer"; - else if (request.status === "waitingForTribute") - { - response = "waitingForTribute"; - requiredTribute = request; + const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); + // TODO: Save this event and wait until we get more resources if we don't have enough + if (evt.resourceWanted < availableResources[evt.resourceType]) + { + const responseTribute = {}; + responseTribute[evt.resourceType] = evt.resourceWanted; + if (this.Config.debug > 0) + { + aiWarn("Responding to tribute request from AI player " + evt.source + " with " + + uneval(responseTribute)); + } + Engine.PostCommand(PlayerID, { "type": "tribute", "player": evt.source, "amounts": responseTribute }); + this.nextTributeUpdate = gameState.ai.elapsedTime + 15; + } } } - else if (requestType === "ally" && gameState.getEntities(player).length < gameState.getOwnEntities().length && randBool(0.4) || - requestType === "neutral" && moreEnemiesThanAllies && randBool(0.8)) + + /** + * If the "Last Man Standing" option is enabled, check if the only remaining players are allies or neutral. + * If so, turn against the strongest first, but be more likely to first turn against neutral players, if there are any. + */ + lastManStandingCheck(gameState) { - response = "accept"; - this.changePlayerDiplomacy(gameState, player, requestType); - this.receivedDiplomacyRequests.set(player, { "requestType": requestType, "status": "accepted" }); - } - else - { - // Try to request a tribute. - // If a resource is not tributable, do not request it. - // If no resources are tributable, decline. - const resTribCodes = Resources.GetTributableCodes(); - if (resTribCodes.length) + if (gameState.sharedScript.playersData[PlayerID].teamsLocked || gameState.isCeasefireActive() || + gameState.getAlliedVictory() && gameState.hasAllies()) + return; + + if (gameState.hasEnemies()) { - requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState, resTribCodes)[0]; - response = "acceptWithTribute"; - requiredTribute.wanted = Math.max(1000, gameState.getOwnUnits().length * (requestType === "ally" ? 10 : 5)); - this.receivedDiplomacyRequests.set(player, { - "status": "waitingForTribute", - "wanted": requiredTribute.wanted, - "type": requiredTribute.type, - "warnTime": gameState.ai.elapsedTime + 60, - "sentWarning": false, - "requestType": requestType - }); + this.waitingToBetray = false; + return; } - else + + if (!gameState.hasAllies() && !gameState.hasNeutrals()) + return; + + // wait a bit before turning + if (!this.waitingToBetray) + { + this.betrayLapseTime = gameState.ai.elapsedTime + randFloat(10, 110); + this.waitingToBetray = true; + return; + } + + // do not turn against a player yet if we are not strong enough + if (gameState.getOwnUnits().length < 50) + { + this.betrayLapseTime += 60; + return; + } + + let playerToTurnAgainst; + let max = 0; + + // count the amount of entities remaining players have + for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) + { + if (i === PlayerID || gameState.ai.HQ.attackManager.defeated[i]) + continue; + + let turnFactor = gameState.getEntities(i).length; + + if (gameState.isPlayerNeutral(i)) // be more inclined to turn against neutral players + turnFactor += this.betrayWeighting; + + if (gameState.getVictoryConditions().has("wonder")) + { + const wonder = gameState.getEnemyStructures(i).filter(filters.byClass("Wonder"))[0]; + if (wonder) + { + const wonderProgess = wonder.foundationProgress(); + if (wonderProgess === undefined) + { + playerToTurnAgainst = i; + break; + } + turnFactor += wonderProgess * 2.5 + this.betrayWeighting; + } + } + + if (gameState.getVictoryConditions().has("capture_the_relic")) + { + const relicsCount = gameState.updatingGlobalCollection("allRelics", + filters.byClass("Relic")).filter(relic => relic.owner() === i).length; + turnFactor += relicsCount * this.betrayWeighting; + } + + if (turnFactor < max) + continue; + + max = turnFactor; + playerToTurnAgainst = i; + } + + if (playerToTurnAgainst) + { + this.changePlayerDiplomacy(gameState, playerToTurnAgainst, "enemy"); + const request = this.receivedDiplomacyRequests.get(playerToTurnAgainst); + if (request && request.status !== "allianceBroken") + { + if (request.status === "waitingForTribute") + chat.answerRequestDiplomacy(gameState, playerToTurnAgainst, request.requestType, "decline"); + request.status = request.status === "accepted" ? "allianceBroken" : "declinedRequest"; + } + // If we had sent this player a diplomacy request, just rescind it + this.sentDiplomacyRequests.delete(playerToTurnAgainst); + } + this.betrayLapseTime = -1; + this.waitingToBetray = false; + } + + /** + * Do not become allies with a player if the game would be over. + * Overall, be reluctant to become allies with any one player, but be more likely to accept neutral requests. + */ + handleDiplomacyRequest(gameState, player, requestType) + { + if (gameState.sharedScript.playersData[PlayerID].teamsLocked) + return; + let response; + let requiredTribute; + const request = this.receivedDiplomacyRequests.get(player); + const moreEnemiesThanAllies = gameState.getEnemies().length > gameState.getMutualAllies().length; + + // For any given diplomacy request be likely to permanently decline + if (!request && gameState.getPlayerCiv() !== gameState.getPlayerCiv(player) && randBool(0.6) || + !moreEnemiesThanAllies || gameState.ai.HQ.attackManager.currentEnemyPlayer === player) { this.receivedDiplomacyRequests.set(player, { "requestType": requestType, "status": "declinedRequest" }); response = "decline"; } - } - chat.answerRequestDiplomacy(gameState, player, requestType, response, requiredTribute); -}; - -DiplomacyManager.prototype.changePlayerDiplomacy = function(gameState, player, newDiplomaticStance) -{ - if (gameState.isPlayerEnemy(player) && (newDiplomaticStance === "ally" || newDiplomaticStance === "neutral")) - gameState.ai.HQ.attackManager.cancelAttacksAgainstPlayer(gameState, player); - Engine.PostCommand(PlayerID, { "type": "diplomacy", "player": player, "to": newDiplomaticStance }); - if (this.Config.debug > 1) - aiWarn("diplomacy stance with player " + player + " is now " + newDiplomaticStance); - if (this.Config.chat) - chat.newDiplomacy(gameState, player, newDiplomaticStance); -}; - -DiplomacyManager.prototype.checkRequestedTributes = function(gameState) -{ - for (const [player, data] of this.receivedDiplomacyRequests) - if (data.status === "waitingForTribute" && gameState.ai.elapsedTime > data.warnTime) + else if (request && request.status !== "accepted" && request.requestType !== "ally") { - if (data.sentWarning) + if (request.status === "declinedRequest") + response = "decline"; + else if (request.status === "allianceBroken") // Previous alliance was broken, so decline + response = "declineRepeatedOffer"; + else if (request.status === "waitingForTribute") { - this.receivedDiplomacyRequests.delete(player); - chat.answerRequestDiplomacy(gameState, player, data.requestType, "decline"); + response = "waitingForTribute"; + requiredTribute = request; + } + } + else if (requestType === "ally" && gameState.getEntities(player).length < gameState.getOwnEntities().length && randBool(0.4) || + requestType === "neutral" && moreEnemiesThanAllies && randBool(0.8)) + { + response = "accept"; + this.changePlayerDiplomacy(gameState, player, requestType); + this.receivedDiplomacyRequests.set(player, { "requestType": requestType, "status": "accepted" }); + } + else + { + // Try to request a tribute. + // If a resource is not tributable, do not request it. + // If no resources are tributable, decline. + const resTribCodes = Resources.GetTributableCodes(); + if (resTribCodes.length) + { + requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState, resTribCodes)[0]; + response = "acceptWithTribute"; + requiredTribute.wanted = Math.max(1000, gameState.getOwnUnits().length * (requestType === "ally" ? 10 : 5)); + this.receivedDiplomacyRequests.set(player, { + "status": "waitingForTribute", + "wanted": requiredTribute.wanted, + "type": requiredTribute.type, + "warnTime": gameState.ai.elapsedTime + 60, + "sentWarning": false, + "requestType": requestType + }); } else { - data.sentWarning = true; - data.warnTime = gameState.ai.elapsedTime + 60; - chat.answerRequestDiplomacy(gameState, player, data.requestType, "waitingForTribute", { - "wanted": data.wanted, - "type": data.type - }); + this.receivedDiplomacyRequests.set(player, { "requestType": requestType, "status": "declinedRequest" }); + response = "decline"; } } -}; + chat.answerRequestDiplomacy(gameState, player, requestType, response, requiredTribute); + } -/** - * Try to become allies with a player who has a lot of mutual enemies in common with us. - * TODO: Possibly let human players demand tributes from AIs who send diplomacy requests. - */ -DiplomacyManager.prototype.sendDiplomacyRequest = function(gameState) -{ - let player; - let max = 0; - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) + changePlayerDiplomacy(gameState, player, newDiplomaticStance) { - let mutualEnemies = 0; - const request = this.receivedDiplomacyRequests.get(i); // Do not send to players we have already rejected before - if (i === PlayerID || gameState.isPlayerMutualAlly(i) || gameState.ai.HQ.attackManager.defeated[i] || - gameState.ai.HQ.attackManager.currentEnemyPlayer === i || - this.sentDiplomacyRequests.get(i) !== undefined || request && request.status === "declinedRequest") - continue; + if (gameState.isPlayerEnemy(player) && (newDiplomaticStance === "ally" || newDiplomaticStance === "neutral")) + gameState.ai.HQ.attackManager.cancelAttacksAgainstPlayer(gameState, player); + Engine.PostCommand(PlayerID, { "type": "diplomacy", "player": player, "to": newDiplomaticStance }); + if (this.Config.debug > 1) + aiWarn("diplomacy stance with player " + player + " is now " + newDiplomaticStance); + if (this.Config.chat) + chat.newDiplomacy(gameState, player, newDiplomaticStance); + } - for (let j = 1; j < gameState.sharedScript.playersData.length; ++j) + checkRequestedTributes(gameState) + { + for (const [player, data] of this.receivedDiplomacyRequests) + if (data.status === "waitingForTribute" && gameState.ai.elapsedTime > data.warnTime) + { + if (data.sentWarning) + { + this.receivedDiplomacyRequests.delete(player); + chat.answerRequestDiplomacy(gameState, player, data.requestType, "decline"); + } + else + { + data.sentWarning = true; + data.warnTime = gameState.ai.elapsedTime + 60; + chat.answerRequestDiplomacy(gameState, player, data.requestType, "waitingForTribute", { + "wanted": data.wanted, + "type": data.type + }); + } + } + } + + /** + * Try to become allies with a player who has a lot of mutual enemies in common with us. + * TODO: Possibly let human players demand tributes from AIs who send diplomacy requests. + */ + sendDiplomacyRequest(gameState) + { + let player; + let max = 0; + for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { - if (gameState.sharedScript.playersData[i].isEnemy[j] && gameState.isPlayerEnemy(j) && - !gameState.ai.HQ.attackManager.defeated[j]) - ++mutualEnemies; - - if (mutualEnemies < max) + let mutualEnemies = 0; + const request = this.receivedDiplomacyRequests.get(i); // Do not send to players we have already rejected before + if (i === PlayerID || gameState.isPlayerMutualAlly(i) || gameState.ai.HQ.attackManager.defeated[i] || + gameState.ai.HQ.attackManager.currentEnemyPlayer === i || + this.sentDiplomacyRequests.get(i) !== undefined || request && request.status === "declinedRequest") continue; - max = mutualEnemies; - player = i; + for (let j = 1; j < gameState.sharedScript.playersData.length; ++j) + { + if (gameState.sharedScript.playersData[i].isEnemy[j] && gameState.isPlayerEnemy(j) && + !gameState.ai.HQ.attackManager.defeated[j]) + ++mutualEnemies; + + if (mutualEnemies < max) + continue; + + max = mutualEnemies; + player = i; + } } + if (!player) + return; + + const requestType = gameState.isPlayerNeutral(player) ? "ally" : "neutral"; + + this.sentDiplomacyRequests.set(player, { + "requestType": requestType, + "timeSent": gameState.ai.elapsedTime + }); + + if (this.Config.debug > 0) + aiWarn("Sending diplomacy request to player " + player + " with " + requestType); + Engine.PostCommand(PlayerID, { "type": "diplomacy-request", "source": PlayerID, "player": player, "to": requestType }); + chat.newRequestDiplomacy(gameState, player, requestType, "sendRequest"); } - if (!player) - return; - const requestType = gameState.isPlayerNeutral(player) ? "ally" : "neutral"; - - this.sentDiplomacyRequests.set(player, { - "requestType": requestType, - "timeSent": gameState.ai.elapsedTime - }); - - if (this.Config.debug > 0) - aiWarn("Sending diplomacy request to player " + player + " with " + requestType); - Engine.PostCommand(PlayerID, { "type": "diplomacy-request", "source": PlayerID, "player": player, "to": requestType }); - chat.newRequestDiplomacy(gameState, player, requestType, "sendRequest"); -}; - -DiplomacyManager.prototype.checkSentDiplomacyRequests = function(gameState) -{ - for (const [player, data] of this.sentDiplomacyRequests) - if (gameState.ai.elapsedTime > data.timeSent + 60 && !gameState.ai.HQ.saveResources && - gameState.getPopulation() > 70) - { - chat.newRequestDiplomacy(gameState, player, data.requestType, "requestExpired"); - this.sentDiplomacyRequests.delete(player); - } -}; - -DiplomacyManager.prototype.update = function(gameState, events) -{ - this.checkEvents(gameState, events); - - if (Resources.GetTributableCodes().length && !gameState.ai.HQ.saveResources && gameState.ai.elapsedTime > this.nextTributeUpdate) - this.tributes(gameState); - - if (this.waitingToBetray && gameState.ai.elapsedTime > this.betrayLapseTime) - this.lastManStandingCheck(gameState); - - this.checkRequestedTributes(gameState); - - if (gameState.sharedScript.playersData[PlayerID].teamsLocked || gameState.isCeasefireActive()) - return; - - // Be unlikely to send diplomacy requests to other players - if (gameState.ai.elapsedTime > this.sentDiplomacyRequestLapseTime) + checkSentDiplomacyRequests(gameState) { - this.sentDiplomacyRequestLapseTime = gameState.ai.elapsedTime + 300 + randFloat(10, 100); - const numEnemies = gameState.getEnemies().length; - // Don't consider gaia - if (numEnemies > 2 && gameState.getMutualAllies().length < numEnemies - 1 && randBool(0.1)) - this.sendDiplomacyRequest(gameState); + for (const [player, data] of this.sentDiplomacyRequests) + if (gameState.ai.elapsedTime > data.timeSent + 60 && !gameState.ai.HQ.saveResources && + gameState.getPopulation() > 70) + { + chat.newRequestDiplomacy(gameState, player, data.requestType, "requestExpired"); + this.sentDiplomacyRequests.delete(player); + } } - this.checkSentDiplomacyRequests(gameState); -}; + update(gameState, events) + { + this.checkEvents(gameState, events); -DiplomacyManager.prototype.Serialize = function() -{ - return { - "nextTributeUpdate": this.nextTributeUpdate, - "nextTributeRequest": this.nextTributeRequest, - "betrayLapseTime": this.betrayLapseTime, - "waitingToBetray": this.waitingToBetray, - "betrayWeighting": this.betrayWeighting, - "receivedDiplomacyRequests": this.receivedDiplomacyRequests, - "sentDiplomacyRequests": this.sentDiplomacyRequests, - "sentDiplomacyRequestLapseTime": this.sentDiplomacyRequestLapseTime - }; -}; + if (Resources.GetTributableCodes().length && !gameState.ai.HQ.saveResources && gameState.ai.elapsedTime > this.nextTributeUpdate) + this.tributes(gameState); -DiplomacyManager.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; -}; + if (this.waitingToBetray && gameState.ai.elapsedTime > this.betrayLapseTime) + this.lastManStandingCheck(gameState); + + this.checkRequestedTributes(gameState); + + if (gameState.sharedScript.playersData[PlayerID].teamsLocked || gameState.isCeasefireActive()) + return; + + // Be unlikely to send diplomacy requests to other players + if (gameState.ai.elapsedTime > this.sentDiplomacyRequestLapseTime) + { + this.sentDiplomacyRequestLapseTime = gameState.ai.elapsedTime + 300 + randFloat(10, 100); + const numEnemies = gameState.getEnemies().length; + // Don't consider gaia + if (numEnemies > 2 && gameState.getMutualAllies().length < numEnemies - 1 && randBool(0.1)) + this.sendDiplomacyRequest(gameState); + } + + this.checkSentDiplomacyRequests(gameState); + } + + Serialize() + { + return { + "nextTributeUpdate": this.nextTributeUpdate, + "nextTributeRequest": this.nextTributeRequest, + "betrayLapseTime": this.betrayLapseTime, + "waitingToBetray": this.waitingToBetray, + "betrayWeighting": this.betrayWeighting, + "receivedDiplomacyRequests": this.receivedDiplomacyRequests, + "sentDiplomacyRequests": this.sentDiplomacyRequests, + "sentDiplomacyRequestLapseTime": this.sentDiplomacyRequestLapseTime + }; + } + + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/emergencyManager.js b/binaries/data/mods/public/simulation/ai/petra/emergencyManager.js index 636b45a78d..33b6363add 100644 --- a/binaries/data/mods/public/simulation/ai/petra/emergencyManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/emergencyManager.js @@ -3,94 +3,98 @@ import { emergency as chatEmergency } from "simulation/ai/petra/chatHelper.js"; /** * Checks for emergencies and acts accordingly */ -export function EmergencyManager(Config) +export class EmergencyManager { - this.Config = Config; - this.referencePopulation = 0; - this.referenceStructureCount = 0; - this.numRoots = 0; - this.hasEmergency = false; + referencePopulation = 0; + referenceStructureCount = 0; + numRoots = 0; + hasEmergency = false; + + constructor(Config) + { + this.Config = Config; + } + + init(gameState) + { + this.referencePopulation = gameState.getPopulation(); + this.referenceStructureCount = gameState.getOwnStructures().length; + this.numRoots = this.rootCount(gameState); + } + + update(gameState) + { + if (this.hasEmergency) + { + this.emergencyUpdate(gameState); + return; + } + const pop = gameState.getPopulation(); + const nStructures = gameState.getOwnStructures().length; + const nRoots = this.rootCount(gameState); + const factors = this.Config.emergencyValues; + if (((pop / this.referencePopulation) < factors.population || pop == 0) && + ((nStructures / this.referenceStructureCount) < factors.structures || nStructures == 0)) + this.setEmergency(gameState, true); + else if ((nRoots / this.numRoots) <= factors.roots || (nRoots == 0 && this.numRoots != 0)) + this.setEmergency(gameState, true); + + if (pop > this.referencePopulation || this.hasEmergency) + this.referencePopulation = pop; + if (nStructures > this.referenceStructureCount || this.hasEmergency) + this.referenceStructureCount = nStructures; + if (nRoots > this.numRoots || this.hasEmergency) + this.numRoots = nRoots; + } + + emergencyUpdate(gameState) + { + const pop = gameState.getPopulation(); + const nStructures = gameState.getOwnStructures().length; + const nRoots = this.rootCount(gameState); + const factors = this.Config.emergencyValues; + + if ((pop > this.referencePopulation * 1.2 && + nStructures > this.referenceStructureCount * 1.2) || + nRoots > this.numRoots) + { + this.setEmergency(gameState, false); + this.referencePopulation = pop; + this.referenceStructureCount = nStructures; + this.numRoots = nRoots; + } + } + + rootCount(gameState) + { + let roots = 0; + gameState.getOwnStructures().toEntityArray().forEach(ent => + { + if (ent?.get("TerritoryInfluence")?.Root === "true") + roots++; + }); + return roots; + } + + setEmergency(gameState, enable) + { + this.hasEmergency = enable; + chatEmergency(gameState, enable); + } + + Serialize() + { + return { + "referencePopulation": this.referencePopulation, + "referenceStructureCount": this.referenceStructureCount, + "numRoots": this.numRoots, + "hasEmergency": this.hasEmergency + }; + } + + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; + } } - -EmergencyManager.prototype.init = function(gameState) -{ - this.referencePopulation = gameState.getPopulation(); - this.referenceStructureCount = gameState.getOwnStructures().length; - this.numRoots = this.rootCount(gameState); -}; - -EmergencyManager.prototype.update = function(gameState) -{ - if (this.hasEmergency) - { - this.emergencyUpdate(gameState); - return; - } - const pop = gameState.getPopulation(); - const nStructures = gameState.getOwnStructures().length; - const nRoots = this.rootCount(gameState); - const factors = this.Config.emergencyValues; - if (((pop / this.referencePopulation) < factors.population || pop == 0) && - ((nStructures / this.referenceStructureCount) < factors.structures || nStructures == 0)) - this.setEmergency(gameState, true); - else if ((nRoots / this.numRoots) <= factors.roots || (nRoots == 0 && this.numRoots != 0)) - this.setEmergency(gameState, true); - - if (pop > this.referencePopulation || this.hasEmergency) - this.referencePopulation = pop; - if (nStructures > this.referenceStructureCount || this.hasEmergency) - this.referenceStructureCount = nStructures; - if (nRoots > this.numRoots || this.hasEmergency) - this.numRoots = nRoots; -}; - -EmergencyManager.prototype.emergencyUpdate = function(gameState) -{ - const pop = gameState.getPopulation(); - const nStructures = gameState.getOwnStructures().length; - const nRoots = this.rootCount(gameState); - const factors = this.Config.emergencyValues; - - if ((pop > this.referencePopulation * 1.2 && - nStructures > this.referenceStructureCount * 1.2) || - nRoots > this.numRoots) - { - this.setEmergency(gameState, false); - this.referencePopulation = pop; - this.referenceStructureCount = nStructures; - this.numRoots = nRoots; - } -}; - -EmergencyManager.prototype.rootCount = function(gameState) -{ - let roots = 0; - gameState.getOwnStructures().toEntityArray().forEach(ent => - { - if (ent?.get("TerritoryInfluence")?.Root === "true") - roots++; - }); - return roots; -}; - -EmergencyManager.prototype.setEmergency = function(gameState, enable) -{ - this.hasEmergency = enable; - chatEmergency(gameState, enable); -}; - -EmergencyManager.prototype.Serialize = function() -{ - return { - "referencePopulation": this.referencePopulation, - "referenceStructureCount": this.referenceStructureCount, - "numRoots": this.numRoots, - "hasEmergency": this.hasEmergency - }; -}; - -EmergencyManager.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; -}; diff --git a/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js b/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js index 4f1a401137..527afd9e86 100644 --- a/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js @@ -10,38 +10,75 @@ import { Worker } from "simulation/ai/petra/worker.js"; * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) */ -export function GarrisonManager(Config) +export class GarrisonManager { - this.Config = Config; - this.holders = new Map(); - this.decayingStructures = new Map(); -} + holders = new Map(); + decayingStructures = new Map(); -GarrisonManager.TYPE_FORCE = "force"; -GarrisonManager.TYPE_TRADE = "trade"; -GarrisonManager.TYPE_PROTECTION = "protection"; -GarrisonManager.TYPE_DECAY = "decay"; -GarrisonManager.TYPE_EMERGENCY = "emergency"; - -GarrisonManager.prototype.update = function(gameState, events) -{ - // First check for possible upgrade of a structure - for (const evt of events.EntityRenamed) + constructor(Config) { - for (const id of this.holders.keys()) + this.Config = Config; + } + + static TYPE_FORCE = "force"; + static TYPE_TRADE = "trade"; + static TYPE_PROTECTION = "protection"; + static TYPE_DECAY = "decay"; + static TYPE_EMERGENCY = "emergency"; + + update(gameState, events) + { + // First check for possible upgrade of a structure + for (const evt of events.EntityRenamed) { - if (id != evt.entity) - continue; - const data = this.holders.get(id); - const newHolder = gameState.getEntityById(evt.newentity); - if (newHolder && newHolder.isGarrisonHolder()) + for (const id of this.holders.keys()) { - this.holders.delete(id); - this.holders.set(evt.newentity, data); + if (id != evt.entity) + continue; + const data = this.holders.get(id); + const newHolder = gameState.getEntityById(evt.newentity); + if (newHolder && newHolder.isGarrisonHolder()) + { + this.holders.delete(id); + this.holders.set(evt.newentity, data); + } + else + { + for (const entId of data.list) + { + const ent = gameState.getEntityById(entId); + if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) + continue; + this.leaveGarrison(ent); + ent.stopMoving(); + } + this.holders.delete(id); + } } - else + + for (const id of this.decayingStructures.keys()) { - for (const entId of data.list) + if (id !== evt.entity) + continue; + this.decayingStructures.delete(id); + if (this.decayingStructures.has(evt.newentity)) + continue; + const ent = gameState.getEntityById(evt.newentity); + if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) + continue; + const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); + this.decayingStructures.set(evt.newentity, gmin); + } + } + + for (const [id, data] of this.holders.entries()) + { + const list = data.list; + const holder = gameState.getEntityById(id); + if (!holder || !gameState.isPlayerAlly(holder.owner())) + { + // this holder was certainly destroyed or captured. Let's remove it + for (const entId of list) { const ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) @@ -50,340 +87,307 @@ GarrisonManager.prototype.update = function(gameState, events) ent.stopMoving(); } this.holders.delete(id); - } - } - - for (const id of this.decayingStructures.keys()) - { - if (id !== evt.entity) continue; - this.decayingStructures.delete(id); - if (this.decayingStructures.has(evt.newentity)) - continue; - const ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) - continue; - const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(evt.newentity, gmin); - } - } - - for (const [id, data] of this.holders.entries()) - { - const list = data.list; - const holder = gameState.getEntityById(id); - if (!holder || !gameState.isPlayerAlly(holder.owner())) - { - // this holder was certainly destroyed or captured. Let's remove it - for (const entId of list) - { - const ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) - continue; - 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 (const evt of events.EntityRenamed) - if (evt.entity === list[j]) - list[j] = evt.newentity; + // Update the list of garrisoned units + for (let j = 0; j < list.length; ++j) + { + for (const evt of events.EntityRenamed) + if (evt.entity === list[j]) + list[j] = evt.newentity; - const 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 - { - if (ent.unitAIOrderData().some(order => order.target && order.target == id)) - continue; - if (ent.getMetadata(PlayerID, "garrisonHolder") == id) + const 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 { - // The garrison order must have failed this.leaveGarrison(ent); list.splice(j--, 1); } else { - if (gameState.ai.Config.debug > 0) + if (ent.unitAIOrderData().some(order => order.target && order.target == id)) + continue; + if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { - aiWarn("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())); - dumpEntity(ent); + // The garrison order must have failed + this.leaveGarrison(ent); + list.splice(j--, 1); + } + else + { + if (gameState.ai.Config.debug > 0) + { + aiWarn("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())); + 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) + { + const range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; + const around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; + for (const ent of gameState.getEnemyEntities().values()) + { + if (ent.hasClass("Structure")) + { + if (!ent.attackRange("Ranged")) + continue; + } + else if (ent.hasClass("Unit")) + { + if (ent.owner() == 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT")) + continue; + } + else + continue; + if (!ent.position()) + continue; + const dist = SquareVectorDistance(ent.position(), holder.position()); + if (dist > range*range) + continue; + if (ent.hasClass("Structure")) + around.defenseStructure = true; + else if (isSiegeUnit(ent)) + { + if (ent.attackTypes().indexOf("Melee") !== -1) + around.meleeSiege = true; + else + around.rangeSiege = true; + } + else + { + around.unit = true; + break; + } + } + // Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units + data.allowMelee = around.defenseStructure || around.unit; + + for (const entId of holder.garrisoned()) + { + const ent = gameState.getEntityById(entId); + if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) + holder.unload(entId); + } + for (let j = 0; j < list.length; ++j) + { + const ent = gameState.getEntityById(list[j]); + if (this.keepGarrisoned(ent, holder, around)) + continue; + if (ent.getMetadata(PlayerID, "garrisonHolder") == id) + { + this.leaveGarrison(ent); + ent.stopMoving(); } list.splice(j--, 1); } + if (this.numberOfGarrisonedSlots(holder) === 0) + this.holders.delete(id); + else + holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); } - } - if (!holder.position()) // could happen with siege unit inside a ship - continue; - - if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) + // 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 (const [id, gmin] of this.decayingStructures.entries()) { - const range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; - const around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; - for (const ent of gameState.getEnemyEntities().values()) - { - if (ent.hasClass("Structure")) - { - if (!ent.attackRange("Ranged")) - continue; - } - else if (ent.hasClass("Unit")) - { - if (ent.owner() == 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT")) - continue; - } - else - continue; - if (!ent.position()) - continue; - const dist = SquareVectorDistance(ent.position(), holder.position()); - if (dist > range*range) - continue; - if (ent.hasClass("Structure")) - around.defenseStructure = true; - else if (isSiegeUnit(ent)) - { - if (ent.attackTypes().indexOf("Melee") !== -1) - around.meleeSiege = true; - else - around.rangeSiege = true; - } - else - { - around.unit = true; - break; - } - } - // Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units - data.allowMelee = around.defenseStructure || around.unit; - - for (const entId of holder.garrisoned()) - { - const ent = gameState.getEntityById(entId); - if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) - holder.unload(entId); - } - for (let j = 0; j < list.length; ++j) - { - const ent = gameState.getEntityById(list[j]); - if (this.keepGarrisoned(ent, holder, around)) - continue; - if (ent.getMetadata(PlayerID, "garrisonHolder") == id) - { - this.leaveGarrison(ent); - ent.stopMoving(); - } - list.splice(j--, 1); - } - if (this.numberOfGarrisonedSlots(holder) === 0) - this.holders.delete(id); - else - holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); + const ent = gameState.getEntityById(id); + if (!ent || ent.owner() !== PlayerID) + this.decayingStructures.delete(id); + else if (this.numberOfGarrisonedSlots(ent) < gmin) + gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": GarrisonManager.TYPE_DECAY }); } } - // 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 (const [id, gmin] of this.decayingStructures.entries()) + /** TODO should add the units garrisoned inside garrisoned units */ + numberOfGarrisonedUnits(holder) { - const ent = gameState.getEntityById(id); - if (!ent || ent.owner() !== PlayerID) - this.decayingStructures.delete(id); - else if (this.numberOfGarrisonedSlots(ent) < gmin) - gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": GarrisonManager.TYPE_DECAY }); - } -}; + if (!this.holders.has(holder.id())) + return holder.garrisoned().length; -/** TODO should add the units garrisoned inside garrisoned units */ -GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) -{ - if (!this.holders.has(holder.id())) - return holder.garrisoned().length; - - return holder.garrisoned().length + this.holders.get(holder.id()).list.length; -}; - -/** TODO should add the units garrisoned inside garrisoned units */ -GarrisonManager.prototype.numberOfGarrisonedSlots = function(holder) -{ - if (!this.holders.has(holder.id())) - return holder.garrisonedSlots(); - - return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length; -}; - -GarrisonManager.prototype.allowMelee = function(holder) -{ - if (!this.holders.has(holder.id())) - return undefined; - - return this.holders.get(holder.id()).allowMelee; -}; - -/** This is just a pre-garrison state, while the entity walk to the garrison holder */ -GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) -{ - if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison()) - return; - - this.registerHolder(gameState, holder); - this.holders.get(holder.id()).list.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")); + return holder.garrisoned().length + this.holders.get(holder.id()).list.length; } - if (ent.getMetadata(PlayerID, "plan") !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - ent.setMetadata(PlayerID, "subrole", Worker.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) - This function is for internal use inside garrisonManager. From outside, you should also update - the holder and then using cancelGarrison should be the preferred solution - */ -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); -}; - -/** Cancel a pre-garrison state */ -GarrisonManager.prototype.cancelGarrison = function(ent) -{ - ent.stopMoving(); - this.leaveGarrison(ent); - const holderId = ent.getMetadata(PlayerID, "garrisonHolder"); - if (!holderId || !this.holders.has(holderId)) - return; - const list = this.holders.get(holderId).list; - const index = list.indexOf(ent.id()); - if (index !== -1) - list.splice(index, 1); -}; - -GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around) -{ - switch (ent.getMetadata(PlayerID, "garrisonType")) + /** TODO should add the units garrisoned inside garrisoned units */ + numberOfGarrisonedSlots(holder) { - case GarrisonManager.TYPE_FORCE: // force the ungarrisoning - return false; - case GarrisonManager.TYPE_TRADE: // trader garrisoned in ship - return true; - case GarrisonManager.TYPE_PROTECTION: // hurt unit for healing or infantry for defense + if (!this.holders.has(holder.id())) + return holder.garrisonedSlots(); + + return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length; + } + + allowMelee(holder) { - if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) - return true; - const capture = ent.capturePoints(); - if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8) - return true; - if (ent.hasClasses(holder.getGarrisonArrowClasses())) + if (!this.holders.has(holder.id())) + return undefined; + + return this.holders.get(holder.id()).allowMelee; + } + + /** This is just a pre-garrison state, while the entity walk to the garrison holder */ + garrison(gameState, ent, holder, type) + { + if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison()) + return; + + this.registerHolder(gameState, holder); + this.holders.get(holder.id()).list.push(ent.id()); + + if (gameState.ai.Config.debug > 2) { - if (around.unit || around.defenseStructure) + 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")); + } + + if (ent.getMetadata(PlayerID, "plan") !== undefined) + ent.setMetadata(PlayerID, "plan", -2); + else + ent.setMetadata(PlayerID, "plan", -3); + ent.setMetadata(PlayerID, "subrole", Worker.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) + This function is for internal use inside garrisonManager. From outside, you should also update + the holder and then using cancelGarrison should be the preferred solution + */ + leaveGarrison(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); + } + + /** Cancel a pre-garrison state */ + cancelGarrison(ent) + { + ent.stopMoving(); + this.leaveGarrison(ent); + const holderId = ent.getMetadata(PlayerID, "garrisonHolder"); + if (!holderId || !this.holders.has(holderId)) + return; + const list = this.holders.get(holderId).list; + const index = list.indexOf(ent.id()); + if (index !== -1) + list.splice(index, 1); + } + + keepGarrisoned(ent, holder, around) + { + switch (ent.getMetadata(PlayerID, "garrisonType")) + { + case GarrisonManager.TYPE_FORCE: // force the ungarrisoning + return false; + case GarrisonManager.TYPE_TRADE: // trader garrisoned in ship + return true; + case GarrisonManager.TYPE_PROTECTION: // hurt unit for healing or infantry for defense + { + if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) return true; - if (around.meleeSiege || around.rangeSiege) - return ent.attackTypes().indexOf("Melee") === -1 || ent.healthLevel() < this.Config.garrisonHealthLevel.low; - return false; + const capture = ent.capturePoints(); + if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8) + return true; + if (ent.hasClasses(holder.getGarrisonArrowClasses())) + { + if (around.unit || around.defenseStructure) + return true; + if (around.meleeSiege || around.rangeSiege) + return ent.attackTypes().indexOf("Melee") === -1 || ent.healthLevel() < this.Config.garrisonHealthLevel.low; + return false; + } + if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1) + return false; + if (around.unit) + return ent.hasClass("Support") || isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above + if (isSiegeUnit(ent)) + return around.meleeSiege; + return holder.buffHeal() && ent.needsHeal(); } - if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1) + case GarrisonManager.TYPE_DECAY: + return ent.captureStrength() && this.decayingStructures.has(holder.id()); + case GarrisonManager.TYPE_EMERGENCY: // f.e. hero in regicide mode + if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) + return true; + if (around.unit || around.defenseStructure || around.meleeSiege || + around.rangeSiege && ent.healthLevel() < this.Config.garrisonHealthLevel.high) + return true; + return holder.buffHeal() && ent.needsHeal(); + default: + if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager + return true; + aiWarn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + + " for " + ent.genericName() + " id " + ent.id() + " inside " + holder.genericName() + + " id " + holder.id()); + ent.setMetadata(PlayerID, "garrisonType", GarrisonManager.TYPE_PROTECTION); + return true; + } + } + + /** Add this holder in the list managed by the garrisonManager */ + registerHolder(gameState, holder) + { + if (this.holders.has(holder.id())) // already registered + return; + this.holders.set(holder.id(), { "list": [], "allowMelee": true }); + 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) + */ + addDecayingStructure(gameState, entId, justCaptured) + { + if (this.decayingStructures.has(entId)) + return true; + const ent = gameState.getEntityById(entId); + if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire()) return false; - if (around.unit) - return ent.hasClass("Support") || isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above - if (isSiegeUnit(ent)) - return around.meleeSiege; - return holder.buffHeal() && ent.needsHeal(); - } - case GarrisonManager.TYPE_DECAY: - return ent.captureStrength() && this.decayingStructures.has(holder.id()); - case GarrisonManager.TYPE_EMERGENCY: // f.e. hero in regicide mode - if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) - return true; - if (around.unit || around.defenseStructure || around.meleeSiege || - around.rangeSiege && ent.healthLevel() < this.Config.garrisonHealthLevel.high) - return true; - return holder.buffHeal() && ent.needsHeal(); - default: - if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager - return true; - aiWarn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + - " for " + ent.genericName() + " id " + ent.id() + " inside " + holder.genericName() + - " id " + holder.id()); - ent.setMetadata(PlayerID, "garrisonType", GarrisonManager.TYPE_PROTECTION); + if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) + return false; + const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); + this.decayingStructures.set(entId, gmin); return true; } -}; -/** Add this holder in the list managed by the garrisonManager */ -GarrisonManager.prototype.registerHolder = function(gameState, holder) -{ - if (this.holders.has(holder.id())) // already registered - return; - this.holders.set(holder.id(), { "list": [], "allowMelee": true }); - holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); -}; + removeDecayingStructure(entId) + { + if (!this.decayingStructures.has(entId)) + return; + this.decayingStructures.delete(entId); + } -/** - * 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) - */ -GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) -{ - if (this.decayingStructures.has(entId)) - return true; - const ent = gameState.getEntityById(entId); - if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire()) - return false; - if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) - return false; - const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(entId, gmin); - return true; -}; + Serialize() + { + return { "holders": this.holders, "decayingStructures": this.decayingStructures }; + } -GarrisonManager.prototype.removeDecayingStructure = function(entId) -{ - if (!this.decayingStructures.has(entId)) - return; - this.decayingStructures.delete(entId); -}; - -GarrisonManager.prototype.Serialize = function() -{ - return { "holders": this.holders, "decayingStructures": this.decayingStructures }; -}; - -GarrisonManager.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; -}; + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/headquarters.js b/binaries/data/mods/public/simulation/ai/petra/headquarters.js index 211ce66194..c8661eb8fe 100644 --- a/binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ b/binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -39,2417 +39,2420 @@ import { Worker } from "simulation/ai/petra/worker.js"; * -picking new CC locations. */ -export function Headquarters(config, deserialized) +export class Headquarters { - this.Config = config; - this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i - - // Cache various quantities. - this.turnCache = {}; - this.lastFailedGather = {}; - - this.firstBaseConfig = false; - - // Workers configuration. - this.targetNumWorkers = this.Config.Economy.targetNumWorkers; - this.supportRatio = this.Config.Economy.supportRatio; - - this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime. - this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase). - this.towerLapseTime = this.Config.Military.towerLapseTime; - this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase). - 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.basesManager = new BasesManager(this.Config); - this.attackManager = new AttackManager(this.Config); - this.buildManager = new BuildManager(); - this.defenseManager = new DefenseManager(this.Config); - this.tradeManager = new TradeManager(this.Config); - this.navalManager = new NavalManager(this.Config); - this.researchManager = new ResearchManager(this.Config); - this.diplomacyManager = new DiplomacyManager(this.Config, deserialized); - this.garrisonManager = new GarrisonManager(this.Config); - this.victoryManager = new VictoryManager(this.Config); - this.emergencyManager = new EmergencyManager(this.Config); - - this.capturableTargets = new Map(); - this.capturableTargetsTime = 0; -} - -/** More initialisation for stuff that needs the gameState */ -Headquarters.prototype.init = function(gameState, queues) -{ - this.territoryMap = createTerritoryMap(gameState); - // create borderMap: flag cells on the border of the map - // then this map will be completed with our frontier in updateTerritories - this.borderMap = createBorderMap(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(ent => ent.isTreasure()); - this.treasures.registerUpdates(); - this.currentPhase = gameState.currentPhase(); - this.decayingStructures = new Set(); - this.emergencyManager.init(gameState); -}; - -/** - * initialization needed after deserialization (only called when deserialization) - */ -Headquarters.prototype.postinit = function(gameState) -{ - this.basesManager.postinit(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 - */ -Headquarters.prototype.getSeaBetweenIndices = function(gameState, index1, index2) -{ - const 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) + constructor(config, deserialized) { - aiWarn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); - aiWarn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); - aiWarn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); - } - return undefined; -}; + this.Config = config; + this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i -Headquarters.prototype.checkEvents = function(gameState, events) -{ - this.buildManager.checkEvents(gameState, events); - this.updateTerritories(gameState); + // Cache various quantities. + this.turnCache = {}; + this.lastFailedGather = {}; - for (const evt of events.DiplomacyChanged) - { - if (evt.player != PlayerID && evt.otherPlayer != PlayerID) - continue; - // Reset the entities collections which depend on diplomacy - gameState.resetOnDiplomacyChanged(); - break; + this.firstBaseConfig = false; + + // Workers configuration. + this.targetNumWorkers = this.Config.Economy.targetNumWorkers; + this.supportRatio = this.Config.Economy.supportRatio; + + this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime. + this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase). + this.towerLapseTime = this.Config.Military.towerLapseTime; + this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase). + 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.basesManager = new BasesManager(this.Config); + this.attackManager = new AttackManager(this.Config); + this.buildManager = new BuildManager(); + this.defenseManager = new DefenseManager(this.Config); + this.tradeManager = new TradeManager(this.Config); + this.navalManager = new NavalManager(this.Config); + this.researchManager = new ResearchManager(this.Config); + this.diplomacyManager = new DiplomacyManager(this.Config, deserialized); + this.garrisonManager = new GarrisonManager(this.Config); + this.victoryManager = new VictoryManager(this.Config); + this.emergencyManager = new EmergencyManager(this.Config); + + this.capturableTargets = new Map(); + this.capturableTargetsTime = 0; } - this.basesManager.checkEvents(gameState, events); - - for (const evt of events.ConstructionFinished) + /** More initialisation for stuff that needs the gameState */ + init(gameState, queues) { - if (evt.newentity == evt.entity) // repaired building - continue; - const ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID) - continue; - if (ent.hasClass("Market") && this.maxFields) - this.maxFields = false; + this.territoryMap = createTerritoryMap(gameState); + // create borderMap: flag cells on the border of the map + // then this map will be completed with our frontier in updateTerritories + this.borderMap = createBorderMap(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(ent => ent.isTreasure()); + this.treasures.registerUpdates(); + this.currentPhase = gameState.currentPhase(); + this.decayingStructures = new Set(); + this.emergencyManager.init(gameState); } - for (const evt of events.OwnershipChanged) // capture events + /** + * initialization needed after deserialization (only called when deserialization) + */ + postinit(gameState) { - if (evt.to != PlayerID) - continue; - const ent = gameState.getEntityById(evt.entity); - if (!ent) - continue; - if (!ent.hasClass("Unit")) + this.basesManager.postinit(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 + */ + getSeaBetweenIndices(gameState, index1, index2) + { + const 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) { - if (ent.decaying()) + aiWarn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); + aiWarn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); + aiWarn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); + } + return undefined; + } + + checkEvents(gameState, events) + { + this.buildManager.checkEvents(gameState, events); + this.updateTerritories(gameState); + + for (const evt of events.DiplomacyChanged) + { + if (evt.player != PlayerID && evt.otherPlayer != PlayerID) + continue; + // Reset the entities collections which depend on diplomacy + gameState.resetOnDiplomacyChanged(); + break; + } + + this.basesManager.checkEvents(gameState, events); + + for (const evt of events.ConstructionFinished) + { + if (evt.newentity == evt.entity) // repaired building + continue; + const ent = gameState.getEntityById(evt.newentity); + if (!ent || ent.owner() != PlayerID) + continue; + if (ent.hasClass("Market") && this.maxFields) + this.maxFields = false; + } + + for (const evt of events.OwnershipChanged) // capture events + { + if (evt.to != PlayerID) + continue; + const ent = gameState.getEntityById(evt.entity); + if (!ent) + continue; + if (!ent.hasClass("Unit")) { - if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) + 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); + } + continue; + } + + 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", Worker.ROLE_TRADER); + ent.setMetadata(PlayerID, "route", undefined); + } + if (ent.hasClass("Worker")) + { + ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + } + if (ent.hasClass("Ship")) + setSeaAccess(gameState, ent); + if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) + ent.setMetadata(PlayerID, "plan", -1); + } + + // 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 (const evt of events.TrainingStarted) + { + const ent = gameState.getEntityById(evt.entity); + if (!ent || !ent.isOwn(PlayerID)) + continue; + + if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) + continue; + const metadata = ent._entity.trainingQueue[0].metadata; + if (metadata && metadata.garrisonType) + ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison + else + ent.unsetRallyPoint(); + } + + for (const evt of events.TrainingFinished) + { + for (const entId of evt.entities) + { + const 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 + const holder = gameState.getEntityById(ent.garrisonHolderID()); + 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) + const plan = ent.getMetadata(PlayerID, "plan"); + if (plan !== undefined && plan >= 0) + { + const attack = this.attackManager.getPlan(plan); + if (!attack || attack.state !== AttackPlan.STATE_UNEXECUTED) + ent.setMetadata(PlayerID, "plan", -1); + } + } + } + + for (const evt of events.TerritoryDecayChanged) + { + const 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); } - continue; + else if (ent.isGarrisonHolder()) + this.garrisonManager.removeDecayingStructure(evt.entity); } - 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", Worker.ROLE_TRADER); - ent.setMetadata(PlayerID, "route", undefined); - } - if (ent.hasClass("Worker")) - { - ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER); - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - } - if (ent.hasClass("Ship")) - setSeaAccess(gameState, ent); - if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) - ent.setMetadata(PlayerID, "plan", -1); - } - - // 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 (const evt of events.TrainingStarted) - { - const ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) - continue; - const metadata = ent._entity.trainingQueue[0].metadata; - if (metadata && metadata.garrisonType) - ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison - else - ent.unsetRallyPoint(); - } - - for (const evt of events.TrainingFinished) - { - for (const entId of evt.entities) + // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) + if (this.Config.difficulty < difficulty.EASY) + return; + for (const entId of this.decayingStructures) { const ent = gameState.getEntityById(entId); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent.position()) + if (ent && ent.decaying() && ent.isOwn(PlayerID)) { - // we are autogarrisoned, check that the holder is registered in the garrisonManager - const holder = gameState.getEntityById(ent.garrisonHolderID()); - 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) - const plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - const attack = this.attackManager.getPlan(plan); - if (!attack || attack.state !== AttackPlan.STATE_UNEXECUTED) - ent.setMetadata(PlayerID, "plan", -1); - } - } - } - - for (const evt of events.TerritoryDecayChanged) - { - const 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: destroy them if being lost to enemy (except in easier difficulties) - if (this.Config.difficulty < difficulty.EASY) - return; - for (const entId of this.decayingStructures) - { - const ent = gameState.getEntityById(entId); - if (ent && ent.decaying() && ent.isOwn(PlayerID)) - { - const capture = ent.capturePoints(); - if (!capture) - continue; - const 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]) + const capture = ent.capturePoints(); + if (!capture) continue; - decayToGaia = false; - break; - } - if (decayToGaia) - continue; - let ratioMax = 0.7 + randFloat(0, 0.1); - for (const evt of events.Attacked) - { - if (ent.id() != evt.target) + const captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); + if (captureRatio < 0.50) continue; - ratioMax = 0.85 + randFloat(0, 0.1); - break; + 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.7 + randFloat(0, 0.1); + for (const evt of events.Attacked) + { + if (ent.id() != evt.target) + continue; + ratioMax = 0.85 + randFloat(0, 0.1); + break; + } + if (captureRatio > ratioMax) + continue; + ent.destroy(); } - if (captureRatio > ratioMax) - continue; - ent.destroy(); + this.decayingStructures.delete(entId); } - this.decayingStructures.delete(entId); } -}; -Headquarters.prototype.handleNewBase = function(gameState) -{ - if (!this.firstBaseConfig) - // This is our first base, let us configure our starting resources. - configFirstBase(this, gameState); - else + handleNewBase(gameState) { - // Let us hope this new base will fix our possible resource shortage. - this.saveResources = undefined; - this.saveSpace = undefined; - this.maxFields = false; - } -}; - -/** Ensure that all requirements are met when phasing up*/ -Headquarters.prototype.checkPhaseRequirements = function(gameState, queues) -{ - if (gameState.getNumberOfPhases() == this.currentPhase) - return; - - const requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); - let plan; - let queue; - for (const entityReq of requirements) - { - // Village requirements are met elsewhere by constructing more houses - if (entityReq.class == "Village" || entityReq.class == "NotField") - continue; - if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) - continue; - switch (entityReq.class) + if (!this.firstBaseConfig) + // This is our first base, let us configure our starting resources. + configFirstBase(this, gameState); + else { - case "Town": - if (!queues.economicBuilding.hasQueuedUnits() && - !queues.militaryBuilding.hasQueuedUnits()) - { - if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}/market")) - { - plan = new ConstructionPlan(gameState, "structures/{civ}/market", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}/temple")) - { - plan = new ConstructionPlan(gameState, "structures/{civ}/temple", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Forge", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}/forge")) - { - plan = new ConstructionPlan(gameState, "structures/{civ}/forge", { "phaseUp": true }); - queue = "militaryBuilding"; - break; - } - } - break; - default: - // All classes not dealt with inside vanilla game. - // We put them for the time being on the economic queue, except if wonder - queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; - if (!queues[queue].hasQueuedUnits()) - { - const structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); - if (structure && this.canBuild(gameState, structure)) - plan = new ConstructionPlan(gameState, structure, { "phaseUp": true }); - } + // Let us hope this new base will fix our possible resource shortage. + this.saveResources = undefined; + this.saveSpace = undefined; + this.maxFields = false; } + } - if (plan) - { - if (queue == "wonder") - { - gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true }); - plan.queueToReset = "majorTech"; - } - else - { - gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true }); - plan.queueToReset = queue; - } - queues[queue].addPlan(plan); + /** Ensure that all requirements are met when phasing up*/ + checkPhaseRequirements(gameState, queues) + { + if (gameState.getNumberOfPhases() == this.currentPhase) return; - } - } -}; -/** Called by any "phase" research plan once it's started */ -Headquarters.prototype.OnPhaseUp = function(gameState, phase) -{ -}; - -/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ -Headquarters.prototype.trainMoreWorkers = function(gameState, queues) -{ - // default template - const requirementsDef = [ ["costsResource", 1, "food"] ]; - const classesDef = ["Support+Worker"]; - const 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(ent => - { - if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER && ent.getMetadata(PlayerID, "plan") === undefined) + const requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); + let plan; + let queue; + for (const entityReq of requirements) { - ++numberOfWorkers; - if (ent.hasClass("Support")) - ++numberOfSupports; - } - }); - let numberInTraining = 0; - gameState.getOwnTrainingFacilities().forEach(function(ent) - { - for (const item of ent.trainingQueue()) - { - numberInTraining += item.count; - if (item.metadata && item.metadata.role && item.metadata.role === Worker.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) - const 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); - } - - const numberOfQueuedSupports = queues.villager.countQueuedUnits(); - const numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); - const numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; - const numberTotal = numberOfWorkers + numberQueued; - - if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) - return; - if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && - this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) - return; - if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) - return; - - // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers - // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. - // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) - // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers - // and gives a ratio alfa at startup. - - let supportRatio = this.supportRatio; - let alpha = 0.85; - if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field"))) - supportRatio = Math.min(this.supportRatio, 0.1); - if (this.attackManager.rushNumber < this.attackManager.maxRushes || - this.attackManager.upcomingAttacks[AttackPlan.TYPE_RUSH].length) - { - alpha = 0.7; - } - if (gameState.isCeasefireActive()) - alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; - const supportMax = supportRatio * this.targetNumWorkers; - const supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); - - let template; - if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum) - { - let requirements; - if (numberTotal < 45) - requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ]; - else - requirements = [ ["strength", 1] ]; - - const classes = [["CitizenSoldier", "Infantry"]]; - // We want at least 33% ranged and 33% melee. - classes[0].push(pickRandom(["Ranged", "Melee", "Infantry"])); - - template = this.findBestTrainableUnit(gameState, classes, requirements); - } - - // If the template variable is empty, the default unit (Support unit) will be used - // base "0" means automatic choice of base - if (!template && templateDef) - queues.villager.addPlan(new TrainingPlan(gameState, templateDef, { "role": Worker.ROLE_WORKER, "base": 0, "support": true }, size, size)); - else if (template) - queues.citizenSoldier.addPlan(new TrainingPlan(gameState, template, { "role": Worker.ROLE_WORKER, "base": 0 }, size, size)); -}; - -/** picks the best template based on parameters and classes */ -Headquarters.prototype.findBestTrainableUnit = function(gameState, classes, requirements) -{ - let units; - if (classes.indexOf("Hero") != -1) - units = gameState.findTrainableUnits(classes, []); - // We do not want siege tower as AI does not know how to use it nor hero when not explicitely specified. - else - units = gameState.findTrainableUnits(classes, ["Hero", "SiegeTower"]); - - if (!units.length) - return undefined; - - const parameters = requirements.slice(); - const remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory - const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources - for (const type in remainingResources) - { - if (availableResources[type] > 800) - continue; - if (remainingResources[type] > 800) - continue; - const costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; - let toAdd = true; - for (const param of parameters) - { - if (param[0] != "costsResource" || param[2] != type) + // Village requirements are met elsewhere by constructing more houses + if (entityReq.class == "Village" || entityReq.class == "NotField") continue; - param[1] = Math.min(param[1], costsResource); - toAdd = false; - break; - } - if (toAdd) - parameters.push(["costsResource", costsResource, type]); - } - - units.sort((a, b) => - { - const aCost = 1 + a[1].costSum(); - const bCost = 1 + b[1].costSum(); - let aValue = 0.1; - let bValue = 0.1; - for (const param of parameters) - { - if (param[0] == "strength") + if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) + continue; + switch (entityReq.class) { - aValue += getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; - bValue += getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; - } - else if (param[0] == "siegeStrength") - { - aValue += getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; - bValue += getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; - } - else if (param[0] == "speed") - { - aValue += a[1].walkSpeed() * param[1]; - bValue += b[1].walkSpeed() * param[1]; - } - else if (param[0] == "costsResource") - { - // requires a third parameter which is the resource - if (a[1].cost()[param[2]]) - aValue *= param[1]; - if (b[1].cost()[param[2]]) - bValue *= param[1]; - } - else if (param[0] == "canGather") - { - // checking against wood, could be anything else really. - if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) - aValue *= param[1]; - if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) - bValue *= param[1]; - } - else - aiWarn(" trainMoreUnits avec non prevu " + uneval(param)); - } - return -aValue/aCost + bValue/bCost; - }); - return units[0][0]; -}; - -/** - * returns an entity collection of workers through BaseManager.pickBuilders - * TODO: when same accessIndex, sort by distance - */ -Headquarters.prototype.bulkPickWorkers = function(gameState, baseRef, number) -{ - return this.basesManager.bulkPickWorkers(gameState, baseRef, number); -}; - -Headquarters.prototype.getTotalResourceLevel = function(gameState, resources, proximity) -{ - return this.basesManager.getTotalResourceLevel(gameState, resources, proximity); -}; - -/** - * 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. - */ -Headquarters.prototype.GetCurrentGatherRates = function(gameState) -{ - return this.basesManager.GetCurrentGatherRates(gameState); -}; - -/** - * Returns the wanted gather rate. - */ -Headquarters.prototype.GetWantedGatherRates = function(gameState) -{ - if (!this.turnCache.wantedRates) - this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); - - return this.turnCache.wantedRates; -}; - -/** - * 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. - */ -Headquarters.prototype.pickMostNeededResources = function(gameState, allowedResources = []) -{ - const wantedRates = this.GetWantedGatherRates(gameState); - const currentRates = this.GetCurrentGatherRates(gameState); - if (!allowedResources.length) - allowedResources = Resources.GetCodes(); - - const needed = []; - for (const res of allowedResources) - needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] }); - - needed.sort((a, b) => - { - if (a.current < a.wanted && b.current < b.wanted) - { - if (a.current && b.current) - return b.wanted / b.current - a.wanted / a.current; - if (a.current) - return 1; - if (b.current) - return -1; - return b.wanted - a.wanted; - } - if (a.current < a.wanted || a.wanted && !b.wanted) - return -1; - if (b.current < b.wanted || b.wanted && !a.wanted) - return 1; - return a.current - a.wanted - b.current + b.wanted; - }); - return needed; -}; - -/** - * Returns the best position to build a new Civil Center - * Whose primary function would be to reach new resources of type "resource". - */ -Headquarters.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) -{ - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then look for a good spot. - - Engine.ProfileStart("findEconomicCCLocation"); - - // obstruction map - const obstructions = 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"); - - const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); - const dpEnts = gameState.getOwnDropsites().filter( - filters.not(filters.byClasses(["CivCentre", "Unit"]))); - const ccList = []; - for (const cc of ccEnts.values()) - ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); - const dpList = []; - for (const dp of dpEnts.values()) - dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) }); - - let bestIdx; - let bestVal; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - let scale = 250 * 250; - let proxyAccess; - const nbShips = this.navalManager.transportShips.length; - if (proximity) // this is our first base - { - // if our first base, ensure room around - radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); - // scale is the typical scale at which we want to find a location for our first base - // look for bigger scale if we start from a ship (access < 2) or from a small island - const cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; - proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); - if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) - scale = 400 * 400; - } - - const width = this.territoryMap.width; - const cellSize = this.territoryMap.cellSize; - - // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). - const reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; - const nearbyRejected = Math.square(120); // Reject if too near from any cc - const nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc - const nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc - const maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc - const maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc - const maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc - - let cut = 60; - if (fromStrategic || proximity) // be less restrictive - cut = 30; - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.territoryMap.getOwnerIndex(j) != 0) - continue; - // With enough room around to build the cc - const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // We require that it is accessible - const 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 - const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // We will be more tolerant for cc around our oversea docks - let oversea = false; - - if (proximity) // This is our first cc, let's do it near our units - norm /= 1 + SquareVectorDistance(proximity, pos) / scale; - else - { - let minDist = Math.min(); - let accessible = false; - - for (const cc of ccList) - { - const dist = SquareVectorDistance(cc.pos, pos); - if (dist < nearbyRejected) + case "Town": + if (!queues.economicBuilding.hasQueuedUnits() && + !queues.militaryBuilding.hasQueuedUnits()) { - norm = 0; - break; + if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && + this.canBuild(gameState, "structures/{civ}/market")) + { + plan = new ConstructionPlan(gameState, "structures/{civ}/market", { "phaseUp": true }); + queue = "economicBuilding"; + break; + } + if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && + this.canBuild(gameState, "structures/{civ}/temple")) + { + plan = new ConstructionPlan(gameState, "structures/{civ}/temple", { "phaseUp": true }); + queue = "economicBuilding"; + break; + } + if (!gameState.getOwnEntitiesByClass("Forge", true).hasEntities() && + this.canBuild(gameState, "structures/{civ}/forge")) + { + plan = new ConstructionPlan(gameState, "structures/{civ}/forge", { "phaseUp": true }); + queue = "militaryBuilding"; + break; + } } - if (!cc.ally) - continue; - if (dist < nearbyAllyRejected) + break; + default: + // All classes not dealt with inside vanilla game. + // We put them for the time being on the economic queue, except if wonder + queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; + if (!queues[queue].hasQueuedUnits()) { - norm = 0; - break; + const structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); + if (structure && this.canBuild(gameState, structure)) + plan = new ConstructionPlan(gameState, structure, { "phaseUp": true }); } - if (dist < nearbyAllyDisfavored) - norm *= 0.5; - - if (dist < minDist) - minDist = dist; - accessible = accessible || index == getLandAccess(gameState, cc.ent); } - if (norm == 0) - continue; - if (accessible && minDist > maxAccessRejected) - continue; - - if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc + if (plan) { - if (!accessible) + if (queue == "wonder") { - if (minDist > maxNoAccessDisfavored) - norm *= 0.5; - else - norm *= 0.8; + gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true }); + plan.queueToReset = "majorTech"; } else - norm *= 0.5; - } - - // Not near any of our dropsite, except for oversea docks - oversea = !accessible && dpList.some(dp => getLandAccess(gameState, dp.ent) == index); - if (!oversea) - { - for (const dp of dpList) { - const dist = SquareVectorDistance(dp.pos, pos); - if (dist < 3600) + gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true }); + plan.queueToReset = queue; + } + queues[queue].addPlan(plan); + return; + } + } + } + + /** Called by any "phase" research plan once it's started */ + OnPhaseUp(gameState, phase) + { + } + + /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ + trainMoreWorkers(gameState, queues) + { + // default template + const requirementsDef = [ ["costsResource", 1, "food"] ]; + const classesDef = ["Support+Worker"]; + const 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(ent => + { + if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER && ent.getMetadata(PlayerID, "plan") === undefined) + { + ++numberOfWorkers; + if (ent.hasClass("Support")) + ++numberOfSupports; + } + }); + let numberInTraining = 0; + gameState.getOwnTrainingFacilities().forEach(function(ent) + { + for (const item of ent.trainingQueue()) + { + numberInTraining += item.count; + if (item.metadata && item.metadata.role && item.metadata.role === Worker.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) + const 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); + } + + const numberOfQueuedSupports = queues.villager.countQueuedUnits(); + const numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); + const numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; + const numberTotal = numberOfWorkers + numberQueued; + + if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) + return; + if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && + this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) + return; + if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) + return; + + // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers + // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. + // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) + // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers + // and gives a ratio alfa at startup. + + let supportRatio = this.supportRatio; + let alpha = 0.85; + if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field"))) + supportRatio = Math.min(this.supportRatio, 0.1); + if (this.attackManager.rushNumber < this.attackManager.maxRushes || + this.attackManager.upcomingAttacks[AttackPlan.TYPE_RUSH].length) + { + alpha = 0.7; + } + if (gameState.isCeasefireActive()) + alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; + const supportMax = supportRatio * this.targetNumWorkers; + const supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); + + let template; + if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum) + { + let requirements; + if (numberTotal < 45) + requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ]; + else + requirements = [ ["strength", 1] ]; + + const classes = [["CitizenSoldier", "Infantry"]]; + // We want at least 33% ranged and 33% melee. + classes[0].push(pickRandom(["Ranged", "Melee", "Infantry"])); + + template = this.findBestTrainableUnit(gameState, classes, requirements); + } + + // If the template variable is empty, the default unit (Support unit) will be used + // base "0" means automatic choice of base + if (!template && templateDef) + queues.villager.addPlan(new TrainingPlan(gameState, templateDef, { "role": Worker.ROLE_WORKER, "base": 0, "support": true }, size, size)); + else if (template) + queues.citizenSoldier.addPlan(new TrainingPlan(gameState, template, { "role": Worker.ROLE_WORKER, "base": 0 }, size, size)); + } + + /** picks the best template based on parameters and classes */ + findBestTrainableUnit(gameState, classes, requirements) + { + let units; + if (classes.indexOf("Hero") != -1) + units = gameState.findTrainableUnits(classes, []); + // We do not want siege tower as AI does not know how to use it nor hero when not explicitely specified. + else + units = gameState.findTrainableUnits(classes, ["Hero", "SiegeTower"]); + + if (!units.length) + return undefined; + + const parameters = requirements.slice(); + const remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory + const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources + for (const type in remainingResources) + { + if (availableResources[type] > 800) + continue; + if (remainingResources[type] > 800) + continue; + const costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; + let toAdd = true; + for (const 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((a, b) => + { + const aCost = 1 + a[1].costSum(); + const bCost = 1 + b[1].costSum(); + let aValue = 0.1; + let bValue = 0.1; + for (const param of parameters) + { + if (param[0] == "strength") + { + aValue += getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; + bValue += getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; + } + else if (param[0] == "siegeStrength") + { + aValue += getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; + bValue += getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; + } + else if (param[0] == "speed") + { + aValue += a[1].walkSpeed() * param[1]; + bValue += b[1].walkSpeed() * param[1]; + } + else if (param[0] == "costsResource") + { + // requires a third parameter which is the resource + if (a[1].cost()[param[2]]) + aValue *= param[1]; + if (b[1].cost()[param[2]]) + bValue *= param[1]; + } + else if (param[0] == "canGather") + { + // checking against wood, could be anything else really. + if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) + aValue *= param[1]; + if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) + bValue *= param[1]; + } + else + aiWarn(" trainMoreUnits avec non prevu " + uneval(param)); + } + return -aValue/aCost + bValue/bCost; + }); + return units[0][0]; + } + + /** + * returns an entity collection of workers through BaseManager.pickBuilders + * TODO: when same accessIndex, sort by distance + */ + bulkPickWorkers(gameState, baseRef, number) + { + return this.basesManager.bulkPickWorkers(gameState, baseRef, number); + } + + getTotalResourceLevel(gameState, resources, proximity) + { + return this.basesManager.getTotalResourceLevel(gameState, resources, proximity); + } + + /** + * 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. + */ + GetCurrentGatherRates(gameState) + { + return this.basesManager.GetCurrentGatherRates(gameState); + } + + /** + * Returns the wanted gather rate. + */ + GetWantedGatherRates(gameState) + { + if (!this.turnCache.wantedRates) + this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); + + return this.turnCache.wantedRates; + } + + /** + * 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. + */ + pickMostNeededResources(gameState, allowedResources = []) + { + const wantedRates = this.GetWantedGatherRates(gameState); + const currentRates = this.GetCurrentGatherRates(gameState); + if (!allowedResources.length) + allowedResources = Resources.GetCodes(); + + const needed = []; + for (const res of allowedResources) + needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] }); + + needed.sort((a, b) => + { + if (a.current < a.wanted && b.current < b.wanted) + { + if (a.current && b.current) + return b.wanted / b.current - a.wanted / a.current; + if (a.current) + return 1; + if (b.current) + return -1; + return b.wanted - a.wanted; + } + if (a.current < a.wanted || a.wanted && !b.wanted) + return -1; + if (b.current < b.wanted || b.wanted && !a.wanted) + return 1; + return a.current - a.wanted - b.current + b.wanted; + }); + return needed; + } + + /** + * Returns the best position to build a new Civil Center + * Whose primary function would be to reach new resources of type "resource". + */ + findEconomicCCLocation(gameState, template, resource, proximity, fromStrategic) + { + // This builds a map. The procedure is fairly simple. It adds the resource maps + // (which are dynamically updated and are made so that they will facilitate DP placement) + // Then look for a good spot. + + Engine.ProfileStart("findEconomicCCLocation"); + + // obstruction map + const obstructions = 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"); + + const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); + const dpEnts = gameState.getOwnDropsites().filter( + filters.not(filters.byClasses(["CivCentre", "Unit"]))); + const ccList = []; + for (const cc of ccEnts.values()) + ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); + const dpList = []; + for (const dp of dpEnts.values()) + dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) }); + + let bestIdx; + let bestVal; + let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); + let scale = 250 * 250; + let proxyAccess; + const nbShips = this.navalManager.transportShips.length; + if (proximity) // this is our first base + { + // if our first base, ensure room around + radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); + // scale is the typical scale at which we want to find a location for our first base + // look for bigger scale if we start from a ship (access < 2) or from a small island + const cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; + proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); + if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) + scale = 400 * 400; + } + + const width = this.territoryMap.width; + const cellSize = this.territoryMap.cellSize; + + // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). + const reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; + const nearbyRejected = Math.square(120); // Reject if too near from any cc + const nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc + const nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc + const maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc + const maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc + const maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc + + let cut = 60; + if (fromStrategic || proximity) // be less restrictive + cut = 30; + + for (let j = 0; j < this.territoryMap.length; ++j) + { + if (this.territoryMap.getOwnerIndex(j) != 0) + continue; + // With enough room around to build the cc + const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + // We require that it is accessible + const 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 + const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + // We will be more tolerant for cc around our oversea docks + let oversea = false; + + if (proximity) // This is our first cc, let's do it near our units + norm /= 1 + SquareVectorDistance(proximity, pos) / scale; + else + { + let minDist = Math.min(); + let accessible = false; + + for (const cc of ccList) + { + const dist = SquareVectorDistance(cc.pos, pos); + if (dist < nearbyRejected) { norm = 0; break; } - else if (dist < 6400) + if (!cc.ally) + continue; + if (dist < nearbyAllyRejected) + { + norm = 0; + break; + } + if (dist < nearbyAllyDisfavored) + norm *= 0.5; + + if (dist < minDist) + minDist = dist; + accessible = accessible || index == getLandAccess(gameState, cc.ent); + } + if (norm == 0) + continue; + + if (accessible && minDist > maxAccessRejected) + continue; + + if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc + { + if (!accessible) + { + if (minDist > maxNoAccessDisfavored) + norm *= 0.5; + else + norm *= 0.8; + } + else norm *= 0.5; } - } - if (norm == 0) - continue; - } - if (this.borderMap.map[j] & mapMask.fullBorder) // disfavor the borders of the map - norm *= 0.5; - - let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j]; - for (const res in gameState.sharedScript.resourceMaps) - if (res != "food") - val += gameState.sharedScript.ccResourceMaps[res].map[j]; - val *= norm; - - // If oversea, be just above threshold to be accepted if nothing else - if (oversea) - val = Math.max(val, cut + 0.1); - - if (bestVal !== undefined && val < bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = val; - bestIdx = i; - } - - Engine.ProfileStop(); - - if (bestVal === undefined) - return false; - if (this.Config.debug > 1) - aiWarn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); - // not good enough. - if (bestVal < cut) - return false; - - const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - const indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (const base of this.baseManagers()) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - const sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new Civil Center - * Whose primary function would be to assure territorial continuity with our allies - */ -Headquarters.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. - - const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); - const ccList = []; - let numAllyCC = 0; - for (const cc of ccEnts.values()) - { - const 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 - const obstructions = 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; - const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - const width = this.territoryMap.width; - const cellSize = this.territoryMap.cellSize; - let currentVal, delta; - let distcc0, distcc1, distcc2; - const favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive; - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.territoryMap.getOwnerIndex(j) != 0) - continue; - // with enough room around to build the cc - const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // we require that it is accessible - const index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - - // checking distances to other cc - const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - let minDist = Math.min(); - distcc0 = undefined; - - for (const cc of ccList) - { - const dist = SquareVectorDistance(cc.pos, pos); - if (dist < 14000) // Reject if too near from any cc - { - minDist = 0; - break; - } - if (!cc.ally) - continue; - if (dist < 62000) // Reject if quite near from ally cc - { - minDist = 0; - break; - } - if (dist < minDist) - minDist = dist; - - if (!distcc0 || dist < distcc0) - { - distcc2 = distcc1; - distcc1 = distcc0; - distcc0 = dist; - } - else if (!distcc1 || dist < distcc1) - { - distcc2 = distcc1; - distcc1 = dist; - } - else if (!distcc2 || dist < distcc2) - distcc2 = dist; - } - if (minDist < 1 || minDist > 170000 && !this.navalMap) - continue; - - delta = Math.sqrt(distcc0) - favoredDistance; - currentVal = delta*delta; - delta = Math.sqrt(distcc1) - favoredDistance; - currentVal += delta*delta; - if (distcc2) - { - delta = Math.sqrt(distcc2) - favoredDistance; - currentVal += delta*delta; - } - // disfavor border of the map - if (this.borderMap.map[j] & mapMask.fullBorder) - currentVal += 10000; - - if (bestVal !== undefined && currentVal > bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = currentVal; - bestIdx = i; - } - - if (this.Config.debug > 1) - aiWarn("We've found a strategic base with bestVal = " + bestVal); - - Engine.ProfileStop(); - - if (bestVal === undefined) - return undefined; - - const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - const indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (const base of this.baseManagers()) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - const sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new market: if the allies already have a market, build it as far as possible - * from it, although not in our border to be able to defend it easily. If no allied market, our second market will - * follow the same logic. - * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance - * for performance reasons. - */ -Headquarters.prototype.findMarketLocation = function(gameState, template) -{ - let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", filters.byClass("Trade"), - gameState.getExclusiveAllyEntities()).toEntityArray(); - if (!markets.length) - markets = gameState.updatingCollection("OwnMarkets", filters.byClass("Trade"), 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]; - - // No need for more than one market when we cannot trade. - if (!Resources.GetTradableCodes().length) - return false; - - // obstruction map - const obstructions = createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestJdx; - let bestVal; - let bestDistSq; - let bestGainMult; - const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - const isNavalMarket = template.hasClasses(["Naval+Trade"]); - - const width = this.territoryMap.width; - const cellSize = this.territoryMap.cellSize; - - const traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (let j = 0; j < this.territoryMap.length; ++j) - { - // do not try on the narrow border of our territory - if (this.borderMap.map[j] & mapMask.narrowFrontier) - continue; - if (this.baseAtIndex(j) == 0) // only in our territory - continue; - // with enough room around to build the market - const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - const index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // checking distances to other markets - let maxVal = 0; - let maxDistSq; - let maxGainMult; - let gainMultiplier; - for (const market of markets) - { - if (isNavalMarket && template.hasClasses(["Naval+Trade"])) - { - if (getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) + // Not near any of our dropsite, except for oversea docks + oversea = !accessible && dpList.some(dp => getLandAccess(gameState, dp.ent) == index); + if (!oversea) + { + for (const dp of dpList) + { + const dist = SquareVectorDistance(dp.pos, pos); + if (dist < 3600) + { + norm = 0; + break; + } + else if (dist < 6400) + norm *= 0.5; + } + } + if (norm == 0) continue; - gainMultiplier = traderTemplatesGains.navalGainMultiplier; } - else if (getLandAccess(gameState, market) == index && - !isLineInsideEnemyTerritory(gameState, market.position(), pos)) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else + + if (this.borderMap.map[j] & mapMask.fullBorder) // disfavor the borders of the map + norm *= 0.5; + + let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j]; + for (const res in gameState.sharedScript.resourceMaps) + if (res != "food") + val += gameState.sharedScript.ccResourceMaps[res].map[j]; + val *= norm; + + // If oversea, be just above threshold to be accepted if nothing else + if (oversea) + val = Math.max(val, cut + 0.1); + + if (bestVal !== undefined && val < bestVal) continue; - if (!gainMultiplier) + if (this.isDangerousLocation(gameState, pos, halfSize)) continue; - const distSq = SquareVectorDistance(market.position(), pos); - if (gainMultiplier * distSq > maxVal) - { - maxVal = gainMultiplier * distSq; - maxDistSq = distSq; - maxGainMult = gainMultiplier; - } + bestVal = val; + bestIdx = i; } - if (maxVal == 0) - continue; - if (bestVal !== undefined && maxVal < bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = maxVal; - bestDistSq = maxDistSq; - bestGainMult = maxGainMult; - bestIdx = i; - bestJdx = j; + + Engine.ProfileStop(); + + if (bestVal === undefined) + return false; + if (this.Config.debug > 1) + aiWarn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); + // not good enough. + if (bestVal < cut) + return false; + + const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + + // Define a minimal number of wanted ships in the seas reaching this new base + const indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; + for (const base of this.baseManagers()) + { + if (!base.anchor || base.accessIndex == indexIdx) + continue; + const sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); + if (sea !== undefined) + this.navalManager.setMinimalTransportShips(gameState, sea, 1); + } + + return [x, z]; } - if (this.Config.debug > 1) - aiWarn("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]; - const expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); - if (this.Config.debug > 1) - aiWarn("this would give a trading gain of " + expectedGain); - // Do not keep it if gain is too small, except if this is our first Market. - let idx; - if (expectedGain < this.tradeManager.minimalGain) + /** + * Returns the best position to build a new Civil Center + * Whose primary function would be to assure territorial continuity with our allies + */ + findStrategicCCLocation(gameState, template) { - if (template.hasClass("Market") && - !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) - idx = -1; // Needed by queueplanBuilding manager to keep that Market. + // 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. + + const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); + const ccList = []; + let numAllyCC = 0; + for (const cc of ccEnts.values()) + { + const 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 + const obstructions = 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; + const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); + + const width = this.territoryMap.width; + const cellSize = this.territoryMap.cellSize; + let currentVal, delta; + let distcc0, distcc1, distcc2; + const favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive; + + for (let j = 0; j < this.territoryMap.length; ++j) + { + if (this.territoryMap.getOwnerIndex(j) != 0) + continue; + // with enough room around to build the cc + const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + // we require that it is accessible + const index = gameState.ai.accessibility.landPassMap[i]; + if (!this.landRegions[index]) + continue; + + // checking distances to other cc + const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + let minDist = Math.min(); + distcc0 = undefined; + + for (const cc of ccList) + { + const dist = SquareVectorDistance(cc.pos, pos); + if (dist < 14000) // Reject if too near from any cc + { + minDist = 0; + break; + } + if (!cc.ally) + continue; + if (dist < 62000) // Reject if quite near from ally cc + { + minDist = 0; + break; + } + if (dist < minDist) + minDist = dist; + + if (!distcc0 || dist < distcc0) + { + distcc2 = distcc1; + distcc1 = distcc0; + distcc0 = dist; + } + else if (!distcc1 || dist < distcc1) + { + distcc2 = distcc1; + distcc1 = dist; + } + else if (!distcc2 || dist < distcc2) + distcc2 = dist; + } + if (minDist < 1 || minDist > 170000 && !this.navalMap) + continue; + + delta = Math.sqrt(distcc0) - favoredDistance; + currentVal = delta*delta; + delta = Math.sqrt(distcc1) - favoredDistance; + currentVal += delta*delta; + if (distcc2) + { + delta = Math.sqrt(distcc2) - favoredDistance; + currentVal += delta*delta; + } + // disfavor border of the map + if (this.borderMap.map[j] & mapMask.fullBorder) + currentVal += 10000; + + if (bestVal !== undefined && currentVal > bestVal) + continue; + if (this.isDangerousLocation(gameState, pos, halfSize)) + continue; + bestVal = currentVal; + bestIdx = i; + } + + if (this.Config.debug > 1) + aiWarn("We've found a strategic base with bestVal = " + bestVal); + + Engine.ProfileStop(); + + if (bestVal === undefined) + return undefined; + + const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + + // Define a minimal number of wanted ships in the seas reaching this new base + const indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; + for (const base of this.baseManagers()) + { + if (!base.anchor || base.accessIndex == indexIdx) + continue; + const sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); + if (sea !== undefined) + this.navalManager.setMinimalTransportShips(gameState, sea, 1); + } + + return [x, z]; + } + + /** + * Returns the best position to build a new market: if the allies already have a market, build it as far as possible + * from it, although not in our border to be able to defend it easily. If no allied market, our second market will + * follow the same logic. + * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance + * for performance reasons. + */ + findMarketLocation(gameState, template) + { + let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", filters.byClass("Trade"), + gameState.getExclusiveAllyEntities()).toEntityArray(); + if (!markets.length) + markets = gameState.updatingCollection("OwnMarkets", filters.byClass("Trade"), 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]; + + // No need for more than one market when we cannot trade. + if (!Resources.GetTradableCodes().length) + return false; + + // obstruction map + const obstructions = createObstructionMap(gameState, 0, template); + let halfSize = 0; + if (template.get("Footprint/Square")) + halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; + else if (template.get("Footprint/Circle")) + halfSize = +template.get("Footprint/Circle/@radius"); + + let bestIdx; + let bestJdx; + let bestVal; + let bestDistSq; + let bestGainMult; + const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); + const isNavalMarket = template.hasClasses(["Naval+Trade"]); + + const width = this.territoryMap.width; + const cellSize = this.territoryMap.cellSize; + + const traderTemplatesGains = gameState.getTraderTemplatesGains(); + + for (let j = 0; j < this.territoryMap.length; ++j) + { + // do not try on the narrow border of our territory + if (this.borderMap.map[j] & mapMask.narrowFrontier) + continue; + if (this.baseAtIndex(j) == 0) // only in our territory + continue; + // with enough room around to build the market + const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + const index = gameState.ai.accessibility.landPassMap[i]; + if (!this.landRegions[index]) + continue; + const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + // checking distances to other markets + let maxVal = 0; + let maxDistSq; + let maxGainMult; + let gainMultiplier; + for (const market of markets) + { + if (isNavalMarket && template.hasClasses(["Naval+Trade"])) + { + if (getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) + continue; + gainMultiplier = traderTemplatesGains.navalGainMultiplier; + } + else if (getLandAccess(gameState, market) == index && + !isLineInsideEnemyTerritory(gameState, market.position(), pos)) + gainMultiplier = traderTemplatesGains.landGainMultiplier; + else + continue; + if (!gainMultiplier) + continue; + const distSq = SquareVectorDistance(market.position(), pos); + if (gainMultiplier * distSq > maxVal) + { + maxVal = gainMultiplier * distSq; + maxDistSq = distSq; + maxGainMult = gainMultiplier; + } + } + if (maxVal == 0) + continue; + if (bestVal !== undefined && maxVal < bestVal) + continue; + if (this.isDangerousLocation(gameState, pos, halfSize)) + continue; + bestVal = maxVal; + bestDistSq = maxDistSq; + bestGainMult = maxGainMult; + bestIdx = i; + bestJdx = j; + } + + if (this.Config.debug > 1) + aiWarn("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]; + const expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); + if (this.Config.debug > 1) + aiWarn("this would give a trading gain of " + expectedGain); + // Do not keep it if gain is too small, except if this is our first Market. + let idx; + if (expectedGain < this.tradeManager.minimalGain) + { + if (template.hasClass("Market") && + !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) + idx = -1; // Needed by queueplanBuilding manager to keep that Market. + else + return false; + } + else + idx = this.baseAtIndex(bestJdx); + + const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + return [x, z, idx, expectedGain]; + } + + /** + * Returns the best position to build defensive buildings (fortress and towers) + * Whose primary function is to defend our borders + */ + findDefensiveLocation(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. + + const ownStructures = gameState.getOwnStructures().filter(filters.byClasses(["Fortress", "Tower"])) + .toEntityArray(); + let enemyStructures = gameState.getEnemyStructures().filter(filters.not(filters.byOwner(0))) + .filter(filters.byClasses(["CivCentre", "Fortress", "Tower"])); + if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals + { + enemyStructures = gameState.getNeutralStructures().filter(filters.not(filters.byOwner(0))) + .filter(filters.byClasses(["CivCentre", "Fortress", "Tower"])); + if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) + { + enemyStructures = gameState.getAllyStructures().filter( + filters.not(filters.byOwner(PlayerID))).filter( + filters.byClasses(["CivCentre", "Fortress", "Tower"])); + } + if (!enemyStructures.hasEntities()) + return undefined; + } + enemyStructures = enemyStructures.toEntityArray(); + + let wonderMode = gameState.getVictoryConditions().has("wonder"); + let wonderDistmin; + let wonders; + if (wonderMode) + { + wonders = gameState.getOwnStructures().filter(filters.byClass("Wonder")).toEntityArray(); + wonderMode = wonders.length != 0; + if (wonderMode) + wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); + } + + // obstruction map + const obstructions = 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; + const width = this.territoryMap.width; + const cellSize = this.territoryMap.cellSize; + + const isTower = template.hasClass("Tower"); + const isFortress = template.hasClass("Fortress"); + let radius; + if (isFortress) + radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); + else + radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); + + for (let j = 0; j < this.territoryMap.length; ++j) + { + if (!wonderMode) + { + // do not try if well inside or outside territory + if (!(this.borderMap.map[j] & mapMask.fullFrontier)) + continue; + if (this.borderMap.map[j] & mapMask.largeFrontier && isTower) + continue; + } + if (this.baseAtIndex(j) == 0) // inaccessible cell + continue; + // with enough room around to build the cc + const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + + const 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 = 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 (const str of enemyStructures) + { + if (str.foundationProgress() !== undefined) + continue; + const strPos = str.position(); + if (!strPos) + continue; + const dist = 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; + + const cutDist = 900; // 30×30 TODO maybe increase it + for (const str of ownStructures) + { + const strPos = str.position(); + if (!strPos) + continue; + if (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; + + const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + return [x, z, this.baseAtIndex(bestJdx)]; + } + + buildTemple(gameState, queues) + { + // at least one market (which have the same queue) should be build before any temple + if (queues.economicBuilding.hasQueuedUnits() || + gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || + !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) + return; + // Try to build a temple earlier if in regicide to recruit healer guards + if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide")) + return; + + let templateName = "structures/{civ}/temple"; + if (this.canBuild(gameState, "structures/{civ}/temple_vesta")) + templateName = "structures/{civ}/temple_vesta"; + else if (!this.canBuild(gameState, templateName)) + return; + queues.economicBuilding.addPlan(new ConstructionPlan(gameState, templateName)); + } + + buildMarket(gameState, queues) + { + if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() || + !this.canBuild(gameState, "structures/{civ}/market")) + return; + + if (queues.economicBuilding.hasQueuedUnitsWithClass("Market")) + { + if (!queues.economicBuilding.paused) + { + // Put available resources in this market + const queueManager = gameState.ai.queueManager; + const cost = queues.economicBuilding.plans[0].getCost(); + queueManager.setAccounts(gameState, cost, "economicBuilding"); + if (!queueManager.canAfford("economicBuilding", cost)) + { + for (const q in queueManager.queues) + { + if (q == "economicBuilding") + continue; + queueManager.transferAccounts(cost, q, "economicBuilding"); + if (queueManager.canAfford("economicBuilding", cost)) + break; + } + } + } + return; + } + + gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding); + const plan = new ConstructionPlan(gameState, "structures/{civ}/market"); + plan.queueToReset = "economicBuilding"; + queues.economicBuilding.addPlan(plan); + } + + /** Build a farmstead */ + buildFarmstead(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 ConstructionPlan(gameState, "structures/{civ}/farmstead")); + } + + /** + * Try to build a wonder when required + * force = true when called from the victoryManager in case of Wonder victory condition. + */ + buildWonder(gameState, queues, force = false) + { + if (queues.wonder && queues.wonder.hasQueuedUnits() || + gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || + !this.canBuild(gameState, "structures/{civ}/wonder")) + return; + + if (!force) + { + const template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/wonder")); + // Check that we have enough resources to start thinking to build a wonder + const cost = template.cost(); + const resources = gameState.getResources(); + let highLevel = 0; + let lowLevel = 0; + for (const res in cost) + { + if (resources[res] && resources[res] > 0.7 * cost[res]) + ++highLevel; + else if (!resources[res] || resources[res] < 0.3 * cost[res]) + ++lowLevel; + } + if (highLevel == 0 || lowLevel > 1) + return; + } + + queues.wonder.addPlan(new ConstructionPlan(gameState, "structures/{civ}/wonder")); + } + + /** Build a corral, and train animals there */ + manageCorral(gameState, queues) + { + if (queues.corral.hasQueuedUnits()) + return; + + const nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; + if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")) && + nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral) + { + if (this.canBuild(gameState, "structures/{civ}/corral")) + { + queues.corral.addPlan(new ConstructionPlan(gameState, "structures/{civ}/corral")); + return; + } + if (!nCorral) + return; + } + + // And train some animals + const civ = gameState.getPlayerCiv(); + for (const corral of gameState.getOwnEntitiesByClass("Corral", true).values()) + { + if (corral.foundationProgress() !== undefined) + continue; + const trainables = corral.trainableEntities(civ); + for (const trainable of trainables) + { + if (gameState.isTemplateDisabled(trainable)) + continue; + const template = gameState.getTemplate(trainable); + if (!template || !template.isHuntable()) + continue; + let count = gameState.countEntitiesByType(trainable, true); + for (const item of corral.trainingQueue()) + count += item.count; + if (count > nCorral) + continue; + queues.corral.addPlan(new 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… + */ + buildMoreHouses(gameState, queues) + { + let houseTemplateString = "structures/{civ}/apartment"; + if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) || + !this.canBuild(gameState, houseTemplateString)) + { + houseTemplateString = "structures/{civ}/house"; + if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString))) + return; + } + if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) + return; + + const numPlanned = queues.house.length(); + if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) + { + const plan = new ConstructionPlan(gameState, houseTemplateString); + // change the starting condition according to the situation. + plan.goRequirement = "houseNeeded"; + queues.house.addPlan(plan); + } + + if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) + { + const houseTemplateName = gameState.applyCiv(houseTemplateString); + const houseTemplate = gameState.getTemplate(houseTemplateName); + + let needed = 0; + for (const entityReq of gameState.getPhaseEntityRequirements(this.phasing)) + { + if (!houseTemplate.hasClass(entityReq.class)) + continue; + + const count = gameState.getOwnStructures().filter(filters.byClass(entityReq.class)) + .length; + if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) + { + if (this.Config.debug > 1) + aiWarn("no room to place a house ... try to be less restrictive"); + this.buildManager.setBuildable(houseTemplateName); + this.requireHouses = true; + } + needed = Math.max(needed, entityReq.count - count); + } + + const houseQueue = queues.house.plans; + for (let i = 0; i < numPlanned; ++i) + if (houseQueue[i].isGo(gameState)) + --needed; + else if (needed > 0) + { + houseQueue[i].goRequirement = undefined; + --needed; + } + } + + if (this.requireHouses) + { + const houseTemplate = gameState.getTemplate(gameState.applyCiv(houseTemplateString)); + if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => + !houseTemplate.hasClass(req.class) || + gameState.getOwnStructures().filter(filters.byClass(req.class)).length >= req.count)) + { + this.requireHouses = undefined; + } + } + + // When population limit too tight + // - if no room to build, try to improve with technology + // - otherwise increase temporarily the priority of houses + const house = gameState.applyCiv(houseTemplateString); + const HouseNb = gameState.getOwnFoundations().filter(filters.byClass("House")).length; + const popBonus = gameState.getTemplate(house).getPopulationBonus(); + const freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); + let priority; + if (freeSlots < 5) + { + if (this.buildManager.isUnbuildable(gameState, house)) + { + if (this.Config.debug > 1) + aiWarn("no room to place a house ... try to improve with technology"); + this.researchManager.researchPopulationBonus(gameState, queues); + } + else + priority = 2 * this.Config.priorities.house; + } + else + priority = this.Config.priorities.house; + + if (priority && priority != gameState.ai.queueManager.getPriority("house")) + gameState.ai.queueManager.changePriority("house", priority); + } + + /** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ + checkBaseExpansion(gameState, queues) + { + if (queues.civilCentre.hasQueuedUnits()) + return; + // First build one cc if all have been destroyed + if (!this.hasPotentialBase()) + { + this.buildFirstBase(gameState); + return; + } + // Then expand if we have not enough room available for buildings + if (this.buildManager.numberMissingRoom(gameState) > 1) + { + if (this.Config.debug > 2) + aiWarn("try to build a new base because not enough room to build "); + this.buildNewBase(gameState, queues); + return; + } + // If we've already planned to phase up, wait a bit before trying to expand + if (this.phasing) + return; + // Finally expand if we have lots of units (threshold depending on the aggressivity value) + const activeBases = this.numActiveBases(); + const numUnits = gameState.getOwnUnits().length; + const 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) + { + aiWarn("try to build a new base because of population " + numUnits + " for " + + activeBases + " CCs"); + } + this.buildNewBase(gameState, queues); + } + } + + buildNewBase(gameState, queues, resource) + { + if (this.hasPotentialBase() && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) + return false; + if (gameState.getOwnFoundations().filter(filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) + return false; + + let template; + // We require at least one of this civ civCentre as they may allow specific units or techs + let hasOwnCC = false; + for (const ent of gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")).values()) + { + if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}/civil_centre")) + continue; + hasOwnCC = true; + break; + } + if (hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) + template = "structures/{civ}/military_colony"; + else if (this.canBuild(gameState, "structures/{civ}/civil_centre")) + template = "structures/{civ}/civil_centre"; + else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) + template = "structures/{civ}/military_colony"; else return false; - } - else - idx = this.baseAtIndex(bestJdx); - const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return [x, z, idx, expectedGain]; -}; - -/** - * Returns the best position to build defensive buildings (fortress and towers) - * Whose primary function is to defend our borders - */ -Headquarters.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. - - const ownStructures = gameState.getOwnStructures().filter(filters.byClasses(["Fortress", "Tower"])) - .toEntityArray(); - let enemyStructures = gameState.getEnemyStructures().filter(filters.not(filters.byOwner(0))) - .filter(filters.byClasses(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals - { - enemyStructures = gameState.getNeutralStructures().filter(filters.not(filters.byOwner(0))) - .filter(filters.byClasses(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) - { - enemyStructures = gameState.getAllyStructures().filter( - filters.not(filters.byOwner(PlayerID))).filter( - filters.byClasses(["CivCentre", "Fortress", "Tower"])); - } - if (!enemyStructures.hasEntities()) - return undefined; - } - enemyStructures = enemyStructures.toEntityArray(); - - let wonderMode = gameState.getVictoryConditions().has("wonder"); - let wonderDistmin; - let wonders; - if (wonderMode) - { - wonders = gameState.getOwnStructures().filter(filters.byClass("Wonder")).toEntityArray(); - wonderMode = wonders.length != 0; - if (wonderMode) - wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); + // base "-1" means new base. + if (this.Config.debug > 1) + aiWarn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); + queues.civilCentre.addPlan(new ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); + return true; } - // obstruction map - const obstructions = 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; - const width = this.territoryMap.width; - const cellSize = this.territoryMap.cellSize; - - const isTower = template.hasClass("Tower"); - const isFortress = template.hasClass("Fortress"); - let radius; - if (isFortress) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - for (let j = 0; j < this.territoryMap.length; ++j) + /** Deals with building fortresses and towers along our border with enemies. */ + buildDefenses(gameState, queues) { - if (!wonderMode) - { - // do not try if well inside or outside territory - if (!(this.borderMap.map[j] & mapMask.fullFrontier)) - continue; - if (this.borderMap.map[j] & mapMask.largeFrontier && isTower) - continue; - } - if (this.baseAtIndex(j) == 0) // inaccessible cell - continue; - // with enough room around to build the cc - const i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; + if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) + return; - const 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) + if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) { - dista = 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 (const str of enemyStructures) - { - if (str.foundationProgress() !== undefined) - continue; - const strPos = str.position(); - if (!strPos) - continue; - const dist = SquareVectorDistance(strPos, pos); - if (dist < 6400) // TODO check on true attack range instead of this 80×80 + // Try to build fortresses. + if (this.canBuild(gameState, "structures/{civ}/fortress")) { - minDist = -1; - break; - } - if (str.hasClass("CivCentre") && dist + dista < minDist) - minDist = dist + dista; - } - if (minDist < 0) - continue; - - const cutDist = 900; // 30×30 TODO maybe increase it - for (const str of ownStructures) - { - const strPos = str.position(); - if (!strPos) - continue; - if (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; - - const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return [x, z, this.baseAtIndex(bestJdx)]; -}; - -Headquarters.prototype.buildTemple = function(gameState, queues) -{ - // at least one market (which have the same queue) should be build before any temple - if (queues.economicBuilding.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || - !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) - return; - // Try to build a temple earlier if in regicide to recruit healer guards - if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide")) - return; - - let templateName = "structures/{civ}/temple"; - if (this.canBuild(gameState, "structures/{civ}/temple_vesta")) - templateName = "structures/{civ}/temple_vesta"; - else if (!this.canBuild(gameState, templateName)) - return; - queues.economicBuilding.addPlan(new ConstructionPlan(gameState, templateName)); -}; - -Headquarters.prototype.buildMarket = function(gameState, queues) -{ - if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}/market")) - return; - - if (queues.economicBuilding.hasQueuedUnitsWithClass("Market")) - { - if (!queues.economicBuilding.paused) - { - // Put available resources in this market - const queueManager = gameState.ai.queueManager; - const cost = queues.economicBuilding.plans[0].getCost(); - queueManager.setAccounts(gameState, cost, "economicBuilding"); - if (!queueManager.canAfford("economicBuilding", cost)) - { - for (const q in queueManager.queues) + const numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; + if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) && + numFortresses < this.numActiveBases() + 1 + this.extraFortresses && + numFortresses < Math.floor(gameState.getPopulation() / 25) && + gameState.getOwnFoundationsByClass("Fortress").length < 2) { - if (q == "economicBuilding") - continue; - queueManager.transferAccounts(cost, q, "economicBuilding"); - if (queueManager.canAfford("economicBuilding", cost)) - break; + this.fortressStartTime = gameState.ai.elapsedTime; + if (!numFortresses) + gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding); + const plan = new ConstructionPlan(gameState, "structures/{civ}/fortress"); + plan.queueToReset = "defenseBuilding"; + queues.defenseBuilding.addPlan(plan); + return; } } } - return; - } - gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding); - const plan = new ConstructionPlan(gameState, "structures/{civ}/market"); - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -/** Build a farmstead */ -Headquarters.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 ConstructionPlan(gameState, "structures/{civ}/farmstead")); -}; - -/** - * Try to build a wonder when required - * force = true when called from the victoryManager in case of Wonder victory condition. - */ -Headquarters.prototype.buildWonder = function(gameState, queues, force = false) -{ - if (queues.wonder && queues.wonder.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}/wonder")) - return; - - if (!force) - { - const template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/wonder")); - // Check that we have enough resources to start thinking to build a wonder - const cost = template.cost(); - const resources = gameState.getResources(); - let highLevel = 0; - let lowLevel = 0; - for (const res in cost) + if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}/sentry_tower")) { - if (resources[res] && resources[res] > 0.7 * cost[res]) - ++highLevel; - else if (!resources[res] || resources[res] < 0.3 * cost[res]) - ++lowLevel; - } - if (highLevel == 0 || lowLevel > 1) - return; - } - - queues.wonder.addPlan(new ConstructionPlan(gameState, "structures/{civ}/wonder")); -}; - -/** Build a corral, and train animals there */ -Headquarters.prototype.manageCorral = function(gameState, queues) -{ - if (queues.corral.hasQueuedUnits()) - return; - - const nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; - if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")) && - nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral) - { - if (this.canBuild(gameState, "structures/{civ}/corral")) - { - queues.corral.addPlan(new ConstructionPlan(gameState, "structures/{civ}/corral")); + // Count all towers + wall towers. + const numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length; + const towerLapseTime = this.saveResources ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime; + if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) + { + this.fortStartTime = gameState.ai.elapsedTime; + queues.defenseBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/sentry_tower")); + } return; } - if (!nCorral) + + if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}/defense_tower")) return; + + const numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; + const towerLapseTime = this.saveResources ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; + if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) && + numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && + numTowers < Math.floor(gameState.getPopulation() / 8) && + gameState.getOwnFoundationsByClass("Tower").length < 3) + { + this.towerStartTime = gameState.ai.elapsedTime; + if (numTowers > 2 * this.numActiveBases() + 3) + gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding)); + const plan = new ConstructionPlan(gameState, "structures/{civ}/defense_tower"); + plan.queueToReset = "defenseBuilding"; + queues.defenseBuilding.addPlan(plan); + } } - // And train some animals - const civ = gameState.getPlayerCiv(); - for (const corral of gameState.getOwnEntitiesByClass("Corral", true).values()) + buildForge(gameState, queues) { - if (corral.foundationProgress() !== undefined) - continue; - const trainables = corral.trainableEntities(civ); + if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge || + queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length) + return; + // Build a Market before the Forge. + if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities()) + return; + + if (this.canBuild(gameState, "structures/{civ}/forge")) + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/forge")); + } + + /** + * Deals with constructing military buildings (e.g. barracks, stable). + * They are mostly defined by Config.js. This is unreliable since changes could be done easily. + */ + constructTrainingBuildings(gameState, queues) + { + if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) + return; + + const numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; + if (this.saveResources && numBarracks != 0) + return; + + const barracksTemplate = this.canBuild(gameState, "structures/{civ}/barracks") ? "structures/{civ}/barracks" : undefined; + + const rangeTemplate = this.canBuild(gameState, "structures/{civ}/range") ? "structures/{civ}/range" : undefined; + const numRanges = gameState.getOwnEntitiesByClass("Range", true).length; + + const stableTemplate = this.canBuild(gameState, "structures/{civ}/stable") ? "structures/{civ}/stable" : undefined; + const numStables = gameState.getOwnEntitiesByClass("Stable", true).length; + + if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || + this.phasing == 2 && gameState.getOwnStructures().filter(filters.byClass("Village")).length < 5) + { + // First barracks/range and stable. + if (numBarracks + numRanges == 0) + { + const template = barracksTemplate || rangeTemplate; + if (template) + { + gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); + const plan = new ConstructionPlan(gameState, template, { "militaryBase": true }); + plan.queueToReset = "militaryBuilding"; + queues.militaryBuilding.addPlan(plan); + return; + } + } + if (numStables == 0 && stableTemplate) + { + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); + return; + } + + // Second barracks/range and stable. + if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) + { + const template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); + if (template) + { + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, template, { "militaryBase": true })); + return; + } + } + if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) + { + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); + return; + } + + // Third barracks/range and stable, if needed. + if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) + { + const template = barracksTemplate || stableTemplate || rangeTemplate; + if (template) + { + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, template, { "militaryBase": true })); + return; + } + } + } + + if (this.saveResources) + return; + + if (this.currentPhase < 3) + return; + + if (this.canBuild(gameState, "structures/{civ}/elephant_stable") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities()) + { + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/elephant_stable", { "militaryBase": true })); + return; + } + + if (this.canBuild(gameState, "structures/{civ}/arsenal") && !gameState.getOwnEntitiesByClass("Arsenal", true).hasEntities()) + { + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/arsenal", { "militaryBase": true })); + return; + } + + if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) + return; + + // Build advanced military buildings + let nAdvanced = 0; + for (const advanced of this.bAdvanced) + nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); + + if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) + { + for (const advanced of this.bAdvanced) + { + if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) + continue; + const template = gameState.getTemplate(advanced); + if (!template) + continue; + const civ = gameState.getPlayerCiv(); + if (template.hasDefensiveFire() || template.trainableEntities(civ)) + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, advanced, { "militaryBase": true })); + else // not a military building, but still use this queue + queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, advanced)); + return; + } + } + } + + /** + * Find base nearest to ennemies for military buildings. + */ + findBestBaseForMilitary(gameState) + { + const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")).toEntityArray(); + let bestBase; + let enemyFound = false; + let distMin = Math.min(); + for (const cce of ccEnts) + { + if (gameState.isPlayerAlly(cce.owner())) + continue; + if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) + continue; + const access = getLandAccess(gameState, cce); + const isEnemy = gameState.isPlayerEnemy(cce.owner()); + for (const cc of ccEnts) + { + if (cc.owner() != PlayerID) + continue; + if (getLandAccess(gameState, cc) != access) + continue; + const dist = SquareVectorDistance(cc.position(), cce.position()); + if (!enemyFound && isEnemy) + enemyFound = true; + else if (dist > distMin) + continue; + bestBase = cc.getMetadata(PlayerID, "base"); + distMin = dist; + } + } + return bestBase; + } + + /** + * train with highest priority ranged infantry in the nearest civil center from a given set of positions + * and garrison them there for defense + */ + trainEmergencyUnits(gameState, positions) + { + if (gameState.ai.queues.emergency.hasQueuedUnits()) + return false; + + const civ = gameState.getPlayerCiv(); + // find nearest base anchor + const distcut = 20000; + let nearestAnchor; + let distmin; + for (const pos of positions) + { + const access = gameState.ai.accessibility.getAccessValue(pos); + // check nearest base anchor + for (const base of this.baseManagers()) + { + if (!base.anchor || !base.anchor.position()) + continue; + if (getLandAccess(gameState, base.anchor) != access) + continue; + if (!base.anchor.trainableEntities(civ)) // base still in construction + continue; + const queue = base.anchor._entity.trainingQueue; + if (queue) + { + let time = 0; + for (const item of queue) + if (item.progress > 0 || item.metadata && item.metadata.garrisonType) + time += item.timeRemaining; + if (time/1000 > 5) + continue; + } + const dist = 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.numberOfGarrisonedSlots(nearestAnchor); + if (nearestAnchor._entity.trainingQueue) + { + for (const 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); + } + } + const autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && + nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); + const rangedWanted = randBool() && autogarrison; + + const total = gameState.getResources(); + let templateFound; + const trainables = nearestAnchor.trainableEntities(civ); + const garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); for (const trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; const template = gameState.getTemplate(trainable); - if (!template || !template.isHuntable()) + if (!template || !template.hasClasses(["Infantry+CitizenSoldier"])) continue; - let count = gameState.countEntitiesByType(trainable, true); - for (const item of corral.trainingQueue()) - count += item.count; - if (count > nCorral) + if (autogarrison && !template.hasClasses(garrisonArrowClasses)) continue; - queues.corral.addPlan(new 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… - */ -Headquarters.prototype.buildMoreHouses = function(gameState, queues) -{ - let houseTemplateString = "structures/{civ}/apartment"; - if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) || - !this.canBuild(gameState, houseTemplateString)) - { - houseTemplateString = "structures/{civ}/house"; - if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString))) - return; - } - if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return; - - const numPlanned = queues.house.length(); - if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) - { - const plan = new ConstructionPlan(gameState, houseTemplateString); - // change the starting condition according to the situation. - plan.goRequirement = "houseNeeded"; - queues.house.addPlan(plan); - } - - if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) - { - const houseTemplateName = gameState.applyCiv(houseTemplateString); - const houseTemplate = gameState.getTemplate(houseTemplateName); - - let needed = 0; - for (const entityReq of gameState.getPhaseEntityRequirements(this.phasing)) - { - if (!houseTemplate.hasClass(entityReq.class)) + if (!total.canAfford(new ResourcesManager(template.cost()))) continue; - - const count = gameState.getOwnStructures().filter(filters.byClass(entityReq.class)) - .length; - if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) - { - if (this.Config.debug > 1) - aiWarn("no room to place a house ... try to be less restrictive"); - this.buildManager.setBuildable(houseTemplateName); - this.requireHouses = true; - } - needed = Math.max(needed, entityReq.count - count); - } - - const houseQueue = queues.house.plans; - for (let i = 0; i < numPlanned; ++i) - if (houseQueue[i].isGo(gameState)) - --needed; - else if (needed > 0) - { - houseQueue[i].goRequirement = undefined; - --needed; - } - } - - if (this.requireHouses) - { - const houseTemplate = gameState.getTemplate(gameState.applyCiv(houseTemplateString)); - if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => - !houseTemplate.hasClass(req.class) || - gameState.getOwnStructures().filter(filters.byClass(req.class)).length >= req.count)) - { - this.requireHouses = undefined; - } - } - - // When population limit too tight - // - if no room to build, try to improve with technology - // - otherwise increase temporarily the priority of houses - const house = gameState.applyCiv(houseTemplateString); - const HouseNb = gameState.getOwnFoundations().filter(filters.byClass("House")).length; - const popBonus = gameState.getTemplate(house).getPopulationBonus(); - const freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); - let priority; - if (freeSlots < 5) - { - if (this.buildManager.isUnbuildable(gameState, house)) - { - if (this.Config.debug > 1) - aiWarn("no room to place a house ... try to improve with technology"); - this.researchManager.researchPopulationBonus(gameState, queues); - } - else - priority = 2 * this.Config.priorities.house; - } - else - priority = this.Config.priorities.house; - - if (priority && priority != gameState.ai.queueManager.getPriority("house")) - gameState.ai.queueManager.changePriority("house", priority); -}; - -/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ -Headquarters.prototype.checkBaseExpansion = function(gameState, queues) -{ - if (queues.civilCentre.hasQueuedUnits()) - return; - // First build one cc if all have been destroyed - if (!this.hasPotentialBase()) - { - buildFirstBase(this, gameState); - return; - } - // Then expand if we have not enough room available for buildings - if (this.buildManager.numberMissingRoom(gameState) > 1) - { - if (this.Config.debug > 2) - aiWarn("try to build a new base because not enough room to build "); - this.buildNewBase(gameState, queues); - return; - } - // If we've already planned to phase up, wait a bit before trying to expand - if (this.phasing) - return; - // Finally expand if we have lots of units (threshold depending on the aggressivity value) - const activeBases = this.numActiveBases(); - const numUnits = gameState.getOwnUnits().length; - const 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) - { - aiWarn("try to build a new base because of population " + numUnits + " for " + - activeBases + " CCs"); - } - this.buildNewBase(gameState, queues); - } -}; - -Headquarters.prototype.buildNewBase = function(gameState, queues, resource) -{ - if (this.hasPotentialBase() && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) - return false; - if (gameState.getOwnFoundations().filter(filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) - return false; - - let template; - // We require at least one of this civ civCentre as they may allow specific units or techs - let hasOwnCC = false; - for (const ent of gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")).values()) - { - if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}/civil_centre")) - continue; - hasOwnCC = true; - break; - } - if (hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) - template = "structures/{civ}/military_colony"; - else if (this.canBuild(gameState, "structures/{civ}/civil_centre")) - template = "structures/{civ}/civil_centre"; - else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) - template = "structures/{civ}/military_colony"; - else - return false; - - // base "-1" means new base. - if (this.Config.debug > 1) - aiWarn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); - queues.civilCentre.addPlan(new ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); - return true; -}; - -/** Deals with building fortresses and towers along our border with enemies. */ -Headquarters.prototype.buildDefenses = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) - return; - - if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) - { - // Try to build fortresses. - if (this.canBuild(gameState, "structures/{civ}/fortress")) - { - const numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; - if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) && - numFortresses < this.numActiveBases() + 1 + this.extraFortresses && - numFortresses < Math.floor(gameState.getPopulation() / 25) && - gameState.getOwnFoundationsByClass("Fortress").length < 2) - { - this.fortressStartTime = gameState.ai.elapsedTime; - if (!numFortresses) - gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding); - const plan = new ConstructionPlan(gameState, "structures/{civ}/fortress"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - return; - } - } - } - - if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}/sentry_tower")) - { - // Count all towers + wall towers. - const numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length; - const towerLapseTime = this.saveResources ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime; - if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) - { - this.fortStartTime = gameState.ai.elapsedTime; - queues.defenseBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/sentry_tower")); - } - return; - } - - if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}/defense_tower")) - return; - - const numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; - const towerLapseTime = this.saveResources ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; - if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) && - numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && - numTowers < Math.floor(gameState.getPopulation() / 8) && - gameState.getOwnFoundationsByClass("Tower").length < 3) - { - this.towerStartTime = gameState.ai.elapsedTime; - if (numTowers > 2 * this.numActiveBases() + 3) - gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding)); - const plan = new ConstructionPlan(gameState, "structures/{civ}/defense_tower"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - } -}; - -Headquarters.prototype.buildForge = function(gameState, queues) -{ - if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge || - queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length) - return; - // Build a Market before the Forge. - if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities()) - return; - - if (this.canBuild(gameState, "structures/{civ}/forge")) - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/forge")); -}; - -/** - * Deals with constructing military buildings (e.g. barracks, stable). - * They are mostly defined by Config.js. This is unreliable since changes could be done easily. - */ -Headquarters.prototype.constructTrainingBuildings = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) - return; - - const numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; - if (this.saveResources && numBarracks != 0) - return; - - const barracksTemplate = this.canBuild(gameState, "structures/{civ}/barracks") ? "structures/{civ}/barracks" : undefined; - - const rangeTemplate = this.canBuild(gameState, "structures/{civ}/range") ? "structures/{civ}/range" : undefined; - const numRanges = gameState.getOwnEntitiesByClass("Range", true).length; - - const stableTemplate = this.canBuild(gameState, "structures/{civ}/stable") ? "structures/{civ}/stable" : undefined; - const numStables = gameState.getOwnEntitiesByClass("Stable", true).length; - - if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || - this.phasing == 2 && gameState.getOwnStructures().filter(filters.byClass("Village")).length < 5) - { - // First barracks/range and stable. - if (numBarracks + numRanges == 0) - { - const template = barracksTemplate || rangeTemplate; - if (template) - { - gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); - const plan = new ConstructionPlan(gameState, template, { "militaryBase": true }); - plan.queueToReset = "militaryBuilding"; - queues.militaryBuilding.addPlan(plan); - return; - } - } - if (numStables == 0 && stableTemplate) - { - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Second barracks/range and stable. - if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - const template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); - if (template) - { - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Third barracks/range and stable, if needed. - if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) - { - const template = barracksTemplate || stableTemplate || rangeTemplate; - if (template) - { - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - } - - if (this.saveResources) - return; - - if (this.currentPhase < 3) - return; - - if (this.canBuild(gameState, "structures/{civ}/elephant_stable") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/elephant_stable", { "militaryBase": true })); - return; - } - - if (this.canBuild(gameState, "structures/{civ}/arsenal") && !gameState.getOwnEntitiesByClass("Arsenal", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/arsenal", { "militaryBase": true })); - return; - } - - if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) - return; - - // Build advanced military buildings - let nAdvanced = 0; - for (const advanced of this.bAdvanced) - nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); - - if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) - { - for (const advanced of this.bAdvanced) - { - if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) - continue; - const template = gameState.getTemplate(advanced); - if (!template) - continue; - const civ = gameState.getPlayerCiv(); - if (template.hasDefensiveFire() || template.trainableEntities(civ)) - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, advanced, { "militaryBase": true })); - else // not a military building, but still use this queue - queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, advanced)); - return; - } - } -}; - -/** - * Find base nearest to ennemies for military buildings. - */ -Headquarters.prototype.findBestBaseForMilitary = function(gameState) -{ - const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")).toEntityArray(); - let bestBase; - let enemyFound = false; - let distMin = Math.min(); - for (const cce of ccEnts) - { - if (gameState.isPlayerAlly(cce.owner())) - continue; - if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) - continue; - const access = getLandAccess(gameState, cce); - const isEnemy = gameState.isPlayerEnemy(cce.owner()); - for (const cc of ccEnts) - { - if (cc.owner() != PlayerID) - continue; - if (getLandAccess(gameState, cc) != access) - continue; - const dist = SquareVectorDistance(cc.position(), cce.position()); - if (!enemyFound && isEnemy) - enemyFound = true; - else if (dist > distMin) - continue; - bestBase = cc.getMetadata(PlayerID, "base"); - distMin = dist; - } - } - return bestBase; -}; - -/** - * train with highest priority ranged infantry in the nearest civil center from a given set of positions - * and garrison them there for defense - */ -Headquarters.prototype.trainEmergencyUnits = function(gameState, positions) -{ - if (gameState.ai.queues.emergency.hasQueuedUnits()) - return false; - - const civ = gameState.getPlayerCiv(); - // find nearest base anchor - const distcut = 20000; - let nearestAnchor; - let distmin; - for (const pos of positions) - { - const access = gameState.ai.accessibility.getAccessValue(pos); - // check nearest base anchor - for (const base of this.baseManagers()) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (getLandAccess(gameState, base.anchor) != access) - continue; - if (!base.anchor.trainableEntities(civ)) // base still in construction - continue; - const queue = base.anchor._entity.trainingQueue; - if (queue) - { - let time = 0; - for (const item of queue) - if (item.progress > 0 || item.metadata && item.metadata.garrisonType) - time += item.timeRemaining; - if (time/1000 > 5) - continue; - } - const dist = 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.numberOfGarrisonedSlots(nearestAnchor); - if (nearestAnchor._entity.trainingQueue) - { - for (const 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); - } - } - const autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && - nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); - const rangedWanted = randBool() && autogarrison; - - const total = gameState.getResources(); - let templateFound; - const trainables = nearestAnchor.trainableEntities(civ); - const garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); - for (const trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - const template = gameState.getTemplate(trainable); - if (!template || !template.hasClasses(["Infantry+CitizenSoldier"])) - continue; - if (autogarrison && !template.hasClasses(garrisonArrowClasses)) - continue; - if (!total.canAfford(new ResourcesManager(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 - const queueManager = gameState.ai.queueManager; - const cost = new ResourcesManager(templateFound[1].cost()); - queueManager.setAccounts(gameState, cost, "emergency"); - if (!queueManager.canAfford("emergency", cost)) - { - for (const q in queueManager.queues) - { - if (q == "emergency") - continue; - queueManager.transferAccounts(cost, q, "emergency"); - if (queueManager.canAfford("emergency", cost)) + templateFound = [trainable, template]; + if (template.hasClass("Ranged") == rangedWanted) break; } - } - const metadata = { "role": Worker.ROLE_WORKER, "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; - if (autogarrison) - metadata.garrisonType = GarrisonManager.TYPE_PROTECTION; - gameState.ai.queues.emergency.addPlan(new TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); - return true; -}; + if (!templateFound) + return false; -Headquarters.prototype.canBuild = function(gameState, structure) -{ - const type = gameState.applyCiv(structure); - if (this.buildManager.isUnbuildable(gameState, type)) - return false; - - if (gameState.isTemplateDisabled(type)) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); - return false; - } - - const template = gameState.getTemplate(type); - if (!template) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); - return false; - } - - if (!template.available(gameState)) - { - this.buildManager.setUnbuildable(gameState, type, 30, "requirements"); - return false; - } - - if (!this.buildManager.hasBuilder(type)) - { - this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); - return false; - } - - if (!this.hasActiveBase()) - { - // if no base, check that we can build outside our territory - const buildTerritories = template.buildTerritories(); - if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) + // 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 + const queueManager = gameState.ai.queueManager; + const cost = new ResourcesManager(templateFound[1].cost()); + queueManager.setAccounts(gameState, cost, "emergency"); + if (!queueManager.canAfford("emergency", cost)) { - this.buildManager.setUnbuildable(gameState, type, 180, "room"); + for (const q in queueManager.queues) + { + if (q == "emergency") + continue; + queueManager.transferAccounts(cost, q, "emergency"); + if (queueManager.canAfford("emergency", cost)) + break; + } + } + const metadata = { "role": Worker.ROLE_WORKER, "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; + if (autogarrison) + metadata.garrisonType = GarrisonManager.TYPE_PROTECTION; + gameState.ai.queues.emergency.addPlan(new TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); + return true; + } + + canBuild(gameState, structure) + { + const type = gameState.applyCiv(structure); + if (this.buildManager.isUnbuildable(gameState, type)) + return false; + + if (gameState.isTemplateDisabled(type)) + { + this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); return false; } + + const template = gameState.getTemplate(type); + if (!template) + { + this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); + return false; + } + + if (!template.available(gameState)) + { + this.buildManager.setUnbuildable(gameState, type, 30, "requirements"); + return false; + } + + if (!this.buildManager.hasBuilder(type)) + { + this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); + return false; + } + + if (!this.hasActiveBase()) + { + // if no base, check that we can build outside our territory + const buildTerritories = template.buildTerritories(); + if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) + { + this.buildManager.setUnbuildable(gameState, type, 180, "room"); + return false; + } + } + + // build limits + const limits = gameState.getEntityLimits(); + const category = template.buildCategory(); + if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) + { + this.buildManager.setUnbuildable(gameState, type, 90, "limit"); + return false; + } + + return true; } - // build limits - const limits = gameState.getEntityLimits(); - const category = template.buildCategory(); - if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) + updateTerritories(gameState) { - this.buildManager.setUnbuildable(gameState, type, 90, "limit"); + const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; + const alliedVictory = gameState.getAlliedVictory(); + const passabilityMap = gameState.getPassabilityMap(); + const width = this.territoryMap.width; + const cellSize = this.territoryMap.cellSize; + const insideSmall = Math.round(45 / cellSize); + const insideLarge = Math.round(80 / cellSize); // should be about the range of towers + let expansion = 0; + + for (let j = 0; j < this.territoryMap.length; ++j) + { + if (this.borderMap.map[j] & mapMask.outside) + continue; + if (this.borderMap.map[j] & mapMask.fullFrontier) + this.borderMap.map[j] &= ~mapMask.fullFrontier; // reset the frontier + + if (this.territoryMap.getOwnerIndex(j) != PlayerID) + this.basesManager.removeBaseFromTerritoryIndex(j); + else + { + // Update the frontier + const ix = j%width; + const iz = Math.floor(j/width); + let onFrontier = false; + for (const a of around) + { + let jx = ix + Math.round(insideSmall*a[0]); + if (jx < 0 || jx >= width) + continue; + let jz = iz + Math.round(insideSmall*a[1]); + if (jz < 0 || jz >= width) + continue; + if (this.borderMap.map[jx+width*jz] & mapMask.outside) + continue; + let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); + if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) + { + this.borderMap.map[j] |= mapMask.narrowFrontier; + break; + } + jx = ix + Math.round(insideLarge*a[0]); + if (jx < 0 || jx >= width) + continue; + jz = iz + Math.round(insideLarge*a[1]); + if (jz < 0 || jz >= width) + continue; + if (this.borderMap.map[jx+width*jz] & mapMask.outside) + continue; + territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); + if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) + onFrontier = true; + } + if (onFrontier && !(this.borderMap.map[j] & mapMask.narrowFrontier)) + this.borderMap.map[j] |= mapMask.largeFrontier; + + if (this.basesManager.addTerritoryIndexToBase(gameState, j, passabilityMap)) + expansion++; + } + } + + if (!expansion) + return; + // We've increased our territory, so we may have some new room to build + this.buildManager.resetMissingRoom(gameState); + // And if sufficient expansion, check if building a new market would improve our present trade routes + const cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; + if (expansion * cellArea > 960) + this.tradeManager.routeProspection = true; + } + + /** + * returns the base corresponding to baseID + */ + getBaseByID(baseID) + { + return this.basesManager.getBaseByID(baseID); + } + + /** + * returns the number of bases with a cc + * ActiveBases includes only those with a built cc + * PotentialBases includes also those with a cc in construction + */ + numActiveBases() + { + return this.basesManager.numActiveBases(); + } + + hasActiveBase() + { + return this.basesManager.hasActiveBase(); + } + + numPotentialBases() + { + return this.basesManager.numPotentialBases(); + } + + hasPotentialBase() + { + return this.basesManager.hasPotentialBase(); + } + + isDangerousLocation(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 */ + isNearInvadingArmy(pos) + { + for (const army of this.defenseManager.armies) + if (army.foePosition && SquareVectorDistance(army.foePosition, pos) < 12000) + return true; return false; } - return true; -}; - -Headquarters.prototype.updateTerritories = function(gameState) -{ - const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; - const alliedVictory = gameState.getAlliedVictory(); - const passabilityMap = gameState.getPassabilityMap(); - const width = this.territoryMap.width; - const cellSize = this.territoryMap.cellSize; - const insideSmall = Math.round(45 / cellSize); - const insideLarge = Math.round(80 / cellSize); // should be about the range of towers - let expansion = 0; - - for (let j = 0; j < this.territoryMap.length; ++j) + isUnderEnemyFire(gameState, pos, radius = 0) { - if (this.borderMap.map[j] & mapMask.outside) - continue; - if (this.borderMap.map[j] & mapMask.fullFrontier) - this.borderMap.map[j] &= ~mapMask.fullFrontier; // reset the frontier - - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - this.basesManager.removeBaseFromTerritoryIndex(j); - else + if (!this.turnCache.firingStructures) { - // Update the frontier - const ix = j%width; - const iz = Math.floor(j/width); - let onFrontier = false; - for (const a of around) - { - let jx = ix + Math.round(insideSmall*a[0]); - if (jx < 0 || jx >= width) - continue; - let jz = iz + Math.round(insideSmall*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & mapMask.outside) - continue; - let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - { - this.borderMap.map[j] |= mapMask.narrowFrontier; - break; - } - jx = ix + Math.round(insideLarge*a[0]); - if (jx < 0 || jx >= width) - continue; - jz = iz + Math.round(insideLarge*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & mapMask.outside) - continue; - territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - onFrontier = true; - } - if (onFrontier && !(this.borderMap.map[j] & mapMask.narrowFrontier)) - this.borderMap.map[j] |= mapMask.largeFrontier; - - if (this.basesManager.addTerritoryIndexToBase(gameState, j, passabilityMap)) - expansion++; + this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", + filters.hasDefensiveFire(), gameState.getEnemyStructures()); } - } - - if (!expansion) - return; - // We've increased our territory, so we may have some new room to build - this.buildManager.resetMissingRoom(gameState); - // And if sufficient expansion, check if building a new market would improve our present trade routes - const cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; - if (expansion * cellArea > 960) - this.tradeManager.routeProspection = true; -}; - -/** - * returns the base corresponding to baseID - */ -Headquarters.prototype.getBaseByID = function(baseID) -{ - return this.basesManager.getBaseByID(baseID); -}; - -/** - * returns the number of bases with a cc - * ActiveBases includes only those with a built cc - * PotentialBases includes also those with a cc in construction - */ -Headquarters.prototype.numActiveBases = function() -{ - return this.basesManager.numActiveBases(); -}; - -Headquarters.prototype.hasActiveBase = function() -{ - return this.basesManager.hasActiveBase(); -}; - -Headquarters.prototype.numPotentialBases = function() -{ - return this.basesManager.numPotentialBases(); -}; - -Headquarters.prototype.hasPotentialBase = function() -{ - return this.basesManager.hasPotentialBase(); -}; - -Headquarters.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 */ -Headquarters.prototype.isNearInvadingArmy = function(pos) -{ - for (const army of this.defenseManager.armies) - if (army.foePosition && SquareVectorDistance(army.foePosition, pos) < 12000) - return true; - return false; -}; - -Headquarters.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) -{ - if (!this.turnCache.firingStructures) - { - this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", - filters.hasDefensiveFire(), gameState.getEnemyStructures()); - } - for (const ent of this.turnCache.firingStructures.values()) - { - const range = radius + ent.attackRange("Ranged").max; - if (SquareVectorDistance(ent.position(), pos) < range*range) - return true; - } - return false; -}; - -/** Compute the capture strength of all units attacking a capturable target */ -Headquarters.prototype.updateCaptureStrength = function(gameState) -{ - this.capturableTargets.clear(); - for (const ent of gameState.getOwnUnits().values()) - { - if (!ent.canCapture()) - continue; - const state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - const orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - const targetId = orderData[0].target; - const target = gameState.getEntityById(targetId); - if (!target || !target.isCapturable() || !ent.canCapture(target)) - continue; - if (!this.capturableTargets.has(targetId)) - this.capturableTargets.set(targetId, { - "strength": ent.captureStrength() * getAttackBonus(ent, target, "Capture"), - "ents": new Set([ent.id()]) - }); - else + for (const ent of this.turnCache.firingStructures.values()) { - const capturableTarget = this.capturableTargets.get(target.id()); - capturableTarget.strength += ent.captureStrength() * getAttackBonus(ent, target, "Capture"); - capturableTarget.ents.add(ent.id()); + const range = radius + ent.attackRange("Ranged").max; + if (SquareVectorDistance(ent.position(), pos) < range*range) + return true; } + return false; } - for (const [targetId, capturableTarget] of this.capturableTargets) + /** Compute the capture strength of all units attacking a capturable target */ + updateCaptureStrength(gameState) { - const target = gameState.getEntityById(targetId); - let shouldCapture; - for (const entId of capturableTarget.ents) + this.capturableTargets.clear(); + for (const ent of gameState.getOwnUnits().values()) { - const ent = gameState.getEntityById(entId); - if (shouldCapture === undefined) - shouldCapture = allowCapture(gameState, ent, target); - const orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].attackType) + if (!ent.canCapture()) continue; - if ((orderData[0].attackType == "Capture") !== shouldCapture) - ent.attack(targetId, shouldCapture); - } - } - - this.capturableTargetsTime = gameState.ai.elapsedTime; -}; - -/** - * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) - */ -Headquarters.prototype.isDefendable = function(ent) -{ - if (!this.turnCache.numAround) - this.turnCache.numAround = {}; - if (this.turnCache.numAround[ent.id()] === undefined) - this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); - return +this.turnCache.numAround[ent.id()] > 8; -}; - -/** - * Get the number of population already accounted for - */ -Headquarters.prototype.getAccountedPopulation = function(gameState) -{ - if (this.turnCache.accountedPopulation == undefined) - { - let pop = gameState.getPopulation(); - for (const ent of gameState.getOwnTrainingFacilities().values()) - { - for (const item of ent.trainingQueue()) + const state = ent.unitAIState(); + if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") + continue; + const orderData = ent.unitAIOrderData(); + if (!orderData || !orderData.length || !orderData[0].target) + continue; + const targetId = orderData[0].target; + const target = gameState.getEntityById(targetId); + if (!target || !target.isCapturable() || !ent.canCapture(target)) + continue; + if (!this.capturableTargets.has(targetId)) + this.capturableTargets.set(targetId, { + "strength": ent.captureStrength() * getAttackBonus(ent, target, "Capture"), + "ents": new Set([ent.id()]) + }); + else { - if (!item.unitTemplate) - continue; - const unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population"); - if (unitPop) - pop += item.count * unitPop; + const capturableTarget = this.capturableTargets.get(target.id()); + capturableTarget.strength += ent.captureStrength() * getAttackBonus(ent, target, "Capture"); + capturableTarget.ents.add(ent.id()); } } - this.turnCache.accountedPopulation = pop; - } - return this.turnCache.accountedPopulation; -}; -/** - * Get the number of workers already accounted for - */ -Headquarters.prototype.getAccountedWorkers = function(gameState) -{ - if (this.turnCache.accountedWorkers == undefined) - { - let workers = gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length; - for (const ent of gameState.getOwnTrainingFacilities().values()) + for (const [targetId, capturableTarget] of this.capturableTargets) { - for (const item of ent.trainingQueue()) + const target = gameState.getEntityById(targetId); + let shouldCapture; + for (const entId of capturableTarget.ents) { - if (!item.metadata || !item.metadata.role || item.metadata.role !== Worker.ROLE_WORKER) + const ent = gameState.getEntityById(entId); + if (shouldCapture === undefined) + shouldCapture = allowCapture(gameState, ent, target); + const orderData = ent.unitAIOrderData(); + if (!orderData || !orderData.length || !orderData[0].attackType) continue; - workers += item.count; + if ((orderData[0].attackType == "Capture") !== shouldCapture) + ent.attack(targetId, shouldCapture); } } - this.turnCache.accountedWorkers = workers; + + this.capturableTargetsTime = gameState.ai.elapsedTime; } - return this.turnCache.accountedWorkers; -}; -Headquarters.prototype.baseManagers = function() -{ - return this.basesManager.baseManagers; -}; - -/** - * @param {number} territoryIndex - The index to get the map for. - * @return {number} - The ID of the base at the given territory index. - */ -Headquarters.prototype.baseAtIndex = function(territoryIndex) -{ - return this.basesManager.baseAtIndex(territoryIndex); -}; - -/** - * Some functions are run every turn - * Others once in a while - */ -Headquarters.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Headquarters update"); - this.emergencyManager.update(gameState); - this.turnCache = {}; - this.territoryMap = createTerritoryMap(gameState); - this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(filters.isBuilt()) - .hasEntities(); - // TODO find a better way to update - if (this.currentPhase != gameState.currentPhase()) + /** + * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) + */ + isDefendable(ent) { - if (this.Config.debug > 0) - aiWarn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + - " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + - " phasing " + this.phasing); - this.currentPhase = gameState.currentPhase(); + if (!this.turnCache.numAround) + this.turnCache.numAround = {}; + if (this.turnCache.numAround[ent.id()] === undefined) + this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); + return +this.turnCache.numAround[ent.id()] > 8; + } + + /** + * Get the number of population already accounted for + */ + getAccountedPopulation(gameState) + { + if (this.turnCache.accountedPopulation == undefined) + { + let pop = gameState.getPopulation(); + for (const ent of gameState.getOwnTrainingFacilities().values()) + { + for (const item of ent.trainingQueue()) + { + if (!item.unitTemplate) + continue; + const unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population"); + if (unitPop) + pop += item.count * unitPop; + } + } + this.turnCache.accountedPopulation = pop; + } + return this.turnCache.accountedPopulation; + } + + /** + * Get the number of workers already accounted for + */ + getAccountedWorkers(gameState) + { + if (this.turnCache.accountedWorkers == undefined) + { + let workers = gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length; + for (const ent of gameState.getOwnTrainingFacilities().values()) + { + for (const item of ent.trainingQueue()) + { + if (!item.metadata || !item.metadata.role || item.metadata.role !== Worker.ROLE_WORKER) + continue; + workers += item.count; + } + } + this.turnCache.accountedWorkers = workers; + } + return this.turnCache.accountedWorkers; + } + + baseManagers() + { + return this.basesManager.baseManagers; + } + + /** + * @param {number} territoryIndex - The index to get the map for. + * @return {number} - The ID of the base at the given territory index. + */ + baseAtIndex(territoryIndex) + { + return this.basesManager.baseAtIndex(territoryIndex); + } + + /** + * Some functions are run every turn + * Others once in a while + */ + update(gameState, queues, events) + { + Engine.ProfileStart("Headquarters update"); + this.emergencyManager.update(gameState); + this.turnCache = {}; + this.territoryMap = createTerritoryMap(gameState); + this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(filters.isBuilt()) + .hasEntities(); + // TODO find a better way to update + if (this.currentPhase != gameState.currentPhase()) + { + if (this.Config.debug > 0) + aiWarn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + + " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + + " phasing " + this.phasing); + this.currentPhase = gameState.currentPhase(); + + // In principle, this.phasing should be already reset to 0 when starting the research + // but this does not work in case of an autoResearch tech + if (this.phasing) + this.phasing = 0; + } + + /* + if (this.Config.debug > 1) + { + gameState.getOwnUnits().forEach (function (ent) { + if (!ent.position()) + return; + dumpEntity(ent); + }); + } + */ + + this.checkEvents(gameState, events); + this.navalManager.checkEvents(gameState, queues, events); - // In principle, this.phasing should be already reset to 0 when starting the research - // but this does not work in case of an autoResearch tech if (this.phasing) - this.phasing = 0; - } + this.checkPhaseRequirements(gameState, queues); + else + this.researchManager.checkPhase(gameState, queues); - /* - if (this.Config.debug > 1) - { - gameState.getOwnUnits().forEach (function (ent) { - if (!ent.position()) - return; - dumpEntity(ent); - }); - } - */ - - this.checkEvents(gameState, events); - this.navalManager.checkEvents(gameState, queues, events); - - if (this.phasing) - this.checkPhaseRequirements(gameState, queues); - else - this.researchManager.checkPhase(gameState, queues); - - if (this.hasActiveBase()) - { - if (gameState.ai.playedTurn % 4 == 0) - this.trainMoreWorkers(gameState, queues); - - if (gameState.ai.playedTurn % 4 == 1) - this.buildMoreHouses(gameState, queues); - - if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) - this.buildFarmstead(gameState, queues); - - if (this.needCorral && gameState.ai.playedTurn % 4 == 3) - this.manageCorral(gameState, queues); - - if (gameState.ai.playedTurn % 5 == 1) - this.researchManager.update(gameState, queues); - } - - if (!this.hasPotentialBase() || - this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) - this.checkBaseExpansion(gameState, queues); - - if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) - { - if (!this.canBarter) - this.buildMarket(gameState, queues); - - if (!this.saveResources) + if (this.hasActiveBase()) { - this.buildForge(gameState, queues); - this.buildTemple(gameState, queues); + if (gameState.ai.playedTurn % 4 == 0) + this.trainMoreWorkers(gameState, queues); + + if (gameState.ai.playedTurn % 4 == 1) + this.buildMoreHouses(gameState, queues); + + if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) + this.buildFarmstead(gameState, queues); + + if (this.needCorral && gameState.ai.playedTurn % 4 == 3) + this.manageCorral(gameState, queues); + + if (gameState.ai.playedTurn % 5 == 1) + this.researchManager.update(gameState, queues); } - if (gameState.ai.playedTurn % 30 == 0 && - gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) - this.buildWonder(gameState, queues, false); + if (!this.hasPotentialBase() || + this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) + this.checkBaseExpansion(gameState, queues); + + if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) + { + if (!this.canBarter) + this.buildMarket(gameState, queues); + + if (!this.saveResources) + { + this.buildForge(gameState, queues); + this.buildTemple(gameState, queues); + } + + if (gameState.ai.playedTurn % 30 == 0 && + gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) + this.buildWonder(gameState, queues, false); + } + + this.tradeManager.update(gameState, events, queues); + + this.garrisonManager.update(gameState, events); + this.defenseManager.update(gameState, events); + + if (gameState.ai.playedTurn % 3 == 0) + { + this.constructTrainingBuildings(gameState, queues); + if (this.Config.difficulty > difficulty.SANDBOX) + this.buildDefenses(gameState, queues); + } + + this.basesManager.update(gameState, queues, events); + + this.navalManager.update(gameState, queues, events); + + if (this.Config.difficulty > difficulty.SANDBOX && (this.hasActiveBase() || !this.canBuildUnits)) + this.attackManager.update(gameState, queues, events); + + this.diplomacyManager.update(gameState, events); + + this.victoryManager.update(gameState, events, queues); + + // We update the capture strength at the end as it can change attack orders + if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3) + this.updateCaptureStrength(gameState); + + Engine.ProfileStop(); } - this.tradeManager.update(gameState, events, queues); - - this.garrisonManager.update(gameState, events); - this.defenseManager.update(gameState, events); - - if (gameState.ai.playedTurn % 3 == 0) + Serialize() { - this.constructTrainingBuildings(gameState, queues); - if (this.Config.difficulty > difficulty.SANDBOX) - this.buildDefenses(gameState, queues); + const properties = { + "phasing": this.phasing, + "lastFailedGather": this.lastFailedGather, + "firstBaseConfig": this.firstBaseConfig, + "supportRatio": this.supportRatio, + "targetNumWorkers": this.targetNumWorkers, + "fortStartTime": this.fortStartTime, + "towerStartTime": this.towerStartTime, + "fortressStartTime": this.fortressStartTime, + "bAdvanced": this.bAdvanced, + "saveResources": this.saveResources, + "saveSpace": this.saveSpace, + "needCorral": this.needCorral, + "needFarm": this.needFarm, + "needFish": this.needFish, + "maxFields": this.maxFields, + "canExpand": this.canExpand, + "canBuildUnits": this.canBuildUnits, + "navalMap": this.navalMap, + "landRegions": this.landRegions, + "navalRegions": this.navalRegions, + "decayingStructures": this.decayingStructures, + "capturableTargets": this.capturableTargets, + "capturableTargetsTime": this.capturableTargetsTime + }; + + if (this.Config.debug == -100) + { + aiWarn(" HQ serialization ---------------------"); + aiWarn(" properties " + uneval(properties)); + aiWarn(" basesManager " + uneval(this.basesManager.Serialize())); + aiWarn(" attackManager " + uneval(this.attackManager.Serialize())); + aiWarn(" buildManager " + uneval(this.buildManager.Serialize())); + aiWarn(" defenseManager " + uneval(this.defenseManager.Serialize())); + aiWarn(" tradeManager " + uneval(this.tradeManager.Serialize())); + aiWarn(" navalManager " + uneval(this.navalManager.Serialize())); + aiWarn(" researchManager " + uneval(this.researchManager.Serialize())); + aiWarn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); + aiWarn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); + aiWarn(" victoryManager " + uneval(this.victoryManager.Serialize())); + aiWarn(" emergencyManager " + uneval(this.emergencyManager.Serialize())); + } + + return { + "properties": properties, + + "basesManager": this.basesManager.Serialize(), + "attackManager": this.attackManager.Serialize(), + "buildManager": this.buildManager.Serialize(), + "defenseManager": this.defenseManager.Serialize(), + "tradeManager": this.tradeManager.Serialize(), + "navalManager": this.navalManager.Serialize(), + "researchManager": this.researchManager.Serialize(), + "diplomacyManager": this.diplomacyManager.Serialize(), + "garrisonManager": this.garrisonManager.Serialize(), + "victoryManager": this.victoryManager.Serialize(), + "emergencyManager": this.emergencyManager.Serialize(), + }; } - this.basesManager.update(gameState, queues, events); - - this.navalManager.update(gameState, queues, events); - - if (this.Config.difficulty > difficulty.SANDBOX && (this.hasActiveBase() || !this.canBuildUnits)) - this.attackManager.update(gameState, queues, events); - - this.diplomacyManager.update(gameState, events); - - this.victoryManager.update(gameState, events, queues); - - // We update the capture strength at the end as it can change attack orders - if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3) - this.updateCaptureStrength(gameState); - - Engine.ProfileStop(); -}; - -Headquarters.prototype.Serialize = function() -{ - const properties = { - "phasing": this.phasing, - "lastFailedGather": this.lastFailedGather, - "firstBaseConfig": this.firstBaseConfig, - "supportRatio": this.supportRatio, - "targetNumWorkers": this.targetNumWorkers, - "fortStartTime": this.fortStartTime, - "towerStartTime": this.towerStartTime, - "fortressStartTime": this.fortressStartTime, - "bAdvanced": this.bAdvanced, - "saveResources": this.saveResources, - "saveSpace": this.saveSpace, - "needCorral": this.needCorral, - "needFarm": this.needFarm, - "needFish": this.needFish, - "maxFields": this.maxFields, - "canExpand": this.canExpand, - "canBuildUnits": this.canBuildUnits, - "navalMap": this.navalMap, - "landRegions": this.landRegions, - "navalRegions": this.navalRegions, - "decayingStructures": this.decayingStructures, - "capturableTargets": this.capturableTargets, - "capturableTargetsTime": this.capturableTargetsTime - }; - - if (this.Config.debug == -100) + Deserialize(gameState, data) { - aiWarn(" HQ serialization ---------------------"); - aiWarn(" properties " + uneval(properties)); - aiWarn(" basesManager " + uneval(this.basesManager.Serialize())); - aiWarn(" attackManager " + uneval(this.attackManager.Serialize())); - aiWarn(" buildManager " + uneval(this.buildManager.Serialize())); - aiWarn(" defenseManager " + uneval(this.defenseManager.Serialize())); - aiWarn(" tradeManager " + uneval(this.tradeManager.Serialize())); - aiWarn(" navalManager " + uneval(this.navalManager.Serialize())); - aiWarn(" researchManager " + uneval(this.researchManager.Serialize())); - aiWarn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); - aiWarn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); - aiWarn(" victoryManager " + uneval(this.victoryManager.Serialize())); - aiWarn(" emergencyManager " + uneval(this.emergencyManager.Serialize())); + for (const key in data.properties) + this[key] = data.properties[key]; + + + this.basesManager = new BasesManager(this.Config); + this.basesManager.init(gameState, true); + this.basesManager.Deserialize(gameState, data.basesManager); + + this.navalManager = new NavalManager(this.Config); + this.navalManager.init(gameState, true); + this.navalManager.Deserialize(gameState, data.navalManager); + + this.attackManager = new AttackManager(this.Config); + this.attackManager.Deserialize(gameState, data.attackManager); + this.attackManager.init(gameState); + this.attackManager.Deserialize(gameState, data.attackManager); + + this.buildManager = new BuildManager(); + this.buildManager.Deserialize(data.buildManager); + + this.defenseManager = new DefenseManager(this.Config); + this.defenseManager.Deserialize(gameState, data.defenseManager); + + this.tradeManager = new TradeManager(this.Config); + this.tradeManager.init(gameState); + this.tradeManager.Deserialize(gameState, data.tradeManager); + + this.researchManager = new ResearchManager(this.Config); + this.researchManager.Deserialize(data.researchManager); + + this.diplomacyManager = new DiplomacyManager(this.Config, true); + this.diplomacyManager.Deserialize(data.diplomacyManager); + + this.garrisonManager = new GarrisonManager(this.Config); + this.garrisonManager.Deserialize(data.garrisonManager); + + this.victoryManager = new VictoryManager(this.Config); + this.victoryManager.Deserialize(data.victoryManager); + + this.emergencyManager = new EmergencyManager(this.Config); + this.emergencyManager.Deserialize(data.emergencyManager); } - - return { - "properties": properties, - - "basesManager": this.basesManager.Serialize(), - "attackManager": this.attackManager.Serialize(), - "buildManager": this.buildManager.Serialize(), - "defenseManager": this.defenseManager.Serialize(), - "tradeManager": this.tradeManager.Serialize(), - "navalManager": this.navalManager.Serialize(), - "researchManager": this.researchManager.Serialize(), - "diplomacyManager": this.diplomacyManager.Serialize(), - "garrisonManager": this.garrisonManager.Serialize(), - "victoryManager": this.victoryManager.Serialize(), - "emergencyManager": this.emergencyManager.Serialize(), - }; -}; - -Headquarters.prototype.Deserialize = function(gameState, data) -{ - for (const key in data.properties) - this[key] = data.properties[key]; - - - this.basesManager = new BasesManager(this.Config); - this.basesManager.init(gameState, true); - this.basesManager.Deserialize(gameState, data.basesManager); - - this.navalManager = new NavalManager(this.Config); - this.navalManager.init(gameState, true); - this.navalManager.Deserialize(gameState, data.navalManager); - - this.attackManager = new AttackManager(this.Config); - this.attackManager.Deserialize(gameState, data.attackManager); - this.attackManager.init(gameState); - this.attackManager.Deserialize(gameState, data.attackManager); - - this.buildManager = new BuildManager(); - this.buildManager.Deserialize(data.buildManager); - - this.defenseManager = new DefenseManager(this.Config); - this.defenseManager.Deserialize(gameState, data.defenseManager); - - this.tradeManager = new TradeManager(this.Config); - this.tradeManager.init(gameState); - this.tradeManager.Deserialize(gameState, data.tradeManager); - - this.researchManager = new ResearchManager(this.Config); - this.researchManager.Deserialize(data.researchManager); - - this.diplomacyManager = new DiplomacyManager(this.Config, true); - this.diplomacyManager.Deserialize(data.diplomacyManager); - - this.garrisonManager = new GarrisonManager(this.Config); - this.garrisonManager.Deserialize(data.garrisonManager); - - this.victoryManager = new VictoryManager(this.Config); - this.victoryManager.Deserialize(data.victoryManager); - - this.emergencyManager = new EmergencyManager(this.Config); - this.emergencyManager.Deserialize(data.emergencyManager); -}; +} diff --git a/binaries/data/mods/public/simulation/ai/petra/navalManager.js b/binaries/data/mods/public/simulation/ai/petra/navalManager.js index 769e67587c..b1c1d68861 100644 --- a/binaries/data/mods/public/simulation/ai/petra/navalManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/navalManager.js @@ -16,906 +16,909 @@ import { Worker } from "simulation/ai/petra/worker.js"; * -Scouting, ultimately. * Also deals with handling docks, making sure we have access and stuffs like that. */ -export function NavalManager(Config) +export class NavalManager { - this.Config = Config; - // ship subCollections. Also exist for land zones, idem, not caring. - this.seaShips = []; - this.seaTransportShips = []; - this.seaWarShips = []; - this.seaFishShips = []; + seaShips = []; + seaTransportShips = []; + seaWarShips = []; + seaFishShips = []; // wanted NB per zone. - this.wantedTransportShips = []; - this.wantedWarShips = []; - this.wantedFishShips = []; + wantedTransportShips = []; + wantedWarShips = []; + wantedFishShips = []; // needed NB per zone. - this.neededTransportShips = []; - this.neededWarShips = []; + neededTransportShips = []; + neededWarShips = []; - this.transportPlans = []; + transportPlans = []; // shore-line regions where we can load and unload units - this.landingZones = {}; -} + landingZones = {}; -/** More initialisation for stuff that needs the gameState */ -NavalManager.prototype.init = function(gameState, deserializing) -{ - // docks - this.docks = gameState.getOwnStructures().filter(filters.byClasses(["Dock", "Shipyard"])); - this.docks.registerUpdates(); - - this.ships = gameState.getOwnUnits().filter(filters.and(filters.byClass("Ship"), - filters.not(filters.byMetadata(PlayerID, "role", Worker.ROLE_TRADER)))); - // note: those two can overlap (some transport ships are warships too and vice-versa). - this.transportShips = this.ships.filter(filters.and(filters.byCanGarrison(), - filters.not(filters.byClass("FishingBoat")))); - this.warShips = this.ships.filter(filters.byClass("Warship")); - this.fishShips = this.ships.filter(filters.byClass("FishingBoat")); - - this.ships.registerUpdates(); - this.transportShips.registerUpdates(); - this.warShips.registerUpdates(); - this.fishShips.registerUpdates(); - - const availableFishes = {}; - for (const fish of gameState.getFishableSupplies().values()) + constructor(Config) { - const sea = this.getFishSea(gameState, fish); - if (sea && availableFishes[sea]) - availableFishes[sea] += fish.resourceSupplyAmount(); - else if (sea) - availableFishes[sea] = fish.resourceSupplyAmount(); + this.Config = Config; } - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) + /** More initialisation for stuff that needs the gameState */ + init(gameState, deserializing) { - if (!gameState.ai.HQ.navalRegions[i]) + // docks + this.docks = gameState.getOwnStructures().filter(filters.byClasses(["Dock", "Shipyard"])); + this.docks.registerUpdates(); + + this.ships = gameState.getOwnUnits().filter(filters.and(filters.byClass("Ship"), + filters.not(filters.byMetadata(PlayerID, "role", Worker.ROLE_TRADER)))); + // note: those two can overlap (some transport ships are warships too and vice-versa). + this.transportShips = this.ships.filter(filters.and(filters.byCanGarrison(), + filters.not(filters.byClass("FishingBoat")))); + this.warShips = this.ships.filter(filters.byClass("Warship")); + this.fishShips = this.ships.filter(filters.byClass("FishingBoat")); + + this.ships.registerUpdates(); + this.transportShips.registerUpdates(); + this.warShips.registerUpdates(); + this.fishShips.registerUpdates(); + + const availableFishes = {}; + for (const fish of gameState.getFishableSupplies().values()) { - // 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); + const sea = this.getFishSea(gameState, fish); + if (sea && availableFishes[sea]) + availableFishes[sea] += fish.resourceSupplyAmount(); + else if (sea) + availableFishes[sea] = fish.resourceSupplyAmount(); } - else + + for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { - let collec = this.ships.filter(filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaShips.push(collec); - collec = this.transportShips.filter(filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaTransportShips.push(collec); - collec = this.warShips.filter(filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaWarShips.push(collec); - collec = this.fishShips.filter(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 + 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); - } - } - - if (deserializing) - return; - - // determination of the possible landing zones - const width = gameState.getPassabilityMap().width; - const length = width * gameState.getPassabilityMap().height; - for (let i = 0; i < length; ++i) - { - const land = gameState.ai.accessibility.landPassMap[i]; - if (land < 2) - continue; - const 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 (const land in this.landingZones) - { - for (const sea in this.landingZones[land]) - { - const landing = this.landingZones[land][sea]; - const nbaround = {}; - let nbcut = 0; - for (const 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); + this.neededTransportShips.push(0); + this.neededWarShips.push(0); } - nbcut = Math.min(2, nbcut); - for (const i of landing) + else { - if (nbaround[i] < nbcut) - landing.delete(i); + let collec = this.ships.filter(filters.byMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaShips.push(collec); + collec = this.transportShips.filter(filters.byMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaTransportShips.push(collec); + collec = this.warShips.filter(filters.byMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaWarShips.push(collec); + collec = this.fishShips.filter(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); } } - } - // Assign our initial docks and ships - for (const ship of this.ships.values()) - setSeaAccess(gameState, ship); - for (const dock of this.docks.values()) - setSeaAccess(gameState, dock); -}; + if (deserializing) + return; -NavalManager.prototype.updateFishingBoats = function(sea, num) -{ - if (this.wantedFishShips[sea]) - this.wantedFishShips[sea] = num; -}; - -NavalManager.prototype.resetFishingBoats = function(gameState, sea) -{ - if (sea !== undefined) - this.wantedFishShips[sea] = 0; - else - this.wantedFishShips.fill(0); -}; - -/** Get the sea, cache it if not yet done and check if in opensea */ -NavalManager.prototype.getFishSea = function(gameState, fish) -{ - let sea = fish.getMetadata(PlayerID, "sea"); - if (sea) - return sea; - const ntry = 4; - const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; - const pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); - const width = gameState.ai.accessibility.width; - const k = pos[0] + pos[1]*width; - sea = gameState.ai.accessibility.navalPassMap[k]; - fish.setMetadata(PlayerID, "sea", sea); - const radius = 120 / gameState.ai.accessibility.cellSize / ntry; - if (around.every(a => - { - for (let t = 0; t < ntry; ++t) + // determination of the possible landing zones + const width = gameState.getPassabilityMap().width; + const length = width * gameState.getPassabilityMap().height; + for (let i = 0; i < length; ++i) { - const i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - const j = pos[1] + Math.round(a[1]*radius*(ntry-t)); - if (i < 0 || i >= width || j < 0 || j >= width) + const land = gameState.ai.accessibility.landPassMap[i]; + if (land < 2) continue; - if (gameState.ai.accessibility.landPassMap[i + j*width] === 1) + const 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 (const land in this.landingZones) + { + for (const sea in this.landingZones[land]) { - const navalPass = gameState.ai.accessibility.navalPassMap[i + j*width]; - if (navalPass == sea) - return true; - else if (navalPass == 1) // we could be outside the map + const landing = this.landingZones[land][sea]; + const nbaround = {}; + let nbcut = 0; + for (const 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 (const i of landing) + { + if (nbaround[i] < nbcut) + landing.delete(i); + } + } + } + + // Assign our initial docks and ships + for (const ship of this.ships.values()) + setSeaAccess(gameState, ship); + for (const dock of this.docks.values()) + setSeaAccess(gameState, dock); + } + + updateFishingBoats(sea, num) + { + if (this.wantedFishShips[sea]) + this.wantedFishShips[sea] = num; + } + + resetFishingBoats(gameState, sea) + { + if (sea !== undefined) + this.wantedFishShips[sea] = 0; + else + this.wantedFishShips.fill(0); + } + + /** Get the sea, cache it if not yet done and check if in opensea */ + getFishSea(gameState, fish) + { + let sea = fish.getMetadata(PlayerID, "sea"); + if (sea) + return sea; + const ntry = 4; + const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; + const pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); + const width = gameState.ai.accessibility.width; + const k = pos[0] + pos[1]*width; + sea = gameState.ai.accessibility.navalPassMap[k]; + fish.setMetadata(PlayerID, "sea", sea); + const radius = 120 / gameState.ai.accessibility.cellSize / ntry; + if (around.every(a => + { + for (let t = 0; t < ntry; ++t) + { + const i = pos[0] + Math.round(a[0]*radius*(ntry-t)); + const j = pos[1] + Math.round(a[1]*radius*(ntry-t)); + if (i < 0 || i >= width || j < 0 || j >= width) continue; + if (gameState.ai.accessibility.landPassMap[i + j*width] === 1) + { + const navalPass = gameState.ai.accessibility.navalPassMap[i + j*width]; + if (navalPass == sea) + return true; + else if (navalPass == 1) // we could be outside the map + continue; + } + return false; + } + return true; + })) + fish.setMetadata(PlayerID, "opensea", true); + return sea; + } + + /** check if we can safely fish at the fish position */ + canFishSafely(gameState, fish) + { + if (fish.getMetadata(PlayerID, "opensea")) + return true; + const ntry = 2; + const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; + const territoryMap = gameState.ai.HQ.territoryMap; + const width = territoryMap.width; + const radius = 120 / territoryMap.cellSize / ntry; + const pos = territoryMap.gamePosToMapPos(fish.position()); + return around.every(a => + { + for (let t = 0; t < ntry; ++t) + { + const i = pos[0] + Math.round(a[0]*radius*(ntry-t)); + const j = pos[1] + Math.round(a[1]*radius*(ntry-t)); + if (i < 0 || i >= width || j < 0 || j >= width) + continue; + const owner = territoryMap.getOwnerIndex(i + j*width); + if (owner != 0 && gameState.isPlayerEnemy(owner)) + return false; + } + return true; + }); + } + + /** get the list of seas (or lands) around this region not connected by a dock */ + getUnconnectedSeas(gameState, region) + { + const seas = gameState.ai.accessibility.regionLinks[region].slice(); + this.docks.forEach(dock => + { + if (!dock.hasClass("Dock") || getLandAccess(gameState, dock) != region) + return; + const i = seas.indexOf(getSeaAccess(gameState, dock)); + if (i != -1) + seas.splice(i, 1); + }); + return seas; + } + + checkEvents(gameState, queues, events) + { + for (const evt of events.Create) + { + if (!evt.entity) + continue; + const ent = gameState.getEntityById(evt.entity); + if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && ent.hasClasses(["Dock", "Shipyard"])) + setSeaAccess(gameState, ent); + } + + for (const evt of events.TrainingFinished) + { + if (!evt.entities) + continue; + for (const entId of evt.entities) + { + const ent = gameState.getEntityById(entId); + if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) + continue; + setSeaAccess(gameState, ent); + } + } + + for (const evt of events.Destroy) + { + const transporter = evt.metadata?.[PlayerID]?.transporter; + if (!transporter) + continue; + const plan = this.getPlan(transporter); + if (!plan) + continue; + + const shipId = evt.entity; + if (this.Config.debug > 1) + { + aiWarn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + + plan.state); + } + if (plan.state === TransportPlan.BOARDING) + { + // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan + plan.units.forEach(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 === TransportPlan.SAILING) + { + const endIndex = plan.endIndex; + for (const ent of plan.units.values()) + { + if (!ent.position()) // unit from another ship of this plan ... do nothing + continue; + const access = getLandAccess(gameState, ent); + const 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) + this.requireTransport(gameState, ent, access, endIndex, endPos); + } + } + } + + for (const evt of events.OwnershipChanged) // capture events + { + if (evt.to !== PlayerID) + continue; + const ent = gameState.getEntityById(evt.entity); + if (ent && ent.hasClasses(["Dock", "Shipyard"])) + setSeaAccess(gameState, ent); + } + } + + + getPlan(ID) + { + for (const plan of this.transportPlans) + if (plan.ID === ID) + return plan; + return undefined; + } + + addPlan(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 + */ + requireTransport(gameState, ent, startIndex, endIndex, endPos) + { + if (!ent.canGarrison()) + return false; + + if (ent.getMetadata(PlayerID, "transport") !== undefined) + { + if (this.Config.debug > 0) + { + aiWarn("Petra naval manager error: unit " + ent.id() + + " has already required a transport"); } return false; } - return true; - })) - fish.setMetadata(PlayerID, "opensea", true); - return sea; -}; -/** check if we can safely fish at the fish position */ -NavalManager.prototype.canFishSafely = function(gameState, fish) -{ - if (fish.getMetadata(PlayerID, "opensea")) - return true; - const ntry = 2; - const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; - const territoryMap = gameState.ai.HQ.territoryMap; - const width = territoryMap.width; - const radius = 120 / territoryMap.cellSize / ntry; - const pos = territoryMap.gamePosToMapPos(fish.position()); - return around.every(a => - { - for (let t = 0; t < ntry; ++t) + const plans = []; + for (const plan of this.transportPlans) { - const i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - const j = pos[1] + Math.round(a[1]*radius*(ntry-t)); - if (i < 0 || i >= width || j < 0 || j >= width) - continue; - const owner = territoryMap.getOwnerIndex(i + j*width); - if (owner != 0 && gameState.isPlayerEnemy(owner)) - return false; - } - return true; - }); -}; - -/** get the list of seas (or lands) around this region not connected by a dock */ -NavalManager.prototype.getUnconnectedSeas = function(gameState, region) -{ - const seas = gameState.ai.accessibility.regionLinks[region].slice(); - this.docks.forEach(dock => - { - if (!dock.hasClass("Dock") || getLandAccess(gameState, dock) != region) - return; - const i = seas.indexOf(getSeaAccess(gameState, dock)); - if (i != -1) - seas.splice(i, 1); - }); - return seas; -}; - -NavalManager.prototype.checkEvents = function(gameState, queues, events) -{ - for (const evt of events.Create) - { - if (!evt.entity) - continue; - const ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && ent.hasClasses(["Dock", "Shipyard"])) - setSeaAccess(gameState, ent); - } - - for (const evt of events.TrainingFinished) - { - if (!evt.entities) - continue; - for (const entId of evt.entities) - { - const ent = gameState.getEntityById(entId); - if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) - continue; - setSeaAccess(gameState, ent); - } - } - - for (const evt of events.Destroy) - { - const transporter = evt.metadata?.[PlayerID]?.transporter; - if (!transporter) - continue; - const plan = this.getPlan(transporter); - if (!plan) - continue; - - const shipId = evt.entity; - if (this.Config.debug > 1) - { - aiWarn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + - plan.state); - } - if (plan.state === TransportPlan.BOARDING) - { - // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan - plan.units.forEach(ent => + if (plan.startIndex != startIndex || plan.endIndex != endIndex || + plan.state !== TransportPlan.BOARDING) { - 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 === TransportPlan.SAILING) - { - const endIndex = plan.endIndex; - for (const ent of plan.units.values()) - { - if (!ent.position()) // unit from another ship of this plan ... do nothing - continue; - const access = getLandAccess(gameState, ent); - const 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) - this.requireTransport(gameState, ent, access, endIndex, endPos); + continue; } + // Limit the number of siege units per transport to avoid problems when ungarrisoning + if (isSiegeUnit(ent) && plan.units.filter(unit => isSiegeUnit(unit)).length > 3) + continue; + plans.push(plan); } - } - for (const evt of events.OwnershipChanged) // capture events - { - if (evt.to !== PlayerID) - continue; - const ent = gameState.getEntityById(evt.entity); - if (ent && ent.hasClasses(["Dock", "Shipyard"])) - setSeaAccess(gameState, ent); - } -}; - - -NavalManager.prototype.getPlan = function(ID) -{ - for (const plan of this.transportPlans) - if (plan.ID === ID) - return plan; - return undefined; -}; - -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 - */ -NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos) -{ - if (!ent.canGarrison()) - return false; - - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - if (this.Config.debug > 0) + if (plans.length) { - aiWarn("Petra naval manager error: unit " + ent.id() + - " has already required a transport"); + plans.sort(plan => plan.units.length); + plans[0].addUnit(ent, endPos); + return true; } - return false; - } - const plans = []; - for (const plan of this.transportPlans) - { - if (plan.startIndex != startIndex || plan.endIndex != endIndex || - plan.state !== TransportPlan.BOARDING) - { - continue; - } - // Limit the number of siege units per transport to avoid problems when ungarrisoning - if (isSiegeUnit(ent) && plan.units.filter(unit => isSiegeUnit(unit)).length > 3) - continue; - plans.push(plan); - } - - if (plans.length) - { - plans.sort(plan => plan.units.length); - plans[0].addUnit(ent, endPos); - return true; - } - - const plan = new TransportPlan(gameState, [ent], startIndex, endIndex, endPos); - if (plan.failed) - { - if (this.Config.debug > 1) - aiWarn(">>>> 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 */ -NavalManager.prototype.splitTransport = function(gameState, plan) -{ - if (this.Config.debug > 1) - aiWarn(">>>> split of transport plan started <<<<"); - const newplan = new TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); - if (newplan.failed) - { - if (this.Config.debug > 1) - aiWarn(">>>> split of transport plan aborted <<<<"); - return false; - } - newplan.init(gameState); - - for (const ent of plan.needSplit) - { - if (ent.getMetadata(PlayerID, "onBoard")) // Should never happen. - continue; - newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos")); - plan.units.updateEnt(ent); - } - - if (newplan.units.length) - this.transportPlans.push(newplan); - return newplan.units.length != 0; -}; - -/** - * create a transport from a garrisoned ship to a land location - * needed at start game when starting with a garrisoned ship - */ -NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) -{ - const fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); - if (fromAccess !== 1) - return; - if (toAccess < 2) - return; - - for (const ship of this.ships.values()) - { - if (!ship.isGarrisonHolder() || !ship.garrisoned().length) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined) - continue; - const units = []; - for (const entId of ship.garrisoned()) - units.push(gameState.getEntityById(entId)); - // TODO check that the garrisoned units have not another purpose - const plan = new TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); + const plan = new TransportPlan(gameState, [ent], startIndex, endIndex, endPos); if (plan.failed) - continue; + { + if (this.Config.debug > 1) + aiWarn(">>>> transport plan aborted <<<<"); + return false; + } plan.init(gameState); this.transportPlans.push(plan); + return true; } -}; -// set minimal number of needed ships when a new event (new base or new attack plan) -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. -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 (const plan of this.transportPlans) + /** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */ + splitTransport(gameState, plan) { - if (!plan.needTransportShips || plan.units.length < 2) - continue; - const 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) + if (this.Config.debug > 1) + aiWarn(">>>> split of transport plan started <<<<"); + const newplan = new TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); + if (newplan.failed) { - ++this.wantedTransportShips[sea]; + if (this.Config.debug > 1) + aiWarn(">>>> split of transport plan aborted <<<<"); + return false; + } + newplan.init(gameState); + + for (const ent of plan.needSplit) + { + if (ent.getMetadata(PlayerID, "onBoard")) // Should never happen. + continue; + newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos")); + plan.units.updateEnt(ent); + } + + if (newplan.units.length) + this.transportPlans.push(newplan); + return newplan.units.length != 0; + } + + /** + * create a transport from a garrisoned ship to a land location + * needed at start game when starting with a garrisoned ship + */ + createTransportIfNeeded(gameState, fromPos, toPos, toAccess) + { + const fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); + if (fromAccess !== 1) return; + if (toAccess < 2) + return; + + for (const ship of this.ships.values()) + { + if (!ship.isGarrisonHolder() || !ship.garrisoned().length) + continue; + if (ship.getMetadata(PlayerID, "transporter") !== undefined) + continue; + const units = []; + for (const entId of ship.garrisoned()) + units.push(gameState.getEntityById(entId)); + // TODO check that the garrisoned units have not another purpose + const plan = new TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); + if (plan.failed) + continue; + plan.init(gameState); + this.transportPlans.push(plan); } } - for (let sea = 0; sea < this.neededTransportShips.length; sea++) - if (this.neededTransportShips[sea] > 2) - ++this.wantedTransportShips[sea]; -}; - -NavalManager.prototype.maintainFleet = function(gameState, queues) -{ - if (queues.ships.hasQueuedUnits()) - return; - if (!this.docks.filter(filters.isBuilt()).hasEntities()) - return; - // check if we have enough transport ships per region. - for (let sea = 0; sea < this.seaShips.length; ++sea) + // set minimal number of needed ships when a new event (new base or new attack plan) + setMinimalTransportShips(gameState, sea, number) { - if (this.seaShips[sea] === undefined) - continue; - if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) - continue; - - if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) - { - const template = this.getBestShip(gameState, sea, "transport"); - if (template) - { - queues.ships.addPlan(new TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); - continue; - } - } - - - if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) - { - const template = this.getBestShip(gameState, sea, "fishing"); - if (template) - { - queues.ships.addPlan(new TrainingPlan(gameState, template, - { "base": 0, "role": Worker.ROLE_WORKER, "sea": sea }, 1, 1)); - continue; - } - } - } -}; - -/** assigns free ships to plans that need some */ -NavalManager.prototype.assignShipsToPlans = function(gameState) -{ - for (const plan of this.transportPlans) - if (plan.needTransportShips) - plan.assignShip(gameState); -}; - -/** Return true if this ship is likeky (un)garrisoning units */ -NavalManager.prototype.isShipBoarding = function(ship) -{ - if (!ship.position()) - return false; - const plan = this.getPlan(ship.getMetadata(PlayerID, "transporter")); - if (!plan || !plan.boardingPos[ship.id()]) - return false; - return SquareVectorDistance(plan.boardingPos[ship.id()], ship.position()) < plan.boardingRange; -}; - -/** let blocking ships move apart from active ships (waiting for a better pathfinder) - * TODO Ships entity collections are currently in two parts as the trader ships are dealt with - * in the tradeManager. That should be modified to avoid dupplicating all the code here. - */ -NavalManager.prototype.moveApart = function(gameState) -{ - const blockedShips = []; - const blockedIds = []; - - for (const ship of this.ships.values()) - { - const shipPosition = ship.position(); - if (!shipPosition) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship)) - continue; - - const unitAIState = ship.unitAIState(); - if (ship.getMetadata(PlayerID, "transporter") !== undefined || - unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") - { - const previousPosition = ship.getMetadata(PlayerID, "previousPosition"); - if (!previousPosition || previousPosition[0] != shipPosition[0] || - previousPosition[1] != shipPosition[1]) - { - ship.setMetadata(PlayerID, "previousPosition", shipPosition); - ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); - continue; - } - // New transport ships receive boarding commands only on the following turn. - if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) - continue; - ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - const previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); - if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || - previousIdlePosition[1] != shipPosition[1]) - { - ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); - ship.setMetadata(PlayerID, "stationnary", undefined); - continue; - } - if (ship.getMetadata(PlayerID, "stationnary")) - continue; - ship.setMetadata(PlayerID, "stationnary", true); - // Check if there are some treasure around - if (gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - const sea = ship.getMetadata(PlayerID, "sea"); - for (const dock of - gameState.getAllyStructures().filter(filters.byClass("Dock")).values()) - { - if (getSeaAccess(gameState, dock) != sea) - continue; - if (SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); - } - - } + if (!sea) + return; + if (this.wantedTransportShips[sea] < number) + this.wantedTransportShips[sea] = number; } - for (const ship of gameState.ai.HQ.tradeManager.traders.filter(filters.byClass("Ship")).values()) + // bumps up the number of ships we want if we need more. + checkLevels(gameState, queues) { - const shipPosition = ship.position(); - if (!shipPosition) - continue; - const role = ship.getMetadata(PlayerID, "role"); - if (role === undefined || role !== Worker.ROLE_TRADER) // already accounted before - continue; + if (queues.ships.hasQueuedUnits()) + return; - const unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET") + for (let sea = 0; sea < this.neededTransportShips.length; sea++) + this.neededTransportShips[sea] = 0; + + for (const plan of this.transportPlans) { - const previousPosition = ship.getMetadata(PlayerID, "previousPosition"); - if (!previousPosition || previousPosition[0] != shipPosition[0] || - previousPosition[1] != shipPosition[1]) + if (!plan.needTransportShips || plan.units.length < 2) + continue; + const 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) { - ship.setMetadata(PlayerID, "previousPosition", shipPosition); - ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); - continue; - } - // New transport ships receives boarding commands only on the following turn. - if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) - continue; - ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - const previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); - if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || - previousIdlePosition[1] != shipPosition[1]) - { - ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); - ship.setMetadata(PlayerID, "stationnary", undefined); - continue; - } - if (ship.getMetadata(PlayerID, "stationnary")) - continue; - ship.setMetadata(PlayerID, "stationnary", true); - // Check if there are some treasure around - if (gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - const sea = ship.getMetadata(PlayerID, "sea"); - for (const dock of gameState.getAllyStructures().filter(filters.byClass("Dock")).values()) - { - if (getSeaAccess(gameState, dock) != sea) - continue; - if (SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); + ++this.wantedTransportShips[sea]; + return; } } + + for (let sea = 0; sea < this.neededTransportShips.length; sea++) + if (this.neededTransportShips[sea] > 2) + ++this.wantedTransportShips[sea]; } - for (const ship of blockedShips) + maintainFleet(gameState, queues) { - const shipPosition = ship.position(); - const sea = ship.getMetadata(PlayerID, "sea"); - for (const blockingShip of this.seaShips[sea].values()) + if (queues.ships.hasQueuedUnits()) + return; + if (!this.docks.filter(filters.isBuilt()).hasEntities()) + return; + // check if we have enough transport ships per region. + for (let sea = 0; sea < this.seaShips.length; ++sea) { - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) + if (this.seaShips[sea] === undefined) + continue; + if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) continue; - const distSquare = SquareVectorDistance(shipPosition, blockingShip.position()); - const unitAIState = blockingShip.unitAIState(); - if (blockingShip.getMetadata(PlayerID, "transporter") === undefined && - unitAIState != "INDIVIDUAL.GATHER.APPROACHING" && - unitAIState != "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") - { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); - } - for (const blockingShip of - gameState.ai.HQ.tradeManager.traders.filter(filters.byClass("Ship")).values()) - { - if (blockingShip.getMetadata(PlayerID, "sea") != sea) - continue; - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) - continue; - const role = blockingShip.getMetadata(PlayerID, "role"); - if (role === undefined || role !== Worker.ROLE_TRADER) // already accounted before - continue; - const distSquare = SquareVectorDistance(shipPosition, blockingShip.position()); - const unitAIState = blockingShip.unitAIState(); - if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET") + if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); - } - } -}; - -NavalManager.prototype.buildNavalStructures = function(gameState, queues) -{ - if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.hasPotentialBase()) - return; - - if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) - { - if (queues.dock.countQueuedUnitsWithClass("Dock") === 0 && - !gameState.getOwnStructures().filter(filters.and(filters.byClass("Dock"), - filters.isFoundation())).hasEntities() && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}/dock")) - { - let dockStarted = false; - for (const base of gameState.ai.HQ.baseManagers()) - { - if (dockStarted) - break; - if (!base.anchor || base.constructing) - continue; - const remaining = this.getUnconnectedSeas(gameState, base.accessIndex); - for (const sea of remaining) + const template = this.getBestShip(gameState, sea, "transport"); + if (template) { - if (!gameState.ai.HQ.navalRegions[sea]) - continue; - const wantedLand = {}; - wantedLand[base.accessIndex] = true; - queues.dock.addPlan(new ConstructionPlan(gameState, "structures/{civ}/dock", - { "land": wantedLand, "sea": sea })); - dockStarted = true; - break; + queues.ships.addPlan(new TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); + continue; + } + } + + + if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) + { + const template = this.getBestShip(gameState, sea, "fishing"); + if (template) + { + queues.ships.addPlan(new TrainingPlan(gameState, template, + { "base": 0, "role": Worker.ROLE_WORKER, "sea": sea }, 1, 1)); + continue; } } } } - if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || - queues.militaryBuilding.hasQueuedUnits()) - return; - if (!this.docks.filter(filters.byClass("Dock")).hasEntities() || - this.docks.filter(filters.byClass("Shipyard")).hasEntities()) + /** assigns free ships to plans that need some */ + assignShipsToPlans(gameState) { - return; + for (const plan of this.transportPlans) + if (plan.needTransportShips) + plan.assignShip(gameState); } - // Use in priority resources to build a Market. - if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market")) - return; - let template; - if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/super_dock")) - template = "structures/{civ}/super_dock"; - else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/shipyard")) - template = "structures/{civ}/shipyard"; - else - return; - const wantedLand = {}; - for (const base of gameState.ai.HQ.baseManagers()) - if (base.anchor) - wantedLand[base.accessIndex] = true; - const sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); - queues.militaryBuilding.addPlan( - new ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); -}; -/** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ -NavalManager.prototype.getBestShip = function(gameState, sea, goal) -{ - const civ = gameState.getPlayerCiv(); - const trainableShips = []; - gameState.getOwnTrainingFacilities().filter(filters.byMetadata(PlayerID, "sea", sea)).forEach( - function(ent) + /** Return true if this ship is likeky (un)garrisoning units */ + isShipBoarding(ship) + { + if (!ship.position()) + return false; + const plan = this.getPlan(ship.getMetadata(PlayerID, "transporter")); + if (!plan || !plan.boardingPos[ship.id()]) + return false; + return SquareVectorDistance(plan.boardingPos[ship.id()], ship.position()) < plan.boardingRange; + } + + /** let blocking ships move apart from active ships (waiting for a better pathfinder) + * TODO Ships entity collections are currently in two parts as the trader ships are dealt with + * in the tradeManager. That should be modified to avoid dupplicating all the code here. + */ + moveApart(gameState) + { + const blockedShips = []; + const blockedIds = []; + + for (const ship of this.ships.values()) { - const trainables = ent.trainableEntities(civ); - for (const trainable of trainables) + const shipPosition = ship.position(); + if (!shipPosition) + continue; + if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship)) + continue; + + const unitAIState = ship.unitAIState(); + if (ship.getMetadata(PlayerID, "transporter") !== undefined || + unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || + unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { - if (gameState.isTemplateDisabled(trainable)) + const previousPosition = ship.getMetadata(PlayerID, "previousPosition"); + if (!previousPosition || previousPosition[0] != shipPosition[0] || + previousPosition[1] != shipPosition[1]) + { + ship.setMetadata(PlayerID, "previousPosition", shipPosition); + ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); continue; - const template = gameState.getTemplate(trainable); - if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) - trainableShips.push(trainable); + } + // New transport ships receive boarding commands only on the following turn. + if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) + continue; + ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); + blockedShips.push(ship); + blockedIds.push(ship.id()); } - }); + else if (ship.isIdle()) + { + const previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); + if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || + previousIdlePosition[1] != shipPosition[1]) + { + ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); + ship.setMetadata(PlayerID, "stationnary", undefined); + continue; + } + if (ship.getMetadata(PlayerID, "stationnary")) + continue; + ship.setMetadata(PlayerID, "stationnary", true); + // Check if there are some treasure around + if (gatherTreasure(gameState, ship, true)) + continue; + // Do not stay idle near a dock to not disturb other ships + const sea = ship.getMetadata(PlayerID, "sea"); + for (const dock of + gameState.getAllyStructures().filter(filters.byClass("Dock")).values()) + { + if (getSeaAccess(gameState, dock) != sea) + continue; + if (SquareVectorDistance(shipPosition, dock.position()) > 4900) + continue; + ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); + } - let best = 0; - let bestShip; - const limits = gameState.getEntityLimits(); - const current = gameState.getEntityCounts(); - for (const trainable of trainableShips) - { - const template = gameState.getTemplate(trainable); - if (!template.available(gameState)) - continue; - - const category = template.trainingCategory(); - if (category && limits[category] && current[category] >= limits[category]) - continue; - - const 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 + + for (const ship of gameState.ai.HQ.tradeManager.traders.filter(filters.byClass("Ship")).values()) { - let capacity = +(template.garrisonMax() || 0); - if (capacity < 2) + const shipPosition = ship.position(); + if (!shipPosition) continue; - capacity += 10*arrows; - if (MatchesClassList(template.garrisonableClasses(), "Siege")) - capacity += 50; - if (best > capacity) + const role = ship.getMetadata(PlayerID, "role"); + if (role === undefined || role !== Worker.ROLE_TRADER) // already accounted before continue; - best = capacity; + + const unitAIState = ship.unitAIState(); + if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET") + { + const previousPosition = ship.getMetadata(PlayerID, "previousPosition"); + if (!previousPosition || previousPosition[0] != shipPosition[0] || + previousPosition[1] != shipPosition[1]) + { + ship.setMetadata(PlayerID, "previousPosition", shipPosition); + ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); + continue; + } + // New transport ships receives boarding commands only on the following turn. + if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) + continue; + ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); + blockedShips.push(ship); + blockedIds.push(ship.id()); + } + else if (ship.isIdle()) + { + const previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); + if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || + previousIdlePosition[1] != shipPosition[1]) + { + ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); + ship.setMetadata(PlayerID, "stationnary", undefined); + continue; + } + if (ship.getMetadata(PlayerID, "stationnary")) + continue; + ship.setMetadata(PlayerID, "stationnary", true); + // Check if there are some treasure around + if (gatherTreasure(gameState, ship, true)) + continue; + // Do not stay idle near a dock to not disturb other ships + const sea = ship.getMetadata(PlayerID, "sea"); + for (const dock of gameState.getAllyStructures().filter(filters.byClass("Dock")).values()) + { + if (getSeaAccess(gameState, dock) != sea) + continue; + if (SquareVectorDistance(shipPosition, dock.position()) > 4900) + continue; + ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); + } + } } - else if (goal === "fishing") - if (!template.hasClass("FishingBoat")) + + for (const ship of blockedShips) + { + const shipPosition = ship.position(); + const sea = ship.getMetadata(PlayerID, "sea"); + for (const blockingShip of this.seaShips[sea].values()) + { + if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) + continue; + const distSquare = SquareVectorDistance(shipPosition, blockingShip.position()); + const unitAIState = blockingShip.unitAIState(); + if (blockingShip.getMetadata(PlayerID, "transporter") === undefined && + unitAIState != "INDIVIDUAL.GATHER.APPROACHING" && + unitAIState != "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") + { + if (distSquare < 1600) + blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); + } + else if (distSquare < 900) + blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); + } + + for (const blockingShip of + gameState.ai.HQ.tradeManager.traders.filter(filters.byClass("Ship")).values()) + { + if (blockingShip.getMetadata(PlayerID, "sea") != sea) + continue; + if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) + continue; + const role = blockingShip.getMetadata(PlayerID, "role"); + if (role === undefined || role !== Worker.ROLE_TRADER) // already accounted before + continue; + const distSquare = SquareVectorDistance(shipPosition, blockingShip.position()); + const unitAIState = blockingShip.unitAIState(); + if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET") + { + if (distSquare < 1600) + blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); + } + else if (distSquare < 900) + blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); + } + } + } + + buildNavalStructures(gameState, queues) + { + if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.hasPotentialBase()) + return; + + if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) + { + if (queues.dock.countQueuedUnitsWithClass("Dock") === 0 && + !gameState.getOwnStructures().filter(filters.and(filters.byClass("Dock"), + filters.isFoundation())).hasEntities() && + gameState.ai.HQ.canBuild(gameState, "structures/{civ}/dock")) + { + let dockStarted = false; + for (const base of gameState.ai.HQ.baseManagers()) + { + if (dockStarted) + break; + if (!base.anchor || base.constructing) + continue; + const remaining = this.getUnconnectedSeas(gameState, base.accessIndex); + for (const sea of remaining) + { + if (!gameState.ai.HQ.navalRegions[sea]) + continue; + const wantedLand = {}; + wantedLand[base.accessIndex] = true; + queues.dock.addPlan(new ConstructionPlan(gameState, "structures/{civ}/dock", + { "land": wantedLand, "sea": sea })); + dockStarted = true; + break; + } + } + } + } + + if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || + queues.militaryBuilding.hasQueuedUnits()) + return; + if (!this.docks.filter(filters.byClass("Dock")).hasEntities() || + this.docks.filter(filters.byClass("Shipyard")).hasEntities()) + { + return; + } + // Use in priority resources to build a Market. + if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && + gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market")) + return; + let template; + if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/super_dock")) + template = "structures/{civ}/super_dock"; + else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/shipyard")) + template = "structures/{civ}/shipyard"; + else + return; + const wantedLand = {}; + for (const base of gameState.ai.HQ.baseManagers()) + if (base.anchor) + wantedLand[base.accessIndex] = true; + const sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); + queues.militaryBuilding.addPlan( + new ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); + } + + /** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ + getBestShip(gameState, sea, goal) + { + const civ = gameState.getPlayerCiv(); + const trainableShips = []; + gameState.getOwnTrainingFacilities().filter(filters.byMetadata(PlayerID, "sea", sea)).forEach( + function(ent) + { + const trainables = ent.trainableEntities(civ); + for (const trainable of trainables) + { + if (gameState.isTemplateDisabled(trainable)) + continue; + const template = gameState.getTemplate(trainable); + if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) + trainableShips.push(trainable); + } + }); + + let best = 0; + let bestShip; + const limits = gameState.getEntityLimits(); + const current = gameState.getEntityCounts(); + for (const trainable of trainableShips) + { + const template = gameState.getTemplate(trainable); + if (!template.available(gameState)) continue; - bestShip = trainable; + + const category = template.trainingCategory(); + if (category && limits[category] && current[category] >= limits[category]) + continue; + + const 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; } - return bestShip; -}; -NavalManager.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Naval Manager update"); - - // close previous transport plans if finished - for (let i = 0; i < this.transportPlans.length; ++i) + update(gameState, queues, events) { - const remaining = this.transportPlans[i].update(gameState); - if (remaining) - continue; - if (this.Config.debug > 1) - aiWarn("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); + Engine.ProfileStart("Naval Manager update"); - // and require for more ships/structures if needed - if (gameState.ai.playedTurn % 3 === 0) + // close previous transport plans if finished + for (let i = 0; i < this.transportPlans.length; ++i) + { + const remaining = this.transportPlans[i].update(gameState); + if (remaining) + continue; + if (this.Config.debug > 1) + aiWarn("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(); + } + + Serialize() { - this.checkLevels(gameState, queues); - this.maintainFleet(gameState, queues); - this.buildNavalStructures(gameState, queues); + const properties = { + "wantedTransportShips": this.wantedTransportShips, + "wantedWarShips": this.wantedWarShips, + "wantedFishShips": this.wantedFishShips, + "neededTransportShips": this.neededTransportShips, + "neededWarShips": this.neededWarShips, + "landingZones": this.landingZones + }; + + const transports = {}; + for (const plan in this.transportPlans) + transports[plan] = this.transportPlans[plan].Serialize(); + + return { "properties": properties, "transports": transports }; } - // let inactive ships move apart from active ones (waiting for a better pathfinder) - this.moveApart(gameState); - Engine.ProfileStop(); -}; - -NavalManager.prototype.Serialize = function() -{ - const properties = { - "wantedTransportShips": this.wantedTransportShips, - "wantedWarShips": this.wantedWarShips, - "wantedFishShips": this.wantedFishShips, - "neededTransportShips": this.neededTransportShips, - "neededWarShips": this.neededWarShips, - "landingZones": this.landingZones - }; - - const transports = {}; - for (const plan in this.transportPlans) - transports[plan] = this.transportPlans[plan].Serialize(); - - return { "properties": properties, "transports": transports }; -}; - -NavalManager.prototype.Deserialize = function(gameState, data) -{ - for (const key in data.properties) - this[key] = data.properties[key]; - - this.transportPlans = []; - for (const i in data.transports) + Deserialize(gameState, data) { - const dataPlan = data.transports[i]; - const plan = new TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); - plan.Deserialize(dataPlan); - plan.init(gameState); - this.transportPlans.push(plan); + for (const key in data.properties) + this[key] = data.properties[key]; + + this.transportPlans = []; + for (const i in data.transports) + { + const dataPlan = data.transports[i]; + const plan = new TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); + plan.Deserialize(dataPlan); + plan.init(gameState); + this.transportPlans.push(plan); + } } -}; +} diff --git a/binaries/data/mods/public/simulation/ai/petra/queue.js b/binaries/data/mods/public/simulation/ai/petra/queue.js index 214c460cb0..88e4d4156e 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queue.js +++ b/binaries/data/mods/public/simulation/ai/petra/queue.js @@ -7,160 +7,160 @@ import { TrainingPlan } from "simulation/ai/petra/queueplanTraining.js"; /** * Holds a list of wanted plans to train or construct */ -export function Queue() +export class Queue { - this.plans = []; - this.paused = false; - this.switched = 0; + plans = []; + paused = false; + switched = 0; + + empty() + { + this.plans = []; + } + + addPlan(newPlan) + { + if (!newPlan) + return; + for (const plan of this.plans) + { + if (newPlan.category === "unit" && plan.type == newPlan.type && plan.number + newPlan.number <= plan.maxMerge) + { + plan.addItem(newPlan.number); + return; + } + else if (newPlan.category === "technology" && plan.type === newPlan.type) + return; + } + this.plans.push(newPlan); + } + + check(gameState) + { + while (this.plans.length > 0) + { + if (!this.plans[0].isInvalid(gameState)) + return; + const plan = this.plans.shift(); + if (plan.queueToReset) + gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]); + } + } + + getNext() + { + if (this.plans.length > 0) + return this.plans[0]; + return null; + } + + startNext(gameState) + { + if (this.plans.length > 0) + { + this.plans.shift().start(gameState); + return true; + } + return false; + } + + /** + * returns the maximal account we'll accept for this queue. + * Currently all the cost of the first element and fraction of that of the second + */ + maxAccountWanted(gameState, fraction) + { + const cost = new ResourcesManager(); + if (this.plans.length > 0 && this.plans[0].isGo(gameState)) + cost.add(this.plans[0].getCost()); + if (this.plans.length > 1 && this.plans[1].isGo(gameState) && fraction > 0) + { + const costs = this.plans[1].getCost(); + costs.multiply(fraction); + cost.add(costs); + } + return cost; + } + + queueCost() + { + const cost = new ResourcesManager(); + for (const plan of this.plans) + cost.add(plan.getCost()); + return cost; + } + + length() + { + return this.plans.length; + } + + hasQueuedUnits() + { + return this.plans.length > 0; + } + + countQueuedUnits() + { + let count = 0; + for (const plan of this.plans) + count += plan.number; + return count; + } + + hasQueuedUnitsWithClass(classe) + { + return this.plans.some(plan => plan.template && plan.template.hasClass(classe)); + } + + countQueuedUnitsWithClass(classe) + { + let count = 0; + for (const plan of this.plans) + if (plan.template && plan.template.hasClass(classe)) + count += plan.number; + return count; + } + + countQueuedUnitsWithMetadata(data, value) + { + let count = 0; + for (const plan of this.plans) + if (plan.metadata[data] && plan.metadata[data] == value) + count += plan.number; + return count; + } + + Serialize() + { + const plans = []; + for (const plan of this.plans) + plans.push(plan.Serialize()); + + return { "plans": plans, "paused": this.paused, "switched": this.switched }; + } + + Deserialize(gameState, data) + { + this.paused = data.paused; + this.switched = data.switched; + this.plans = []; + for (const dataPlan of data.plans) + { + let plan; + if (dataPlan.category == "unit") + plan = new TrainingPlan(gameState, dataPlan.type); + else if (dataPlan.category == "building") + plan = new ConstructionPlan(gameState, dataPlan.type); + else if (dataPlan.category == "technology") + plan = new ResearchPlan(gameState, dataPlan.type); + else + { + aiWarn("Petra deserialization error: plan unknown " + uneval(dataPlan)); + continue; + } + plan.Deserialize(gameState, dataPlan); + this.plans.push(plan); + } + } } - -Queue.prototype.empty = function() -{ - this.plans = []; -}; - -Queue.prototype.addPlan = function(newPlan) -{ - if (!newPlan) - return; - for (const plan of this.plans) - { - if (newPlan.category === "unit" && plan.type == newPlan.type && plan.number + newPlan.number <= plan.maxMerge) - { - plan.addItem(newPlan.number); - return; - } - else if (newPlan.category === "technology" && plan.type === newPlan.type) - return; - } - this.plans.push(newPlan); -}; - -Queue.prototype.check= function(gameState) -{ - while (this.plans.length > 0) - { - if (!this.plans[0].isInvalid(gameState)) - return; - const plan = this.plans.shift(); - if (plan.queueToReset) - gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]); - } -}; - -Queue.prototype.getNext = function() -{ - if (this.plans.length > 0) - return this.plans[0]; - return null; -}; - -Queue.prototype.startNext = function(gameState) -{ - if (this.plans.length > 0) - { - this.plans.shift().start(gameState); - return true; - } - return false; -}; - -/** - * returns the maximal account we'll accept for this queue. - * Currently all the cost of the first element and fraction of that of the second - */ -Queue.prototype.maxAccountWanted = function(gameState, fraction) -{ - const cost = new ResourcesManager(); - if (this.plans.length > 0 && this.plans[0].isGo(gameState)) - cost.add(this.plans[0].getCost()); - if (this.plans.length > 1 && this.plans[1].isGo(gameState) && fraction > 0) - { - const costs = this.plans[1].getCost(); - costs.multiply(fraction); - cost.add(costs); - } - return cost; -}; - -Queue.prototype.queueCost = function() -{ - const cost = new ResourcesManager(); - for (const plan of this.plans) - cost.add(plan.getCost()); - return cost; -}; - -Queue.prototype.length = function() -{ - return this.plans.length; -}; - -Queue.prototype.hasQueuedUnits = function() -{ - return this.plans.length > 0; -}; - -Queue.prototype.countQueuedUnits = function() -{ - let count = 0; - for (const plan of this.plans) - count += plan.number; - return count; -}; - -Queue.prototype.hasQueuedUnitsWithClass = function(classe) -{ - return this.plans.some(plan => plan.template && plan.template.hasClass(classe)); -}; - -Queue.prototype.countQueuedUnitsWithClass = function(classe) -{ - let count = 0; - for (const plan of this.plans) - if (plan.template && plan.template.hasClass(classe)) - count += plan.number; - return count; -}; - -Queue.prototype.countQueuedUnitsWithMetadata = function(data, value) -{ - let count = 0; - for (const plan of this.plans) - if (plan.metadata[data] && plan.metadata[data] == value) - count += plan.number; - return count; -}; - -Queue.prototype.Serialize = function() -{ - const plans = []; - for (const plan of this.plans) - plans.push(plan.Serialize()); - - return { "plans": plans, "paused": this.paused, "switched": this.switched }; -}; - -Queue.prototype.Deserialize = function(gameState, data) -{ - this.paused = data.paused; - this.switched = data.switched; - this.plans = []; - for (const dataPlan of data.plans) - { - let plan; - if (dataPlan.category == "unit") - plan = new TrainingPlan(gameState, dataPlan.type); - else if (dataPlan.category == "building") - plan = new ConstructionPlan(gameState, dataPlan.type); - else if (dataPlan.category == "technology") - plan = new ResearchPlan(gameState, dataPlan.type); - else - { - aiWarn("Petra deserialization error: plan unknown " + uneval(dataPlan)); - continue; - } - plan.Deserialize(gameState, dataPlan); - this.plans.push(plan); - } -}; diff --git a/binaries/data/mods/public/simulation/ai/petra/queueManager.js b/binaries/data/mods/public/simulation/ai/petra/queueManager.js index 30b25922ac..64c68ec144 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueManager.js @@ -40,597 +40,600 @@ function sortQueues(queueArrays, priorities) }); } -export function QueueManager(Config, queues) +export class QueueManager { - this.Config = Config; - this.queues = queues; - this.priorities = {}; - for (const i in Config.priorities) - this.priorities[i] = Config.priorities[i]; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (const q in this.queues) + constructor(Config, queues) { - this.accounts[q] = new ResourcesManager(); - this.queueArrays.push([q, this.queues[q]]); + this.Config = Config; + this.queues = queues; + this.priorities = {}; + for (const i in Config.priorities) + this.priorities[i] = Config.priorities[i]; + this.accounts = {}; + + // the sorting is updated on priority change. + this.queueArrays = []; + for (const q in this.queues) + { + this.accounts[q] = new ResourcesManager(); + this.queueArrays.push([q, this.queues[q]]); + } + sortQueues(this.queueArrays, this.priorities); } - sortQueues(this.queueArrays, this.priorities); -} -QueueManager.prototype.getAvailableResources = function(gameState) -{ - const resources = gameState.getResources(); - for (const key in this.queues) - resources.subtract(this.accounts[key]); - return resources; -}; - -QueueManager.prototype.getTotalAccountedResources = function() -{ - const resources = new ResourcesManager(); - for (const key in this.queues) - resources.add(this.accounts[key]); - return resources; -}; - -QueueManager.prototype.currentNeeds = function(gameState) -{ - const needed = new ResourcesManager(); - // queueArrays because it's faster. - for (const q of this.queueArrays) + getAvailableResources(gameState) { - const queue = q[1]; - if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) - continue; - const costs = queue.plans[0].getCost(); - needed.add(costs); + const resources = gameState.getResources(); + for (const key in this.queues) + resources.subtract(this.accounts[key]); + return resources; } - // get out current resources, not removing accounts. - const current = gameState.getResources(); - for (const res of Resources.GetCodes()) - needed[res] = Math.max(0, needed[res] - current[res]); - return needed; -}; - -// calculate the gather rates we'd want to be able to start all elements in our queues -// TODO: many things. -QueueManager.prototype.wantedGatherRates = function(gameState) -{ - // default values for first turn when we have not yet set our queues. - if (gameState.ai.playedTurn === 0) + getTotalAccountedResources() { - const ret = {}; + const resources = new ResourcesManager(); + for (const key in this.queues) + resources.add(this.accounts[key]); + return resources; + } + + currentNeeds(gameState) + { + const needed = new ResourcesManager(); + // queueArrays because it's faster. + for (const q of this.queueArrays) + { + const queue = q[1]; + if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) + continue; + const costs = queue.plans[0].getCost(); + needed.add(costs); + } + // get out current resources, not removing accounts. + const current = gameState.getResources(); for (const res of Resources.GetCodes()) - ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; - return ret; + needed[res] = Math.max(0, needed[res] - current[res]); + + return needed; } - // get out current resources, not removing accounts. - const current = gameState.getResources(); - // short queue is the first item of a queue, assumed to be ready in 30s - // medium queue is the second item of a queue, assumed to be ready in 60s - // long queue contains the isGo=false items, assumed to be ready in 300s - const totalShort = {}; - const totalMedium = {}; - const totalLong = {}; - for (const res of Resources.GetCodes()) + // calculate the gather rates we'd want to be able to start all elements in our queues + // TODO: many things. + wantedGatherRates(gameState) { - totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; - totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; - totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; - } - let total; - // queueArrays because it's faster. - for (const q of this.queueArrays) - { - const queue = q[1]; - if (queue.paused) - continue; - for (let j = 0; j < queue.length(); ++j) + // default values for first turn when we have not yet set our queues. + if (gameState.ai.playedTurn === 0) { - if (j > 1) - break; - const cost = queue.plans[j].getCost(); - if (queue.plans[j].isGo(gameState)) + const ret = {}; + for (const res of Resources.GetCodes()) + ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; + return ret; + } + + // get out current resources, not removing accounts. + const current = gameState.getResources(); + // short queue is the first item of a queue, assumed to be ready in 30s + // medium queue is the second item of a queue, assumed to be ready in 60s + // long queue contains the isGo=false items, assumed to be ready in 300s + const totalShort = {}; + const totalMedium = {}; + const totalLong = {}; + for (const res of Resources.GetCodes()) + { + totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; + totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; + totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; + } + let total; + // queueArrays because it's faster. + for (const q of this.queueArrays) + { + const queue = q[1]; + if (queue.paused) + continue; + for (let j = 0; j < queue.length(); ++j) { - if (j === 0) - total = totalShort; + if (j > 1) + break; + const cost = queue.plans[j].getCost(); + if (queue.plans[j].isGo(gameState)) + { + if (j === 0) + total = totalShort; + else + total = totalMedium; + } else - total = totalMedium; - } - else - total = totalLong; - for (const type in total) - total[type] += cost[type]; - if (!queue.plans[j].isGo(gameState)) - break; - } - } - // global rates - const rates = {}; - let diff; - for (const res of Resources.GetCodes()) - { - if (current[res] > 0) - { - diff = Math.min(current[res], totalShort[res]); - totalShort[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - { - diff = Math.min(current[res], totalMedium[res]); - totalMedium[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - totalLong[res] -= Math.min(current[res], totalLong[res]); - } - } - rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; - } - - return rates; -}; - -QueueManager.prototype.printQueues = function(gameState) -{ - let numWorkers = 0; - gameState.getOwnUnits().forEach(ent => - { - if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER && - ent.getMetadata(PlayerID, "plan") === undefined) - { - numWorkers++; - } - }); - aiWarn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + - numWorkers); - for (const i in this.queues) - { - const q = this.queues[i]; - if (q.hasQueuedUnits()) - { - aiWarn(i + ": ( with priority " + this.priorities[i] +" and accounts " + - uneval(this.accounts[i]) +")"); - aiWarn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); - } - for (const plan of q.plans) - { - let qStr = " " + plan.type + " "; - if (plan.number) - qStr += "x" + plan.number; - qStr += " isGo " + plan.isGo(gameState); - aiWarn(qStr); - } - } - aiWarn("Accounts"); - for (const p in this.accounts) - aiWarn(p + ": " + uneval(this.accounts[p])); - aiWarn("Current Resources: " + uneval(gameState.getResources())); - aiWarn("Available Resources: " + uneval(this.getAvailableResources(gameState))); - aiWarn("Wanted Gather Rates: " + uneval(gameState.ai.HQ.GetWantedGatherRates(gameState))); - aiWarn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); - aiWarn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); - aiWarn("------------------------------------"); -}; - -QueueManager.prototype.clear = function() -{ - for (const i in this.queues) - this.queues[i].empty(); -}; - -/** - * set accounts of queue i from the unaccounted resources - */ -QueueManager.prototype.setAccounts = function(gameState, cost, i) -{ - const available = this.getAvailableResources(gameState); - for (const res of Resources.GetCodes()) - { - if (this.accounts[i][res] >= cost[res]) - continue; - this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); - } -}; - -/** - * transfer accounts from queue i to queue j - */ -QueueManager.prototype.transferAccounts = function(cost, i, j) -{ - for (const res of Resources.GetCodes()) - { - if (this.accounts[j][res] >= cost[res]) - continue; - const diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); - this.accounts[i][res] -= diff; - this.accounts[j][res] += diff; - } -}; - -/** - * distribute the resources between the different queues according to their priorities - */ -QueueManager.prototype.distributeResources = function(gameState) -{ - const availableRes = this.getAvailableResources(gameState); - for (const res of Resources.GetCodes()) - { - if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) - { - const total = gameState.getResources()[res]; - const scale = total / (total - availableRes[res]); - availableRes[res] = total; - for (const j in this.queues) - { - this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); - availableRes[res] -= this.accounts[j][res]; - } - } - - if (!availableRes[res]) - { - this.switchResource(gameState, res); - continue; - } - - let totalPriority = 0; - const tempPrio = {}; - const maxNeed = {}; - // Okay so this is where it gets complicated. - // If a queue requires "res" for the next elements (in the queue) - // And the account is not high enough for it. - // Then we add it to the total priority. - // To try and be clever, we don't want a long queue to hog all resources. So two things: - // -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2). - // -queues accounts are capped at "resources for the first + 60% of the next" - // This avoids getting a high priority queue with many elements hogging all of one resource - // uselessly while it awaits for other resources. - for (const j in this.queues) - { - // returns exactly the correct amount, ie 0 if we're not go. - const queueCost = this.queues[j].maxAccountWanted(gameState, 0.6); - if (this.queues[j].hasQueuedUnits() && this.accounts[j][res] < queueCost[res] && !this.queues[j].paused) - { - // adding us to the list of queues that need an update. - tempPrio[j] = this.priorities[j]; - maxNeed[j] = queueCost[res] - this.accounts[j][res]; - // if we have enough of that resource for our first item in the queue, diminish our priority. - if (this.accounts[j][res] >= this.queues[j].getNext().getCost()[res]) - tempPrio[j] /= 2; - - if (tempPrio[j]) - totalPriority += tempPrio[j]; - } - else if (this.accounts[j][res] > queueCost[res]) - { - availableRes[res] += this.accounts[j][res] - queueCost[res]; - this.accounts[j][res] = queueCost[res]; - } - } - // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" - // But we'll sometimes allow less if that would overflow. - let available = availableRes[res]; - let missing = false; - for (const j in tempPrio) - { - // we'll add at much what can be allowed to this queue. - let toAdd = Math.floor(availableRes[res] * tempPrio[j]/totalPriority); - if (toAdd >= maxNeed[j]) - toAdd = maxNeed[j]; - else - missing = true; - this.accounts[j][res] += toAdd; - maxNeed[j] -= toAdd; - available -= toAdd; - } - if (missing && available > 0) // distribute the rest (due to floor) in any queue - { - for (const j in tempPrio) - { - const toAdd = Math.min(maxNeed[j], available); - this.accounts[j][res] += toAdd; - available -= toAdd; - if (available <= 0) + total = totalLong; + for (const type in total) + total[type] += cost[type]; + if (!queue.plans[j].isGo(gameState)) break; } } - if (available < 0) - aiWarn("Petra: problem with remaining " + res + " in queueManager " + available); + // global rates + const rates = {}; + let diff; + for (const res of Resources.GetCodes()) + { + if (current[res] > 0) + { + diff = Math.min(current[res], totalShort[res]); + totalShort[res] -= diff; + current[res] -= diff; + if (current[res] > 0) + { + diff = Math.min(current[res], totalMedium[res]); + totalMedium[res] -= diff; + current[res] -= diff; + if (current[res] > 0) + totalLong[res] -= Math.min(current[res], totalLong[res]); + } + } + rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; + } + + return rates; } -}; -QueueManager.prototype.switchResource = function(gameState, res) -{ - // We have no available resources, see if we can't "compact" them in one queue. - // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. - // TODO: this isn't perfect compression. - for (const j in this.queues) + printQueues(gameState) { - if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) - continue; + let numWorkers = 0; + gameState.getOwnUnits().forEach(ent => + { + if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_WORKER && + ent.getMetadata(PlayerID, "plan") === undefined) + { + numWorkers++; + } + }); + aiWarn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + + numWorkers); + for (const i in this.queues) + { + const q = this.queues[i]; + if (q.hasQueuedUnits()) + { + aiWarn(i + ": ( with priority " + this.priorities[i] +" and accounts " + + uneval(this.accounts[i]) +")"); + aiWarn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); + } + for (const plan of q.plans) + { + let qStr = " " + plan.type + " "; + if (plan.number) + qStr += "x" + plan.number; + qStr += " isGo " + plan.isGo(gameState); + aiWarn(qStr); + } + } + aiWarn("Accounts"); + for (const p in this.accounts) + aiWarn(p + ": " + uneval(this.accounts[p])); + aiWarn("Current Resources: " + uneval(gameState.getResources())); + aiWarn("Available Resources: " + uneval(this.getAvailableResources(gameState))); + aiWarn("Wanted Gather Rates: " + uneval(gameState.ai.HQ.GetWantedGatherRates(gameState))); + aiWarn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); + aiWarn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); + aiWarn("------------------------------------"); + } - const queue = this.queues[j]; - const queueCost = queue.maxAccountWanted(gameState, 0); - if (this.accounts[j][res] >= queueCost[res]) - continue; + clear() + { + for (const i in this.queues) + this.queues[i].empty(); + } + + /** + * set accounts of queue i from the unaccounted resources + */ + setAccounts(gameState, cost, i) + { + const available = this.getAvailableResources(gameState); + for (const res of Resources.GetCodes()) + { + if (this.accounts[i][res] >= cost[res]) + continue; + this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); + } + } + + /** + * transfer accounts from queue i to queue j + */ + transferAccounts(cost, i, j) + { + for (const res of Resources.GetCodes()) + { + if (this.accounts[j][res] >= cost[res]) + continue; + const diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); + this.accounts[i][res] -= diff; + this.accounts[j][res] += diff; + } + } + + /** + * distribute the resources between the different queues according to their priorities + */ + distributeResources(gameState) + { + const availableRes = this.getAvailableResources(gameState); + for (const res of Resources.GetCodes()) + { + if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) + { + const total = gameState.getResources()[res]; + const scale = total / (total - availableRes[res]); + availableRes[res] = total; + for (const j in this.queues) + { + this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); + availableRes[res] -= this.accounts[j][res]; + } + } + + if (!availableRes[res]) + { + this.switchResource(gameState, res); + continue; + } + + let totalPriority = 0; + const tempPrio = {}; + const maxNeed = {}; + // Okay so this is where it gets complicated. + // If a queue requires "res" for the next elements (in the queue) + // And the account is not high enough for it. + // Then we add it to the total priority. + // To try and be clever, we don't want a long queue to hog all resources. So two things: + // -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2). + // -queues accounts are capped at "resources for the first + 60% of the next" + // This avoids getting a high priority queue with many elements hogging all of one resource + // uselessly while it awaits for other resources. + for (const j in this.queues) + { + // returns exactly the correct amount, ie 0 if we're not go. + const queueCost = this.queues[j].maxAccountWanted(gameState, 0.6); + if (this.queues[j].hasQueuedUnits() && this.accounts[j][res] < queueCost[res] && !this.queues[j].paused) + { + // adding us to the list of queues that need an update. + tempPrio[j] = this.priorities[j]; + maxNeed[j] = queueCost[res] - this.accounts[j][res]; + // if we have enough of that resource for our first item in the queue, diminish our priority. + if (this.accounts[j][res] >= this.queues[j].getNext().getCost()[res]) + tempPrio[j] /= 2; + + if (tempPrio[j]) + totalPriority += tempPrio[j]; + } + else if (this.accounts[j][res] > queueCost[res]) + { + availableRes[res] += this.accounts[j][res] - queueCost[res]; + this.accounts[j][res] = queueCost[res]; + } + } + // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" + // But we'll sometimes allow less if that would overflow. + let available = availableRes[res]; + let missing = false; + for (const j in tempPrio) + { + // we'll add at much what can be allowed to this queue. + let toAdd = Math.floor(availableRes[res] * tempPrio[j]/totalPriority); + if (toAdd >= maxNeed[j]) + toAdd = maxNeed[j]; + else + missing = true; + this.accounts[j][res] += toAdd; + maxNeed[j] -= toAdd; + available -= toAdd; + } + if (missing && available > 0) // distribute the rest (due to floor) in any queue + { + for (const j in tempPrio) + { + const toAdd = Math.min(maxNeed[j], available); + this.accounts[j][res] += toAdd; + available -= toAdd; + if (available <= 0) + break; + } + } + if (available < 0) + aiWarn("Petra: problem with remaining " + res + " in queueManager " + available); + } + } + + switchResource(gameState, res) + { + // We have no available resources, see if we can't "compact" them in one queue. + // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. + // TODO: this isn't perfect compression. + for (const j in this.queues) + { + if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) + continue; + + const queue = this.queues[j]; + const queueCost = queue.maxAccountWanted(gameState, 0); + if (this.accounts[j][res] >= queueCost[res]) + continue; + + for (const i in this.queues) + { + if (i === j) + continue; + const otherQueue = this.queues[i]; + if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) + continue; + if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) + continue; + + const diff = queueCost[res] - this.accounts[j][res]; + this.accounts[j][res] += diff; + this.accounts[i][res] -= diff; + ++otherQueue.switched; + if (this.Config.debug > 2) + { + aiWarn("switching queue " + res + " from " + i + " to " + j + " in amount " + + diff); + } + break; + } + } + } + + // Start the next item in the queue if we can afford it. + startNextItems(gameState) + { + for (const q of this.queueArrays) + { + const name = q[0]; + const queue = q[1]; + if (queue.hasQueuedUnits() && !queue.paused) + { + const item = queue.getNext(); + if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) + { + // canStart may update the cost because of the costMultiplier so we must check it again + if (this.accounts[name].canAfford(item.getCost())) + { + this.finishingTime = gameState.ai.elapsedTime; + this.accounts[name].subtract(item.getCost()); + queue.startNext(gameState); + queue.switched = 0; + } + } + } + else if (!queue.hasQueuedUnits()) + { + this.accounts[name].reset(); + queue.switched = 0; + } + } + } + + update(gameState) + { + Engine.ProfileStart("Queue Manager"); for (const i in this.queues) { - if (i === j) + this.queues[i].check(gameState); // do basic sanity checks on the queue + if (this.priorities[i] > 0) continue; - const otherQueue = this.queues[i]; - if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) - continue; - if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) - continue; - - const diff = queueCost[res] - this.accounts[j][res]; - this.accounts[j][res] += diff; - this.accounts[i][res] -= diff; - ++otherQueue.switched; - if (this.Config.debug > 2) - { - aiWarn("switching queue " + res + " from " + i + " to " + j + " in amount " + - diff); - } - break; + aiWarn("QueueManager received bad priorities, please report this error: " + + uneval(this.priorities)); + this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. } - } -}; -// Start the next item in the queue if we can afford it. -QueueManager.prototype.startNextItems = function(gameState) -{ - for (const q of this.queueArrays) + // Pause or unpause queues depending on the situation + this.checkPausedQueues(gameState); + + // Let's assign resources to plans that need them + this.distributeResources(gameState); + + // Start the next item in the queue if we can afford it. + this.startNextItems(gameState); + + if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) + this.printQueues(gameState); + + Engine.ProfileStop(); + } + + // Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training + checkPausedQueues(gameState) { - const name = q[0]; - const queue = q[1]; - if (queue.hasQueuedUnits() && !queue.paused) + const numWorkers = gameState.countOwnEntitiesAndQueuedWithRole(Worker.ROLE_WORKER); + const workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2); + for (const q in this.queues) { - const item = queue.getNext(); - if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) + let toBePaused = false; + if (!gameState.ai.HQ.hasPotentialBase()) + toBePaused = q != "dock" && q != "civilCentre"; + else if (numWorkers < workersMin / 3) + toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency"; + else if (numWorkers < workersMin * 2 / 3) + toBePaused = q == "civilCentre" || q == "economicBuilding" || + q == "militaryBuilding" || q == "defenseBuilding" || q == "healer" || + q == "majorTech" || q == "minorTech" || q.indexOf("plan_") != -1; + else if (numWorkers < workersMin) + toBePaused = q == "civilCentre" || q == "defenseBuilding" || + q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; + + if (toBePaused) { - // canStart may update the cost because of the costMultiplier so we must check it again - if (this.accounts[name].canAfford(item.getCost())) + if (q == "field" && gameState.ai.HQ.needFarm && + !gameState.getOwnStructures().filter(filters.byClass("Field")).hasEntities()) { - this.finishingTime = gameState.ai.elapsedTime; - this.accounts[name].subtract(item.getCost()); - queue.startNext(gameState); - queue.switched = 0; + toBePaused = false; + } + if (q == "corral" && gameState.ai.HQ.needCorral && + !gameState.getOwnStructures().filter(filters.byClass("Field")).hasEntities()) + { + toBePaused = false; + } + if (q == "dock" && gameState.ai.HQ.needFish && + !gameState.getOwnStructures().filter(filters.byClass("Dock")).hasEntities()) + { + toBePaused = false; + } + if (q == "ships" && gameState.ai.HQ.needFish && + !gameState.ai.HQ.navalManager.ships.filter(filters.byClass("FishingBoat")) + .hasEntities()) + { + toBePaused = false; } } - } - else if (!queue.hasQueuedUnits()) - { - this.accounts[name].reset(); - queue.switched = 0; + + const queue = this.queues[q]; + if (!queue.paused && toBePaused) + { + queue.paused = true; + this.accounts[q].reset(); + } + else if (queue.paused && !toBePaused) + queue.paused = false; + + // And reduce the batch sizes of attack queues + if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) + { + queue.plans[0].number = 1; + if (queue.plans[1]) + queue.plans[1].number = 1; + } } } -}; -QueueManager.prototype.update = function(gameState) -{ - Engine.ProfileStart("Queue Manager"); - - for (const i in this.queues) + canAfford(queue, cost) { - this.queues[i].check(gameState); // do basic sanity checks on the queue - if (this.priorities[i] > 0) - continue; - aiWarn("QueueManager received bad priorities, please report this error: " + - uneval(this.priorities)); - this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. + if (!this.accounts[queue]) + return false; + return this.accounts[queue].canAfford(cost); } - // Pause or unpause queues depending on the situation - this.checkPausedQueues(gameState); - - // Let's assign resources to plans that need them - this.distributeResources(gameState); - - // Start the next item in the queue if we can afford it. - this.startNextItems(gameState); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - this.printQueues(gameState); - - Engine.ProfileStop(); -}; - -// Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training -QueueManager.prototype.checkPausedQueues = function(gameState) -{ - const numWorkers = gameState.countOwnEntitiesAndQueuedWithRole(Worker.ROLE_WORKER); - const workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2); - for (const q in this.queues) + pauseQueue(queue, scrapAccounts) { - let toBePaused = false; - if (!gameState.ai.HQ.hasPotentialBase()) - toBePaused = q != "dock" && q != "civilCentre"; - else if (numWorkers < workersMin / 3) - toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency"; - else if (numWorkers < workersMin * 2 / 3) - toBePaused = q == "civilCentre" || q == "economicBuilding" || - q == "militaryBuilding" || q == "defenseBuilding" || q == "healer" || - q == "majorTech" || q == "minorTech" || q.indexOf("plan_") != -1; - else if (numWorkers < workersMin) - toBePaused = q == "civilCentre" || q == "defenseBuilding" || - q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; - - if (toBePaused) - { - if (q == "field" && gameState.ai.HQ.needFarm && - !gameState.getOwnStructures().filter(filters.byClass("Field")).hasEntities()) - { - toBePaused = false; - } - if (q == "corral" && gameState.ai.HQ.needCorral && - !gameState.getOwnStructures().filter(filters.byClass("Field")).hasEntities()) - { - toBePaused = false; - } - if (q == "dock" && gameState.ai.HQ.needFish && - !gameState.getOwnStructures().filter(filters.byClass("Dock")).hasEntities()) - { - toBePaused = false; - } - if (q == "ships" && gameState.ai.HQ.needFish && - !gameState.ai.HQ.navalManager.ships.filter(filters.byClass("FishingBoat")) - .hasEntities()) - { - toBePaused = false; - } - } - - const queue = this.queues[q]; - if (!queue.paused && toBePaused) - { - queue.paused = true; - this.accounts[q].reset(); - } - else if (queue.paused && !toBePaused) - queue.paused = false; - - // And reduce the batch sizes of attack queues - if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) - { - queue.plans[0].number = 1; - if (queue.plans[1]) - queue.plans[1].number = 1; - } - } -}; - -QueueManager.prototype.canAfford = function(queue, cost) -{ - if (!this.accounts[queue]) - return false; - return this.accounts[queue].canAfford(cost); -}; - -QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) -{ - if (!this.queues[queue]) - return; - this.queues[queue].paused = true; - if (scrapAccounts) - this.accounts[queue].reset(); -}; - -QueueManager.prototype.unpauseQueue = function(queue) -{ - if (this.queues[queue]) - this.queues[queue].paused = false; -}; - -QueueManager.prototype.pauseAll = function(scrapAccounts, but) -{ - for (const q in this.queues) - { - if (q == but) - continue; + if (!this.queues[queue]) + return; + this.queues[queue].paused = true; if (scrapAccounts) - this.accounts[q].reset(); - this.queues[q].paused = true; + this.accounts[queue].reset(); } -}; -QueueManager.prototype.unpauseAll = function(but) -{ - for (const q in this.queues) - if (q != but) - this.queues[q].paused = false; -}; - - -QueueManager.prototype.addQueue = function(queueName, priority) -{ - if (this.queues[queueName] !== undefined) - return; - - this.queues[queueName] = new Queue(); - this.priorities[queueName] = priority; - this.accounts[queueName] = new ResourcesManager(); - - this.queueArrays = []; - for (const q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - sortQueues(this.queueArrays, this.priorities); -}; - -QueueManager.prototype.removeQueue = function(queueName) -{ - if (this.queues[queueName] === undefined) - return; - - delete this.queues[queueName]; - delete this.priorities[queueName]; - delete this.accounts[queueName]; - - this.queueArrays = []; - for (const q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - sortQueues(this.queueArrays, this.priorities); -}; - -QueueManager.prototype.getPriority = function(queueName) -{ - return this.priorities[queueName]; -}; - -QueueManager.prototype.changePriority = function(queueName, newPriority) -{ - if (this.Config.debug > 1) + unpauseQueue(queue) { - aiWarn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + - " to " + newPriority); + if (this.queues[queue]) + this.queues[queue].paused = false; } - if (this.queues[queueName] !== undefined) - this.priorities[queueName] = newPriority; - sortQueues(this.queueArrays, this.priorities); -}; -QueueManager.prototype.Serialize = function() -{ - const accounts = {}; - const queues = {}; - for (const q in this.queues) + pauseAll(scrapAccounts, but) { - queues[q] = this.queues[q].Serialize(); - accounts[q] = this.accounts[q].Serialize(); - if (this.Config.debug == -100) + for (const q in this.queues) { - aiWarn("queueManager serialization: queue " + q + " >>> " + uneval(queues[q]) + - " with accounts " + uneval(accounts[q])); + if (q == but) + continue; + if (scrapAccounts) + this.accounts[q].reset(); + this.queues[q].paused = true; } } - return { - "priorities": this.priorities, - "queues": queues, - "accounts": accounts - }; -}; - -QueueManager.prototype.Deserialize = function(gameState, data) -{ - this.priorities = data.priorities; - this.queues = {}; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (const q in data.queues) + unpauseAll(but) { - this.queues[q] = new Queue(); - this.queues[q].Deserialize(gameState, data.queues[q]); - this.accounts[q] = new ResourcesManager(); - this.accounts[q].Deserialize(data.accounts[q]); - this.queueArrays.push([q, this.queues[q]]); + for (const q in this.queues) + if (q != but) + this.queues[q].paused = false; } - sortQueues(this.queueArrays, data.priorities); -}; + + + addQueue(queueName, priority) + { + if (this.queues[queueName] !== undefined) + return; + + this.queues[queueName] = new Queue(); + this.priorities[queueName] = priority; + this.accounts[queueName] = new ResourcesManager(); + + this.queueArrays = []; + for (const q in this.queues) + this.queueArrays.push([q, this.queues[q]]); + sortQueues(this.queueArrays, this.priorities); + } + + removeQueue(queueName) + { + if (this.queues[queueName] === undefined) + return; + + delete this.queues[queueName]; + delete this.priorities[queueName]; + delete this.accounts[queueName]; + + this.queueArrays = []; + for (const q in this.queues) + this.queueArrays.push([q, this.queues[q]]); + sortQueues(this.queueArrays, this.priorities); + } + + getPriority(queueName) + { + return this.priorities[queueName]; + } + + changePriority(queueName, newPriority) + { + if (this.Config.debug > 1) + { + aiWarn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + + " to " + newPriority); + } + if (this.queues[queueName] !== undefined) + this.priorities[queueName] = newPriority; + sortQueues(this.queueArrays, this.priorities); + } + + Serialize() + { + const accounts = {}; + const queues = {}; + for (const q in this.queues) + { + queues[q] = this.queues[q].Serialize(); + accounts[q] = this.accounts[q].Serialize(); + if (this.Config.debug == -100) + { + aiWarn("queueManager serialization: queue " + q + " >>> " + uneval(queues[q]) + + " with accounts " + uneval(accounts[q])); + } + } + + return { + "priorities": this.priorities, + "queues": queues, + "accounts": accounts + }; + } + + Deserialize(gameState, data) + { + this.priorities = data.priorities; + this.queues = {}; + this.accounts = {}; + + // the sorting is updated on priority change. + this.queueArrays = []; + for (const q in data.queues) + { + this.queues[q] = new Queue(); + this.queues[q].Deserialize(gameState, data.queues[q]); + this.accounts[q] = new ResourcesManager(); + this.accounts[q].Deserialize(data.accounts[q]); + this.queueArrays.push([q, this.queues[q]]); + } + sortQueues(this.queueArrays, data.priorities); + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/researchManager.js b/binaries/data/mods/public/simulation/ai/petra/researchManager.js index a0e1cb343a..a2a718741b 100644 --- a/binaries/data/mods/public/simulation/ai/petra/researchManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/researchManager.js @@ -4,242 +4,245 @@ import { Worker } from "simulation/ai/petra/worker.js"; /** * Manage the research */ -export function ResearchManager(Config) +export class ResearchManager { - this.Config = Config; + constructor(Config) + { + this.Config = Config; + } + + /** + * Check if we can go to the next phase + */ + checkPhase(gameState, queues) + { + if (queues.majorTech.hasQueuedUnits()) + return; + // Don't try to phase up if already trying to gather resources for a civil-centre or wonder + if (queues.civilCentre.hasQueuedUnits() || queues.wonder.hasQueuedUnits()) + return; + + const currentPhaseIndex = gameState.currentPhase(); + const nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1); + if (!nextPhaseName) + return; + + const petraRequirements = + currentPhaseIndex == 1 && gameState.ai.HQ.getAccountedPopulation(gameState) >= this.Config.Economy.popPhase2 || + currentPhaseIndex == 2 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase3 || + currentPhaseIndex >= 3 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase4; + if (petraRequirements && gameState.hasResearchers(nextPhaseName, true)) + { + gameState.ai.HQ.phasing = currentPhaseIndex + 1; + // Reset the queue priority in case it was changed during a previous phase update + gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech); + queues.majorTech.addPlan(new ResearchPlan(gameState, nextPhaseName, true)); + } + } + + researchPopulationBonus(gameState, queues) + { + if (queues.minorTech.hasQueuedUnits()) + return; + + const techs = gameState.findAvailableTech(); + for (const tech of techs) + { + if (!tech[1]._template.modifications) + continue; + // TODO may-be loop on all modifs and check if the effect if positive ? + if (tech[1]._template.modifications[0].value !== "Population/Bonus") + continue; + queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0])); + break; + } + } + + researchTradeBonus(gameState, queues) + { + if (queues.minorTech.hasQueuedUnits()) + return; + + const techs = gameState.findAvailableTech(); + for (const tech of techs) + { + if (!tech[1]._template.modifications || !tech[1]._template.affects) + continue; + if (tech[1]._template.affects.indexOf("Trader") === -1) + continue; + // TODO may-be loop on all modifs and check if the effect if positive ? + if (tech[1]._template.modifications[0].value !== "UnitMotion/WalkSpeed" && + tech[1]._template.modifications[0].value !== "Trader/GainMultiplier") + continue; + queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0])); + break; + } + } + + /** Techs to be searched for as soon as they are available */ + researchWantedTechs(gameState, techs) + { + const phase1 = gameState.currentPhase() === 1; + const available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; + const numWorkers = phase1 ? gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length : 0; + for (const tech of techs) + { + if (tech[0].indexOf("unlock_champion") == 0) + return { "name": tech[0], "increasePriority": true }; + if (tech[0] == "traditional_army_sele" || tech[0] == "reformed_army_sele") + return { "name": pickRandom(["traditional_army_sele", "reformed_army_sele"]), "increasePriority": true }; + + if (!tech[1]._template.modifications) + continue; + const template = tech[1]._template; + if (phase1) + { + const cost = template.cost; + let costMax = 0; + for (const res in cost) + costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); + if (10*numWorkers < costMax) + continue; + } + for (const i in template.modifications) + { + if (gameState.ai.HQ.navalMap && template.modifications[i].value === "ResourceGatherer/Rates/food.fish") + return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; + else if (template.modifications[i].value === "ResourceGatherer/Rates/food.fruit") + return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; + else if (template.modifications[i].value === "ResourceGatherer/Rates/food.grain") + return { "name": tech[0], "increasePriority": false }; + else if (template.modifications[i].value === "ResourceGatherer/Rates/wood.tree") + return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; + else if (template.modifications[i].value.startsWith("ResourceGatherer/Capacities")) + return { "name": tech[0], "increasePriority": false }; + else if (template.modifications[i].value === "Attack/Ranged/MaxRange") + return { "name": tech[0], "increasePriority": false }; + } + } + return null; + } + + /** Techs to be searched for as soon as they are available, but only after phase 2 */ + researchPreferredTechs(gameState, techs) + { + const phase2 = gameState.currentPhase() === 2; + const available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; + const numWorkers = phase2 ? gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length : 0; + for (const tech of techs) + { + if (!tech[1]._template.modifications) + continue; + const template = tech[1]._template; + if (phase2) + { + const cost = template.cost; + let costMax = 0; + for (const res in cost) + costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); + if (10*numWorkers < costMax) + continue; + } + for (const i in template.modifications) + { + if (template.modifications[i].value === "ResourceGatherer/Rates/stone.rock") + return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; + else if (template.modifications[i].value === "ResourceGatherer/Rates/metal.ore") + return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; + else if (template.modifications[i].value === "BuildingAI/DefaultArrowCount") + return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; + else if (template.modifications[i].value === "Health/RegenRate") + return { "name": tech[0], "increasePriority": false }; + else if (template.modifications[i].value === "Health/IdleRegenRate") + return { "name": tech[0], "increasePriority": false }; + } + } + return null; + } + + update(gameState, queues) + { + if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits()) + return; + + const techs = gameState.findAvailableTech(); + + let techName = this.researchWantedTechs(gameState, techs); + if (techName) + { + if (techName.increasePriority) + { + gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); + const plan = new ResearchPlan(gameState, techName.name); + plan.queueToReset = "minorTech"; + queues.minorTech.addPlan(plan); + } + else + queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name)); + return; + } + + if (gameState.currentPhase() < 2) + return; + + techName = this.researchPreferredTechs(gameState, techs); + if (techName) + { + if (techName.increasePriority) + { + gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); + const plan = new ResearchPlan(gameState, techName.name); + plan.queueToReset = "minorTech"; + queues.minorTech.addPlan(plan); + } + else + queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name)); + return; + } + + if (gameState.currentPhase() < 3) + return; + + // remove some techs not yet used by this AI + // remove also sharedLos if we have no ally + for (let i = 0; i < techs.length; ++i) + { + const template = techs[i][1]._template; + if (template.affects && template.affects.length === 1 && + (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "Wall")) + { + techs.splice(i--, 1); + continue; + } + if (template.modifications && template.modifications.length === 1 && + this.Config.unusedNoAllyTechs.includes(template.modifications[0].value) && + !gameState.hasAllies()) + { + techs.splice(i--, 1); + continue; + } + } + if (!techs.length) + return; + + // randomly pick one. No worries about pairs in that case. + queues.minorTech.addPlan(new ResearchPlan(gameState, pickRandom(techs)[0])); + } + + CostSum(cost) + { + let costSum = 0; + for (const res in cost) + costSum += cost[res]; + return costSum; + } + + Serialize() + { + return {}; + } + + Deserialize(data) + { + } } - -/** - * Check if we can go to the next phase - */ -ResearchManager.prototype.checkPhase = function(gameState, queues) -{ - if (queues.majorTech.hasQueuedUnits()) - return; - // Don't try to phase up if already trying to gather resources for a civil-centre or wonder - if (queues.civilCentre.hasQueuedUnits() || queues.wonder.hasQueuedUnits()) - return; - - const currentPhaseIndex = gameState.currentPhase(); - const nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1); - if (!nextPhaseName) - return; - - const petraRequirements = - currentPhaseIndex == 1 && gameState.ai.HQ.getAccountedPopulation(gameState) >= this.Config.Economy.popPhase2 || - currentPhaseIndex == 2 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase3 || - currentPhaseIndex >= 3 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase4; - if (petraRequirements && gameState.hasResearchers(nextPhaseName, true)) - { - gameState.ai.HQ.phasing = currentPhaseIndex + 1; - // Reset the queue priority in case it was changed during a previous phase update - gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech); - queues.majorTech.addPlan(new ResearchPlan(gameState, nextPhaseName, true)); - } -}; - -ResearchManager.prototype.researchPopulationBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - const techs = gameState.findAvailableTech(); - for (const tech of techs) - { - if (!tech[1]._template.modifications) - continue; - // TODO may-be loop on all modifs and check if the effect if positive ? - if (tech[1]._template.modifications[0].value !== "Population/Bonus") - continue; - queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0])); - break; - } -}; - -ResearchManager.prototype.researchTradeBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - const techs = gameState.findAvailableTech(); - for (const tech of techs) - { - if (!tech[1]._template.modifications || !tech[1]._template.affects) - continue; - if (tech[1]._template.affects.indexOf("Trader") === -1) - continue; - // TODO may-be loop on all modifs and check if the effect if positive ? - if (tech[1]._template.modifications[0].value !== "UnitMotion/WalkSpeed" && - tech[1]._template.modifications[0].value !== "Trader/GainMultiplier") - continue; - queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0])); - break; - } -}; - -/** Techs to be searched for as soon as they are available */ -ResearchManager.prototype.researchWantedTechs = function(gameState, techs) -{ - const phase1 = gameState.currentPhase() === 1; - const available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - const numWorkers = phase1 ? gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length : 0; - for (const tech of techs) - { - if (tech[0].indexOf("unlock_champion") == 0) - return { "name": tech[0], "increasePriority": true }; - if (tech[0] == "traditional_army_sele" || tech[0] == "reformed_army_sele") - return { "name": pickRandom(["traditional_army_sele", "reformed_army_sele"]), "increasePriority": true }; - - if (!tech[1]._template.modifications) - continue; - const template = tech[1]._template; - if (phase1) - { - const cost = template.cost; - let costMax = 0; - for (const res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (const i in template.modifications) - { - if (gameState.ai.HQ.navalMap && template.modifications[i].value === "ResourceGatherer/Rates/food.fish") - return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; - else if (template.modifications[i].value === "ResourceGatherer/Rates/food.fruit") - return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; - else if (template.modifications[i].value === "ResourceGatherer/Rates/food.grain") - return { "name": tech[0], "increasePriority": false }; - else if (template.modifications[i].value === "ResourceGatherer/Rates/wood.tree") - return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; - else if (template.modifications[i].value.startsWith("ResourceGatherer/Capacities")) - return { "name": tech[0], "increasePriority": false }; - else if (template.modifications[i].value === "Attack/Ranged/MaxRange") - return { "name": tech[0], "increasePriority": false }; - } - } - return null; -}; - -/** Techs to be searched for as soon as they are available, but only after phase 2 */ -ResearchManager.prototype.researchPreferredTechs = function(gameState, techs) -{ - const phase2 = gameState.currentPhase() === 2; - const available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - const numWorkers = phase2 ? gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length : 0; - for (const tech of techs) - { - if (!tech[1]._template.modifications) - continue; - const template = tech[1]._template; - if (phase2) - { - const cost = template.cost; - let costMax = 0; - for (const res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (const i in template.modifications) - { - if (template.modifications[i].value === "ResourceGatherer/Rates/stone.rock") - return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; - else if (template.modifications[i].value === "ResourceGatherer/Rates/metal.ore") - return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; - else if (template.modifications[i].value === "BuildingAI/DefaultArrowCount") - return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; - else if (template.modifications[i].value === "Health/RegenRate") - return { "name": tech[0], "increasePriority": false }; - else if (template.modifications[i].value === "Health/IdleRegenRate") - return { "name": tech[0], "increasePriority": false }; - } - } - return null; -}; - -ResearchManager.prototype.update = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits()) - return; - - const techs = gameState.findAvailableTech(); - - let techName = this.researchWantedTechs(gameState, techs); - if (techName) - { - if (techName.increasePriority) - { - gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); - const plan = new ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name)); - return; - } - - if (gameState.currentPhase() < 2) - return; - - techName = this.researchPreferredTechs(gameState, techs); - if (techName) - { - if (techName.increasePriority) - { - gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); - const plan = new ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name)); - return; - } - - if (gameState.currentPhase() < 3) - return; - - // remove some techs not yet used by this AI - // remove also sharedLos if we have no ally - for (let i = 0; i < techs.length; ++i) - { - const template = techs[i][1]._template; - if (template.affects && template.affects.length === 1 && - (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "Wall")) - { - techs.splice(i--, 1); - continue; - } - if (template.modifications && template.modifications.length === 1 && - this.Config.unusedNoAllyTechs.includes(template.modifications[0].value) && - !gameState.hasAllies()) - { - techs.splice(i--, 1); - continue; - } - } - if (!techs.length) - return; - - // randomly pick one. No worries about pairs in that case. - queues.minorTech.addPlan(new ResearchPlan(gameState, pickRandom(techs)[0])); -}; - -ResearchManager.prototype.CostSum = function(cost) -{ - let costSum = 0; - for (const res in cost) - costSum += cost[res]; - return costSum; -}; - -ResearchManager.prototype.Serialize = function() -{ - return {}; -}; - -ResearchManager.prototype.Deserialize = function(data) -{ -}; diff --git a/binaries/data/mods/public/simulation/ai/petra/tradeManager.js b/binaries/data/mods/public/simulation/ai/petra/tradeManager.js index f2c6083d17..5d424d31fb 100644 --- a/binaries/data/mods/public/simulation/ai/petra/tradeManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/tradeManager.js @@ -12,718 +12,721 @@ import { Worker } from "simulation/ai/petra/worker.js"; /** * Manage the trade */ -export function TradeManager(config) +export class TradeManager { - this.Config = config; - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - this.routeProspection = false; - this.targetNumTraders = this.Config.Economy.targetNumTraders; - this.warnedAllies = {}; -} - -TradeManager.prototype.init = function(gameState) -{ - this.traders = gameState.getOwnUnits().filter( - filters.byMetadata(PlayerID, "role", Worker.ROLE_TRADER)); - this.traders.registerUpdates(); - this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5; -}; - -TradeManager.prototype.hasTradeRoute = function() -{ - return this.tradeRoute !== undefined; -}; - -TradeManager.prototype.assignTrader = function(ent) -{ - ent.setMetadata(PlayerID, "role", Worker.ROLE_TRADER); - this.traders.updateEnt(ent); -}; - -TradeManager.prototype.trainMoreTraders = function(gameState, queues) -{ - if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits()) - return; - - let numTraders = this.traders.length; - let numSeaTraders = this.traders.filter(filters.byClass("Ship")).length; - let numLandTraders = numTraders - numSeaTraders; - // add traders already in training - gameState.getOwnTrainingFacilities().forEach(function(ent) + constructor(config) { - for (const item of ent.trainingQueue()) - { - if (!item.metadata || !item.metadata.role || item.metadata.role !== Worker.ROLE_TRADER) - continue; - numTraders += item.count; - if (item.metadata.sea !== undefined) - numSeaTraders += item.count; - else - numLandTraders += item.count; - } - }); - if (numTraders >= this.targetNumTraders && - (!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) || - this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2))) - return; + this.Config = config; + this.tradeRoute = undefined; + this.potentialTradeRoute = undefined; + this.routeProspection = false; + this.targetNumTraders = this.Config.Economy.targetNumTraders; + this.warnedAllies = {}; + } - let template; - const metadata = { "role": Worker.ROLE_TRADER }; - if (this.tradeRoute.sea) + init(gameState) { - // if we have some merchand ships assigned to transport, try first to reassign them - // May-be, there were produced at an early stage when no other ship were available - // and the naval manager will train now more appropriate ships. - let already = false; - let shipToSwitch; - gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) + this.traders = gameState.getOwnUnits().filter( + filters.byMetadata(PlayerID, "role", Worker.ROLE_TRADER)); + this.traders.registerUpdates(); + this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5; + } + + hasTradeRoute() + { + return this.tradeRoute !== undefined; + } + + assignTrader(ent) + { + ent.setMetadata(PlayerID, "role", Worker.ROLE_TRADER); + this.traders.updateEnt(ent); + } + + trainMoreTraders(gameState, queues) + { + if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits()) + return; + + let numTraders = this.traders.length; + let numSeaTraders = this.traders.filter(filters.byClass("Ship")).length; + let numLandTraders = numTraders - numSeaTraders; + // add traders already in training + gameState.getOwnTrainingFacilities().forEach(function(ent) { - if (already || !ship.hasClass("Trader")) - return; - if (ship.getMetadata(PlayerID, "role") === Worker.ROLE_SWITCH_TO_TRADER) + for (const item of ent.trainingQueue()) { - already = true; - return; + if (!item.metadata || !item.metadata.role || item.metadata.role !== Worker.ROLE_TRADER) + continue; + numTraders += item.count; + if (item.metadata.sea !== undefined) + numSeaTraders += item.count; + else + numLandTraders += item.count; } - shipToSwitch = ship; }); - if (already) + if (numTraders >= this.targetNumTraders && + (!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) || + this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2))) return; - if (shipToSwitch) + + let template; + const metadata = { "role": Worker.ROLE_TRADER }; + if (this.tradeRoute.sea) { - if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined) - shipToSwitch.setMetadata(PlayerID, "role", Worker.ROLE_TRADER); - else - shipToSwitch.setMetadata(PlayerID, "role", Worker.ROLE_SWITCH_TO_TRADER); - return; + // if we have some merchand ships assigned to transport, try first to reassign them + // May-be, there were produced at an early stage when no other ship were available + // and the naval manager will train now more appropriate ships. + let already = false; + let shipToSwitch; + gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) + { + if (already || !ship.hasClass("Trader")) + return; + if (ship.getMetadata(PlayerID, "role") === Worker.ROLE_SWITCH_TO_TRADER) + { + already = true; + return; + } + shipToSwitch = ship; + }); + if (already) + return; + if (shipToSwitch) + { + if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined) + shipToSwitch.setMetadata(PlayerID, "role", Worker.ROLE_TRADER); + else + shipToSwitch.setMetadata(PlayerID, "role", Worker.ROLE_SWITCH_TO_TRADER); + return; + } + + template = gameState.applyCiv("units/{civ}/ship_merchant"); + metadata.sea = this.tradeRoute.sea; } - - template = gameState.applyCiv("units/{civ}/ship_merchant"); - metadata.sea = this.tradeRoute.sea; - } - else - { - template = gameState.applyCiv("units/{civ}/support_trader"); - const sourceObject = gameState.getEntityById(this.tradeRoute.source); - const baseObject = sourceObject.hasClass("Naval") ? - gameState.getEntityById(this.tradeRoute.target) : sourceObject; - - metadata.base = baseObject.getMetadata(PlayerID, "base"); - } - - if (!gameState.getTemplate(template)) - { - if (this.Config.debug > 0) - { - aiWarn("Petra error: trying to train " + template + " for civ " + - gameState.getPlayerCiv() + " but no template found."); - } - return; - } - queues.trader.addPlan(new TrainingPlan(gameState, template, metadata, 1, 1)); -}; - -TradeManager.prototype.updateTrader = function(gameState, ent) -{ - if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 && - !ent.unitAIState().startsWith("INDIVIDUAL.COLLECTTREASURE") && - gatherTreasure(gameState, ent, true)) - { - return; - } - - if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) - return; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return; - - // TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain - - Engine.ProfileStart("Trade Manager"); - const access = ent.hasClass("Ship") ? getSeaAccess(gameState, ent) : getLandAccess(gameState, ent); - const route = this.checkRoutes(gameState, access); - const routeSource = gameState.getEntityById(route.source); - const routeTarget = gameState.getEntityById(route.target); - if (!route) - { - // TODO try to garrison land trader inside merchant ship when only sea routes available - if (this.Config.debug > 0) - aiWarn(" no available route for " + ent.genericName() + " " + ent.id()); - Engine.ProfileStop(); - return; - } - - let nearerSource = true; - if (SquareVectorDistance(routeTarget.position(), ent.position()) < - SquareVectorDistance(routeSource.position(), ent.position())) - { - nearerSource = false; - } - - if (!ent.hasClass("Ship") && route.land != access) - { - if (nearerSource) - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, routeSource.position()); else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, routeTarget.position()); - Engine.ProfileStop(); - return; - } - - if (nearerSource) - ent.tradeRoute(routeTarget, routeSource); - else - ent.tradeRoute(routeSource, routeTarget); - ent.setMetadata(PlayerID, "route", route); - Engine.ProfileStop(); -}; - -TradeManager.prototype.setTradingGoods = function(gameState) -{ - const resTradeCodes = Resources.GetTradableCodes(); - if (!resTradeCodes.length) - return; - const tradingGoods = {}; - for (const res of resTradeCodes) - tradingGoods[res] = 0; - // first, try to anticipate future needs - const stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); - const mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState, resTradeCodes); - const wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); - let remaining = 100; - let targetNum = this.Config.Economy.targetNumTraders; - for (const res of resTradeCodes) - { - if (res == "food") - continue; - const wantedRate = wantedRates[res]; - if (stocks[res] < 200) { - tradingGoods[res] = wantedRate > 0 ? 20 : 10; - targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30)); + template = gameState.applyCiv("units/{civ}/support_trader"); + const sourceObject = gameState.getEntityById(this.tradeRoute.source); + const baseObject = sourceObject.hasClass("Naval") ? + gameState.getEntityById(this.tradeRoute.target) : sourceObject; + + metadata.base = baseObject.getMetadata(PlayerID, "base"); } - else if (stocks[res] < 500) + + if (!gameState.getTemplate(template)) { - tradingGoods[res] = wantedRate > 0 ? 15 : 10; - targetNum += 2; - } - else if (stocks[res] < 1000) - { - tradingGoods[res] = 10; - targetNum += 1; - } - remaining -= tradingGoods[res]; - } - this.targetNumTraders = Math.round(this.Config.popScaling * targetNum); - - - // then add what is needed now - const mainNeed = Math.floor(remaining * 70 / 100); - const nextNeed = remaining - mainNeed; - - tradingGoods[mostNeeded[0].type] += mainNeed; - if (mostNeeded[1] && mostNeeded[1].wanted > 0) - tradingGoods[mostNeeded[1].type] += nextNeed; - else - tradingGoods[mostNeeded[0].type] += nextNeed; - Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods }); - if (this.Config.debug > 2) - aiWarn(" trading goods set to " + uneval(tradingGoods)); -}; - -/** - * Try to barter unneeded resources for needed resources. - * only once per turn because the info is not updated within a turn - */ -TradeManager.prototype.performBarter = function(gameState) -{ - const barterers = gameState.getOwnEntitiesByClass("Barter", true).filter(filters.isBuilt()) - .toEntityArray(); - if (barterers.length == 0) - return false; - const resBarterCodes = Resources.GetBarterableCodes(); - if (!resBarterCodes.length) - return false; - - // Available resources after account substraction - const available = gameState.ai.queueManager.getAvailableResources(gameState); - const needs = gameState.ai.queueManager.currentNeeds(gameState); - - const rates = gameState.ai.HQ.GetCurrentGatherRates(gameState); - - const barterPrices = gameState.getBarterPrices(); - // calculates conversion rates - const getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); - - // loop through each missing resource checking if we could barter and help finishing a queue quickly. - for (const buy of resBarterCodes) - { - // Check if our rate allows to gather it fast enough - if (needs[buy] == 0 || needs[buy] < rates[buy] * 30) - continue; - - // Pick the best resource to barter. - let bestToSell; - let bestRate = 0; - for (const sell of resBarterCodes) - { - if (sell == buy) - continue; - // Do not sell if we need it or do not have enough buffer - if (needs[sell] > 0 || available[sell] < 500) - continue; - - let barterRateMin; - if (sell == "food") + if (this.Config.debug > 0) { - barterRateMin = 30; - if (available[sell] > 40000) - barterRateMin = 0; - else if (available[sell] > 15000) - barterRateMin = 5; - else if (available[sell] > 1000) - barterRateMin = 10; + aiWarn("Petra error: trying to train " + template + " for civ " + + gameState.getPlayerCiv() + " but no template found."); } + return; + } + queues.trader.addPlan(new TrainingPlan(gameState, template, metadata, 1, 1)); + } + + updateTrader(gameState, ent) + { + if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 && + !ent.unitAIState().startsWith("INDIVIDUAL.COLLECTTREASURE") && + gatherTreasure(gameState, ent, true)) + { + return; + } + + if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) + return; + if (ent.getMetadata(PlayerID, "transport") !== undefined) + return; + + // TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain + + Engine.ProfileStart("Trade Manager"); + const access = ent.hasClass("Ship") ? getSeaAccess(gameState, ent) : getLandAccess(gameState, ent); + const route = this.checkRoutes(gameState, access); + const routeSource = gameState.getEntityById(route.source); + const routeTarget = gameState.getEntityById(route.target); + if (!route) + { + // TODO try to garrison land trader inside merchant ship when only sea routes available + if (this.Config.debug > 0) + aiWarn(" no available route for " + ent.genericName() + " " + ent.id()); + Engine.ProfileStop(); + return; + } + + let nearerSource = true; + if (SquareVectorDistance(routeTarget.position(), ent.position()) < + SquareVectorDistance(routeSource.position(), ent.position())) + { + nearerSource = false; + } + + if (!ent.hasClass("Ship") && route.land != access) + { + if (nearerSource) + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, routeSource.position()); else - { - barterRateMin = 70; - if (available[sell] > 5000) - barterRateMin = 30; - else if (available[sell] > 1000) - barterRateMin = 50; - if (buy == "food") - barterRateMin += 20; - } + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, routeTarget.position()); + Engine.ProfileStop(); + return; + } - const barterRate = getBarterRate(barterPrices, buy, sell); - if (barterRate > bestRate && barterRate > barterRateMin) + if (nearerSource) + ent.tradeRoute(routeTarget, routeSource); + else + ent.tradeRoute(routeSource, routeTarget); + ent.setMetadata(PlayerID, "route", route); + Engine.ProfileStop(); + } + + setTradingGoods(gameState) + { + const resTradeCodes = Resources.GetTradableCodes(); + if (!resTradeCodes.length) + return; + const tradingGoods = {}; + for (const res of resTradeCodes) + tradingGoods[res] = 0; + // first, try to anticipate future needs + const stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); + const mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState, resTradeCodes); + const wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); + let remaining = 100; + let targetNum = this.Config.Economy.targetNumTraders; + for (const res of resTradeCodes) + { + if (res == "food") + continue; + const wantedRate = wantedRates[res]; + if (stocks[res] < 200) { - bestRate = barterRate; - bestToSell = sell; + tradingGoods[res] = wantedRate > 0 ? 20 : 10; + targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30)); + } + else if (stocks[res] < 500) + { + tradingGoods[res] = wantedRate > 0 ? 15 : 10; + targetNum += 2; + } + else if (stocks[res] < 1000) + { + tradingGoods[res] = 10; + targetNum += 1; + } + remaining -= tradingGoods[res]; + } + this.targetNumTraders = Math.round(this.Config.popScaling * targetNum); + + + // then add what is needed now + const mainNeed = Math.floor(remaining * 70 / 100); + const nextNeed = remaining - mainNeed; + + tradingGoods[mostNeeded[0].type] += mainNeed; + if (mostNeeded[1] && mostNeeded[1].wanted > 0) + tradingGoods[mostNeeded[1].type] += nextNeed; + else + tradingGoods[mostNeeded[0].type] += nextNeed; + Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods }); + if (this.Config.debug > 2) + aiWarn(" trading goods set to " + uneval(tradingGoods)); + } + + /** + * Try to barter unneeded resources for needed resources. + * only once per turn because the info is not updated within a turn + */ + performBarter(gameState) + { + const barterers = gameState.getOwnEntitiesByClass("Barter", true).filter(filters.isBuilt()) + .toEntityArray(); + if (barterers.length == 0) + return false; + const resBarterCodes = Resources.GetBarterableCodes(); + if (!resBarterCodes.length) + return false; + + // Available resources after account substraction + const available = gameState.ai.queueManager.getAvailableResources(gameState); + const needs = gameState.ai.queueManager.currentNeeds(gameState); + + const rates = gameState.ai.HQ.GetCurrentGatherRates(gameState); + + const barterPrices = gameState.getBarterPrices(); + // calculates conversion rates + const getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); + + // loop through each missing resource checking if we could barter and help finishing a queue quickly. + for (const buy of resBarterCodes) + { + // Check if our rate allows to gather it fast enough + if (needs[buy] == 0 || needs[buy] < rates[buy] * 30) + continue; + + // Pick the best resource to barter. + let bestToSell; + let bestRate = 0; + for (const sell of resBarterCodes) + { + if (sell == buy) + continue; + // Do not sell if we need it or do not have enough buffer + if (needs[sell] > 0 || available[sell] < 500) + continue; + + let barterRateMin; + if (sell == "food") + { + barterRateMin = 30; + if (available[sell] > 40000) + barterRateMin = 0; + else if (available[sell] > 15000) + barterRateMin = 5; + else if (available[sell] > 1000) + barterRateMin = 10; + } + else + { + barterRateMin = 70; + if (available[sell] > 5000) + barterRateMin = 30; + else if (available[sell] > 1000) + barterRateMin = 50; + if (buy == "food") + barterRateMin += 20; + } + + const barterRate = getBarterRate(barterPrices, buy, sell); + if (barterRate > bestRate && barterRate > barterRateMin) + { + bestRate = barterRate; + bestToSell = sell; + } + } + if (bestToSell !== undefined) + { + const amount = available[bestToSell] > 5000 ? 500 : 100; + barterers[0].barter(buy, bestToSell, amount); + if (this.Config.debug > 2) + { + aiWarn("Necessity bartering: sold " + bestToSell +" for " + buy + + " >> need sell " + needs[bestToSell] + " need buy " + needs[buy] + + " rate buy " + rates[buy] + " available sell " + available[bestToSell] + + " available buy " + available[buy] + " barterRate " + bestRate + + " amount " + amount); + } + return true; } } - if (bestToSell !== undefined) + + // now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices) + if (available.food < 1000 || needs.food > 0 || resBarterCodes.indexOf("food") == -1) + return false; + let bestToBuy; + let bestChoice = 0; + for (const buy of resBarterCodes) { - const amount = available[bestToSell] > 5000 ? 500 : 100; - barterers[0].barter(buy, bestToSell, amount); + if (buy == "food") + continue; + let barterRateMin = 80; + if (available[buy] < 5000 && available.food > 5000) + barterRateMin -= 20 - Math.floor(available[buy]/250); + const barterRate = getBarterRate(barterPrices, buy, "food"); + if (barterRate < barterRateMin) + continue; + const choice = barterRate / (100 + available[buy]); + if (choice > bestChoice) + { + bestChoice = choice; + bestToBuy = buy; + } + } + if (bestToBuy !== undefined) + { + const amount = available.food > 5000 ? 500 : 100; + barterers[0].barter(bestToBuy, "food", amount); if (this.Config.debug > 2) { - aiWarn("Necessity bartering: sold " + bestToSell +" for " + buy + - " >> need sell " + needs[bestToSell] + " need buy " + needs[buy] + - " rate buy " + rates[buy] + " available sell " + available[bestToSell] + - " available buy " + available[buy] + " barterRate " + bestRate + - " amount " + amount); + aiWarn("Contingency bartering: sold food for " + bestToBuy + " available sell " + + available.food + " available buy " + available[bestToBuy] + " barterRate " + + getBarterRate(barterPrices, bestToBuy, "food") + " amount " + amount); } return true; } - } - // now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices) - if (available.food < 1000 || needs.food > 0 || resBarterCodes.indexOf("food") == -1) return false; - let bestToBuy; - let bestChoice = 0; - for (const buy of resBarterCodes) - { - if (buy == "food") - continue; - let barterRateMin = 80; - if (available[buy] < 5000 && available.food > 5000) - barterRateMin -= 20 - Math.floor(available[buy]/250); - const barterRate = getBarterRate(barterPrices, buy, "food"); - if (barterRate < barterRateMin) - continue; - const choice = barterRate / (100 + available[buy]); - if (choice > bestChoice) - { - bestChoice = choice; - bestToBuy = buy; - } - } - if (bestToBuy !== undefined) - { - const amount = available.food > 5000 ? 500 : 100; - barterers[0].barter(bestToBuy, "food", amount); - if (this.Config.debug > 2) - { - aiWarn("Contingency bartering: sold food for " + bestToBuy + " available sell " + - available.food + " available buy " + available[bestToBuy] + " barterRate " + - getBarterRate(barterPrices, bestToBuy, "food") + " amount " + amount); - } - return true; } - return false; -}; - -TradeManager.prototype.checkEvents = function(gameState, events) -{ - // check if one market from a traderoute is renamed, change the route accordingly - for (const evt of events.EntityRenamed) + checkEvents(gameState, events) { - const ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.hasClass("Trade")) - continue; - for (const trader of this.traders.values()) + // check if one market from a traderoute is renamed, change the route accordingly + for (const evt of events.EntityRenamed) { - const route = trader.getMetadata(PlayerID, "route"); - if (!route) + const ent = gameState.getEntityById(evt.newentity); + if (!ent || !ent.hasClass("Trade")) continue; - if (route.source == evt.entity) - route.source = evt.newentity; - else if (route.target == evt.entity) - route.target = evt.newentity; - else - continue; - trader.setMetadata(PlayerID, "route", route); + for (const trader of this.traders.values()) + { + const route = trader.getMetadata(PlayerID, "route"); + if (!route) + continue; + if (route.source == evt.entity) + route.source = evt.newentity; + else if (route.target == evt.entity) + route.target = evt.newentity; + else + continue; + trader.setMetadata(PlayerID, "route", route); + } } - } - // if one market (or market-foundation) is destroyed, we should look for a better route - for (const evt of events.Destroy) - { - if (evt.entity === this.tradeRoute?.source || - evt.entity === this.tradeRoute?.target || - evt.entity === this.potentialTradeRoute?.source || - evt.entity === this.potentialTradeRoute?.target) + // if one market (or market-foundation) is destroyed, we should look for a better route + for (const evt of events.Destroy) + { + if (evt.entity === this.tradeRoute?.source || + evt.entity === this.tradeRoute?.target || + evt.entity === this.potentialTradeRoute?.source || + evt.entity === this.potentialTradeRoute?.target) + { + this.activateProspection(gameState); + return true; + } + } + + // same thing if one market is built + for (const evt of events.Create) + { + const ent = gameState.getEntityById(evt.entity); + if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Trade") || + !gameState.isPlayerAlly(ent.owner())) + continue; + this.activateProspection(gameState); + return true; + } + + + // and same thing for captured markets + for (const evt of events.OwnershipChanged) + { + if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to)) + continue; + const ent = gameState.getEntityById(evt.entity); + if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Trade")) + continue; + this.activateProspection(gameState); + return true; + } + + // or if diplomacy changed + if (events.DiplomacyChanged.length) { this.activateProspection(gameState); return true; } - } - // same thing if one market is built - for (const evt of events.Create) - { - const ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Trade") || - !gameState.isPlayerAlly(ent.owner())) - continue; - this.activateProspection(gameState); - return true; - } - - - // and same thing for captured markets - for (const evt of events.OwnershipChanged) - { - if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to)) - continue; - const ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Trade")) - continue; - this.activateProspection(gameState); - return true; - } - - // or if diplomacy changed - if (events.DiplomacyChanged.length) - { - this.activateProspection(gameState); - return true; - } - - return false; -}; - -TradeManager.prototype.activateProspection = function(gameState) -{ - this.routeProspection = true; - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}/market")); - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}/dock")); -}; - -/** - * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute - * If an index is given, it returns the best route with this index or the best land route if index is a land index - */ -TradeManager.prototype.checkRoutes = function(gameState, accessIndex) -{ - // If we cannot trade, do not bother checking routes. - if (!Resources.GetTradableCodes().length) - { - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; return false; } - const market1 = gameState.updatingCollection("OwnMarkets", filters.byClass("Trade"), gameState.getOwnStructures()); - let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", filters.byClass("Trade"), - gameState.getExclusiveAllyEntities()); - if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon + activateProspection(gameState) { - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - return false; + this.routeProspection = true; + gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}/market")); + gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}/dock")); } - const onlyOurs = !market2.hasEntities(); - if (onlyOurs) - market2 = market1; - let candidate = { "gain": 0 }; - let potential = { "gain": 0 }; - let bestIndex = { "gain": 0 }; - let bestLand = { "gain": 0 }; - - const mapSize = gameState.sharedScript.mapSize; - const traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (const m1 of market1.values()) + /** + * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute + * If an index is given, it returns the best route with this index or the best land route if index is a land index + */ + checkRoutes(gameState, accessIndex) { - if (!m1.position()) - continue; - const access1 = getLandAccess(gameState, m1); - const sea1 = m1.hasClass("Naval") ? getSeaAccess(gameState, m1) : undefined; - for (const m2 of market2.values()) + // If we cannot trade, do not bother checking routes. + if (!Resources.GetTradableCodes().length) { - if (onlyOurs && m1.id() >= m2.id()) + this.tradeRoute = undefined; + this.potentialTradeRoute = undefined; + return false; + } + + const market1 = gameState.updatingCollection("OwnMarkets", filters.byClass("Trade"), gameState.getOwnStructures()); + let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", filters.byClass("Trade"), + gameState.getExclusiveAllyEntities()); + if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon + { + this.tradeRoute = undefined; + this.potentialTradeRoute = undefined; + return false; + } + + const onlyOurs = !market2.hasEntities(); + if (onlyOurs) + market2 = market1; + let candidate = { "gain": 0 }; + let potential = { "gain": 0 }; + let bestIndex = { "gain": 0 }; + let bestLand = { "gain": 0 }; + + const mapSize = gameState.sharedScript.mapSize; + const traderTemplatesGains = gameState.getTraderTemplatesGains(); + + for (const m1 of market1.values()) + { + if (!m1.position()) continue; - if (!m2.position()) - continue; - const access2 = getLandAccess(gameState, m2); - const sea2 = m2.hasClass("Naval") ? getSeaAccess(gameState, m2) : undefined; - const land = access1 == access2 ? access1 : undefined; - const sea = sea1 && sea1 == sea2 ? sea1 : undefined; - if (!land && !sea) - continue; - if (land && isLineInsideEnemyTerritory(gameState, m1.position(), m2.position())) - continue; - let gainMultiplier; - if (land && traderTemplatesGains.landGainMultiplier) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else if (sea && traderTemplatesGains.navalGainMultiplier) - gainMultiplier = traderTemplatesGains.navalGainMultiplier; - else - continue; - const gain = Math.round(gainMultiplier * - TradeGain(SquareVectorDistance(m1.position(), m2.position()), mapSize)); - if (gain < this.minimalGain) - continue; - if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined) + const access1 = getLandAccess(gameState, m1); + const sea1 = m1.hasClass("Naval") ? getSeaAccess(gameState, m1) : undefined; + for (const m2 of market2.values()) { - if (accessIndex) - { - if (gameState.ai.accessibility.regionType[accessIndex] == "water" && sea == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && land == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land") - { - if (gain < bestLand.gain) - continue; - bestLand = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; - } - } - if (gain < candidate.gain) + if (onlyOurs && m1.id() >= m2.id()) continue; - candidate = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; + if (!m2.position()) + continue; + const access2 = getLandAccess(gameState, m2); + const sea2 = m2.hasClass("Naval") ? getSeaAccess(gameState, m2) : undefined; + const land = access1 == access2 ? access1 : undefined; + const sea = sea1 && sea1 == sea2 ? sea1 : undefined; + if (!land && !sea) + continue; + if (land && isLineInsideEnemyTerritory(gameState, m1.position(), m2.position())) + continue; + let gainMultiplier; + if (land && traderTemplatesGains.landGainMultiplier) + gainMultiplier = traderTemplatesGains.landGainMultiplier; + else if (sea && traderTemplatesGains.navalGainMultiplier) + gainMultiplier = traderTemplatesGains.navalGainMultiplier; + else + continue; + const gain = Math.round(gainMultiplier * + TradeGain(SquareVectorDistance(m1.position(), m2.position()), mapSize)); + if (gain < this.minimalGain) + continue; + if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined) + { + if (accessIndex) + { + if (gameState.ai.accessibility.regionType[accessIndex] == "water" && sea == accessIndex) + { + if (gain < bestIndex.gain) + continue; + bestIndex = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; + } + else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && land == accessIndex) + { + if (gain < bestIndex.gain) + continue; + bestIndex = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; + } + else if (gameState.ai.accessibility.regionType[accessIndex] == "land") + { + if (gain < bestLand.gain) + continue; + bestLand = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; + } + } + if (gain < candidate.gain) + continue; + candidate = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; + } + if (gain < potential.gain) + continue; + potential = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; } - if (gain < potential.gain) - continue; - potential = { "source": m1.id(), "target": m2.id(), "gain": gain, "land": land, "sea": sea }; } - } - if (potential.gain < 1) - this.potentialTradeRoute = undefined; - else - this.potentialTradeRoute = potential; + if (potential.gain < 1) + this.potentialTradeRoute = undefined; + else + this.potentialTradeRoute = potential; - if (candidate.gain < 1) - { - if (this.Config.debug > 2) - aiWarn("no better trade route possible"); - this.tradeRoute = undefined; - return false; - } - - if (this.Config.debug > 1 && this.tradeRoute) - { - if (candidate.gain > this.tradeRoute.gain) + if (candidate.gain < 1) { - aiWarn("one better trade route set with gain " + candidate.gain + " instead of " + - this.tradeRoute.gain); + if (this.Config.debug > 2) + aiWarn("no better trade route possible"); + this.tradeRoute = undefined; + return false; } - } - else if (this.Config.debug > 1) - aiWarn("one trade route set with gain " + candidate.gain); - this.tradeRoute = candidate; - if (this.Config.chat) - { - let owner = gameState.getEntityById(this.tradeRoute.source).owner(); - if (owner == PlayerID) - owner = gameState.getEntityById(this.tradeRoute.target).owner(); - if (owner != PlayerID && !this.warnedAllies[owner]) - { // Warn an ally that we have a trade route with him - chatNewTradeRoute(gameState, owner); - this.warnedAllies[owner] = true; - } - } - - if (accessIndex) - { - if (bestIndex.gain > 0) - return bestIndex; - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && bestLand.gain > 0) - return bestLand; - return false; - } - return true; -}; - -/** Called when a market was built or destroyed, and checks if trader orders should be changed */ -TradeManager.prototype.checkTrader = function(gameState, ent) -{ - const presentRoute = ent.getMetadata(PlayerID, "route"); - if (!presentRoute) - return; - - if (!ent.position()) - { - // This trader is garrisoned, we will decide later (when ungarrisoning) what to do - ent.setMetadata(PlayerID, "route", undefined); - return; - } - - const access = ent.hasClass("Ship") ? getSeaAccess(gameState, ent) : getLandAccess(gameState, ent); - const possibleRoute = this.checkRoutes(gameState, access); - // Warning: presentRoute is from metadata, so contains entity ids - if (!possibleRoute || - possibleRoute.source != presentRoute.source && possibleRoute.source != presentRoute.target || - possibleRoute.target != presentRoute.source && possibleRoute.target != presentRoute.target) - { - // Trader will be assigned in updateTrader - ent.setMetadata(PlayerID, "route", undefined); - if (!possibleRoute && !ent.hasClass("Ship")) + if (this.Config.debug > 1 && this.tradeRoute) { - const closestBase = getBestBase(gameState, ent, true); - if (closestBase.accessIndex == access) + if (candidate.gain > this.tradeRoute.gain) { - const closestBasePos = closestBase.anchor.position(); - ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15); - return; + aiWarn("one better trade route set with gain " + candidate.gain + " instead of " + + this.tradeRoute.gain); } } - ent.stopMoving(); - } -}; + else if (this.Config.debug > 1) + aiWarn("one trade route set with gain " + candidate.gain); + this.tradeRoute = candidate; -TradeManager.prototype.prospectForNewMarket = function(gameState, queues) -{ - if (queues.economicBuilding.hasQueuedUnitsWithClass("Trade") || queues.dock.hasQueuedUnitsWithClass("Trade")) - return; - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market")) - return; - if (!gameState.updatingCollection("OwnMarkets", filters.byClass("Trade"), - gameState.getOwnStructures()).hasEntities() && - !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", filters.byClass("Trade"), - gameState.getExclusiveAllyEntities()).hasEntities()) - { - return; - } - const template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/market")); - if (!template) - return; - this.checkRoutes(gameState); - const marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); - if (!marketPos || marketPos[3] == 0) // marketPos[3] is the expected gain - { // no position found - if (gameState.getOwnEntitiesByClass("Market", true).hasEntities()) - gameState.ai.HQ.buildManager.setUnbuildable(gameState, gameState.applyCiv("structures/{civ}/market")); - else - this.routeProspection = false; - return; - } - this.routeProspection = false; - if (!this.isNewMarketWorth(marketPos[3])) - return; // position found, but not enough gain compared to our present route - - if (this.Config.debug > 1) - { - if (this.potentialTradeRoute) + if (this.Config.chat) { - aiWarn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " + - marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain); + let owner = gameState.getEntityById(this.tradeRoute.source).owner(); + if (owner == PlayerID) + owner = gameState.getEntityById(this.tradeRoute.target).owner(); + if (owner != PlayerID && !this.warnedAllies[owner]) + { // Warn an ally that we have a trade route with him + chatNewTradeRoute(gameState, owner); + this.warnedAllies[owner] = true; + } } - else + + if (accessIndex) { - aiWarn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " + - marketPos[3]); + if (bestIndex.gain > 0) + return bestIndex; + else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && bestLand.gain > 0) + return bestLand; + return false; + } + return true; + } + + /** Called when a market was built or destroyed, and checks if trader orders should be changed */ + checkTrader(gameState, ent) + { + const presentRoute = ent.getMetadata(PlayerID, "route"); + if (!presentRoute) + return; + + if (!ent.position()) + { + // This trader is garrisoned, we will decide later (when ungarrisoning) what to do + ent.setMetadata(PlayerID, "route", undefined); + return; + } + + const access = ent.hasClass("Ship") ? getSeaAccess(gameState, ent) : getLandAccess(gameState, ent); + const possibleRoute = this.checkRoutes(gameState, access); + // Warning: presentRoute is from metadata, so contains entity ids + if (!possibleRoute || + possibleRoute.source != presentRoute.source && possibleRoute.source != presentRoute.target || + possibleRoute.target != presentRoute.source && possibleRoute.target != presentRoute.target) + { + // Trader will be assigned in updateTrader + ent.setMetadata(PlayerID, "route", undefined); + if (!possibleRoute && !ent.hasClass("Ship")) + { + const closestBase = getBestBase(gameState, ent, true); + if (closestBase.accessIndex == access) + { + const closestBasePos = closestBase.anchor.position(); + ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15); + return; + } + } + ent.stopMoving(); } } - if (!this.tradeRoute) - gameState.ai.queueManager.changePriority("economicBuilding", 2 * this.Config.priorities.economicBuilding); - const plan = new ConstructionPlan(gameState, "structures/{civ}/market"); - if (!this.tradeRoute) - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -TradeManager.prototype.isNewMarketWorth = function(expectedGain) -{ - if (!Resources.GetTradableCodes().length) - return false; - if (expectedGain < this.minimalGain) - return false; - if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && - expectedGain < this.potentialTradeRoute.gain + 20) - return false; - return true; -}; - -TradeManager.prototype.update = function(gameState, events, queues) -{ - if (gameState.ai.HQ.canBarter && Resources.GetBarterableCodes().length) - this.performBarter(gameState); - - if (this.Config.difficulty <= difficulty.VERY_EASY) - return; - - if (this.checkEvents(gameState, events)) // true if one market was built or destroyed + prospectForNewMarket(gameState, queues) { - this.traders.forEach(ent => { this.checkTrader(gameState, ent); }); + if (queues.economicBuilding.hasQueuedUnitsWithClass("Trade") || queues.dock.hasQueuedUnitsWithClass("Trade")) + return; + if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market")) + return; + if (!gameState.updatingCollection("OwnMarkets", filters.byClass("Trade"), + gameState.getOwnStructures()).hasEntities() && + !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", filters.byClass("Trade"), + gameState.getExclusiveAllyEntities()).hasEntities()) + { + return; + } + const template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/market")); + if (!template) + return; this.checkRoutes(gameState); + const marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); + if (!marketPos || marketPos[3] == 0) // marketPos[3] is the expected gain + { // no position found + if (gameState.getOwnEntitiesByClass("Market", true).hasEntities()) + gameState.ai.HQ.buildManager.setUnbuildable(gameState, gameState.applyCiv("structures/{civ}/market")); + else + this.routeProspection = false; + return; + } + this.routeProspection = false; + if (!this.isNewMarketWorth(marketPos[3])) + return; // position found, but not enough gain compared to our present route + + if (this.Config.debug > 1) + { + if (this.potentialTradeRoute) + { + aiWarn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " + + marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain); + } + else + { + aiWarn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " + + marketPos[3]); + } + } + + if (!this.tradeRoute) + gameState.ai.queueManager.changePriority("economicBuilding", 2 * this.Config.priorities.economicBuilding); + const plan = new ConstructionPlan(gameState, "structures/{civ}/market"); + if (!this.tradeRoute) + plan.queueToReset = "economicBuilding"; + queues.economicBuilding.addPlan(plan); } - if (this.tradeRoute) + isNewMarketWorth(expectedGain) { - this.traders.forEach(ent => { this.updateTrader(gameState, ent); }); - if (gameState.ai.playedTurn % 5 == 0) - this.trainMoreTraders(gameState, queues); - if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2) - gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues); - if (gameState.ai.playedTurn % 60 == 0) - this.setTradingGoods(gameState); + if (!Resources.GetTradableCodes().length) + return false; + if (expectedGain < this.minimalGain) + return false; + if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && + expectedGain < this.potentialTradeRoute.gain + 20) + return false; + return true; } - if (this.routeProspection) - this.prospectForNewMarket(gameState, queues); -}; + update(gameState, events, queues) + { + if (gameState.ai.HQ.canBarter && Resources.GetBarterableCodes().length) + this.performBarter(gameState); -TradeManager.prototype.Serialize = function() -{ - return { - "tradeRoute": this.tradeRoute, - "potentialTradeRoute": this.potentialTradeRoute, - "routeProspection": this.routeProspection, - "targetNumTraders": this.targetNumTraders, - "warnedAllies": this.warnedAllies - }; -}; + if (this.Config.difficulty <= difficulty.VERY_EASY) + return; -TradeManager.prototype.Deserialize = function(gameState, data) -{ - for (const key in data) - this[key] = data[key]; -}; + if (this.checkEvents(gameState, events)) // true if one market was built or destroyed + { + this.traders.forEach(ent => { this.checkTrader(gameState, ent); }); + this.checkRoutes(gameState); + } + + if (this.tradeRoute) + { + this.traders.forEach(ent => { this.updateTrader(gameState, ent); }); + if (gameState.ai.playedTurn % 5 == 0) + this.trainMoreTraders(gameState, queues); + if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2) + gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues); + if (gameState.ai.playedTurn % 60 == 0) + this.setTradingGoods(gameState); + } + + if (this.routeProspection) + this.prospectForNewMarket(gameState, queues); + } + + Serialize() + { + return { + "tradeRoute": this.tradeRoute, + "potentialTradeRoute": this.potentialTradeRoute, + "routeProspection": this.routeProspection, + "targetNumTraders": this.targetNumTraders, + "warnedAllies": this.warnedAllies + }; + } + + Deserialize(gameState, data) + { + for (const key in data) + this[key] = data[key]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/transportPlan.js b/binaries/data/mods/public/simulation/ai/petra/transportPlan.js index 39a4e52ffd..b0156a76de 100644 --- a/binaries/data/mods/public/simulation/ai/petra/transportPlan.js +++ b/binaries/data/mods/public/simulation/ai/petra/transportPlan.js @@ -25,726 +25,729 @@ import { Worker } from "simulation/ai/petra/worker.js"; * transporter = this.ID */ -export function TransportPlan(gameState, units, startIndex, endIndex, endPos, ship) +export class TransportPlan { - 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 + constructor(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.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) { - this.failed = true; - return false; - } - this.sea = ship.getMetadata(PlayerID, "sea"); - ship.setMetadata(PlayerID, "transporter", this.ID); - ship.setStance("none"); - for (const ent of units) - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - } - else - { - this.sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, startIndex, endIndex); - if (!this.sea) - { - this.failed = true; - if (this.debug > 1) + // special transport from already garrisoned ship + if (!ship) { - aiWarn("transport plan with bad path: startIndex " + startIndex + " endIndex " + - endIndex); + this.failed = true; + return false; + } + this.sea = ship.getMetadata(PlayerID, "sea"); + ship.setMetadata(PlayerID, "transporter", this.ID); + ship.setStance("none"); + for (const ent of units) + ent.setMetadata(PlayerID, "onBoard", "onBoard"); + } + else + { + this.sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, startIndex, endIndex); + if (!this.sea) + { + this.failed = true; + if (this.debug > 1) + { + aiWarn("transport plan with bad path: startIndex " + startIndex + " endIndex " + + endIndex); + } + return false; } - return false; } - } - for (const ent of units) - { - ent.setMetadata(PlayerID, "transport", this.ID); - ent.setMetadata(PlayerID, "endPos", endPos); - } + for (const ent of units) + { + ent.setMetadata(PlayerID, "transport", this.ID); + ent.setMetadata(PlayerID, "endPos", endPos); + } - if (this.debug > 1) - { - aiWarn("Starting a new transport plan with ID " + this.ID + " to index " + endIndex + - " with units length " + units.length); - } - - this.state = TransportPlan.BOARDING; - this.boardingPos = {}; - this.needTransportShips = ship === undefined; - this.nTry = {}; - return true; -} - -/** - * We're trying to board units onto our ships. - */ -TransportPlan.BOARDING = "boarding"; -/** - * We're moving ships and eventually unload units. - */ -TransportPlan.SAILING = "sailing"; - -TransportPlan.prototype.init = function(gameState) -{ - this.units = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "transport", this.ID)); - this.ships = gameState.ai.HQ.navalManager.ships.filter(filters.byMetadata(PlayerID, "transporter", - this.ID)); - this.transportShips = gameState.ai.HQ.navalManager.transportShips.filter(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 */ -TransportPlan.prototype.countFreeSlots = function() -{ - let slots = 0; - for (const ship of this.transportShips.values()) - slots += this.countFreeSlotsOnShip(ship); - return slots; -}; - -TransportPlan.prototype.countFreeSlotsOnShip = function(ship) -{ - if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints()) - return 0; - const occupied = ship.garrisoned().length + - this.units.filter(filters.byMetadata(PlayerID, "onBoard", ship.id())).length; - return Math.max(ship.garrisonMax() - occupied, 0); -}; - -TransportPlan.prototype.assignUnitToShip = function(gameState, ent) -{ - if (this.needTransportShips) - return; - - for (const 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") === Worker.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] }); + aiWarn("Starting a new transport plan with ID " + this.ID + " to index " + endIndex + + " with units length " + units.length); } - return; - } - if (this.flotilla) - { - this.needTransportShips = true; - return; - } - - if (!this.needSplit) - this.needSplit = [ent]; - else - this.needSplit.push(ent); -}; - -TransportPlan.prototype.assignShip = function(gameState) -{ - let pos; - // choose a unit of this plan not yet assigned to a ship - for (const 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(ship => - { - if (ship.getMetadata(PlayerID, "transporter")) - return; - if (pos) - { - const dist = 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); - nearest.setStance("none"); - this.ships.updateEnt(nearest); - this.transportShips.updateEnt(nearest); - this.needTransportShips = false; - return true; -}; - -/** add a unit to this plan */ -TransportPlan.prototype.addUnit = function(unit, endPos) -{ - unit.setMetadata(PlayerID, "transport", this.ID); - unit.setMetadata(PlayerID, "endPos", endPos); - this.units.updateEnt(unit); -}; - -/** remove a unit from this plan, if not yet on board */ -TransportPlan.prototype.removeUnit = function(gameState, unit) -{ - const shipId = unit.getMetadata(PlayerID, "onBoard"); - if (shipId == "onBoard") - return; // too late, already onBoard - else if (shipId !== undefined) - unit.stopMoving(); // cancel the garrison order - unit.setMetadata(PlayerID, "transport", undefined); - unit.setMetadata(PlayerID, "endPos", undefined); - this.units.updateEnt(unit); - if (shipId) - { - unit.setMetadata(PlayerID, "onBoard", undefined); - const ship = gameState.getEntityById(shipId); - if (ship && !ship.garrisoned().length && - !this.units.filter(filters.byMetadata(PlayerID, "onBoard", shipId)).length) - { - this.releaseShip(ship); - this.ships.updateEnt(ship); - this.transportShips.updateEnt(ship); - } - } -}; - -TransportPlan.prototype.releaseShip = function(ship) -{ - if (ship.getMetadata(PlayerID, "transporter") != this.ID) - { - aiWarn(" Petra: try removing a transporter ship with " + - ship.getMetadata(PlayerID, "transporter") + " from " + this.ID + " and stance " + - ship.getStance()); - return; - } - - const defaultStance = ship.get("UnitAI/DefaultStance"); - if (defaultStance) - ship.setStance(defaultStance); - - ship.setMetadata(PlayerID, "transporter", undefined); - if (ship.getMetadata(PlayerID, "role") === Worker.ROLE_SWITCH_TO_TRADER) - ship.setMetadata(PlayerID, "role", Worker.ROLE_TRADER); -}; - -TransportPlan.prototype.releaseAll = function() -{ - for (const ship of this.ships.values()) - this.releaseShip(ship); - - for (const ent of this.units.values()) - { - 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(); -}; - -/** TODO not currently used ... to be fixed */ -TransportPlan.prototype.cancelTransport = function(gameState) -{ - const ent = this.units.toEntityArray()[0]; - let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchor || !base.anchor.position()) - { - for (const 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(unit => { unit.setMetadata(PlayerID, "base", base.ID); }); - } - this.endIndex = this.startIndex; - this.endPos = base.anchor.position(); - this.canceled = true; - return true; -}; - - -/** - * Try to move on and then clear the plan. - */ -TransportPlan.prototype.update = function(gameState) -{ - if (this.state === TransportPlan.BOARDING) - this.onBoarding(gameState); - else if (this.state === TransportPlan.SAILING) - this.onSailing(gameState); - - return this.units.length; -}; - -TransportPlan.prototype.onBoarding = function(gameState) -{ - let ready = true; - const time = gameState.ai.elapsedTime; - const shipTested = {}; - - for (const ent of this.units.values()) - { - if (!ent.getMetadata(PlayerID, "onBoard")) - { - ready = false; - this.assignUnitToShip(gameState, ent); - if (ent.getMetadata(PlayerID, "onBoard")) - { - const shipId = ent.getMetadata(PlayerID, "onBoard"); - const ship = gameState.getEntityById(shipId); - if (!this.boardingPos[shipId]) - { - this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.startIndex, this.sea, ent.position(), false); - ship.move(this.boardingPos[shipId][0], this.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" && !this.isOnBoard(ent)) - { - ready = false; - const shipId = ent.getMetadata(PlayerID, "onBoard"); - const ship = gameState.getEntityById(shipId); - if (!ship) // the ship must have been destroyed - { - ent.setMetadata(PlayerID, "onBoard", undefined); - continue; - } - const distShip = SquareVectorDistance(this.boardingPos[shipId], ship.position()); - if (!shipTested[shipId] && distShip > this.boardingRange) - { - shipTested[shipId] = true; - let retry = false; - const unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.WALKING" || - unitAIState == "INDIVIDUAL.PICKUP.APPROACHING") - { - if (time - ship.getMetadata(PlayerID, "timeGarrison") > 2) - { - const oldPos = ent.getMetadata(PlayerID, "posGarrison"); - const newPos = ent.position(); - if (oldPos[0] == newPos[0] && oldPos[1] == newPos[1]) - retry = true; - ent.setMetadata(PlayerID, "posGarrison", newPos); - ent.setMetadata(PlayerID, "timeGarrison", time); - } - } - - else if (unitAIState != "INDIVIDUAL.PICKUP.LOADING" && - time - ship.getMetadata(PlayerID, "timeGarrison") > 5 || - time - ship.getMetadata(PlayerID, "timeGarrison") > 8) - { - retry = true; - ent.setMetadata(PlayerID, "timeGarrison", time); - } - - if (retry) - { - if (!this.nTry[shipId]) - this.nTry[shipId] = 1; - else - ++this.nTry[shipId]; - if (this.nTry[shipId] > 1) // we must have been blocked by something ... try with another boarding point - { - this.nTry[shipId] = 0; - if (this.debug > 1) - aiWarn("ship " + shipId + " new attempt for a landing point "); - this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.startIndex, this.sea, undefined, false); - } - ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); - ship.setMetadata(PlayerID, "timeGarrison", time); - } - } - - if (time - ent.getMetadata(PlayerID, "timeGarrison") > 2) - { - const oldPos = ent.getMetadata(PlayerID, "posGarrison"); - const newPos = ent.position(); - if (oldPos[0] == newPos[0] && oldPos[1] == newPos[1]) - { - if (distShip < this.boardingRange) // looks like we are blocked ... try to go out of this trap - { - if (!this.nTry[ent.id()]) - this.nTry[ent.id()] = 1; - else - ++this.nTry[ent.id()]; - if (this.nTry[ent.id()] > 5) - { - if (this.debug > 1) - { - aiWarn("unit blocked, but no ways out of the trap ... " + - "destroy it"); - } - this.resetUnit(gameState, ent); - ent.destroy(); - continue; - } - if (this.nTry[ent.id()] > 1) - ent.moveToRange(newPos[0], newPos[1], 30, 35); - ent.garrison(ship, true); - } - else if (SquareVectorDistance(this.boardingPos[shipId], newPos) > 225) - ent.moveToRange(this.boardingPos[shipId][0], this.boardingPos[shipId][1], 0, 15); - } - else - this.nTry[ent.id()] = 0; - ent.setMetadata(PlayerID, "timeGarrison", time); - ent.setMetadata(PlayerID, "posGarrison", ent.position()); - } - } - } - - if (this.needSplit) - { - gameState.ai.HQ.navalManager.splitTransport(gameState, this); - this.needSplit = undefined; - } - - if (!ready) - return; - - for (const 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.state = TransportPlan.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 */ -TransportPlan.prototype.isOnBoard = function(ent) -{ - for (const ship of this.transportShips.values()) - { - if (ship.garrisoned().indexOf(ent.id()) == -1) - continue; - ent.setMetadata(PlayerID, "onBoard", "onBoard"); + this.state = TransportPlan.BOARDING; + this.boardingPos = {}; + this.needTransportShips = ship === undefined; + this.nTry = {}; return true; } - return false; -}; -/** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */ -TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, seaIndex, destination, - avoidEnnemy) -{ - if (!gameState.ai.HQ.navalManager.landingZones[landIndex]) + /** + * We're trying to board units onto our ships. + */ + static BOARDING = "boarding"; + /** + * We're moving ships and eventually unload units. + */ + static SAILING = "sailing"; + + init(gameState) { - aiWarn(" >>> no landing zone for land " + landIndex); - return destination; - } - else if (!gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) - { - aiWarn(" >>> no landing zone for land " + landIndex + " and sea " + seaIndex); - return destination; + this.units = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "transport", this.ID)); + this.ships = gameState.ai.HQ.navalManager.ships.filter(filters.byMetadata(PlayerID, "transporter", + this.ID)); + this.transportShips = gameState.ai.HQ.navalManager.transportShips.filter(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 } - const startPos = ship.position(); - let distmin = Math.min(); - let posmin = destination; - const width = gameState.getPassabilityMap().width; - const cell = gameState.getPassabilityMap().cellSize; - const alliedDocks = gameState.getAllyStructures().filter(filters.and(filters.byClass("Dock"), - filters.byMetadata(PlayerID, "sea", seaIndex))).toEntityArray(); - for (const i of gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) + /** count available slots */ + countFreeSlots() { - let pos = [i%width+0.5, Math.floor(i/width)+0.5]; - pos = [cell*pos[0], cell*pos[1]]; - let dist = VectorDistance(startPos, pos); - if (destination) - dist += VectorDistance(pos, destination); - if (avoidEnnemy) + let slots = 0; + for (const ship of this.transportShips.values()) + slots += this.countFreeSlotsOnShip(ship); + return slots; + } + + countFreeSlotsOnShip(ship) + { + if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints()) + return 0; + const occupied = ship.garrisoned().length + + this.units.filter(filters.byMetadata(PlayerID, "onBoard", ship.id())).length; + return Math.max(ship.garrisonMax() - occupied, 0); + } + + assignUnitToShip(gameState, ent) + { + if (this.needTransportShips) + return; + + for (const ship of this.transportShips.values()) { - const 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 (const shipId in this.boardingPos) - { - if (this.boardingPos[shipId] !== undefined && - SquareVectorDistance(this.boardingPos[shipId], pos) < this.boardingRange) + if (this.countFreeSlotsOnShip(ship) == 0) + continue; + ent.setMetadata(PlayerID, "onBoard", ship.id()); + if (this.debug > 1) { - dist += 1000000; + if (ent.getMetadata(PlayerID, "role") === Worker.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; + return; + } + + if (!this.needSplit) + this.needSplit = [ent]; + else + this.needSplit.push(ent); + } + + assignShip(gameState) + { + let pos; + // choose a unit of this plan not yet assigned to a ship + for (const 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(ship => + { + if (ship.getMetadata(PlayerID, "transporter")) + return; + if (pos) + { + const dist = 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); + nearest.setStance("none"); + this.ships.updateEnt(nearest); + this.transportShips.updateEnt(nearest); + this.needTransportShips = false; + return true; + } + + /** add a unit to this plan */ + addUnit(unit, endPos) + { + unit.setMetadata(PlayerID, "transport", this.ID); + unit.setMetadata(PlayerID, "endPos", endPos); + this.units.updateEnt(unit); + } + + /** remove a unit from this plan, if not yet on board */ + removeUnit(gameState, unit) + { + const shipId = unit.getMetadata(PlayerID, "onBoard"); + if (shipId == "onBoard") + return; // too late, already onBoard + else if (shipId !== undefined) + unit.stopMoving(); // cancel the garrison order + unit.setMetadata(PlayerID, "transport", undefined); + unit.setMetadata(PlayerID, "endPos", undefined); + this.units.updateEnt(unit); + if (shipId) + { + unit.setMetadata(PlayerID, "onBoard", undefined); + const ship = gameState.getEntityById(shipId); + if (ship && !ship.garrisoned().length && + !this.units.filter(filters.byMetadata(PlayerID, "onBoard", shipId)).length) + { + this.releaseShip(ship); + this.ships.updateEnt(ship); + this.transportShips.updateEnt(ship); } } - // and not too near our allied docks to not disturb naval traffic - let distSquare; - for (const dock of alliedDocks) - { - if (dock.foundationProgress() !== undefined) - distSquare = 900; - else - distSquare = 4900; - const dockDist = SquareVectorDistance(dock.position(), pos); - if (dockDist < distSquare) - dist += 100000 * (distSquare - dockDist) / distSquare; - } - 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; -}; -TransportPlan.prototype.onSailing = function(gameState) -{ - // Check that the units recovered on the previous turn have been reloaded - for (const recov of this.recovered) + releaseShip(ship) { - const ent = gameState.getEntityById(recov.entId); - if (!ent) // entity destroyed - continue; - if (!ent.position()) // reloading succeeded ... move a bit the ship before trying again + if (ship.getMetadata(PlayerID, "transporter") != this.ID) { - const ship = gameState.getEntityById(recov.shipId); - if (ship) - ship.moveApart(recov.entPos, 15); - continue; - } - if (this.debug > 1) - aiWarn(">>> transport " + this.ID + " reloading failed ... <<<"); - // destroy the unit if inaccessible otherwise leave it there - const index = getLandAccess(gameState, ent); - if (gameState.ai.HQ.landRegions[index]) - { - if (this.debug > 1) - aiWarn(" 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) - aiWarn("recovered entity destroyed " + ent.id()); - this.resetUnit(gameState, ent); - ent.destroy(); + aiWarn(" Petra: try removing a transporter ship with " + + ship.getMetadata(PlayerID, "transporter") + " from " + this.ID + " and stance " + + ship.getStance()); + return; } + + const defaultStance = ship.get("UnitAI/DefaultStance"); + if (defaultStance) + ship.setStance(defaultStance); + + ship.setMetadata(PlayerID, "transporter", undefined); + if (ship.getMetadata(PlayerID, "role") === Worker.ROLE_SWITCH_TO_TRADER) + ship.setMetadata(PlayerID, "role", Worker.ROLE_TRADER); } - this.recovered = []; - // Check that the units unloaded on the previous turn have been really unloaded and in the right position - const shipsToMove = {}; - for (const entId of this.unloaded) + releaseAll() { - const ent = gameState.getEntityById(entId); - if (!ent) // entity destroyed - continue; - else if (!ent.position()) // unloading failed + for (const ship of this.ships.values()) + this.releaseShip(ship); + + for (const ent of this.units.values()) { - const ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); - if (ship) + 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(); + } + + /** TODO not currently used ... to be fixed */ + cancelTransport(gameState) + { + const ent = this.units.toEntityArray()[0]; + let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); + if (!base.anchor || !base.anchor.position()) + { + for (const newbase of gameState.ai.HQ.baseManagers()) { - if (ship.garrisoned().indexOf(entId) != -1) - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - else + 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(unit => { unit.setMetadata(PlayerID, "base", base.ID); }); + } + this.endIndex = this.startIndex; + this.endPos = base.anchor.position(); + this.canceled = true; + return true; + } + + + /** + * Try to move on and then clear the plan. + */ + update(gameState) + { + if (this.state === TransportPlan.BOARDING) + this.onBoarding(gameState); + else if (this.state === TransportPlan.SAILING) + this.onSailing(gameState); + + return this.units.length; + } + + onBoarding(gameState) + { + let ready = true; + const time = gameState.ai.elapsedTime; + const shipTested = {}; + + for (const ent of this.units.values()) + { + if (!ent.getMetadata(PlayerID, "onBoard")) + { + ready = false; + this.assignUnitToShip(gameState, ent); + if (ent.getMetadata(PlayerID, "onBoard")) { - aiWarn("Petra transportPlan problem: unit not on ship without position ???"); - this.resetUnit(gameState, ent); - ent.destroy(); + const shipId = ent.getMetadata(PlayerID, "onBoard"); + const ship = gameState.getEntityById(shipId); + if (!this.boardingPos[shipId]) + { + this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.startIndex, this.sea, ent.position(), false); + ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); + ship.setMetadata(PlayerID, "timeGarrison", time); + } + ent.garrison(ship); + ent.setMetadata(PlayerID, "timeGarrison", time); + ent.setMetadata(PlayerID, "posGarrison", ent.position()); } } - else + else if (ent.getMetadata(PlayerID, "onBoard") != "onBoard" && !this.isOnBoard(ent)) { - aiWarn("Petra transportPlan problem: unit on ship, but no ship ???"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else if (getLandAccess(gameState, ent) != this.endIndex) - { - // unit unloaded on a wrong region - try to regarrison it and move a bit the ship - if (this.debug > 1) - aiWarn(">>> unit unloaded on a wrong region ! try to garrison it again <<<"); - const 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) - aiWarn("no way ... we destroy it"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else - { - // And make some room for other units - const pos = ent.position(); - const goal = ent.getMetadata(PlayerID, "endPos"); - const dist = goal ? VectorDistance(pos, goal) : 0; - if (dist > 30) - ent.moveToRange(goal[0], goal[1], dist-25, dist-20); - else - ent.moveToRange(pos[0], pos[1], 20, 25); - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - } - } - for (const 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 = []; + ready = false; + const shipId = ent.getMetadata(PlayerID, "onBoard"); + const ship = gameState.getEntityById(shipId); + if (!ship) // the ship must have been destroyed + { + ent.setMetadata(PlayerID, "onBoard", undefined); + continue; + } + const distShip = SquareVectorDistance(this.boardingPos[shipId], ship.position()); + if (!shipTested[shipId] && distShip > this.boardingRange) + { + shipTested[shipId] = true; + let retry = false; + const unitAIState = ship.unitAIState(); + if (unitAIState == "INDIVIDUAL.WALKING" || + unitAIState == "INDIVIDUAL.PICKUP.APPROACHING") + { + if (time - ship.getMetadata(PlayerID, "timeGarrison") > 2) + { + const oldPos = ent.getMetadata(PlayerID, "posGarrison"); + const newPos = ent.position(); + if (oldPos[0] == newPos[0] && oldPos[1] == newPos[1]) + retry = true; + ent.setMetadata(PlayerID, "posGarrison", newPos); + ent.setMetadata(PlayerID, "timeGarrison", time); + } + } + + else if (unitAIState != "INDIVIDUAL.PICKUP.LOADING" && + time - ship.getMetadata(PlayerID, "timeGarrison") > 5 || + time - ship.getMetadata(PlayerID, "timeGarrison") > 8) + { + retry = true; + ent.setMetadata(PlayerID, "timeGarrison", time); + } + + if (retry) + { + if (!this.nTry[shipId]) + this.nTry[shipId] = 1; + else + ++this.nTry[shipId]; + if (this.nTry[shipId] > 1) // we must have been blocked by something ... try with another boarding point + { + this.nTry[shipId] = 0; + if (this.debug > 1) + aiWarn("ship " + shipId + " new attempt for a landing point "); + this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.startIndex, this.sea, undefined, false); + } + ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); + ship.setMetadata(PlayerID, "timeGarrison", time); + } + } + + if (time - ent.getMetadata(PlayerID, "timeGarrison") > 2) + { + const oldPos = ent.getMetadata(PlayerID, "posGarrison"); + const newPos = ent.position(); + if (oldPos[0] == newPos[0] && oldPos[1] == newPos[1]) + { + if (distShip < this.boardingRange) // looks like we are blocked ... try to go out of this trap + { + if (!this.nTry[ent.id()]) + this.nTry[ent.id()] = 1; + else + ++this.nTry[ent.id()]; + if (this.nTry[ent.id()] > 5) + { + if (this.debug > 1) + { + aiWarn("unit blocked, but no ways out of the trap ... " + + "destroy it"); + } + this.resetUnit(gameState, ent); + ent.destroy(); + continue; + } + if (this.nTry[ent.id()] > 1) + ent.moveToRange(newPos[0], newPos[1], 30, 35); + ent.garrison(ship, true); + } + else if (SquareVectorDistance(this.boardingPos[shipId], newPos) > 225) + ent.moveToRange(this.boardingPos[shipId][0], this.boardingPos[shipId][1], 0, 15); + } + else + this.nTry[ent.id()] = 0; + ent.setMetadata(PlayerID, "timeGarrison", time); + ent.setMetadata(PlayerID, "posGarrison", ent.position()); + } + } + } + + if (this.needSplit) + { + gameState.ai.HQ.navalManager.splitTransport(gameState, this); + this.needSplit = undefined; + } + + if (!ready) + return; - if (this.canceled) - { for (const 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; + this.state = TransportPlan.SAILING; + this.nTry = {}; + this.unloaded = []; + this.recovered = []; } - for (const ship of this.transportShips.values()) + /** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */ + isOnBoard(ent) { - if (ship.unitAIState() == "INDIVIDUAL.WALKING") - continue; - const shipId = ship.id(); - const dist = SquareVectorDistance(ship.position(), this.boardingPos[shipId]); - let remaining = 0; - for (const entId of ship.garrisoned()) + for (const 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 */ + getBoardingPos(gameState, ship, landIndex, seaIndex, destination, + avoidEnnemy) + { + if (!gameState.ai.HQ.navalManager.landingZones[landIndex]) + { + aiWarn(" >>> no landing zone for land " + landIndex); + return destination; + } + else if (!gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) + { + aiWarn(" >>> no landing zone for land " + landIndex + " and sea " + seaIndex); + return destination; + } + + const startPos = ship.position(); + let distmin = Math.min(); + let posmin = destination; + const width = gameState.getPassabilityMap().width; + const cell = gameState.getPassabilityMap().cellSize; + const alliedDocks = gameState.getAllyStructures().filter(filters.and(filters.byClass("Dock"), + filters.byMetadata(PlayerID, "sea", seaIndex))).toEntityArray(); + for (const 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 = VectorDistance(startPos, pos); + if (destination) + dist += VectorDistance(pos, destination); + if (avoidEnnemy) + { + const 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 (const shipId in this.boardingPos) + { + if (this.boardingPos[shipId] !== undefined && + SquareVectorDistance(this.boardingPos[shipId], pos) < this.boardingRange) + { + dist += 1000000; + } + } + // and not too near our allied docks to not disturb naval traffic + let distSquare; + for (const dock of alliedDocks) + { + if (dock.foundationProgress() !== undefined) + distSquare = 900; + else + distSquare = 4900; + const dockDist = SquareVectorDistance(dock.position(), pos); + if (dockDist < distSquare) + dist += 100000 * (distSquare - dockDist) / distSquare; + } + 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; + } + + onSailing(gameState) + { + // Check that the units recovered on the previous turn have been reloaded + for (const recov of this.recovered) + { + const ent = gameState.getEntityById(recov.entId); + if (!ent) // entity destroyed + continue; + if (!ent.position()) // reloading succeeded ... move a bit the ship before trying again + { + const ship = gameState.getEntityById(recov.shipId); + if (ship) + ship.moveApart(recov.entPos, 15); + continue; + } + if (this.debug > 1) + aiWarn(">>> transport " + this.ID + " reloading failed ... <<<"); + // destroy the unit if inaccessible otherwise leave it there + const index = getLandAccess(gameState, ent); + if (gameState.ai.HQ.landRegions[index]) + { + if (this.debug > 1) + aiWarn(" 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) + aiWarn("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 + const shipsToMove = {}; + for (const entId of this.unloaded) { const ent = gameState.getEntityById(entId); - if (!ent.getMetadata(PlayerID, "transport")) + if (!ent) // entity destroyed continue; - remaining++; - if (dist < 625) + else if (!ent.position()) // unloading failed { - ship.unload(entId); - this.unloaded.push(entId); - ent.setMetadata(PlayerID, "onBoard", shipId); + const ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); + if (ship) + { + if (ship.garrisoned().indexOf(entId) != -1) + ent.setMetadata(PlayerID, "onBoard", "onBoard"); + else + { + aiWarn("Petra transportPlan problem: unit not on ship without position ???"); + this.resetUnit(gameState, ent); + ent.destroy(); + } + } + else + { + aiWarn("Petra transportPlan problem: unit on ship, but no ship ???"); + this.resetUnit(gameState, ent); + ent.destroy(); + } } - } - - let recovering = 0; - for (const 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], 30); - this.releaseShip(ship); - 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 + else if (getLandAccess(gameState, ent) != this.endIndex) { - this.nTry[shipId] = 0; + // unit unloaded on a wrong region - try to regarrison it and move a bit the ship if (this.debug > 1) - aiWarn(shipId + " new attempt for a landing point "); - this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, undefined, true); + aiWarn(">>> unit unloaded on a wrong region ! try to garrison it again <<<"); + const 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) + aiWarn("no way ... we destroy it"); + this.resetUnit(gameState, ent); + ent.destroy(); + } + } + else + { + // And make some room for other units + const pos = ent.position(); + const goal = ent.getMetadata(PlayerID, "endPos"); + const dist = goal ? VectorDistance(pos, goal) : 0; + if (dist > 30) + ent.moveToRange(goal[0], goal[1], dist-25, dist-20); + else + ent.moveToRange(pos[0], pos[1], 20, 25); + ent.setMetadata(PlayerID, "transport", undefined); + ent.setMetadata(PlayerID, "onBoard", undefined); + ent.setMetadata(PlayerID, "endPos", undefined); + } + } + for (const 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 (const 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 (const ship of this.transportShips.values()) + { + if (ship.unitAIState() == "INDIVIDUAL.WALKING") + continue; + const shipId = ship.id(); + const dist = SquareVectorDistance(ship.position(), this.boardingPos[shipId]); + let remaining = 0; + for (const entId of ship.garrisoned()) + { + const 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 (const 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], 30); + this.releaseShip(ship); + 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) + aiWarn(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]); } - ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); } } -}; -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") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) + resetUnit(gameState, ent) { - const attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); + 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") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) + { + const attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); + if (attackPlan) + attackPlan.removeUnit(ent, true); + } + if (ent.getMetadata(PlayerID, "PartOfArmy")) + { + const army = gameState.ai.HQ.defenseManager.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")); + if (army) + army.removeOwn(gameState, ent.id()); + } } - if (ent.getMetadata(PlayerID, "PartOfArmy")) + + Serialize() { - const army = gameState.ai.HQ.defenseManager.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")); - if (army) - army.removeOwn(gameState, ent.id()); + 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 + }; } -}; -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 - }; -}; + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; -TransportPlan.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; - - this.failed = false; -}; + this.failed = false; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/victoryManager.js b/binaries/data/mods/public/simulation/ai/petra/victoryManager.js index 53308fbf91..088bb11385 100644 --- a/binaries/data/mods/public/simulation/ai/petra/victoryManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/victoryManager.js @@ -13,756 +13,759 @@ import { Worker } from "simulation/ai/petra/worker.js"; * in wonder, train military guards. */ -export function VictoryManager(Config) +export class VictoryManager { - this.Config = Config; - this.criticalEnts = new Map(); - // Holds ids of all ents who are (or can be) guarding and if the ent is currently guarding - this.guardEnts = new Map(); - this.healersPerCriticalEnt = 2 + Math.round(this.Config.personality.defensive * 2); - this.tryCaptureGaiaRelic = false; - this.tryCaptureGaiaRelicLapseTime = -1; - // Gaia relics which we are targeting currently and have not captured yet - this.targetedGaiaRelics = new Map(); -} - -/** - * Cache the ids of any inital victory-critical entities. - */ -VictoryManager.prototype.init = function(gameState) -{ - if (gameState.getVictoryConditions().has("wonder")) + constructor(Config) { - for (const wonder of gameState.getOwnEntitiesByClass("Wonder", true).values()) - this.criticalEnts.set(wonder.id(), { "guardsAssigned": new Set(), "guards": new Map() }); + this.Config = Config; + this.criticalEnts = new Map(); + // Holds ids of all ents who are (or can be) guarding and if the ent is currently guarding + this.guardEnts = new Map(); + this.healersPerCriticalEnt = 2 + Math.round(this.Config.personality.defensive * 2); + this.tryCaptureGaiaRelic = false; + this.tryCaptureGaiaRelicLapseTime = -1; + // Gaia relics which we are targeting currently and have not captured yet + this.targetedGaiaRelics = new Map(); } - if (gameState.getVictoryConditions().has("regicide")) + /** + * Cache the ids of any inital victory-critical entities. + */ + init(gameState) { - for (const hero of gameState.getOwnEntitiesByClass("Hero", true).values()) + if (gameState.getVictoryConditions().has("wonder")) { - const defaultStance = hero.hasClass("Soldier") ? "aggressive" : "passive"; - if (hero.getStance() != defaultStance) - hero.setStance(defaultStance); - this.criticalEnts.set(hero.id(), { - "garrisonEmergency": false, - "healersAssigned": new Set(), - "guardsAssigned": new Set(), // for non-healer guards - "guards": new Map() // ids of ents who are currently guarding this hero - }); - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - for (const relic of - gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic")).values()) - { - if (relic.owner() == PlayerID) - this.criticalEnts.set(relic.id(), { "guardsAssigned": new Set(), "guards": new Map() }); - } - } -}; - -/** - * In regicide victory condition, if the hero has less than 70% health, try to garrison it in a healing structure - * If it is less than 40%, try to garrison in the closest possible structure - * If the hero cannot garrison, retreat it to the closest base - */ -VictoryManager.prototype.checkEvents = function(gameState, events) -{ - if (gameState.getVictoryConditions().has("wonder")) - { - for (const evt of events.Create) - { - const ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() === undefined || - !ent.hasClass("Wonder")) - continue; - - // Let's get a few units from other bases to build the wonder. - const base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - const builders = gameState.ai.HQ.bulkPickWorkers(gameState, base, 10); - if (builders) - for (const worker of builders.values()) - { - worker.setMetadata(PlayerID, "base", base.ID); - worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - } + for (const wonder of gameState.getOwnEntitiesByClass("Wonder", true).values()) + this.criticalEnts.set(wonder.id(), { "guardsAssigned": new Set(), "guards": new Map() }); } - for (const evt of events.ConstructionFinished) + if (gameState.getVictoryConditions().has("regicide")) { - if (!evt || !evt.newentity) - continue; - - const ent = gameState.getEntityById(evt.newentity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Wonder")) - this.criticalEnts.set(ent.id(), { "guardsAssigned": new Set(), "guards": new Map() }); - } - } - - if (gameState.getVictoryConditions().has("regicide")) - { - for (const evt of events.Attacked) - { - if (!this.criticalEnts.has(evt.target)) - continue; - - const target = gameState.getEntityById(evt.target); - if (!target || !target.position() || target.healthLevel() > this.Config.garrisonHealthLevel.high) - continue; - - const plan = target.getMetadata(PlayerID, "plan"); - const hero = this.criticalEnts.get(evt.target); - if (plan != -2 && plan != -3) + for (const hero of gameState.getOwnEntitiesByClass("Hero", true).values()) { - target.stopMoving(); - - if (plan >= 0) - { - const attackPlan = gameState.ai.HQ.attackManager.getPlan(plan); - if (attackPlan) - attackPlan.removeUnit(target, true); - } - - if (target.getMetadata(PlayerID, "PartOfArmy")) - { - const army = gameState.ai.HQ.defenseManager.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); - if (army) - army.removeOwn(gameState, target.id()); - } - - hero.garrisonEmergency = target.healthLevel() < this.Config.garrisonHealthLevel.low; - this.pickCriticalEntRetreatLocation(gameState, target, hero.garrisonEmergency); - } - else if (target.healthLevel() < this.Config.garrisonHealthLevel.low && !hero.garrisonEmergency) - { - // the hero is severely wounded, try to retreat/garrison quicker - gameState.ai.HQ.garrisonManager.cancelGarrison(target); - this.pickCriticalEntRetreatLocation(gameState, target, true); - hero.garrisonEmergency = true; + const defaultStance = hero.hasClass("Soldier") ? "aggressive" : "passive"; + if (hero.getStance() != defaultStance) + hero.setStance(defaultStance); + this.criticalEnts.set(hero.id(), { + "garrisonEmergency": false, + "healersAssigned": new Set(), + "guardsAssigned": new Set(), // for non-healer guards + "guards": new Map() // ids of ents who are currently guarding this hero + }); } } - for (const evt of events.TrainingFinished) - for (const entId of evt.entities) + if (gameState.getVictoryConditions().has("capture_the_relic")) + { + for (const relic of + gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic")).values()) { - const ent = gameState.getEntityById(entId); - if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_HEALER) - this.assignGuardToCriticalEnt(gameState, ent); + if (relic.owner() == PlayerID) + this.criticalEnts.set(relic.id(), { "guardsAssigned": new Set(), "guards": new Map() }); + } + } + } + + /** + * In regicide victory condition, if the hero has less than 70% health, try to garrison it in a healing structure + * If it is less than 40%, try to garrison in the closest possible structure + * If the hero cannot garrison, retreat it to the closest base + */ + checkEvents(gameState, events) + { + if (gameState.getVictoryConditions().has("wonder")) + { + for (const evt of events.Create) + { + const ent = gameState.getEntityById(evt.entity); + if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() === undefined || + !ent.hasClass("Wonder")) + continue; + + // Let's get a few units from other bases to build the wonder. + const base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); + const builders = gameState.ai.HQ.bulkPickWorkers(gameState, base, 10); + if (builders) + for (const worker of builders.values()) + { + worker.setMetadata(PlayerID, "base", base.ID); + worker.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + worker.setMetadata(PlayerID, "target-foundation", ent.id()); + } } - for (const evt of events.Garrison) - { - if (!this.criticalEnts.has(evt.entity)) - continue; - - const hero = this.criticalEnts.get(evt.entity); - if (hero.garrisonEmergency) - hero.garrisonEmergency = false; - - const holderEnt = gameState.getEntityById(evt.holder); - if (!holderEnt) - continue; - - if (holderEnt.hasClass("Ship")) + for (const evt of events.ConstructionFinished) { - // If the hero is garrisoned on a ship, remove its guards + if (!evt || !evt.newentity) + continue; + + const ent = gameState.getEntityById(evt.newentity); + if (ent && ent.isOwn(PlayerID) && ent.hasClass("Wonder")) + this.criticalEnts.set(ent.id(), { "guardsAssigned": new Set(), "guards": new Map() }); + } + } + + if (gameState.getVictoryConditions().has("regicide")) + { + for (const evt of events.Attacked) + { + if (!this.criticalEnts.has(evt.target)) + continue; + + const target = gameState.getEntityById(evt.target); + if (!target || !target.position() || target.healthLevel() > this.Config.garrisonHealthLevel.high) + continue; + + const plan = target.getMetadata(PlayerID, "plan"); + const hero = this.criticalEnts.get(evt.target); + if (plan != -2 && plan != -3) + { + target.stopMoving(); + + if (plan >= 0) + { + const attackPlan = gameState.ai.HQ.attackManager.getPlan(plan); + if (attackPlan) + attackPlan.removeUnit(target, true); + } + + if (target.getMetadata(PlayerID, "PartOfArmy")) + { + const army = gameState.ai.HQ.defenseManager.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); + if (army) + army.removeOwn(gameState, target.id()); + } + + hero.garrisonEmergency = target.healthLevel() < this.Config.garrisonHealthLevel.low; + this.pickCriticalEntRetreatLocation(gameState, target, hero.garrisonEmergency); + } + else if (target.healthLevel() < this.Config.garrisonHealthLevel.low && !hero.garrisonEmergency) + { + // the hero is severely wounded, try to retreat/garrison quicker + gameState.ai.HQ.garrisonManager.cancelGarrison(target); + this.pickCriticalEntRetreatLocation(gameState, target, true); + hero.garrisonEmergency = true; + } + } + + for (const evt of events.TrainingFinished) + for (const entId of evt.entities) + { + const ent = gameState.getEntityById(entId); + if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_HEALER) + this.assignGuardToCriticalEnt(gameState, ent); + } + + for (const evt of events.Garrison) + { + if (!this.criticalEnts.has(evt.entity)) + continue; + + const hero = this.criticalEnts.get(evt.entity); + if (hero.garrisonEmergency) + hero.garrisonEmergency = false; + + const holderEnt = gameState.getEntityById(evt.holder); + if (!holderEnt) + continue; + + if (holderEnt.hasClass("Ship")) + { + // If the hero is garrisoned on a ship, remove its guards + for (const guardId of hero.guards.keys()) + { + const guardEnt = gameState.getEntityById(guardId); + if (!guardEnt) + continue; + + guardEnt.removeGuard(); + this.guardEnts.set(guardId, false); + } + hero.guards.clear(); + continue; + } + + // Move the current guards to the garrison location. + // TODO: try to garrison them with the critical ent. for (const guardId of hero.guards.keys()) { const guardEnt = gameState.getEntityById(guardId); if (!guardEnt) continue; - guardEnt.removeGuard(); - this.guardEnts.set(guardId, false); + const plan = guardEnt.getMetadata(PlayerID, "plan"); + + // Current military guards (with Soldier class) will have been assigned plan metadata, but healer guards + // are not assigned a plan, and so they could be already moving to garrison somewhere due to low health. + if (!guardEnt.hasClass("Soldier") && (plan == -2 || plan == -3)) + continue; + + const pos = holderEnt.position(); + const radius = holderEnt.obstructionRadius().max; + if (pos) + guardEnt.moveToRange(pos[0], pos[1], radius, radius + 5); } - hero.guards.clear(); + } + } + + for (const evt of events.EntityRenamed) + { + if (!this.guardEnts.has(evt.entity)) continue; - } - - // Move the current guards to the garrison location. - // TODO: try to garrison them with the critical ent. - for (const guardId of hero.guards.keys()) + for (const data of this.criticalEnts.values()) { - const guardEnt = gameState.getEntityById(guardId); - if (!guardEnt) + if (!data.guards.has(evt.entity)) continue; - - const plan = guardEnt.getMetadata(PlayerID, "plan"); - - // Current military guards (with Soldier class) will have been assigned plan metadata, but healer guards - // are not assigned a plan, and so they could be already moving to garrison somewhere due to low health. - if (!guardEnt.hasClass("Soldier") && (plan == -2 || plan == -3)) - continue; - - const pos = holderEnt.position(); - const radius = holderEnt.obstructionRadius().max; - if (pos) - guardEnt.moveToRange(pos[0], pos[1], radius, radius + 5); - } - } - } - - for (const evt of events.EntityRenamed) - { - if (!this.guardEnts.has(evt.entity)) - continue; - for (const data of this.criticalEnts.values()) - { - if (!data.guards.has(evt.entity)) - continue; - data.guards.set(evt.newentity, data.guards.get(evt.entity)); - data.guards.delete(evt.entity); - break; - } - this.guardEnts.set(evt.newentity, this.guardEnts.get(evt.entity)); - this.guardEnts.delete(evt.entity); - } - - // Check if new healers/guards need to be assigned to an ent - for (const evt of events.Destroy) - { - if (this.criticalEnts.has(evt.entity)) - { - this.removeCriticalEnt(gameState, evt.entity); - continue; - } - - if (!this.guardEnts.delete(evt.entity)) - continue; - - for (const data of this.criticalEnts.values()) - { - if (data.guards.delete(evt.entity)) - { - data.healersAssigned?.delete(evt.entity); - data.guardsAssigned.delete(evt.entity); - } - } - } - - for (const evt of events.UnGarrison) - { - if (!this.guardEnts.has(evt.entity) && !this.criticalEnts.has(evt.entity)) - continue; - - const ent = gameState.getEntityById(evt.entity); - if (!ent) - continue; - - // If this ent travelled to a criticalEnt's accessValue, try again to assign as a guard - if ((ent.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_HEALER || - ent.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_GUARD) && !this.guardEnts.get(evt.entity)) - { - this.assignGuardToCriticalEnt(gameState, ent, ent.getMetadata(PlayerID, "guardedEnt")); - continue; - } - - if (!this.criticalEnts.has(evt.entity)) - continue; - - // If this is a hero, try to assign ents that should be guarding it, but couldn't previously - const criticalEnt = this.criticalEnts.get(evt.entity); - for (const [id, isGuarding] of this.guardEnts) - { - if (criticalEnt.guards.size >= this.healersPerCriticalEnt) + data.guards.set(evt.newentity, data.guards.get(evt.entity)); + data.guards.delete(evt.entity); break; + } + this.guardEnts.set(evt.newentity, this.guardEnts.get(evt.entity)); + this.guardEnts.delete(evt.entity); + } - if (!isGuarding) + // Check if new healers/guards need to be assigned to an ent + for (const evt of events.Destroy) + { + if (this.criticalEnts.has(evt.entity)) { - const guardEnt = gameState.getEntityById(id); - if (guardEnt) - this.assignGuardToCriticalEnt(gameState, guardEnt, evt.entity); + this.removeCriticalEnt(gameState, evt.entity); + continue; + } + + if (!this.guardEnts.delete(evt.entity)) + continue; + + for (const data of this.criticalEnts.values()) + { + if (data.guards.delete(evt.entity)) + { + data.healersAssigned?.delete(evt.entity); + data.guardsAssigned.delete(evt.entity); + } + } + } + + for (const evt of events.UnGarrison) + { + if (!this.guardEnts.has(evt.entity) && !this.criticalEnts.has(evt.entity)) + continue; + + const ent = gameState.getEntityById(evt.entity); + if (!ent) + continue; + + // If this ent travelled to a criticalEnt's accessValue, try again to assign as a guard + if ((ent.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_HEALER || + ent.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_GUARD) && !this.guardEnts.get(evt.entity)) + { + this.assignGuardToCriticalEnt(gameState, ent, ent.getMetadata(PlayerID, "guardedEnt")); + continue; + } + + if (!this.criticalEnts.has(evt.entity)) + continue; + + // If this is a hero, try to assign ents that should be guarding it, but couldn't previously + const criticalEnt = this.criticalEnts.get(evt.entity); + for (const [id, isGuarding] of this.guardEnts) + { + if (criticalEnt.guards.size >= this.healersPerCriticalEnt) + break; + + if (!isGuarding) + { + const guardEnt = gameState.getEntityById(id); + if (guardEnt) + this.assignGuardToCriticalEnt(gameState, guardEnt, evt.entity); + } + } + } + + for (const evt of events.OwnershipChanged) + { + if (evt.from == PlayerID && this.criticalEnts.has(evt.entity)) + { + this.removeCriticalEnt(gameState, evt.entity); + continue; + } + if (evt.from == 0 && this.targetedGaiaRelics.has(evt.entity)) + this.abortCaptureGaiaRelic(gameState, evt.entity); + + if (evt.to != PlayerID) + continue; + + const ent = gameState.getEntityById(evt.entity); + if (ent && (gameState.getVictoryConditions().has("wonder") && ent.hasClass("Wonder") || + gameState.getVictoryConditions().has("capture_the_relic") && ent.hasClass("Relic"))) + { + this.criticalEnts.set(ent.id(), { "guardsAssigned": new Set(), "guards": new Map() }); + // Move captured relics to the closest base + if (ent.hasClass("Relic")) + this.pickCriticalEntRetreatLocation(gameState, ent, false); } } } - for (const evt of events.OwnershipChanged) + removeCriticalEnt(gameState, criticalEntId) { - if (evt.from == PlayerID && this.criticalEnts.has(evt.entity)) + for (const [guardId, role] of this.criticalEnts.get(criticalEntId).guards) { - this.removeCriticalEnt(gameState, evt.entity); - continue; - } - if (evt.from == 0 && this.targetedGaiaRelics.has(evt.entity)) - this.abortCaptureGaiaRelic(gameState, evt.entity); + const guardEnt = gameState.getEntityById(guardId); + if (!guardEnt) + continue; - if (evt.to != PlayerID) - continue; - - const ent = gameState.getEntityById(evt.entity); - if (ent && (gameState.getVictoryConditions().has("wonder") && ent.hasClass("Wonder") || - gameState.getVictoryConditions().has("capture_the_relic") && ent.hasClass("Relic"))) - { - this.criticalEnts.set(ent.id(), { "guardsAssigned": new Set(), "guards": new Map() }); - // Move captured relics to the closest base - if (ent.hasClass("Relic")) - this.pickCriticalEntRetreatLocation(gameState, ent, false); - } - } -}; - -VictoryManager.prototype.removeCriticalEnt = function(gameState, criticalEntId) -{ - for (const [guardId, role] of this.criticalEnts.get(criticalEntId).guards) - { - const guardEnt = gameState.getEntityById(guardId); - if (!guardEnt) - continue; - - if (role == "healer") - this.guardEnts.set(guardId, false); - else - { - guardEnt.setMetadata(PlayerID, "plan", -1); - guardEnt.setMetadata(PlayerID, "role", undefined); - this.guardEnts.delete(guardId); - } - - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - } - this.criticalEnts.delete(criticalEntId); -}; - -/** - * Train more healers to be later affected to critical entities if needed - */ -VictoryManager.prototype.manageCriticalEntHealers = function(gameState, queues) -{ - if (gameState.ai.HQ.saveResources || queues.healer.hasQueuedUnits() || - !gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || - this.guardEnts.size > Math.min(gameState.getPopulationMax() / 10, gameState.getPopulation() / 4)) - return; - - for (const data of this.criticalEnts.values()) - { - if (data.healersAssigned === undefined) - continue; - if (data.healersAssigned.size >= this.healersPerCriticalEnt) - continue; - - const template = gameState.applyCiv("units/{civ}/support_healer_b"); - queues.healer.addPlan(new TrainingPlan(gameState, template, - { "role": Worker.ROLE_CRITICAL_ENT_HEALER, "base": 0 }, 1, 1)); - return; - } -}; - -/** - * Try to keep some military units guarding any criticalEnts, if we can afford it. - * If we have too low a population and require units for other needs, remove guards so they can be reassigned. - * TODO: Swap citizen soldier guards with champions if they become available. - */ -VictoryManager.prototype.manageCriticalEntGuards = function(gameState) -{ - let numWorkers = gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length; - if (numWorkers < 20) - { - for (const data of this.criticalEnts.values()) - { - for (const guardId of data.guards.keys()) + if (role == "healer") + this.guardEnts.set(guardId, false); + else { - const guardEnt = gameState.getEntityById(guardId); - if (!guardEnt || !guardEnt.hasClass("CitizenSoldier") || - guardEnt.getMetadata(PlayerID, "role") !== Worker.ROLE_CRITICAL_ENT_GUARD) - continue; - - guardEnt.removeGuard(); guardEnt.setMetadata(PlayerID, "plan", -1); guardEnt.setMetadata(PlayerID, "role", undefined); this.guardEnts.delete(guardId); - data.guardsAssigned.add(guardId); - - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - - if (++numWorkers >= 20) - break; } - if (numWorkers >= 20) - break; + + if (guardEnt.getMetadata(PlayerID, "guardedEnt")) + guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); } + this.criticalEnts.delete(criticalEntId); } - const minWorkers = 25; - const deltaWorkers = 3; - for (const [id, data] of this.criticalEnts) + /** + * Train more healers to be later affected to critical entities if needed + */ + manageCriticalEntHealers(gameState, queues) { - const criticalEnt = gameState.getEntityById(id); - if (!criticalEnt) - continue; + if (gameState.ai.HQ.saveResources || queues.healer.hasQueuedUnits() || + !gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || + this.guardEnts.size > Math.min(gameState.getPopulationMax() / 10, gameState.getPopulation() / 4)) + return; - const militaryGuardsPerCriticalEnt = (criticalEnt.hasClass("Wonder") ? 10 : 4) + - Math.round(this.Config.personality.defensive * 5); - - if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt) - continue; - - // First try to pick guards in the criticalEnt's accessIndex, to avoid unnecessary transports - for (const checkForSameAccess of [true, false]) + for (const data of this.criticalEnts.values()) { - // First try to assign any Champion units we might have - for (const entity of gameState.getOwnEntitiesByClass("Champion", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - data.guardsAssigned.add(entity.id()); - if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt) - break; - } + if (data.healersAssigned === undefined) + continue; + if (data.healersAssigned.size >= this.healersPerCriticalEnt) + continue; - if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || - numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) - { - break; - } - - for (const entity of gameState.ai.HQ.attackManager.outOfPlan.values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - data.guardsAssigned.add(entity.id()); - if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || - numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) - { - break; - } - } - - if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || - numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) - { - break; - } - - for (const entity of gameState.getOwnEntitiesByClass("Soldier", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - data.guardsAssigned.add(entity.id()); - if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || - numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) - { - break; - } - } - - if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || - numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) - { - break; - } + const template = gameState.applyCiv("units/{civ}/support_healer_b"); + queues.healer.addPlan(new TrainingPlan(gameState, template, + { "role": Worker.ROLE_CRITICAL_ENT_HEALER, "base": 0 }, 1, 1)); + return; } } -}; -VictoryManager.prototype.tryAssignMilitaryGuard = function(gameState, guardEnt, criticalEnt, checkForSameAccess) -{ - if (guardEnt.getMetadata(PlayerID, "plan") !== undefined || - guardEnt.getMetadata(PlayerID, "transport") !== undefined || this.criticalEnts.has(guardEnt.id()) || - checkForSameAccess && (!guardEnt.position() || !criticalEnt.position() || - getLandAccess(gameState, criticalEnt) != getLandAccess(gameState, guardEnt))) - return false; - - if (!this.assignGuardToCriticalEnt(gameState, guardEnt, criticalEnt.id())) - return false; - - guardEnt.setMetadata(PlayerID, "plan", -2); - guardEnt.setMetadata(PlayerID, "role", Worker.ROLE_CRITICAL_ENT_GUARD); - return true; -}; - -VictoryManager.prototype.pickCriticalEntRetreatLocation = function(gameState, criticalEnt, emergency) -{ - gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, criticalEnt, emergency); - const plan = criticalEnt.getMetadata(PlayerID, "plan"); - - if (plan == -2 || plan == -3) - return; - - if (this.criticalEnts.get(criticalEnt.id()).garrisonEmergency) - this.criticalEnts.get(criticalEnt.id()).garrisonEmergency = false; - - // Couldn't find a place to garrison, so the ent will flee from attacks - if (!criticalEnt.hasClass("Relic") && criticalEnt.getStance() != "passive") - criticalEnt.setStance("passive"); - const accessIndex = getLandAccess(gameState, criticalEnt); - const bestBase = getBestBase(gameState, criticalEnt, true); - if (bestBase.accessIndex == accessIndex) + /** + * Try to keep some military units guarding any criticalEnts, if we can afford it. + * If we have too low a population and require units for other needs, remove guards so they can be reassigned. + * TODO: Swap citizen soldier guards with champions if they become available. + */ + manageCriticalEntGuards(gameState) { - const bestBasePos = bestBase.anchor.position(); - criticalEnt.moveToRange(bestBasePos[0], bestBasePos[1], - 0, bestBase.anchor.obstructionRadius().max); - } -}; + let numWorkers = gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length; + if (numWorkers < 20) + { + for (const data of this.criticalEnts.values()) + { + for (const guardId of data.guards.keys()) + { + const guardEnt = gameState.getEntityById(guardId); + if (!guardEnt || !guardEnt.hasClass("CitizenSoldier") || + guardEnt.getMetadata(PlayerID, "role") !== Worker.ROLE_CRITICAL_ENT_GUARD) + continue; -/** - * Only send the guard command if the guard's accessIndex is the same as the critical ent - * and the critical ent has a position (i.e. not garrisoned). - * Request a transport if the accessIndex value is different, and if a transport is needed, - * the guardEnt will be given metadata describing which entity it is being sent to guard, - * which will be used once its transport has finished. - * Return false if the guardEnt is not a valid guard unit (i.e. cannot guard or is being transported). - */ -VictoryManager.prototype.assignGuardToCriticalEnt = function(gameState, guardEnt, criticalEntId) -{ - if (guardEnt.getMetadata(PlayerID, "transport") !== undefined || !guardEnt.canGuard()) - return false; + guardEnt.removeGuard(); + guardEnt.setMetadata(PlayerID, "plan", -1); + guardEnt.setMetadata(PlayerID, "role", undefined); + this.guardEnts.delete(guardId); + data.guardsAssigned.add(guardId); - if (criticalEntId && !this.criticalEnts.has(criticalEntId)) - { - criticalEntId = undefined; - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - } + if (guardEnt.getMetadata(PlayerID, "guardedEnt")) + guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - if (!criticalEntId) - { - const isHealer = guardEnt.hasClass("Healer"); + if (++numWorkers >= 20) + break; + } + if (numWorkers >= 20) + break; + } + } - // Assign to the critical ent with the fewest guards - let min = Math.min(); + const minWorkers = 25; + const deltaWorkers = 3; for (const [id, data] of this.criticalEnts) { - if (isHealer && (data.healersAssigned === undefined || data.healersAssigned.size > min)) - continue; - if (!isHealer && data.guardsAssigned.size > min) + const criticalEnt = gameState.getEntityById(id); + if (!criticalEnt) continue; - criticalEntId = id; - min = data[isHealer ? "healersAssigned" : "guardsAssigned"].size; - } - if (criticalEntId) - { - const data = this.criticalEnts.get(criticalEntId); - data[isHealer ? "healersAssigned" : "guardsAssigned"].add(guardEnt.id()); - } - } + const militaryGuardsPerCriticalEnt = (criticalEnt.hasClass("Wonder") ? 10 : 4) + + Math.round(this.Config.personality.defensive * 5); - if (!criticalEntId) - { - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - return false; - } - - const criticalEnt = gameState.getEntityById(criticalEntId); - if (!criticalEnt || !criticalEnt.position() || !guardEnt.position()) - { - this.guardEnts.set(guardEnt.id(), false); - return false; - } - - if (guardEnt.getMetadata(PlayerID, "guardedEnt") != criticalEntId) - guardEnt.setMetadata(PlayerID, "guardedEnt", criticalEntId); - - const guardEntAccess = getLandAccess(gameState, guardEnt); - const criticalEntAccess = getLandAccess(gameState, criticalEnt); - if (guardEntAccess == criticalEntAccess) - { - const queued = returnResources(gameState, guardEnt); - guardEnt.guard(criticalEnt, queued); - const guardRole = guardEnt.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_HEALER ? "healer" : "guard"; - this.criticalEnts.get(criticalEntId).guards.set(guardEnt.id(), guardRole); - - // Switch this guard ent to the criticalEnt's base - if (criticalEnt.hasClass("Structure") && criticalEnt.getMetadata(PlayerID, "base") !== undefined) - guardEnt.setMetadata(PlayerID, "base", criticalEnt.getMetadata(PlayerID, "base")); - } - else - gameState.ai.HQ.navalManager.requireTransport(gameState, guardEnt, guardEntAccess, criticalEntAccess, criticalEnt.position()); - - this.guardEnts.set(guardEnt.id(), guardEntAccess == criticalEntAccess); - return true; -}; - -VictoryManager.prototype.resetCaptureGaiaRelic = function(gameState) -{ - // Do not capture gaia relics too frequently as the ai has access to the entire map - this.tryCaptureGaiaRelicLapseTime = gameState.ai.elapsedTime + 240 - 30 * (this.Config.difficulty - 3); - this.tryCaptureGaiaRelic = false; -}; - -VictoryManager.prototype.update = function(gameState, events, queues) -{ - // Wait a turn for trigger scripts to spawn any critical ents (i.e. in regicide) - if (gameState.ai.playedTurn == 1) - this.init(gameState); - - this.checkEvents(gameState, events); - - if (gameState.ai.playedTurn % 10 != 0 || - !gameState.getVictoryConditions().has("wonder") && !gameState.getVictoryConditions().has("regicide") && - !gameState.getVictoryConditions().has("capture_the_relic")) - return; - - this.manageCriticalEntGuards(gameState); - - if (gameState.getVictoryConditions().has("wonder")) - gameState.ai.HQ.buildWonder(gameState, queues, true); - - if (gameState.getVictoryConditions().has("regicide")) - { - for (const id of this.criticalEnts.keys()) - { - const ent = gameState.getEntityById(id); - if (ent && ent.healthLevel() > this.Config.garrisonHealthLevel.high && ent.hasClass("Soldier") && - ent.getStance() != "aggressive") - ent.setStance("aggressive"); - } - this.manageCriticalEntHealers(gameState, queues); - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - if (!this.tryCaptureGaiaRelic && gameState.ai.elapsedTime > this.tryCaptureGaiaRelicLapseTime) - this.tryCaptureGaiaRelic = true; - - // Reinforce (if needed) any raid currently trying to capture a gaia relic - for (const relicId of this.targetedGaiaRelics.keys()) - { - const relic = gameState.getEntityById(relicId); - if (!relic || relic.owner() != 0) - this.abortCaptureGaiaRelic(gameState, relicId); - else - this.captureGaiaRelic(gameState, relic); - } - // And look for some new gaia relics visible by any of our units - // or that may be on our territory - const allGaiaRelics = gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic")) - .filter(relic => relic.owner() == 0); - for (const relic of allGaiaRelics.values()) - { - const relicPosition = relic.position(); - if (!relicPosition || this.targetedGaiaRelics.has(relic.id())) + if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt) continue; - const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(relicPosition); - if (territoryOwner == PlayerID) + + // First try to pick guards in the criticalEnt's accessIndex, to avoid unnecessary transports + for (const checkForSameAccess of [true, false]) { - this.targetedGaiaRelics.set(relic.id(), []); - this.captureGaiaRelic(gameState, relic); - break; - } + // First try to assign any Champion units we might have + for (const entity of gameState.getOwnEntitiesByClass("Champion", true).values()) + { + if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) + continue; + data.guardsAssigned.add(entity.id()); + if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt) + break; + } - if (territoryOwner != 0 && gameState.isPlayerEnemy(territoryOwner)) - continue; + if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || + numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) + { + break; + } - for (const ent of gameState.getOwnUnits().values()) - { - if (!ent.position() || !ent.visionRange()) - continue; - if (SquareVectorDistance(ent.position(), relicPosition) > Math.square(ent.visionRange())) - continue; - this.targetedGaiaRelics.set(relic.id(), []); - this.captureGaiaRelic(gameState, relic); - break; + for (const entity of gameState.ai.HQ.attackManager.outOfPlan.values()) + { + if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) + continue; + --numWorkers; + data.guardsAssigned.add(entity.id()); + if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || + numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) + { + break; + } + } + + if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || + numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) + { + break; + } + + for (const entity of gameState.getOwnEntitiesByClass("Soldier", true).values()) + { + if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) + continue; + --numWorkers; + data.guardsAssigned.add(entity.id()); + if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || + numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) + { + break; + } + } + + if (data.guardsAssigned.size >= militaryGuardsPerCriticalEnt || + numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned.size) + { + break; + } } } } -}; -/** - * Send an expedition to capture a gaia relic, or reinforce an existing one. - */ -VictoryManager.prototype.captureGaiaRelic = function(gameState, relic) -{ - let capture = -relic.defaultRegenRate(); - const sumCapturePoints = relic.capturePoints().reduce((a, b) => a + b); - const plans = this.targetedGaiaRelics.get(relic.id()); - for (const plan of plans) + tryAssignMilitaryGuard(gameState, guardEnt, criticalEnt, checkForSameAccess) { - const attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (!attack) - continue; - for (const ent of attack.unitCollection.values()) - capture += ent.captureStrength() * getAttackBonus(ent, relic, "Capture"); + if (guardEnt.getMetadata(PlayerID, "plan") !== undefined || + guardEnt.getMetadata(PlayerID, "transport") !== undefined || this.criticalEnts.has(guardEnt.id()) || + checkForSameAccess && (!guardEnt.position() || !criticalEnt.position() || + getLandAccess(gameState, criticalEnt) != getLandAccess(gameState, guardEnt))) + return false; + + if (!this.assignGuardToCriticalEnt(gameState, guardEnt, criticalEnt.id())) + return false; + + guardEnt.setMetadata(PlayerID, "plan", -2); + guardEnt.setMetadata(PlayerID, "role", Worker.ROLE_CRITICAL_ENT_GUARD); + return true; } - // No need to make a new attack if already enough units - if (capture > sumCapturePoints / 50) - return; - const relicPosition = relic.position(); - const access = getLandAccess(gameState, relic); - const units = gameState.getOwnUnits().filter(ent => + + pickCriticalEntRetreatLocation(gameState, criticalEnt, emergency) { - if (!ent.position() || !ent.canCapture(relic)) - return false; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return false; - if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) - return false; - const plan = ent.getMetadata(PlayerID, "plan"); + gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, criticalEnt, emergency); + const plan = criticalEnt.getMetadata(PlayerID, "plan"); + if (plan == -2 || plan == -3) + return; + + if (this.criticalEnts.get(criticalEnt.id()).garrisonEmergency) + this.criticalEnts.get(criticalEnt.id()).garrisonEmergency = false; + + // Couldn't find a place to garrison, so the ent will flee from attacks + if (!criticalEnt.hasClass("Relic") && criticalEnt.getStance() != "passive") + criticalEnt.setStance("passive"); + const accessIndex = getLandAccess(gameState, criticalEnt); + const bestBase = getBestBase(gameState, criticalEnt, true); + if (bestBase.accessIndex == accessIndex) + { + const bestBasePos = bestBase.anchor.position(); + criticalEnt.moveToRange(bestBasePos[0], bestBasePos[1], + 0, bestBase.anchor.obstructionRadius().max); + } + } + + /** + * Only send the guard command if the guard's accessIndex is the same as the critical ent + * and the critical ent has a position (i.e. not garrisoned). + * Request a transport if the accessIndex value is different, and if a transport is needed, + * the guardEnt will be given metadata describing which entity it is being sent to guard, + * which will be used once its transport has finished. + * Return false if the guardEnt is not a valid guard unit (i.e. cannot guard or is being transported). + */ + assignGuardToCriticalEnt(gameState, guardEnt, criticalEntId) + { + if (guardEnt.getMetadata(PlayerID, "transport") !== undefined || !guardEnt.canGuard()) return false; - if (plan !== undefined && plan >= 0) + + if (criticalEntId && !this.criticalEnts.has(criticalEntId)) + { + criticalEntId = undefined; + if (guardEnt.getMetadata(PlayerID, "guardedEnt")) + guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); + } + + if (!criticalEntId) + { + const isHealer = guardEnt.hasClass("Healer"); + + // Assign to the critical ent with the fewest guards + let min = Math.min(); + for (const [id, data] of this.criticalEnts) + { + if (isHealer && (data.healersAssigned === undefined || data.healersAssigned.size > min)) + continue; + if (!isHealer && data.guardsAssigned.size > min) + continue; + + criticalEntId = id; + min = data[isHealer ? "healersAssigned" : "guardsAssigned"].size; + } + if (criticalEntId) + { + const data = this.criticalEnts.get(criticalEntId); + data[isHealer ? "healersAssigned" : "guardsAssigned"].add(guardEnt.id()); + } + } + + if (!criticalEntId) + { + if (guardEnt.getMetadata(PlayerID, "guardedEnt")) + guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); + return false; + } + + const criticalEnt = gameState.getEntityById(criticalEntId); + if (!criticalEnt || !criticalEnt.position() || !guardEnt.position()) + { + this.guardEnts.set(guardEnt.id(), false); + return false; + } + + if (guardEnt.getMetadata(PlayerID, "guardedEnt") != criticalEntId) + guardEnt.setMetadata(PlayerID, "guardedEnt", criticalEntId); + + const guardEntAccess = getLandAccess(gameState, guardEnt); + const criticalEntAccess = getLandAccess(gameState, criticalEnt); + if (guardEntAccess == criticalEntAccess) + { + const queued = returnResources(gameState, guardEnt); + guardEnt.guard(criticalEnt, queued); + const guardRole = guardEnt.getMetadata(PlayerID, "role") === Worker.ROLE_CRITICAL_ENT_HEALER ? "healer" : "guard"; + this.criticalEnts.get(criticalEntId).guards.set(guardEnt.id(), guardRole); + + // Switch this guard ent to the criticalEnt's base + if (criticalEnt.hasClass("Structure") && criticalEnt.getMetadata(PlayerID, "base") !== undefined) + guardEnt.setMetadata(PlayerID, "base", criticalEnt.getMetadata(PlayerID, "base")); + } + else + gameState.ai.HQ.navalManager.requireTransport(gameState, guardEnt, guardEntAccess, criticalEntAccess, criticalEnt.position()); + + this.guardEnts.set(guardEnt.id(), guardEntAccess == criticalEntAccess); + return true; + } + + resetCaptureGaiaRelic(gameState) + { + // Do not capture gaia relics too frequently as the ai has access to the entire map + this.tryCaptureGaiaRelicLapseTime = gameState.ai.elapsedTime + 240 - 30 * (this.Config.difficulty - 3); + this.tryCaptureGaiaRelic = false; + } + + update(gameState, events, queues) + { + // Wait a turn for trigger scripts to spawn any critical ents (i.e. in regicide) + if (gameState.ai.playedTurn == 1) + this.init(gameState); + + this.checkEvents(gameState, events); + + if (gameState.ai.playedTurn % 10 != 0 || + !gameState.getVictoryConditions().has("wonder") && !gameState.getVictoryConditions().has("regicide") && + !gameState.getVictoryConditions().has("capture_the_relic")) + return; + + this.manageCriticalEntGuards(gameState); + + if (gameState.getVictoryConditions().has("wonder")) + gameState.ai.HQ.buildWonder(gameState, queues, true); + + if (gameState.getVictoryConditions().has("regicide")) + { + for (const id of this.criticalEnts.keys()) + { + const ent = gameState.getEntityById(id); + if (ent && ent.healthLevel() > this.Config.garrisonHealthLevel.high && ent.hasClass("Soldier") && + ent.getStance() != "aggressive") + ent.setStance("aggressive"); + } + this.manageCriticalEntHealers(gameState, queues); + } + + if (gameState.getVictoryConditions().has("capture_the_relic")) + { + if (!this.tryCaptureGaiaRelic && gameState.ai.elapsedTime > this.tryCaptureGaiaRelicLapseTime) + this.tryCaptureGaiaRelic = true; + + // Reinforce (if needed) any raid currently trying to capture a gaia relic + for (const relicId of this.targetedGaiaRelics.keys()) + { + const relic = gameState.getEntityById(relicId); + if (!relic || relic.owner() != 0) + this.abortCaptureGaiaRelic(gameState, relicId); + else + this.captureGaiaRelic(gameState, relic); + } + // And look for some new gaia relics visible by any of our units + // or that may be on our territory + const allGaiaRelics = gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic")) + .filter(relic => relic.owner() == 0); + for (const relic of allGaiaRelics.values()) + { + const relicPosition = relic.position(); + if (!relicPosition || this.targetedGaiaRelics.has(relic.id())) + continue; + const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(relicPosition); + if (territoryOwner == PlayerID) + { + this.targetedGaiaRelics.set(relic.id(), []); + this.captureGaiaRelic(gameState, relic); + break; + } + + if (territoryOwner != 0 && gameState.isPlayerEnemy(territoryOwner)) + continue; + + for (const ent of gameState.getOwnUnits().values()) + { + if (!ent.position() || !ent.visionRange()) + continue; + if (SquareVectorDistance(ent.position(), relicPosition) > Math.square(ent.visionRange())) + continue; + this.targetedGaiaRelics.set(relic.id(), []); + this.captureGaiaRelic(gameState, relic); + break; + } + } + } + } + + /** + * Send an expedition to capture a gaia relic, or reinforce an existing one. + */ + captureGaiaRelic(gameState, relic) + { + let capture = -relic.defaultRegenRate(); + const sumCapturePoints = relic.capturePoints().reduce((a, b) => a + b); + const plans = this.targetedGaiaRelics.get(relic.id()); + for (const plan of plans) { const attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && (attack.state !== AttackPlan.STATE_UNEXECUTED || - attack.type === AttackPlan.TYPE_RAID)) - { - return false; - } + if (!attack) + continue; + for (const ent of attack.unitCollection.values()) + capture += ent.captureStrength() * getAttackBonus(ent, relic, "Capture"); } - if (getLandAccess(gameState, ent) != access) - return false; - return true; - }).filterNearest(relicPosition); - const expedition = []; - for (const ent of units.values()) - { - capture += ent.captureStrength() * getAttackBonus(ent, relic, "Capture"); - expedition.push(ent); - if (capture > sumCapturePoints / 25) - break; + // No need to make a new attack if already enough units + if (capture > sumCapturePoints / 50) + return; + const relicPosition = relic.position(); + const access = getLandAccess(gameState, relic); + const units = gameState.getOwnUnits().filter(ent => + { + if (!ent.position() || !ent.canCapture(relic)) + return false; + if (ent.getMetadata(PlayerID, "transport") !== undefined) + return false; + if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) + return false; + const plan = ent.getMetadata(PlayerID, "plan"); + if (plan == -2 || plan == -3) + return false; + if (plan !== undefined && plan >= 0) + { + const attack = gameState.ai.HQ.attackManager.getPlan(plan); + if (attack && (attack.state !== AttackPlan.STATE_UNEXECUTED || + attack.type === AttackPlan.TYPE_RAID)) + { + return false; + } + } + if (getLandAccess(gameState, ent) != access) + return false; + return true; + }).filterNearest(relicPosition); + const expedition = []; + for (const ent of units.values()) + { + capture += ent.captureStrength() * getAttackBonus(ent, relic, "Capture"); + expedition.push(ent); + if (capture > sumCapturePoints / 25) + break; + } + if (!expedition.length || !plans.length && capture < sumCapturePoints / 100) + return; + const attack = gameState.ai.HQ.attackManager.raidTargetEntity(gameState, relic); + if (!attack) + return; + const plan = attack.name; + attack.rallyPoint = undefined; + for (const ent of expedition) + { + ent.setMetadata(PlayerID, "plan", plan); + attack.unitCollection.updateEnt(ent); + if (!attack.rallyPoint) + attack.rallyPoint = ent.position(); + } + attack.forceStart(); + this.targetedGaiaRelics.get(relic.id()).push(plan); } - if (!expedition.length || !plans.length && capture < sumCapturePoints / 100) - return; - const attack = gameState.ai.HQ.attackManager.raidTargetEntity(gameState, relic); - if (!attack) - return; - const plan = attack.name; - attack.rallyPoint = undefined; - for (const ent of expedition) + + abortCaptureGaiaRelic(gameState, relicId) { - ent.setMetadata(PlayerID, "plan", plan); - attack.unitCollection.updateEnt(ent); - if (!attack.rallyPoint) - attack.rallyPoint = ent.position(); + for (const plan of this.targetedGaiaRelics.get(relicId)) + { + const attack = gameState.ai.HQ.attackManager.getPlan(plan); + if (attack) + attack.Abort(gameState); + } + this.targetedGaiaRelics.delete(relicId); } - attack.forceStart(); - this.targetedGaiaRelics.get(relic.id()).push(plan); -}; -VictoryManager.prototype.abortCaptureGaiaRelic = function(gameState, relicId) -{ - for (const plan of this.targetedGaiaRelics.get(relicId)) + Serialize() { - const attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack) - attack.Abort(gameState); + return { + "criticalEnts": this.criticalEnts, + "guardEnts": this.guardEnts, + "healersPerCriticalEnt": this.healersPerCriticalEnt, + "tryCaptureGaiaRelic": this.tryCaptureGaiaRelic, + "tryCaptureGaiaRelicLapseTime": this.tryCaptureGaiaRelicLapseTime, + "targetedGaiaRelics": this.targetedGaiaRelics + }; } - this.targetedGaiaRelics.delete(relicId); -}; -VictoryManager.prototype.Serialize = function() -{ - return { - "criticalEnts": this.criticalEnts, - "guardEnts": this.guardEnts, - "healersPerCriticalEnt": this.healersPerCriticalEnt, - "tryCaptureGaiaRelic": this.tryCaptureGaiaRelic, - "tryCaptureGaiaRelicLapseTime": this.tryCaptureGaiaRelicLapseTime, - "targetedGaiaRelics": this.targetedGaiaRelics - }; -}; - -VictoryManager.prototype.Deserialize = function(data) -{ - for (const key in data) - this[key] = data[key]; -}; + Deserialize(data) + { + for (const key in data) + this[key] = data[key]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/worker.js b/binaries/data/mods/public/simulation/ai/petra/worker.js index 596e323295..b38c3b42e8 100644 --- a/binaries/data/mods/public/simulation/ai/petra/worker.js +++ b/binaries/data/mods/public/simulation/ai/petra/worker.js @@ -7,336 +7,362 @@ import { TransportPlan } from "simulation/ai/petra/transportPlan.js"; /** * This class makes a worker do as instructed by the economy manager */ -export function Worker(base) +export class Worker { - this.ent = undefined; - this.base = base; - this.baseID = base.ID; -} - -Worker.ROLE_ATTACK = "attack"; -Worker.ROLE_TRADER = "trader"; -Worker.ROLE_SWITCH_TO_TRADER = "switchToTrader"; -Worker.ROLE_WORKER = "worker"; -Worker.ROLE_CRITICAL_ENT_GUARD = "criticalEntGuard"; -Worker.ROLE_CRITICAL_ENT_HEALER = "criticalEntHealer"; - -Worker.SUBROLE_DEFENDER = "defender"; -Worker.SUBROLE_IDLE = "idle"; -Worker.SUBROLE_BUILDER = "builder"; -Worker.SUBROLE_COMPLETING = "completing"; -Worker.SUBROLE_WALKING = "walking"; -Worker.SUBROLE_ATTACKING = "attacking"; -Worker.SUBROLE_GATHERER = "gatherer"; -Worker.SUBROLE_HUNTER = "hunter"; -Worker.SUBROLE_FISHER = "fisher"; -Worker.SUBROLE_GARRISONING = "garrisoning"; - -Worker.prototype.update = function(gameState, ent) -{ - if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return; - - const subrole = ent.getMetadata(PlayerID, "subrole"); - - // If we are waiting for a transport or we are sailing, just wait - if (ent.getMetadata(PlayerID, "transport") !== undefined) + constructor(base) { - // Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board - if (subrole === Worker.SUBROLE_BUILDER && ent.getMetadata(PlayerID, "target-foundation") !== undefined) + this.ent = undefined; + this.base = base; + this.baseID = base.ID; + } + + static ROLE_ATTACK = "attack"; + static ROLE_TRADER = "trader"; + static ROLE_SWITCH_TO_TRADER = "switchToTrader"; + static ROLE_WORKER = "worker"; + static ROLE_CRITICAL_ENT_GUARD = "criticalEntGuard"; + static ROLE_CRITICAL_ENT_HEALER = "criticalEntHealer"; + + static SUBROLE_DEFENDER = "defender"; + static SUBROLE_IDLE = "idle"; + static SUBROLE_BUILDER = "builder"; + static SUBROLE_COMPLETING = "completing"; + static SUBROLE_WALKING = "walking"; + static SUBROLE_ATTACKING = "attacking"; + static SUBROLE_GATHERER = "gatherer"; + static SUBROLE_HUNTER = "hunter"; + static SUBROLE_FISHER = "fisher"; + static SUBROLE_GARRISONING = "garrisoning"; + + update(gameState, ent) + { + if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) + return; + + const subrole = ent.getMetadata(PlayerID, "subrole"); + + // If we are waiting for a transport or we are sailing, just wait + if (ent.getMetadata(PlayerID, "transport") !== undefined) { - const plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - const target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target && plan && plan.state === TransportPlan.BOARDING && ent.position()) - plan.removeUnit(gameState, ent); - } - // and gatherer if there are no more dropsite accessible in the base the ent is going to - if (subrole === Worker.SUBROLE_GATHERER || subrole === Worker.SUBROLE_HUNTER) - { - const plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan.state === TransportPlan.BOARDING && ent.position()) + // Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board + if (subrole === Worker.SUBROLE_BUILDER && ent.getMetadata(PlayerID, "target-foundation") !== undefined) { - let hasDropsite = false; - const gatherType = ent.getMetadata(PlayerID, "gather-type") || "food"; - for (const structure of gameState.getOwnStructures().values()) + const plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); + const target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); + if (!target && plan && plan.state === TransportPlan.BOARDING && ent.position()) + plan.removeUnit(gameState, ent); + } + // and gatherer if there are no more dropsite accessible in the base the ent is going to + if (subrole === Worker.SUBROLE_GATHERER || subrole === Worker.SUBROLE_HUNTER) + { + const plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); + if (plan.state === TransportPlan.BOARDING && ent.position()) { - if (getLandAccess(gameState, structure) != plan.endIndex) - continue; - const resourceDropsiteTypes = getBuiltEntity(gameState, structure).resourceDropsiteTypes(); - if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) - continue; - hasDropsite = true; - break; - } - if (!hasDropsite) - { - for (const unit of gameState.getOwnUnits().filter(filters.byClass("Support")).values()) + let hasDropsite = false; + const gatherType = ent.getMetadata(PlayerID, "gather-type") || "food"; + for (const structure of gameState.getOwnStructures().values()) { - if (!unit.position() || getLandAccess(gameState, unit) != plan.endIndex) + if (getLandAccess(gameState, structure) != plan.endIndex) continue; - const resourceDropsiteTypes = unit.resourceDropsiteTypes(); + const resourceDropsiteTypes = getBuiltEntity(gameState, structure).resourceDropsiteTypes(); if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) continue; hasDropsite = true; break; } - } - if (!hasDropsite) - plan.removeUnit(gameState, ent); - } - } - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return; - } - - this.entAccess = getLandAccess(gameState, ent); - // Base for unassigned entities has no accessIndex, so take the one from the entity. - if (this.baseID == gameState.ai.HQ.basesManager.baselessBase().ID) - this.baseAccess = this.entAccess; - else - this.baseAccess = this.base.accessIndex; - - if (subrole == undefined) // subrole may-be undefined after a transport, garrisoning, army, ... - { - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - - this.ent = ent; - - const unitAIState = ent.unitAIState(); - if ((subrole === Worker.SUBROLE_HUNTER || subrole === Worker.SUBROLE_GATHERER) && - (unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.COMBAT.APPROACHING")) - { - if (this.isInaccessibleSupply(gameState)) - { - if (this.retryWorking(gameState, subrole)) - return; - ent.stopMoving(); - } - - if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) - { - const orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) - { - // Check that we have not drifted too far when hunting - const target = gameState.getEntityById(orderData.target); - if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food") - { - const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(target.position()); - if (gameState.isPlayerEnemy(territoryOwner)) + if (!hasDropsite) { - if (this.retryWorking(gameState, subrole)) - return; - ent.stopMoving(); - } - else if (!gameState.isPlayerAlly(territoryOwner)) - { - const distanceSquare = isFastMoving(ent) ? 90000 : 30000; - const targetAccess = getLandAccess(gameState, target); - const foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - let hasFoodDropsiteWithinDistance = false; - for (const dropsite of foodDropsites.values()) + for (const unit of gameState.getOwnUnits().filter(filters.byClass("Support")).values()) { - if (!dropsite.position()) + if (!unit.position() || getLandAccess(gameState, unit) != plan.endIndex) continue; - const owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) + const resourceDropsiteTypes = unit.resourceDropsiteTypes(); + if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) continue; - if (targetAccess != getLandAccess(gameState, dropsite)) - continue; - if (SquareVectorDistance(target.position(), dropsite.position()) < distanceSquare) - { - hasFoodDropsiteWithinDistance = true; - break; - } + hasDropsite = true; + break; } - if (!hasFoodDropsiteWithinDistance) + } + if (!hasDropsite) + plan.removeUnit(gameState, ent); + } + } + if (ent.getMetadata(PlayerID, "transport") !== undefined) + return; + } + + this.entAccess = getLandAccess(gameState, ent); + // Base for unassigned entities has no accessIndex, so take the one from the entity. + if (this.baseID == gameState.ai.HQ.basesManager.baselessBase().ID) + this.baseAccess = this.entAccess; + else + this.baseAccess = this.base.accessIndex; + + if (subrole == undefined) // subrole may-be undefined after a transport, garrisoning, army, ... + { + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + this.base.reassignIdleWorkers(gameState, [ent]); + this.update(gameState, ent); + return; + } + + this.ent = ent; + + const unitAIState = ent.unitAIState(); + if ((subrole === Worker.SUBROLE_HUNTER || subrole === Worker.SUBROLE_GATHERER) && + (unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || + unitAIState == "INDIVIDUAL.COMBAT.APPROACHING")) + { + if (this.isInaccessibleSupply(gameState)) + { + if (this.retryWorking(gameState, subrole)) + return; + ent.stopMoving(); + } + + if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) + { + const orderData = ent.unitAIOrderData()[0]; + if (orderData && orderData.target) + { + // Check that we have not drifted too far when hunting + const target = gameState.getEntityById(orderData.target); + if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food") + { + const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(target.position()); + if (gameState.isPlayerEnemy(territoryOwner)) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } + else if (!gameState.isPlayerAlly(territoryOwner)) + { + const distanceSquare = isFastMoving(ent) ? 90000 : 30000; + const targetAccess = getLandAccess(gameState, target); + const foodDropsites = gameState.playerData.hasSharedDropsites ? + gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); + let hasFoodDropsiteWithinDistance = false; + for (const dropsite of foodDropsites.values()) + { + if (!dropsite.position()) + continue; + const owner = dropsite.owner(); + // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again + if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) + continue; + if (targetAccess != getLandAccess(gameState, dropsite)) + continue; + if (SquareVectorDistance(target.position(), dropsite.position()) < distanceSquare) + { + hasFoodDropsiteWithinDistance = true; + break; + } + } + if (!hasFoodDropsiteWithinDistance) + { + if (this.retryWorking(gameState, subrole)) + return; + ent.stopMoving(); + } + } } } } } - } - else if (ent.getMetadata(PlayerID, "approachingTarget")) - { - ent.setMetadata(PlayerID, "approachingTarget", undefined); - ent.setMetadata(PlayerID, "alreadyTried", undefined); - } - - const unitAIStateOrder = unitAIState.split(".")[1]; - // If we're fighting or hunting, let's not start gathering except if inaccessible target - // but for fishers where UnitAI must have made us target a moving whale. - // Also, if we are attacking, do not capture - if (unitAIStateOrder == "COMBAT") - { - if (subrole === Worker.SUBROLE_FISHER) - this.startFishing(gameState); - else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length && - !ent.getMetadata(PlayerID, "PartOfArmy")) + else if (ent.getMetadata(PlayerID, "approachingTarget")) { - const orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) + ent.setMetadata(PlayerID, "approachingTarget", undefined); + ent.setMetadata(PlayerID, "alreadyTried", undefined); + } + + const unitAIStateOrder = unitAIState.split(".")[1]; + // If we're fighting or hunting, let's not start gathering except if inaccessible target + // but for fishers where UnitAI must have made us target a moving whale. + // Also, if we are attacking, do not capture + if (unitAIStateOrder == "COMBAT") + { + if (subrole === Worker.SUBROLE_FISHER) + this.startFishing(gameState); + else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length && + !ent.getMetadata(PlayerID, "PartOfArmy")) { - const target = gameState.getEntityById(orderData.target); - if (target && (!target.position() || getLandAccess(gameState, target) != this.entAccess)) + const orderData = ent.unitAIOrderData()[0]; + if (orderData && orderData.target) { - if (this.retryWorking(gameState, subrole)) - return; - ent.stopMoving(); + const target = gameState.getEntityById(orderData.target); + if (target && (!target.position() || getLandAccess(gameState, target) != this.entAccess)) + { + if (this.retryWorking(gameState, subrole)) + return; + ent.stopMoving(); + } } } - } - else if (unitAIState == "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length && - !ent.getMetadata(PlayerID, "PartOfArmy")) - { - const orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") + else if (unitAIState == "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length && + !ent.getMetadata(PlayerID, "PartOfArmy")) { - // If we are here, an enemy structure must have targeted one of our workers - // and UnitAI sent it fight back with allowCapture=true - const target = gameState.getEntityById(orderData.target); - if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner())) - ent.attack(orderData.target, allowCapture(gameState, ent, target)); - } - } - return; - } - - // Okay so we have a few tasks. - // If we're gathering, we'll check that we haven't run idle. - // And we'll also check that we're gathering a resource we want to gather. - - if (subrole === Worker.SUBROLE_GATHERER) - { - if (ent.isIdle()) - { - // if we aren't storing resources or it's the same type as what we're about to gather, - // let's just pick a new resource. - // TODO if we already carry the max we can -> returnresources - if (!ent.resourceCarrying() || !ent.resourceCarrying().length || - ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type")) - { - this.startGathering(gameState); - } - else if (!returnResources(gameState, ent)) // try to deposit resources - { - // no dropsite, abandon old resources and start gathering new ones - this.startGathering(gameState); - } - } - else if (unitAIStateOrder == "GATHER") - { - // we're already gathering. But let's check if there is nothing better - // in case UnitAI did something bad - if (ent.unitAIOrderData().length) - { - const supplyId = ent.unitAIOrderData()[0].target; - const supply = gameState.getEntityById(supplyId); - if (supply && !supply.hasClasses(["Field", "Animal"]) && - supplyId != ent.getMetadata(PlayerID, "supply")) + const orderData = ent.unitAIOrderData()[0]; + if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") { - const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supplyId); - if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) + // If we are here, an enemy structure must have targeted one of our workers + // and UnitAI sent it fight back with allowCapture=true + const target = gameState.getEntityById(orderData.target); + if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner())) + ent.attack(orderData.target, allowCapture(gameState, ent, target)); + } + } + return; + } + + // Okay so we have a few tasks. + // If we're gathering, we'll check that we haven't run idle. + // And we'll also check that we're gathering a resource we want to gather. + + if (subrole === Worker.SUBROLE_GATHERER) + { + if (ent.isIdle()) + { + // if we aren't storing resources or it's the same type as what we're about to gather, + // let's just pick a new resource. + // TODO if we already carry the max we can -> returnresources + if (!ent.resourceCarrying() || !ent.resourceCarrying().length || + ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type")) + { + this.startGathering(gameState); + } + else if (!returnResources(gameState, ent)) // try to deposit resources + { + // no dropsite, abandon old resources and start gathering new ones + this.startGathering(gameState); + } + } + else if (unitAIStateOrder == "GATHER") + { + // we're already gathering. But let's check if there is nothing better + // in case UnitAI did something bad + if (ent.unitAIOrderData().length) + { + const supplyId = ent.unitAIOrderData()[0].target; + const supply = gameState.getEntityById(supplyId); + if (supply && !supply.hasClasses(["Field", "Animal"]) && + supplyId != ent.getMetadata(PlayerID, "supply")) { - this.base.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - { - const gatherType = ent.getMetadata(PlayerID, "gather-type"); - const nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (nearby.some(sup => sup.id == supplyId)) - ent.setMetadata(PlayerID, "supply", supplyId); - else if (nearby.length) + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supplyId); + if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) { this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { - const medium = this.base.dropsiteSupplies[gatherType].medium; - if (medium.length && !medium.some(sup => sup.id == supplyId)) + const gatherType = ent.getMetadata(PlayerID, "gather-type"); + const nearby = this.base.dropsiteSupplies[gatherType].nearby; + if (nearby.some(sup => sup.id == supplyId)) + ent.setMetadata(PlayerID, "supply", supplyId); + else if (nearby.length) { this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else - ent.setMetadata(PlayerID, "supply", supplyId); + { + const medium = this.base.dropsiteSupplies[gatherType].medium; + if (medium.length && !medium.some(sup => sup.id == supplyId)) + { + this.base.RemoveTCGatherer(supplyId); + this.startGathering(gameState); + } + else + ent.setMetadata(PlayerID, "supply", supplyId); + } } } } - } - if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") - { - if (gameState.ai.playedTurn % 10 == 0) + if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { - // Check from time to time that UnitAI does not send us to an inaccessible dropsite - const dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && - this.entAccess != getLandAccess(gameState, dropsite)) + if (gameState.ai.playedTurn % 10 == 0) { - returnResources(gameState, this.ent); - } - } - - // If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full. - // Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be - // reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one). - const gatherType = ent.getMetadata(PlayerID, "gather-type"); - const influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup; - if (influenceGroup && influenceGroup == "sparse") - { - const supplyId = ent.getMetadata(PlayerID, "supply"); - if (supplyId) - { - const nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (!nearby.some(sup => sup.id == supplyId)) + // Check from time to time that UnitAI does not send us to an inaccessible dropsite + const dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); + if (dropsite && dropsite.position() && + this.entAccess != getLandAccess(gameState, dropsite)) { - if (nearby.length) - ent.setMetadata(PlayerID, "supply", undefined); - else + returnResources(gameState, this.ent); + } + } + + // If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full. + // Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be + // reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one). + const gatherType = ent.getMetadata(PlayerID, "gather-type"); + const influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup; + if (influenceGroup && influenceGroup == "sparse") + { + const supplyId = ent.getMetadata(PlayerID, "supply"); + if (supplyId) + { + const nearby = this.base.dropsiteSupplies[gatherType].nearby; + if (!nearby.some(sup => sup.id == supplyId)) { - const medium = this.base.dropsiteSupplies[gatherType].medium; - if (!medium.some(sup => sup.id == supplyId) && medium.length) + if (nearby.length) ent.setMetadata(PlayerID, "supply", undefined); + else + { + const medium = this.base.dropsiteSupplies[gatherType].medium; + if (!medium.some(sup => sup.id == supplyId) && medium.length) + ent.setMetadata(PlayerID, "supply", undefined); + } } } } } } } - } - else if (subrole === Worker.SUBROLE_BUILDER) - { - if (unitAIStateOrder == "REPAIR") + else if (subrole === Worker.SUBROLE_BUILDER) { - // Update our target in case UnitAI sent us to a different foundation because of autocontinue - // and abandon it if UnitAI has sent us to build a field (as we build them only when needed) - if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && - ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target) + if (unitAIStateOrder == "REPAIR") { - const targetId = ent.unitAIOrderData()[0].target; - const target = gameState.getEntityById(targetId); - if (target && !target.hasClass("Field")) + // Update our target in case UnitAI sent us to a different foundation because of autocontinue + // and abandon it if UnitAI has sent us to build a field (as we build them only when needed) + if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && + ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target) { - ent.setMetadata(PlayerID, "target-foundation", targetId); - return; + const targetId = ent.unitAIOrderData()[0].target; + const target = gameState.getEntityById(targetId); + if (target && !target.hasClass("Field")) + { + ent.setMetadata(PlayerID, "target-foundation", targetId); + return; + } + ent.setMetadata(PlayerID, "target-foundation", undefined); + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + ent.stopMoving(); + if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) + { + // reassign it to something useful + this.base.reassignIdleWorkers(gameState, [ent]); + this.update(gameState, ent); + return; + } } - ent.setMetadata(PlayerID, "target-foundation", undefined); - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + // Otherwise check that the target still exists (useful in REPAIR.APPROACHING) + const targetId = ent.getMetadata(PlayerID, "target-foundation"); + if (targetId && gameState.getEntityById(targetId)) + return; ent.stopMoving(); - if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) + } + // okay so apparently we aren't working. + // Unless we've been explicitely told to keep our role, make us idle. + const target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); + if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) + { + ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + ent.setMetadata(PlayerID, "target-foundation", undefined); + // If worker elephant, move away to avoid being trapped in between constructions + if (ent.hasClass("Elephant")) + this.moveToGatherer(gameState, ent, true); + else if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); @@ -344,408 +370,278 @@ Worker.prototype.update = function(gameState, ent) return; } } - // Otherwise check that the target still exists (useful in REPAIR.APPROACHING) - const targetId = ent.getMetadata(PlayerID, "target-foundation"); - if (targetId && gameState.getEntityById(targetId)) - return; - ent.stopMoving(); - } - // okay so apparently we aren't working. - // Unless we've been explicitely told to keep our role, make us idle. - const target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) - { - ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - ent.setMetadata(PlayerID, "target-foundation", undefined); - // If worker elephant, move away to avoid being trapped in between constructions - if (ent.hasClass("Elephant")) - this.moveToGatherer(gameState, ent, true); - else if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) + else { - // reassign it to something useful - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; + const goalAccess = getLandAccess(gameState, target); + const queued = returnResources(gameState, ent); + if (this.entAccess == goalAccess) + ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses + else + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, goalAccess, target.position()); } } - else + else if (subrole === Worker.SUBROLE_HUNTER) { - const goalAccess = getLandAccess(gameState, target); - const queued = returnResources(gameState, ent); - if (this.entAccess == goalAccess) - ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, goalAccess, target.position()); - } - } - else if (subrole === Worker.SUBROLE_HUNTER) - { - const lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); - if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20)) - { - if (!this.startHunting(gameState)) + const lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); + if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20)) { - // nothing to hunt around. Try another region if any - let nowhereToHunt = true; - for (const base of gameState.ai.HQ.baseManagers()) + if (!this.startHunting(gameState)) { - if (!base.anchor || !base.anchor.position()) - continue; - const basePos = base.anchor.position(); - if (this.startHunting(gameState, basePos)) + // nothing to hunt around. Try another region if any + let nowhereToHunt = true; + for (const base of gameState.ai.HQ.baseManagers()) { - ent.setMetadata(PlayerID, "base", base.ID); - if (base.accessIndex == this.entAccess) - ent.move(basePos[0], basePos[1]); - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, base.accessIndex, basePos); - nowhereToHunt = false; - break; + if (!base.anchor || !base.anchor.position()) + continue; + const basePos = base.anchor.position(); + if (this.startHunting(gameState, basePos)) + { + ent.setMetadata(PlayerID, "base", base.ID); + if (base.accessIndex == this.entAccess) + ent.move(basePos[0], basePos[1]); + else + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, base.accessIndex, basePos); + nowhereToHunt = false; + break; + } + } + if (nowhereToHunt) + ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime); + } + } + else // Perform some sanity checks + { + if (unitAIStateOrder == "GATHER") + { + // we may have drifted towards ennemy territory during the hunt, if yes go home + const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); + if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally + this.startHunting(gameState); + else if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") + { + // Check that UnitAI does not send us to an inaccessible dropsite + const dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); + if (dropsite && dropsite.position() && + this.entAccess != getLandAccess(gameState, dropsite)) + { + returnResources(gameState, ent); + } } } - if (nowhereToHunt) - ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime); } } - else // Perform some sanity checks + else if (subrole === Worker.SUBROLE_FISHER) { - if (unitAIStateOrder == "GATHER") + if (ent.isIdle()) + this.startFishing(gameState); + else // if we have drifted towards ennemy territory during the fishing, go home { - // we may have drifted towards ennemy territory during the hunt, if yes go home const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startHunting(gameState); - else if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") - { - // Check that UnitAI does not send us to an inaccessible dropsite - const dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && - this.entAccess != getLandAccess(gameState, dropsite)) - { - returnResources(gameState, ent); - } - } + this.startFishing(gameState); } } } - else if (subrole === Worker.SUBROLE_FISHER) + + retryWorking(gameState, subrole) { - if (ent.isIdle()) - this.startFishing(gameState); - else // if we have drifted towards ennemy territory during the fishing, go home + switch (subrole) { - const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startFishing(gameState); + case Worker.SUBROLE_GATHERER: + return this.startGathering(gameState); + case Worker.SUBROLE_HUNTER: + return this.startHunting(gameState); + case Worker.SUBROLE_FISHER: + return this.startFishing(gameState); + case Worker.SUBROLE_BUILDER: + return this.startBuilding(gameState); + default: + return false; } } -}; -Worker.prototype.retryWorking = function(gameState, subrole) -{ - switch (subrole) + startBuilding(gameState) { - case Worker.SUBROLE_GATHERER: - return this.startGathering(gameState); - case Worker.SUBROLE_HUNTER: - return this.startHunting(gameState); - case Worker.SUBROLE_FISHER: - return this.startFishing(gameState); - case Worker.SUBROLE_BUILDER: - return this.startBuilding(gameState); - default: - return false; - } -}; - -Worker.prototype.startBuilding = function(gameState) -{ - const target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) - return false; - if (getLandAccess(gameState, target) != this.entAccess) - return false; - this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses - return true; -}; - -Worker.prototype.startGathering = function(gameState) -{ - // First look for possible treasure if any - if (gatherTreasure(gameState, this.ent)) + const target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); + if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) + return false; + if (getLandAccess(gameState, target) != this.entAccess) + return false; + this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses return true; + } - const resource = this.ent.getMetadata(PlayerID, "gather-type"); - - // If we are gathering food, try to hunt first - if (resource == "food" && this.startHunting(gameState)) - return true; - - const findSupply = function(worker, supplies) + startGathering(gameState) { - const ent = worker.ent; - let ret = false; - const gatherRates = ent.resourceGatherRates(); - for (let i = 0; i < supplies.length; ++i) - { - const entity = gameState.getEntityById(supplies[i].id); - // exhausted resource, remove it from this list - if (!entity) - { - supplies.splice(i--, 1); - continue; - } - if (isSupplyFull(gameState, entity)) - continue; - const inaccessibleTime = entity.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - const supplyType = entity.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - // check if available resource is worth one additionnal gatherer (except for farms) - const nbGatherers = entity.resourceSupplyNumGatherers() + worker.base.GetTCGatherer(supplies[i].id); - if (entity.resourceSupplyType().specific != "grain" && nbGatherers > 0 && - entity.resourceSupplyAmount()/(1+nbGatherers) < 30) - { - continue; - } - // not in ennemy territory - const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(entity.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - continue; - worker.base.AddTCGatherer(supplies[i].id); - ent.setMetadata(PlayerID, "supply", supplies[i].id); - ret = entity; - break; - } - return ret; - }; - - const navalManager = gameState.ai.HQ.navalManager; - let supply; - - // first look in our own base if accessible from our present position - if (this.baseAccess == this.entAccess) - { - supply = findSupply(this, this.base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.gather(supply); + // First look for possible treasure if any + if (gatherTreasure(gameState, this.ent)) return true; - } - // --> for food, try to gather from fields if any, otherwise build one if any - if (resource == "food") + + const resource = this.ent.getMetadata(PlayerID, "gather-type"); + + // If we are gathering food, try to hunt first + if (resource == "food" && this.startHunting(gameState)) + return true; + + const findSupply = function(worker, supplies) { - supply = this.gatherNearestField(gameState, this.baseID); + const ent = worker.ent; + let ret = false; + const gatherRates = ent.resourceGatherRates(); + for (let i = 0; i < supplies.length; ++i) + { + const entity = gameState.getEntityById(supplies[i].id); + // exhausted resource, remove it from this list + if (!entity) + { + supplies.splice(i--, 1); + continue; + } + if (isSupplyFull(gameState, entity)) + continue; + const inaccessibleTime = entity.getMetadata(PlayerID, "inaccessibleTime"); + if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) + continue; + const supplyType = entity.get("ResourceSupply/Type"); + if (!gatherRates[supplyType]) + continue; + // check if available resource is worth one additionnal gatherer (except for farms) + const nbGatherers = entity.resourceSupplyNumGatherers() + worker.base.GetTCGatherer(supplies[i].id); + if (entity.resourceSupplyType().specific != "grain" && nbGatherers > 0 && + entity.resourceSupplyAmount()/(1+nbGatherers) < 30) + { + continue; + } + // not in ennemy territory + const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(entity.position()); + if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally + continue; + worker.base.AddTCGatherer(supplies[i].id); + ent.setMetadata(PlayerID, "supply", supplies[i].id); + ret = entity; + break; + } + return ret; + }; + + const navalManager = gameState.ai.HQ.navalManager; + let supply; + + // first look in our own base if accessible from our present position + if (this.baseAccess == this.entAccess) + { + supply = findSupply(this, this.base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.gather(supply); return true; } - supply = this.buildAnyField(gameState, this.baseID); + // --> for food, try to gather from fields if any, otherwise build one if any + if (resource == "food") + { + supply = this.gatherNearestField(gameState, this.baseID); + if (supply) + { + this.ent.gather(supply); + return true; + } + supply = this.buildAnyField(gameState, this.baseID); + if (supply) + { + this.ent.repair(supply); + return true; + } + } + supply = findSupply(this, this.base.dropsiteSupplies[resource].medium); if (supply) { - this.ent.repair(supply); + this.ent.gather(supply); return true; } } - supply = findSupply(this, this.base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - // So if we're here we have checked our whole base for a proper resource (or it was not accessible) - // --> check other bases directly accessible - for (const base of gameState.ai.HQ.baseManagers()) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this, base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any - { + // So if we're here we have checked our whole base for a proper resource (or it was not accessible) + // --> check other bases directly accessible for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; - supply = this.gatherNearestField(gameState, base.ID); + supply = findSupply(this, base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } - supply = this.buildAnyField(gameState, base.ID); + } + if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any + { + for (const base of gameState.ai.HQ.baseManagers()) + { + if (base.ID == this.baseID) + continue; + if (base.accessIndex != this.entAccess) + continue; + supply = this.gatherNearestField(gameState, base.ID); + if (supply) + { + this.ent.setMetadata(PlayerID, "base", base.ID); + this.ent.gather(supply); + return true; + } + supply = this.buildAnyField(gameState, base.ID); + if (supply) + { + this.ent.setMetadata(PlayerID, "base", base.ID); + this.ent.repair(supply); + return true; + } + } + } + for (const base of gameState.ai.HQ.baseManagers()) + { + if (base.ID == this.baseID) + continue; + if (base.accessIndex != this.entAccess) + continue; + supply = findSupply(this, base.dropsiteSupplies[resource].medium); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.repair(supply); + this.ent.gather(supply); return true; } } - } - for (const base of gameState.ai.HQ.baseManagers()) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this, base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - // Okay may-be we haven't found any appropriate dropsite anywhere. - // Try to help building one if any accessible foundation available - const foundations = gameState.getOwnFoundations().toEntityArray(); - let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) - { - if (!foundation || getLandAccess(gameState, foundation) != this.entAccess) - return false; - const structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) + // Okay may-be we haven't found any appropriate dropsite anywhere. + // Try to help building one if any accessible foundation available + const foundations = gameState.getOwnFoundations().toEntityArray(); + let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { - if (foundation.getMetadata(PlayerID, "base") != this.baseID) - this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); - this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); - this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); - this.ent.repair(foundation); - return true; - } - return false; - }, this); - if (shouldBuild) - return true; - - // Still nothing ... try bases which need a transport - for (const base of gameState.ai.HQ.baseManagers()) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this, base.dropsiteSupplies[resource].nearby); - if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) - { - if (base.ID != this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - return true; - } - } - if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any - { - for (const base of gameState.ai.HQ.baseManagers()) - { - if (base.accessIndex == this.entAccess) - continue; - supply = this.gatherNearestField(gameState, base.ID); - if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) - { - if (base.ID != this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - return true; - } - supply = this.buildAnyField(gameState, base.ID); - if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) - { - if (base.ID != this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - return true; - } - } - } - for (const base of gameState.ai.HQ.baseManagers()) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this, base.dropsiteSupplies[resource].medium); - if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) - { - if (base.ID != this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - return true; - } - } - // Okay so we haven't found any appropriate dropsite anywhere. - // Try to help building one if any non-accessible foundation available - shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) - { - if (!foundation || getLandAccess(gameState, foundation) == this.entAccess) - return false; - const structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) - { - const foundationAccess = getLandAccess(gameState, foundation); - if (navalManager.requireTransport(gameState, this.ent, this.entAccess, foundationAccess, foundation.position())) + if (!foundation || getLandAccess(gameState, foundation) != this.entAccess) + return false; + const structure = gameState.getBuiltTemplate(foundation.templateName()); + if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) { if (foundation.getMetadata(PlayerID, "base") != this.baseID) this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + this.ent.repair(foundation); return true; } - } - return false; - }, this); - if (shouldBuild) - return true; + return false; + }, this); + if (shouldBuild) + return true; - // Still nothing, we look now for faraway resources, first in the accessible ones, then in the others - // except for food when farms or corrals can be used - let allowDistant = true; - if (resource == "food") - { - if (gameState.ai.HQ.turnCache.allowDistantFood === undefined) - gameState.ai.HQ.turnCache.allowDistantFood = - !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field") && - !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"); - allowDistant = gameState.ai.HQ.turnCache.allowDistantFood; - } - if (allowDistant) - { - if (this.baseAccess == this.entAccess) - { - supply = findSupply(this, this.base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - for (const base of gameState.ai.HQ.baseManagers()) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this, base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } + // Still nothing ... try bases which need a transport for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; - supply = findSupply(this, base.dropsiteSupplies[resource].faraway); + supply = findSupply(this, base.dropsiteSupplies[resource].nearby); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) @@ -753,398 +649,505 @@ Worker.prototype.startGathering = function(gameState) return true; } } - } - - // If we are here, we have nothing left to gather ... certainly no more resources of this type - gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime; - if (gameState.ai.Config.debug > 2) - aiWarn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); - this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - return false; -}; - -/** - * if position is given, we only check if we could hunt from this position but do nothing - * otherwise the position of the entity is taken, and if something is found, we directly start the hunt - */ -Worker.prototype.startHunting = function(gameState, position) -{ - // First look for possible treasure if any - if (!position && gatherTreasure(gameState, this.ent)) - return true; - - const resources = gameState.getHuntableSupplies(); - if (!resources.hasEntities()) - return false; - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - const entIsFastMoving = isFastMoving(this.ent); - const isRanged = this.ent.hasClass("Ranged"); - const entPosition = position ? position : this.ent.position(); - const foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - - const hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare) - { - for (const dropsite of foodDropsites.values()) + if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any { - if (!dropsite.position()) - continue; - const owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (supplyAccess != getLandAccess(gameState, dropsite)) - continue; - if (SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) - return true; - } - return false; - }; - - const gatherRates = this.ent.resourceGatherRates(); - for (const supply of resources.values()) - { - if (!supply.position()) - continue; - - const inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - - const supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - if (isSupplyFull(gameState, supply)) - continue; - // Check if available resource is worth one additionnal gatherer (except for farms). - const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - continue; - - const canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; - // Only FastMoving and Ranged units should hunt fleeing animals. - if (canFlee && !entIsFastMoving && !isRanged) - continue; - - const supplyAccess = getLandAccess(gameState, supply); - if (supplyAccess != this.entAccess) - continue; - - // measure the distance to the resource. - const dist = SquareVectorDistance(entPosition, supply.position()); - if (dist > nearestSupplyDist) - continue; - - // Only FastMoving should hunt faraway. - if (!entIsFastMoving && dist > 25000) - continue; - - // Avoid enemy territory. - const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // Player is its own ally. - continue; - // And if in ally territory, don't hunt this ally's cattle. - if (territoryOwner != 0 && territoryOwner != PlayerID && supply.owner() == territoryOwner) - continue; - - // Only FastMoving should hunt far from dropsite (specially for non-Domestic animals which flee). - if (!entIsFastMoving && canFlee && territoryOwner == 0) - continue; - const distanceSquare = entIsFastMoving ? 35000 : (canFlee ? 7000 : 12000); - if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) - continue; - - nearestSupplyDist = dist; - nearestSupply = supply; - } - - if (nearestSupply) - { - if (position) - return true; - this.base.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - return false; -}; - -Worker.prototype.startFishing = function(gameState) -{ - if (!this.ent.position()) - return false; - - const resources = gameState.getFishableSupplies(); - if (!resources.hasEntities()) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState); - this.ent.destroy(); - return false; - } - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - const fisherSea = getSeaAccess(gameState, this.ent); - const fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : - gameState.getOwnDropsites("food")).filter(filters.byClass("Dock")).toEntityArray(); - - const nearestDropsiteDist = function(supply) - { - let distMin = 1000000; - const pos = supply.position(); - for (const dropsite of fishDropsites) - { - if (!dropsite.position()) - continue; - const owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (fisherSea != getSeaAccess(gameState, dropsite)) - continue; - distMin = Math.min(distMin, SquareVectorDistance(pos, dropsite.position())); - } - return distMin; - }; - - let exhausted = true; - const gatherRates = this.ent.resourceGatherRates(); - resources.forEach((supply) => - { - if (!supply.position()) - return; - - // check that it is accessible - if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea) - return; - - exhausted = false; - - const supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - return; - - if (isSupplyFull(gameState, supply)) - return; - // check if available resource is worth one additionnal gatherer (except for farms) - const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - return; - - // Avoid ennemy territory - if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply)) - return; - - // measure the distance from the resource to the nearest dropsite - const dist = nearestDropsiteDist(supply); - if (dist > nearestSupplyDist) - return; - - nearestSupplyDist = dist; - nearestSupply = supply; - }); - - if (exhausted) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea); - this.ent.destroy(); - return false; - } - - if (nearestSupply) - { - this.base.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - if (this.ent.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_FISHER) - this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); - return false; -}; - -Worker.prototype.gatherNearestField = function(gameState, baseID) -{ - const ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(filters.isBuilt()) - .filter(filters.byMetadata(PlayerID, "base", baseID)); - let bestFarm; - - const gatherRates = this.ent.resourceGatherRates(); - for (const field of ownFields.values()) - { - if (isSupplyFull(gameState, field)) - continue; - const supplyType = field.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - let rate = 1; - const diminishing = field.getDiminishingReturns(); - if (diminishing < 1) - { - const num = field.resourceSupplyNumGatherers() + this.base.GetTCGatherer(field.id()); - if (num > 0) - rate = Math.pow(diminishing, num); - } - // Add a penalty distance depending on rate - const dist = SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000; - if (!bestFarm || dist < bestFarm.dist) - bestFarm = { "ent": field, "dist": dist, "rate": rate }; - } - // If other field foundations available, better build them when rate becomes too small - if (!bestFarm || bestFarm.rate < 0.70 && gameState.getOwnFoundations() - .filter(filters.byClass("Field")).filter(filters.byMetadata(PlayerID, "base", baseID)) - .hasEntities()) - { - return false; - } - this.base.AddTCGatherer(bestFarm.ent.id()); - this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); - return bestFarm.ent; -}; - -/** - * WARNING with the present options of AI orders, the unit will not gather after building the farm. - * This is done by calling the gatherNearestField function when construction is completed. - */ -Worker.prototype.buildAnyField = function(gameState, baseID) -{ - if (!this.ent.isBuilder()) - return false; - let bestFarmEnt = false; - let bestFarmDist = 10000000; - const pos = this.ent.position(); - for (const found of gameState.getOwnFoundations().values()) - { - if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field")) - continue; - const current = found.getBuildersNb(); - if (current === undefined || - current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers()) - continue; - const dist = SquareVectorDistance(found.position(), pos); - if (dist > bestFarmDist) - continue; - bestFarmEnt = found; - bestFarmDist = dist; - } - return bestFarmEnt; -}; - -/** - * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions. - * For the time being, we move towards the nearest gatherer (providing him a dropsite). - * BaseManager does also use that function to deal with its mobile dropsites. - */ -Worker.prototype.moveToGatherer = function(gameState, ent, forced) -{ - const pos = ent.position(); - if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5)) - return; - const gatherers = this.base.workersBySubrole(gameState, Worker.SUBROLE_GATHERER); - let dist = Math.min(); - let destination; - const access = getLandAccess(gameState, ent); - const types = ent.resourceDropsiteTypes(); - for (const gatherer of gatherers.values()) - { - const gathererType = gatherer.getMetadata(PlayerID, "gather-type"); - if (!gathererType || types.indexOf(gathererType) == -1) - continue; - if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined || - getLandAccess(gameState, gatherer) != access || gatherer.isIdle()) - { - continue; - } - const distance = SquareVectorDistance(pos, gatherer.position()); - if (distance > dist) - continue; - dist = distance; - destination = gatherer.position(); - } - ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5)); - if (destination && dist > 10) - ent.move(destination[0], destination[1]); -}; - -/** - * Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes - * inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case - * it will be cleared later). - */ -Worker.prototype.isInaccessibleSupply = function(gameState) -{ - if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) - return false; - const targetId = this.ent.unitAIOrderData()[0].target; - const target = gameState.getEntityById(targetId); - if (!target) - return true; - - if (!target.resourceSupplyType()) - return false; - - const approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); - const carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; - if (!approachingTarget || approachingTarget != targetId) - { - this.ent.setMetadata(PlayerID, "approachingTarget", targetId); - this.ent.setMetadata(PlayerID, "approachingTime", undefined); - this.ent.setMetadata(PlayerID, "approachingPos", undefined); - this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount); - const alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried"); - if (alreadyTried && alreadyTried != targetId) - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - } - - const carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore"); - if (carriedBefore != carriedAmount) - { - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - if (target.getMetadata(PlayerID, "inaccessibleTime")) - target.setMetadata(PlayerID, "inaccessibleTime", 0); - return false; - } - - const inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - return true; - - const approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); - if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3) - { - const presentPos = this.ent.position(); - const approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); - if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) - { - this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); - this.ent.setMetadata(PlayerID, "approachingPos", presentPos); - return false; - } - if (gameState.ai.elapsedTime - approachingTime > 10) - { - if (this.ent.getMetadata(PlayerID, "alreadyTried")) + for (const base of gameState.ai.HQ.baseManagers()) { - target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); + if (base.accessIndex == this.entAccess) + continue; + supply = this.gatherNearestField(gameState, base.ID); + if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) + { + if (base.ID != this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } + supply = this.buildAnyField(gameState, base.ID); + if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) + { + if (base.ID != this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } + } + } + for (const base of gameState.ai.HQ.baseManagers()) + { + if (base.accessIndex == this.entAccess) + continue; + supply = findSupply(this, base.dropsiteSupplies[resource].medium); + if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) + { + if (base.ID != this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); return true; } - // let's try again to reach it - this.ent.setMetadata(PlayerID, "alreadyTried", targetId); - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.gather(target); + } + // Okay so we haven't found any appropriate dropsite anywhere. + // Try to help building one if any non-accessible foundation available + shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) + { + if (!foundation || getLandAccess(gameState, foundation) == this.entAccess) + return false; + const structure = gameState.getBuiltTemplate(foundation.templateName()); + if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) + { + const foundationAccess = getLandAccess(gameState, foundation); + if (navalManager.requireTransport(gameState, this.ent, this.entAccess, foundationAccess, foundation.position())) + { + if (foundation.getMetadata(PlayerID, "base") != this.baseID) + this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); + this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); + this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_BUILDER); + return true; + } + } + return false; + }, this); + if (shouldBuild) + return true; + + // Still nothing, we look now for faraway resources, first in the accessible ones, then in the others + // except for food when farms or corrals can be used + let allowDistant = true; + if (resource == "food") + { + if (gameState.ai.HQ.turnCache.allowDistantFood === undefined) + gameState.ai.HQ.turnCache.allowDistantFood = + !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field") && + !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"); + allowDistant = gameState.ai.HQ.turnCache.allowDistantFood; + } + if (allowDistant) + { + if (this.baseAccess == this.entAccess) + { + supply = findSupply(this, this.base.dropsiteSupplies[resource].faraway); + if (supply) + { + this.ent.gather(supply); + return true; + } + } + for (const base of gameState.ai.HQ.baseManagers()) + { + if (base.ID == this.baseID) + continue; + if (base.accessIndex != this.entAccess) + continue; + supply = findSupply(this, base.dropsiteSupplies[resource].faraway); + if (supply) + { + this.ent.setMetadata(PlayerID, "base", base.ID); + this.ent.gather(supply); + return true; + } + } + for (const base of gameState.ai.HQ.baseManagers()) + { + if (base.accessIndex == this.entAccess) + continue; + supply = findSupply(this, base.dropsiteSupplies[resource].faraway); + if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) + { + if (base.ID != this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } + } + } + + // If we are here, we have nothing left to gather ... certainly no more resources of this type + gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime; + if (gameState.ai.Config.debug > 2) + aiWarn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); + this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + return false; + } + + /** + * if position is given, we only check if we could hunt from this position but do nothing + * otherwise the position of the entity is taken, and if something is found, we directly start the hunt + */ + startHunting(gameState, position) + { + // First look for possible treasure if any + if (!position && gatherTreasure(gameState, this.ent)) + return true; + + const resources = gameState.getHuntableSupplies(); + if (!resources.hasEntities()) + return false; + + let nearestSupplyDist = Math.min(); + let nearestSupply; + + const entIsFastMoving = isFastMoving(this.ent); + const isRanged = this.ent.hasClass("Ranged"); + const entPosition = position ? position : this.ent.position(); + const foodDropsites = gameState.playerData.hasSharedDropsites ? + gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); + + const hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare) + { + for (const dropsite of foodDropsites.values()) + { + if (!dropsite.position()) + continue; + const owner = dropsite.owner(); + // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again + if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) + continue; + if (supplyAccess != getLandAccess(gameState, dropsite)) + continue; + if (SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) + return true; + } + return false; + }; + + const gatherRates = this.ent.resourceGatherRates(); + for (const supply of resources.values()) + { + if (!supply.position()) + continue; + + const inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); + if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) + continue; + + const supplyType = supply.get("ResourceSupply/Type"); + if (!gatherRates[supplyType]) + continue; + + if (isSupplyFull(gameState, supply)) + continue; + // Check if available resource is worth one additionnal gatherer (except for farms). + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); + if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) + continue; + + const canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; + // Only FastMoving and Ranged units should hunt fleeing animals. + if (canFlee && !entIsFastMoving && !isRanged) + continue; + + const supplyAccess = getLandAccess(gameState, supply); + if (supplyAccess != this.entAccess) + continue; + + // measure the distance to the resource. + const dist = SquareVectorDistance(entPosition, supply.position()); + if (dist > nearestSupplyDist) + continue; + + // Only FastMoving should hunt faraway. + if (!entIsFastMoving && dist > 25000) + continue; + + // Avoid enemy territory. + const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); + if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // Player is its own ally. + continue; + // And if in ally territory, don't hunt this ally's cattle. + if (territoryOwner != 0 && territoryOwner != PlayerID && supply.owner() == territoryOwner) + continue; + + // Only FastMoving should hunt far from dropsite (specially for non-Domestic animals which flee). + if (!entIsFastMoving && canFlee && territoryOwner == 0) + continue; + const distanceSquare = entIsFastMoving ? 35000 : (canFlee ? 7000 : 12000); + if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) + continue; + + nearestSupplyDist = dist; + nearestSupply = supply; + } + + if (nearestSupply) + { + if (position) + return true; + this.base.AddTCGatherer(nearestSupply.id()); + this.ent.gather(nearestSupply); + this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); + this.ent.setMetadata(PlayerID, "target-foundation", undefined); + return true; + } + return false; + } + + startFishing(gameState) + { + if (!this.ent.position()) + return false; + + const resources = gameState.getFishableSupplies(); + if (!resources.hasEntities()) + { + gameState.ai.HQ.navalManager.resetFishingBoats(gameState); + this.ent.destroy(); return false; } + + let nearestSupplyDist = Math.min(); + let nearestSupply; + + const fisherSea = getSeaAccess(gameState, this.ent); + const fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : + gameState.getOwnDropsites("food")).filter(filters.byClass("Dock")).toEntityArray(); + + const nearestDropsiteDist = function(supply) + { + let distMin = 1000000; + const pos = supply.position(); + for (const dropsite of fishDropsites) + { + if (!dropsite.position()) + continue; + const owner = dropsite.owner(); + // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again + if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) + continue; + if (fisherSea != getSeaAccess(gameState, dropsite)) + continue; + distMin = Math.min(distMin, SquareVectorDistance(pos, dropsite.position())); + } + return distMin; + }; + + let exhausted = true; + const gatherRates = this.ent.resourceGatherRates(); + resources.forEach((supply) => + { + if (!supply.position()) + return; + + // check that it is accessible + if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea) + return; + + exhausted = false; + + const supplyType = supply.get("ResourceSupply/Type"); + if (!gatherRates[supplyType]) + return; + + if (isSupplyFull(gameState, supply)) + return; + // check if available resource is worth one additionnal gatherer (except for farms) + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); + if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) + return; + + // Avoid ennemy territory + if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply)) + return; + + // measure the distance from the resource to the nearest dropsite + const dist = nearestDropsiteDist(supply); + if (dist > nearestSupplyDist) + return; + + nearestSupplyDist = dist; + nearestSupply = supply; + }); + + if (exhausted) + { + gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea); + this.ent.destroy(); + return false; + } + + if (nearestSupply) + { + this.base.AddTCGatherer(nearestSupply.id()); + this.ent.gather(nearestSupply); + this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); + this.ent.setMetadata(PlayerID, "target-foundation", undefined); + return true; + } + if (this.ent.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_FISHER) + this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE); + return false; } - return false; -}; + + gatherNearestField(gameState, baseID) + { + const ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(filters.isBuilt()) + .filter(filters.byMetadata(PlayerID, "base", baseID)); + let bestFarm; + + const gatherRates = this.ent.resourceGatherRates(); + for (const field of ownFields.values()) + { + if (isSupplyFull(gameState, field)) + continue; + const supplyType = field.get("ResourceSupply/Type"); + if (!gatherRates[supplyType]) + continue; + + let rate = 1; + const diminishing = field.getDiminishingReturns(); + if (diminishing < 1) + { + const num = field.resourceSupplyNumGatherers() + this.base.GetTCGatherer(field.id()); + if (num > 0) + rate = Math.pow(diminishing, num); + } + // Add a penalty distance depending on rate + const dist = SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000; + if (!bestFarm || dist < bestFarm.dist) + bestFarm = { "ent": field, "dist": dist, "rate": rate }; + } + // If other field foundations available, better build them when rate becomes too small + if (!bestFarm || bestFarm.rate < 0.70 && gameState.getOwnFoundations() + .filter(filters.byClass("Field")).filter(filters.byMetadata(PlayerID, "base", baseID)) + .hasEntities()) + { + return false; + } + this.base.AddTCGatherer(bestFarm.ent.id()); + this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); + return bestFarm.ent; + } + + /** + * WARNING with the present options of AI orders, the unit will not gather after building the farm. + * This is done by calling the gatherNearestField function when construction is completed. + */ + buildAnyField(gameState, baseID) + { + if (!this.ent.isBuilder()) + return false; + let bestFarmEnt = false; + let bestFarmDist = 10000000; + const pos = this.ent.position(); + for (const found of gameState.getOwnFoundations().values()) + { + if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field")) + continue; + const current = found.getBuildersNb(); + if (current === undefined || + current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers()) + continue; + const dist = SquareVectorDistance(found.position(), pos); + if (dist > bestFarmDist) + continue; + bestFarmEnt = found; + bestFarmDist = dist; + } + return bestFarmEnt; + } + + /** + * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions. + * For the time being, we move towards the nearest gatherer (providing him a dropsite). + * BaseManager does also use that function to deal with its mobile dropsites. + */ + moveToGatherer(gameState, ent, forced) + { + const pos = ent.position(); + if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined) + return; + if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5)) + return; + const gatherers = this.base.workersBySubrole(gameState, Worker.SUBROLE_GATHERER); + let dist = Math.min(); + let destination; + const access = getLandAccess(gameState, ent); + const types = ent.resourceDropsiteTypes(); + for (const gatherer of gatherers.values()) + { + const gathererType = gatherer.getMetadata(PlayerID, "gather-type"); + if (!gathererType || types.indexOf(gathererType) == -1) + continue; + if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined || + getLandAccess(gameState, gatherer) != access || gatherer.isIdle()) + { + continue; + } + const distance = SquareVectorDistance(pos, gatherer.position()); + if (distance > dist) + continue; + dist = distance; + destination = gatherer.position(); + } + ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5)); + if (destination && dist > 10) + ent.move(destination[0], destination[1]); + } + + /** + * Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes + * inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case + * it will be cleared later). + */ + isInaccessibleSupply(gameState) + { + if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) + return false; + const targetId = this.ent.unitAIOrderData()[0].target; + const target = gameState.getEntityById(targetId); + if (!target) + return true; + + if (!target.resourceSupplyType()) + return false; + + const approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); + const carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; + if (!approachingTarget || approachingTarget != targetId) + { + this.ent.setMetadata(PlayerID, "approachingTarget", targetId); + this.ent.setMetadata(PlayerID, "approachingTime", undefined); + this.ent.setMetadata(PlayerID, "approachingPos", undefined); + this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount); + const alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried"); + if (alreadyTried && alreadyTried != targetId) + this.ent.setMetadata(PlayerID, "alreadyTried", undefined); + } + + const carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore"); + if (carriedBefore != carriedAmount) + { + this.ent.setMetadata(PlayerID, "approachingTarget", undefined); + this.ent.setMetadata(PlayerID, "alreadyTried", undefined); + if (target.getMetadata(PlayerID, "inaccessibleTime")) + target.setMetadata(PlayerID, "inaccessibleTime", 0); + return false; + } + + const inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime"); + if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) + return true; + + const approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); + if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3) + { + const presentPos = this.ent.position(); + const approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); + if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) + { + this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); + this.ent.setMetadata(PlayerID, "approachingPos", presentPos); + return false; + } + if (gameState.ai.elapsedTime - approachingTime > 10) + { + if (this.ent.getMetadata(PlayerID, "alreadyTried")) + { + target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); + return true; + } + // let's try again to reach it + this.ent.setMetadata(PlayerID, "alreadyTried", targetId); + this.ent.setMetadata(PlayerID, "approachingTarget", undefined); + this.ent.gather(target); + return false; + } + } + return false; + } +}